1 package server
2
3 import (
4 "fmt"
5 "html"
6 "html/template"
7 "io/fs"
8 "log"
9 "net/http"
10 "os"
11 "path/filepath"
12 "regexp"
13 "slices"
14 "strconv"
15 "strings"
16
17 "codeberg.org/tslocum/sriracha/internal/database"
18 . "codeberg.org/tslocum/sriracha/model"
19 . "codeberg.org/tslocum/sriracha/util"
20 )
21
22 func (s *Server) loadBoardForm(db *database.DB, r *http.Request, b *Board) {
23 b.Dir = FormString(r, "dir")
24 b.Name = FormString(r, "name")
25 b.Description = FormString(r, "description")
26 b.Type = FormRange(r, "type", TypeImageboard, TypeForum)
27 b.Hide = FormRange(r, "hide", HideNowhere, HideEverywhere)
28 b.Lock = FormRange(r, "lock", LockNone, LockStaff)
29 b.Approval = FormRange(r, "approval", ApprovalNone, ApprovalAll)
30 b.Reports = FormBool(r, "reports")
31 b.Style = FormString(r, "style")
32 b.Locale = FormString(r, "locale")
33 b.Delay = FormInt(r, "delay")
34 b.MinName = FormInt(r, "minname")
35 b.MaxName = FormInt(r, "maxname")
36 b.MinEmail = FormInt(r, "minemail")
37 b.MaxEmail = FormInt(r, "maxemail")
38 b.MinSubject = FormInt(r, "minsubject")
39 b.MaxSubject = FormInt(r, "maxsubject")
40 b.MinMessage = FormInt(r, "minmessage")
41 b.MaxMessage = FormInt(r, "maxmessage")
42 b.MinSizeThread = FormInt64(r, "minsizethread")
43 b.MaxSizeThread = FormInt64(r, "maxsizethread")
44 b.MinSizeReply = FormInt64(r, "minsizereply")
45 b.MaxSizeReply = FormInt64(r, "maxsizereply")
46 b.ThumbWidth = FormInt(r, "thumbwidth")
47 b.ThumbHeight = FormInt(r, "thumbheight")
48 b.DefaultName = FormString(r, "defaultname")
49 b.WordBreak = FormInt(r, "wordbreak")
50 b.Truncate = FormInt(r, "truncate")
51 b.Threads = FormInt(r, "threads")
52 b.Replies = FormInt(r, "replies")
53 b.MaxThreads = FormInt(r, "maxthreads")
54 b.MaxReplies = FormInt(r, "maxreplies")
55 b.Oekaki = FormBool(r, "oekaki")
56 b.Rules = FormMultiString(r, "rules")
57 b.Backlinks = FormBool(r, "backlinks")
58 b.Instances = FormNegInt(r, "instances")
59 b.Identifiers = FormRange(r, "identifiers", IdentifiersDisable, IdentifiersGlobal)
60 b.Files = FormInt(r, "files")
61 b.Gallery = FormBool(r, "gallery")
62
63 if b.Locale != "" && !slices.Contains(s.opt.LocalesSorted, b.Locale) {
64 b.Locale = ""
65 }
66
67 if b.Files < 0 {
68 b.Files = 0
69 }
70
71 b.Uploads = nil
72 uploads := r.Form["uploads"]
73 availableUploads := s.config.UploadTypes()
74 for _, upload := range uploads {
75 var found bool
76 for _, u := range availableUploads {
77 if u.MIME == upload {
78 found = true
79 break
80 }
81 }
82 if found {
83 b.Uploads = append(b.Uploads, upload)
84 }
85 }
86
87 b.Embeds = nil
88 embeds := r.Form["embeds"]
89 for _, embed := range embeds {
90 var found bool
91 for _, info := range s.opt.Embeds {
92 if info[0] == embed {
93 found = true
94 break
95 }
96 }
97 if found {
98 b.Embeds = append(b.Embeds, embed)
99 }
100 }
101 }
102
103 func (s *Server) serveBoard(data *templateData, db *database.DB, w http.ResponseWriter, r *http.Request) (skipExecute bool) {
104 data.Template = "manage_board"
105
106 boardID := PathInt(r, "/sriracha/board/rebuild/")
107 if boardID > 0 {
108 if data.forbidden(w, RoleAdmin) {
109 return false
110 }
111 b := db.BoardByID(boardID)
112 if b == nil {
113 data.ManageError("Board not found")
114 return false
115 }
116 s.rebuildBoard(db, b)
117 data.Info = fmt.Sprintf("Rebuilt %s", b.Path())
118 }
119
120 modBoard := PathString(r, "/sriracha/board/mod/")
121 if modBoard != "" {
122 var postID int
123 var page int
124 split := strings.Split(modBoard, "/")
125 if len(split) == 2 {
126 boardID, _ = strconv.Atoi(split[0])
127 if strings.HasPrefix(split[1], "p") {
128 page = ParseInt(split[1][1:])
129 } else {
130 postID = ParseInt(split[1])
131 }
132 } else if len(split) == 1 {
133 boardID, _ = strconv.Atoi(split[0])
134 }
135
136 b := db.BoardByID(boardID)
137 if b == nil {
138 data.ManageError("Invalid or deleted board or post")
139 return false
140 }
141
142 data.Template = "board_page"
143 data.Board = b
144 data.Boards = db.AllBoards()
145 data.ModMode = true
146 if postID > 0 {
147 data.Threads = [][]*Post{db.AllPostsInThread(postID, true)}
148 data.ReplyMode = postID
149 } else {
150 allThreads := db.AllThreads(b, true)
151
152 data.Page = page
153 data.Pages = pageCount(len(allThreads), b.Threads)
154
155 start := page * b.Threads
156 end := len(allThreads)
157 if b.Threads != 0 && end > start+b.Threads {
158 end = start + b.Threads
159 }
160 for _, threadInfo := range allThreads[start:end] {
161 thread := db.PostByID(threadInfo[0])
162 thread.Replies = threadInfo[1]
163 posts := []*Post{thread}
164 if b.Type == TypeImageboard {
165 posts = append(posts, db.AllReplies(threadInfo[0], b.Replies, true)...)
166 }
167 data.Threads = append(data.Threads, posts)
168 }
169 }
170 return false
171 }
172
173 resetBoardID := PathInt(r, "/sriracha/board/reset/")
174 if resetBoardID > 0 {
175 if s.forbidden(w, data, "board.update") {
176 return
177 }
178
179 b := db.BoardByID(resetBoardID)
180 if b == nil {
181 data.ManageError("Invalid board.")
182 return
183 }
184
185 bb := NewBoard()
186 bb.ID = b.ID
187 bb.Dir = b.Dir
188 bb.Name = b.Name
189 bb.Description = b.Description
190 db.UpdateBoard(bb)
191
192 s.refreshMaxRequestSize(db)
193 s.refreshBannerCache(db)
194 s.refreshRulesCache(db)
195 s.refreshCategoryCache(db)
196 s.refreshKeywordCache(db)
197 s.rebuildBoard(db, bb)
198 s.writeSiteIndex(db)
199
200 changes := printChanges(*b, *bb)
201 s.log(db, data.Account, nil, fmt.Sprintf("Reset >>/board/%d", bb.ID), changes)
202
203 data.Redirect(w, r, fmt.Sprintf("/sriracha/board/%d", bb.ID))
204 return
205 }
206
207 deleteBoardID := PathInt(r, "/sriracha/board/delete/")
208 if deleteBoardID > 0 {
209 if s.forbidden(w, data, "board.delete") {
210 return
211 }
212
213 b := db.BoardByID(deleteBoardID)
214 if b == nil {
215 data.ManageError("Invalid board.")
216 return
217 }
218
219 allThreads := db.AllThreads(b, false)
220 if !FormBool(r, "confirmation") {
221 data.Template = "manage_info"
222 data.Message = template.HTML(`<form method="post">
223 <input type="hidden" name="confirmation" value="1">
224 <fieldset>
225 <legend>
226 Delete ` + b.Path() + ` ` + html.EscapeString(b.Name) + `
227 </legend>
228 <div>
229 <h1>WARNING!</h1>
230 You are about to <b>PERMANENTLY DELETE</b> ` + b.Path() + ` ` + html.EscapeString(b.Name) + `!<br>
231 ` + strconv.Itoa(len(allThreads)) + ` threads in ` + b.Path() + ` will be <b>permanently deleted</b>.<br>
232 This operation cannot be undone.<br><br>
233 <input type="submit" value="Delete ` + b.Path() + `">
234 </div>
235 </fieldset>
236 </form>`)
237 return
238 }
239 for _, threadInfo := range allThreads {
240 s.deletePost(db, db.PostByID(threadInfo[0]))
241 }
242 db.DeleteBoard(b.ID)
243
244 if b.Dir != "" {
245 var skipDeleteDir bool
246 boardPath := filepath.Join(s.config.Root, b.Dir)
247 pattern := regexp.MustCompile(`^(index|catalog|[0-9]+).html$`)
248 filepath.WalkDir(boardPath, func(path string, d fs.DirEntry, err error) error {
249 if !d.IsDir() && !pattern.MatchString(d.Name()) && err == nil {
250 skipDeleteDir = true
251 return filepath.SkipAll
252 }
253 return nil
254 })
255 if !skipDeleteDir {
256 os.RemoveAll(boardPath)
257 }
258 }
259
260 s.refreshMaxRequestSize(db)
261 s.refreshBannerCache(db)
262 s.refreshRulesCache(db)
263 s.refreshCategoryCache(db)
264 s.refreshKeywordCache(db)
265 s.writeSiteIndex(db)
266
267 s.log(db, data.Account, nil, fmt.Sprintf("Deleted board #%d", b.ID), "")
268
269 data.Template = "manage_info"
270 data.Redirect(w, r, "/sriracha/board/")
271 return
272 }
273
274 boardID = PathInt(r, "/sriracha/board/")
275 if boardID > 0 {
276 data.Manage.Board = db.BoardByID(boardID)
277 if data.Manage.Board == nil {
278 data.ManageError("Board not found")
279 return false
280 }
281
282 if data.Manage.Board != nil && r.Method == http.MethodPost {
283 if s.forbidden(w, data, "board.update") {
284 return false
285 }
286 oldBoard := *data.Manage.Board
287
288 oldDir := data.Manage.Board.Dir
289 oldPath := data.Manage.Board.Path()
290 s.loadBoardForm(db, r, data.Manage.Board)
291
292 err := data.Manage.Board.Validate()
293 if err != nil {
294 data.ManageError(err.Error())
295 return false
296 }
297
298 if data.Manage.Board.Dir != "" && data.Manage.Board.Dir != oldDir {
299 _, err := os.Stat(filepath.Join(s.config.Root, data.Manage.Board.Dir))
300 if err != nil {
301 if !os.IsNotExist(err) {
302 log.Fatal(err)
303 }
304 } else {
305 data.ManageError("New directory already exists")
306 return false
307 }
308 }
309
310 db.UpdateBoard(data.Manage.Board)
311
312 if data.Manage.Board.Dir != oldDir {
313 subDirs := []string{"src", "thumb", "res"}
314 for _, subDir := range subDirs {
315 newPath := filepath.Join(s.config.Root, data.Manage.Board.Dir, subDir)
316 _, err := os.Stat(newPath)
317 if err == nil {
318 data.ManageError(fmt.Sprintf("New board directory %s already exists", newPath))
319 return false
320 }
321 }
322 moveSubDirs := func() error {
323 for _, subDir := range subDirs {
324 oldPath := filepath.Join(s.config.Root, oldDir, subDir)
325 newPath := filepath.Join(s.config.Root, data.Manage.Board.Dir, subDir)
326 err := os.Rename(oldPath, newPath)
327 if err != nil {
328 return fmt.Errorf("Failed to rename board directory %s to %s: %s", oldPath, newPath, err)
329 }
330 }
331 return nil
332 }
333 if data.Manage.Board.Dir == "" {
334 err = moveSubDirs()
335 if err != nil {
336 data.ManageError(err.Error())
337 return false
338 }
339 } else {
340 if oldDir == "" {
341 err := os.Mkdir(filepath.Join(s.config.Root, data.Manage.Board.Dir), NewDirPermission)
342 if err != nil {
343 data.ManageError(fmt.Sprintf("Failed to create board directory: %s", err))
344 return false
345 }
346 err = moveSubDirs()
347 if err != nil {
348 data.ManageError(err.Error())
349 return false
350 }
351 } else {
352 err := os.Rename(filepath.Join(s.config.Root, oldDir), filepath.Join(s.config.Root, data.Manage.Board.Dir))
353 if err != nil {
354 data.ManageError(fmt.Sprintf("Failed to rename board directory: %s", err))
355 return false
356 }
357 }
358 }
359
360 for _, info := range db.AllThreads(data.Manage.Board, false) {
361 for _, post := range db.AllPostsInThread(info[0], false) {
362 var modified bool
363 resPattern, err := regexp.Compile(`<a href="` + regexp.QuoteMeta(oldPath) + `res\/([0-9]+).html#([0-9]+)"`)
364 if err != nil {
365 log.Fatalf("failed to compile res pattern: %s", err)
366 }
367 post.Message = resPattern.ReplaceAllStringFunc(post.Message, func(s string) string {
368 modified = true
369 match := resPattern.FindStringSubmatch(s)
370 return fmt.Sprintf(`<a href="%sres/%s.html#%s"`, data.Manage.Board.Path(), match[1], match[2])
371 })
372 if modified {
373 db.UpdatePostMessage(post.ID, post.Message)
374 }
375 }
376 }
377 }
378
379 s.refreshMaxRequestSize(db)
380 s.refreshBannerCache(db)
381 s.refreshRulesCache(db)
382 s.refreshCategoryCache(db)
383 s.refreshKeywordCache(db)
384 s.rebuildBoard(db, data.Manage.Board)
385 s.writeSiteIndex(db)
386
387 changes := printChanges(oldBoard, *data.Manage.Board)
388 s.log(db, data.Account, nil, fmt.Sprintf("Updated >>/board/%d", data.Manage.Board.ID), changes)
389
390 data.Redirect(w, r, "/sriracha/board/")
391 return true
392 }
393 return false
394 }
395
396 if r.Method == http.MethodPost {
397 if s.forbidden(w, data, "board.add") {
398 return
399 }
400 b := &Board{}
401 s.loadBoardForm(db, r, b)
402
403 if FormBool(r, "duplicate") {
404 duplicateID := FormInt(r, "board")
405 d := db.BoardByID(duplicateID)
406 if d == nil {
407 data.ManageError("Board not found")
408 return false
409 }
410 b.Type = d.Type
411 b.Hide = d.Hide
412 b.Lock = d.Lock
413 b.Approval = d.Approval
414 b.Reports = d.Reports
415 b.Style = d.Style
416 b.Locale = d.Locale
417 b.Delay = d.Delay
418 b.MinName = d.MinName
419 b.MaxName = d.MaxName
420 b.MinEmail = d.MinEmail
421 b.MaxEmail = d.MaxEmail
422 b.MinSubject = d.MinSubject
423 b.MaxSubject = d.MaxSubject
424 b.MinMessage = d.MinMessage
425 b.MaxMessage = d.MaxMessage
426 b.MinSizeThread = d.MinSizeThread
427 b.MaxSizeThread = d.MaxSizeThread
428 b.MinSizeReply = d.MinSizeReply
429 b.MaxSizeReply = d.MaxSizeReply
430 b.ThumbWidth = d.ThumbWidth
431 b.ThumbHeight = d.ThumbHeight
432 b.DefaultName = d.DefaultName
433 b.WordBreak = d.WordBreak
434 b.Truncate = d.Truncate
435 b.Threads = d.Threads
436 b.Replies = d.Replies
437 b.MaxThreads = d.MaxThreads
438 b.MaxReplies = d.MaxReplies
439 b.Oekaki = d.Oekaki
440 b.Backlinks = d.Backlinks
441 b.Files = d.Files
442 b.Instances = d.Instances
443 b.Identifiers = d.Identifiers
444 b.Gallery = d.Gallery
445 b.Uploads = d.Uploads
446 b.Embeds = d.Embeds
447 b.Rules = d.Rules
448 }
449
450 err := b.Validate()
451 if err != nil {
452 data.ManageError(err.Error())
453 return false
454 }
455
456 dirs := []string{"", "src", "thumb", "res"}
457 for _, boardDir := range dirs {
458 if b.Dir == "" && boardDir == "" {
459 continue
460 }
461 boardPath := filepath.Join(s.config.Root, b.Dir, boardDir)
462 err = os.Mkdir(boardPath, NewDirPermission)
463 if err != nil {
464 if os.IsExist(err) {
465 data.ManageError(fmt.Sprintf("Board directory %s already exists.", boardPath))
466 } else {
467 data.ManageError(fmt.Sprintf("Failed to create board directory %s: %s", boardPath, err))
468 }
469 return false
470 }
471 }
472
473 db.AddBoard(b)
474
475 s.refreshMaxRequestSize(db)
476 s.refreshBannerCache(db)
477 s.refreshRulesCache(db)
478 s.refreshCategoryCache(db)
479 s.refreshKeywordCache(db)
480 s.rebuildBoard(db, b)
481 s.writeSiteIndex(db)
482
483 s.log(db, data.Account, nil, fmt.Sprintf("Added >>/board/%d", b.ID), "")
484
485 data.Redirect(w, r, "/sriracha/board/")
486 return true
487 }
488
489 data.Manage.Board = NewBoard()
490
491 data.Manage.Boards = db.AllBoards()
492 return false
493 }
494
View as plain text