1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-26 10:36:23 -07:00

Improve request validation and logging, implement cleanup for reports of deleted posts.

This commit is contained in:
Eggbertx 2025-03-28 17:34:00 -07:00
parent faf1457c36
commit 4aab676c67
7 changed files with 100 additions and 39 deletions

View file

@ -7,7 +7,6 @@ import (
"net"
"net/http"
"net/http/fcgi"
"path"
"strconv"
"time"
@ -89,7 +88,6 @@ func randomBanner(writer http.ResponseWriter, request *http.Request) {
// handles requests to /util
func utilHandler(writer http.ResponseWriter, request *http.Request) {
action := request.FormValue("action")
board := request.FormValue("board")
deleteBtn := request.PostFormValue("delete_btn")
reportBtn := request.PostFormValue("report_btn")
@ -97,28 +95,30 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
doEdit := request.PostFormValue("doedit")
moveBtn := request.PostFormValue("move_btn")
doMove := request.PostFormValue("domove")
systemCritical := config.GetSystemCriticalConfig()
wantsJSON := serverutil.IsRequestingJSON(request)
redirectTo := request.Referer()
if wantsJSON {
writer.Header().Set("Content-Type", "application/json")
}
if action == "" && deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit post" && doEdit != "post" && doEdit != "upload" && moveBtn != "Move thread" && doMove != "1" {
gcutil.LogAccess(request).
if redirectTo == "" || (deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit post" && doEdit != "post" && doEdit != "upload" && moveBtn != "Move thread" && doMove != "1") {
accessEv := gcutil.LogAccess(request).
Int("status", http.StatusBadRequest).
Msg("received invalid /util request")
if wantsJSON {
writer.WriteHeader(http.StatusBadRequest)
server.ServeJSON(writer, map[string]any{"error": "Invalid /util request"})
Str("redirect", redirectTo)
if redirectTo == "" {
accessEv.Msg("received /util request with no referer")
} else {
http.Redirect(writer, request, path.Join(systemCritical.WebRoot, "/"), http.StatusBadRequest)
accessEv.Msg("received invalid /util request")
}
accessEv.Discard()
server.ServeError(writer, server.NewServerError("bad /util request", http.StatusBadRequest), wantsJSON, nil)
return
}
var err error
var id int
var checkedPosts []int
for key, val := range request.Form {
for key, val := range request.PostForm {
// get checked posts into an array
if _, err = fmt.Sscanf(key, "check%d", &id); err != nil || val[0] != "on" {
err = nil
@ -146,11 +146,6 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
Str("board", board).
Str("IP", gcutil.GetRealIP(request)).Send()
redirectTo := request.Referer()
if redirectTo == "" {
// request doesn't have a referer for some reason, redirect to board
redirectTo = config.WebPath(board)
}
http.Redirect(writer, request, redirectTo, http.StatusFound)
return
}

View file

@ -9,11 +9,13 @@ import (
"time"
"github.com/DATA-DOG/go-sqlmock"
_ "github.com/go-sql-driver/mysql"
"github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcutil"
)
const (
@ -352,6 +354,7 @@ func setupDBConn(cfg *config.SQLConfig) (db *GCDB, err error) {
case "mysql":
db.connStr = fmt.Sprintf(mysqlConnStr, cfg.DBusername, cfg.DBpassword, cfg.DBhost, cfg.DBname)
replacerArr = append(replacerArr, mysqlReplacerArr...)
mysql.SetLogger(gcutil.Logger())
case "postgres":
db.connStr = fmt.Sprintf(postgresConnStr, cfg.DBusername, cfg.DBpassword, cfg.DBhost, cfg.DBname)
replacerArr = append(replacerArr, postgresReplacerArr...)

View file

@ -33,13 +33,14 @@ var (
TempPosts []Post
)
func GetPostFromID(id int, onlyNotDeleted bool) (*Post, error) {
func GetPostFromID(id int, onlyNotDeleted bool, requestOptions ...*RequestOptions) (*Post, error) {
query := selectPostsBaseSQL + "WHERE id = ?"
if onlyNotDeleted {
query += " AND is_deleted = FALSE"
}
post := new(Post)
err := QueryRow(nil, query, []any{id}, []any{
opts := setupOptions(requestOptions...)
err := QueryRow(opts, query, []any{id}, []any{
&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,

View file

@ -123,3 +123,37 @@ func GetReports(includeCleared bool) ([]Report, error) {
}
return reports, rows.Close()
}
// DeleteReportsOfDeletedPosts removes reports and report audits of posts that have been deleted
func DeleteReportsOfDeletedPosts(requestOptions ...*RequestOptions) error {
opts := setupOptionsWithTimeout(requestOptions...)
shouldCommit := opts.Tx == nil
var err error
if shouldCommit {
opts.Tx, err = BeginTx()
if err != nil {
return err
}
defer func() {
opts.Tx.Rollback()
opts.Tx = nil
}()
}
sql := `DELETE FROM DBPREFIXreports WHERE post_id IN (SELECT id FROM DBPREFIXposts WHERE is_deleted = TRUE)`
if _, err = ExecTimeoutSQL(opts.Tx, sql); err != nil {
return err
}
sql = `DELETE FROM DBPREFIXreports_audit WHERE report_id IN (SELECT id FROM DBPREFIXposts WHERE is_deleted = TRUE)`
if _, err = ExecTimeoutSQL(opts.Tx, sql); err != nil {
return err
}
if shouldCommit {
return opts.Tx.Commit()
}
return nil
}

View file

@ -2,6 +2,7 @@ package manage
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
@ -12,6 +13,7 @@ import (
"net/url"
"strconv"
"strings"
"time"
"github.com/gochan-org/gochan/pkg/building"
"github.com/gochan-org/gochan/pkg/config"
@ -415,46 +417,66 @@ func reportsCallback(_ http.ResponseWriter, request *http.Request, staff *gcsql.
Bool("blocked", block != "").
Msg("Report cleared")
}
rows, err := gcsql.Query(nil, `SELECT id, handled_by_staff_id as staff_id,
(SELECT username FROM DBPREFIXstaff WHERE id = DBPREFIXreports.handled_by_staff_id) as staff_user,
post_id, IP_NTOA, reason, is_cleared from DBPREFIXreports WHERE is_cleared = FALSE`)
ctx, cancel := context.WithTimeout(context.Background(), config.DefaultSQLTimeout*time.Second)
defer cancel()
requestOptions := &gcsql.RequestOptions{
Context: ctx,
Cancel: cancel,
}
if err = gcsql.DeleteReportsOfDeletedPosts(requestOptions); err != nil {
errEv.Err(err).Caller().Send()
return nil, server.NewServerError("failed to clean up reports of deleted posts", http.StatusInternalServerError)
}
rows, err := gcsql.Query(requestOptions, `SELECT id, staff_id, staff_user, post_id, ip, reason, is_cleared FROM DBPREFIXv_post_reports`)
if err != nil {
errEv.Err(err).Caller().Send()
return nil, err
}
defer rows.Close()
reports := make([]map[string]any, 0)
for rows.Next() {
var id int
var staff_id any
var staff_user []byte
var post_id int
var staffID any
var staffUser []byte
var postID int
var ip string
var reason string
var is_cleared int
err = rows.Scan(&id, &staff_id, &staff_user, &post_id, &ip, &reason, &is_cleared)
var isCleared int
err = rows.Scan(&id, &staffID, &staffUser, &postID, &ip, &reason, &isCleared)
if err != nil {
return nil, err
errEv.Err(err).Caller().Send()
return nil, server.NewServerError("failed to scan report row", http.StatusInternalServerError)
}
post, err := gcsql.GetPostFromID(post_id, true)
post, err := gcsql.GetPostFromID(postID, true, requestOptions)
if err != nil {
return nil, err
errEv.Err(err).Caller().Msg("failed to get post from ID")
return nil, server.NewServerError("failed to get post from ID", http.StatusInternalServerError)
}
staff_id_int, _ := staff_id.(int64)
staffIDint, _ := staffID.(int64)
reports = append(reports, map[string]any{
"id": id,
"staff_id": int(staff_id_int),
"staff_user": string(staff_user),
"staff_id": int(staffIDint),
"staff_user": string(staffUser),
"post_link": post.WebPath(),
"ip": ip,
"reason": reason,
"is_cleared": is_cleared,
"is_cleared": isCleared,
})
}
if wantsJSON {
return reports, err
if err = rows.Close(); err != nil {
errEv.Err(err).Caller().Send()
return nil, err
}
if wantsJSON {
return reports, nil
}
reportsBuffer := bytes.NewBufferString("")
err = serverutil.MinifyTemplate(gctemplates.ManageReports,
map[string]any{
@ -561,7 +583,7 @@ func threadAttrsCallback(_ http.ResponseWriter, request *http.Request, _ *gcsql.
if err = building.BuildThreadPages(post); err != nil {
return "", err
}
fmt.Println("Done rebuilding", board.Dir)
gcutil.LogInfo().Msg("Done rebuilding")
}
data["thread"] = thread
}

View file

@ -1,6 +1,7 @@
-- SQL views for simplifying queries in gochan
-- First drop views if they exist in reverse order to avoid dependency issues
DROP VIEW IF EXISTS DBPREFIXv_post_reports;
DROP VIEW IF EXISTS DBPREFIXv_post_with_board;
DROP VIEW IF EXISTS DBPREFIXv_top_post_board_dir;
DROP VIEW IF EXISTS DBPREFIXv_upload_info;
@ -99,3 +100,8 @@ banned_message, ip, flag, country, dir, board_id
FROM DBPREFIXposts p
LEFT JOIN DBPREFIXthreads t ON t.id = p.thread_id
LEFT JOIN DBPREFIXboards b ON b.id = t.board_id;
CREATE VIEW DBPREFIXv_post_reports AS
SELECT r.id, handled_by_staff_id AS staff_id, username AS staff_user, post_id, IP_NTOA as ip, reason, is_cleared
FROM DBPREFIXreports r LEFT JOIN DBPREFIXstaff s ON handled_by_staff_id = s.id
WHERE is_cleared = FALSE;

View file

@ -34,8 +34,8 @@
<img src="{{getThumbnailWebPath $.post.ID}}" alt="{{webPath $.board.Dir "src" .Filename}}" width="{{.ThumbnailWidth}}" height="{{.ThumbnailHeight}}" class="upload" />
</td></tr>
{{- end -}}
<tr><th>Spoiler</th><td><input type="checkbox" name="spoiler" id="spoiler" {{with .upload}}{{if .IsSpoilered}}checked{{end}}{{end}}></td></tr>
<tr><th>Replace</th><td>
<tr><th class="postblock">Spoiler</th><td><input type="checkbox" name="spoiler" id="spoiler" {{with .upload}}{{if .IsSpoilered}}checked{{end}}{{end}}></td></tr>
<tr><th class="postblock">Replace</th><td>
<input name="imagefile" type="file" accept="image/jpeg,image/png,image/gif,video/webm,video/mp4" onchange="var sub = document.getElementById('update-file'); if(this.value != '') { sub.disabled = false; sub.value = 'Update file'; } else { sub.disabled = true; sub.value = 'No file selected'}"/>
</td></tr>
</table>