...

Source file src/codeberg.org/tslocum/sriracha/internal/server/template.go

Documentation: codeberg.org/tslocum/sriracha/internal/server

     1  package server
     2  
     3  import (
     4  	"embed"
     5  	"fmt"
     6  	"html/template"
     7  	"io"
     8  	"log"
     9  	"math/rand"
    10  	"net/http"
    11  	"net/url"
    12  	"path/filepath"
    13  	"slices"
    14  	"strings"
    15  
    16  	. "codeberg.org/tslocum/sriracha/model"
    17  	"github.com/leonelquinteros/gotext"
    18  )
    19  
    20  //go:embed template
    21  var templateFS embed.FS
    22  
    23  type manageData struct {
    24  	Account  *Account
    25  	Accounts []*Account
    26  	Ban      *Ban
    27  	Bans     []*Ban
    28  	Banner   *Banner
    29  	Banners  []*Banner
    30  	Board    *Board
    31  	Boards   []*Board
    32  	Keyword  *Keyword
    33  	Keywords []*Keyword
    34  	Log      *Log
    35  	Logs     []*Log
    36  	News     *News
    37  	AllNews  []*News
    38  	Page     *Page
    39  	Pages    []*Page
    40  	Plugin   *pluginInfo
    41  	Plugins  []*pluginInfo
    42  	Report   *Report
    43  	Reports  []*Report
    44  }
    45  
    46  type templateData struct {
    47  	Account       *Account
    48  	Info          string
    49  	Message       template.HTML
    50  	Message2      template.HTML
    51  	Message3      template.HTML
    52  	Board         *Board
    53  	Boards        []*Board
    54  	News          *News
    55  	AllNews       []*News
    56  	Subscriptions []*Subscription
    57  	Page          int
    58  	Pages         int
    59  	Post          *Post
    60  	Threads       [][]*Post
    61  	ReplyMode     int
    62  	ModMode       bool
    63  	Extra         string
    64  	Extra2        string
    65  	Extra3        string
    66  	Opt           *ServerOptions
    67  	Manage        *manageData
    68  	Template      string
    69  
    70  	// Calculated fields.
    71  	IndexBoards []*Board
    72  	tpl         *template.Template
    73  }
    74  
    75  func (data *templateData) Style() string {
    76  	switch {
    77  	case data.Account != nil:
    78  		return data.Account.Style
    79  	case data.Board != nil:
    80  		return data.Board.Style
    81  	default:
    82  		return ""
    83  	}
    84  }
    85  
    86  func (data *templateData) BoardError(w http.ResponseWriter, message string) {
    87  	data.Template = "board_error"
    88  	data.Info = message
    89  	data.execute(w)
    90  }
    91  
    92  func (data *templateData) ManageError(message string) {
    93  	data.Template = "manage_error"
    94  	data.Info = message
    95  }
    96  
    97  func (data *templateData) forbidden(w http.ResponseWriter, required AccountRole) bool {
    98  	allow := required != 0 && data.Account != nil && data.Account.Role != 0 && data.Account.Role <= required
    99  	if allow {
   100  		return false
   101  	}
   102  	data.Template = "manage_error"
   103  	data.Info = "Access forbidden."
   104  	return true
   105  }
   106  
   107  func (data *templateData) executeWithError(w io.Writer) error {
   108  	if data.Template == "" {
   109  		return nil
   110  	}
   111  
   112  	if data.Account != nil {
   113  		data.IndexBoards = data.Boards
   114  	} else {
   115  		data.IndexBoards = data.IndexBoards[:0]
   116  		for _, b := range data.Boards {
   117  			if b.Hide == HideIndex || b.Hide == HideEverywhere {
   118  				continue
   119  			}
   120  			data.IndexBoards = append(data.IndexBoards, b)
   121  		}
   122  	}
   123  
   124  	var boardTemplate bool
   125  	if strings.HasPrefix(data.Template, "board_") {
   126  		prefix := "imgboard_"
   127  		if data.Board != nil && data.Board.Type == TypeForum {
   128  			prefix = "forum_"
   129  		}
   130  		data.Template = prefix + strings.TrimPrefix(data.Template, "board_")
   131  		boardTemplate = true
   132  	}
   133  
   134  	responseWriter, ok := w.(http.ResponseWriter)
   135  	if ok {
   136  		responseWriter.Header().Set("Content-Type", "text/html")
   137  	}
   138  
   139  	var funcMap template.FuncMap
   140  	if strings.HasPrefix(data.Template, "manage_") && data.Account != nil && data.Account.Locale != "" {
   141  		funcMap = templateFuncMaps[data.Account.Locale]
   142  	} else if boardTemplate {
   143  		var locale string
   144  		if data.Account != nil {
   145  			locale = data.Account.Locale
   146  		} else if data.Board != nil {
   147  			locale = data.Board.Locale
   148  		}
   149  		funcMap = templateFuncMaps[locale]
   150  	}
   151  	if funcMap == nil {
   152  		funcMap = templateFuncMaps[""]
   153  	}
   154  
   155  	tplName := data.Template + ".gohtml"
   156  	if data.Template == "line" {
   157  		tplName = data.Template
   158  	}
   159  	return data.tpl.Funcs(funcMap).ExecuteTemplate(w, tplName, data)
   160  }
   161  
   162  func (data *templateData) execute(w io.Writer) {
   163  	err := data.executeWithError(w)
   164  	if err != nil {
   165  		log.Fatal(err)
   166  	}
   167  }
   168  
   169  var expandableMedia = []string{".bmp", ".gif", ".jpg", ".png", ".svg", ".tif"}
   170  
   171  var templateFuncMap = template.FuncMap{
   172  	"Banner": func(banners []*Banner) *Banner {
   173  		l := len(banners)
   174  		switch l {
   175  		case 0:
   176  			return nil
   177  		case 1:
   178  			return banners[0]
   179  		default:
   180  			return banners[rand.Intn(l)]
   181  		}
   182  	},
   183  	"Contains": strings.Contains,
   184  	"Format": func(text string) template.HTML {
   185  		return template.HTML(strings.ReplaceAll(text, "\n", "<br>\n"))
   186  	},
   187  	"GetBoard": func(boardID int, boards []*Board) *Board {
   188  		for _, board := range boards {
   189  			if board.ID == boardID {
   190  				return board
   191  			}
   192  		}
   193  		return nil
   194  	},
   195  	"HasExpandableMedia": func(thread []*Post) bool {
   196  		for _, p := range thread {
   197  			if p.File != "" && !p.IsEmbed() && slices.Contains(expandableMedia, filepath.Ext(p.File)) {
   198  				return true
   199  			}
   200  		}
   201  		return false
   202  	},
   203  	"HasPrefix": strings.HasPrefix,
   204  	"HasSuffix": strings.HasSuffix,
   205  	"HTML": func(text string) template.HTML {
   206  		return template.HTML(text)
   207  	},
   208  	"Iterate": func(i int) []int {
   209  		var values []int
   210  		for v := 0; v <= i; v++ {
   211  			values = append(values, v)
   212  		}
   213  		return values
   214  	},
   215  	"May": func(action string, account *Account, access map[string]string) bool {
   216  		var required AccountRole
   217  		switch access[action] {
   218  		case "mod":
   219  			required = RoleMod
   220  		case "admin":
   221  			required = RoleAdmin
   222  		case "super-admin":
   223  			required = RoleSuperAdmin
   224  		default:
   225  			return false
   226  		}
   227  		return account != nil && account.Role <= required
   228  	},
   229  	"MinusOne": func(i int) int {
   230  		return i - 1
   231  	},
   232  	"Omitted": func(showReplies int, numReplies int) int {
   233  		if showReplies == 0 {
   234  			return numReplies
   235  		} else if numReplies <= showReplies {
   236  			return 0
   237  		}
   238  		return numReplies - showReplies
   239  	},
   240  	"PlusOne": func(i int) int {
   241  		return i + 1
   242  	},
   243  	"ShowReply": func(showReplies int, threadPosts int, postIndex int) bool {
   244  		if showReplies == 0 {
   245  			return true
   246  		}
   247  		return postIndex >= threadPosts-showReplies
   248  	},
   249  	"Slice": func(elements ...any) []any {
   250  		return elements
   251  	},
   252  	"ToUpper": strings.ToUpper,
   253  	"ToLower": strings.ToLower,
   254  	"Title":   strings.Title,
   255  	"UnderscoreTitle": func(text string) string {
   256  		return strings.Title(strings.ReplaceAll(text, "_", " "))
   257  	},
   258  	"URLEscape": func(text string) string {
   259  		return url.PathEscape(text)
   260  	},
   261  	"ZeroPadTo3": func(i int) string {
   262  		return fmt.Sprintf("%03d", i)
   263  	},
   264  }
   265  
   266  var templateFuncMaps map[string]template.FuncMap
   267  
   268  func newTemplateFuncMap(locale string) template.FuncMap {
   269  	f := make(template.FuncMap)
   270  	for name, v := range templateFuncMap {
   271  		f[name] = v
   272  	}
   273  
   274  	domain := "sriracha"
   275  	if locale != "" {
   276  		domain += "-" + locale
   277  	}
   278  	f["T"] = func(message string, vars ...interface{}) string {
   279  		return gotext.GetD(domain, message, vars...)
   280  	}
   281  	f["TN"] = func(singular string, plural string, n int, vars ...interface{}) string {
   282  		return gotext.GetND(domain, singular, plural, n, vars...)
   283  	}
   284  	return f
   285  }
   286  
   287  func (s *Server) newTemplateData() *templateData {
   288  	return &templateData{
   289  		Manage: &manageData{
   290  			Plugins: allPluginInfo,
   291  		},
   292  		Opt: &s.opt,
   293  		tpl: s.tpl,
   294  	}
   295  }
   296  

View as plain text