...

Source file src/codeberg.org/tslocum/sriracha/internal/server/server_mod.go

Documentation: codeberg.org/tslocum/sriracha/internal/server

     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  			// Verify attachments do not already exist at destination board.
   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  			// Copy attachments.
   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  			// Remove source attachments.
   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  			// Delete thread page.
   181  			os.Remove(filepath.Join(s.config.Root, data.Post.Board.Path(), "res", fmt.Sprintf("%d.html", data.Post.ID)))
   182  			// Update post board.
   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">&gt;&gt;` + strconv.Itoa(p.ID) + `</a>', '<a href="` + newPath + `" class="refop">&gt;&gt;` + strconv.Itoa(p.ID) + `</a>'), '<a href="` + oldPath + `" class="refreply">&gt;&gt;` + strconv.Itoa(p.ID) + `</a>', '<a href="` + newPath + `" class="refreply">&gt;&gt;` + strconv.Itoa(p.ID) + `</a>') WHERE message LIKE '%&gt;&gt;` + 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  			// Add notice.
   201  			if FormInt(r, "notice") == 1 {
   202  				const linkFormat = `<a href="%s">&gt;&gt;&gt;%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  			// Rebuild static files.
   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>&nbsp;</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