...

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  	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