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 deleteBoardID := PathInt(r, "/sriracha/board/delete/")
174 if deleteBoardID > 0 {
175 if s.forbidden(w, data, "board.delete") {
176 return
177 }
178
179 b := db.BoardByID(deleteBoardID)
180 if b == nil {
181 data.ManageError("Invalid board.")
182 return
183 }
184
185 allThreads := db.AllThreads(b, false)
186 if !FormBool(r, "confirmation") {
187 data.Template = "manage_info"
188 data.Message = template.HTML(`<form method="post">
189 <input type="hidden" name="confirmation" value="1">
190 <fieldset>
191 <legend>
192 Delete ` + b.Path() + ` ` + html.EscapeString(b.Name) + `
193 </legend>
194 <div>
195 <h1>WARNING!</h1>
196 You are about to <b>PERMANENTLY DELETE</b> ` + b.Path() + ` ` + html.EscapeString(b.Name) + `!<br>
197 ` + strconv.Itoa(len(allThreads)) + ` threads in ` + b.Path() + ` will be <b>permanently deleted</b>.<br>
198 This operation cannot be undone.<br><br>
199 <input type="submit" value="Delete ` + b.Path() + `">
200 </div>
201 </fieldset>
202 </form>`)
203 return
204 }
205 for _, threadInfo := range allThreads {
206 s.deletePost(db, db.PostByID(threadInfo[0]))
207 }
208 db.DeleteBoard(b.ID)
209
210 if b.Dir != "" {
211 var skipDeleteDir bool
212 boardPath := filepath.Join(s.config.Root, b.Dir)
213 pattern := regexp.MustCompile(`^(index|catalog|[0-9]+).html$`)
214 filepath.WalkDir(boardPath, func(path string, d fs.DirEntry, err error) error {
215 if !d.IsDir() && !pattern.MatchString(d.Name()) && err == nil {
216 skipDeleteDir = true
217 return filepath.SkipAll
218 }
219 return nil
220 })
221 if !skipDeleteDir {
222 os.RemoveAll(boardPath)
223 }
224 }
225
226 s.log(db, data.Account, nil, fmt.Sprintf("Deleted board #%d", b.ID), "")
227
228 data.Template = "manage_info"
229 http.Redirect(w, r, "/sriracha/board/", http.StatusFound)
230 return
231 }
232
233 boardID = PathInt(r, "/sriracha/board/")
234 if boardID > 0 {
235 data.Manage.Board = db.BoardByID(boardID)
236 if data.Manage.Board == nil {
237 data.ManageError("Board not found")
238 return false
239 }
240
241 if data.Manage.Board != nil && r.Method == http.MethodPost {
242 if s.forbidden(w, data, "board.update") {
243 return false
244 }
245 oldBoard := *data.Manage.Board
246
247 oldDir := data.Manage.Board.Dir
248 oldPath := data.Manage.Board.Path()
249 s.loadBoardForm(db, r, data.Manage.Board)
250
251 err := data.Manage.Board.Validate()
252 if err != nil {
253 data.ManageError(err.Error())
254 return false
255 }
256
257 if data.Manage.Board.Dir != "" && data.Manage.Board.Dir != oldDir {
258 _, err := os.Stat(filepath.Join(s.config.Root, data.Manage.Board.Dir))
259 if err != nil {
260 if !os.IsNotExist(err) {
261 log.Fatal(err)
262 }
263 } else {
264 data.ManageError("New directory already exists")
265 return false
266 }
267 }
268
269 db.UpdateBoard(data.Manage.Board)
270
271 if data.Manage.Board.Dir != oldDir {
272 subDirs := []string{"src", "thumb", "res"}
273 for _, subDir := range subDirs {
274 newPath := filepath.Join(s.config.Root, data.Manage.Board.Dir, subDir)
275 _, err := os.Stat(newPath)
276 if err == nil {
277 data.ManageError(fmt.Sprintf("New board directory %s already exists", newPath))
278 return false
279 }
280 }
281 moveSubDirs := func() error {
282 for _, subDir := range subDirs {
283 oldPath := filepath.Join(s.config.Root, oldDir, subDir)
284 newPath := filepath.Join(s.config.Root, data.Manage.Board.Dir, subDir)
285 err := os.Rename(oldPath, newPath)
286 if err != nil {
287 return fmt.Errorf("Failed to rename board directory %s to %s: %s", oldPath, newPath, err)
288 }
289 }
290 return nil
291 }
292 if data.Manage.Board.Dir == "" {
293 err = moveSubDirs()
294 if err != nil {
295 data.ManageError(err.Error())
296 return false
297 }
298 } else {
299 if oldDir == "" {
300 err := os.Mkdir(filepath.Join(s.config.Root, data.Manage.Board.Dir), NewDirPermission)
301 if err != nil {
302 data.ManageError(fmt.Sprintf("Failed to create board directory: %s", err))
303 return false
304 }
305 err = moveSubDirs()
306 if err != nil {
307 data.ManageError(err.Error())
308 return false
309 }
310 } else {
311 err := os.Rename(filepath.Join(s.config.Root, oldDir), filepath.Join(s.config.Root, data.Manage.Board.Dir))
312 if err != nil {
313 data.ManageError(fmt.Sprintf("Failed to rename board directory: %s", err))
314 return false
315 }
316 }
317 }
318
319 for _, info := range db.AllThreads(data.Manage.Board, false) {
320 for _, post := range db.AllPostsInThread(info[0], false) {
321 var modified bool
322 resPattern, err := regexp.Compile(`<a href="` + regexp.QuoteMeta(oldPath) + `res\/([0-9]+).html#([0-9]+)"`)
323 if err != nil {
324 log.Fatalf("failed to compile res pattern: %s", err)
325 }
326 post.Message = resPattern.ReplaceAllStringFunc(post.Message, func(s string) string {
327 modified = true
328 match := resPattern.FindStringSubmatch(s)
329 return fmt.Sprintf(`<a href="%sres/%s.html#%s"`, data.Manage.Board.Path(), match[1], match[2])
330 })
331 if modified {
332 db.UpdatePostMessage(post.ID, post.Message)
333 }
334 }
335 }
336 }
337
338 s.rebuildBoard(db, data.Manage.Board)
339
340 changes := printChanges(oldBoard, *data.Manage.Board)
341 s.log(db, data.Account, nil, fmt.Sprintf("Updated >>/board/%d", data.Manage.Board.ID), changes)
342
343 http.Redirect(w, r, "/sriracha/board/", http.StatusFound)
344 return true
345 }
346 return false
347 }
348
349 if r.Method == http.MethodPost {
350 if s.forbidden(w, data, "board.add") {
351 return
352 }
353 b := &Board{}
354 s.loadBoardForm(db, r, b)
355
356 err := b.Validate()
357 if err != nil {
358 data.ManageError(err.Error())
359 return false
360 }
361
362 dirs := []string{"", "src", "thumb", "res"}
363 for _, boardDir := range dirs {
364 if b.Dir == "" && boardDir == "" {
365 continue
366 }
367 boardPath := filepath.Join(s.config.Root, b.Dir, boardDir)
368 err = os.Mkdir(boardPath, NewDirPermission)
369 if err != nil {
370 if os.IsExist(err) {
371 data.ManageError(fmt.Sprintf("Board directory %s already exists.", boardPath))
372 } else {
373 data.ManageError(fmt.Sprintf("Failed to create board directory %s: %s", boardPath, err))
374 }
375 return false
376 }
377 }
378
379 db.AddBoard(b)
380
381 s.rebuildBoard(db, b)
382
383 s.log(db, data.Account, nil, fmt.Sprintf("Added >>/board/%d", b.ID), "")
384
385 http.Redirect(w, r, "/sriracha/board/", http.StatusFound)
386 return true
387 }
388
389 data.Manage.Board = NewBoard()
390
391 data.Manage.Boards = db.AllBoards()
392 return false
393 }
394
View as plain text