...

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

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

     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