1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-03 11:46:22 -07:00

Add filename and username banning, add more de-deprecation stuff to posting/post.go

This commit is contained in:
Eggbertx 2022-11-07 12:56:51 -08:00
parent b69536b772
commit 379e846daf
10 changed files with 531 additions and 405 deletions

View file

@ -66,34 +66,34 @@ func (s gochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
// set mime type/cache headers according to the file's extension // set mime type/cache headers according to the file's extension
func (*gochanServer) setFileHeaders(filename string, writer http.ResponseWriter) { func (*gochanServer) setFileHeaders(filename string, writer http.ResponseWriter) {
extension := strings.ToLower(gcutil.GetFileExtension(filename)) extension := strings.ToLower(path.Ext(filename))
switch extension { switch extension {
case "png": case ".png":
writer.Header().Set("Content-Type", "image/png") writer.Header().Set("Content-Type", "image/png")
writer.Header().Set("Cache-Control", "max-age=86400") writer.Header().Set("Cache-Control", "max-age=86400")
case "gif": case ".gif":
writer.Header().Set("Content-Type", "image/gif") writer.Header().Set("Content-Type", "image/gif")
writer.Header().Set("Cache-Control", "max-age=86400") writer.Header().Set("Cache-Control", "max-age=86400")
case "jpg": case ".jpg":
fallthrough fallthrough
case "jpeg": case ".jpeg":
writer.Header().Set("Content-Type", "image/jpeg") writer.Header().Set("Content-Type", "image/jpeg")
writer.Header().Set("Cache-Control", "max-age=86400") writer.Header().Set("Cache-Control", "max-age=86400")
case "css": case ".css":
writer.Header().Set("Content-Type", "text/css") writer.Header().Set("Content-Type", "text/css")
writer.Header().Set("Cache-Control", "max-age=43200") writer.Header().Set("Cache-Control", "max-age=43200")
case "js": case ".js":
writer.Header().Set("Content-Type", "text/javascript") writer.Header().Set("Content-Type", "text/javascript")
writer.Header().Set("Cache-Control", "max-age=43200") writer.Header().Set("Cache-Control", "max-age=43200")
case "json": case ".json":
writer.Header().Set("Content-Type", "application/json") writer.Header().Set("Content-Type", "application/json")
writer.Header().Set("Cache-Control", "max-age=5, must-revalidate") writer.Header().Set("Cache-Control", "max-age=5, must-revalidate")
case "webm": case ".webm":
writer.Header().Set("Content-Type", "video/webm") writer.Header().Set("Content-Type", "video/webm")
writer.Header().Set("Cache-Control", "max-age=86400") writer.Header().Set("Cache-Control", "max-age=86400")
case "htm": case ".htm":
fallthrough fallthrough
case "html": case ".html":
writer.Header().Set("Content-Type", "text/html") writer.Header().Set("Content-Type", "text/html")
writer.Header().Set("Cache-Control", "max-age=5, must-revalidate") writer.Header().Set("Cache-Control", "max-age=5, must-revalidate")
default: default:
@ -135,7 +135,7 @@ func initServer() {
fmt.Println("Got error when initializing Akismet spam protection, it will be disabled:", err) fmt.Println("Got error when initializing Akismet spam protection, it will be disabled:", err)
} }
server.namespaces["banned"] = posting.BanHandler // server.namespaces["banned"] = posting.BanHandler
server.namespaces["captcha"] = posting.ServeCaptcha server.namespaces["captcha"] = posting.ServeCaptcha
server.namespaces["manage"] = manage.CallManageFunction server.namespaces["manage"] = manage.CallManageFunction
server.namespaces["post"] = posting.MakePost server.namespaces["post"] = posting.MakePost

View file

@ -1,18 +1,26 @@
package gcsql package gcsql
import "database/sql" import (
"database/sql"
"regexp"
)
type Ban interface {
IsGlobalBan() bool
}
// CheckIPBan returns the latest active IP ban for the given IP, as well as any errors. If the // CheckIPBan returns the latest active IP ban for the given IP, as well as any errors. If the
// IPBan pointer is nil, the IP has no active bans // IPBan pointer is nil, the IP has no active bans
func CheckIPBan(ip string) (*IPBan, error) { func CheckIPBan(ip string, boardID int) (*IPBan, error) {
const query = `SELECT const query = `SELECT
id, staff_id, board_id, banned_for_post_id, copy_post_text, is_thread_ban, 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, is_active, ip, issued_at, appeal_at, expires_at, permanent, staff_note,
message, can_appeal message, can_appeal
FROM DBPREFIXip_ban WHERE ip = ? AND is_active AND (expires_at > CURRENT_TIMESTAMP OR permanent) FROM DBPREFIXip_ban WHERE ip = ? AND (board_id IS NULL OR board_id = ?) AND
is_active AND (expires_at > CURRENT_TIMESTAMP OR permanent)
ORDER BY id DESC LIMIT 1` ORDER BY id DESC LIMIT 1`
var ban IPBan var ban IPBan
err := QueryRowSQL(query, interfaceSlice(ip), interfaceSlice( err := QueryRowSQL(query, interfaceSlice(ip, boardID), interfaceSlice(
&ban.ID, &ban.StaffID, &ban.BoardID, &ban.BannedForPostID, &ban.CopyPostText, &ban.IsThreadBan, &ban.ID, &ban.StaffID, &ban.BoardID, &ban.BannedForPostID, &ban.CopyPostText, &ban.IsThreadBan,
&ban.IsActive, &ban.IP, &ban.IssuedAt, &ban.AppealAt, &ban.ExpiresAt, &ban.Permanent, &ban.StaffNote, &ban.IsActive, &ban.IP, &ban.IssuedAt, &ban.AppealAt, &ban.ExpiresAt, &ban.Permanent, &ban.StaffNote,
&ban.Message, &ban.CanAppeal)) &ban.Message, &ban.CanAppeal))
@ -23,6 +31,70 @@ func CheckIPBan(ip string) (*IPBan, error) {
} }
// IsGlobalBan returns true if BoardID is a nil int, meaning they are banned on all boards, as opposed to a specific one // IsGlobalBan returns true if BoardID is a nil int, meaning they are banned on all boards, as opposed to a specific one
func (ipb *IPBan) IsGlobalBan() bool { func (ipb IPBan) IsGlobalBan() bool {
return ipb.BoardID == nil return ipb.BoardID == nil
} }
func checkUsernameOrFilename(usernameFilename string, check string, boardID int) (*filenameOrUsernameBanBase, error) {
query := `SELECT
id, board_id, staff_id, staff_note, issued_at, ` + usernameFilename + `, is_regex
FROM DBPREFIX` + usernameFilename + `_ban WHERE (` + usernameFilename + ` = ? OR is_regex) AND (board_id IS NULL OR board_id = ?)`
rows, err := QuerySQL(query, check, boardID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var ban filenameOrUsernameBanBase
err = rows.Scan(&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.check, &ban.IsRegex)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
if ban.IsRegex {
match, err := regexp.MatchString(ban.check, check)
if err != nil {
return nil, err
}
if match {
return &ban, nil
}
} else if ban.check == check {
return &ban, nil
}
}
return nil, nil
}
func CheckNameBan(name string, boardID int) (*UsernameBan, error) {
banBase, err := checkUsernameOrFilename("username", name, boardID)
if err != nil {
return nil, err
}
if banBase == nil {
return nil, nil
}
return &UsernameBan{
Username: banBase.check,
filenameOrUsernameBanBase: *banBase,
}, nil
}
func (ub filenameOrUsernameBanBase) IsGlobalBan() bool {
return ub.BoardID == nil
}
func CheckFilenameBan(filename string, boardID int) (*FilenameBan, error) {
banBase, err := checkUsernameOrFilename("filename", filename, boardID)
if err != nil {
return nil, err
}
if banBase == nil {
return nil, nil
}
return &FilenameBan{
Filename: banBase.check,
filenameOrUsernameBanBase: *banBase,
}, nil
}

View file

@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"html"
"html/template" "html/template"
"time" "time"
@ -231,6 +232,18 @@ func (p *Post) GetUpload() (*Upload, error) {
return upload, err return upload, err
} }
// Sanitize escapes HTML strings in a post. This should be run immediately before
// the post is inserted into the database
func (p *Post) Sanitize() {
if !p.sanitized {
p.Name = html.EscapeString(p.Name)
p.Email = html.EscapeString(p.Email)
p.Subject = html.EscapeString(p.Subject)
p.Password = html.EscapeString(p.Password)
p.sanitized = true
}
}
// UnlinkUploads disassociates the post with any uploads in DBPREFIXfiles // UnlinkUploads disassociates the post with any uploads in DBPREFIXfiles
// that may have been uploaded with it, optionally leaving behind a "File Deleted" // that may have been uploaded with it, optionally leaving behind a "File Deleted"
// frame where the thumbnail appeared // frame where the thumbnail appeared

View file

@ -60,16 +60,22 @@ type FileBan struct {
Checksum string `json:"checksum"` // sql: `checksum` Checksum string `json:"checksum"` // sql: `checksum`
} }
type filenameOrUsernameBanBase struct {
ID int // sql: id
BoardID *int // sql: board_id
StaffID int // sql: staff_id
StaffNote string // sql: staff_note
IssuedAt time.Time // sql: issued_at
check string // replaced with username or filename
IsRegex bool // sql: is_regex
}
// FilenameBan represents a ban on a specific filename or filename regular expression. // FilenameBan represents a ban on a specific filename or filename regular expression.
// table: DBPREFIXfilename_ban // table: DBPREFIXfilename_ban
type FilenameBan struct { type FilenameBan struct {
ID int `json:"id"` // sql: `id` filenameOrUsernameBanBase
BoardID int `json:"board_id"` // sql: `board_id` Filename string // sql: `filename`
StaffID int `json:"staff_id"` // sql: `staff_id` IsRegex bool // sql: `is_regex`
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`
} }
// Upload represents a file attached to a post. // Upload represents a file attached to a post.
@ -161,6 +167,8 @@ type Post struct {
DeletedAt time.Time // sql: `deleted_at` DeletedAt time.Time // sql: `deleted_at`
IsDeleted bool // sql: `is_deleted` IsDeleted bool // sql: `is_deleted`
BannedMessage string // sql: `banned_message` BannedMessage string // sql: `banned_message`
sanitized bool
} }
// table: DBPREFIXreports // table: DBPREFIXreports
@ -224,13 +232,8 @@ type Thread struct {
// table: DBPREFIXusername_ban // table: DBPREFIXusername_ban
type UsernameBan struct { type UsernameBan struct {
ID int `json:"id"` // sql: `id` filenameOrUsernameBanBase
BoardID *int `json:"board"` // sql: `board_id` Username string // sql: `username`
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 // table DBPREFIXwordfilters

View file

@ -1,6 +1,8 @@
package gcsql package gcsql
import ( import (
"errors"
"github.com/gochan-org/gochan/pkg/gcsql.bak" "github.com/gochan-org/gochan/pkg/gcsql.bak"
"github.com/gochan-org/gochan/pkg/gcutil" "github.com/gochan-org/gochan/pkg/gcutil"
) )
@ -12,10 +14,9 @@ const (
FROM DBPREFIXfiles ` FROM DBPREFIXfiles `
) )
// ThumbnailPath returns the thumbnail path of the upload, given an thumbnail type ("thumbnail" or "catalog") var (
func (u *Upload) ThumbnailPath(thumbType string) string { ErrAlreadyAttached = errors.New("upload already processed")
return gcutil.GetThumbnailPath(thumbType, u.Filename) )
}
// GetThreadFiles gets a list of the files owned by posts in the thread, including thumbnails for convenience. // GetThreadFiles gets a list of the files owned by posts in the thread, including thumbnails for convenience.
func GetThreadFiles(post *Post) ([]Upload, error) { func GetThreadFiles(post *Post) ([]Upload, error) {
@ -40,3 +41,30 @@ func GetThreadFiles(post *Post) ([]Upload, error) {
} }
return uploads, nil return uploads, nil
} }
func (p *Post) AttachFile(upload *Upload) error {
const query = `INSERT INTO DBPREFIXfiles (
post_id, file_order, original_filename, filename, checksum, file_size,
is_spoilered, thumbnail_width, thumbnail_height, width, height)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`
if upload.ID > 0 {
return ErrAlreadyAttached
}
uploadID, err := getNextFreeID("DBPREFIXfiles")
if err != nil {
return err
}
if _, err = ExecSQL(query,
&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 err
}
upload.ID = uploadID
return nil
}
// 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)
}

View file

@ -153,51 +153,51 @@ var funcMap = template.FuncMap{
return dict, nil return dict, nil
}, },
// Imageboard functions // Imageboard functions
"bannedForever": func(banInfo *gcsql.BanInfo) bool { // "bannedForever": func(banInfo *gcsql.BanInfo) bool {
return banInfo.BannedForever() // return banInfo.BannedForever()
}, // },
"isBanned": func(banInfo *gcsql.BanInfo, board string) bool { // "isBanned": func(banInfo *gcsql.BanInfo, board string) bool {
return banInfo.IsBanned(board) // return banInfo.IsBanned(board)
}, // },
"isOP": func(post gcsql.Post) bool { // "isOP": func(post gcsql.Post) bool {
return post.ParentID == 0 // return post.ParentID == 0
}, // },
"getCatalogThumbnail": func(img string) string { "getCatalogThumbnail": func(img string) string {
return gcutil.GetThumbnailPath("catalog", img) return gcutil.GetThumbnailPath("catalog", img)
}, },
"getThreadID": func(postInterface interface{}) (thread int) { // "getThreadID": func(postInterface interface{}) (thread int) {
post, ok := postInterface.(gcsql.Post) // post, ok := postInterface.(gcsql.Post)
if !ok { // if !ok {
thread = 0 // thread = 0
} else if post.ParentID == 0 { // } else if post.ParentID == 0 {
thread = post.ID // thread = post.ID
} else { // } else {
thread = post.ParentID // thread = post.ParentID
} // }
return // return
}, // },
"getPostURL": func(postInterface interface{}, typeOf string, withDomain bool) (postURL string) { // "getPostURL": func(postInterface interface{}, typeOf string, withDomain bool) (postURL string) {
systemCritical := config.GetSystemCriticalConfig() // systemCritical := config.GetSystemCriticalConfig()
if withDomain { // if withDomain {
postURL = systemCritical.SiteDomain // postURL = systemCritical.SiteDomain
} // }
postURL += systemCritical.WebRoot // postURL += systemCritical.WebRoot
if typeOf == "recent" { // if typeOf == "recent" {
post, ok := postInterface.(gcsql.RecentPost) // post, ok := postInterface.(gcsql.RecentPost)
if !ok { // if !ok {
return // return
} // }
postURL = post.GetURL(withDomain) // postURL = post.GetURL(withDomain)
} else { // } else {
post, ok := postInterface.(*gcsql.Post) // post, ok := postInterface.(*gcsql.Post)
if !ok { // if !ok {
return // return
} // }
postURL = post.GetURL(withDomain) // postURL = post.GetURL(withDomain)
} // }
return // return
}, // },
"getThreadThumbnail": func(img string) string { "getThreadThumbnail": func(img string) string {
return gcutil.GetThumbnailPath("thread", img) return gcutil.GetThumbnailPath("thread", img)
}, },

View file

@ -109,12 +109,6 @@ func GetFileParts(filename string) (string, string, string) {
return base, noExt, ext return base, noExt, ext
} }
// GetFileExtension returns the given file's extension, or a blank string if it has none
func GetFileExtension(filename string) string {
_, _, ext := GetFileParts(filename)
return ext
}
// GetFormattedFilesize returns a human readable filesize // GetFormattedFilesize returns a human readable filesize
func GetFormattedFilesize(size float64) string { func GetFormattedFilesize(size float64) string {
if size < 1000 { if size < 1000 {
@ -144,19 +138,41 @@ func GetRealIP(request *http.Request) string {
return remoteHost return remoteHost
} }
// GetThumbnailExt returns the extension to be used when creating a thumbnail of img. For non-image files,
// it just returns the extension, in which case a generic icon will be (eventually) used
func GetThumbnailExt(filename string) string {
ext := filepath.Ext(strings.ToLower(filename))
switch ext {
case ".gif":
fallthrough
case ".png":
fallthrough
case ".webm":
fallthrough
case ".webp":
return "png"
case ".jpg":
fallthrough
case ".jpeg":
fallthrough
case "mp4":
return "jpg"
default:
// invalid file format
return ""
}
}
// GetThumbnailPath returns the thumbnail path of the given filename // GetThumbnailPath returns the thumbnail path of the given filename
func GetThumbnailPath(thumbType string, img string) string { func GetThumbnailPath(thumbType string, img string) string {
filetype := strings.ToLower(img[strings.LastIndex(img, ".")+1:]) ext := GetThumbnailExt(img)
if filetype == "gif" || filetype == "webm" || filetype == "mp4" {
filetype = "jpg"
}
index := strings.LastIndex(img, ".") index := strings.LastIndex(img, ".")
if index < 0 || index > len(img) { if index < 0 || index > len(img) {
return "" return ""
} }
thumbSuffix := "t." + filetype thumbSuffix := "t." + ext
if thumbType == "catalog" { if thumbType == "catalog" {
thumbSuffix = "c." + filetype thumbSuffix = "c." + ext
} }
return img[0:index] + thumbSuffix return img[0:index] + thumbSuffix
} }
@ -273,26 +289,3 @@ func StripHTML(htmlIn string) string {
} }
return "" return ""
} }
func ThumbnailExtension(filename string) string {
ext := filepath.Ext(strings.ToLower(filename))
switch ext {
case ".gif":
fallthrough
case ".png":
fallthrough
case ".webm":
fallthrough
case ".webp":
return "png"
case ".jpg":
fallthrough
case ".jpeg":
fallthrough
case "mp4":
return "jpg"
default:
// invalid file format
return ""
}
}

View file

@ -5,15 +5,12 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"html"
"net" "net"
"net/http" "net/http"
"os" "os"
"path" "path"
"regexp" "regexp"
"strconv" "strconv"
"strings"
"time"
"github.com/gochan-org/gochan/pkg/building" "github.com/gochan-org/gochan/pkg/building"
"github.com/gochan-org/gochan/pkg/config" "github.com/gochan-org/gochan/pkg/config"
@ -117,15 +114,14 @@ var actions = []Action{
} }
return "You are not logged in", err return "You are not logged in", err
} }
numSessions, err := staff.CleanSessions() if err = staff.ClearSessions(); err != nil && err != sql.ErrNoRows {
if err != nil && err != sql.ErrNoRows {
// something went wrong when trying to clean out sessions for this user // something went wrong when trying to clean out sessions for this user
return nil, err return nil, err
} }
serverutil.DeleteCookie(writer, request, "sessiondata") serverutil.DeleteCookie(writer, request, "sessiondata")
gcutil.LogInfo(). gcutil.LogAccess(request).
Str("clearSessions", staff.Username). Str("clearSessions", staff.Username).
Int64("cleared", numSessions) Send()
if !wantsJSON { if !wantsJSON {
http.Redirect(writer, request, http.Redirect(writer, request,
config.GetSystemCriticalConfig().WebRoot+"manage", config.GetSystemCriticalConfig().WebRoot+"manage",
@ -197,7 +193,7 @@ var actions = []Action{
} }
return manageRecentsBuffer.String(), nil return manageRecentsBuffer.String(), nil
}}, }},
{ /* {
ID: "filebans", ID: "filebans",
Title: "File bans", Title: "File bans",
Permissions: ModPerms, Permissions: ModPerms,
@ -334,7 +330,7 @@ var actions = []Action{
} }
return manageBansBuffer.String(), nil return manageBansBuffer.String(), nil
}, },
}, }, */
{ {
ID: "ipbans", ID: "ipbans",
Title: "IP Bans", Title: "IP Bans",
@ -344,7 +340,7 @@ var actions = []Action{
return "", gcutil.ErrNotImplemented return "", gcutil.ErrNotImplemented
}, },
}, },
{ /* {
ID: "bans", ID: "bans",
Title: "Bans", Title: "Bans",
Permissions: ModPerms, Permissions: ModPerms,
@ -479,7 +475,7 @@ var actions = []Action{
} }
outputStr += manageBansBuffer.String() outputStr += manageBansBuffer.String()
return outputStr, nil return outputStr, nil
}}, }}, */
{ {
ID: "ipsearch", ID: "ipsearch",
Title: "IP Search", Title: "IP Search",
@ -573,6 +569,7 @@ var actions = []Action{
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close()
reports := make([]map[string]interface{}, 0) reports := make([]map[string]interface{}, 0)
for rows.Next() { for rows.Next() {
var id int var id int
@ -1063,6 +1060,7 @@ var actions = []Action{
Permissions: AdminPerms, Permissions: AdminPerms,
Callback: func(writer http.ResponseWriter, request *http.Request, staff *gcsql.Staff, wantsJSON bool) (output interface{}, err error) { Callback: func(writer http.ResponseWriter, request *http.Request, staff *gcsql.Staff, wantsJSON bool) (output interface{}, err error) {
var outputStr string var outputStr string
messages, err := gcsql.GetAllNondeletedMessageRaw() messages, err := gcsql.GetAllNondeletedMessageRaw()
if err != nil { if err != nil {
return "", err return "", err
@ -1192,7 +1190,7 @@ var actions = []Action{
request.FormValue("find"), request.FormValue("find"),
request.FormValue("replace"), request.FormValue("replace"),
request.FormValue("isregex") == "on", request.FormValue("isregex") == "on",
strings.Split(request.FormValue("boarddirs"), ","), request.FormValue("boarddirs"),
staff.ID, staff.ID,
request.FormValue("staffnote")) request.FormValue("staffnote"))
} }
@ -1200,11 +1198,11 @@ var actions = []Action{
return err, err return err, err
} }
wordfilters, err := gcsql.GetWordFilters() wordfilters, err := gcsql.GetWordfilters()
if err != nil { if err != nil {
return wordfilters, nil return wordfilters, nil
} }
var editFilter *gcsql.WordFilter var editFilter *gcsql.Wordfilter
if editIDstr != "" { if editIDstr != "" {
editID := gcutil.HackyStringToInt(editIDstr) editID := gcutil.HackyStringToInt(editIDstr)
for _, filter := range wordfilters { for _, filter := range wordfilters {

View file

@ -1,11 +1,6 @@
package posting package posting
import ( import (
"crypto/md5"
"database/sql"
"fmt"
"html"
"io"
"net/http" "net/http"
"github.com/gochan-org/gochan/pkg/config" "github.com/gochan-org/gochan/pkg/config"
@ -15,82 +10,114 @@ import (
"github.com/gochan-org/gochan/pkg/serverutil" "github.com/gochan-org/gochan/pkg/serverutil"
) )
const ( func showBanpage(ban gcsql.Ban, banType string, filename string, post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWriter, request *http.Request) {
_ = iota // TODO: possibly split file/username/filename bans into separate page template
ThreadBan err := serverutil.MinifyTemplate(gctemplates.Banpage, map[string]interface{}{
ImageBan "systemCritical": config.GetSystemCriticalConfig(),
FullBan "siteConfig": config.GetSiteConfig(),
) "boardConfig": config.GetBoardConfig(postBoard.Dir),
"ban": ban,
// BanHandler is used for serving ban pages "board": postBoard,
func BanHandler(writer http.ResponseWriter, request *http.Request) { }, writer, "text/html")
appealMsg := request.FormValue("appealmsg") if err != nil {
// banStatus, err := getBannedStatus(request) // TODO refactor to use ipban
var banStatus gcsql.BanInfo
var err error
systemCritical := config.GetSystemCriticalConfig()
siteConfig := config.GetSiteConfig()
boardConfig := config.GetBoardConfig("")
if appealMsg != "" {
if banStatus.BannedForever() {
fmt.Fprint(writer, "No.")
return
}
escapedMsg := html.EscapeString(appealMsg)
if err = gcsql.AddBanAppeal(banStatus.ID, escapedMsg); err != nil {
serverutil.ServeErrorPage(writer, err.Error())
}
fmt.Fprint(writer,
"Appeal sent. It will (hopefully) be read by a staff member. check "+systemCritical.WebRoot+"banned occasionally for a response",
)
return
}
if err != nil && err != sql.ErrNoRows {
gcutil.LogError(err).Msg("Failed getting banned status")
serverutil.ServeErrorPage(writer, "Error getting banned status: "+err.Error())
return
}
if err = serverutil.MinifyTemplate(gctemplates.Banpage, map[string]interface{}{
"systemCritical": systemCritical,
"siteConfig": siteConfig,
"boardConfig": boardConfig,
"ban": banStatus,
"banBoards": banStatus.Boards,
"post": gcsql.Post{},
}, writer, "text/html"); err != nil {
gcutil.LogError(err). gcutil.LogError(err).
Str("template", "banpage"). Str("IP", post.IP).
Msg("Failed minifying template") Str("building", "minifier").
serverutil.ServeErrorPage(writer, "Error minifying page template: "+err.Error()) Str("banType", banType).
Str("template", "banpage.html").Send()
serverutil.ServeErrorPage(writer, "Error minifying page: "+err.Error())
return return
} }
ev := gcutil.LogInfo().
Str("IP", post.IP).
Str("boardDir", postBoard.Dir).
Str("banType", banType)
switch banType {
case "ip":
ev.Msg("Rejected post from banned IP")
case "username":
ev.
Str("name", post.Name).
Str("tripcode", post.Tripcode).
Msg("Rejected post with banned name/tripcode")
case "filename":
ev.
Str("filename", filename).
Msg("Rejected post with banned filename")
}
} }
// Checks check poster's name/tripcode/file checksum (from Post post) for banned status // func BanHandler(writer http.ResponseWriter, request *http.Request) {
// returns ban table if the user is banned or sql.ErrNoRows if they aren't // ip := gcutil.GetRealIP(request)
func getBannedStatus(request *http.Request) (*gcsql.BanInfo, error) { // ipBan, err := gcsql.CheckIPBan(ip, 0)
formName := request.FormValue("postname") // if err != nil {
var tripcode string // gcutil.LogError(err).
if formName != "" { // Str("IP", ip).
parsedName := gcutil.ParseName(formName) // Msg("Error checking IP banned status (/banned request)")
tripcode += parsedName["name"] // serverutil.ServeErrorPage(writer, "Error checking banned status: "+err.Error())
if tc, ok := parsedName["tripcode"]; ok { // return
tripcode += "!" + tc // }
}
}
ip := gcutil.GetRealIP(request)
var filename string // }
var checksum string
file, fileHandler, err := request.FormFile("imagefile") // checks the post for spam. It returns true if a ban page or an error page was served (causing MakePost() to return)
if err == nil { func checkIpBan(post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWriter, request *http.Request) bool {
html.EscapeString(fileHandler.Filename) ipBan, err := gcsql.CheckIPBan(post.IP, postBoard.ID)
if data, err2 := io.ReadAll(file); err2 == nil { if err != nil {
checksum = fmt.Sprintf("%x", md5.Sum(data)) gcutil.LogError(err).
Str("IP", post.IP).
Str("boardDir", postBoard.Dir).
Msg("Error getting IP banned status")
serverutil.ServeErrorPage(writer, "Error getting ban info"+err.Error())
return true
} }
file.Close() if ipBan == nil {
return false // ip is not banned and there were no errors, keep going
} }
return gcsql.CheckBan(ip, tripcode, filename, checksum) // IP is banned
showBanpage(ipBan, "ip", "", post, postBoard, writer, request)
return true
}
func checkUsernameBan(formName string, post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWriter, request *http.Request) bool {
if formName == "" {
return false
}
nameBan, err := gcsql.CheckNameBan(formName, postBoard.ID)
if err != nil {
gcutil.LogError(err).
Str("IP", post.IP).
Str("name", formName).
Str("boardDir", postBoard.Dir).
Msg("Error getting name banned status")
serverutil.ServeErrorPage(writer, "Error getting name ban info")
return true
}
if nameBan == nil {
return false // name is not banned
}
showBanpage(nameBan, "username", "", post, postBoard, writer, request)
return true
}
func checkFilenameBan(filename string, post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWriter, request *http.Request) bool {
if filename == "" {
return false
}
filenameBan, err := gcsql.CheckFilenameBan(filename, postBoard.ID)
if err != nil {
gcutil.LogError(err).
Str("IP", post.IP).
Str("filename", filename).
Str("boardDir", postBoard.Dir).
Msg("Error getting name banned status")
serverutil.ServeErrorPage(writer, "Error getting filename ban info")
return true
}
if filenameBan == nil {
return false
}
showBanpage(filenameBan, "filename", filename, post, postBoard, writer, request)
return true
} }

View file

@ -1,9 +1,8 @@
package posting package posting
import ( import (
"bytes"
"crypto/md5" "crypto/md5"
"database/sql" "errors"
"fmt" "fmt"
"html" "html"
"image" "image"
@ -13,6 +12,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@ -22,7 +22,6 @@ import (
"github.com/gochan-org/gochan/pkg/building" "github.com/gochan-org/gochan/pkg/building"
"github.com/gochan-org/gochan/pkg/config" "github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql" "github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gctemplates"
"github.com/gochan-org/gochan/pkg/gcutil" "github.com/gochan-org/gochan/pkg/gcutil"
"github.com/gochan-org/gochan/pkg/serverutil" "github.com/gochan-org/gochan/pkg/serverutil"
) )
@ -31,9 +30,21 @@ const (
yearInSeconds = 31536000 yearInSeconds = 31536000
) )
var (
ErrorPostTooLong = errors.New("post is too long")
)
func rejectPost(reasonShort string, reasonLong string, data map[string]interface{}, writer http.ResponseWriter, request *http.Request) {
gcutil.LogError(errors.New(reasonLong)).
Str("rejectedPost", reasonShort).
Str("IP", gcutil.GetRealIP(request)).
Fields(data).Send()
data["rejected"] = reasonLong
serverutil.ServeError(writer, reasonLong, serverutil.IsRequestingJSON(request), data)
}
// MakePost is called when a user accesses /post. Parse form data, then insert and build // MakePost is called when a user accesses /post. Parse form data, then insert and build
func MakePost(writer http.ResponseWriter, request *http.Request) { func MakePost(writer http.ResponseWriter, request *http.Request) {
var maxMessageLength int
var post gcsql.Post var post gcsql.Post
var formName string var formName string
var nameCookie string var nameCookie string
@ -46,17 +57,33 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
http.Redirect(writer, request, systemCritical.WebRoot, http.StatusFound) http.Redirect(writer, request, systemCritical.WebRoot, http.StatusFound)
return return
} }
wantsJSON := serverutil.IsRequestingJSON(request)
post.IP = gcutil.GetRealIP(request) post.IP = gcutil.GetRealIP(request)
post.ParentID, _ = strconv.Atoi(request.FormValue("threadid")) var err error
post.BoardID, _ = strconv.Atoi(request.FormValue("boardid")) threadidStr := request.FormValue("threadid")
var postBoard gcsql.Board if threadidStr != "" {
postBoard, err := gcsql.GetBoardFromID(post.BoardID) // post is a reply
if post.ThreadID, err = strconv.Atoi(threadidStr); err != nil {
rejectPost("invalidFormData", "Invalid form data (invalid threadid)", map[string]interface{}{
"threadidStr": threadidStr,
}, writer, request)
return
}
}
boardidStr := request.FormValue("boardid")
boardID, err := strconv.Atoi(boardidStr)
if err != nil { if err != nil {
gcutil.LogError(err). rejectPost("invalidForm", "Invalid form data (invalid boardid)", map[string]interface{}{
Int("boardid", post.BoardID). "boardidStr": boardidStr,
Str("IP", post.IP). }, writer, request)
Msg("Error getting board info") return
serverutil.ServeErrorPage(writer, "Error getting board info: "+err.Error()) }
postBoard, err := gcsql.GetBoardFromID(boardID)
if err != nil {
rejectPost("boardInfoError", "Error getting board info: "+err.Error(), map[string]interface{}{
"boardid": boardID,
}, writer, request)
return return
} }
@ -86,32 +113,23 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
} }
post.Subject = request.FormValue("postsubject") post.Subject = request.FormValue("postsubject")
post.MessageText = strings.Trim(request.FormValue("postmsg"), "\r\n") post.MessageRaw = strings.TrimSpace(request.FormValue("postmsg"))
if len(post.MessageRaw) > postBoard.MaxMessageLength {
if maxMessageLength, err = gcsql.GetMaxMessageLength(post.BoardID); err != nil { rejectPost("messageLength", "Message is too long", map[string]interface{}{
gcutil.LogError(err). "messageLength": len(post.MessageRaw),
Int("boardid", post.BoardID). "boardid": boardID,
Str("IP", post.IP). }, writer, request)
Msg("Error getting board info")
serverutil.ServeErrorPage(writer, "Error getting board info: "+err.Error())
return return
} }
if len(post.MessageText) > maxMessageLength { if post.MessageRaw, err = ApplyWordFilters(post.MessageRaw, postBoard.Dir); err != nil {
serverutil.ServeErrorPage(writer, "Post body is too long") rejectPost("wordfilterError", "Error formatting post: "+err.Error(), map[string]interface{}{
"boardDir": postBoard.Dir,
}, writer, request)
return return
} }
if post.MessageText, err = ApplyWordFilters(post.MessageText, postBoard.Dir); err != nil { post.Message = FormatMessage(post.MessageRaw, postBoard.Dir)
gcutil.LogError(err).
Str("IP", post.IP).
Str("boardDir", postBoard.Dir).
Msg("Error applying wordfilters")
serverutil.ServeErrorPage(writer, "Error formatting post: "+err.Error())
return
}
post.MessageHTML = FormatMessage(post.MessageText, postBoard.Dir)
password := request.FormValue("postpassword") password := request.FormValue("postpassword")
if password == "" { if password == "" {
password = gcutil.RandomString(8) password = gcutil.RandomString(8)
@ -135,11 +153,11 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
MaxAge: yearInSeconds, MaxAge: yearInSeconds,
}) })
post.Timestamp = time.Now() post.CreatedOn = time.Now()
// post.PosterAuthority = getStaffRank(request) // post.PosterAuthority = getStaffRank(request)
post.Bumped = time.Now() // bumpedTimestamp := time.Now()
post.Stickied = request.FormValue("modstickied") == "on" // isSticky := request.FormValue("modstickied") == "on"
post.Locked = request.FormValue("modlocked") == "on" // isLocked := request.FormValue("modlocked") == "on"
//post has no referrer, or has a referrer from a different domain, probably a spambot //post has no referrer, or has a referrer from a different domain, probably a spambot
if !serverutil.ValidReferer(request) { if !serverutil.ValidReferer(request) {
@ -147,12 +165,13 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
Str("spam", "badReferer"). Str("spam", "badReferer").
Str("IP", post.IP). Str("IP", post.IP).
Msg("Rejected post from possible spambot") Msg("Rejected post from possible spambot")
serverutil.ServeError(writer, "Your post looks like spam", wantsJSON, nil)
return return
} }
akismetResult := serverutil.CheckPostForSpam( akismetResult := serverutil.CheckPostForSpam(
post.IP, request.Header.Get("User-Agent"), request.Referer(), post.IP, request.Header.Get("User-Agent"), request.Referer(),
post.Name, post.Email, post.MessageText, post.Name, post.Email, post.MessageRaw,
) )
logEvent := gcutil.LogInfo(). logEvent := gcutil.LogInfo().
Str("User-Agent", request.Header.Get("User-Agent")). Str("User-Agent", request.Header.Get("User-Agent")).
@ -160,55 +179,41 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
switch akismetResult { switch akismetResult {
case "discard": case "discard":
logEvent.Str("akismet", "discard").Send() logEvent.Str("akismet", "discard").Send()
serverutil.ServeErrorPage(writer, "Your post looks like spam.") serverutil.ServeError(writer, "Your post looks like spam.", wantsJSON, nil)
return return
case "spam": case "spam":
logEvent.Str("akismet", "spam").Send() logEvent.Str("akismet", "spam").Send()
serverutil.ServeErrorPage(writer, "Your post looks like spam.") serverutil.ServeError(writer, "Your post looks like spam.", wantsJSON, nil)
return return
default: default:
logEvent.Discard() logEvent.Discard()
} }
postDelay, _ := gcsql.SinceLastPost(post.IP) var delay int
if postDelay > -1 { var tooSoon bool
if post.ParentID == 0 && postDelay < boardConfig.NewThreadDelay { if threadidStr == "" {
serverutil.ServeErrorPage(writer, "Please wait before making a new thread.") // creating a new thread
return delay, err = gcsql.SinceLastThread(post.IP)
} else if post.ParentID > 0 && postDelay < boardConfig.ReplyDelay { tooSoon = delay < boardConfig.NewThreadDelay
serverutil.ServeErrorPage(writer, "Please wait before making a reply.") } else {
delay, err = gcsql.SinceLastPost(post.IP)
tooSoon = delay < boardConfig.ReplyDelay
}
if err != nil {
rejectPost("cooldownError", "Error checking post cooldown: "+err.Error(), map[string]interface{}{
"boardDir": postBoard.Dir,
}, writer, request)
return return
} }
} if tooSoon {
rejectPost("cooldownError", "Please wait before making a new post", map[string]interface{}{}, writer, request)
banStatus, err := getBannedStatus(request)
if err != nil && err != sql.ErrNoRows {
gcutil.LogError(err).
Str("IP", post.IP).
Fields(gcutil.ParseName(formName)).
Msg("Error getting banned status")
serverutil.ServeErrorPage(writer, "Error getting banned status: "+err.Error())
return return
} }
boards, _ := gcsql.GetAllBoards() if checkIpBan(&post, postBoard, writer, request) {
if banStatus != nil && banStatus.IsBanned(postBoard.Dir) {
var banpageBuffer bytes.Buffer
if err = serverutil.MinifyTemplate(gctemplates.Banpage, map[string]interface{}{
"systemCritical": config.GetSystemCriticalConfig(),
"siteConfig": config.GetSiteConfig(),
"boardConfig": config.GetBoardConfig(""),
"ban": banStatus,
"banBoards": boards[post.BoardID-1].Dir,
}, writer, "text/html"); err != nil {
gcutil.LogError(err).
Str("building", "minifier").Send()
serverutil.ServeErrorPage(writer, "Error minifying page: "+err.Error())
return return
} }
writer.Write(banpageBuffer.Bytes()) if checkUsernameBan(formName, &post, postBoard, writer, request) {
return return
} }
@ -232,8 +237,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
var filePath, thumbPath, catalogThumbPath string var filePath, thumbPath, catalogThumbPath string
if err != nil || handler.Size == 0 { if err != nil || handler.Size == 0 {
// no file was uploaded // no file was uploaded
post.Filename = "" if strings.TrimSpace(post.MessageRaw) == "" {
if strings.TrimSpace(post.MessageText) == "" {
serverutil.ServeErrorPage(writer, "Post must contain a message if no image is uploaded.") serverutil.ServeErrorPage(writer, "Post must contain a message if no image is uploaded.")
return return
} }
@ -250,33 +254,21 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
return return
} }
defer file.Close() defer file.Close()
post.FilenameOriginal = html.EscapeString(handler.Filename) var upload gcsql.Upload
ext := gcutil.GetFileExtension(post.FilenameOriginal) upload.OriginalFilename = html.EscapeString(handler.Filename)
thumbExt := strings.ToLower(ext)
if thumbExt == "gif" || thumbExt == "webm" || thumbExt == "mp4" { ext := strings.ToLower(filepath.Ext(upload.OriginalFilename))
thumbExt = "jpg" upload.Filename = getNewFilename() + ext
}
post.Filename = getNewFilename() + "." + ext
boardExists := gcsql.DoesBoardExistByID( boardExists := gcsql.DoesBoardExistByID(
gcutil.HackyStringToInt(request.FormValue("boardid"))) gcutil.HackyStringToInt(request.FormValue("boardid")))
if !boardExists { if !boardExists {
serverutil.ServeErrorPage(writer, "No boards have been created yet") serverutil.ServeErrorPage(writer, "No boards have been created yet")
return return
} }
var _board = gcsql.Board{} filePath = path.Join(systemCritical.DocumentRoot, postBoard.Dir, "src", upload.Filename)
err = _board.PopulateData(gcutil.HackyStringToInt(request.FormValue("boardid"))) thumbPath = path.Join(systemCritical.DocumentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("thumb"))
if err != nil { catalogThumbPath = path.Join(systemCritical.DocumentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("catalog"))
gcutil.LogError(err).
Str("IP", post.IP).
Str("posting", "updateBoard").Send()
serverutil.ServeErrorPage(writer, "Server error: "+err.Error())
return
}
boardDir := _board.Dir
filePath = path.Join(systemCritical.DocumentRoot, boardDir, "src", post.Filename)
thumbPath = path.Join(systemCritical.DocumentRoot, boardDir, "thumb", strings.Replace(post.Filename, "."+ext, "t."+thumbExt, -1))
catalogThumbPath = path.Join(systemCritical.DocumentRoot, boardDir, "thumb", strings.Replace(post.Filename, "."+ext, "c."+thumbExt, -1))
if err = os.WriteFile(filePath, data, 0644); err != nil { if err = os.WriteFile(filePath, data, 0644); err != nil {
gcutil.LogError(err). gcutil.LogError(err).
@ -353,7 +345,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
} }
} }
thumbType := "reply" thumbType := "reply"
if post.ParentID == 0 { if post.IsTopPost {
thumbType = "op" thumbType = "op"
} }
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, boardDir, thumbType) post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, boardDir, thumbType)
@ -474,7 +466,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
} }
// rebuild the board page // rebuild the board page
building.BuildBoards(false, post.BoardID) building.BuildBoards(false, postBoard.ID)
building.BuildFrontPage() building.BuildFrontPage()
if emailCommand == "noko" { if emailCommand == "noko" {