From 6567da33002f8107ed5638c012ef7abb074b4cf2 Mon Sep 17 00:00:00 2001 From: Eggbertx Date: Tue, 11 Oct 2022 14:26:31 -0700 Subject: [PATCH] Add de-deprecation stuff (will not build yet) --- README.md | 6 +- .../internal/pre2021/boards.go | 24 +- cmd/gochan/deleteposts.go | 162 ++-- cmd/gochan/editpost.go | 68 +- cmd/gochan/main.go | 4 +- cmd/gochan/movethread.go | 90 +-- pkg/building/boards.go | 13 +- pkg/building/catalog.go | 18 + pkg/config/config.go | 29 +- pkg/gcplugin/lua_test.go | 12 +- pkg/gcsql/boards.go | 364 ++++----- pkg/gcsql/boards_test.go | 12 - pkg/gcsql/connect.go | 15 - pkg/gcsql/database.go | 11 +- pkg/gcsql/filebans.go | 121 --- pkg/gcsql/initdb_test.go | 60 -- pkg/gcsql/posts.go | 332 ++++---- pkg/gcsql/posts_test.go | 32 - pkg/gcsql/postsretrievalqueries.go | 313 -------- .../{databasecheck.go => provisioning.go} | 21 +- pkg/gcsql/queries.go | 613 --------------- pkg/gcsql/sections.go | 141 ++-- pkg/gcsql/sections_test.go | 48 -- pkg/gcsql/staff.go | 335 ++------ pkg/gcsql/tables.go | 729 +++++------------- pkg/gcsql/threads.go | 74 ++ pkg/gcsql/uploads.go | 37 + pkg/gcsql/util.go | 71 +- pkg/gctemplates/funcs.go | 10 +- 29 files changed, 1094 insertions(+), 2671 deletions(-) create mode 100644 pkg/building/catalog.go delete mode 100644 pkg/gcsql/boards_test.go delete mode 100644 pkg/gcsql/filebans.go delete mode 100644 pkg/gcsql/initdb_test.go delete mode 100644 pkg/gcsql/posts_test.go delete mode 100644 pkg/gcsql/postsretrievalqueries.go rename pkg/gcsql/{databasecheck.go => provisioning.go} (82%) delete mode 100644 pkg/gcsql/queries.go delete mode 100644 pkg/gcsql/sections_test.go create mode 100644 pkg/gcsql/threads.go create mode 100644 pkg/gcsql/uploads.go diff --git a/README.md b/README.md index fbce7c5a..d51e32bd 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,8 @@ See [`docker/README.md`](docker/README.md) ## Configuration See [config.md](config.md) -## Migration -If you run gochan and get a message telling you your database is out of data, please run gochan-migration. If this does not work, please contact the developers. - -gochan-migration has been a gargantuan time sink and has wasted a lot of time that would be much better spent working on other features, so I am putting its development on indefinite hiatus as of 12/18/2021 so I can focus on gochan's development. It may or may not come back, but for the time being, RIP gochan-migration, we hardly knew ya. + ## For developers (using Vagrant) 1. Install Vagrant and Virtualbox. Vagrant lets you create a virtual machine and run a custom setup/installation script to make installation easier and faster. diff --git a/cmd/gochan-migration/internal/pre2021/boards.go b/cmd/gochan-migration/internal/pre2021/boards.go index b6740daa..89596236 100644 --- a/cmd/gochan-migration/internal/pre2021/boards.go +++ b/cmd/gochan-migration/internal/pre2021/boards.go @@ -14,9 +14,9 @@ func (m *Pre2021Migrator) MigrateBoards() error { m.newBoards = map[int]string{} } // get all boards from new db - boards, err := gcsql.GetAllBoards() + err := gcsql.ResetBoardSectionArrays() if err != nil { - return err + return nil } // get boards from old db @@ -27,8 +27,6 @@ func (m *Pre2021Migrator) MigrateBoards() error { for rows.Next() { var id int var dir string - var board_type int - var upload_type int var title string var subtitle string var description string @@ -50,8 +48,6 @@ func (m *Pre2021Migrator) MigrateBoards() error { if err = rows.Scan( &id, &dir, - &board_type, - &upload_type, &title, &subtitle, &description, @@ -73,11 +69,11 @@ func (m *Pre2021Migrator) MigrateBoards() error { return err } found := false - for b := range boards { + for b := range gcsql.AllBoards { if _, ok := m.oldBoards[id]; !ok { m.oldBoards[id] = dir } - if boards[b].Dir == dir { + if gcsql.AllBoards[b].Dir == dir { log.Printf("Board /%s/ already exists in new db, moving on\n", dir) found = true break @@ -90,27 +86,25 @@ func (m *Pre2021Migrator) MigrateBoards() error { // omitting things like ID and creation date since we don't really care if err = gcsql.CreateBoard(&gcsql.Board{ Dir: dir, - Type: board_type, // ?? - UploadType: upload_type, // ?? Title: title, Subtitle: subtitle, Description: description, - Section: section, + SectionID: section, MaxFilesize: max_file_size, MaxPages: max_pages, DefaultStyle: default_style, Locked: locked, - Anonymous: anonymous, - ForcedAnon: forced_anon, + AnonymousName: anonymous, + ForceAnonymous: forced_anon, MaxAge: max_age, AutosageAfter: autosage_after, NoImagesAfter: no_images_after, MaxMessageLength: max_message_length, - EmbedsAllowed: embeds_allowed, + AllowEmbeds: embeds_allowed, RedirectToThread: redirect_to_thread, RequireFile: require_file, EnableCatalog: enable_catalog, - }); err != nil { + }, false); err != nil { return err } m.newBoards[id] = dir diff --git a/cmd/gochan/deleteposts.go b/cmd/gochan/deleteposts.go index 0780af50..c3402f7b 100644 --- a/cmd/gochan/deleteposts.go +++ b/cmd/gochan/deleteposts.go @@ -55,15 +55,11 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R } for _, checkedPostID := range checkedPosts { - var post gcsql.Post - var err error - post.ID = checkedPostID - post.BoardID = boardid - post, err = gcsql.GetSpecificPost(post.ID, true) + post, err := gcsql.GetPostFromID(checkedPostID, true) if err == sql.ErrNoRows { serverutil.ServeError(writer, "Post does not exist", wantsJSON, map[string]interface{}{ "postid": post.ID, - "boardid": post.BoardID, + "boardid": board.ID, }) return } else if err != nil { @@ -71,11 +67,11 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R Str("requestType", "deletePost"). Err(err). Int("postid", post.ID). - Int("boardid", post.BoardID). + Int("boardid", board.ID). Msg("Error deleting post") serverutil.ServeError(writer, "Error deleting post: "+err.Error(), wantsJSON, map[string]interface{}{ "postid": post.ID, - "boardid": post.BoardID, + "boardid": board.ID, }) return } @@ -83,93 +79,135 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R if passwordMD5 != post.Password && rank == 0 { serverutil.ServeError(writer, fmt.Sprintf("Incorrect password for #%d", post.ID), wantsJSON, map[string]interface{}{ "postid": post.ID, - "boardid": post.BoardID, + "boardid": board.ID, }) return } if fileOnly { - fileName := post.Filename - if fileName != "" && fileName != "deleted" { - var files []string - if files, err = post.GetFilePaths(); err != nil { - gcutil.Logger().Error(). - Str("requestType", "deleteFile"). + upload, err := post.GetUpload() + if err != nil { + gcutil.LogError(err). + Str("IP", gcutil.GetRealIP(request)). + Int("postid", post.ID). + Msg("Unable to get file upload info") + serverutil.ServeError(writer, "Error getting file uplaod info: "+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) + return + } + documentRoot := config.GetSystemCriticalConfig().DocumentRoot + if upload != nil && upload.Filename != "deleted" { + filePath := path.Join(documentRoot, board.Dir, "src", upload.Filename) + if err = os.Remove(filePath); err != nil { + gcutil.LogError(err). + Str("IP", gcutil.GetRealIP(request)). Int("postid", post.ID). - Err(err). - Msg("Error getting file upload info") - serverutil.ServeError(writer, "Error getting file upload info: "+err.Error(), wantsJSON, map[string]interface{}{ - "postid": post.ID, - }) + Str("filename", upload.Filename). + Msg("Unable to delete file") + serverutil.ServeError(writer, "Unable to delete file: "+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) return } - - if err = post.UnlinkUploads(true); err != nil { - gcutil.Logger().Error(). - Str("requestType", "deleteFile"). + // delete the file's thumbnail + thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("thumb")) + if err = os.Remove(thumbPath); err != nil { + gcutil.LogError(err). + Str("IP", gcutil.GetRealIP(request)). Int("postid", post.ID). - Err(err). - Msg("Error unlinking post uploads") - serverutil.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{ - "postid": post.ID, - }) + Str("thumbnail", upload.ThumbnailPath("thumb")). + Msg("Unable to delete thumbnail") + serverutil.ServeError(writer, "Unable to delete thumbnail: "+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) return } - - for _, filePath := range files { - if err = os.Remove(filePath); err != nil { - fileBase := path.Base(filePath) - gcutil.Logger().Error(). - Str("requestType", "deleteFile"). + // delete the catalog thumbnail + if post.IsTopPost { + thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("catalog")) + if err = os.Remove(thumbPath); err != nil { + gcutil.LogError(err). + Str("IP", gcutil.GetRealIP(request)). Int("postid", post.ID). - Str("file", filePath). - Err(err). - Msg("Error unlinking post uploads") - serverutil.ServeError(writer, fmt.Sprintf("Error deleting %s: %s", fileBase, err.Error()), wantsJSON, map[string]interface{}{ - "postid": post.ID, - "file": fileBase, - }) + Str("catalogThumb", upload.ThumbnailPath("catalog")). + Msg("Unable to delete catalog thumbnail") + serverutil.ServeError(writer, "Unable to delete catalog thumbnail: "+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) return } } + if err = post.UnlinkUploads(true); err != nil { + gcutil.LogError(err). + Str("requestType", "deleteFile"). + Int("postid", post.ID). + Msg("Error unlinking post uploads") + serverutil.ServeError(writer, "Unable to unlink post uploads"+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) + return + } } - _board, _ := gcsql.GetBoardFromID(post.BoardID) - building.BuildBoardPages(&_board) + // _board, err := post.GetBoard() + // if err != nil { + // gcutil.LogError(err). + // Int("postid", post.ID). + // Str("IP", post.IP). + // Msg("Unable to get board info from post") + // serverutil.ServeError(writer, "Unable to get board info from post: "+err.Error(), wantsJSON, map[string]interface{}{ + // "postid": post.ID, + // }) + // } + // building.BuildBoardPages(_board) + building.BuildBoardPages(board) - var opPost gcsql.Post - if post.ParentID > 0 { - // post is a reply, get the OP - opPost, _ = gcsql.GetSpecificPost(post.ParentID, true) - } else { + var opPost *gcsql.Post + if post.IsTopPost { opPost = post + } else { + if opPost, err = post.GetTopPost(); err != nil { + gcutil.LogError(err). + Int("postid", post.ID). + Str("IP", post.IP). + Msg("Unable to get thread information from post") + serverutil.ServeError(writer, "Unable to get thread info from post: "+err.Error(), wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) + return + } + } + if building.BuildThreadPages(opPost); err != nil { + gcutil.LogError(err). + Int("postid", post.ID). + Str("IP", post.IP). + Msg("Unable to build thread pages") + serverutil.ServeError(writer, "Unable to get board info from post: "+err.Error(), wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) + return } - building.BuildThreadPages(&opPost) } else { // delete the post - if err = gcsql.DeletePost(post.ID, true); err != nil { - gcutil.Logger().Error(). - Str("requestType", "deleteFile"). + if err = post.Delete(); err != nil { + gcutil.LogError(err). + Str("requestType", "deletePost"). Int("postid", post.ID). - Err(err). Msg("Error deleting post") serverutil.ServeError(writer, "Error deleting post: "+err.Error(), wantsJSON, map[string]interface{}{ "postid": post.ID, }) return } - if post.ParentID == 0 { + if post.IsTopPost { threadIndexPath := path.Join(config.GetSystemCriticalConfig().DocumentRoot, board.WebPath(strconv.Itoa(post.ID), "threadPage")) os.Remove(threadIndexPath + ".html") os.Remove(threadIndexPath + ".json") } else { - _board, _ := gcsql.GetBoardFromID(post.BoardID) - building.BuildBoardPages(&_board) + building.BuildBoardPages(board) + // _board, _ := gcsql.GetBoardFromID(post.BoardID) + // building.BuildBoardPages(&_board) } - building.BuildBoards(false, post.BoardID) + building.BuildBoards(false, boardid) } gcutil.LogAccess(request). Str("requestType", "deletePost"). - Int("boardid", post.BoardID). + Int("boardid", boardid). Int("postid", post.ID). Bool("fileOnly", fileOnly). Msg("Post deleted") @@ -177,16 +215,16 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R serverutil.ServeJSON(writer, map[string]interface{}{ "success": "post deleted", "postid": post.ID, - "boardid": post.BoardID, + "boardid": boardid, "fileOnly": fileOnly, }) } else { - if post.ParentID == 0 { + if post.IsTopPost { // deleted thread http.Redirect(writer, request, board.WebPath("/", "boardPage"), http.StatusFound) } else { // deleted a post in the thread - http.Redirect(writer, request, post.GetURL(false), http.StatusFound) + http.Redirect(writer, request, post.WebPath(), http.StatusFound) } } } diff --git a/cmd/gochan/editpost.go b/cmd/gochan/editpost.go index 562f6d90..e81dc0bd 100644 --- a/cmd/gochan/editpost.go +++ b/cmd/gochan/editpost.go @@ -35,8 +35,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res } passwordMD5 := gcutil.Md5Sum(password) - var post gcsql.Post - post, err = gcsql.GetSpecificPost(checkedPosts[0], true) + post, err := gcsql.GetPostFromID(checkedPosts[0], true) if err != nil { gcutil.Logger().Error(). Err(err). @@ -69,11 +68,24 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res var password string postid, err := strconv.Atoi(request.FormValue("postid")) if err != nil { - gcutil.Logger().Error(). - Err(err). + gcutil.LogError(err). + Str("postid", request.FormValue("postid")). Str("IP", gcutil.GetRealIP(request)). Msg("Invalid form data") - serverutil.ServeErrorPage(writer, "Invalid form data: "+err.Error()) + serverutil.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]interface{}{ + "postid": postid, + }) + return + } + post, err := gcsql.GetPostFromID(postid, true) + if err != nil { + gcutil.LogError(err). + Str("IP", gcutil.GetRealIP(request)). + Int("postid", postid). + Msg("Unable to find post") + serverutil.ServeError(writer, "Unable to find post: "+err.Error(), wantsJSON, map[string]interface{}{ + "postid": postid, + }) return } boardid, err := strconv.Atoi(request.FormValue("boardid")) @@ -82,41 +94,47 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res Err(err). Str("IP", gcutil.GetRealIP(request)). Msg("Invalid form data") - serverutil.ServeErrorPage(writer, "Invalid form data: "+err.Error()) - return - } - password, err = gcsql.GetPostPassword(postid) - if err != nil { - gcutil.Logger().Error(). - Err(err). - Str("IP", gcutil.GetRealIP(request)). - Msg("Invalid form data") + serverutil.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, nil) return } + // password, err = gcsql.GetPostPassword(postid) + // if err != nil { + // gcutil.LogError(err). + // Str("IP", gcutil.GetRealIP(request)). + // Msg("Invalid form data") + // return + // } rank := manage.GetStaffRank(request) if request.FormValue("password") != password && rank == 0 { - serverutil.ServeErrorPage(writer, "Wrong password") + serverutil.ServeError(writer, "Wrong password", wantsJSON, nil) return } - var board gcsql.Board - if err = board.PopulateData(boardid); err != nil { - serverutil.ServeErrorPage(writer, "Invalid form data: "+err.Error()) - gcutil.Logger().Error(). - Err(err). + board, err := gcsql.GetBoardFromID(boardid) + if err != nil { + serverutil.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]interface{}{ + "boardid": boardid, + }) + gcutil.LogError(err). Str("IP", gcutil.GetRealIP(request)). Msg("Invalid form data") return } - if err = gcsql.UpdatePost(postid, request.FormValue("editemail"), request.FormValue("editsubject"), - posting.FormatMessage(request.FormValue("editmsg"), board.Dir), request.FormValue("editmsg")); err != nil { - gcutil.Logger().Error(). - Err(err). + if err = post.UpdateContents( + request.FormValue("editemail"), + request.FormValue("editsubject"), + posting.FormatMessage(request.FormValue("editmsg"), board.Dir), + request.FormValue("editmsg"), + ); err != nil { + gcutil.LogError(err). + Int("postid", post.ID). Str("IP", gcutil.GetRealIP(request)). Msg("Unable to edit post") - serverutil.ServeErrorPage(writer, "Unable to edit post: "+err.Error()) + serverutil.ServeError(writer, "Unable to edit post: "+err.Error(), wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) return } diff --git a/cmd/gochan/main.go b/cmd/gochan/main.go index 234d5507..ba53fce5 100644 --- a/cmd/gochan/main.go +++ b/cmd/gochan/main.go @@ -104,7 +104,7 @@ func parseCommandLine() { os.Exit(1) } fmt.Printf("Creating new staff: %q, with password: %q and rank: %d from command line", arr[0], arr[1], rank) - if err = gcsql.NewStaff(arr[0], arr[1], rank); err != nil { + if _, err = gcsql.NewStaff(arr[0], arr[1], rank); err != nil { fmt.Printf("Failed creating new staff account for %q: %s\n", arr[0], err.Error()) gcutil.LogFatal(). Str("staff", "add"). @@ -132,7 +132,7 @@ func parseCommandLine() { fmt.Scanln(&answer) answer = strings.ToLower(answer) if answer == "y" || answer == "yes" { - if err = gcsql.DeleteStaff(delstaff); err != nil { + if err = gcsql.DeactivateStaff(delstaff); err != nil { fmt.Printf("Error deleting %q: %s", delstaff, err.Error()) gcutil.LogFatal().Str("staff", "delete").Err(err).Send() } diff --git a/cmd/gochan/movethread.go b/cmd/gochan/movethread.go index 51a16155..f4161558 100644 --- a/cmd/gochan/movethread.go +++ b/cmd/gochan/movethread.go @@ -40,10 +40,21 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R Msg("Error getting post from ID") return } - if post.ParentID != post.ID { + if !post.IsTopPost { + topPostID, err := post.TopPostID() + if err != nil { + serverutil.ServeError(writer, "Unable to get top post ID: "+err.Error(), wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) + gcutil.LogError(err). + Str("IP", gcutil.GetRealIP(request)). + Int("postid", post.ID). + Msg("Unable to get top post ID") + return + } serverutil.ServeError(writer, "You appear to be trying to move a post that is not the top post in the thread", wantsJSON, map[string]interface{}{ - "postid": checkedPosts[0], - "parentid": post.ParentID, + "postid": checkedPosts[0], + "toppost": topPostID, }) return } @@ -160,7 +171,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R }) return } - threadUploads, err := getThreadFiles(post) + threadUploads, err := gcsql.GetThreadFiles(post) if err != nil { gcutil.LogError(err).Int("postid", post.ID).Send() writer.WriteHeader(http.StatusInternalServerError) @@ -172,11 +183,11 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R for _, upload := range threadUploads { // move the upload itself tmpErr := moveFileIfExists( - path.Join(documentRoot, srcBoard.Dir, "src", upload.filename), - path.Join(documentRoot, destBoard.Dir, "src", upload.filename)) + path.Join(documentRoot, srcBoard.Dir, "src", upload.Filename), + path.Join(documentRoot, destBoard.Dir, "src", upload.Filename)) if tmpErr != nil { gcutil.LogError(err). - Str("filename", upload.filename). + Str("filename", upload.Filename). Str("srcBoard", srcBoard.Dir). Str("destBoard", destBoard.Dir). Msg("Unable to move file from source board to destination board") @@ -188,11 +199,11 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R // move the upload thumbnail if tmpErr = moveFileIfExists( - path.Join(documentRoot, srcBoard.Dir, "thumb", upload.thumbnail), - path.Join(documentRoot, destBoard.Dir, "thumb", upload.thumbnail), + path.Join(documentRoot, srcBoard.Dir, "thumb", upload.ThumbnailPath("upload")), + path.Join(documentRoot, destBoard.Dir, "thumb", upload.ThumbnailPath("upload")), ); tmpErr != nil { gcutil.LogError(err). - Str("thumbnail", upload.thumbnail). + Str("thumbnail", upload.ThumbnailPath("upload")). Str("srcBoard", srcBoard.Dir). Str("destBoard", destBoard.Dir). Msg("Unable to move thumbnail from source board to destination board") @@ -200,14 +211,14 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R err = tmpErr } } - if upload.postID == post.ID { + if upload.PostID == post.ID { // move the upload catalog thumbnail if tmpErr = moveFileIfExists( - path.Join(documentRoot, srcBoard.Dir, "thumb", upload.catalogThumbnail), - path.Join(documentRoot, destBoard.Dir, "thumb", upload.catalogThumbnail), + path.Join(documentRoot, srcBoard.Dir, "thumb", upload.ThumbnailPath("catalog")), + path.Join(documentRoot, destBoard.Dir, "thumb", upload.ThumbnailPath("catalog")), ); tmpErr != nil { gcutil.LogError(err). - Str("catalogThumbnail", upload.catalogThumbnail). + Str("catalogThumbnail", upload.ThumbnailPath("catalog")). Str("srcBoard", srcBoard.Dir). Str("destBoard", destBoard.Dir). Msg("Unable to move catalog thumbnail from source board to destination board") @@ -222,7 +233,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R Int("movedFileForPost", post.ID). Str("srcBoard", srcBoard.Dir). Str("destBoard", destBoard.Dir). - Str("filename", upload.filename).Send() + Str("filename", upload.Filename).Send() } } if err != nil { @@ -263,8 +274,10 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R return } - oldParentID := post.ParentID // hacky, this will likely be fixed when gcsql's handling of ParentID struct properties is changed - post.ParentID = 0 + // oldThreadID := post.ThreadID + + // oldParentID := post.ParentID // hacky, this will likely be fixed when gcsql's handling of ParentID struct properties is changed + // post.ParentID = 0 if err = building.BuildThreadPages(post); err != nil { gcutil.LogError(err).Int("postID", postID).Msg("Failed moved thread page") writer.WriteHeader(500) @@ -273,8 +286,8 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R }) return } - post.ParentID = oldParentID - if err = building.BuildBoardPages(&srcBoard); err != nil { + // post.ParentID = oldParentID + if err = building.BuildBoardPages(srcBoard); err != nil { gcutil.LogError(err).Int("srcBoardID", srcBoardID).Send() writer.WriteHeader(500) serverutil.ServeError(writer, "Failed building board page: "+err.Error(), wantsJSON, map[string]interface{}{ @@ -282,7 +295,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R }) return } - if err = building.BuildBoardPages(&destBoard); err != nil { + if err = building.BuildBoardPages(destBoard); err != nil { gcutil.LogError(err).Int("destBoardID", destBoardID).Send() writer.WriteHeader(500) serverutil.ServeError(writer, "Failed building destination board page: "+err.Error(), wantsJSON, map[string]interface{}{ @@ -312,40 +325,3 @@ func moveFileIfExists(src string, dest string) error { } return err } - -type postUpload struct { - filename string - thumbnail string - catalogThumbnail string - postID int -} - -// getThreadFiles gets a list of the files owned by posts in the thread, including thumbnails for convenience. -// TODO: move this to gcsql when the package is de-deprecated -func getThreadFiles(post *gcsql.Post) ([]postUpload, error) { - query := `SELECT filename,post_id FROM DBPREFIXfiles WHERE post_id IN ( - SELECT id FROM DBPREFIXposts WHERE thread_id = ( - SELECT thread_id FROM DBPREFIXposts WHERE id = ?)) AND filename != 'deleted'` - rows, err := gcsql.QuerySQL(query, post.ID) - if err != nil { - return nil, err - } - var uploads []postUpload - for rows.Next() { - var upload postUpload - if err = rows.Scan(&upload.filename, &upload.postID); err != nil { - return uploads, err - } - upload.thumbnail = gcutil.GetThumbnailPath("thumb", upload.filename) - - var parentID int - if parentID, err = gcsql.GetThreadIDZeroIfTopPost(upload.postID); err != nil { - return uploads, err - } - if parentID == 0 { - upload.catalogThumbnail = gcutil.GetThumbnailPath("catalog", upload.filename) - } - uploads = append(uploads, upload) - } - return uploads, nil -} diff --git a/pkg/building/boards.go b/pkg/building/boards.go index e4dcde9d..c5602671 100644 --- a/pkg/building/boards.go +++ b/pkg/building/boards.go @@ -36,13 +36,20 @@ func BuildBoardPages(board *gcsql.Board) error { return err } var currentPageFile *os.File - var threads []interface{} var threadPages [][]interface{} var stickiedThreads []interface{} var nonStickiedThreads []interface{} var opPosts []gcsql.Post - // Get all top level posts for the board. + threads, err := gcsql.GetThreadsWithBoardID(board.ID, true) + if err != nil { + gcutil.LogError(err). + Int("boardID", board.ID). + Msg("Failed getting OP posts") + return fmt.Errorf("error getting OP posts for /%s/: %s", board.Dir, err.Error()) + } + + // Get all top level posts for the board if opPosts, err = gcsql.GetTopPosts(board.ID); err != nil { gcutil.LogError(err). Str("boardDir", board.Dir). @@ -172,7 +179,7 @@ func BuildBoardPages(board *gcsql.Board) error { board.NumPages = len(threadPages) // Create array of page wrapper objects, and open the file. - pagesArr := make([]map[string]interface{}, board.NumPages) + var pagesArr boardCatalog catalogJSONFile, err := os.OpenFile(path.Join(criticalCfg.DocumentRoot, board.Dir, "catalog.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777) if err != nil { diff --git a/pkg/building/catalog.go b/pkg/building/catalog.go new file mode 100644 index 00000000..dab47a16 --- /dev/null +++ b/pkg/building/catalog.go @@ -0,0 +1,18 @@ +package building + +type catalogThreadData struct { + Replies int `json:"replies"` + Images int `json:"images"` + OmittedPosts int `json:"omitted_posts"` + OmittedImages int `json:"omitted_images"` + Sticky int `json:"sticky"` + Locked int `json:"locked"` + numPages int +} + +type catalogPage struct { + PageNum int `json:"page"` + Threads []catalogThreadData `json:"threads"` +} + +type boardCatalog []catalogPage diff --git a/pkg/config/config.go b/pkg/config/config.go index 07c665c8..32ecc478 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -316,6 +316,12 @@ type SiteConfig struct { AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."` } +type BoardCooldowns struct { + NewThread int `json:"threads"` + Reply int `json:"replies"` + ImageReply int `json:"images"` +} + // BoardConfig contains information about a specific board to be stored in /path/to/board/board.json // If a board doesn't have board.json, the site's default board config (with values set in gochan.json) will be used type BoardConfig struct { @@ -328,14 +334,21 @@ type BoardConfig struct { PostConfig UploadConfig - DateTimeFormat string `description:"The format used for dates. See here for more info."` - AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."` - UseCaptcha bool - CaptchaWidth int - CaptchaHeight int - CaptchaMinutesTimeout int - - EnableGeoIP bool + DateTimeFormat string `description:"The format used for dates. See here for more info."` + AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."` + UseCaptcha bool + CaptchaWidth int + CaptchaHeight int + CaptchaMinutesTimeout int + MaxBoardPages int + ShowPosterID bool + EnableSpoileredImages bool + EnableSpoileredThreads bool + Worksafe bool + ThreadPage int + Cooldowns BoardCooldowns + ThreadsPerPage int + EnableGeoIP bool } type BoardListConfig struct { diff --git a/pkg/gcplugin/lua_test.go b/pkg/gcplugin/lua_test.go index 0d47c6c9..c77a1dab 100644 --- a/pkg/gcplugin/lua_test.go +++ b/pkg/gcplugin/lua_test.go @@ -40,18 +40,18 @@ func TestVersionFunction(t *testing.T) { func TestStructPassing(t *testing.T) { initPluginTests() p := &gcsql.Post{ - Name: "Joe Poster", - Email: "joeposter@gmail.com", - MessageHTML: "Message test
", - MessageText: "Message text\n", + Name: "Joe Poster", + Email: "joeposter@gmail.com", + Message: "Message test
", + MessageRaw: "Message text\n", } lState.SetGlobal("post", luar.New(lState, p)) err := lState.DoString(structPassingStr) if err != nil { t.Fatal(err.Error()) } - t.Logf("Modified message text after Lua: %q", p.MessageText) - if p.MessageText != "Message modified by a plugin\n" || p.MessageHTML != "Message modified by a plugin
" { + t.Logf("Modified message text after Lua: %q", p.MessageRaw) + if p.MessageRaw != "Message modified by a plugin\n" || p.Message != "Message modified by a plugin
" { t.Fatal("message was not properly modified by plugin") } } diff --git a/pkg/gcsql/boards.go b/pkg/gcsql/boards.go index 205f409b..4ddc892a 100644 --- a/pkg/gcsql/boards.go +++ b/pkg/gcsql/boards.go @@ -1,114 +1,29 @@ package gcsql import ( - "database/sql" - "net/http" - "strconv" + "errors" + "path" + "time" + + "github.com/gochan-org/gochan/pkg/config" ) -// UpdateID takes a board struct and sets the database id according to the dir that is already set -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design. (Just bad design in general, try to avoid directly mutating state like this) -func (board *Board) UpdateID() error { - const query = `SELECT id FROM DBPREFIXboards WHERE dir = ?` - return QueryRowSQL(query, interfaceSlice(board.Dir), interfaceSlice(&board.ID)) -} +const ( + // selects all columns from DBPREFIXboards + selectBoardsBaseSQL = `SELECT + id, section_id, dir, navbar_position, title, subtitle, description, + max_file_size, default_style, locked, created_at, anonymous_name, force_anonymous, + autosage_after, no_images_after, max_message_length, allow_embeds, redirect_to_thread, + require_file, enable_catalog + FROM DBPREFIXboards ` +) -// ChangeFromRequest takes values from a HTTP request -func (board *Board) ChangeFromRequest(request *http.Request, dbUpdate bool) error { - if request.FormValue("docreate") != "" { - // prevent directory changes if the board already exists - board.Dir = request.FormValue("dir") - } - board.Title = request.FormValue("title") - board.Subtitle = request.FormValue("subtitle") - board.Description = request.FormValue("description") - board.Type, _ = strconv.Atoi(request.FormValue("boardtype")) - board.UploadType, _ = strconv.Atoi(request.FormValue("uploadtype")) - board.Section, _ = strconv.Atoi(request.FormValue("section")) - board.MaxFilesize, _ = strconv.Atoi(request.FormValue("maxfilesize")) - board.MaxPages, _ = strconv.Atoi(request.FormValue("maxpages")) - board.DefaultStyle = request.FormValue("defaultstyle") - board.Locked = len(request.Form["locked"]) > 0 - board.Anonymous = request.FormValue("anonname") - board.ForcedAnon = len(request.Form["forcedanon"]) > 0 - board.MaxAge, _ = strconv.Atoi(request.FormValue("maxage")) - board.AutosageAfter, _ = strconv.Atoi(request.FormValue("autosageafter")) - board.NoImagesAfter, _ = strconv.Atoi(request.FormValue("nouploadsafter")) - board.MaxMessageLength, _ = strconv.Atoi(request.FormValue("maxmessagelength")) - board.EmbedsAllowed = len(request.Form["embedsallowed"]) > 0 - board.RedirectToThread = len(request.Form["redirecttothread"]) > 0 - board.ShowID = len(request.Form["showid"]) > 0 - board.RequireFile = len(request.Form["requirefile"]) > 0 - board.EnableCatalog = len(request.Form["enablecatalog"]) > 0 - board.EnableSpoileredImages = len(request.Form["enablefilespoilers"]) > 0 - board.EnableSpoileredThreads = len(request.Form["enablethreadspoilers"]) > 0 - board.Worksafe = len(request.Form["worksafe"]) > 0 - board.Cooldowns.NewThread, _ = strconv.Atoi(request.FormValue("threadcooldown")) - board.Cooldowns.Reply, _ = strconv.Atoi(request.FormValue("replycooldown")) - board.Cooldowns.ImageReply, _ = strconv.Atoi(request.FormValue("imagecooldown")) - board.ThreadsPerPage, _ = strconv.Atoi(request.FormValue("threadsperpage")) - if !dbUpdate { - return nil - } - id, err := getBoardIDFromURI(board.Dir) - if err != nil { - return err - } - const query = `UPDATE DBPREFIXboards SET - section_id = ?,navbar_position = ?, - title = ?,subtitle = ?,description = ?,max_file_size = ?,default_style = ?, - locked = ?,anonymous_name = ?,force_anonymous = ?,autosage_after = ?,no_images_after = ?, - max_message_length = ?,allow_embeds = ?,redirect_to_thread = ?,require_file = ?, - enable_catalog = ? WHERE id = ?` - - _, err = ExecSQL(query, - board.Section, board.ListOrder, - board.Title, board.Subtitle, board.Description, board.MaxFilesize, board.DefaultStyle, - board.Locked, board.Anonymous, board.ForcedAnon, board.AutosageAfter, board.NoImagesAfter, - board.MaxMessageLength, board.EmbedsAllowed, board.RedirectToThread, board.RequireFile, - board.EnableCatalog, id) - return err -} - -// PopulateData gets the board data from the database, according to its id, and sets the respective properties. -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func (board *Board) PopulateData(id int) error { - const sql = "SELECT id, section_id, dir, navbar_position, title, subtitle, description, max_file_size, default_style, locked, created_at, anonymous_name, force_anonymous, autosage_after, no_images_after, max_message_length, allow_embeds, redirect_to_thread, require_file, enable_catalog FROM DBPREFIXboards WHERE id = ?" - return QueryRowSQL(sql, interfaceSlice(id), interfaceSlice(&board.ID, &board.Section, &board.Dir, &board.ListOrder, &board.Title, &board.Subtitle, &board.Description, &board.MaxFilesize, &board.DefaultStyle, &board.Locked, &board.CreatedOn, &board.Anonymous, &board.ForcedAnon, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength, &board.EmbedsAllowed, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog)) -} - -// Delete deletes the board from the database (if a row with the struct's ID exists) and -// returns any errors. It does not remove the board directory or its files -func (board *Board) Delete() error { - exists := DoesBoardExistByID(board.ID) - if !exists { - return ErrBoardDoesNotExist - } - if board.ID == 0 { - return ErrNilBoard - } - const delSql = `DELETE FROM DBPREFIXboards WHERE id = ?` - _, err := ExecSQL(delSql, board.ID) - return err -} - -// WordFilters gets an array of wordfilters that should be applied to new posts on -// this board -func (board *Board) WordFilters() ([]WordFilter, error) { - wfs, err := GetWordFilters() - if err != nil { - return wfs, err - } - var applicable []WordFilter - for _, filter := range wfs { - if filter.OnBoard(board.Dir) { - applicable = append(applicable, filter) - } - } - return applicable, nil -} +var ( + AllBoards []Board + ErrNilBoard = errors.New("board is nil") + ErrBoardExists = errors.New("board already exists") + ErrBoardDoesNotExist = errors.New("board does not exist") +) // DoesBoardExistByID returns a bool indicating whether a board with a given id exists func DoesBoardExistByID(ID int) bool { @@ -126,20 +41,21 @@ func DoesBoardExistByDir(dir string) bool { return count > 0 } -// GetAllBoards gets a list of all existing boards -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetAllBoards() ([]Board, error) { - const sql = `SELECT id, section_id, dir, navbar_position, title, subtitle, description, max_file_size, default_style, locked, created_at, anonymous_name, force_anonymous, autosage_after, no_images_after, max_message_length, allow_embeds, redirect_to_thread, require_file, enable_catalog FROM DBPREFIXboards - ORDER BY navbar_position ASC, dir ASC` - rows, err := QuerySQL(sql) +// getAllBoards gets a list of all existing boards +func getAllBoards() ([]Board, error) { + const query = selectBoardsBaseSQL + "ORDER BY navbar_position ASC, id ASC" + rows, err := QuerySQL(query) if err != nil { return nil, err } var boards []Board for rows.Next() { var board Board - err = rows.Scan(&board.ID, &board.Section, &board.Dir, &board.ListOrder, &board.Title, &board.Subtitle, &board.Description, &board.MaxFilesize, &board.DefaultStyle, &board.Locked, &board.CreatedOn, &board.Anonymous, &board.ForcedAnon, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength, &board.EmbedsAllowed, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog) + err = rows.Scan( + &board.ID, &board.SectionID, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle, &board.Description, + &board.MaxFilesize, &board.DefaultStyle, &board.Locked, &board.CreatedAt, &board.AnonymousName, &board.ForceAnonymous, + &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength, &board.AllowEmbeds, &board.RedirectToThread, + &board.RequireFile, &board.EnableCatalog) if err != nil { return nil, err } @@ -148,101 +64,161 @@ func GetAllBoards() ([]Board, error) { return boards, nil } +func GetBoardDir(id int) (string, error) { + const query = `SELECT dir FROM DBPREFIXboards WHERE id = ?` + var dir string + err := QueryRowSQL(query, interfaceSlice(id), interfaceSlice(&dir)) + return dir, err +} + // GetBoardFromID returns the board corresponding to a given id -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetBoardFromID(boardID int) (Board, error) { - var board Board - err := board.PopulateData(boardID) +func GetBoardFromID(id int) (*Board, error) { + const query = selectBoardsBaseSQL + "WHERE id = ?" + board := new(Board) + err := QueryRowSQL(query, interfaceSlice(id), interfaceSlice( + &board.ID, &board.SectionID, &board.URI, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle, + &board.Description, &board.MaxFilesize, &board.MaxThreads, &board.DefaultStyle, &board.Locked, + &board.CreatedAt, &board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter, + &board.MaxMessageLength, &board.MinMessageLength, &board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile, + &board.EnableCatalog, + )) return board, err } -// GetBoardFromPostID gets the boardURI that a given postid exists on -func GetBoardFromPostID(postID int) (boardURI string, wasFound bool, err error) { - const query = `SELECT board.uri FROM DBPREFIXboards as board - JOIN ( - SELECT threads.board_id FROM DBPREFIXthreads as threads - JOIN DBPREFIXposts as posts ON posts.thread_id = threads.id - WHERE posts.id = ? - ) as threads ON threads.board_id = board.id` - err = QueryRowSQL(query, interfaceSlice(postID), interfaceSlice(&boardURI)) - if err == sql.ErrNoRows { - return "", false, nil - } - return boardURI, true, err -} +// ResetBoardSectionArrays is run when the board list needs to be changed +// (board/section is added, deleted, etc) +func ResetBoardSectionArrays() error { + AllBoards = nil + AllSections = nil -func getBoardIDFromURI(URI string) (id int, err error) { - const sql = `SELECT id FROM DBPREFIXboards WHERE uri = ?` - err = QueryRowSQL(sql, interfaceSlice(URI), interfaceSlice(&id)) - return id, err -} - -// CreateDefaultBoardIfNoneExist creates a default board if no boards exist yet -func CreateDefaultBoardIfNoneExist() error { - const sqlStr = `SELECT COUNT(id) FROM DBPREFIXboards` - var count int - QueryRowSQL(sqlStr, interfaceSlice(), interfaceSlice(&count)) - if count > 0 { - return nil - } - defaultSectionID, err := GetOrCreateDefaultSectionID() - if err != nil && err != sql.ErrNoRows { + allBoardsArr, err := getAllBoards() + if err != nil { return err } - board := Board{} - board.SetDefaults("", "", "") - board.Section = defaultSectionID - if err = CreateBoard(&board); err != nil { + AllBoards = append(AllBoards, allBoardsArr...) + + allSectionsArr, err := getAllSections() + if err != nil { return err } + AllSections = append(AllSections, allSectionsArr...) + return nil +} + +// NewBoardSimple creates a new board in the database given the directory, title, subtitle, and description. +// Generic values are used for the other columns to be optionally changed later +func NewBoardSimple(dir string, title string, subtitle string, description string, appendToAllBoards bool) (*Board, error) { + sectionID, err := getOrCreateDefaultSectionID() + if err != nil { + return nil, err + } + board := &Board{ + SectionID: sectionID, + URI: dir, + Dir: dir, + NavbarPosition: 3, + Title: title, + Subtitle: subtitle, + Description: description, + MaxFilesize: 15000, + MaxThreads: 300, + DefaultStyle: config.GetBoardConfig("").DefaultStyle, + Locked: false, + AnonymousName: "Anonymous", + ForceAnonymous: false, + AutosageAfter: 500, + NoImagesAfter: -1, + MaxMessageLength: 1500, + MinMessageLength: 0, + AllowEmbeds: false, + RedirectToThread: false, + RequireFile: false, + EnableCatalog: true, + } + // board.ShowID = false + // board.EnableSpoileredImages = true + // board.Worksafe = true + // board.ThreadsPerPage = 20 + // board.Cooldowns = BoardCooldowns{ + // NewThread: 30, + // Reply: 7, + // ImageReply: 7, + // } + return board, CreateBoard(board, appendToAllBoards) +} + +// CreateBoard inserts a new board into the database, using the fields from the given Board pointer. +// It sets board.ID and board.CreatedAt if it is successfull +func CreateBoard(board *Board, appendToAllBoards bool) error { + const sqlINSERT = `INSERT INTO DBPREFIXboards + (section_id, uri, dir, navbar_position, title, suttitle, + description, max_file_size, max_threads, default_style, locked, + anonymous_name, force_anonymous, autosage_after, no_images_after, max_message_length, + min_message_length, allow_embeds, redirect_to_thread, require_file, enable_catalog) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)` + if board == nil { + return ErrNilBoard + } + id, err := getNextFreeID("DBPREFIXboards") + if err != nil { + return err + } + _, err = ExecSQL(sqlINSERT, + &board.SectionID, &board.URI, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle, + &board.Description, &board.MaxFilesize, &board.MaxThreads, &board.DefaultStyle, &board.Locked, + &board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength, + &board.MinMessageLength, &board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog) + if err != nil { + return err + } + board.ID = id + board.CreatedAt = time.Now() + if appendToAllBoards { + AllBoards = append(AllBoards, *board) + } return nil } -// CreateBoard creates this board in the database if it doesnt exist already, also sets ID to correct value -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func CreateBoard(values *Board) error { - exists := DoesBoardExistByDir(values.Dir) - if exists { - return ErrBoardExists +// createDefaultBoardIfNoneExist creates a default board if no boards exist yet +func createDefaultBoardIfNoneExist() error { + const query = `SELECT COUNT(id) FROM DBPREFIXboards` + var count int + QueryRowSQL(query, interfaceSlice(), interfaceSlice(&count)) + if count > 0 { + return nil } - const maxThreads = 300 - const sqlINSERT = `INSERT INTO DBPREFIXboards ( - navbar_position, dir, uri, title, subtitle, description, max_file_size, max_threads, default_style, locked, anonymous_name, force_anonymous, autosage_after, no_images_after, max_message_length, min_message_length, allow_embeds, redirect_to_thread, require_file, enable_catalog, section_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - const sqlSELECT = "SELECT id FROM DBPREFIXboards WHERE dir = ?" - //Excecuted in two steps this way because last row id functions arent thread safe, dir and uri is unique - if values == nil { - return ErrNilBoard - } - _, err := ExecSQL(sqlINSERT, - values.ListOrder, values.Dir, values.Dir, values.Title, values.Subtitle, - values.Description, values.MaxFilesize, maxThreads, values.DefaultStyle, - values.Locked, values.Anonymous, values.ForcedAnon, values.AutosageAfter, - values.NoImagesAfter, values.MaxMessageLength, 1, values.EmbedsAllowed, - values.RedirectToThread, values.RequireFile, values.EnableCatalog, values.Section) - if err != nil { - return err - } - return QueryRowSQL(sqlSELECT, interfaceSlice(values.Dir), interfaceSlice(&values.ID)) + // create a default generic /test/ board + _, err := NewBoardSimple("test", "Testing Board", "Board for testing stuff", "Board for testing stuff", true) + return err } -// GetBoardUris gets a list of all existing board URIs -func GetBoardUris() (URIS []string, err error) { - const sql = `SELECT uri FROM DBPREFIXboards` - rows, err := QuerySQL(sql) - if err != nil { - return nil, err - } - var uris []string - for rows.Next() { - var uri string - if err = rows.Scan(&uri); err != nil { - return nil, err - } - uris = append(uris, uri) - } - return uris, nil +func getBoardIDFromURI(uri string) (int, error) { + const sql = `SELECT id FROM DBPREFIXboards WHERE uri = ?` + var id int + err := QueryRowSQL(sql, interfaceSlice(uri), interfaceSlice(&id)) + return id, err +} + +// WebPath returns a string that represents the file's path as accessible by a browser +// fileType should be "boardPage", "threadPage", "upload", or "thumb" +func (board *Board) WebPath(fileName, fileType string) string { + var filePath string + systemCritical := config.GetSystemCriticalConfig() + + switch fileType { + case "": + fallthrough + case "boardPage": + filePath = path.Join(systemCritical.WebRoot, board.Dir, fileName) + case "threadPage": + filePath = path.Join(systemCritical.WebRoot, board.Dir, "res", fileName) + case "upload": + filePath = path.Join(systemCritical.WebRoot, board.Dir, "src", fileName) + case "thumb": + fallthrough + case "thumbnail": + filePath = path.Join(systemCritical.WebRoot, board.Dir, "thumb", fileName) + } + return filePath } diff --git a/pkg/gcsql/boards_test.go b/pkg/gcsql/boards_test.go deleted file mode 100644 index 4c07c89f..00000000 --- a/pkg/gcsql/boards_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package gcsql - -import ( - "testing" -) - -func TestCreateBoard(t *testing.T) { - err := CreateDefaultBoardIfNoneExist() - if err != nil { - t.Fatalf("Failed creating default board if none exists: %s", err.Error()) - } -} diff --git a/pkg/gcsql/connect.go b/pkg/gcsql/connect.go index af56557b..bea8b4a3 100644 --- a/pkg/gcsql/connect.go +++ b/pkg/gcsql/connect.go @@ -1,16 +1,12 @@ package gcsql import ( - "fmt" "os" "regexp" "strings" - - "github.com/gochan-org/gochan/pkg/gcutil" ) var ( - gcdb *GCDB tcpHostIsolator = regexp.MustCompile(`\b(tcp\()?([^\(\)]*)\b`) ) @@ -21,17 +17,6 @@ func ConnectToDB(host, driver, dbName, username, password, prefix string) error return err } -func initDB(initFile string) error { - filePath := gcutil.FindResource(initFile, - "/usr/local/share/gochan/"+initFile, - "/usr/share/gochan/"+initFile) - if filePath == "" { - return fmt.Errorf( - "SQL database initialization file (%s) missing. Please reinstall gochan", initFile) - } - return RunSQLFile(filePath) -} - // RunSQLFile cuts a given sql file into individual statements and runs it. func RunSQLFile(path string) error { sqlBytes, err := os.ReadFile(path) diff --git a/pkg/gcsql/database.go b/pkg/gcsql/database.go index 7465274c..b7216e82 100644 --- a/pkg/gcsql/database.go +++ b/pkg/gcsql/database.go @@ -10,6 +10,8 @@ import ( ) const ( + // GochanVersionKeyConstant is the key value used in the version table of the database to store and receive the (database) version of base gochan + gochanVersionKeyConstant = "gochan" UnsupportedSQLVersionMsg = `Received syntax error while preparing a SQL string. This means that either there is a bug in gochan's code (hopefully not) or that you are using an unsupported MySQL/PostgreSQL version. Before reporting an error, make sure that you are using the up to date version of your selected SQL server. @@ -19,12 +21,13 @@ const ( sqlite3ConnStr = "file:%s?_auth&_auth_user=%s&_auth_pass=%s&_auth_crypt=sha1" ) +var gcdb *GCDB + type GCDB struct { db *sql.DB connStr string driver string replacer *strings.Replacer - // nilTimestamp string } func (db *GCDB) ConnectionString() string { @@ -39,10 +42,6 @@ func (db *GCDB) SQLDriver() string { return db.driver } -/* func (db *GCDB) NilSQLTimestamp() string { - return db.nilTimestamp -} */ - func (db *GCDB) Close() error { if db.db != nil { return db.db.Close() @@ -112,7 +111,7 @@ Example: var intVal int var stringVal string err := db.QueryRowSQL("SELECT intval,stringval FROM table WHERE id = ?", - []interface{}{&id}, + []interface{}{id}, []interface{}{&intVal, &stringVal}) */ func (db *GCDB) QueryRowSQL(query string, values, out []interface{}) error { diff --git a/pkg/gcsql/filebans.go b/pkg/gcsql/filebans.go deleted file mode 100644 index 7bf7e9af..00000000 --- a/pkg/gcsql/filebans.go +++ /dev/null @@ -1,121 +0,0 @@ -package gcsql - -import "database/sql" - -// GetFilenameBans returns an array of filename bans. If matchFilename is a blank string, it returns all of them. -// If exactMatch is true, it returns an array of bans that = matchFilename, otherwise it treats matchFilename -// as a SQL wildcard query using LIKE -func GetFilenameBans(matchFilename string, exactMatch bool) ([]FilenameBan, error) { - query := `SELECT id,board_id,staff_id,staff_note,issued_at,filename,is_regex FROM DBPREFIXfilename_ban` - var rows *sql.Rows - var err error - if matchFilename != "" { - if exactMatch { - rows, err = QuerySQL(query+" WHERE filename = ?", matchFilename) - } else { - rows, err = QuerySQL(query+" WHERE filename LIKE ?", matchFilename) - } - } else { - rows, err = QuerySQL(query) - } - if err != nil { - return nil, err - } - var fnBans []FilenameBan - for rows.Next() { - var ban FilenameBan - if err = rows.Scan( - &ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, - &ban.IssuedAt, &ban.Filename, &ban.IsRegex, - ); err != nil { - return fnBans, err - } - fnBans = append(fnBans, ban) - } - return fnBans, err -} - -// CreateFileNameBan creates a new ban on a filename. If boards is an empty string -// or the resulting query = nil, the ban is global, whether or not allBoards is set -func CreateFileNameBan(fileName string, isRegex bool, staffName string, staffNote, boardURI string) error { - const sql = `INSERT INTO DBPREFIXfilename_ban (board_id, staff_id, staff_note, filename, is_regex) VALUES(?,?,?,?,?)` - staffID, err := getStaffID(staffName) - if err != nil { - return err - } - var boardID *int = nil - if boardURI != "" { - boardID = getBoardIDFromURIOrNil(boardURI) - } - _, err = ExecSQL(sql, boardID, staffID, staffNote, fileName, isRegex) - return err -} - -// DeleteFilenameBanByID deletes the ban, given the id column value -func DeleteFilenameBanByID(id int) error { - _, err := ExecSQL("DELETE FROM DBPREFIXfilename_ban WHERE id = ?", id) - return err -} - -func GetFileChecksumBans(matchChecksum string) ([]FileBan, error) { - query := `SELECT id,board_id,staff_id,staff_note,issued_at,checksum FROM DBPREFIXfile_ban` - if matchChecksum != "" { - query += " WHERE checksum = ?" - } - var rows *sql.Rows - var err error - if matchChecksum == "" { - rows, err = QuerySQL(query) - } else { - rows, err = QuerySQL(query, matchChecksum) - } - if err != nil { - return nil, err - } - var fileBans []FileBan - for rows.Next() { - var fileBan FileBan - if err = rows.Scan( - &fileBan.ID, &fileBan.BoardID, &fileBan.StaffID, &fileBan.StaffNote, - &fileBan.IssuedAt, &fileBan.Checksum, - ); err != nil { - return fileBans, err - } - fileBans = append(fileBans, fileBan) - } - return fileBans, nil -} - -// CreateFileBan creates a new ban on a file. If boards = nil, the ban is global. -func CreateFileBan(fileChecksum, staffName string, staffNote, boardURI string) error { - const sql = `INSERT INTO DBPREFIXfile_ban (board_id, staff_id, staff_note, checksum) VALUES(?,?,?,?)` - staffID, err := getStaffID(staffName) - if err != nil { - return err - } - boardID := getBoardIDFromURIOrNil(boardURI) - _, err = ExecSQL(sql, boardID, staffID, staffNote, fileChecksum) - return err -} - -// DeleteFileBanByID deletes the ban, given the id column value -func DeleteFileBanByID(id int) error { - _, err := ExecSQL("DELETE FROM DBPREFIXfile_ban WHERE id = ?", id) - return err -} - -func checkFilenameBan(filename string) (*FilenameBan, error) { - const sql = `SELECT id, board_id, staff_id, staff_note, issued_at, filename, is_regex - FROM DBPREFIXfilename_ban WHERE filename = ?` - var ban = new(FilenameBan) - err := QueryRowSQL(sql, interfaceSlice(filename), interfaceSlice(&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Filename, &ban.IsRegex)) - return ban, err -} - -func checkFileBan(checksum string) (*FileBan, error) { - const sql = `SELECT id, board_id, staff_id, staff_note, issued_at, checksum - FROM DBPREFIXfile_ban WHERE checksum = ?` - var ban = new(FileBan) - err := QueryRowSQL(sql, interfaceSlice(checksum), interfaceSlice(&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Checksum)) - return ban, err -} diff --git a/pkg/gcsql/initdb_test.go b/pkg/gcsql/initdb_test.go deleted file mode 100644 index 82b784a0..00000000 --- a/pkg/gcsql/initdb_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package gcsql - -import ( - "log" - "os" - "testing" - - "github.com/gochan-org/gochan/pkg/config" - - _ "github.com/go-sql-driver/mysql" - _ "github.com/lib/pq" - _ "github.com/mattn/go-sqlite3" -) - -func populateTestDB() error { - var err error - if err = initDB("../../initdb_sqlite3.sql"); err != nil { - return err - } - err = CreateDefaultBoardIfNoneExist() - if err != nil { - return err - } - err = CreateDefaultAdminIfNoStaff() - return err -} - -func TestDatabaseVersion(t *testing.T) { - dbVersion, dbFlag, err := GetCompleteDatabaseVersion() - if err != nil { - t.Fatalf(err.Error()) - } - if dbVersion < 1 { - t.Fatalf("Database version should be > 0 (got %d)", dbVersion) - } - if dbFlag != DBClean && dbFlag != DBUpToDate { - t.Fatalf("Got an unexpected DB flag (%#x), should be either clean or up to date", dbFlag) - } -} - -func TestMain(m *testing.M) { - log.SetFlags(0) - config.InitConfig("3.2.0") - - os.Remove("./testdata/gochantest.db") - var err error - gcdb, err = Open("./testdata/gochantest.db", "sqlite3", "gochan", "gochan", "gochan", "gc_") - if err != nil { - panic(err.Error()) - } - defer gcdb.Close() - - if err = populateTestDB(); err != nil { - panic(err.Error()) - } - - exitCode := m.Run() - - os.Exit(exitCode) -} diff --git a/pkg/gcsql/posts.go b/pkg/gcsql/posts.go index e294e0e5..cdef05e6 100644 --- a/pkg/gcsql/posts.go +++ b/pkg/gcsql/posts.go @@ -2,211 +2,147 @@ package gcsql import ( "database/sql" - "path" - "time" + "errors" + "fmt" + "html/template" "github.com/gochan-org/gochan/pkg/config" - "github.com/gochan-org/gochan/pkg/gcutil" ) -// SinceLastPost returns the number of seconds since the given IP address created a post -// (used for checking against the new thread/new reply cooldown) -func SinceLastPost(postIP string) (int, error) { - const sql = `SELECT MAX(created_on) FROM DBPREFIXposts WHERE ip = ?` - var when time.Time - err := QueryRowSQL(sql, interfaceSlice(postIP), interfaceSlice(&when)) - if err != nil { - return -1, err - } - return int(time.Since(when).Seconds()), nil -} +const ( + // should be appended when selecting info from DBPREFIXboards, requires a post ID + boardFromPostIdSuffixSQL = ` WHERE id = ( + SELECT board_id FROM DBPREFIXthreads WHERE id = ( + SELECT thread_id FROM DBPREFIXposts WHERE id = ?))` + selectPostsBaseSQL = `SELECT + id, thread_id, is_top_post, ip, created_on, name, tripcode, is_role_signature, + email, subject, message, message_raw, password, deleted_at, is_deleted, banned_message + FROM DBPREFIXposts ` +) -// InsertPost insersts prepared post object into the SQL table so that it can be rendered -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func InsertPost(post *Post, bump bool) error { - const sql = `INSERT INTO DBPREFIXposts (id, thread_id, name, tripcode, is_role_signature, email, subject, ip, is_top_post, message, message_raw, banned_message, password) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - isNewThread := post.ParentID == 0 - var threadID int - var err error - if isNewThread { - threadID, err = createThread(post.BoardID, post.Locked, post.Stickied, post.Autosage, false) - } else { - threadID, err = getThreadID(post.ParentID) - } - if err != nil { - return err - } +var ( + ErrNotTopPost = errors.New("not the top post in the thread") + ErrPostDoesNotExist = errors.New("post does not exist") + ErrPostDeleted = errors.New("post is deleted") +) - //Retrieves next free ID, explicitly inserts it, keeps retrying until succesfull insert or until a non-pk error is encountered. - //This is done because mysql doesnt support RETURNING and both LAST_INSERT_ID() and last_row_id() are not thread-safe - isPrimaryKeyError := true - for isPrimaryKeyError { - nextFreeID, err := getNextFreeID("DBPREFIXposts") - if err != nil { - return err - } - _, err = ExecSQL(sql, nextFreeID, threadID, post.Name, post.Tripcode, false, post.Email, post.Subject, post.IP, isNewThread, string(post.MessageHTML), post.MessageText, "", post.Password) - - isPrimaryKeyError, err = errFilterDuplicatePrimaryKey(err) - if err != nil { - return err - } - if !isPrimaryKeyError { - post.ID = nextFreeID - } - } - - if post.Filename != "" { - err = appendFile(post.ID, post.FilenameOriginal, post.Filename, post.FileChecksum, post.Filesize, false, post.ImageW, post.ImageH, post.ThumbW, post.ThumbH) - } - if err != nil { - return err - } - if bump { - return bumpThread(threadID) - } - return nil -} - -// GetReplyCount gets the total amount non-deleted of replies in a thread -func GetReplyCount(postID int) (int, error) { - const sql = `SELECT COUNT(posts.id) FROM DBPREFIXposts as posts - JOIN ( - SELECT threads.id FROM DBPREFIXthreads as threads - JOIN DBPREFIXposts as posts - ON posts.thread_id = threads.id - WHERE posts.id = ? - ) as thread - ON posts.thread_id = thread.id - WHERE posts.is_deleted = FALSE` - var count int - err := QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&count)) - return count, err -} - -// GetReplyFileCount gets the amount of files non-deleted posted in total in a thread -func GetReplyFileCount(postID int) (int, error) { - const sql = `SELECT COUNT(files.id) from DBPREFIXfiles as files - JOIN (SELECT posts.id FROM DBPREFIXposts as posts - JOIN ( - SELECT threads.id FROM DBPREFIXthreads as threads - JOIN DBPREFIXposts as posts - ON posts.thread_id = threads.id - WHERE posts.id = ? - ) as thread - ON posts.thread_id = thread.id - WHERE posts.is_deleted = FALSE) as posts - ON posts.id = files.post_id` - var count int - err := QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&count)) - return count, err -} - -const selectPostsQuery = `SELECT - DBPREFIXposts.id, - thread_id AS threadid, - (SELECT id FROM DBPREFIXposts WHERE is_top_post = TRUE AND thread_id = threadid LIMIT 1), - (SELECT board_id FROM DBPREFIXthreads WHERE id = DBPREFIXposts.thread_id) as board_id, - ip,created_on,name,tripcode,email,subject,message,message_raw,password, - - COALESCE(files.filename,''), - COALESCE(files.original_filename,''), - COALESCE(files.file_size, 0), - COALESCE(files.checksum, ''), - COALESCE(files.thumbnail_width, 0), - COALESCE(files.thumbnail_height, 0), - COALESCE(files.width, 0), - COALESCE(files.height, 0) - FROM DBPREFIXposts LEFT OUTER JOIN DBPREFIXfiles AS files ON files.post_id = DBPREFIXposts.id` - -// GetPostFromID gets the post from the database with a matching ID, -// optionally requiring it to not be deleted func GetPostFromID(id int, onlyNotDeleted bool) (*Post, error) { - sql := selectPostsQuery + ` WHERE DBPREFIXposts.id = ?` + query := selectPostsBaseSQL + "WHERE id = ?" if onlyNotDeleted { - sql += " AND is_deleted = 0" + query += " AND is_deleted = 0" } - var drop int post := new(Post) - err := QueryRowSQL(sql, []interface{}{id}, []interface{}{ - &post.ID, &drop, &post.ParentID, - &post.BoardID, - &post.IP, &post.Timestamp, &post.Name, &post.Tripcode, &post.Email, &post.Subject, &post.MessageHTML, &post.MessageText, &post.Password, - &post.Filename, &post.FilenameOriginal, &post.Filesize, &post.FileChecksum, &post.ThumbW, &post.ThumbH, &post.ImageW, &post.ImageH, - }) - if err != nil { - return nil, err + post.ID = id + err := QueryRowSQL(query, interfaceSlice(id), interfaceSlice( + &post.ID, &post.ThreadID, &post.IsTopPost, &post.IP, &post.CreatedOn, &post.Name, &post.Tripcode, &post.IsRoleSignature, + &post.Email, &post.Subject, &post.Message, &post.MessageRaw, &post.Password, &post.DeletedAt, &post.IsDeleted, &post.BannedMessage, + )) + if err == sql.ErrNoRows { + return nil, ErrPostDoesNotExist + } return post, err } -// GetPostsFromIP gets the posts from the database with a matching IP address, specifying -// optionally requiring them to not be deleted -func GetPostsFromIP(ip string, limit int, onlyNotDeleted bool) ([]Post, error) { - sql := selectPostsQuery + ` WHERE DBPREFIXposts.ip = ?` - if onlyNotDeleted { - sql += " AND is_deleted = 0" - } - - sql += " ORDER BY id DESC LIMIT ?" - rows, err := QuerySQL(sql, ip, limit) - if err != nil { - return nil, err - } - var posts []Post - for rows.Next() { - var post Post - var drop int - if err = rows.Scan( - &post.ID, &drop, &post.ParentID, - &post.BoardID, - &post.IP, &post.Timestamp, &post.Name, &post.Tripcode, &post.Email, &post.Subject, &post.MessageHTML, &post.MessageText, &post.Password, - &post.Filename, &post.FilenameOriginal, &post.Filesize, &post.FileChecksum, &post.ThumbW, &post.ThumbH, &post.ImageW, &post.ImageH, - ); err != nil { - return nil, err - } - posts = append(posts, post) - } - return posts, nil +// GetPostPassword returns the password checksum of the post with the given ID +func GetPostPassword(id int) (string, error) { + const query = `SELECT password FROM DBPREFIXposts WHERE id = ?` + var passwordChecksum string + err := QueryRowSQL(query, interfaceSlice(id), interfaceSlice(&passwordChecksum)) + return passwordChecksum, err } -// GetFilePaths returns an array of absolute paths to uploaded files and thumbnails associated -// with this post, and any errors that occurred -func (p *Post) GetFilePaths() ([]string, error) { - boardDir, err := p.GetBoardDir() +// UpdateContents updates the email, subject, and message text of the post +func (p *Post) UpdateContents(email string, subject string, message template.HTML, messageRaw string) error { + const sqlUpdate = `UPDATE DBPREFIXposts SET email = ?, subject = ?, message = ?, message_raw = ? WHERE ID = ?` + _, err := ExecSQL(sqlUpdate, email, subject, message, messageRaw) + if err != nil { + return err + } + p.Email = email + p.Subject = subject + p.Message = message + p.MessageRaw = messageRaw + return nil +} + +func (p *Post) GetBoardID() (int, error) { + const query = `SELECT board_id FROM DBPREFIXthreads where id = ?` + var boardID int + err := QueryRowSQL(query, interfaceSlice(p.ThreadID), interfaceSlice(&boardID)) + if errors.Is(err, sql.ErrNoRows) { + err = ErrBoardDoesNotExist + } + return boardID, err +} + +func (p *Post) GetBoardDir() (string, error) { + const query = "SELECT dir FROM DBPREFIXboards" + boardFromPostIdSuffixSQL + var dir string + err := QueryRowSQL(query, interfaceSlice(p.ID), interfaceSlice(&dir)) + return dir, err +} + +func (p *Post) GetBoard() (*Board, error) { + const query = selectBoardsBaseSQL + boardFromPostIdSuffixSQL + + board := new(Board) + err := QueryRowSQL(query, interfaceSlice(), interfaceSlice( + &board.ID, &board.SectionID, &board.URI, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle, + &board.Description, &board.MaxFilesize, &board.MaxThreads, &board.DefaultStyle, &board.Locked, + &board.CreatedAt, &board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter, + &board.MaxMessageLength, &board.MinMessageLength, &board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile, + &board.EnableCatalog, + )) + return board, err +} + +// ChangeBoardID updates the post with the new board ID if it is a top post. It returns an error if it is not +// an OP or if ChangeThreadBoardID returned any errors +func (p *Post) ChangeBoardID(newBoardID int) error { + if !p.IsTopPost { + return ErrNotTopPost + } + return ChangeThreadBoardID(p.ThreadID, newBoardID) +} + +// TopPostID returns the OP post ID of the thread that p is in +func (p *Post) TopPostID() (int, error) { + if p.IsTopPost { + return p.ID, nil + } + const query = `SELECT id FROM DBPREFIXposts WHERE thread_id = ? and is_top_post = TRUE ORDER BY id ASC LIMIT 1` + var topPostID int + err := QueryRowSQL(query, interfaceSlice(p.ThreadID), interfaceSlice(&topPostID)) + return topPostID, err +} + +// GetTopPost returns the OP of the thread that p is in +func (p *Post) GetTopPost() (*Post, error) { + opID, err := p.TopPostID() if err != nil { return nil, err } - const filenameSQL = `SELECT filename FROM DBPREFIXfiles WHERE post_id = ?` - rows, err := QuerySQL(filenameSQL, p.ID) - var paths []string - if err == sql.ErrNoRows { - return paths, nil - } else if err != nil { - return nil, err + return GetPostFromID(opID, true) +} + +// GetPostUpload returns the upload info associated with the file as well as any errors encountered. +// If the file has no uploads, then *Upload is nil. If the file was removed from the post, then Filename +// and OriginalFilename = "deleted" +func (p *Post) GetUpload() (*Upload, error) { + const query = `SELECT + id, post_id, file_order, original_filename, filename, checksum, + file_size, is_spoilered, thumbnail_width, thumbnail_height, width, height + FROM DBPREFIXfiles WHERE post_id = ?` + upload := new(Upload) + err := QueryRowSQL(query, interfaceSlice(p.ID), interfaceSlice( + &upload.ID, &upload.PostID, &upload.FileOrder, &upload.OriginalFilename, &upload.Filename, &upload.Checksum, + &upload.FileSize, &upload.IsSpoilered, &upload.ThumbnailWidth, &upload.ThumbnailHeight, &upload.Width, &upload.Height, + )) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil } - documentRoot := config.GetSystemCriticalConfig().DocumentRoot - for rows.Next() { - var filename string - if err = rows.Scan(&filename); err != nil { - return paths, err - } - _, filenameBase, fileExt := gcutil.GetFileParts(filename) - thumbExt := fileExt - if thumbExt == "gif" || thumbExt == "webm" || thumbExt == "mp4" { - thumbExt = "jpg" - } - paths = append(paths, - path.Join(documentRoot, boardDir, "/src/", filenameBase+"."+fileExt), - path.Join(documentRoot, boardDir, "/thumb/", filenameBase+"t."+thumbExt), // thumbnail path - ) - if p.ParentID == 0 { - paths = append(paths, path.Join(documentRoot, boardDir, "/thumb/", filenameBase+"c."+thumbExt)) // catalog thumbnail path - } - } - return paths, nil + return upload, err } // UnlinkUploads disassociates the post with any uploads in DBPREFIXfiles @@ -223,3 +159,29 @@ func (p *Post) UnlinkUploads(leaveDeletedBox bool) error { _, err := ExecSQL(sqlStr, p.ID) return err } + +// Delete sets the post as deleted and sets the deleted_at timestamp to the current time +func (p *Post) Delete() error { + if p.IsTopPost { + return deleteThread(p.ThreadID) + } + const deleteSQL = `UPDATE DBPREFIXposts SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?` + _, err := ExecSQL(deleteSQL, p.ID) + return err +} + +func (p *Post) WebPath() string { + webRoot := config.GetSystemCriticalConfig().WebRoot + var threadID, opID, boardID int + var boardDir string + const query = `SELECT thread_id as threadid, + (SELECT id from DBPREFIXposts WHERE thread_id = threadid AND is_top_post = TRUE LIMIT 1) as op, + (SELECT board_id from DBPREFIXthreads WHERE id = threadid) AS boardid, + (SELECT dir FROM DBPREFIXboards WHERE id = boardid) AS dir + FROM DBPREFIXposts WHERE id = ?` + err := QueryRowSQL(query, interfaceSlice(p.ID), interfaceSlice(&threadID, &opID, &boardID, &boardDir)) + if err != nil { + return webRoot + } + return webRoot + boardDir + fmt.Sprintf("/res/%d.html#%d", opID, p.ID) +} diff --git a/pkg/gcsql/posts_test.go b/pkg/gcsql/posts_test.go deleted file mode 100644 index a942aa8e..00000000 --- a/pkg/gcsql/posts_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package gcsql - -import ( - "testing" -) - -func TestInsertPosts(t *testing.T) { - err := InsertPost(&Post{ - ParentID: 0, - BoardID: 1, - Name: "Joe Poster", - Tripcode: "Blah", - Email: "any@example.com", - Subject: "First thread", - MessageHTML: "First post best post", - MessageText: "First post best post", - Password: "12345", - Filename: "12345.png", - FilenameOriginal: "somefile.png", - FileChecksum: "abcd1234", - FileExt: ".png", - Filesize: 1000, - ImageW: 2000, - ImageH: 2000, - ThumbW: 250, - ThumbH: 250, - IP: "192.168.56.1", - }, false) - if err != nil { - t.Fatal(err.Error()) - } -} diff --git a/pkg/gcsql/postsretrievalqueries.go b/pkg/gcsql/postsretrievalqueries.go deleted file mode 100644 index 1140c94a..00000000 --- a/pkg/gcsql/postsretrievalqueries.go +++ /dev/null @@ -1,313 +0,0 @@ -package gcsql - -import ( - "database/sql" - "errors" - "html/template" - "strconv" -) - -/* -Left join singlefiles on recentposts where recentposts.selfid = singlefiles.post_id -Coalesce filenames to "" (if filename = null -> "" else filename) -*/ -var abstractSelectPosts = ` -SELECT - recentposts.selfid AS id, - recentposts.toppostid AS parentid, - recentposts.boardid, - recentposts.name, - recentposts.tripcode, - recentposts.email, - recentposts.subject, - recentposts.message, - recentposts.message_raw, - recentposts.password, - COALESCE(singlefiles.filename, '') as filename, - COALESCE(singlefiles.original_filename, '') as original_filename, - COALESCE(singlefiles.checksum, ''), - COALESCE(singlefiles.file_size, 0), - COALESCE(singlefiles.width, 0), - COALESCE(singlefiles.height, 0), - COALESCE(singlefiles.thumbnail_width, 0), - COALESCE(singlefiles.thumbnail_height, 0), - recentposts.ip, - recentposts.created_on, - recentposts.anchored, - recentposts.last_bump, - recentposts.stickied, - recentposts.locked -FROM - (SELECT - posts.id AS selfid, - COALESCE(NULLIF(topposts.id, posts.id), 0) AS toppostid, - boards.id AS boardid, - posts.name, - posts.ip, - posts.tripcode, - posts.message, - posts.email, - posts.subject, - posts.message_raw, - posts.password, - posts.created_on, - posts.is_top_post, - threads.anchored, - threads.last_bump, - threads.stickied, - threads.locked - FROM - DBPREFIXposts AS posts - JOIN DBPREFIXthreads AS threads - ON threads.id = posts.thread_id - JOIN DBPREFIXposts AS topposts - ON threads.id = topposts.thread_id - JOIN DBPREFIXboards AS boards - ON threads.board_id = boards.id - WHERE - topposts.is_top_post = TRUE AND posts.is_deleted = FALSE - - ) as recentposts -LEFT JOIN - (SELECT files.* - FROM DBPREFIXfiles as files - JOIN - (SELECT post_id, min(file_order) as file_order - FROM DBPREFIXfiles - GROUP BY post_id) as topfiles - ON files.post_id = topfiles.post_id AND files.file_order = topfiles.file_order - ) AS singlefiles - - ON recentposts.selfid = singlefiles.post_id` - -// getPostsExcecution excecutes a given variation on abstractSelectPosts with parameters and loads the result into an array of posts -func getPostsExcecution(sql string, arguments ...interface{}) ([]Post, error) { - rows, err := QuerySQL(sql, arguments...) - if err != nil { - return nil, err - } - var posts []Post - for rows.Next() { - post := new(Post) - var messageHTML string - if err = rows.Scan(&post.ID, &post.ParentID, &post.BoardID, &post.Name, &post.Tripcode, &post.Email, - &post.Subject, &messageHTML, &post.MessageText, &post.Password, &post.Filename, - &post.FilenameOriginal, &post.FileChecksum, &post.Filesize, &post.ImageW, &post.ImageH, - &post.ThumbW, &post.ThumbH, &post.IP, &post.Timestamp, &post.Autosage, &post.Bumped, &post.Stickied, &post.Locked, - ); err != nil { - return nil, err - } - post.MessageHTML = template.HTML(messageHTML) - post.FileExt = "placeholder" - posts = append(posts, *post) - } - return posts, nil -} - -var onlyTopPosts = abstractSelectPosts + "\nWHERE recentposts.is_top_post AND recentposts.boardid = ?" -var sortedTopPosts = onlyTopPosts + "\nORDER BY recentposts.last_bump DESC" - -// GetTopPostsNoSort gets the thread ops for a given board. -// Results are unsorted -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetTopPostsNoSort(boardID int) (posts []Post, err error) { - return getPostsExcecution(onlyTopPosts, boardID) -} - -// GetTopPosts gets the thread ops for a given board. -// newestFirst sorts the ops by the newest first if true, by newest last if false -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetTopPosts(boardID int) (posts []Post, err error) { - return getPostsExcecution(sortedTopPosts, boardID) -} - -var repliesToX = abstractSelectPosts + "\nWHERE recentposts.toppostid = ?" -var oldestRepliesFirst = repliesToX + "\nORDER BY recentposts.created_on ASC" -var newestFirstLimited = repliesToX + "\nORDER BY recentposts.created_on DESC\nLIMIT ?" - -// GetExistingReplies gets all the reply posts to a given thread, ordered by oldest first. -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetExistingReplies(topPost int) (posts []Post, err error) { - return getPostsExcecution(oldestRepliesFirst, topPost) -} - -// GetExistingRepliesLimitedRev gets N amount of reply posts to a given thread, ordered by newest first. -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetExistingRepliesLimitedRev(topPost, limit int) (posts []Post, err error) { - return getPostsExcecution(newestFirstLimited, topPost, limit) -} - -//Toppost: where a post with a given id has this as their top post - -// GetSpecificTopPost gets the information for the top post for a given id. -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetSpecificTopPost(ID int) (Post, error) { - const topPostIDQuery = `SELECT posts.id from DBPREFIXposts as posts - JOIN ( - SELECT threads.id from DBPREFIXthreads as threads - JOIN DBPREFIXposts as posts - ON posts.thread_id = threads.id - WHERE posts.id = ? - ) as thread - ON posts.thread_id = thread.id - WHERE posts.is_top_post` - //get top post of item with given id - var FoundID int - err := QueryRowSQL(topPostIDQuery, interfaceSlice(ID), interfaceSlice(&FoundID)) - if err != nil { - return Post{}, err - } - return GetSpecificPost(FoundID, false) -} - -// GetSpecificPostByString gets a specific post for a given string id. -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetSpecificPostByString(ID string, onlyNotDeleted bool) (Post, error) { - return getSpecificPostStringDecorated(ID, onlyNotDeleted) -} - -// GetSpecificPost gets a specific post for a given id. -// returns SQL.ErrNoRows if no post could be found -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetSpecificPost(ID int, onlyNotDeleted bool) (Post, error) { - return getSpecificPostStringDecorated(strconv.Itoa(ID), onlyNotDeleted) -} - -var specificPostSQL = abstractSelectPosts + "\nWHERE recentposts.selfid = ?" -var specificPostSQLNotDeleted = specificPostSQL + "\nAND recentposts.is_deleted = FALSE" - -func getSpecificPostStringDecorated(ID string, onlyNotDeleted bool) (Post, error) { - var sql string - if onlyNotDeleted { - sql = specificPostSQL - } else { - sql = specificPostSQLNotDeleted - } - posts, err := getPostsExcecution(sql, ID) - if err != nil { - return Post{}, err - } - if len(posts) == 0 { - return Post{}, errors.New("Could not find a post with the ID: " + ID) - } - return posts[0], nil -} - -// getRecentPostsInternal returns the most recent N posts, on a specific board if specified, only with files if specified -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -// TODO: rework so it uses all features/better sql -// get recent posts -func getRecentPostsInternal(amount int, onlyWithFile bool, boardID int, onSpecificBoard bool) ([]RecentPost, error) { - // recentposts = join all non-deleted posts with the post id of their thread and the board it belongs on, sort by date and grab top x posts - // singlefiles = the top file per post id - - // Left join singlefiles on recentposts where recentposts.selfid = singlefiles.post_id - // Coalesce filenames to "" (if filename = null -> "" else filename) - - // Query might benefit from [filter on posts with at least one file -> ] filter N most recent -> manually loop N results for file/board/parentthreadid - recentQueryStr := `SELECT - recentposts.selfid AS id, - recentposts.toppostid AS parentid, - recentposts.boardname, - recentposts.boardid, - recentposts.ip, - recentposts.name, - recentposts.tripcode, - recentposts.message, - COALESCE(singlefiles.filename, '') as filename, - COALESCE(singlefiles.thumbnail_width, 0) as thumb_w, - COALESCE(singlefiles.thumbnail_height, 0) as thumb_h - FROM - (SELECT - posts.id AS selfid, - topposts.id AS toppostid, - boards.dir AS boardname, - boards.id AS boardid, - posts.ip, - posts.name, - posts.tripcode, - posts.message, - posts.email, - posts.created_on - FROM - DBPREFIXposts AS posts - JOIN DBPREFIXthreads AS threads - ON threads.id = posts.thread_id - JOIN DBPREFIXposts AS topposts - ON threads.id = topposts.thread_id - JOIN DBPREFIXboards AS boards - ON threads.board_id = boards.id - WHERE - topposts.is_top_post = TRUE AND posts.is_deleted = FALSE - ) as recentposts - LEFT JOIN - (SELECT files.post_id, filename, files.thumbnail_width, files.thumbnail_height - FROM DBPREFIXfiles as files - JOIN - (SELECT post_id, min(file_order) as file_order - FROM DBPREFIXfiles - GROUP BY post_id) as topfiles - ON files.post_id = topfiles.post_id AND files.file_order = topfiles.file_order - ) AS singlefiles - - ON recentposts.selfid = singlefiles.post_id` - var rows *sql.Rows - var err error - - if onlyWithFile && onSpecificBoard { - recentQueryStr += "\n" + `WHERE singlefiles.filename IS NOT NULL AND recentposts.boardid = ? - ORDER BY recentposts.created_on DESC LIMIT ?` - rows, err = QuerySQL(recentQueryStr, boardID, amount) - } - if onlyWithFile && !onSpecificBoard { - recentQueryStr += "\n" + `WHERE singlefiles.filename IS NOT NULL - ORDER BY recentposts.created_on DESC LIMIT ?` - rows, err = QuerySQL(recentQueryStr, amount) - } - if !onlyWithFile && onSpecificBoard { - recentQueryStr += "\n" + `WHERE recentposts.boardid = ? - ORDER BY recentposts.created_on DESC LIMIT ?` - rows, err = QuerySQL(recentQueryStr, boardID, amount) - } - if !onlyWithFile && !onSpecificBoard { - recentQueryStr += "\nORDER BY recentposts.created_on DESC LIMIT ?" - rows, err = QuerySQL(recentQueryStr, amount) - } - - if err != nil { - return nil, err - } - defer rows.Close() - var recentPostsArr []RecentPost - - for rows.Next() { - recentPost := new(RecentPost) - var formattedHTML template.HTML - if err = rows.Scan( - &recentPost.PostID, &recentPost.ParentID, &recentPost.BoardName, &recentPost.BoardID, - &recentPost.IP, &recentPost.Name, &recentPost.Tripcode, &formattedHTML, &recentPost.Filename, - &recentPost.ThumbW, &recentPost.ThumbH, - ); err != nil { - return nil, err - } - recentPost.Message = formattedHTML - recentPostsArr = append(recentPostsArr, *recentPost) - } - - return recentPostsArr, nil -} - -// GetRecentPostsGlobal returns the global N most recent posts from the database. -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetRecentPostsGlobal(amount int, onlyWithFile bool) ([]RecentPost, error) { - return getRecentPostsInternal(amount, onlyWithFile, 0, false) -} diff --git a/pkg/gcsql/databasecheck.go b/pkg/gcsql/provisioning.go similarity index 82% rename from pkg/gcsql/databasecheck.go rename to pkg/gcsql/provisioning.go index e7c4abfa..a9286c44 100644 --- a/pkg/gcsql/databasecheck.go +++ b/pkg/gcsql/provisioning.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/gochan-org/gochan/pkg/config" + "github.com/gochan-org/gochan/pkg/gcutil" ) const ( @@ -27,6 +28,16 @@ var ( ErrInvalidDBVersion = errors.New("invalid version flag returned by GetCompleteDatabaseVersion()") ) +func initDB(initFile string) error { + filePath := gcutil.FindResource(initFile, + "/usr/local/share/gochan/"+initFile, + "/usr/share/gochan/"+initFile) + if filePath == "" { + return fmt.Errorf("missing SQL database initialization file (%s), please reinstall gochan", initFile) + } + return RunSQLFile(filePath) +} + // GetCompleteDatabaseVersion checks the database for any versions and errors that may exist. // If a version is found, execute the version check. Otherwise check for deprecated info // If no deprecated info is found, check if any databases exist prefixed with config.DBprefix @@ -37,7 +48,7 @@ func GetCompleteDatabaseVersion() (dbVersion, dbFlag int, err error) { return 0, 0, err } if versionTableExists { - databaseVersion, versionError := getDatabaseVersion(GochanVersionKeyConstant) + databaseVersion, versionError := getDatabaseVersion(gochanVersionKeyConstant) if versionError != nil { return 0, 0, ErrInvalidVersion } @@ -102,12 +113,12 @@ func CheckAndInitializeDatabase(dbType string) error { func buildNewDatabase(dbType string) error { var err error if err = initDB("initdb_" + dbType + ".sql"); err != nil { - return err + return errors.New("database initialization failed: " + err.Error()) } - if err = CreateDefaultBoardIfNoneExist(); err != nil { - return errors.New("Failed creating default board if non already exists: " + err.Error()) + if err = createDefaultBoardIfNoneExist(); err != nil { + return errors.New("failed creating default board if non already exists: " + err.Error()) } - if err = CreateDefaultAdminIfNoStaff(); err != nil { + if err = createDefaultAdminIfNoStaff(); err != nil { return errors.New("failed creating default admin account: " + err.Error()) } return nil diff --git a/pkg/gcsql/queries.go b/pkg/gcsql/queries.go deleted file mode 100644 index e43eab3e..00000000 --- a/pkg/gcsql/queries.go +++ /dev/null @@ -1,613 +0,0 @@ -package gcsql - -import ( - "database/sql" - "errors" - "html/template" - "regexp" - "strings" - "time" - - "github.com/gochan-org/gochan/pkg/config" - "github.com/gochan-org/gochan/pkg/gcutil" -) - -// GochanVersionKeyConstant is the key value used in the version table of the database to store and receive the (database) version of base gochan -const GochanVersionKeyConstant = "gochan" - -var ( - ErrNilBoard = errors.New("board is nil") - ErrBoardExists = errors.New("board already exists") - ErrBoardDoesNotExist = errors.New("board does not exist") - ErrThreadExists = errors.New("thread already exists") - ErrThreadDoesNotExist = errors.New("thread does not exist") -) - -// GetAllNondeletedMessageRaw gets all the raw message texts from the database, saved per id -func GetAllNondeletedMessageRaw() ([]MessagePostContainer, error) { - const sql = `SELECT posts.id, posts.message, posts.message_raw, DBPREFIXboards.dir as dir - FROM DBPREFIXposts as posts, DBPREFIXboards - where posts.is_deleted = FALSE` - rows, err := QuerySQL(sql) - if err != nil { - return nil, err - } - var messages []MessagePostContainer - for rows.Next() { - var message MessagePostContainer - var formattedHTML template.HTML - if err = rows.Scan(&message.ID, &formattedHTML, &message.MessageRaw, &message.Board); err != nil { - return nil, err - } - message.Message = template.HTML(formattedHTML) - messages = append(messages, message) - } - return messages, nil -} - -// SetFormattedInDatabase sets all the non-raw text for a given array of items. -func SetFormattedInDatabase(messages []MessagePostContainer) error { - const sql = `UPDATE DBPREFIXposts - SET message = ? - WHERE id = ?` - stmt, err := PrepareSQL(sql, nil) - if err != nil { - return err - } - defer stmt.Close() - - for _, message := range messages { - if _, err = stmt.Exec(string(message.Message), message.ID); err != nil { - return err - } - } - return err -} - -// CreateReport inserts a new report into the database and returns a Report pointer and any -// errors encountered -func CreateReport(postID int, ip string, reason string) (*Report, error) { - currentTime := time.Now() - sql := `INSERT INTO DBPREFIXreports (post_id, ip, reason, is_cleared) VALUES(?, ?, ?, FALSE)` - result, err := ExecSQL(sql, postID, ip, reason) - if err != nil { - return nil, err - } - reportID, err := result.LastInsertId() - if err != nil { - return nil, err - } - - sql = `INSERT INTO DBPREFIXreports_audit (report_id, timestamp, is_cleared) VALUES(?, ?, FALSE)` - if _, err = ExecSQL(sql, reportID, currentTime); err != nil { - return nil, err - } - return &Report{ - ID: int(reportID), - HandledByStaffID: -1, - PostID: postID, - IP: ip, - Reason: reason, - IsCleared: false, - }, nil -} - -// ClearReport dismisses the report with the given `id`. If `block` is true, future reports of the post will -// be ignored. It returns a boolean value representing whether or not any reports matched, -// as well as any errors encountered -func ClearReport(id int, staffID int, block bool) (bool, error) { - sql := `UPDATE DBPREFIXreports SET is_cleared = ?, handled_by_staff_id = ? WHERE id = ?` - isCleared := 1 - if block { - isCleared = 2 - } - result, err := ExecSQL(sql, isCleared, staffID, id) - if err != nil { - return false, err - } - affected, err := result.RowsAffected() - if err != nil { - return affected > 0, err - } - sql = `UPDATE DBPREFIXreports_audit SET is_cleared = ?, handled_by_staff_id = ? WHERE report_id = ?` - _, err = ExecSQL(sql, isCleared, staffID, id) - return affected > 0, err -} - -// CheckPostReports checks to see if the given post ID has already been reported, and if a report of the post has been -// dismissed with prejudice (so that more reports of that post can't be made) -func CheckPostReports(postID int, reason string) (bool, bool, error) { - sql := `SELECT COUNT(*), MAX(is_cleared) FROM DBPREFIXreports - WHERE post_id = ? AND (reason = ? OR is_cleared = 2)` - var num int - var isCleared interface{} - err := QueryRowSQL(sql, interfaceSlice(postID, reason), interfaceSlice(&num, &isCleared)) - isClearedInt, _ := isCleared.(int64) - return num > 0, isClearedInt == 2, err -} - -// GetReports returns a Report array and any errors encountered. If `includeCleared` is true, -// the array will include reports that have already been dismissed -func GetReports(includeCleared bool) ([]Report, error) { - sql := `SELECT id,handled_by_staff_id,post_id,ip,reason,is_cleared FROM DBPREFIXreports` - if !includeCleared { - sql += ` WHERE is_cleared = FALSE` - } - rows, err := QuerySQL(sql) - if err != nil { - return nil, err - } - var reports []Report - for rows.Next() { - var report Report - var staffID interface{} - err = rows.Scan(&report.ID, &staffID, &report.PostID, &report.IP, &report.Reason, &report.IsCleared) - if err != nil { - return nil, err - } - - staffID64, _ := (staffID.(int64)) - report.HandledByStaffID = int(staffID64) - reports = append(reports, report) - } - return reports, nil -} - -// PermanentlyRemoveDeletedPosts removes all posts and files marked as deleted from the database -func PermanentlyRemoveDeletedPosts() error { - const sql1 = `DELETE FROM DBPREFIXposts WHERE is_deleted` - const sql2 = `DELETE FROM DBPREFIXthreads WHERE is_deleted` - _, err := ExecSQL(sql1) - if err != nil { - return err - } - _, err = ExecSQL(sql2) - return err -} - -// OptimizeDatabase peforms a database optimisation -func OptimizeDatabase() error { - tableRows, tablesErr := QuerySQL("SHOW TABLES") - if tablesErr != nil { - return tablesErr - } - for tableRows.Next() { - var table string - tableRows.Scan(&table) - if _, err := ExecSQL("OPTIMIZE TABLE " + table); err != nil { - return err - } - } - return nil -} - -func (p *Post) ChangeBoardID(newBoardID int) error { - err := ChangeThreadBoardID(p.ID, newBoardID) - if err != nil { - return err - } - p.BoardID = newBoardID - return nil -} - -// ChangeThreadBoardID updates the given thread's post ID and the destination board ID -func ChangeThreadBoardID(postID int, newBoardID int) error { - if !DoesBoardExistByID(newBoardID) { - return ErrBoardDoesNotExist - } - var threadRowID int - err := QueryRowSQL(`SELECT thread_id FROM DBPREFIXposts WHERE id = ?`, - interfaceSlice(postID), - interfaceSlice(&threadRowID)) - if err != nil { - return err - } - if threadRowID < 1 { - return ErrThreadDoesNotExist - } - _, err = ExecSQL(`UPDATE DBPREFIXthreads SET board_id = ? WHERE id = ?`, newBoardID, threadRowID) - return err -} - -// ChangeThreadBoardByURI updates a thread's board ID, given the thread's post ID and -// the destination board's uri -func ChangeThreadBoardByURI(postID int, uri string) error { - boardID, err := getBoardIDFromURI(uri) - if err != nil { - return err - } - return ChangeThreadBoardID(postID, boardID) -} - -func getBoardIDFromURIOrNil(URI string) *int { - ID, err := getBoardIDFromURI(URI) - if err != nil { - return nil - } - return &ID -} - -// CreateUserNameBan creates a new ban on a username. If boards = nil, the ban is global. -func CreateUserNameBan(userName string, isRegex bool, staffName string, permaban bool, staffNote, boardURI string) error { - const sql = `INSERT INTO DBPREFIXusername_ban (board_id, staff_id, staff_note, username, is_regex) VALUES board_id = ?, staff_id = ?, staff_note = ?, username = ?, is_regex = ?` - staffID, err := getStaffID(staffName) - if err != nil { - return err - } - boardID := getBoardIDFromURIOrNil(boardURI) - _, err = ExecSQL(sql, boardID, staffID, staffNote, userName, isRegex) - return err -} - -// CreateUserBan creates either a full ip ban, or an ip ban for threads only, for a given IP. -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func CreateUserBan(IP string, threadBan bool, staffName, boardURI string, expires time.Time, permaban bool, - staffNote, message string, canAppeal bool, appealAt time.Time) error { - const sql = `INSERT INTO DBPREFIXip_ban (board_id, staff_id, staff_note, is_thread_ban, ip, appeal_at, expires_at, permanent, message, can_appeal, issued_at, copy_posted_text, is_active) - VALUES (?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP,'OLD SYSTEM BAN, NO TEXT AVAILABLE',TRUE)` - staffID, err := getStaffID(staffName) - if err != nil { - return err - } - boardID := getBoardIDFromURIOrNil(boardURI) - _, err = ExecSQL(sql, boardID, staffID, staffNote, threadBan, IP, appealAt, expires, permaban, message, canAppeal) - return err -} - -// GetAllAccouncements gets all announcements, newest first -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetAllAccouncements() ([]Announcement, error) { - const sql = `SELECT s.username, a.timestamp, a.subject, a.message FROM DBPREFIXannouncements AS a - JOIN DBPREFIXstaff AS s - ON a.staff_id = s.id - ORDER BY a.id DESC` - rows, err := QuerySQL(sql) - if err != nil { - return nil, err - } - announcements := []Announcement{} - for rows.Next() { - var announcement Announcement - err = rows.Scan(&announcement.Poster, &announcement.Timestamp, &announcement.Subject, &announcement.Message) - if err != nil { - return nil, err - } - announcements = append(announcements, announcement) - } - return announcements, nil -} - -// GetAllBans gets a list of all bans -// Warning, currently only gets ip bans, not other types of bans, as the ban functionality needs a major revamp anyway -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetAllBans() ([]BanInfo, error) { - const sql = `SELECT - ban.id, - ban.ip, - COALESCE(board.title, '') as boardname, - staff.username as staff, - ban.issued_at, - ban.expires_at, - ban.permanent, - ban.message, - ban.staff_note, - ban.appeal_at, - ban.can_appeal -FROM DBPREFIXip_ban as ban -JOIN DBPREFIXstaff as staff -ON ban.staff_id = staff.id -JOIN DBPREFIXboards as board -ON ban.board_id = board.id` - rows, err := QuerySQL(sql) - if err != nil { - return nil, err - } - var bans []BanInfo - for rows.Next() { - var ban BanInfo - err = rows.Scan(&ban.ID, &ban.IP, &ban.Boards, &ban.Staff, &ban.Timestamp, &ban.Expires, &ban.Permaban, &ban.Reason, &ban.StaffNote, &ban.AppealAt, &ban.CanAppeal) - if err != nil { - return nil, err - } - bans = append(bans, ban) - } - return bans, nil -} - -// CheckBan returns banentry if a ban was found or a sql.ErrNoRows if not banned -// name, filename and checksum may be empty strings and will be treated as not requested if done so -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func CheckBan(ip, name, filename, checksum string) (*BanInfo, error) { - ban := new(BanInfo) - ipban, err1 := checkIPBan(ip) - err1NoRows := (err1 == sql.ErrNoRows) - _, err2 := checkFileBan(checksum) - err2NoRows := (err2 == sql.ErrNoRows) - _, err3 := checkFilenameBan(filename) - err3NoRows := (err3 == sql.ErrNoRows) - _, err4 := checkUsernameBan(name) - err4NoRows := (err4 == sql.ErrNoRows) - - if err1NoRows && err2NoRows && err3NoRows && err4NoRows { - return nil, sql.ErrNoRows - } - - if err1NoRows { - return nil, err1 - } - if err2NoRows { - return nil, err2 - } - if err3NoRows { - return nil, err3 - } - if err4NoRows { - return nil, err4 - } - - if ipban != nil { - ban.ID = 0 - ban.IP = string(ipban.IP) - staff, _ := getStaffByID(ipban.StaffID) - ban.Staff = staff.Username - ban.Timestamp = ipban.IssuedAt - ban.Expires = ipban.ExpiresAt - ban.Permaban = ipban.Permanent - ban.Reason = ipban.Message - ban.StaffNote = ipban.StaffNote - ban.AppealAt = ipban.AppealAt - ban.CanAppeal = ipban.CanAppeal - return ban, nil - } - - // TODO implement other types of bans or refactor banning code - return nil, gcutil.ErrNotImplemented -} - -func checkIPBan(ip string) (*IPBan, error) { - const sql = `SELECT id, staff_id, board_id, banned_for_post_id, copy_post_text, is_thread_ban, is_active, ip, issued_at, appeal_at, expires_at, permanent, staff_note, message, can_appeal - FROM DBPREFIXip_ban WHERE ip = ?` - var ban = new(IPBan) - var formattedHTMLcopyposttest template.HTML - err := QueryRowSQL(sql, interfaceSlice(ip), interfaceSlice(&ban.ID, &ban.StaffID, &ban.BoardID, &ban.BannedForPostID, &formattedHTMLcopyposttest, &ban.IsThreadBan, &ban.IsActive, &ban.IP, &ban.IssuedAt, &ban.AppealAt, &ban.ExpiresAt, &ban.Permanent, &ban.StaffNote, &ban.Message, &ban.CanAppeal)) - ban.CopyPostText = formattedHTMLcopyposttest - return ban, err -} - -func checkUsernameBan(name string) (*UsernameBan, error) { - const sql = `SELECT id, board_id, staff_id, staff_note, issued_at, username, is_regex - FROM DBPREFIXusername_ban WHERE username = ?` - var ban = new(UsernameBan) - err := QueryRowSQL(sql, interfaceSlice(name), interfaceSlice(&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Username, &ban.IsRegex)) - return ban, err -} - -// GetMaxMessageLength returns the max message length on a board -func GetMaxMessageLength(boardID int) (length int, err error) { - const sql = `SELECT max_message_length FROM DBPREFIXboards - WHERE id = ?` - err = QueryRowSQL(sql, interfaceSlice(boardID), interfaceSlice(&length)) - return length, err -} - -// GetEmbedsAllowed returns if embeds are allowed on a given board -func GetEmbedsAllowed(boardID int) (allowed bool, err error) { - const sql = `SELECT allow_embeds FROM DBPREFIXboards - WHERE id = ?` - err = QueryRowSQL(sql, interfaceSlice(boardID), interfaceSlice(&allowed)) - return allowed, err -} - -// AddBanAppeal adds a given appeal to a given ban -func AddBanAppeal(banID uint, message string) error { - // copy old to audit - const sql1 = ` - INSERT INTO DBPREFIXip_ban_appeals_audit (appeal_id, staff_id, appeal_text, staff_response, is_denied) - SELECT id, staff_id, appeal_text, staff_response, is_denied - FROM DBPREFIXip_ban_appeals - WHERE DBPREFIXip_ban_appeals.ip_ban_id = ?` - // update old values to new values - const sql2 = `UPDATE DBPREFIXip_ban_appeals SET appeal_text = ? WHERE ip_ban_id = ?` - _, err := ExecSQL(sql1, banID) - if err != nil { - return err - } - _, err = ExecSQL(sql2, message, banID) - return err -} - -// CreateDefaultAdminIfNoStaff creates a new default admin account if no accounts exist -func CreateDefaultAdminIfNoStaff() error { - const sql = `SELECT COUNT(id) FROM DBPREFIXstaff` - var count int - QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&count)) - if count > 0 { - return nil - } - _, err := createUser("admin", gcutil.BcryptSum("password"), 3) - return err -} - -// CreateWordFilter inserts the given wordfilter data into the database and returns a pointer to a new WordFilter struct -func CreateWordFilter(from string, to string, isRegex bool, boards []string, staffID int, staffNote string) (*WordFilter, error) { - var err error - if isRegex { - _, err = regexp.Compile(from) - if err != nil { - return nil, err - } - } - - _, err = ExecSQL(`INSERT INTO DBPREFIXwordfilters - (board_dirs,staff_id,staff_note,search,is_regex,change_to) - VALUES(?,?,?,?,?,?)`, strings.Join(boards, ","), staffID, staffNote, from, isRegex, to) - if err != nil { - return nil, err - } - return &WordFilter{ - BoardDirs: boards, - StaffID: staffID, - StaffNote: staffNote, - IssuedAt: time.Now(), - Search: from, - IsRegex: isRegex, - ChangeTo: to, - }, err -} - -// GetWordFilters gets a list of wordfilters from the database and returns an array of them and any errors -// encountered -func GetWordFilters() ([]WordFilter, error) { - var wfs []WordFilter - query := `SELECT id,board_dirs,staff_id,staff_note,issued_at,search,is_regex,change_to FROM DBPREFIXwordfilters` - rows, err := QuerySQL(query) - if err != nil { - return wfs, err - } - defer rows.Close() - for rows.Next() { - var dirsStr string - var wf WordFilter - if err = rows.Scan( - &wf.ID, - &dirsStr, - &wf.StaffID, - &wf.StaffNote, - &wf.IssuedAt, - &wf.Search, - &wf.IsRegex, - &wf.ChangeTo, - ); err != nil { - return wfs, err - } - wf.BoardDirs = strings.Split(dirsStr, ",") - wfs = append(wfs, wf) - } - return wfs, err -} - -func GetBoardWordFilters(board string) ([]WordFilter, error) { - wfs, err := GetWordFilters() - if err != nil { - return wfs, err - } - var boardFilters []WordFilter - for _, wf := range wfs { - if wf.OnBoard(board) { - boardFilters = append(boardFilters, wf) - } - } - return boardFilters, nil -} - -// BoardString returns a string representing the boards that this wordfilter applies to, -// or "*" if the filter should be applied to posts on all boards -func (wf *WordFilter) BoardsString() string { - if len(wf.BoardDirs) == 0 { - return "*" - } - return strings.Join(wf.BoardDirs, ",") -} - -func (wf *WordFilter) OnBoard(dir string) bool { - if dir == "*" { - return true - } - for _, board := range wf.BoardDirs { - if board == "*" || dir == board { - return true - } - } - return false -} - -func (wf *WordFilter) StaffName() string { - staff, err := getStaffByID(wf.StaffID) - if err != nil { - return "?" - } - return staff.Username -} - -// Apply runs the current wordfilter on the given string, without checking the board or (re)building the post -// It returns an error if it is a regular expression and regexp.Compile failed to parse it -func (wf *WordFilter) Apply(message string) (string, error) { - if wf.IsRegex { - re, err := regexp.Compile(wf.Search) - if err != nil { - return message, err - } - message = re.ReplaceAllString(message, wf.ChangeTo) - } else { - message = strings.Replace(message, wf.Search, wf.ChangeTo, -1) - } - return message, nil -} - -// getDatabaseVersion gets the version of the database, or an error if none or multiple exist -func getDatabaseVersion(componentKey string) (int, error) { - const sql = `SELECT version FROM DBPREFIXdatabase_version WHERE component = ?` - var version int - err := QueryRowSQL(sql, []interface{}{componentKey}, []interface{}{&version}) - if err != nil { - return 0, err - } - return version, err -} - -func getNextFreeID(tableName string) (ID int, err error) { - var sql = `SELECT COALESCE(MAX(id), 0) + 1 FROM ` + tableName - err = QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&ID)) - return ID, err -} - -func doesTableExist(tableName string) (bool, error) { - var existQuery string - - switch config.GetSystemCriticalConfig().DBtype { - case "mysql": - fallthrough - case "postgresql": - existQuery = `SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ?` - case "sqlite3": - existQuery = `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?` - default: - return false, ErrUnsupportedDB - } - - var count int - err := QueryRowSQL(existQuery, []interface{}{config.GetSystemCriticalConfig().DBprefix + tableName}, []interface{}{&count}) - if err != nil { - return false, err - } - return count == 1, nil -} - -// doesGochanPrefixTableExist returns true if any table with a gochan prefix was found. -// Returns false if the prefix is an empty string -func doesGochanPrefixTableExist() (bool, error) { - systemCritical := config.GetSystemCriticalConfig() - if systemCritical.DBprefix == "" { - return false, nil - } - var prefixTableExist string - switch systemCritical.DBtype { - case "mysql": - fallthrough - case "postgresql": - prefixTableExist = `SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'DBPREFIX%'` - case "sqlite3": - prefixTableExist = `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name LIKE 'DBPREFIX%'` - } - - var count int - err := QueryRowSQL(prefixTableExist, []interface{}{}, []interface{}{&count}) - if err != nil { - return false, err - } - return count > 0, nil -} diff --git a/pkg/gcsql/sections.go b/pkg/gcsql/sections.go index 877eb6cd..cc2612de 100644 --- a/pkg/gcsql/sections.go +++ b/pkg/gcsql/sections.go @@ -1,25 +1,22 @@ package gcsql -import ( - "database/sql" - "errors" -) +import "database/sql" var ( - ErrCannotDeleteOnlySection = errors.New("cannot delete the only remaining section") + AllSections []Section ) -// GetAllSections gets a list of all existing sections -func GetAllSections() ([]BoardSection, error) { - const sql = `SELECT id, name, abbreviation, position, hidden FROM DBPREFIXsections ORDER BY position ASC, name ASC` - rows, err := QuerySQL(sql) +// getAllSections gets a list of all existing sections +func getAllSections() ([]Section, error) { + const query = `SELECT id, name, abbreviation, position, hidden FROM DBPREFIXsections ORDER BY position ASC, name ASC` + rows, err := QuerySQL(query) if err != nil { return nil, err } - var sections []BoardSection + var sections []Section for rows.Next() { - var section BoardSection - err = rows.Scan(§ion.ID, §ion.Name, §ion.Abbreviation, §ion.ListOrder, §ion.Hidden) + var section Section + err = rows.Scan(§ion.ID, §ion.Name, §ion.Abbreviation, §ion.Position, §ion.Hidden) if err != nil { return nil, err } @@ -28,95 +25,69 @@ func GetAllSections() ([]BoardSection, error) { return sections, nil } -// GetAllSectionsOrCreateDefault gets all sections in the database, creates default if none exist -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetAllSectionsOrCreateDefault() ([]BoardSection, error) { - _, err := GetOrCreateDefaultSectionID() - if err != nil { - return nil, err - } - return GetAllSections() -} - func getNextSectionListOrder() (int, error) { - const sql = `SELECT COALESCE(MAX(position) + 1, 0) FROM DBPREFIXsections` - var ID int - err := QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&ID)) - return ID, err + const query = `SELECT COALESCE(MAX(position) + 1, 0) FROM DBPREFIXsections` + var id int + err := QueryRowSQL(query, interfaceSlice(), interfaceSlice(&id)) + return id, err } -// GetOrCreateDefaultSectionID creates the default section if it does not exist yet, returns default section ID if it exists -func GetOrCreateDefaultSectionID() (sectionID int, err error) { - const SQL = `SELECT id FROM DBPREFIXsections WHERE name = 'Main'` - var ID int - err = QueryRowSQL(SQL, interfaceSlice(), interfaceSlice(&ID)) +// getOrCreateDefaultSectionID creates the default section if no sections have been created yet, +// returns default section ID if it exists +func getOrCreateDefaultSectionID() (sectionID int, err error) { + const query = `SELECT id FROM DBPREFIXsections WHERE name = 'Main'` + var id int + err = QueryRowSQL(query, interfaceSlice(), interfaceSlice(&id)) if err == sql.ErrNoRows { - //create it - ID, err := getNextSectionListOrder() - if err != nil { + var section *Section + if section, err = NewSection("Main", "main", false, -1); err != nil { return 0, err } - section := BoardSection{Name: "Main", Abbreviation: "Main", Hidden: false, ListOrder: ID} - err = CreateSection(§ion) return section.ID, err } if err != nil { return 0, err //other error } - return ID, nil + return id, nil } -// CreateSection creates a section, setting the newly created id in the given struct -func CreateSection(section *BoardSection) error { +// NewSection creates a new board section in the database and returns a *Section struct pointer. +// If position < 0, it will use the ID +func NewSection(name string, abbreviation string, hidden bool, position int) (*Section, error) { const sqlINSERT = `INSERT INTO DBPREFIXsections (name, abbreviation, hidden, position) VALUES (?,?,?,?)` - const sqlSELECT = `SELECT id FROM DBPREFIXsections WHERE position = ?` - //Excecuted in two steps this way because last row id functions arent thread safe, position is unique - _, err := ExecSQL(sqlINSERT, section.Name, section.Abbreviation, section.Hidden, section.ListOrder) + + id, err := getNextFreeID("DBPREFIXsections") if err != nil { - return err + return nil, err } - return QueryRowSQL( - sqlSELECT, - interfaceSlice(section.ListOrder), - interfaceSlice(§ion.ID)) + if position < 0 { + // position not specified, use the ID + position = id + } + if _, err = ExecSQL(sqlINSERT, name, abbreviation, hidden, position); err != nil { + return nil, err + } + return &Section{ + ID: id, + Name: name, + Abbreviation: abbreviation, + Position: position, + Hidden: hidden, + }, nil } -// GetSectionFromID queries the database for a section with the given ID and returns the section -// (or nil if it doesn't exist) and any errors -func GetSectionFromID(id int) (*BoardSection, error) { - sql := `SELECT name,abbreviation,position,hidden FROM DBPREFIXsections WHERE id = ?` - section := &BoardSection{ - ID: id, - } - err := QueryRowSQL(sql, []interface{}{id}, []interface{}{§ion.Name, §ion.Abbreviation, §ion.ListOrder, §ion.Hidden}) - return section, err -} +// // CreateSection creates a section, setting the newly created id in the given struct +// func CreateSection(section *Section) error { +// const sqlINSERT = `INSERT INTO DBPREFIXsections (name, abbreviation, hidden, position) VALUES (?,?,?,?)` +// const sqlSELECT = `SELECT id FROM DBPREFIXsections WHERE position = ?` -// DeleteSection deletes the section with the given ID from the database and returns any errors -func DeleteSection(id int) error { - sqlCount := `SELECT COUNT(*) FROM DBPREFIXsections` - var numRows int - err := QueryRowSQL(sqlCount, interfaceSlice(), interfaceSlice(&numRows)) - if err != nil { - return err - } - if numRows <= 1 { - return ErrCannotDeleteOnlySection - } - sqlDelete := `DELETE FROM DBPREFIXsections WHERE id = ?` - _, err = ExecSQL(sqlDelete, id) - if err == nil { - ResetBoardSectionArrays() - } - return err -} - -func (s *BoardSection) UpdateValues() error { - sql := `UPDATE DBPREFIXsections SET name = ?, abbreviation = ?, position = ?, hidden = ? where id = ?` - _, err := ExecSQL(sql, s.Name, s.Abbreviation, s.ListOrder, s.Hidden, s.ID) - if err == nil { - ResetBoardSectionArrays() - } - return err -} +// //Excecuted in two steps this way because last row id functions arent thread safe, position is unique +// _, err := ExecSQL(sqlINSERT, section.Name, section.Abbreviation, section.Hidden, section.Position) +// if err != nil { +// return err +// } +// return QueryRowSQL( +// sqlSELECT, +// interfaceSlice(section.Position), +// interfaceSlice(§ion.ID)) +// } diff --git a/pkg/gcsql/sections_test.go b/pkg/gcsql/sections_test.go deleted file mode 100644 index 7f29d269..00000000 --- a/pkg/gcsql/sections_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package gcsql - -import ( - "testing" -) - -func TestSectionCreation(t *testing.T) { - section := &BoardSection{ - Name: "Staff", - Abbreviation: "hidden1", - Hidden: true, - ListOrder: 2, - } - err := CreateSection(section) - if err != nil { - t.Fatalf("Failed creating section 'Staff': %s", err.Error()) - } - - if err = section.UpdateValues(); err != nil { - t.Fatalf("Error updating section: %s", err.Error()) - } - bs, err := GetSectionFromID(section.ID) - if err != nil { - t.Fatalf("Error getting section #%d: %s", section.ID, err.Error()) - } - if bs.Name != section.Name { - t.Fatalf("Got unexpected section when requesting section with ID %d: %s", section.ID, bs.Name) - } -} - -func TestDeleteSections(t *testing.T) { - section := &BoardSection{ - Name: "Temp", - Abbreviation: "temp", - Hidden: false, - ListOrder: 3, - } - err := CreateSection(section) - if err != nil { - t.Fatalf("Failed creating temporary section for deletion testing: %s", err.Error()) - } - if err = section.UpdateValues(); err != nil { - t.Fatalf("Failed updating temp section data: %s", err.Error()) - } - if err = DeleteSection(section.ID); err != nil { - t.Fatalf("Failed deleting temp section: %s", err.Error()) - } -} diff --git a/pkg/gcsql/staff.go b/pkg/gcsql/staff.go index cfc3331d..7072a52b 100644 --- a/pkg/gcsql/staff.go +++ b/pkg/gcsql/staff.go @@ -4,42 +4,68 @@ import ( "database/sql" "errors" "fmt" - "html/template" "net/http" "time" "github.com/gochan-org/gochan/pkg/gcutil" ) -// GetStaffName returns the name associated with a session -func GetStaffName(session string) (string, error) { - const sql = `SELECT staff.username from DBPREFIXstaff as staff - JOIN DBPREFIXsessions as sessions - ON sessions.staff_id = staff.id - WHERE sessions.data = ?` - var username string - err := QueryRowSQL(sql, interfaceSlice(session), interfaceSlice(&username)) - return username, err +// createDefaultAdminIfNoStaff creates a new default admin account if no accounts exist +func createDefaultAdminIfNoStaff() error { + const sql = `SELECT COUNT(id) FROM DBPREFIXstaff` + var count int + QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&count)) + if count > 0 { + return nil + } + _, err := NewStaff("admin", "password", 3) + return err } -// GetStaffBySession gets the staff that is logged in in the given session -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetStaffBySession(session string) (*Staff, error) { - const sql = `SELECT - staff.id, - staff.username, - staff.password_checksum, - staff.global_rank, - staff.added_on, - staff.last_login - FROM DBPREFIXstaff as staff - JOIN DBPREFIXsessions as sessions - ON sessions.staff_id = staff.id - WHERE sessions.data = ?` - staff := new(Staff) - err := QueryRowSQL(sql, interfaceSlice(session), interfaceSlice(&staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastActive)) - return staff, err +func NewStaff(username string, password string, rank int) (*Staff, error) { + const sqlINSERT = `INSERT INTO DBPREFIXstaff + (username, password_checksum, global_rank) + VALUES(?,?,?)` + passwordChecksum := gcutil.BcryptSum(password) + _, err := ExecSQL(sqlINSERT, username, passwordChecksum, rank) + if err != nil { + return nil, err + } + return &Staff{ + Username: username, + PasswordChecksum: passwordChecksum, + Rank: rank, + AddedOn: time.Now(), + IsActive: true, + }, nil +} + +// SetActive changes the active status of the staff member. If `active` is false, the login sessions are cleared +func (s *Staff) SetActive(active bool) error { + const updateActive = `UPDATE DBPREFIXstaff SET is_active = 0 WHERE username = ?` + _, err := ExecSQL(updateActive, s.Username) + if err != nil { + return err + } + if active { + return nil + } + return s.ClearSessions() +} + +// ClearSessions clears all login sessions for the user, requiring them to login again +func (s *Staff) ClearSessions() error { + const query = `SELECT id FROM DBPREFIXstaff WHERE username = ?` + const deleteSessions = `DELETE FROM DBPREFIXsessions WHERE staff_id = ?` + var err error + if s.ID == 0 { + // ID field not set, get it from the DB + if err = QueryRowSQL(query, interfaceSlice(s.Username), interfaceSlice(&s.ID)); err != nil { + return err + } + } + _, err = ExecSQL(deleteSessions, s.ID) + return err } // EndStaffSession deletes any session rows associated with the requests session cookie and then @@ -52,8 +78,7 @@ func EndStaffSession(writer http.ResponseWriter, request *http.Request) error { } // make it so that the next time the page is loaded, the browser will delete it sessionVal := session.Value - session.MaxAge = 0 - session.Expires = time.Now().Add(-7 * 24 * time.Hour) + session.MaxAge = -1 http.SetCookie(writer, session) staffID := 0 @@ -70,10 +95,13 @@ func EndStaffSession(writer http.ResponseWriter, request *http.Request) error { return nil } -// GetStaffByName gets the staff with a given name -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetStaffByName(name string) (*Staff, error) { +func DeactivateStaff(username string) error { + s := Staff{Username: username} + return s.SetActive(false) +} + +// GetStaffBySession gets the staff that is logged in in the given session +func GetStaffBySession(session string) (*Staff, error) { const sql = `SELECT staff.id, staff.username, @@ -82,241 +110,10 @@ func GetStaffByName(name string) (*Staff, error) { staff.added_on, staff.last_login FROM DBPREFIXstaff as staff - WHERE staff.username = ?` + JOIN DBPREFIXsessions as sessions + ON sessions.staff_id = staff.id + WHERE sessions.data = ?` staff := new(Staff) - err := QueryRowSQL(sql, interfaceSlice(name), interfaceSlice(&staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastActive)) + err := QueryRowSQL(sql, interfaceSlice(session), interfaceSlice(&staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastLogin)) return staff, err } - -func getStaffByID(id int) (*Staff, error) { - const sql = `SELECT - staff.id, - staff.username, - staff.password_checksum, - staff.global_rank, - staff.added_on, - staff.last_login - FROM DBPREFIXstaff as staff - WHERE staff.id = ?` - staff := new(Staff) - err := QueryRowSQL(sql, interfaceSlice(id), interfaceSlice(&staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastActive)) - return staff, err -} - -// NewStaff creates a new staff account from a given username, password and rank -func NewStaff(username, password string, rank int) error { - const sql = `INSERT INTO DBPREFIXstaff (username, password_checksum, global_rank) - VALUES (?, ?, ?)` - _, err := ExecSQL(sql, username, gcutil.BcryptSum(password), rank) - return err -} - -// DeleteStaff deletes the staff with a given name. -// Implemented to change the account name to a random string and set it to inactive -func DeleteStaff(username string) error { - const sql = `UPDATE DBPREFIXstaff SET username = ?, is_active = FALSE WHERE username = ?` - _, err := ExecSQL(sql, gcutil.RandomString(45), username) - return err -} - -func getStaffID(username string) (int, error) { - staff, err := GetStaffByName(username) - if err != nil { - return -1, err - } - return staff.ID, nil -} - -// CreateSession inserts a session for a given key and username into the database -func CreateSession(key, username string) error { - const sql1 = `INSERT INTO DBPREFIXsessions (staff_id,data,expires) VALUES(?,?,?)` - const sql2 = `UPDATE DBPREFIXstaff SET last_login = CURRENT_TIMESTAMP WHERE id = ?` - staffID, err := getStaffID(username) - if err != nil { - return err - } - _, err = ExecSQL(sql1, staffID, key, time.Now().Add(time.Duration(time.Hour*730))) //TODO move amount of time to config file - if err != nil { - return err - } - _, err = ExecSQL(sql2, staffID) - return err -} - -// GetAllStaffNopass gets all staff accounts without their password -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func GetAllStaffNopass(onlyactive bool) ([]Staff, error) { - sql := `SELECT id, username, global_rank, added_on, last_login FROM DBPREFIXstaff` - if onlyactive { - sql += " where is_active = 1" - } - rows, err := QuerySQL(sql) - if err != nil { - return nil, err - } - var staffs []Staff - for rows.Next() { - var staff Staff - err = rows.Scan(&staff.ID, &staff.Username, &staff.Rank, &staff.AddedOn, &staff.LastActive) - if err != nil { - return nil, err - } - staffs = append(staffs, staff) - } - return staffs, nil -} - -func createThread(boardID int, locked, stickied, anchored, cyclical bool) (threadID int, err error) { - const sql = `INSERT INTO DBPREFIXthreads (board_id, locked, stickied, anchored, cyclical) VALUES (?,?,?,?,?)` - //Retrieves next free ID, explicitly inserts it, keeps retrying until succesfull insert or until a non-pk error is encountered. - //This is done because mysql doesnt support RETURNING and both LAST_INSERT_ID() and last_row_id() are not thread-safe - isPrimaryKeyError := true - for isPrimaryKeyError { - threadID, err = getNextFreeID("DBPREFIXthreads") - if err != nil { - return 0, err - } - _, err = ExecSQL(sql, boardID, locked, stickied, anchored, cyclical) - - isPrimaryKeyError, err = errFilterDuplicatePrimaryKey(err) - if err != nil { - return 0, err - } - } - return threadID, nil -} - -func bumpThreadOfPost(postID int) error { - id, err := getThreadID(postID) - if err != nil { - return err - } - return bumpThread(id) -} - -func bumpThread(threadID int) error { - const sql = "UPDATE DBPREFIXthreads SET last_bump = CURRENT_TIMESTAMP WHERE id = ?" - _, err := ExecSQL(sql, threadID) - return err -} - -func appendFile(postID int, originalFilename, filename, checksum string, fileSize int, isSpoilered bool, width, height, thumbnailWidth, thumbnailHeight int) error { - const nextIDSQL = `SELECT COALESCE(MAX(file_order) + 1, 0) FROM DBPREFIXfiles WHERE post_id = ?` - var nextID int - err := QueryRowSQL(nextIDSQL, interfaceSlice(postID), interfaceSlice(&nextID)) - if err != nil { - return err - } - const insertSQL = `INSERT INTO DBPREFIXfiles (file_order, post_id, original_filename, filename, checksum, file_size, is_spoilered, width, height, thumbnail_width, thumbnail_height) - VALUES (?,?,?,?,?,?,?,?,?,?,?)` - _, err = ExecSQL(insertSQL, nextID, postID, originalFilename, filename, checksum, fileSize, isSpoilered, width, height, thumbnailWidth, thumbnailHeight) - return err -} - -// GetThreadIDZeroIfTopPost gets the post id of the top post of the thread a post belongs to, zero if the post itself is the top post -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design. Posts do not directly reference their post post anymore. -func GetThreadIDZeroIfTopPost(postID int) (ID int, err error) { - const sql = `SELECT t1.id FROM DBPREFIXposts as t1 - JOIN (SELECT thread_id FROM DBPREFIXposts where id = ?) as t2 ON t1.thread_id = t2.thread_id - WHERE t1.is_top_post` - err = QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&ID)) - if err != nil { - return 0, err - } - if ID == postID { - return 0, nil - } - return ID, nil -} - -func getThreadID(postID int) (ID int, err error) { - const sql = `SELECT thread_id FROM DBPREFIXposts WHERE id = ?` - err = QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&ID)) - return ID, err -} - -// GetPostPassword gets the password associated with a given post -func GetPostPassword(postID int) (password string, err error) { - const sql = `SELECT password FROM DBPREFIXposts WHERE id = ?` - err = QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&password)) - return password, err -} - -// UpdatePost updates a post with new information -// Deprecated: This method was created to support old functionality during the database refactor of april 2020 -// The code should be changed to reflect the new database design -func UpdatePost(postID int, email, subject string, message template.HTML, messageRaw string) error { - const sql = `UPDATE DBPREFIXposts SET email = ?, subject = ?, message = ?, message_raw = ? WHERE id = ?` - _, err := ExecSQL(sql, email, subject, string(message), messageRaw, postID) - return err -} - -// DeletePost deletes a post with a given ID -func DeletePost(postID int, checkIfTopPost bool) error { - if checkIfTopPost { - isTopPost, err := isTopPost(postID) - if err != nil { - return err - } - if isTopPost { - threadID, err := getThreadID(postID) - if err != nil { - return err - } - return deleteThread(threadID) - } - } - - const sql = `UPDATE DBPREFIXposts SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?` - _, err := ExecSQL(sql, postID) - return err -} - -func isTopPost(postID int) (val bool, err error) { - const sql = `SELECT is_top_post FROM DBPREFIXposts WHERE id = ?` - err = QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&val)) - return val, err -} - -func deleteThread(threadID int) error { - const sql1 = `UPDATE DBPREFIXthreads SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?` - const sql2 = `SELECT id FROM DBPREFIXposts WHERE thread_id = ?` - - _, err := QuerySQL(sql1, threadID) - if err != nil { - return err - } - rows, err := QuerySQL(sql2, threadID) - if err != nil { - return err - } - var ids []int - for rows.Next() { - var id int - if err = rows.Scan(&id); err != nil { - return err - } - ids = append(ids, id) - } - - for _, id := range ids { - if err = DeletePost(id, false); err != nil { - return err - } - } - return nil -} - -func createUser(username, passwordEncrypted string, globalRank int) (userID int, err error) { - const sqlInsert = `INSERT INTO DBPREFIXstaff (username, password_checksum, global_rank) VALUES (?,?,?)` - const sqlSelect = `SELECT id FROM DBPREFIXstaff WHERE username = ?` - //Excecuted in two steps this way because last row id functions arent thread safe, username is unique - _, err = ExecSQL(sqlInsert, username, passwordEncrypted, globalRank) - if err != nil { - return 0, err - } - err = QueryRowSQL(sqlSelect, interfaceSlice(username), interfaceSlice(&userID)) - return userID, err -} diff --git a/pkg/gcsql/tables.go b/pkg/gcsql/tables.go index 8dec7fb2..8e62f0d0 100644 --- a/pkg/gcsql/tables.go +++ b/pkg/gcsql/tables.go @@ -1,549 +1,246 @@ package gcsql import ( - "fmt" - "html" "html/template" - "path" - "strconv" - "strings" "time" - - "github.com/gochan-org/gochan/pkg/config" -) - -const ( - _ = iota - threadBan - imageBan - fullBan -) - -var ( - // AllSections is a cached list of all of the board sections - AllSections []BoardSection - // AllBoards is a cached list of all of the boards - AllBoards []Board - // TempPosts is a cached list of all of the posts in the temporary posts table - TempPosts []Post ) +// table: DBPREFIXannouncements type Announcement struct { - ID uint `json:"no"` - Subject string `json:"sub"` - Message string `json:"com"` - Poster string `json:"name"` - Timestamp time.Time + ID uint `json:"no"` // sql: `id` + StaffID string `json:"name"` // sql: `staff_id` + Subject string `json:"sub"` // sql: `subject` + Message string `json:"com"` // sql: `message` + Timestamp time.Time `json:"-"` // sql: `timestamp` } -type BanAppeal struct { - ID int - Ban int - Message string - Denied bool - StaffResponse string -} - -type BanInfo struct { - ID uint - IP string - Name string - NameIsRegex bool - Boards string - Staff string - Timestamp time.Time - Expires time.Time - Permaban bool - Reason string - Type int - StaffNote string - AppealAt time.Time - CanAppeal bool -} - -// BannedForever returns true if the ban is an unappealable permaban -func (ban *BanInfo) BannedForever() bool { - return ban.Permaban && !ban.CanAppeal && ban.Type == fullBan && ban.Boards == "" -} - -// IsActive returns true if the ban is still active (unexpired or a permaban) -func (ban *BanInfo) IsActive(board string) bool { - if ban.Boards == "" && (ban.Expires.After(time.Now()) || ban.Permaban) { - return true - } - boardsArr := strings.Split(ban.Boards, ",") - for _, b := range boardsArr { - if b == board && (ban.Expires.After(time.Now()) || ban.Permaban) { - return true - } - } - - return false -} - -// IsBanned checks to see if the ban applies to the given board -func (ban *BanInfo) IsBanned(board string) bool { - if ban.Boards == "" && (ban.Expires.After(time.Now()) || ban.Permaban) { - return true - } - boardsArr := strings.Split(ban.Boards, ",") - for _, b := range boardsArr { - if b == board && (ban.Expires.After(time.Now()) || ban.Permaban) { - return true - } - } - - return false -} - -type BannedHash struct { - ID uint - Checksum string - Description string +// table: DBPREFIXboard_staff +type BoardStaff struct { + BoardID uint // sql: `board_id` + StaffID uint // sql: `staff_id` } +// table: DBPREFIXboards type Board struct { - ID int `json:"-"` - CurrentPage int `json:"-"` - NumPages int `json:"pages"` - ListOrder int `json:"-"` - Dir string `json:"board"` - Type int `json:"-"` - UploadType int `json:"-"` - Title string `json:"title"` - Subtitle string `json:"meta_description"` - Description string `json:"-"` - Section int `json:"-"` - MaxFilesize int `json:"max_filesize"` - MaxPages int `json:"max_pages"` - DefaultStyle string `json:"-"` - Locked bool `json:"is_archived"` - CreatedOn time.Time `json:"-"` - Anonymous string `json:"-"` - ForcedAnon bool `json:"-"` - MaxAge int `json:"-"` - AutosageAfter int `json:"bump_limit"` - NoImagesAfter int `json:"image_limit"` - MaxMessageLength int `json:"max_comment_chars"` - EmbedsAllowed bool `json:"-"` - RedirectToThread bool `json:"-"` - ShowID bool `json:"-"` - RequireFile bool `json:"-"` - EnableCatalog bool `json:"-"` - EnableSpoileredImages bool `json:"-"` - EnableSpoileredThreads bool `json:"-"` - Worksafe bool `json:"ws_board"` - ThreadPage int `json:"-"` - Cooldowns BoardCooldowns `json:"cooldowns"` - ThreadsPerPage int `json:"per_page"` + ID int `json:"-"` // sql: `id` + SectionID int `json:"-"` // sql: `section_id` + URI string `json:"-"` // sql: `uri` + Dir string `json:"board"` // sql: `dir` + NavbarPosition int `json:"-"` // sql: `navbar_position` + Title string `json:"title"` // sql: `title` + Subtitle string `json:"meta_description"` // sql: `suttitle` + Description string `json:"-"` // sql: `description` + MaxFilesize int `json:"max_filesize"` // sql: `max_file_size` + MaxThreads int `json:"-"` // sql: `max_threads` + DefaultStyle string `json:"-"` // sql: `default_style` + Locked bool `json:"is_archived"` // sql: `locked` + CreatedAt time.Time `json:"-"` // sql: `created_at` + AnonymousName string `json:"-"` // sql: `anonymous_name` + ForceAnonymous bool `json:"-"` // sql: `force_anonymous` + AutosageAfter int `json:"bump_limit"` // sql: `autosage_after` + NoImagesAfter int `json:"image_limit"` // sql: `no_images_after` + MaxMessageLength int `json:"max_comment_chars"` // sql: `max_message_length` + MinMessageLength int `json:"min_comment_chars"` // sql: `min_message_length` + AllowEmbeds bool `json:"-"` // sql: `allow_embeds` + RedirectToThread bool `json:"-"` // sql: `redirect_to_thread` + RequireFile bool `json:"-"` // sql: `require_file` + EnableCatalog bool `json:"-"` // sql: `enable_catalog` + + // // not in the database, may end up moving this to a separate struct, possibly in a separate pkg } -// AbsolutePath returns the full filepath of the board directory -func (board *Board) AbsolutePath(subpath ...string) string { - return path.Join(config.GetSystemCriticalConfig().DocumentRoot, board.Dir, path.Join(subpath...)) -} - -// WebPath returns a string that represents the file's path as accessible by a browser -// fileType should be "boardPage", "threadPage", "upload", or "thumb" -func (board *Board) WebPath(fileName, fileType string) string { - var filePath string - systemCritical := config.GetSystemCriticalConfig() - - switch fileType { - case "": - fallthrough - case "boardPage": - filePath = path.Join(systemCritical.WebRoot, board.Dir, fileName) - case "threadPage": - filePath = path.Join(systemCritical.WebRoot, board.Dir, "res", fileName) - case "upload": - filePath = path.Join(systemCritical.WebRoot, board.Dir, "src", fileName) - case "thumb": - filePath = path.Join(systemCritical.WebRoot, board.Dir, "thumb", fileName) - } - return filePath -} - -func (board *Board) PagePath(pageNum interface{}) string { - var page string - pageNumStr := fmt.Sprintf("%v", pageNum) - if pageNumStr == "prev" { - if board.CurrentPage < 2 { - page = "1" - } else { - page = strconv.Itoa(board.CurrentPage - 1) - } - } else if pageNumStr == "next" { - if board.CurrentPage >= board.NumPages { - page = strconv.Itoa(board.NumPages) - } else { - page = strconv.Itoa(board.CurrentPage + 1) - } - } else { - page = pageNumStr - } - return board.WebPath(page+".html", "boardPage") -} - -func (board *Board) SetDefaults(title string, subtitle string, description string) { - board.CurrentPage = 1 - board.NumPages = 15 - board.ListOrder = 0 - board.Dir = "test" - board.Type = 0 - board.UploadType = 0 - if title == "" { - board.Title = "Testing board" - } else { - board.Title = title - } - if subtitle == "" { - board.Subtitle = "Board for testing stuff" - } else { - board.Subtitle = subtitle - } - if description == "" { - board.Description = "/test/ board description" - } else { - board.Description = description - } - board.Section = 1 - board.MaxFilesize = 10000 - board.MaxPages = 21 - board.DefaultStyle = config.GetBoardConfig("").DefaultStyle - board.Locked = false - board.CreatedOn = time.Now() - board.Anonymous = "Anonymous" - board.ForcedAnon = false - board.AutosageAfter = 200 - board.NoImagesAfter = 500 - board.MaxMessageLength = 8192 - board.EmbedsAllowed = false - board.RedirectToThread = false - board.ShowID = false - board.RequireFile = false - board.EnableCatalog = true - board.EnableSpoileredImages = true - board.EnableSpoileredThreads = true - board.Worksafe = true - board.Cooldowns = BoardCooldowns{ - NewThread: 30, - Reply: 7, - ImageReply: 7, - } - board.ThreadsPerPage = 20 -} - -func (board *Board) Create() error { - return CreateBoard(board) -} - -type BoardSection struct { - ID int - ListOrder int - Hidden bool - Name string - Abbreviation string -} - -// Post represents each post in the database -// Deprecated. Struct was made for use with old database, deprecated since refactor of april 2020. -// Please refactor all code that uses this struct to use a struct that alligns with the new database structure and functions. -type Post struct { - ID int `json:"no"` - ParentID int `json:"resto"` - CurrentPage int `json:"-"` - BoardID int `json:"-"` - Name string `json:"name"` - Tripcode string `json:"trip"` - Email string `json:"email"` - Subject string `json:"sub"` - MessageHTML template.HTML `json:"com"` - MessageText string `json:"-"` - Password string `json:"-"` - Filename string `json:"tim"` - FilenameOriginal string `json:"filename"` - FileChecksum string `json:"md5"` - FileExt string `json:"extension"` - Filesize int `json:"fsize"` - ImageW int `json:"w"` - ImageH int `json:"h"` - ThumbW int `json:"tn_w"` - ThumbH int `json:"tn_h"` - IP string `json:"-"` - Capcode string `json:"capcode"` - Timestamp time.Time `json:"time"` - Autosage bool `json:"-"` - Bumped time.Time `json:"last_modified"` - Stickied bool `json:"-"` - Locked bool `json:"-"` - Reviewed bool `json:"-"` - - boardDir string -} - -func (p *Post) GetBoardDir() (string, error) { - if p.boardDir != "" { - return p.boardDir, nil - } - const sql = `SELECT dir FROM DBPREFIXboards WHERE id = ( - SELECT board_id FROM DBPREFIXthreads WHERE id = ( - SELECT thread_id FROM DBPREFIXposts WHERE id = ? LIMIT 1) LIMIT 1)` - - err := QueryRowSQL(sql, []interface{}{p.ID}, interfaceSlice(&p.boardDir)) - return p.boardDir, err -} - -func (p *Post) GetURL(includeDomain bool) string { - postURL := "" - systemCritical := config.GetSystemCriticalConfig() - if includeDomain { - postURL += systemCritical.SiteDomain - } - var board Board - if err := board.PopulateData(p.BoardID); err != nil { - return postURL - } - - postURL += systemCritical.WebRoot + board.Dir + "/res/" - if p.ParentID == 0 { - postURL += fmt.Sprintf("%d.html#%d", p.ID, p.ID) - } else { - postURL += fmt.Sprintf("%d.html#%d", p.ParentID, p.ID) - } - return postURL -} - -// Sanitize escapes HTML strings in a post. This should be run immediately before -// the post is inserted into the database -func (p *Post) Sanitize() { - p.Name = html.EscapeString(p.Name) - p.Email = html.EscapeString(p.Email) - p.Subject = html.EscapeString(p.Subject) - p.Password = html.EscapeString(p.Password) - if p.ParentID < 0 { - p.ParentID = 0 - } -} - -type Report struct { - ID int - HandledByStaffID int - PostID int - IP string - Reason string - IsCleared bool -} - -func (r *Report) Timestamp() (*time.Time, error) { - sql := `SELECT timestamp FROM DBPREFIXreports_audit WHERE report_id = ?` - timestamp := new(time.Time) - err := QueryRowSQL(sql, interfaceSlice(r.ID), interfaceSlice(timestamp)) - if err != nil { - return nil, err - } - return timestamp, nil -} - -type LoginSession struct { - ID uint - Name string - Data string - Expires string -} - -// Staff represents a single staff member's info stored in the database -type Staff struct { - ID int - Username string - PasswordChecksum string `json:"-"` - Rank int - AddedOn time.Time - LastActive time.Time -} - -// CleanSessions clears out all of the sessions with this -// staff ID, regardless of cookie data -func (s *Staff) CleanSessions() (int64, error) { - var err error - query := `DELETE FROM DBPREFIXsessions WHERE staff_id = ?` - result, err := ExecSQL(query, s.ID) - if err != nil { - return 0, err - } - return result.RowsAffected() -} - -func (s *Staff) RankString() string { - switch s.Rank { - case 3: - return "Administrator" - case 2: - return "Moderator" - case 1: - return "Janitor" - } - return "Unknown" -} - -type BoardCooldowns struct { - NewThread int `json:"threads"` - Reply int `json:"replies"` - ImageReply int `json:"images"` -} - -type MessagePostContainer struct { - ID int - MessageRaw string - Message template.HTML - Board string -} - -// Deprecated. Struct was made for use with old database, deprecated since refactor of april 2020. -// Please refactor all code that uses this struct to use a struct that alligns with the new database structure and functions. -type RecentPost struct { - BoardName string - BoardID int - PostID int - ParentID int - Name string - Tripcode string - Message template.HTML - Filename string - ThumbW int - ThumbH int - IP string - Timestamp time.Time -} - -// GetURL returns the full URL of the recent post, or the full path if includeDomain is false -func (p *RecentPost) GetURL(includeDomain bool) string { - postURL := "" - systemCritical := config.GetSystemCriticalConfig() - if includeDomain { - postURL += systemCritical.SiteDomain - } - idStr := strconv.Itoa(p.PostID) - postURL += systemCritical.WebRoot + p.BoardName + "/res/" - if p.ParentID == 0 { - postURL += idStr + ".html#" + idStr - } else { - postURL += strconv.Itoa(p.ParentID) + ".html#" + idStr - } - return postURL -} - -type Thread struct { - OP Post `json:"-"` - NumReplies int `json:"replies"` - NumImages int `json:"images"` - OmittedPosts int `json:"omitted_posts"` - OmittedImages int `json:"omitted_images"` - BoardReplies []Post `json:"-"` - Sticky int `json:"sticky"` - Locked int `json:"locked"` - ThreadPage int `json:"-"` -} - -// FileBan contains the information associated with a specific file ban +// FileBan contains the information associated with a specific file ban. +// table: DBPREFIXfile_ban type FileBan struct { - ID int `json:"id"` - BoardID *int `json:"board"` - StaffID int `json:"staff_id"` - StaffNote string `json:"staff_note"` - IssuedAt time.Time `json:"issued_at"` - Checksum string `json:"checksum"` + ID int `json:"id"` // sql: `id` + BoardID int `json:"board_id"` // sql: `board_id` + StaffID int `json:"staff_id"` // sql: `staff_id` + StaffNote string `json:"staff_note"` // sql: `staff_note` + IssuedAt time.Time `json:"issued_at"` // sql: `issued_at` + Checksum string `json:"checksum"` // sql: `checksum` } -func (fb *FileBan) BoardURI() string { - if fb.BoardID == nil { - return "" - } - var uri string - err := QueryRowSQL(`SELECT uri FROM DBPREFIXboards WHERE id = ?`, interfaceSlice(fb.BoardID), interfaceSlice(&uri)) - if err != nil { - return "" - } - return uri -} - -func (fb *FileBan) StaffName() string { - staff, err := getStaffByID(fb.StaffID) - if err != nil { - return "" - } - return staff.Username -} - -// FilenameBan contains the information associated with a specific filename ban +// FilenameBan represents a ban on a specific filename or filename regular expression. +// table: DBPREFIXfilename_ban type FilenameBan struct { - ID int `json:"id"` - BoardID *int `json:"board"` - StaffID int `json:"staff_id"` - StaffNote string `json:"staff_note"` - IssuedAt time.Time `json:"issued_at"` - Filename string `json:"filename"` - IsRegex bool `json:"is_regex"` + ID int `json:"id"` // sql: `id` + BoardID int `json:"board_id"` // sql: `board_id` + StaffID int `json:"staff_id"` // sql: `staff_id` + StaffNote string `json:"staff_note"` // sql: `staff_note` + IssuedAt time.Time `json:"issued_at"` // sql: `issued_at` + Filename string `json:"filename"` // sql: `filename` + IsRegex bool `json:"is_regex"` // sql: `is_regex` } -func (fnb *FilenameBan) BoardURI() string { - if fnb.BoardID == nil { - return "" - } - var uri string - err := QueryRowSQL(`SELECT uri FROM DBPREFIXboards WHERE id = ?`, interfaceSlice(fnb.BoardID), interfaceSlice(&uri)) - if err != nil { - return "" - } - return uri +// Upload represents a file attached to a post. +// table: DBPREFIXfiles +type Upload struct { + ID int // sql: `id` + PostID int // sql: `post_id` + FileOrder int // sql: `file_order` + OriginalFilename string // sql: `original_filename` + Filename string // sql: `filename` + Checksum string // sql: `checksum` + FileSize int // sql: `file_size` + IsSpoilered bool // sql: `is_spoilered` + ThumbnailWidth int // sql: `thumbnail_width` + ThumbnailHeight int // sql: `thumbnail_height` + Width int // sql: `width` + Height int // sql: `height` } -func (fnb *FilenameBan) StaffName() string { - staff, err := getStaffByID(fnb.StaffID) - if err != nil { - return "" - } - return staff.Username +// used to composition IPBan and IPBanAudit +type ipBanBase struct { + IsActive bool `json:"is_active"` + IsThreadBan bool `json:"is_thread_ban"` + ExpiresAt time.Time `json:"expires_at"` + StaffID int `json:"staff_id"` + AppealAt time.Time `json:"appeal_at"` + Permanent bool `json:"permanent"` + StaffNote string `json:"staff_note"` + Message string `json:"message"` + CanAppeal bool `json:"can_appeal"` } -// UsernameBan contains the information associated with a specific username ban -type UsernameBan struct { - ID int `json:"id"` - BoardID *int `json:"board"` - StaffID int `json:"staff_id"` - StaffNote string `json:"staff_note"` - IssuedAt time.Time `json:"issued_at"` - Username string `json:"username"` - IsRegex bool `json:"is_regex"` -} - -// WordFilter contains the information associated with a specific wordfilter -type WordFilter struct { - ID int `json:"id"` - BoardDirs []string `json:"boards"` - StaffID int `json:"staff_id"` - StaffNote string `json:"staff_note"` - IssuedAt time.Time `json:"issued_at"` - Search string `json:"search"` - IsRegex bool `json:"is_regex"` - ChangeTo string `json:"change_to"` -} - -// IPBan contains the information association with a specific ip ban +// IPBan contains the information association with a specific ip ban. +// table: DBPREFIXip_ban type IPBan struct { ID int `json:"id"` BoardID *int `json:"board"` - StaffID int `json:"staff_id"` BannedForPostID *int `json:"banned_for_post_id"` CopyPostText template.HTML `json:"copy_post_text"` - IsThreadBan bool `json:"is_thread_ban"` - IsActive bool `json:"is_active"` IP string `json:"ip"` IssuedAt time.Time `json:"issued_at"` - AppealAt time.Time `json:"appeal_at"` - ExpiresAt time.Time `json:"expires_at"` - Permanent bool `json:"permanent"` - StaffNote string `json:"staff_note"` - Message string `json:"message"` - CanAppeal bool `json:"can_appeal"` + ipBanBase +} + +// table: DBPREFIXip_ban_audit +type IPBanAudit struct { + IPBanID int // sql: `ip_ban_id` + Timestamp time.Time // sql: `timestamp` + ipBanBase +} + +// used to composition IPBanAppeal and IPBanAppealAudit +type ipBanAppealBase struct { + StaffID int // sql: `staff_id` + AppealText string // sql: `appeal_text` + StaffResponse string // sql: `staff_response` + IsDenied bool // sql: `is_denied` +} + +// table: DBPREFIXip_ban_appeals +type IPBanAppeal struct { + ID int // sql: `id` + IPBanID int // sql: `ip_ban_id` + ipBanAppealBase +} + +// table: DBPREFIXip_ban_appeals_audit +type IPBanAppealAudit struct { + AppealID int // sql: `appeal_id` + Timestamp time.Time // sql: `timestamp` + ipBanAppealBase +} + +// table: DBPREFIXposts +type Post struct { + ID int // sql: `id` + ThreadID int // sql: `thread_id` + IsTopPost bool // sql: `is_top_post` + IP string // sql: `ip` + CreatedOn time.Time // sql: `created_on` + Name string // sql: `name` + Tripcode string // sql: `tripcode` + IsRoleSignature bool // sql: `is_role_signature` + Email string // sql: `email` + Subject string // sql: `subject` + Message template.HTML // sql: `message` + MessageRaw string // sql: `message_raw` + Password string // sql: `password` + DeletedAt time.Time // sql: `deleted_at` + IsDeleted bool // sql: `is_deleted` + BannedMessage string // sql: `banned_message` +} + +// table: DBPREFIXreports +type Report struct { + ID int // sql: `id` + HandledByStaffID int // sql: `handled_by_staff_id` + PostID int // sql: `post_id` + IP string // sql: `ip` + Reason string // sql: `reason` + IsCleared bool // sql: `is_cleared` +} + +// table: DBPREFIXreports_audit +type ReportAudit struct { + Report int // sql: `report_id` + Timestamp time.Time // sql: `timestamp` + HandledByStaffID int // sql: `handled_by_staff_id` + IsCleared bool // sql: `is_cleared` +} + +// table: DBPREFIXsections +type Section struct { + ID int // sql: `id` + Name string // sql: `name` + Abbreviation string // sql: `abbreviation` + Position int // sql: `position` + Hidden bool // sql: `hidden` +} + +// table: DBPREFIXsessions +type LoginSession struct { + ID int // sql: `id` + StaffID int // sql: `staff_id` + Expires time.Time // sql: `expires` + Data string // sql: `data` +} + +// DBPREFIXstaff +type Staff struct { + ID int // sql: `id` + Username string // sql: `username` + PasswordChecksum string // sql: `password_checksum` + Rank int // sql: `global_rank` + AddedOn time.Time // sql: `added_on` + LastLogin time.Time // sql: `last_login` + IsActive bool // sql: `is_active` +} + +// table: DBPREFIXthreads +type Thread struct { + ID int // sql: `id` + BoardID int // sql: `board_id` + Locked bool // sql: `locked` + Stickied bool // sql: `stickied` + Anchored bool // sql: `anchored` + Cyclical bool // sql: `cyclical` + LastBump time.Time // sql: `last_bump` + DeletedAt time.Time // sql: `deleted_at` + IsDeleted bool // sql: `is_deleted` +} + +// table: DBPREFIXusername_ban +type UsernameBan struct { + ID int `json:"id"` // sql: `id` + BoardID *int `json:"board"` // sql: `board_id` + StaffID int `json:"staff_id"` // sql: `staff_id` + StaffNote string `json:"staff_note"` // sql: `staff_note` + IssuedAt time.Time `json:"issued_at"` // sql: `issued_at` + Username string `json:"username"` // sql: `username` + IsRegex bool `json:"is_regex"` // sql: `is_regex` +} + +// table DBPREFIXwordfilters +type Wordfilter struct { + ID int `json:"id"` // sql: `id` + BoardDirs *string `json:"boards"` // sql: `board_dirs` + StaffID int `json:"staff_id"` // sql: `staff_id` + StaffNote string `json:"staff_note"` // sql: `staff_note` + IssuedAt time.Time `json:"issued_at"` // sql: `issued_at` + Search string `json:"search"` // sql: `search` + IsRegex bool `json:"is_regex"` // sql: `is_regex` + ChangeTo string `json:"change_to"` // sql: `change_to` } diff --git a/pkg/gcsql/threads.go b/pkg/gcsql/threads.go new file mode 100644 index 00000000..b78afb26 --- /dev/null +++ b/pkg/gcsql/threads.go @@ -0,0 +1,74 @@ +package gcsql + +import "errors" + +var ( + ErrThreadExists = errors.New("thread already exists") + ErrThreadDoesNotExist = errors.New("thread does not exist") +) + +// GetThreadsWithBoardID queries the database for the threads with the given board ID from the database. +// If onlyNotDeleted is true, it omits deleted threads and threads that were removed because the max +// thread limit was reached +func GetThreadsWithBoardID(boardID int, onlyNotDeleted bool) ([]Thread, error) { + query := `SELECT + id, board_id, locked, stickied, anchored, cyclical, last_bump, deleted_at, is_deleted + FROM DBPREFIXthreads WHERE board_id = ?` + if onlyNotDeleted { + query += " AND is_deleted = FALSE" + } + rows, err := QuerySQL(query, boardID) + if err != nil { + return nil, err + } + var threads []Thread + for rows.Next() { + var thread Thread + if err = rows.Scan( + &thread.ID, &thread.BoardID, &thread.Locked, &thread.Stickied, &thread.Anchored, + &thread.Cyclical, &thread.LastBump, + ); err != nil { + return threads, err + } + } + return threads, nil +} + +func GetThreadReplyCountFromOP(opID int) (int, error) { + const query = `SELECT COUNT(*) FROM DBPREFIXposts WHERE thread_id = ( + SELECT thread_id FROM DBPREFIXposts WHERE id = ?) AND is_deleted = 0` + var num int + err := QueryRowSQL(query, interfaceSlice(opID), interfaceSlice(&num)) + return num, err +} + +// ChangeThreadBoardID updates the given thread's post ID and the destination board ID +func ChangeThreadBoardID(threadID int, newBoardID int) error { + if !DoesBoardExistByID(newBoardID) { + return ErrBoardDoesNotExist + } + _, err := ExecSQL(`UPDATE DBPREFIXthreads SET board_id = ? WHERE id = ?`, newBoardID, threadID) + return err +} + +// ChangeThreadBoardByURI updates a thread's board ID, given the thread's post ID and +// the destination board's uri +func ChangeThreadBoardByURI(postID int, uri string) error { + boardID, err := getBoardIDFromURI(uri) + if err != nil { + return err + } + return ChangeThreadBoardID(postID, boardID) +} + +// deleteThread updates the thread and sets it as deleted, as well as the posts where thread_id = threadID +func deleteThread(threadID int) error { + const deletePostsSQL = `UPDATE DBPREFIXposts SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE thread_id = ?` + const deleteThreadSQL = `UPDATE DBPREFIXthreads SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?` + _, err := ExecSQL(deletePostsSQL, threadID) + if err != nil { + return err + } + _, err = ExecSQL(deleteThreadSQL, threadID) + return err +} diff --git a/pkg/gcsql/uploads.go b/pkg/gcsql/uploads.go new file mode 100644 index 00000000..b5f116ac --- /dev/null +++ b/pkg/gcsql/uploads.go @@ -0,0 +1,37 @@ +package gcsql + +import ( + "github.com/gochan-org/gochan/pkg/gcsql.bak" + "github.com/gochan-org/gochan/pkg/gcutil" +) + +// ThumbnailPath returns the thumbnail path of the upload, given an thumbnail type ("thumbnail" or "catalog") +func (u *Upload) ThumbnailPath(thumbType string) string { + return gcutil.GetThumbnailPath(thumbType, u.Filename) +} + +// GetThreadFiles gets a list of the files owned by posts in the thread, including thumbnails for convenience. +func GetThreadFiles(post *Post) ([]Upload, error) { + query := `SELECT + id, post_id, file_order, original_filename, filename, checksum, + file_size, is_spoilered, thumbnail_width, thumbnail_height, width, height + FROM DBPREFIXfiles WHERE post_id IN ( + SELECT id FROM DBPREFIXposts WHERE thread_id = ( + SELECT thread_id FROM DBPREFIXposts WHERE id = ?)) AND filename != 'deleted'` + rows, err := gcsql.QuerySQL(query, post.ID) + if err != nil { + return nil, err + } + var uploads []Upload + for rows.Next() { + var upload Upload + if err = rows.Scan( + &upload.ID, &upload.PostID, &upload.FileOrder, &upload.OriginalFilename, &upload.Filename, &upload.Checksum, + &upload.FileSize, &upload.IsSpoilered, &upload.ThumbnailWidth, &upload.ThumbnailHeight, &upload.Width, &upload.Height, + ); err != nil { + return uploads, err + } + uploads = append(uploads, upload) + } + return uploads, nil +} diff --git a/pkg/gcsql/util.go b/pkg/gcsql/util.go index cfe42bed..54605a84 100644 --- a/pkg/gcsql/util.go +++ b/pkg/gcsql/util.go @@ -95,7 +95,7 @@ Example: var intVal int var stringVal string err := QueryRowSQL("SELECT intval,stringval FROM table WHERE id = ?", - []interface{}{&id}, + []interface{}{id}, []interface{}{&intVal, &stringVal}) */ func QueryRowSQL(query string, values, out []interface{}) error { @@ -138,17 +138,68 @@ func BeginTx() (*sql.Tx, error) { }) } -// ResetBoardSectionArrays is run when the board list needs to be changed -// (board/section is added, deleted, etc) -func ResetBoardSectionArrays() { - AllBoards = nil - AllSections = nil +func getNextFreeID(tableName string) (ID int, err error) { + var sql = `SELECT COALESCE(MAX(id), 0) + 1 FROM ` + tableName + err = QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&ID)) + return ID, err +} - allBoardsArr, _ := GetAllBoards() - AllBoards = append(AllBoards, allBoardsArr...) +func doesTableExist(tableName string) (bool, error) { + var existQuery string - allSectionsArr, _ := GetAllSections() - AllSections = append(AllSections, allSectionsArr...) + switch config.GetSystemCriticalConfig().DBtype { + case "mysql": + fallthrough + case "postgresql": + existQuery = `SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ?` + case "sqlite3": + existQuery = `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?` + default: + return false, ErrUnsupportedDB + } + + var count int + err := QueryRowSQL(existQuery, []interface{}{config.GetSystemCriticalConfig().DBprefix + tableName}, []interface{}{&count}) + if err != nil { + return false, err + } + return count == 1, nil +} + +// getDatabaseVersion gets the version of the database, or an error if none or multiple exist +func getDatabaseVersion(componentKey string) (int, error) { + const sql = `SELECT version FROM DBPREFIXdatabase_version WHERE component = ?` + var version int + err := QueryRowSQL(sql, []interface{}{componentKey}, []interface{}{&version}) + if err != nil { + return 0, err + } + return version, err +} + +// doesGochanPrefixTableExist returns true if any table with a gochan prefix was found. +// Returns false if the prefix is an empty string +func doesGochanPrefixTableExist() (bool, error) { + systemCritical := config.GetSystemCriticalConfig() + if systemCritical.DBprefix == "" { + return false, nil + } + var prefixTableExist string + switch systemCritical.DBtype { + case "mysql": + fallthrough + case "postgresql": + prefixTableExist = `SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'DBPREFIX%'` + case "sqlite3": + prefixTableExist = `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name LIKE 'DBPREFIX%'` + } + + var count int + err := QueryRowSQL(prefixTableExist, []interface{}{}, []interface{}{&count}) + if err != nil { + return false, err + } + return count > 0, nil } // interfaceSlice creates a new interface slice from an arbitrary collection of values diff --git a/pkg/gctemplates/funcs.go b/pkg/gctemplates/funcs.go index 1df4376f..083ae4ee 100644 --- a/pkg/gctemplates/funcs.go +++ b/pkg/gctemplates/funcs.go @@ -239,19 +239,19 @@ var funcMap = template.FuncMap{ } return img[0:index] + thumbSuffix }, - "numReplies": func(boardid, threadid int) int { - num, err := gcsql.GetReplyCount(threadid) + "numReplies": func(boardid, opID int) int { + num, err := gcsql.GetThreadReplyCountFromOP(opID) if err != nil { return 0 } return num }, "getBoardDir": func(id int) string { - var board gcsql.Board - if err := board.PopulateData(id); err != nil { + dir, err := gcsql.GetBoardDir(id) + if err != nil { return "" } - return board.Dir + return dir }, "webPath": func(part ...string) string { return config.WebPath(part...)