diff --git a/README.md b/README.md index d51e32bd..28d34780 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ See [`frontend/README.md`](frontend/README.md) for information on working with S ## Style guide * For Go source, follow the standard Go [style guide](https://github.com/golang/go/wiki/CodeReviewComments). +* variables and functions exposed to Go templates should be in camelCase, like Go variables * All exported functions and variables should have a documentation comment explaining their functionality, as per go style guides. * Unexported functions are preferred to have a documentation comment explaining it, unless it is sufficiently self explanatory or simple. * Git commits should be descriptive. Put further explanation in the comment of the commit. @@ -55,18 +56,19 @@ See [`frontend/README.md`](frontend/README.md) for information on working with S ## Roadmap ### Near future -All features that are to be realised for the near future are found in the issues tab with the milestone "Next Release" +* Make the plugin support actually useful +* Improve moderation tools, using path routing instead of the action variable +* Add support for sticky and locked threads +* Add support for more filetypes (zip,) +* Improve API support for existing chan browing phone apps ### Lower priority -* Improve moderation tools heavily +* Better image fingerpringing and banning system (as opposed to a hash) * Rework any legacy structs that uses comma separated fields to use a slice instead. -* Readd sqlite support * RSS feeds from boards/specific threads/specific usernames+tripcodes (such as newsanon) * Pinning a post within a thread even if its not the OP, to prevent its deletion in a cyclical thread. ### Later down the line -* Improve API support for existing chan browing phone apps -* Better image fingerpringing and banning system (as opposed to a hash) ### Possible experimental features: * Allow users to be mini-moderators within threads they posted themselves, to prevent spammers/derailers. \ No newline at end of file diff --git a/cmd/gochan/deleteposts.go b/cmd/gochan/deleteposts.go index 5abf699a..55e2bfa4 100644 --- a/cmd/gochan/deleteposts.go +++ b/cmd/gochan/deleteposts.go @@ -2,7 +2,9 @@ package main import ( "database/sql" + "errors" "fmt" + "io/fs" "net/http" "os" "path" @@ -14,8 +16,15 @@ import ( "github.com/gochan-org/gochan/pkg/gcutil" "github.com/gochan-org/gochan/pkg/manage" "github.com/gochan-org/gochan/pkg/serverutil" + "github.com/rs/zerolog" ) +type upload struct { + postID int + filename string + boardDir string +} + func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.Request) { // Delete a post or thread errEv := gcutil.LogError(nil). @@ -85,61 +94,9 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R } if fileOnly { - upload, err := post.GetUpload() - if err != nil { - errEv.Err(err).Caller(). - 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}) + if deletePostUpload(post, board, writer, request, errEv) { 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 { - errEv.Err(err).Caller(). - Int("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 - } - // delete the file's thumbnail - thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("thumb")) - if err = os.Remove(thumbPath); err != nil { - errEv.Err(err).Caller(). - Int("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 - } - // delete the catalog thumbnail - if post.IsTopPost { - thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("catalog")) - if err = os.Remove(thumbPath); err != nil { - errEv.Err(err).Caller(). - Int("postid", post.ID). - 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 { - errEv.Err(err).Caller(). - 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 - } - } if err = building.BuildBoardPages(board); err != nil { errEv.Err(err).Caller().Send() serverutil.ServeError(writer, "Unable to build board pages for /"+board.Dir+"/: "+err.Error(), wantsJSON, map[string]interface{}{ @@ -172,6 +129,53 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R return } } else { + if post.IsTopPost { + rows, err := gcsql.QuerySQL( + `SELECT filename FROM DBPREFIXfiles + LEFT JOIN ( + SELECT id FROM DBPREFIXposts WHERE thread_id = ? + ) p + ON p.id = post_id + WHERE post_id = p.id AND filename != 'deleted'`, + post.ThreadID) + if err != nil { + errEv.Err(err).Caller(). + Str("requestType", "deleteThread"). + Int("postid", post.ID). + Int("threadID", post.ThreadID). + Msg("Unable to get list of filenames in thread") + serverutil.ServeError(writer, "Unable to get list of filenames in thread", wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) + return + } + defer rows.Close() + var uploads []upload + for rows.Next() { + var filename string + if err = rows.Scan(&filename); err != nil { + errEv.Err(err).Caller(). + Str("requestType", "deleteThread"). + Int("postid", post.ID). + Int("threadID", post.ThreadID). + Msg("Unable to get list of filenames in thread") + serverutil.ServeError(writer, "Unable to get list of filenames in thread", wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) + return + } + uploads = append(uploads, upload{ + filename: filename, + boardDir: board.Dir, + }) + } + // done as a goroutine to avoid delays if the thread has a lot of files + // the downside is of course that if something goes wrong, deletion errors + // won't be seen in the browser + go deleteUploads(uploads) + } else if deletePostUpload(post, board, writer, request, errEv) { + return + } // delete the post if err = post.Delete(); err != nil { errEv.Err(err).Caller(). @@ -189,8 +193,6 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R os.Remove(threadIndexPath + ".json") } else { building.BuildBoardPages(board) - // _board, _ := gcsql.GetBoardFromID(post.BoardID) - // building.BuildBoardPages(&_board) } building.BuildBoards(false, boardid) } @@ -218,3 +220,90 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R } } } + +func deleteUploads(uploads []upload) { + documentRoot := config.GetSystemCriticalConfig().DocumentRoot + var filePath, thumbPath, catalogThumbPath string + var err error + for _, upload := range uploads { + filePath = path.Join(documentRoot, upload.boardDir, "src", upload.filename) + if err = os.Remove(filePath); err != nil && !errors.Is(err, os.ErrNotExist) { + gcutil.LogError(err).Caller(). + Str("filePath", filePath). + Int("postid", upload.postID).Send() + } + thumbPath = path.Join(documentRoot, upload.boardDir, "thumb", gcutil.GetThumbnailPath("reply", upload.filename)) + if err = os.Remove(thumbPath); err != nil && !errors.Is(err, os.ErrNotExist) { + gcutil.LogError(err).Caller(). + Str("thumbPath", thumbPath). + Int("postid", upload.postID).Send() + } + catalogThumbPath = path.Join(documentRoot, upload.boardDir, "thumb", gcutil.GetThumbnailPath("catalog", upload.filename)) + if err = os.Remove(catalogThumbPath); err != nil && !errors.Is(err, os.ErrNotExist) { + gcutil.LogError(err).Caller(). + Str("catalogThumbPath", catalogThumbPath). + Int("postid", upload.postID).Send() + } + } +} + +func deletePostUpload(post *gcsql.Post, board *gcsql.Board, writer http.ResponseWriter, request *http.Request, errEv *zerolog.Event) bool { + documentRoot := config.GetSystemCriticalConfig().DocumentRoot + upload, err := post.GetUpload() + wantsJSON := serverutil.IsRequestingJSON(request) + if err != nil { + errEv.Err(err).Caller(). + 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 true + } + if upload != nil && upload.Filename != "deleted" { + filePath := path.Join(documentRoot, board.Dir, "src", upload.Filename) + if err = os.Remove(filePath); err != nil && !errors.Is(err, fs.ErrNotExist) { + errEv.Err(err).Caller(). + Int("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 true + } + // delete the file's thumbnail + thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("thumb")) + if err = os.Remove(thumbPath); err != nil && !errors.Is(err, fs.ErrNotExist) { + errEv.Err(err).Caller(). + Int("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 true + } + // delete the catalog thumbnail + if post.IsTopPost { + thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("catalog")) + if err = os.Remove(thumbPath); err != nil && !errors.Is(err, fs.ErrNotExist) { + errEv.Err(err).Caller(). + Int("postid", post.ID). + 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 true + } + } + // remove the upload from the database + if err = post.UnlinkUploads(true); err != nil { + errEv.Err(err).Caller(). + 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 true + } + } + return false +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 88d3514b..a3606829 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "gochan.js", - "version": "3.3.0", + "version": "3.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gochan.js", - "version": "3.3.0", + "version": "3.4.0", "license": "BSD-2-Clause", "dependencies": { "jquery": "^3.5.1", @@ -4922,9 +4922,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001367", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz", - "integrity": "sha512-XDgbeOHfifWV3GEES2B8rtsrADx4Jf+juKX2SICJcaUhjYBO3bR96kvEIHa15VU6ohtOhBZuPGGYGbXMRn0NCw==", + "version": "1.0.30001441", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", + "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", "dev": true, "funding": [ { @@ -13729,9 +13729,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001367", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz", - "integrity": "sha512-XDgbeOHfifWV3GEES2B8rtsrADx4Jf+juKX2SICJcaUhjYBO3bR96kvEIHa15VU6ohtOhBZuPGGYGbXMRn0NCw==", + "version": "1.0.30001441", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", + "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", "dev": true }, "chalk": { diff --git a/frontend/package.json b/frontend/package.json index dbaab23b..007aadd3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "gochan.js", - "version": "3.3.0", + "version": "3.4.0", "description": "", "type": "module", "source": "js/gochan.js", diff --git a/html/error/404.html b/html/error/404.html index ee77d01e..0c4b7fda 100755 --- a/html/error/404.html +++ b/html/error/404.html @@ -7,6 +7,6 @@
The requested file could not be found on this server.
-