1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-09-05 11:06:23 -07:00

replace is_regex in filters with more string matching options (substring, regex, exact match)

This commit is contained in:
Eggbertx 2024-08-24 15:37:34 -07:00
parent dbc9702b6d
commit ce293d3e24
20 changed files with 393 additions and 670 deletions

View file

@ -95,7 +95,7 @@ func MigrateFileBans(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, cfg *confi
field = "checksum"
}
if _, err = db.ExecContextSQL(ctx, tx,
`INSERT INTO DBPREFIXfilter_conditions(filter_id,is_regex,search,field) VALUES(?,?,?,?)`, filterID, false, fBanChecksum, field,
`INSERT INTO DBPREFIXfilter_conditions(filter_id,match_mode,search,field) VALUES(?,?,?,?)`, filterID, gcsql.ExactMatch, fBanChecksum, field,
); err != nil {
return err
}
@ -104,7 +104,7 @@ func MigrateFileBans(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, cfg *confi
}
func MigrateFilenameBans(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, cfg *config.SQLConfig) error {
rows, err := db.QueryContextSQL(ctx, nil, `SELECT board_id,staff_id,staff_note,issued_at,filename,is_regex FROM DBPREFIXfilename_ban`)
rows, err := db.QueryContextSQL(ctx, nil, `SELECT board_id,staff_id,staff_note,issued_at,filename,match_mode FROM DBPREFIXfilename_ban`)
if err != nil {
return err
}
@ -117,6 +117,7 @@ func MigrateFilenameBans(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, cfg *c
var fnBanFilename string
var fnBanIsRegex bool
var filterID int
var matchMode gcsql.StringMatchMode
for rows.Next() {
if err = rows.Scan(
&fnBanBoardID, &fnBanStaffID, &fnBanStaffNote, &fnBanIssuedAt, &fnBanFilename, &fnBanIsRegex,
@ -138,9 +139,14 @@ func MigrateFilenameBans(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, cfg *c
}
}
if fnBanIsRegex {
matchMode = gcsql.RegexMatch
} else {
matchMode = gcsql.SubstrMatch
}
if _, err = db.ExecContextSQL(ctx, tx,
`INSERT INTO DBPREFIXfilter_conditions(filter_id,is_regex,search,field) VALUES(?,?,?,?)`,
filterID, fnBanIsRegex, fnBanFilename, "filename",
`INSERT INTO DBPREFIXfilter_conditions(filter_id,match_mode,search,field) VALUES(?,?,?,?)`,
filterID, matchMode, fnBanFilename, "filename",
); err != nil {
return err
}
@ -162,6 +168,7 @@ func MigrateUsernameBans(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, cfg *c
var unBanUsername string
var unBanIsRegex bool
var filterID int
var matchMode gcsql.StringMatchMode
for rows.Next() {
if err = rows.Scan(
&unBanBoardID, &unBanStaffID, &unBanStaffNote, &unBanIssuedAt, &unBanUsername, &unBanIsRegex,
@ -183,10 +190,14 @@ func MigrateUsernameBans(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, cfg *c
return err
}
}
if unBanIsRegex {
matchMode = gcsql.RegexMatch
} else {
matchMode = gcsql.SubstrMatch
}
if _, err = db.ExecContextSQL(ctx, tx,
`INSERT INTO DBPREFIXfilter_conditions(filter_id,is_regex,search,field) VALUES(?,?,?,?)`,
filterID, unBanIsRegex, unBanUsername, "name",
`INSERT INTO DBPREFIXfilter_conditions(filter_id,match_mode,search,field) VALUES(?,?,?,?)`,
filterID, matchMode, unBanUsername, "name",
); err != nil {
return err
}
@ -196,7 +207,7 @@ func MigrateUsernameBans(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, cfg *c
}
func MigrateWordfilters(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, sqlConfig *config.SQLConfig) error {
rows, err := db.QueryContextSQL(ctx, nil, `SELECT board_dirs, staff_id, staff_note, issued_at, search, is_regex, change_to FROM DBPREFIXwordfilters`)
rows, err := db.QueryContextSQL(ctx, nil, `SELECT board_dirs, staff_id, staff_note, issued_at, search, match_mode, change_to FROM DBPREFIXwordfilters`)
if err != nil {
return err
}
@ -212,6 +223,7 @@ func MigrateWordfilters(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, sqlConf
var isRegex bool
var changeTo string
var filterID int
var matchMode gcsql.StringMatchMode
for rows.Next() {
if err = rows.Scan(&boardDirsPtr, &staffID, &staffNote, &issuedAt, &search, &isRegex, &changeTo); err != nil {
return err
@ -250,9 +262,14 @@ func MigrateWordfilters(db *gcsql.GCDB, ctx context.Context, tx *sql.Tx, sqlConf
}
}
}
if isRegex {
matchMode = gcsql.RegexMatch
} else {
matchMode = gcsql.SubstrMatch
}
if _, err = db.ExecContextSQL(ctx, tx,
`INSERT INTO DBPREFIXfilter_conditions(filter_id, is_regex, search, field) VALUES(?,?,?,'body')`, filterID, isRegex, search,
`INSERT INTO DBPREFIXfilter_conditions(filter_id, match_mode, search, field) VALUES(?,?,?,'body')`, filterID, matchMode, search,
); err != nil {
return err
}

View file

@ -26,7 +26,7 @@ function onFieldChange(e:JQuery.ChangeEvent) {
const isBoolean = e.target.value === "firsttimeboard" || e.target.value === "notfirsttimeboard" ||
e.target.value === "firsttimesite" || e.target.value === "notfirsttimesite" || e.target.value === "isop" ||
e.target.value === "notop" || e.target.value === "hasfile" || e.target.value === "nofile";
const noRegex = isBoolean || e.target.value === "checksum" || e.target.value === "ahash";
const noMatchMode = isBoolean || e.target.value === "checksum" || e.target.value === "ahash";
const $searchContainer = $fieldset.find("tr.search-cndtn");
if(isBoolean) {
$searchContainer.hide();
@ -34,10 +34,10 @@ function onFieldChange(e:JQuery.ChangeEvent) {
$searchContainer.show();
}
if(noRegex) {
$fieldset.find(".regex-cndtn").hide();
if(noMatchMode) {
$fieldset.find(".strmatch-cndtn").hide();
} else {
$fieldset.find(".regex-cndtn").show();
$fieldset.find(".strmatch-cndtn").show();
}
}

View file

@ -1,10 +1,8 @@
package gcsql
import (
"context"
"database/sql"
"errors"
"regexp"
"strconv"
"github.com/gochan-org/gochan/pkg/config"
@ -173,366 +171,96 @@ func (ipb *IPBan) Deactivate(_ int) error {
return tx.Commit()
}
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
}
// NewNameBan is a wrapper around ApplyFilter for creating a name and/or tripcode ban.
// func NewNameBan(name string, nameIsRegex bool, tripcode string, tripcodeIsRegex bool, boardID int, staffID int, staffNote string) (*Filter, error) {
// const query = `INSERT INTO DBPREFIXusername_ban
// (board_id, staff_id, staff_note, username, is_regex)
// VALUES(?,?,?,?,?)`
// var ban UsernameBan
// if boardID > 0 {
// ban.BoardID = new(int)
// *ban.BoardID = boardID
// }
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
}
// tx, err := BeginTx()
// if err != nil {
// return nil, err
// }
// defer tx.Rollback()
func NewNameBan(name string, isRegex bool, boardID int, staffID int, staffNote string) (*UsernameBan, error) {
const query = `INSERT INTO DBPREFIXusername_ban
(board_id, staff_id, staff_note, username, is_regex)
VALUES(?,?,?,?,?)`
var ban UsernameBan
if boardID > 0 {
ban.BoardID = new(int)
*ban.BoardID = boardID
}
// stmt, err := PrepareSQL(query, tx)
// if err != nil {
// return nil, err
// }
// defer stmt.Close()
// if _, err = stmt.Exec(ban.BoardID, staffID, staffNote, name, isRegex); err != nil {
// return nil, err
// }
// if ban.ID, err = getLatestID("DBPREFIXusername_ban", tx); err != nil {
// return nil, err
// }
// if err = tx.Commit(); err != nil {
// return nil, err
// }
tx, err := BeginTx()
if err != nil {
return nil, err
}
defer tx.Rollback()
// ban.StaffID = staffID
// ban.StaffNote = staffNote
// ban.Username = name
// ban.IsRegex = isRegex
// return &ban, stmt.Close()
// }
stmt, err := PrepareSQL(query, tx)
if err != nil {
return nil, err
}
defer stmt.Close()
if _, err = stmt.Exec(ban.BoardID, staffID, staffNote, name, isRegex); err != nil {
return nil, err
}
if ban.ID, err = getLatestID("DBPREFIXusername_ban", tx); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
// func NewFilenameBan(filename string, isRegex bool, boardID int, staffID int, staffNote string) (*FilenameBan, error) {
// const query = `INSERT INTO DBPREFIXfilename_ban (board_id, staff_id, staff_note, filename, is_regex) VALUES(?,?,?,?,?)`
// var ban FilenameBan
// if boardID > 0 {
// ban.BoardID = new(int)
// *ban.BoardID = boardID
// }
ban.StaffID = staffID
ban.StaffNote = staffNote
ban.Username = name
ban.IsRegex = isRegex
return &ban, stmt.Close()
}
// tx, err := BeginTx()
// if err != nil {
// return nil, err
// }
// defer tx.Rollback()
// stmt, err := PrepareSQL(query, tx)
// if err != nil {
// return nil, err
// }
// defer stmt.Close()
// if _, err = stmt.Exec(ban.BoardID, staffID, staffNote, filename, isRegex); err != nil {
// return nil, err
// }
// if ban.ID, err = getLatestID("DBPREFIXfilename_ban", tx); err != nil {
// return nil, err
// }
// if err = tx.Commit(); err != nil {
// return nil, err
// }
func GetNameBans(boardID int, limit int) ([]UsernameBan, error) {
query := `SELECT
id, board_id, staff_id, staff_note, issued_at, username, is_regex
FROM DBPREFIXusername_ban`
limitStr := ""
if limit > 0 {
limitStr = " LIMIT " + strconv.Itoa(limit)
}
var rows *sql.Rows
var err error
if boardID > 0 {
query += " WHERE board_id = ?"
rows, err = QuerySQL(query+limitStr, boardID)
} else {
rows, err = QuerySQL(query + limitStr)
}
if err != nil {
return nil, err
}
defer rows.Close()
var bans []UsernameBan
for rows.Next() {
var ban UsernameBan
if err = rows.Scan(
&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Username,
&ban.IsRegex,
); err != nil {
return nil, err
}
bans = append(bans, ban)
}
return bans, nil
}
// ban.StaffID = staffID
// ban.StaffNote = staffNote
// ban.Filename = filename
// ban.IsRegex = isRegex
// return &ban, stmt.Close()
// }
func DeleteNameBan(id int) error {
const query = `DELETE FROM DBPREFIXusername_ban WHERE id = ?`
_, err := ExecSQL(query, id)
return err
}
func GetFileBans(boardID int, limit int) ([]FileBan, error) {
query := `SELECT id, board_id, staff_id, staff_note, issued_at, checksum, fingerprinter, ban_ip, ban_ip_message FROM DBPREFIXfile_ban`
limitStr := ""
if limit > 0 {
limitStr = " LIMIT " + strconv.Itoa(limit)
}
var rows *sql.Rows
var err error
if boardID > 0 {
query += " WHERE board_id = ?"
rows, err = QuerySQL(query+limitStr, boardID)
} else {
rows, err = QuerySQL(query + limitStr)
}
if err != nil {
return nil, err
}
defer rows.Close()
var bans []FileBan
for rows.Next() {
var ban FileBan
if err = rows.Scan(
&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt,
&ban.Checksum, &ban.Fingerprinter, &ban.BanIP, &ban.BanIPMessage,
); err != nil {
return nil, err
}
bans = append(bans, ban)
}
return bans, nil
}
func GetFilenameBans(boardID int, limit int) ([]FilenameBan, error) {
query := `SELECT id, board_id, staff_id, staff_note, issued_at, filename, is_regex FROM DBPREFIXfilename_ban`
limitStr := ""
if limit > 0 {
limitStr = " LIMIT " + strconv.Itoa(limit)
}
var rows *sql.Rows
var err error
if boardID > 0 {
query += " WHERE board_id = ?"
rows, err = QuerySQL(query+limitStr, boardID)
} else {
rows, err = QuerySQL(query + limitStr)
}
if err != nil {
return nil, err
}
defer rows.Close()
var bans []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 nil, err
}
bans = append(bans, ban)
}
return bans, nil
}
func NewFilenameBan(filename string, isRegex bool, boardID int, staffID int, staffNote string) (*FilenameBan, error) {
const query = `INSERT INTO DBPREFIXfilename_ban (board_id, staff_id, staff_note, filename, is_regex) VALUES(?,?,?,?,?)`
var ban FilenameBan
if boardID > 0 {
ban.BoardID = new(int)
*ban.BoardID = boardID
}
tx, err := BeginTx()
if err != nil {
return nil, err
}
defer tx.Rollback()
stmt, err := PrepareSQL(query, tx)
if err != nil {
return nil, err
}
defer stmt.Close()
if _, err = stmt.Exec(ban.BoardID, staffID, staffNote, filename, isRegex); err != nil {
return nil, err
}
if ban.ID, err = getLatestID("DBPREFIXfilename_ban", tx); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
ban.StaffID = staffID
ban.StaffNote = staffNote
ban.Filename = filename
ban.IsRegex = isRegex
return &ban, stmt.Close()
}
func (ub filenameOrUsernameBanBase) IsGlobalBan() bool {
return ub.BoardID == nil
}
func (fnb *FilenameBan) Deactivate(_ int) error {
const deleteQuery = `DELETE FROM DBPREFIXfilename_ban WHERE id = ?`
_, err := ExecSQL(deleteQuery, fnb.ID)
return err
}
func (fnb *UsernameBan) Deactivate(_ int) error {
const deleteQuery = `DELETE FROM DBPREFIXusername_ban WHERE id = ?`
_, err := ExecSQL(deleteQuery, fnb.ID)
return err
}
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
}
// CheckFileChecksumBan checks to see if the given checksum is banned on the given boardID, or on all boards.
// It returns the ban info (or nil if it is not banned) and any errors
func CheckFileChecksumBan(checksum string, boardID int) (*FileBan, error) {
const query = `SELECT
id, board_id, staff_id, staff_note, issued_at, checksum
FROM DBPREFIXfile_ban
WHERE checksum = ? AND (board_id IS NULL OR board_id = ?) ORDER BY id DESC LIMIT 1`
var ban FileBan
err := QueryRowSQL(query, []any{checksum, boardID}, []any{
&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Checksum,
})
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &ban, err
}
func GetChecksumBans(boardID int, limit int) ([]FileBan, error) {
query := `SELECT
id, board_id, staff_id, staff_note, issued_at, checksum
FROM DBPREFIXfile_ban`
if boardID > 0 {
query += " WHERE board_id = ?"
}
query += " LIMIT " + strconv.Itoa(limit)
var rows *sql.Rows
var cancel context.CancelFunc
var err error
if boardID > 0 {
rows, cancel, err = QueryTimeoutSQL(nil, query, boardID)
} else {
rows, cancel, err = QueryTimeoutSQL(nil, query)
}
if err != nil {
return nil, err
}
defer func() {
rows.Close()
cancel()
}()
var bans []FileBan
for rows.Next() {
var ban FileBan
if err = rows.Scan(
&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Checksum,
); err != nil {
return nil, err
}
bans = append(bans, ban)
}
return bans, rows.Close()
}
func NewFileChecksumBan(checksum string, fingerprinter string, boardID int, staffID int, staffNote string, banIP bool, banReason string) (*FileBan, error) {
const query = `INSERT INTO DBPREFIXfile_ban
(board_id, staff_id, staff_note, checksum, fingerprinter, ban_ip, ban_ip_message)
VALUES(?,?,?,?,?,?,?)`
var ban FileBan
var err error
if boardID > 0 {
ban.BoardID = new(int)
*ban.BoardID = boardID
}
if banIP {
ban.BanIP = banIP
ban.BanIPMessage = new(string)
*ban.BanIPMessage = banReason
}
ctx, cancel := context.WithTimeout(context.Background(), gcdb.defaultTimeout)
defer cancel()
tx, err := BeginContextTx(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
if _, err = ExecContextSQL(ctx, tx, query,
ban.BoardID, staffID, staffNote, checksum, fingerprinter, banIP, banReason,
); err != nil {
return nil, err
}
if ban.ID, err = getLatestID("DBPREFIXfile_ban", tx); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
ban.StaffID = staffID
ban.StaffNote = staffNote
ban.Checksum = checksum
return &ban, nil
}
func (fb *FileBan) IsGlobalBan() bool {
return fb.BoardID == nil
}
func (fb FileBan) Deactivate(_ int) error {
const deleteQuery = `DELETE FROM DBPREFIXfile_ban WHERE id = ?`
_, err := ExecTimeoutSQL(nil, deleteQuery, fb.ID)
return err
}
// DeleteFileBanByID deletes the ban, given the id column value
func DeleteFileBanByID(id int) error {
_, err := ExecTimeoutSQL(nil, "DELETE FROM DBPREFIXfile_ban WHERE id = ?", id)
return err
}
// // CheckFileChecksumBan checks to see if the given checksum is banned on the given boardID, or on all boards.
// // It returns the ban info (or nil if it is not banned) and any errors
// func CheckFileChecksumBan(checksum string, boardID int) (*FileBan, error) {
// const query = `SELECT
// id, board_id, staff_id, staff_note, issued_at, checksum
// FROM DBPREFIXfile_ban
// WHERE checksum = ? AND (board_id IS NULL OR board_id = ?) ORDER BY id DESC LIMIT 1`
// var ban FileBan
// err := QueryRowSQL(query, []any{checksum, boardID}, []any{
// &ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Checksum,
// })
// if err == sql.ErrNoRows {
// return nil, nil
// }
// if err != nil {
// return nil, err
// }
// return &ban, err
// }

View file

@ -97,14 +97,21 @@ func firstPost(post *Post, global bool) (bool, error) {
}
func matchString(fc *FilterCondition, checkStr string) (bool, error) {
if fc.IsRegex {
switch fc.MatchMode {
case SubstrMatch:
return strings.Contains(checkStr, fc.Search), nil
case SubstrMatchCaseInsensitive:
return strings.Contains(strings.ToLower(checkStr), strings.ToLower(fc.Search)), nil
case RegexMatch:
re, err := regexp.Compile(fc.Search)
if err != nil {
return false, err
}
return re.MatchString(checkStr), nil
case ExactMatch:
return checkStr == fc.Search, nil
}
return strings.Contains(checkStr, fc.Search), nil
return false, ErrInvalidStringMatchMode
}
func init() {
@ -118,7 +125,7 @@ func init() {
"trip": &conditionHandler{
fieldType: StringField,
matchFunc: func(r *http.Request, p *Post, _ *Upload, fc *FilterCondition) (bool, error) {
return matchString(fc, p.Name)
return matchString(fc, p.Tripcode)
},
},
"email": &conditionHandler{

View file

@ -13,23 +13,26 @@ import (
)
const (
AllFilters ActiveFilter = iota
OnlyActiveFilters
OnlyInactiveFilters
)
const (
onlyWordfilters wordFilterFilter = iota
onlyNonWordfilters
// SubstrMatch represents a condition that checks if the field condtains a string, case sensitive
SubstrMatch StringMatchMode = iota
// SubstrMatchCaseInsensitive represents a condition that checks if the field condtains a string, not case sensitive
SubstrMatchCaseInsensitive
// RegexMatch represents a condition that checks if the field matches a regular expression
RegexMatch
// ExactMatch represents a condition that checks if the field exactly matches string
ExactMatch
)
var (
ErrInvalidConditionField = errors.New("unrecognized filter condition field")
ErrInvalidMatchAction = errors.New("unrecognized filter match action")
ErrInvalidFilter = errors.New("unrecognized filter id")
ErrNoConditions = errors.New("error has no match conditions")
ErrInvalidStringMatchMode = errors.New("invalid string match mode")
ErrInvalidConditionField = errors.New("unrecognized filter condition field")
ErrInvalidMatchAction = errors.New("unrecognized filter match action")
ErrInvalidFilter = errors.New("unrecognized filter id")
ErrNoConditions = errors.New("error has no match conditions")
)
type wordFilterFilter int
// StringMatchMode is used when matching a string, determining how it should be checked (substring, regex, or exact match)
type StringMatchMode int
// GetFilterByID returns the filter with the given ID, and an error if one occured
func GetFilterByID(id int) (*Filter, error) {
@ -48,10 +51,10 @@ func GetFilterByID(id int) (*Filter, error) {
// GetAllFilters returns an array of all post filters, and an error if one occured. It can optionally return only the active or
// only the inactive filters (or return all)
func GetAllFilters(show ActiveFilter) ([]Filter, error) {
func GetAllFilters(activeFilter BooleanFilter) ([]Filter, error) {
query := `SELECT id, staff_id, staff_note, issued_at, match_action, match_detail, is_active
FROM DBPREFIXfilters
WHERE match_action <> 'replace'` + show.whereClause(true)
WHERE match_action <> 'replace'` + activeFilter.whereClause("is_active", true)
rows, cancel, err := QueryTimeoutSQL(nil, query)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
@ -75,29 +78,28 @@ func GetAllFilters(show ActiveFilter) ([]Filter, error) {
return filters, rows.Close()
}
func getFiltersByBoardDir(dir string, includeAllBoards bool, show ActiveFilter, filterWordFilters wordFilterFilter) ([]Filter, error) {
func getFiltersByBoardDir(dir string, includeAllBoards bool, activeFilter BooleanFilter, useWordFilters bool) ([]Filter, error) {
query := `SELECT DBPREFIXfilters.id, staff_id, staff_note, issued_at, match_action, match_detail, is_active
FROM DBPREFIXfilters
LEFT JOIN DBPREFIXfilter_boards ON filter_id = DBPREFIXfilters.id
LEFT JOIN DBPREFIXboards ON DBPREFIXboards.id = board_id`
switch filterWordFilters {
case onlyWordfilters:
if useWordFilters {
query += ` WHERE match_action = 'replace'`
case onlyNonWordfilters:
} else {
query += ` WHERE match_action <> 'replace'`
}
var params []any
if dir == "" {
query += show.whereClause(true)
query += activeFilter.whereClause("is_active", true)
params = []any{}
} else {
query += ` AND dir = ?`
if includeAllBoards {
query += " OR board_id IS NULL"
}
query += show.whereClause(true)
query += activeFilter.whereClause("is_active", true)
params = []any{dir}
}
rows, cancel, err := QueryTimeoutSQL(nil, query, params...)
@ -126,21 +128,23 @@ func getFiltersByBoardDir(dir string, includeAllBoards bool, show ActiveFilter,
// GetFiltersByBoardDir returns the filters associated with the given board dir, optionally including filters
// not associated with a specific board. It can optionally return only the active or only the inactive filters
// (or return all)
func GetFiltersByBoardDir(dir string, includeAllBoards bool, show ActiveFilter) ([]Filter, error) {
return getFiltersByBoardDir(dir, includeAllBoards, show, onlyNonWordfilters)
func GetFiltersByBoardDir(dir string, includeAllBoards bool, show BooleanFilter) ([]Filter, error) {
return getFiltersByBoardDir(dir, includeAllBoards, show, false)
}
// GetFiltersByBoardID returns an array of post filters associated to the given board ID, including
// filters set to "All boards" if includeAllBoards is true. It can optionally return only the active or
// only the inactive filters (or return all)
func GetFiltersByBoardID(boardID int, includeAllBoards bool, show ActiveFilter) ([]Filter, error) {
func GetFiltersByBoardID(boardID int, includeAllBoards bool, activeFilter BooleanFilter) ([]Filter, error) {
query := `SELECT DBPREFIXfilters.id, staff_id, staff_note, issued_at, match_action, match_detail, is_active
FROM DBPREFIXfilters LEFT JOIN DBPREFIXfilter_boards ON filter_id = DBPREFIXfilters.id
WHERE match_action <> 'replace' AND board_id = ?`
WHERE match_action <> 'replace' AND`
if includeAllBoards {
query += " OR board_id IS NULL"
query += " (board_id = ? OR board_id IS NULL) "
} else {
query += " board_id = ? "
}
query += show.whereClause(true)
query += activeFilter.whereClause("is_active", true)
rows, cancel, err := QueryTimeoutSQL(nil, query, boardID)
if errors.Is(err, sql.ErrNoRows) {
@ -170,7 +174,7 @@ func (f *Filter) Conditions() ([]FilterCondition, error) {
if len(f.conditions) > 0 {
return f.conditions, nil
}
rows, cancel, err := QueryTimeoutSQL(nil, `SELECT id, filter_id, is_regex, search, field FROM DBPREFIXfilter_conditions WHERE filter_id = ?`, f.ID)
rows, cancel, err := QueryTimeoutSQL(nil, `SELECT id, filter_id, match_mode, search, field FROM DBPREFIXfilter_conditions WHERE filter_id = ?`, f.ID)
if err != nil {
return nil, err
}
@ -180,7 +184,7 @@ func (f *Filter) Conditions() ([]FilterCondition, error) {
}()
for rows.Next() {
var condition FilterCondition
if err = rows.Scan(&condition.ID, &condition.FilterID, &condition.IsRegex, &condition.Search, &condition.Field); err != nil {
if err = rows.Scan(&condition.ID, &condition.FilterID, &condition.MatchMode, &condition.Search, &condition.Field); err != nil {
return nil, err
}
f.conditions = append(f.conditions, condition)
@ -528,11 +532,9 @@ func (fc *FilterCondition) testCondition(post *Post, upload *Upload, request *ht
return match, err
}
// CanDoRegex is a convenience function for templates. It returns true if the filter condition should show a regular expression
// checkbox
func (fc FilterCondition) CanDoRegex() bool {
return fc.HasSearchField() && (fc.Field == "name" || fc.Field == "trip" || fc.Field == "email" || fc.Field == "subject" ||
fc.Field == "body" || fc.Field == "filename" || fc.Field == "useragent")
// ShowStringMatchOptions is a convenience function for templates.
func (fc FilterCondition) ShowStringMatchOptions() bool {
return fc.HasSearchField() && fc.Field != "checksum" && fc.Field != "ahash"
}
// HasSearchField is a convenience function for templates. It returns true if the filter condition should show a search box
@ -545,7 +547,7 @@ func (fc FilterCondition) HasSearchField() bool {
// DoPostFiltering checks the filters against the given post. If a match is found, its respective action is taken and the filter
// is returned. It logs any errors it receives and returns a sanitized error (if one occured) that can be shown to the end user
func DoPostFiltering(post *Post, upload *Upload, boardID int, request *http.Request, errEv *zerolog.Event) (*Filter, error) {
filters, err := GetFiltersByBoardID(boardID, true, OnlyActiveFilters)
filters, err := GetFiltersByBoardID(boardID, true, OnlyTrue)
if err != nil {
errEv.Err(err).Caller().Msg("Unable to get filter list")
return nil, errors.New("unable to get post filter list")

View file

@ -32,7 +32,7 @@ var (
`CREATE TABLE reports_audit\(\s+report_id BIGINT NOT NULL,\s+timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\s+handled_by_staff_id BIGINT,\s+is_cleared BOOL NOT NULL,\s+CONSTRAINT reports_audit_handled_by_staff_id_fk\s+FOREIGN KEY\(handled_by_staff_id\) REFERENCES staff\(id\),\s+CONSTRAINT reports_audit_report_id_fk\s+FOREIGN KEY\(report_id\) REFERENCES reports\(id\) ON DELETE CASCADE\s+\)`,
`CREATE TABLE filters\(\s*id BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,\s*staff_id BIGINT,\s*staff_note VARCHAR\(255\) NOT NULL,\s*issued_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\s*match_action VARCHAR\(45\) NOT NULL DEFAULT 'replace',\s*match_detail TEXT NOT NULL,\s*is_active BOOL NOT NULL,\s*CONSTRAINT filters_staff_id_fk\s*FOREIGN KEY\(staff_id\) REFERENCES staff\(id\)\s*ON DELETE SET NULL\s*\)`,
`CREATE TABLE filter_boards\(\s*id BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,\s*filter_id BIGINT NOT NULL,\s*board_id BIGINT NOT NULL,\s*CONSTRAINT filter_boards_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE,\s*CONSTRAINT filter_boards_board_id_fk\s*FOREIGN KEY\(board_id\)\s*REFERENCES boards\(id\)\s*ON DELETE CASCADE\s*\)`,
`CREATE TABLE filter_conditions\(\s*id BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,\s*filter_id BIGINT NOT NULL,\s*is_regex SMALLINT NOT NULL,\s*search VARCHAR\(75\) NOT NULL,\s*field VARCHAR\(75\) NOT NULL,\s*CONSTRAINT filter_conditions_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE,\s*CONSTRAINT filter_conditions_search_check CHECK \(search <> ''\)\s*\)`,
`CREATE TABLE filter_conditions\(\s*id BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,\s*filter_id BIGINT NOT NULL,\s*match_mode SMALLINT NOT NULL,\s*search VARCHAR\(75\) NOT NULL,\s*field VARCHAR\(75\) NOT NULL,\s*CONSTRAINT filter_conditions_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE,\s*CONSTRAINT filter_conditions_search_check CHECK \(search <> ''\)\s*\)`,
`CREATE TABLE filter_hits\(\s*id BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,\s*filter_id BIGINT NOT NULL,\s*post_data TEXT NOT NULL,\s*match_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\s*CONSTRAINT filter_hits_filter_id_fk\s*FOREIGN KEY\(filter_id\)\s*REFERENCES filters\(id\)\s*ON DELETE CASCADE\s*\)`,
`INSERT INTO database_version\(component, version\)\s+VALUES\('gochan', 3\)`,
}
@ -57,7 +57,7 @@ var (
`CREATE TABLE reports_audit\(\s+report_id BIGINT NOT NULL,\s+timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\s+handled_by_staff_id BIGINT,\s+is_cleared BOOL NOT NULL,\s+CONSTRAINT reports_audit_handled_by_staff_id_fk\s+FOREIGN KEY\(handled_by_staff_id\) REFERENCES staff\(id\),\s+CONSTRAINT reports_audit_report_id_fk\s+FOREIGN KEY\(report_id\) REFERENCES reports\(id\) ON DELETE CASCADE\s+\)`,
`CREATE TABLE filters\(\s*id BIGSERIAL PRIMARY KEY,\s*staff_id BIGINT,\s*staff_note VARCHAR\(255\) NOT NULL,\s*issued_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\s*match_action VARCHAR\(45\) NOT NULL DEFAULT 'replace',\s*match_detail TEXT NOT NULL,\s*is_active BOOL NOT NULL,\s*CONSTRAINT filters_staff_id_fk\s*FOREIGN KEY\(staff_id\) REFERENCES staff\(id\)\s*ON DELETE SET NULL\s*\)`,
`CREATE TABLE filter_boards\(\s*id BIGSERIAL PRIMARY KEY,\s*filter_id BIGINT NOT NULL,\s*board_id BIGINT NOT NULL,\s*CONSTRAINT filter_boards_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE,\s*CONSTRAINT filter_boards_board_id_fk\s*FOREIGN KEY\(board_id\) REFERENCES boards\(id\)\s*ON DELETE CASCADE\s*\)`,
`CREATE TABLE filter_conditions\(\s*id BIGSERIAL PRIMARY KEY,\s*filter_id BIGINT NOT NULL,\s*is_regex SMALLINT NOT NULL,\s*search VARCHAR\(75\) NOT NULL,\s*field VARCHAR\(75\) NOT NULL,\s*CONSTRAINT filter_conditions_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE,\s*CONSTRAINT filter_conditions_search_check CHECK \(search <> ''\)\s*\)`,
`CREATE TABLE filter_conditions\(\s*id BIGSERIAL PRIMARY KEY,\s*filter_id BIGINT NOT NULL,\s*match_mode SMALLINT NOT NULL,\s*search VARCHAR\(75\) NOT NULL,\s*field VARCHAR\(75\) NOT NULL,\s*CONSTRAINT filter_conditions_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE,\s*CONSTRAINT filter_conditions_search_check CHECK \(search <> ''\)\s*\)`,
`CREATE TABLE filter_hits\(\s*id BIGSERIAL PRIMARY KEY,\s*filter_id BIGINT NOT NULL,\s*post_data TEXT NOT NULL,\s*match_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\s*CONSTRAINT filter_hits_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE\s*\)`,
`INSERT INTO database_version\(component, version\)\s+VALUES\('gochan', 3\)`,
}
@ -82,7 +82,7 @@ var (
`CREATE TABLE reports_audit\(\s+report_id BIGINT NOT NULL,\s+timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\s+handled_by_staff_id BIGINT,\s+is_cleared BOOL NOT NULL,\s+CONSTRAINT reports_audit_handled_by_staff_id_fk\s+FOREIGN KEY\(handled_by_staff_id\) REFERENCES staff\(id\),\s+CONSTRAINT reports_audit_report_id_fk\s+FOREIGN KEY\(report_id\) REFERENCES reports\(id\) ON DELETE CASCADE\s+\)`,
`CREATE TABLE filters\(\s*id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\s*staff_id BIGINT,\s*staff_note VARCHAR\(255\) NOT NULL,\s*issued_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\s*match_action VARCHAR\(45\) NOT NULL DEFAULT 'replace',\s*match_detail TEXT NOT NULL,\s*is_active BOOL NOT NULL,\s*CONSTRAINT filters_staff_id_fk\s*FOREIGN KEY\(staff_id\) REFERENCES staff\(id\)\s*ON DELETE SET NULL\s*\)`,
`CREATE TABLE filter_boards\(\s*id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\s*filter_id BIGINT NOT NULL,\s*board_id BIGINT NOT NULL,\s*CONSTRAINT filter_boards_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE,\s*CONSTRAINT filter_boards_board_id_fk\s*FOREIGN KEY\(board_id\) REFERENCES boards\(id\)\s*ON DELETE CASCADE\s*\)`,
`CREATE TABLE filter_conditions\(\s*id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\s*filter_id BIGINT NOT NULL,\s*is_regex SMALLINT NOT NULL,\s*search VARCHAR\(75\) NOT NULL,\s*field VARCHAR\(75\) NOT NULL,\s*CONSTRAINT filter_conditions_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE,\s*CONSTRAINT filter_conditions_search_check CHECK \(search <> ''\)\s*\)`,
`CREATE TABLE filter_conditions\(\s*id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\s*filter_id BIGINT NOT NULL,\s*match_mode SMALLINT NOT NULL,\s*search VARCHAR\(75\) NOT NULL,\s*field VARCHAR\(75\) NOT NULL,\s*CONSTRAINT filter_conditions_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE,\s*CONSTRAINT filter_conditions_search_check CHECK \(search <> ''\)\s*\)`,
`CREATE TABLE filter_hits\(\s*id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\s*filter_id BIGINT NOT NULL,\s*post_data TEXT NOT NULL,\s*match_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\s*CONSTRAINT filter_hits_filter_id_fk\s*FOREIGN KEY\(filter_id\) REFERENCES filters\(id\)\s*ON DELETE CASCADE\s*\)`,
`INSERT INTO database_version\(component, version\)\s+VALUES\('gochan', 3\)`,
}

View file

@ -53,72 +53,53 @@ type Board struct {
EnableCatalog bool // sql: enable_catalog
}
// Deprecated, use PostFilter instead, with a condition field = "checksum" if Fingerprinter is nil
// or "ahash" otherwise.
// FileBan contains the information associated with a specific file ban.
type FileBan 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
Checksum string // sql: checksum
Fingerprinter *string // sql: fingerprinter
BanIP bool // sql: ban_ip
BanIPMessage *string // sql: ban_ip_message
}
// // Deprecated, use PostFilter instead, with a condition field = "checksum" if Fingerprinter is nil
// // or "ahash" otherwise.
// // FileBan contains the information associated with a specific file ban.
// type FileBan 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
// Checksum string // sql: checksum
// Fingerprinter *string // sql: fingerprinter
// BanIP bool // sql: ban_ip
// BanIPMessage *string // sql: ban_ip_message
// }
// ApplyIPBan bans the given IP if it posted a banned image
// If BanIP is false, it returns with no error
func (fb *FileBan) ApplyIPBan(postIP string) error {
if !fb.BanIP {
return nil
}
now := time.Now()
ipBan := &IPBan{
RangeStart: postIP,
RangeEnd: postIP,
IssuedAt: now,
}
ipBan.IsActive = true
ipBan.CanAppeal = true
ipBan.AppealAt = now
ipBan.StaffID = fb.StaffID
ipBan.Permanent = true
if fb.BoardID != nil {
ipBan.BoardID = new(int)
*ipBan.BoardID = *fb.BoardID
}
if fb.BanIPMessage == nil {
ipBan.Message = "posting disallowed image, resulting in ban"
} else {
ipBan.Message = *fb.BanIPMessage
}
if fb.StaffNote == "" {
ipBan.StaffNote = "fingerprint"
}
// // ApplyIPBan bans the given IP if it posted a banned image
// // If BanIP is false, it returns with no error
// func (fb *FileBan) ApplyIPBan(postIP string) error {
// if !fb.BanIP {
// return nil
// }
// now := time.Now()
// ipBan := &IPBan{
// RangeStart: postIP,
// RangeEnd: postIP,
// IssuedAt: now,
// }
// ipBan.IsActive = true
// ipBan.CanAppeal = true
// ipBan.AppealAt = now
// ipBan.StaffID = fb.StaffID
// ipBan.Permanent = true
// if fb.BoardID != nil {
// ipBan.BoardID = new(int)
// *ipBan.BoardID = *fb.BoardID
// }
// if fb.BanIPMessage == nil {
// ipBan.Message = "posting disallowed image, resulting in ban"
// } else {
// ipBan.Message = *fb.BanIPMessage
// }
// if fb.StaffNote == "" {
// ipBan.StaffNote = "fingerprint"
// }
return NewIPBan(ipBan)
}
// Deprecated, use PostFilter instead
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
}
// Deprecated, use PostFilter instead, with a condition field = "filename"
// FilenameBan represents a ban on a specific filename or filename regular expression.
type FilenameBan struct {
filenameOrUsernameBanBase
Filename string // sql: `filename`
IsRegex bool // sql: `is_regex`
}
// return NewIPBan(ipBan)
// }
// Filter represents an entry in gochan's new filter system which merges username bans, file bans, and filename bans,
// and will allow moderators to block posts based on the user's name, email, subject, message content, and other fields.
@ -137,17 +118,17 @@ type Filter struct {
// FilterCondition represents a condition to be checked against when a post is submitted
// table: DBPREFIXfilter_conditions
type FilterCondition struct {
ID int // sql: id
FilterID int // sql: filter_id
IsRegex bool // sql: is_regex
Search string // sql: search
Field string // sql: field
ID int // sql: id
FilterID int // sql: filter_id
MatchMode StringMatchMode // sql: match_mode
Search string // sql: search
Field string // sql: field
}
func (fc *FilterCondition) insert(ctx context.Context, tx *sql.Tx) error {
_, err := ExecContextSQL(ctx, tx,
`INSERT INTO DBPREFIXfilter_conditions(filter_id, is_regex, search, field) VALUES(?,?,?,?)`,
fc.FilterID, fc.IsRegex, fc.Search, fc.Field,
`INSERT INTO DBPREFIXfilter_conditions(filter_id, match_mode, search, field) VALUES(?,?,?,?)`,
fc.FilterID, fc.MatchMode, fc.Search, fc.Field,
)
return err
}
@ -340,12 +321,6 @@ type Thread struct {
IsDeleted bool // sql: `is_deleted`
}
// Deprecated, use PostFilter instead, and a FilterCondition with Field = "name"
type UsernameBan struct {
filenameOrUsernameBanBase
Username string // sql: `username`
}
// Wordfilter is used for filters that are expected to have a single FilterCondition and a "replace" MatchAction
type Wordfilter struct {
Filter

View file

@ -11,6 +11,14 @@ import (
"github.com/gochan-org/gochan/pkg/config"
)
const (
TrueOrFalse BooleanFilter = iota
OnlyTrue
OnlyFalse
)
const ()
var (
dateTimeFormats = []string{
"2006-01-02 15:04:05",
@ -20,19 +28,19 @@ var (
ErrNotConnected = errors.New("error connecting to database")
)
// ActiveFilter is used for optionally limiting the results of tables with an is_active column to
type ActiveFilter int
// BooleanFilter is used for optionally limiting results to true, false, or both
type BooleanFilter int
// whereClause returns part of the where clause of a SQL string. If and is true, it starts with AND, otherwise it starts with WHERE
func (af ActiveFilter) whereClause(and bool) string {
func (af BooleanFilter) whereClause(columnName string, and bool) string {
out := " WHERE "
if and {
out = " AND "
}
if af == OnlyActiveFilters {
return out + "is_active = TRUE"
} else if af == OnlyInactiveFilters {
return out + "is_active = FALSE"
if af == OnlyTrue {
return out + columnName + " = TRUE"
} else if af == OnlyFalse {
return out + columnName + " = FALSE"
}
return ""
}

View file

@ -18,11 +18,15 @@ var (
// boards should be a comma separated list of board strings, or "*" for all boards
func CreateWordFilter(from string, to string, isRegex bool, boards []string, staffID int, staffNote string) (*Wordfilter, error) {
var err error
var matchMode StringMatchMode
if isRegex {
_, err = regexp.Compile(from)
if err != nil {
return nil, err
}
matchMode = RegexMatch
} else {
matchMode = SubstrMatch
}
const query = `INSERT INTO DBPREFIXfilters
(staff_id, staff_note, issued_at, match_action, match_detail, is_active)
@ -70,8 +74,8 @@ func CreateWordFilter(from string, to string, isRegex bool, boards []string, sta
// set filter condition
if _, err = ExecContextSQL(ctx, tx,
`INSERT INTO DBPREFIXfilter_conditions(filter_id, is_regex, search, field) VALUES(?,?,?,'body')`,
filter.ID, isRegex, from,
`INSERT INTO DBPREFIXfilter_conditions(filter_id, match_mode, search, field) VALUES(?,?,?,'body')`,
filter.ID, matchMode, from,
); err != nil {
return nil, err
}
@ -81,8 +85,8 @@ func CreateWordFilter(from string, to string, isRegex bool, boards []string, sta
// GetWordfilters gets a list of wordfilters from the database and returns an array of them and any errors
// encountered
func GetWordfilters(active ActiveFilter) ([]Wordfilter, error) {
filters, err := getFiltersByBoardDir("", true, active, onlyWordfilters)
func GetWordfilters(active BooleanFilter) ([]Wordfilter, error) {
filters, err := getFiltersByBoardDir("", true, active, true)
if err != nil {
return nil, err
}
@ -114,7 +118,7 @@ func GetWordfilterByID(id int) (*Wordfilter, error) {
// GetBoardWordfilters gets an array of wordfilters associated with the given board directory
func GetBoardWordfilters(board string) ([]Wordfilter, error) {
filters, err := getFiltersByBoardDir(board, true, OnlyActiveFilters, onlyWordfilters)
filters, err := getFiltersByBoardDir(board, true, OnlyTrue, true)
if err != nil {
return nil, err
}
@ -168,7 +172,7 @@ func (wf *Wordfilter) Apply(message string) (string, error) {
}
condition := conditions[0]
if condition.IsRegex {
if condition.MatchMode == RegexMatch {
re, err := regexp.Compile(condition.Search)
if err != nil {
return message, err
@ -210,7 +214,7 @@ func (wf *Wordfilter) IsRegex() bool {
if err != nil || len(conditions) != 1 {
return false
}
return conditions[0].IsRegex
return conditions[0].MatchMode == RegexMatch
}
// Deprecated, use the first element in wf.BoardDirs() instead. This is kept here for templates.

View file

@ -277,8 +277,19 @@ func filtersCallback(_ http.ResponseWriter, request *http.Request, staff *gcsql.
return nil, errors.New("failed to get field data: " + err.Error())
}
fc := gcsql.FilterCondition{
Field: v[0],
IsRegex: request.PostFormValue("isregex"+fieldIDstr) == "on",
Field: v[0],
}
switch request.PostFormValue("matchmode" + fieldIDstr) {
case "substr":
fc.MatchMode = gcsql.SubstrMatch
case "substrci":
fc.MatchMode = gcsql.SubstrMatchCaseInsensitive
case "regex":
fc.MatchMode = gcsql.RegexMatch
case "exact":
fc.MatchMode = gcsql.ExactMatch
default:
return nil, gcsql.ErrInvalidStringMatchMode
}
var validField bool
for _, field := range filterFields {
@ -351,14 +362,14 @@ func filtersCallback(_ http.ResponseWriter, request *http.Request, staff *gcsql.
}
showStr := request.FormValue("show")
var show gcsql.ActiveFilter
var show gcsql.BooleanFilter
switch showStr {
case "inactive":
show = gcsql.OnlyInactiveFilters
show = gcsql.OnlyFalse
case "all":
show = gcsql.AllFilters
show = gcsql.TrueOrFalse
default:
show = gcsql.OnlyActiveFilters
show = gcsql.OnlyTrue
}
var filters []gcsql.Filter
boardSearch := request.FormValue("boardsearch")
@ -812,6 +823,11 @@ func wordfiltersCallback(_ http.ResponseWriter, request *http.Request, staff *gc
searchFor := request.PostFormValue("searchfor")
replaceWith := request.PostFormValue("replace")
isRegex := request.PostFormValue("isregex") == "on"
matchMode := gcsql.SubstrMatch
if isRegex {
matchMode = gcsql.RegexMatch
}
staffNote := request.PostFormValue("staffnote")
var boards []string
@ -838,10 +854,10 @@ func wordfiltersCallback(_ http.ResponseWriter, request *http.Request, staff *gc
return nil, errors.New("unable to update wordfilter details")
}
if err = filter.SetConditions(gcsql.FilterCondition{
FilterID: filter.ID,
IsRegex: isRegex,
Search: searchFor,
Field: "body",
FilterID: filter.ID,
MatchMode: matchMode,
Search: searchFor,
Field: "body",
}); err != nil {
errEv.Err(err).Caller().Msg("Unable to set filter condition")
return nil, errors.New("unable to set filter conditions")
@ -859,7 +875,7 @@ func wordfiltersCallback(_ http.ResponseWriter, request *http.Request, staff *gc
infoEv.Str("do", "create")
}
wordfilters, err := gcsql.GetWordfilters(gcsql.AllFilters)
wordfilters, err := gcsql.GetWordfilters(gcsql.TrueOrFalse)
if err != nil {
errEv.Err(err).Caller().Msg("Unable to get wordfilters")
return nil, err

View file

@ -62,39 +62,6 @@ func checkIpBan(post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWr
return true
}
/* func checkUsernameBan(post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWriter, request *http.Request) bool {
nameTrip := post.Name
if post.Tripcode != "" {
nameTrip += "!" + post.Tripcode
}
if nameTrip == "" {
return false
}
nameBan, err := gcsql.CheckNameBan(nameTrip, postBoard.ID)
if err != nil {
gcutil.LogError(err).Caller().
Str("IP", post.IP).
Str("nameTrip", nameTrip).
Str("boardDir", postBoard.Dir).
Msg("Error getting name banned status")
server.ServeErrorPage(writer, "Error getting name ban info")
return true
}
if nameBan == nil {
return false // name is not banned
}
server.ServeError(writer, "Name or tripcode not allowed", serverutil.IsRequestingJSON(request), map[string]interface{}{})
gcutil.LogWarning().
Str("IP", post.IP).
Str("boardDir", postBoard.Dir).
Str("nameTrip", nameTrip).
Str("banUsername", nameBan.Username).
Bool("banIsRegex", nameBan.IsRegex).
Msg("Rejected post with banned name/tripcode")
return true
} */
func handleAppeal(writer http.ResponseWriter, request *http.Request, infoEv *zerolog.Event, errEv *zerolog.Event) {
banIDstr := request.FormValue("banid")
if banIDstr == "" {

View file

@ -52,7 +52,7 @@ func (*MessageFormatter) ApplyWordFilters(message string, boardDir string) (stri
var filters []gcsql.Wordfilter
var err error
if boardDir == "" {
filters, err = gcsql.GetWordfilters(gcsql.OnlyActiveFilters)
filters, err = gcsql.GetWordfilters(gcsql.OnlyTrue)
} else {
filters, err = gcsql.GetBoardWordfilters(boardDir)
}

View file

@ -1,7 +1,6 @@
package uploads
import (
"bytes"
"crypto/md5"
"errors"
"fmt"
@ -16,7 +15,6 @@ import (
"strings"
"time"
"github.com/disintegration/imaging"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/events"
"github.com/gochan-org/gochan/pkg/gcsql"
@ -96,11 +94,11 @@ func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter,
return nil, ErrUnsupportedFileExt
}
if err = CheckFilenameBan(upload, post, postBoard); err != nil {
// If checkFilenameBan returns true, an error occured or the file was
// rejected for having a banned filename, and the incident was logged either way
return nil, err
}
// if err = CheckFilenameBan(upload, post, postBoard); err != nil {
// // If checkFilenameBan returns true, an error occured or the file was
// // rejected for having a banned filename, and the incident was logged either way
// return nil, err
// }
data, err := io.ReadAll(file)
if err != nil {
@ -111,11 +109,11 @@ func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter,
// Calculate image checksum
upload.Checksum = fmt.Sprintf("%x", md5.Sum(data)) // skipcq: GSC-G401
if err = CheckFileChecksumBan(upload, post, postBoard); err != nil {
// If CheckFileChecksumBan returns a non-nil error, an error occured or the file was
// rejected for having a banned checksum, and the incident was logged either way
return nil, err
}
// if err = CheckFileChecksumBan(upload, post, postBoard); err != nil {
// // If CheckFileChecksumBan returns a non-nil error, an error occured or the file was
// // rejected for having a banned checksum, and the incident was logged either way
// return nil, err
// }
ext := strings.ToLower(filepath.Ext(upload.OriginalFilename))
upload.Filename = getNewFilename() + ext
@ -132,25 +130,25 @@ func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter,
errEv.Str("catalogThumbPath", catalogThumbPath)
}
if IsImage(filePath) {
img, err := imaging.Decode(bytes.NewReader(data))
if err != nil {
errEv.Err(err).Caller().Msg("unable to decode file")
return nil, errors.New("unable to decode image")
}
fileBan, err := checkImageFingerprintBan(img, postBoard.Dir)
if err != nil {
errEv.Err(err).Caller().Msg("unable to fingerprint image")
return nil, err
}
if fileBan != nil {
// image is fingerprint-banned
if err = fileBan.ApplyIPBan(post.IP); err != nil {
errEv.Err(err).Caller().Msg("unable to apply IP ban")
}
return nil, ErrFileNotAllowed
}
}
// if IsImage(filePath) {
// img, err := imaging.Decode(bytes.NewReader(data))
// if err != nil {
// errEv.Err(err).Caller().Msg("unable to decode file")
// return nil, errors.New("unable to decode image")
// }
// fileBan, err := checkImageFingerprintBan(img, postBoard.Dir)
// if err != nil {
// errEv.Err(err).Caller().Msg("unable to fingerprint image")
// return nil, err
// }
// if fileBan != nil {
// // image is fingerprint-banned
// if err = fileBan.ApplyIPBan(post.IP); err != nil {
// errEv.Err(err).Caller().Msg("unable to apply IP ban")
// }
// return nil, ErrFileNotAllowed
// }
// }
if err = os.WriteFile(filePath, data, config.NormalFileMode); err != nil {
errEv.Err(err).Caller().Send()
@ -188,20 +186,20 @@ func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter,
return nil, errors.New("error processing upload: " + err.Error())
}
if IsVideo(filePath) && config.GetSiteConfig().FingerprintVideoThumbnails {
fileBan, err := checkFileFingerprintBan(thumbPath, postBoard.Dir)
if err != nil {
errEv.Err(err).Caller().Msg("unable to check video thumbnail ban")
return nil, err
}
if fileBan != nil {
// video thumbnail is fingerprint-banned
if err = fileBan.ApplyIPBan(post.IP); err != nil {
errEv.Err(err).Caller().Msg("unable to apply IP ban")
}
return nil, ErrFileNotAllowed
}
}
// if IsVideo(filePath) && config.GetSiteConfig().FingerprintVideoThumbnails {
// fileBan, err := checkFileFingerprintBan(thumbPath, postBoard.Dir)
// if err != nil {
// errEv.Err(err).Caller().Msg("unable to check video thumbnail ban")
// return nil, err
// }
// if fileBan != nil {
// // video thumbnail is fingerprint-banned
// if err = fileBan.ApplyIPBan(post.IP); err != nil {
// errEv.Err(err).Caller().Msg("unable to apply IP ban")
// }
// return nil, ErrFileNotAllowed
// }
// }
accessEv.Send()
return upload, nil

View file

@ -2,9 +2,6 @@ package uploads
import (
"errors"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gcutil"
)
var (
@ -13,41 +10,41 @@ var (
ErrFileNotAllowed = errors.New("uploaded file not allowed")
)
func CheckFilenameBan(upload *gcsql.Upload, post *gcsql.Post, postBoard *gcsql.Board) error {
filenameBan, err := gcsql.CheckFilenameBan(upload.OriginalFilename, postBoard.ID)
if err != nil {
gcutil.LogError(err).
Str("IP", post.IP).
Str("filename", upload.OriginalFilename).
Str("boardDir", postBoard.Dir).
Msg("Error getting name banned status")
return ErrCheckingFileBan
}
if filenameBan == nil {
return nil
}
gcutil.LogWarning().
Str("originalFilename", upload.OriginalFilename).
Msg("File rejected for having a banned filename")
return ErrFilenameNotAllowed
}
// func CheckFilenameBan(upload *gcsql.Upload, post *gcsql.Post, postBoard *gcsql.Board) error {
// filenameBan, err := gcsql.CheckFilenameBan(upload.OriginalFilename, postBoard.ID)
// if err != nil {
// gcutil.LogError(err).
// Str("IP", post.IP).
// Str("filename", upload.OriginalFilename).
// Str("boardDir", postBoard.Dir).
// Msg("Error getting name banned status")
// return ErrCheckingFileBan
// }
// if filenameBan == nil {
// return nil
// }
// gcutil.LogWarning().
// Str("originalFilename", upload.OriginalFilename).
// Msg("File rejected for having a banned filename")
// return ErrFilenameNotAllowed
// }
func CheckFileChecksumBan(upload *gcsql.Upload, post *gcsql.Post, postBoard *gcsql.Board) error {
fileBan, err := gcsql.CheckFileChecksumBan(upload.Checksum, postBoard.ID)
if err != nil {
gcutil.LogError(err).
Str("IP", post.IP).
Str("boardDir", postBoard.Dir).
Str("checksum", upload.Checksum).
Msg("Error getting file checksum ban status")
return ErrCheckingFileBan
}
if fileBan == nil {
return nil
}
gcutil.LogWarning().
Str("originalFilename", upload.OriginalFilename).
Str("checksum", upload.Checksum).
Msg("File rejected for having a banned checksum")
return ErrFileNotAllowed
}
// func CheckFileChecksumBan(upload *gcsql.Upload, post *gcsql.Post, postBoard *gcsql.Board) error {
// fileBan, err := gcsql.CheckFileChecksumBan(upload.Checksum, postBoard.ID)
// if err != nil {
// gcutil.LogError(err).
// Str("IP", post.IP).
// Str("boardDir", postBoard.Dir).
// Str("checksum", upload.Checksum).
// Msg("Error getting file checksum ban status")
// return ErrCheckingFileBan
// }
// if fileBan == nil {
// return nil
// }
// gcutil.LogWarning().
// Str("originalFilename", upload.OriginalFilename).
// Str("checksum", upload.Checksum).
// Msg("File rejected for having a banned checksum")
// return ErrFileNotAllowed
// }

View file

@ -1,7 +1,6 @@
package uploads
import (
"database/sql"
"errors"
"fmt"
"image"
@ -36,29 +35,29 @@ func getHashLength() int {
return hashLength
}
func checkImageFingerprintBan(img image.Image, _ string) (*gcsql.FileBan, error) {
hashLength := getHashLength()
ba, err := imagehash.Ahash(img, hashLength)
if err != nil {
return nil, err
}
const query = `SELECT id,board_id,staff_id,staff_note,issued_at,checksum,fingerprinter,
ban_ip,ban_ip_message
FROM DBPREFIXfile_ban WHERE fingerprinter = 'ahash' AND checksum = ? LIMIT 1`
// func checkImageFingerprintBan(img image.Image, _ string) (*gcsql.FileBan, error) {
// hashLength := getHashLength()
// ba, err := imagehash.Ahash(img, hashLength)
// if err != nil {
// return nil, err
// }
// const query = `SELECT id,board_id,staff_id,staff_note,issued_at,checksum,fingerprinter,
// ban_ip,ban_ip_message
// FROM DBPREFIXfile_ban WHERE fingerprinter = 'ahash' AND checksum = ? LIMIT 1`
var fileBan gcsql.FileBan
err = gcsql.QueryRowSQL(query, []any{fmt.Sprintf("%x", ba)}, []any{
&fileBan.ID, &fileBan.BoardID, &fileBan.StaffID, &fileBan.StaffNote,
&fileBan.IssuedAt, &fileBan.Checksum, &fileBan.Fingerprinter,
&fileBan.BanIP, &fileBan.BanIPMessage,
})
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
} else if err != nil {
return nil, err
}
return &fileBan, err
}
// var fileBan gcsql.FileBan
// err = gcsql.QueryRowSQL(query, []any{fmt.Sprintf("%x", ba)}, []any{
// &fileBan.ID, &fileBan.BoardID, &fileBan.StaffID, &fileBan.StaffNote,
// &fileBan.IssuedAt, &fileBan.Checksum, &fileBan.Fingerprinter,
// &fileBan.BanIP, &fileBan.BanIPMessage,
// })
// if errors.Is(err, sql.ErrNoRows) {
// return nil, nil
// } else if err != nil {
// return nil, err
// }
// return &fileBan, err
// }
func GetPostImageFingerprint(postID int) (string, error) {
filename, board, err := gcsql.GetUploadFilenameAndBoard(postID)
@ -93,10 +92,10 @@ func GetFileFingerprint(filePath string) (string, error) {
return fmt.Sprintf("%x", ba), nil
}
func checkFileFingerprintBan(filePath string, board string) (*gcsql.FileBan, error) {
img, err := imaging.Open(filePath)
if err != nil {
return nil, err
}
return checkImageFingerprintBan(img, board)
}
// func checkFileFingerprintBan(filePath string, board string) (*gcsql.FileBan, error) {
// img, err := imaging.Open(filePath)
// if err != nil {
// return nil, err
// }
// return checkImageFingerprintBan(img, board)
// }

View file

@ -272,13 +272,13 @@ CREATE TABLE DBPREFIXfilter_boards(
CREATE TABLE DBPREFIXfilter_conditions(
id {serial pk},
filter_id {fk to serial} NOT NULL,
is_regex SMALLINT NOT NULL,
match_mode SMALLINT NOT NULL,
search VARCHAR(75) NOT NULL,
field VARCHAR(75) NOT NULL,
CONSTRAINT filter_conditions_filter_id_fk
FOREIGN KEY(filter_id) REFERENCES DBPREFIXfilters(id)
ON DELETE CASCADE,
CONSTRAINT filter_conditions_search_check CHECK (search <> '')
CONSTRAINT filter_conditions_search_check CHECK (search <> '' OR match_mode = 3)
);
CREATE TABLE DBPREFIXfilter_hits(

View file

@ -272,7 +272,7 @@ CREATE TABLE DBPREFIXfilter_boards(
CREATE TABLE DBPREFIXfilter_conditions(
id BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,
filter_id BIGINT NOT NULL,
is_regex SMALLINT NOT NULL,
match_mode SMALLINT NOT NULL,
search VARCHAR(75) NOT NULL,
field VARCHAR(75) NOT NULL,
CONSTRAINT filter_conditions_filter_id_fk

View file

@ -272,7 +272,7 @@ CREATE TABLE DBPREFIXfilter_boards(
CREATE TABLE DBPREFIXfilter_conditions(
id BIGSERIAL PRIMARY KEY,
filter_id BIGINT NOT NULL,
is_regex SMALLINT NOT NULL,
match_mode SMALLINT NOT NULL,
search VARCHAR(75) NOT NULL,
field VARCHAR(75) NOT NULL,
CONSTRAINT filter_conditions_filter_id_fk

View file

@ -272,7 +272,7 @@ CREATE TABLE DBPREFIXfilter_boards(
CREATE TABLE DBPREFIXfilter_conditions(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filter_id BIGINT NOT NULL,
is_regex SMALLINT NOT NULL,
match_mode SMALLINT NOT NULL,
search VARCHAR(75) NOT NULL,
field VARCHAR(75) NOT NULL,
CONSTRAINT filter_conditions_filter_id_fk

View file

@ -15,9 +15,14 @@
<th>Search:</th>
<td><input type="text" name="search{{.conditionNo}}" value="{{.condition.Search}}"></td>
</tr>
<tr class="regex-cndtn" {{if not .condition.CanDoRegex}}style="display: none"{{end}}>
<th></th>
<td><label>Is regex <input type="checkbox" name="isregex{{.conditionNo}}" {{if .condition.IsRegex}}checked{{end}}/></label></td>
<tr class="strmatch-cndtn" {{if not .condition.ShowStringMatchOptions}}style="display: none"{{end}}>
<th>Match mode</th>
<td><select name="matchmode{{.conditionNo}}">
<option value="substr" {{if eq .condition.MatchMode 0}}selected{{end}}>Substring (case sensitive)</option>
<option value="substrci" {{if eq .condition.MatchMode 1}}selected{{end}}>Substring (not case sensitive)</option>
<option value="regex" {{if eq .condition.MatchMode 2}}selected{{end}}>Regular expression</option>
<option value="exact" {{if eq .condition.MatchMode 3}}selected{{end}}>Exact match</option>
</select></td>
</tr>
<tr class="btns-cndtn">
<td></td>