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
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
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