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