1 package server
2
3 import (
4 "fmt"
5 "html"
6 "html/template"
7 "io"
8 "log"
9 "net/http"
10 "os"
11 "path/filepath"
12 "strconv"
13 "strings"
14 "time"
15
16 "codeberg.org/tslocum/sriracha/internal/database"
17 . "codeberg.org/tslocum/sriracha/model"
18 . "codeberg.org/tslocum/sriracha/util"
19 )
20
21 func (s *Server) serveMod(data *templateData, db *database.DB, w http.ResponseWriter, r *http.Request) {
22 data.Template = "manage_mod"
23
24 var postID int
25 var action = "db"
26 modInfo := PathString(r, "/sriracha/mod/")
27 if modInfo != "" {
28 split := strings.Split(modInfo, "/")
29 if len(split) == 2 {
30 switch split[0] {
31 case "delete":
32 action = "d"
33 case "ban":
34 action = "b"
35 case "sticky":
36 action = "s"
37 case "unsticky":
38 action = "us"
39 case "lock":
40 action = "l"
41 case "unlock":
42 action = "ul"
43 case "view":
44 action = "v"
45 case "move":
46 action = "m"
47 default:
48 data.ManageError("Unknown mod action")
49 return
50 }
51 postID = ParseInt(split[1])
52 } else if len(split) == 1 {
53 postID = ParseInt(split[0])
54 }
55 }
56 if postID == 0 {
57 data.ManageError("Unknown post")
58 return
59 }
60 data.Post = db.PostByID(postID)
61 if data.Post == nil {
62 data.ManageError("Unknown post")
63 return
64 }
65 if action == "v" {
66 if !s.opt.Identifiers {
67 data.ManageError("Identifiers are not enabled")
68 return
69 }
70 data.Template = "board_page"
71 data.ModMode = true
72 data.ReplyMode = 1
73 data.Board = data.Post.Board
74 posts := db.PostsByIP(data.Post.IP)
75 if r.FormValue("confirmation") == "1" {
76 if s.forbidden(w, data, "post.delete") {
77 return
78 }
79 for _, post := range posts {
80 s.deletePost(db, post)
81 s.log(db, data.Account, post.Board, fmt.Sprintf("Deleted >>%d", post.ID), "")
82 s.rebuildThread(db, post)
83 }
84 data.Message = "Deleted all posts by author."
85 return
86 }
87 for _, post := range posts {
88 data.Threads = append(data.Threads, []*Post{post})
89 }
90 data.Message = `<form method="post" onsubmit="javascript:return confirm('Delete all posts by author?');"><input type="hidden" name="confirmation" value="1"><input type="submit" value="Delete all posts by author"></form><br>`
91 return
92 } else if action == "m" {
93 if s.forbidden(w, data, "post.move") {
94 return
95 }
96 if data.Post.Parent != 0 {
97 data.ManageError("Only threads may be moved")
98 return
99 }
100 data.Template = "board_page"
101 data.ModMode = true
102 data.ReplyMode = 1
103 data.Board = data.Post.Board
104 data.Threads = append(data.Threads, []*Post{data.Post})
105 if r.FormValue("confirmation") == "1" {
106 boardID := FormInt(r, "board")
107 destination := db.BoardByID(boardID)
108 if destination == nil {
109 data.ManageError("Failed to move thread: Unknown board")
110 return
111 } else if destination.ID == data.Board.ID {
112 data.ManageError("Failed to move thread: Thread is already located in selected board")
113 return
114 }
115 posts := db.AllPostsInThread(data.Post.ID, false)
116
117 for _, p := range posts {
118 if p.File != "" && !p.IsEmbed() {
119 _, err := os.Stat(filepath.Join(s.config.Root, destination.Path(), "src", p.File))
120 if err != nil && !os.IsNotExist(err) {
121 data.ManageError(fmt.Sprintf("Failed to move thread: File /src/%s already exists at destination", p.File))
122 return
123 }
124 }
125 if p.Thumb != "" {
126 _, err := os.Stat(filepath.Join(s.config.Root, destination.Path(), "thumb", p.File))
127 if err != nil && !os.IsNotExist(err) {
128 data.ManageError(fmt.Sprintf("Failed to move thread: File /thumb/%s already exists at destination", p.File))
129 return
130 }
131 }
132 }
133
134 copyFile := func(dirName string, fileName string) error {
135 srcPath := filepath.Join(s.config.Root, data.Post.Board.Path(), dirName, fileName)
136 dstPath := filepath.Join(s.config.Root, destination.Path(), dirName, fileName)
137
138 srcFile, err := os.Open(srcPath)
139 if err != nil {
140 return fmt.Errorf("Failed to move thread: Failed to open source file /%s/%s: %s", dirName, fileName, err)
141 }
142 dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, NewFilePermission)
143 if err != nil {
144 srcFile.Close()
145 return fmt.Errorf("Failed to move thread: Failed to open destination file /%s/%s: %s", dirName, fileName, err)
146 }
147 _, err = io.Copy(dstFile, srcFile)
148 srcFile.Close()
149 dstFile.Close()
150 if err != nil {
151 return fmt.Errorf("Failed to move thread: Failed to copy file /%s/%s: %s", dirName, fileName, err)
152 }
153 return nil
154 }
155 for _, p := range posts {
156 if p.File != "" && !p.IsEmbed() {
157 err := copyFile("src", p.File)
158 if err != nil {
159 data.ManageError(err.Error())
160 return
161 }
162 }
163 if p.Thumb != "" {
164 err := copyFile("thumb", p.Thumb)
165 if err != nil {
166 data.ManageError(err.Error())
167 return
168 }
169 }
170 }
171
172 for _, p := range posts {
173 if p.File != "" && !p.IsEmbed() {
174 os.Remove(filepath.Join(s.config.Root, data.Post.Board.Path(), "src", p.File))
175 }
176 if p.Thumb != "" {
177 os.Remove(filepath.Join(s.config.Root, data.Post.Board.Path(), "thumb", p.Thumb))
178 }
179 }
180
181 os.Remove(filepath.Join(s.config.Root, data.Post.Board.Path(), "res", fmt.Sprintf("%d.html", data.Post.ID)))
182
183 source := data.Post.Board
184 for _, p := range posts {
185 db.UpdatePostBoard(p.ID, destination.ID)
186 p.Board = destination
187 refPath := fmt.Sprintf("res/%d.html#%d", p.Thread(), p.ID)
188 oldPath := source.Path() + refPath
189 newPath := destination.Path() + refPath
190 _, err := db.Exec(`UPDATE post SET message = replace(replace(message, '<a href="` + oldPath + `" class="refop">>>` + strconv.Itoa(p.ID) + `</a>', '<a href="` + newPath + `" class="refop">>>` + strconv.Itoa(p.ID) + `</a>'), '<a href="` + oldPath + `" class="refreply">>>` + strconv.Itoa(p.ID) + `</a>', '<a href="` + newPath + `" class="refreply">>>` + strconv.Itoa(p.ID) + `</a>') WHERE message LIKE '%>>` + strconv.Itoa(p.ID) + `%'`)
191 if err != nil {
192 log.Fatalf("failed to move thread: failed to update reflinks: %s", err)
193 }
194 }
195 data.Post = db.PostByID(data.Post.ID)
196 data.Board = destination
197 data.Threads = [][]*Post{{data.Post}}
198 data.Message = template.HTML(fmt.Sprintf("Moved No.%d to %s.", data.Post.ID, destination.Path()))
199 s.log(db, data.Account, data.Post.Board, fmt.Sprintf("Moved >>/post/%d to >>/board/%d", data.Post.ID, data.Board.ID), "")
200
201 if FormInt(r, "notice") == 1 {
202 const linkFormat = `<a href="%s">>>>%s</a>`
203 sourceLink := fmt.Sprintf(linkFormat, source.Path(), source.Path())
204 destinationLink := fmt.Sprintf(linkFormat, destination.Path(), destination.Path())
205 now := time.Now().Unix()
206 p := &Post{
207 Board: destination,
208 Parent: data.Post.ID,
209 Timestamp: now,
210 Bumped: now,
211 Message: Get(destination, nil, "Thread moved from %[1]s to %[2]s.", sourceLink, destinationLink),
212 Moderated: ModeratedApproved,
213 }
214 p.SetNameBlock("", "Mod", false)
215 db.AddPost(p)
216 }
217
218 s.rebuildThread(db, data.Post)
219 s.writeIndexes(db, source)
220 } else {
221 moveLabel := Get(data.Board, data.Account, "Move")
222 boardLabel := Get(data.Board, data.Account, "Board")
223 data.Message = `<br><fieldset><legend>` + template.HTML(html.EscapeString(moveLabel)) + ` No.` + template.HTML(strconv.Itoa(data.Post.ID)) + `</legend><form method="post"><table border="0" class="manageform"><input type="hidden" name="confirmation" value="1"><tr><td class="postblock">` + template.HTML(html.EscapeString(boardLabel)) + `</td><td><select name="board">`
224 for _, b := range db.AllBoards() {
225 var extra string
226 if data.Board.ID == b.ID {
227 extra = " selected"
228 }
229 data.Message += template.HTML(fmt.Sprintf(`<option value="%d"%s>%s %s</option>`, b.ID, extra, b.Path(), html.EscapeString(b.Name)))
230 }
231 noticeLabel := Get(data.Board, data.Account, "Notice")
232 addNoticeLabel := Get(data.Board, data.Account, "Add notice")
233 data.Message += `</select></td></tr><tr><td class="postblock"><label for="notice">` + template.HTML(html.EscapeString(noticeLabel)) + `</label></td><td><label><input type="checkbox" id="notice" name="notice" value="1"> ` + template.HTML(html.EscapeString(addNoticeLabel)) + `</label></td></tr><tr><td> </td><td align="right"><input type="submit" class="managebutton" style="width: auto;min-width: 50%;" value="` + template.HTML(html.EscapeString(moveLabel)) + `"></td></tr></table></form></fieldset><br><br>`
234 }
235 return
236 }
237 threadAction := action == "s" || action == "us" || action == "l" || action == "ul"
238 if threadAction {
239 if data.Post.Parent != 0 {
240 data.ManageError("Invalid post")
241 return
242 }
243
244 var skipRebuild bool
245 switch {
246 case action == "s" && !data.Post.Stickied:
247 if s.forbidden(w, data, "post.sticky") {
248 return
249 }
250 db.StickyPost(data.Post.ID, true)
251 s.log(db, data.Account, nil, fmt.Sprintf("Stickied >>/post/%d", data.Post.ID), "")
252 case action == "us" && data.Post.Stickied:
253 if s.forbidden(w, data, "post.sticky") {
254 return
255 }
256 db.StickyPost(data.Post.ID, false)
257 s.log(db, data.Account, nil, fmt.Sprintf("Unstickied >>/post/%d", data.Post.ID), "")
258 case action == "l" && !data.Post.Locked:
259 if s.forbidden(w, data, "post.lock") {
260 return
261 }
262 db.LockPost(data.Post.ID, true)
263 s.log(db, data.Account, nil, fmt.Sprintf("Locked >>/post/%d", data.Post.ID), "")
264 case action == "ul" && data.Post.Locked:
265 if s.forbidden(w, data, "post.lock") {
266 return
267 }
268 db.LockPost(data.Post.ID, false)
269 s.log(db, data.Account, nil, fmt.Sprintf("Unlocked >>/post/%d", data.Post.ID), "")
270 default:
271 skipRebuild = true
272 }
273 if !skipRebuild {
274 s.rebuildThread(db, data.Post)
275 }
276
277 data.Template = "manage_info"
278 http.Redirect(w, r, fmt.Sprintf("/sriracha/board/mod/%d/%d", data.Post.Board.ID, data.Post.ID), http.StatusFound)
279 return
280 }
281 data.Board = data.Post.Board
282 data.Threads = [][]*Post{{data.Post}}
283 data.Manage.Ban = db.BanByIP(data.Post.IP)
284 if r.FormValue("confirmation") == "1" {
285 banFile := FormString(r, "banfile")
286 if banFile != "" && !db.FileBanned(banFile) {
287 if s.forbidden(w, data, "banfile.add") {
288 return
289 }
290 db.AddFileBan(banFile)
291 s.log(db, data.Account, nil, "Banned file", "")
292 }
293
294 var oldBan Ban
295 if data.Manage.Ban != nil {
296 oldBan = *data.Manage.Ban
297 }
298 if action == "b" || action == "db" {
299 if data.Manage.Ban != nil {
300 if s.forbidden(w, data, "ban.lengthen") {
301 return
302 }
303 s.loadBanForm(db, r, data.Manage.Ban)
304 db.UpdateBan(data.Manage.Ban)
305
306 changes := printChanges(oldBan, *data.Manage.Ban)
307 s.log(db, data.Account, nil, fmt.Sprintf("Updated >>/ban/%d", data.Manage.Ban.ID), changes)
308 } else {
309 if s.forbidden(w, data, "ban.add") {
310 return
311 }
312 ban := &Ban{}
313 s.loadBanForm(db, r, ban)
314 ban.IP = data.Post.IP
315 db.AddBan(ban)
316
317 s.log(db, data.Account, nil, fmt.Sprintf("Added >>/ban/%d", ban.ID), ban.Info())
318 }
319 }
320 if action == "d" || action == "db" {
321 if s.forbidden(w, data, "post.delete") {
322 return
323 }
324 s.deletePost(db, data.Post)
325
326 s.log(db, data.Account, data.Board, fmt.Sprintf("Deleted >>%d", data.Post.ID), "")
327
328 s.rebuildThread(db, data.Post)
329 }
330
331 label := "Deleted"
332 switch action {
333 case "b":
334 label = "Banned"
335 case "db":
336 label = "Deleted and banned"
337 }
338
339 data.Template = "manage_info"
340 data.Info = fmt.Sprintf("%s No.%d", label, data.Post.ID)
341 return
342 }
343
344 data.ModMode = true
345 data.ReplyMode = 1
346 data.Extra = action
347 if data.Post != nil {
348 data.Extra2 = data.Post.FileHash
349 }
350 }
351
View as plain text