1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-30 09:56:23 -07:00

Check for NSFW tags and show an error if they are not allowed on the board (or site)

This commit is contained in:
Eggbertx 2025-03-09 14:14:35 -07:00
parent a8207b77d7
commit 8fe4c25938
7 changed files with 81 additions and 23 deletions

View file

@ -249,7 +249,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
return
}
formatted, err := posting.FormatMessage(formattedStr, board.Dir)
formatted, err := posting.FormatMessage(formattedStr, board.Dir, warnEv, errEv)
if err != nil {
errEv.Err(err).Caller().Send()
server.ServeError(writer, err.Error(), wantsJSON, nil)

View file

@ -428,6 +428,7 @@ type BoardConfig struct {
EnableSpoileredThreads bool
// Worksafe determines whether the board is worksafe or not. If it is set to true, threads cannot be marked NSFW
// (given a hashtag with the text NSFW, case insensitive).
// Default: true
Worksafe bool

View file

@ -5,6 +5,8 @@ import (
"os"
"path/filepath"
"testing"
"github.com/rs/zerolog"
)
const (
@ -38,3 +40,9 @@ func GoToGochanRoot(t *testing.T) (string, error) {
}
return dir, errors.New("test running from unexpected dir, should be in gochan root or the current testing dir")
}
// GetTestLogs returns logs with info, warn, and error levels respectively for testing
func GetTestLogs(t *testing.T) (*zerolog.Event, *zerolog.Event, *zerolog.Event) {
logger := zerolog.New(zerolog.NewTestWriter(t))
return logger.Info(), logger.Warn(), logger.Error()
}

View file

@ -600,8 +600,10 @@ func rebuildBoardsCallback(_ http.ResponseWriter, _ *http.Request, _ *gcsql.Staf
return "Boards built successfully", nil
}
func reparseHTMLCallback(_ http.ResponseWriter, _ *http.Request, _ *gcsql.Staff, _ bool, _ *zerolog.Event, errEv *zerolog.Event) (output any, err error) {
func reparseHTMLCallback(_ http.ResponseWriter, request *http.Request, _ *gcsql.Staff, _ bool, _ *zerolog.Event, errEv *zerolog.Event) (output any, err error) {
var outputStr string
_, warnEv, _ := gcutil.LogRequest(request)
defer warnEv.Discard()
tx, err := gcsql.BeginTx()
if err != nil {
errEv.Err(err).Msg("Unable to begin transaction")
@ -635,7 +637,7 @@ func reparseHTMLCallback(_ http.ResponseWriter, _ *http.Request, _ *gcsql.Staff,
errEv.Err(err).Caller().Msg("Unable to scan SQL row")
return "", err
}
if formatted, err := posting.FormatMessage(messageRaw, boardDir); err != nil {
if formatted, err := posting.FormatMessage(messageRaw, boardDir, warnEv, errEv); err != nil {
errEv.Err(err).Caller().Msg("Unable to format message")
return "", err
} else {

View file

@ -12,15 +12,17 @@ import (
"github.com/frustra/bbcode"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gcutil"
"github.com/rs/zerolog"
)
var (
msgfmtr MessageFormatter
urlRE = regexp.MustCompile(`https?://(\S+)`)
unsetBBcodeTags = []string{"center", "color", "img", "quote", "size"}
diceRollRE = regexp.MustCompile(`\[(\d*)d(\d+)(?:([+-])(\d+))?\]`)
hashTagRE = regexp.MustCompile(`\[#(.+)\]`)
msgfmtr MessageFormatter
urlRE = regexp.MustCompile(`https?://(\S+)`)
unsetBBcodeTags = []string{"center", "color", "img", "quote", "size"}
diceRollRE = regexp.MustCompile(`\[(\d*)d(\d+)(?:([+-])(\d+))?\]`)
hashTagRE = regexp.MustCompile(`\[#([^\]]+)\]`)
brRE = regexp.MustCompile(`<br\s*/?>`)
ErrWorksafeBoard = errors.New("this board does not allow NSFW content")
)
// InitPosting prepares the formatter and the temp post pruner
@ -89,14 +91,16 @@ func wrapLinksInURL(urlStr string) string {
return "[url]" + urlStr + "[/url]"
}
func FormatMessage(message string, boardDir string) (template.HTML, error) {
func FormatMessage(message string, boardDir string, warnEv, errEv *zerolog.Event) (template.HTML, error) {
if config.GetBoardConfig(boardDir).RenderURLsAsLinks {
message = urlRE.ReplaceAllStringFunc(message, wrapLinksInURL)
message = msgfmtr.linkFixer.Replace(message)
}
message = msgfmtr.Compile(message, boardDir)
// prepare each line to be formatted
postLines := strings.Split(message, "<br>")
postLines := brRE.Split(message, -1)
boardConfig := config.GetBoardConfig(boardDir)
var err error
for i, line := range postLines {
trimmedLine := strings.TrimSpace(line)
lineWords := strings.Split(trimmedLine, " ")
@ -110,7 +114,7 @@ func FormatMessage(message string, boardDir string) (template.HTML, error) {
var boardDir string
var linkParent int
if linkParent, boardDir, err = gcsql.GetTopPostAndBoardDirFromPostID(postID); err != nil {
gcutil.LogError(err).Caller().Int("childPostID", postID).Msg("Unable to get top post and board")
errEv.Caller().Int("childPostID", postID).Msg("Unable to get top post and board")
return "", fmt.Errorf("unable to get top post and board for post #%d", postID)
}
@ -131,9 +135,28 @@ func FormatMessage(message string, boardDir string) (template.HTML, error) {
if isGreentext {
line += "</span>"
}
err = nil
var classList string
line = hashTagRE.ReplaceAllStringFunc(line, func(tag string) string {
return fmt.Sprintf(`<span class="hashtag">%s</span>`, tag[1:len(tag)-1])
if err != nil {
return tag // don't bother processing if there's already an error
}
tagNoBrackets := tag[1 : len(tag)-1]
classList = "hashtag"
if strings.ToLower(tagNoBrackets) == "#nsfw" {
if boardConfig.Worksafe {
err = ErrWorksafeBoard
return ""
}
classList += " nsfw"
}
return fmt.Sprintf(`<span class="%s">%s</span>`, classList, tagNoBrackets)
})
if err != nil {
warnEv.Str("board", boardDir).Msg("NSFW tag found on worksafe board")
return "", err
}
postLines[i] = line
}
return template.HTML(strings.Join(postLines, "<br />")), nil // skipcq: GSC-G203

View file

@ -7,6 +7,7 @@ import (
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gcutil/testutil"
"github.com/stretchr/testify/assert"
lua "github.com/yuin/gopher-lua"
)
@ -164,7 +165,8 @@ func TestLinks(t *testing.T) {
func TestNoDoubleTags(t *testing.T) {
config.SetVersion(versionStr)
msgfmtr.Init()
rendered, err := FormatMessage(doubleTagPreRender, "")
_, warnEv, errEv := testutil.GetTestLogs(t)
rendered, err := FormatMessage(doubleTagPreRender, "", warnEv, errEv)
assert.NoError(t, err)
assert.EqualValues(t, doubleTagExpected, rendered)
}
@ -185,7 +187,8 @@ func TestLuaBBCode(t *testing.T) {
func diceRollRunner(t *testing.T, tC *diceRollerTestCase) {
var err error
tC.post.Message, err = FormatMessage(tC.post.MessageRaw, "")
_, warnEv, errEv := testutil.GetTestLogs(t)
tC.post.Message, err = FormatMessage(tC.post.MessageRaw, "", warnEv, errEv)
assert.NoError(t, err)
err = ApplyDiceRoll(&tC.post)
if tC.expectError {
@ -215,6 +218,7 @@ func TestDiceRoll(t *testing.T) {
func TestHashTags(t *testing.T) {
config.SetVersion(versionStr)
msgfmtr.Init()
_, warnEv, errEv := testutil.GetTestLogs(t)
msg := `[#tag]
[#t a g]
[ #tag]
@ -223,8 +227,9 @@ func TestHashTags(t *testing.T) {
>greentext [#tag]
[#line
test]
[#single] [#line] [#tags]
[#js<script>alert("lol")</script>injection]`
msgHTML, err := FormatMessage(msg, "test")
msgHTML, err := FormatMessage(msg, "test", warnEv, errEv)
if !assert.NoError(t, err) {
t.FailNow()
}
@ -237,6 +242,25 @@ test]
`<span class="greentext">&gt;greentext <span class="hashtag">#tag</span></span><br />`+
`[#line<br />`+
`test]<br />`+
`<span class="hashtag">#single</span> <span class="hashtag">#line</span> <span class="hashtag">#tags</span><br />`+
`<span class="hashtag">#js&lt;script&gt;alert(&#34;lol&#34;)&lt;/script&gt;injection</span>`,
), msgHTML)
}
func TestWorksafe(t *testing.T) {
config.SetVersion(versionStr)
msgfmtr.Init()
_, warnEv, errEv := testutil.GetTestLogs(t)
boardConfig := config.GetBoardConfig("test")
boardConfig.Worksafe = true
config.SetBoardConfig("test", boardConfig)
_, err := FormatMessage("[#nsfw] [#tag2]", "test", warnEv, errEv)
if !assert.ErrorIs(t, err, ErrWorksafeBoard) {
t.FailNow()
}
boardConfig.Worksafe = false
config.SetBoardConfig("test", boardConfig)
msgHTML, err := FormatMessage("[#nsfw]", "test", warnEv, errEv)
assert.NoError(t, err)
assert.Equal(t, template.HTML(`<span class="hashtag nsfw">#nsfw</span>`), msgHTML)
}

View file

@ -208,11 +208,9 @@ func getPostFromRequest(request *http.Request, infoEv, errEv *zerolog.Event) (po
return
}
func doFormatting(post *gcsql.Post, board *gcsql.Board, request *http.Request, errEv *zerolog.Event) (err error) {
func doFormatting(post *gcsql.Post, board *gcsql.Board, request *http.Request, warnEv, errEv *zerolog.Event) (err error) {
if len(post.MessageRaw) > board.MaxMessageLength {
errEv.Caller().
Int("messageLength", len(post.MessageRaw)).
Int("maxMessageLength", board.MaxMessageLength).Send()
warnEv.Int("messageLength", len(post.MessageRaw)).Int("maxMessageLength", board.MaxMessageLength).Send()
return errors.New("message is too long")
}
@ -232,8 +230,10 @@ func doFormatting(post *gcsql.Post, board *gcsql.Board, request *http.Request, e
return err
}
if post.Message, err = FormatMessage(post.MessageRaw, board.Dir); err != nil {
errEv.Err(err).Caller().Msg("Unable to format message")
post.Message, err = FormatMessage(post.MessageRaw, board.Dir, warnEv, errEv)
if errors.Is(err, ErrWorksafeBoard) {
return err
} else if err != nil {
return errors.New("unable to format message")
}
if err = ApplyDiceRoll(post); err != nil {
@ -345,7 +345,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
}
// do formatting and apply wordfilters
if err = doFormatting(post, board, request, errEv); err != nil {
if err = doFormatting(post, board, request, warnEv, errEv); err != nil {
server.ServeError(writer, err.Error(), wantsJSON, nil)
return
}