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

Merge branch 'master' into staff-rank-update

This commit is contained in:
Eggbertx 2025-02-24 17:28:09 -08:00
commit c5aa7a438d
172 changed files with 7301 additions and 5089 deletions

30
.github/workflows/go.yml vendored Normal file
View file

@ -0,0 +1,30 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Build
run: ./build.py
- name: Build plugins
run: ./build.py build --plugin ./examples/plugins/geoip-legacy/ &&./build.py build --plugin ./examples/plugins/gochaninfo-mgmt-action/ && ./build.py build --plugin ./examples/plugins/ip2location/
- name: Test
run: go test -cover -v ./pkg/... ./cmd/...

35
.gitignore vendored
View file

@ -1,23 +1,30 @@
/gochan*
/lib/
/log/
/releases/
/vagrant/.vagrant/
/html/boards.json
*.bak
/html/index.html
/html/test*
/html/js/
/templates/override
*.bak
*.log
/html/boards.json
.vscode/settings.json
.parcel-cache
*.swp
*.db
.vagrant/
*.log
/templates/override
# Go output
/gochan*
/releases/
*.so
__debug_bin
__debug_bin*
# Node.js/TypeScript
/html/js/
node_modules
/frontend/coverage
/frontend/tests/coverage
.parcel-cache
# Python
__pycache__
.venv/
.vscode/settings.json
# SQLite
*.db
*.*-journal

14
.vscode/launch.json vendored
View file

@ -11,12 +11,7 @@
"mode": "auto",
"cwd":"${workspaceFolder}",
"program": "${workspaceFolder}/cmd/gochan/",
"env": {},
"args": [],
"dlvLoadConfig": {
"maxStringLen": 5000,
"maxArrayValues": 500
},
"showGlobalVariables": true
},
{
"name": "gochan-migration",
@ -25,12 +20,7 @@
"mode": "auto",
"cwd":"${workspaceFolder}",
"program": "${workspaceFolder}/cmd/gochan-migration/",
"env": {},
"args": [],
"dlvLoadConfig": {
"maxStringLen": 5000,
"maxArrayValues": 500
},
"showGlobalVariables": true
}
]
}

View file

@ -1,4 +1,4 @@
Copyright (c) 2013-2024, Gochan development group
Copyright (c) 2013-2025, Gochan development group
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -60,11 +60,10 @@ See [`frontend/README.md`](frontend/README.md) for information on working with S
## Roadmap
### Near future
* Fully implement cyclical threads
* Implement +50
* Add more banners
* Add more plugin support (more event triggers)
### Lower priority
* RSS feeds from boards/specific threads/specific usernames+tripcodes (such as newsanon)
* Pinning a post within a thread even if its not the OP, to prevent its deletion in a cyclical thread.
* Pinning a post within a thread even if its not the OP, to prevent its deletion in a cyclic thread.

View file

@ -39,7 +39,7 @@ release_files = (
"README.md",
)
GOCHAN_VERSION = "4.0.1"
GOCHAN_VERSION = "4.1.0"
DATABASE_VERSION = "4" # stored in DBNAME.DBPREFIXdatabase_version
PATH_NOTHING = -1
@ -425,15 +425,13 @@ def sass(watch=False):
sys.exit(status)
def test(verbose=False, coverage=False):
pkgs = os.listdir("pkg")
for pkg in pkgs:
cmd = ["go", "test"]
if verbose:
cmd += ["-v"]
if coverage:
cmd += ["-cover"]
cmd += [path.join("./pkg", pkg)]
run_cmd(cmd, realtime=True, print_command=True)
cmd = ["go", "test"]
if verbose:
cmd += ["-v"]
if coverage:
cmd += ["-cover"]
cmd += ["./pkg/...", "./cmd/..."]
run_cmd(cmd, realtime=True, print_command=True)
if __name__ == "__main__":

View file

@ -25,10 +25,14 @@ func (me *MigrationError) OldChanType() string {
func (me *MigrationError) Error() string {
from := me.oldChanType
errStr := "unable to migrate"
if from != "" {
from = " from " + from
errStr += " from " + from
}
return "unable to migrate " + from + ": " + me.errMessage
if me.errMessage != "" {
errStr += ": " + me.errMessage
}
return errStr
}
func NewMigrationError(oldChanType string, errMessage string) *MigrationError {
@ -41,48 +45,45 @@ type MigrationOptions struct {
OldChanConfig string
OldDBName string
NewDBName string
DirAction int
}
// DBMigrator is used for handling the migration from one database type to a
// database compatible with gochan 3.x onward
// database compatible with the latest gochan database version
type DBMigrator interface {
// Init sets the variables for connecting to the databases
// Init sets up the migrator and sets up the database connection(s)
Init(options *MigrationOptions) error
// IsMigrated checks to see if the database has already been migrated and quits if it has
// and returns any errors that aren't "table doesn't exist". if the boolean value is true,
// it can be assumed that the database has already been migrated and gochan-migration
// will exit
// IsMigrated returns true if the database is already migrated, and an error if any occured,
// excluding missing table errors
IsMigrated() (bool, error)
// MigrateDB alters the database schema to match the new schema, then migrates the imageboard
// data (posts, boards, etc) to the new database. It is assumed that MigrateDB will handle
// logging any errors that occur during the migration
// IsMigratingInPlace returns true if the source database and the destination database are both the
// same installation, meaning both have the same host/connection, database, table and prefix, meaning that
// the tables will be altered during the migration to match the new schema, instead of creating tables in
// the destination database and copying data over
IsMigratingInPlace() bool
// MigrateDB handles migration of the source database, altering it in place or migrating it to the configured
// gochan database. It returns true if the database is already migrated and an error if any occured. It is
// assumed that MigrateDB implementations will handle logging any errors that occur during the migration
MigrateDB() (bool, error)
// MigrateBoards gets info about the old boards in the board table and inserts each one
// into the new database if they don't already exist
// MigrateBoards migrates the board sections and boards if each one doesn't already exists
MigrateBoards() error
// MigratePosts gets the threads and replies in the old database, and inserts them into
// the new database, creating new threads to avoid putting replies in threads that already
// exist
// MigratePosts migrates the threads and replies (excluding deleted ones), creating new threads where necessary
MigratePosts() error
// MigrateStaff gets the staff list in the old board and inserts them into the new board if
// the username doesn't already exist. It sets the starting password to the given password
MigrateStaff(password string) error
// MigrateStaff migrates the staff, creating new staff accounts that don't already exist. Accounts created by this
// will need to have their password reset in order to be logged into
MigrateStaff() error
// MigrateBans gets the list of bans and appeals in the old database and inserts them into the
// new one if, for each entry, the IP/name/etc isn't already banned for the same length
// e.g. 1.1.1.1 is permabanned on both, 1.1.2.2 is banned for 5 days on both, etc
// MigrateBans migrates IP bans, appeals, and filters
MigrateBans() error
// MigrateAnnouncements gets the list of public and staff announcements in the old database
// and inserts them into the new database,
// MigrateAnnouncements migrates the list of public and staff announcements, if applicable
MigrateAnnouncements() error
// Close closes the database if initialized and deltes the temporary columns created
// Close closes the database if initialized and deletes any temporary columns created
Close() error
}

View file

@ -5,6 +5,7 @@ import (
"io/fs"
"os"
"path"
"testing"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcutil"
@ -21,6 +22,16 @@ var (
migrationLog zerolog.Logger
)
func InitTestMigrationLog(t *testing.T) (err error) {
dir := os.TempDir()
migrationLogFile, err = os.CreateTemp(dir, "migration-test")
if err != nil {
return err
}
migrationLog = zerolog.New(zerolog.NewTestWriter(t))
return nil
}
func InitMigrationLog() (err error) {
if migrationLogFile != nil {
// Migration log already initialized

View file

@ -88,7 +88,7 @@ func RunSQLFile(path string, db *gcsql.GCDB) error {
for _, statement := range sqlArr {
statement = strings.TrimSpace(statement)
if len(statement) > 0 {
if _, err = db.ExecSQL(statement); err != nil {
if _, err = db.Exec(nil, statement); err != nil {
return err
}
}

View file

@ -32,7 +32,6 @@ type filenameOrUsernameBanBase struct {
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
}

View file

@ -2,7 +2,6 @@ package gcupdate
import (
"context"
"errors"
"fmt"
"strings"
"time"
@ -22,6 +21,11 @@ type GCDatabaseUpdater struct {
TargetDBVer int
}
// IsMigratingInPlace implements common.DBMigrator.
func (*GCDatabaseUpdater) IsMigratingInPlace() bool {
return true
}
func (dbu *GCDatabaseUpdater) Init(options *common.MigrationOptions) error {
dbu.options = options
sqlCfg := config.GetSQLConfig()
@ -32,7 +36,7 @@ func (dbu *GCDatabaseUpdater) Init(options *common.MigrationOptions) error {
func (dbu *GCDatabaseUpdater) IsMigrated() (bool, error) {
var currentDatabaseVersion int
err := dbu.db.QueryRowSQL(`SELECT version FROM DBPREFIXdatabase_version WHERE component = 'gochan'`, nil,
err := dbu.db.QueryRow(nil, "SELECT version FROM DBPREFIXdatabase_version WHERE component = 'gochan'", nil,
[]any{&currentDatabaseVersion})
if err != nil {
return false, err
@ -174,7 +178,7 @@ func (dbu *GCDatabaseUpdater) migrateFileBans(ctx context.Context, sqlConfig *co
tx, err := dbu.db.BeginTx(ctx, nil)
defer func() {
if a := recover(); a != nil {
err = errors.New(fmt.Sprintf("recovered: %v", a))
err = fmt.Errorf("recovered: %v", a)
errEv.Caller(4).Err(err).Send()
errEv.Discard()
} else if err != nil {
@ -262,7 +266,7 @@ func (dbu *GCDatabaseUpdater) migrateFilenameBans(ctx context.Context, _ *config
tx, err := dbu.db.BeginTx(ctx, nil)
defer func() {
if a := recover(); a != nil {
err = errors.New(fmt.Sprintf("recovered: %v", a))
err = fmt.Errorf("recovered: %v", a)
errEv.Caller(4).Err(err).Send()
errEv.Discard()
} else if err != nil {
@ -320,7 +324,7 @@ func (dbu *GCDatabaseUpdater) migrateUsernameBans(ctx context.Context, _ *config
tx, err := dbu.db.BeginTx(ctx, nil)
defer func() {
if a := recover(); a != nil {
err = errors.New(fmt.Sprintf("recovered: %v", a))
err = fmt.Errorf("recovered: %v", a)
errEv.Caller(4).Err(err).Send()
errEv.Discard()
} else if err != nil {
@ -378,7 +382,7 @@ func (dbu *GCDatabaseUpdater) migrateWordfilters(ctx context.Context, sqlConfig
tx, err := dbu.db.BeginTx(ctx, nil)
defer func() {
if a := recover(); a != nil {
err = errors.New(fmt.Sprintf("recovered: %v", a))
err = fmt.Errorf("recovered: %v", a)
errEv.Caller(4).Err(err).Send()
errEv.Discard()
} else if err != nil {
@ -471,7 +475,7 @@ func (*GCDatabaseUpdater) MigratePosts() error {
return gcutil.ErrNotImplemented
}
func (*GCDatabaseUpdater) MigrateStaff(_ string) error {
func (*GCDatabaseUpdater) MigrateStaff() error {
return gcutil.ErrNotImplemented
}

View file

@ -0,0 +1,66 @@
package pre2021
import (
"errors"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
"github.com/gochan-org/gochan/pkg/gcsql"
)
type migrationAnnouncement struct {
gcsql.Announcement
oldPoster string
}
func (m *Pre2021Migrator) MigrateAnnouncements() error {
errEv := common.LogError()
defer errEv.Discard()
rows, err := m.db.Query(nil, announcementsQuery)
if err != nil {
errEv.Err(err).Caller().Msg("Failed to get announcements")
return err
}
defer rows.Close()
if _, err = m.getMigrationUser(errEv); err != nil {
return err
}
var oldAnnouncements []migrationAnnouncement
for rows.Next() {
var announcement migrationAnnouncement
if err = rows.Scan(&announcement.ID, &announcement.Subject, &announcement.Message, &announcement.oldPoster, &announcement.Timestamp); err != nil {
errEv.Err(err).Caller().Msg("Failed to scan announcement row")
return err
}
oldAnnouncements = append(oldAnnouncements, announcement)
}
for _, announcement := range oldAnnouncements {
announcement.StaffID, err = gcsql.GetStaffID(announcement.oldPoster)
if errors.Is(err, gcsql.ErrUnrecognizedUsername) {
// user doesn't exist, use migration user
common.LogWarning().Str("staff", announcement.oldPoster).Msg("Staff username not found in database")
announcement.Message += "\n(originally by " + announcement.oldPoster + ")"
announcement.StaffID = m.migrationUser.ID
} else if err != nil {
errEv.Err(err).Caller().Str("staff", announcement.oldPoster).Msg("Failed to get staff ID")
return err
}
if _, err = gcsql.Exec(nil,
"INSERT INTO DBPREFIXannouncements(staff_id,subject,message,timestamp) values(?,?,?,?)",
announcement.StaffID, announcement.Subject, announcement.Message, announcement.Timestamp,
); err != nil {
errEv.Err(err).Caller().Str("staff", announcement.oldPoster).Msg("Failed to migrate announcement")
return err
}
}
if err = rows.Close(); err != nil {
errEv.Err(err).Caller().Msg("Failed to close announcement rows")
return err
}
return nil
}

View file

@ -0,0 +1,32 @@
package pre2021
import (
"testing"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/stretchr/testify/assert"
)
func TestMigrateAnnouncements(t *testing.T) {
outDir := t.TempDir()
migrator := setupMigrationTest(t, outDir, false)
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
t.FailNow()
}
if !assert.NoError(t, migrator.MigrateBoards()) {
t.FailNow()
}
if !assert.NoError(t, migrator.MigrateStaff()) {
t.FailNow()
}
if !assert.NoError(t, migrator.MigrateAnnouncements()) {
t.FailNow()
}
var numAnnouncements int
assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXannouncements WHERE staff_id > 0", nil, []any{&numAnnouncements}))
assert.Equal(t, 2, numAnnouncements, "Expected to have two announcement")
}

View file

@ -0,0 +1,232 @@
package pre2021
import (
"context"
"database/sql"
"errors"
"net"
"os"
"strings"
"time"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gcutil"
"github.com/rs/zerolog"
)
type migrationBan struct {
oldID int
allowRead string
ip string
name string
nameIsRegex bool
filename string
fileChecksum string
boards string
staff string
timestamp time.Time
expires time.Time
permaban bool
reason string
banType int
staffNote string
appealAt time.Time
canAppeal bool
boardIDs []int
staffID int
}
func (m *Pre2021Migrator) migrateBansInPlace() error {
errEv := common.LogError()
defer errEv.Discard()
initSQLPath := gcutil.FindResource("sql/initdb_" + m.db.SQLDriver() + ".sql")
ba, err := os.ReadFile(initSQLPath)
if err != nil {
errEv.Err(err).Caller().
Str("initDBFile", initSQLPath).
Msg("Failed to read initdb file")
return err
}
statements := strings.Split(string(ba), ";")
for _, stmt := range statements {
stmt = strings.TrimSpace(stmt)
if strings.HasPrefix(stmt, "CREATE TABLE DBPREFIXip_ban") || strings.HasPrefix(stmt, "CREATE TABLE DBPREFIXfilter") {
_, err = gcsql.Exec(nil, stmt)
if err != nil {
errEv.Err(err).Caller().
Str("statement", stmt).
Msg("Failed to create table")
return err
}
}
}
// since the table names are different, migrateBansToNewDB can be called directly to migrate bans
return m.migrateBansToNewDB()
}
func (*Pre2021Migrator) migrateBan(tx *sql.Tx, ban *migrationBan, boardID *int, errEv *zerolog.Event) error {
migratedBan := &gcsql.IPBan{
BoardID: boardID,
RangeStart: ban.ip,
RangeEnd: ban.ip,
IssuedAt: ban.timestamp,
}
migratedBan.CanAppeal = ban.canAppeal
migratedBan.AppealAt = ban.appealAt
migratedBan.ExpiresAt = ban.expires
migratedBan.Permanent = ban.permaban
migratedBan.Message = ban.reason
migratedBan.StaffID = ban.staffID
migratedBan.StaffNote = ban.staffNote
if err := gcsql.NewIPBan(migratedBan, &gcsql.RequestOptions{Tx: tx}); err != nil {
errEv.Err(err).Caller().
Int("oldID", ban.oldID).Msg("Failed to migrate ban")
return err
}
return nil
}
func (m *Pre2021Migrator) migrateBansToNewDB() error {
errEv := common.LogError()
defer errEv.Discard()
tx, err := gcsql.BeginTx()
if err != nil {
errEv.Err(err).Caller().Msg("Failed to start transaction")
return err
}
defer tx.Rollback()
rows, err := m.db.Query(nil, bansQuery)
if err != nil {
errEv.Err(err).Caller().Msg("Failed to get bans")
return err
}
defer rows.Close()
for rows.Next() {
var ban migrationBan
if err = rows.Scan(
&ban.oldID, &ban.allowRead, &ban.ip, &ban.name, &ban.nameIsRegex, &ban.filename, &ban.fileChecksum,
&ban.boards, &ban.staff, &ban.timestamp, &ban.expires, &ban.permaban, &ban.reason, &ban.banType, &ban.staffNote, &ban.appealAt, &ban.canAppeal,
); err != nil {
errEv.Err(err).Caller().Msg("Failed to scan ban row")
return err
}
if ban.boards != "" && ban.boards != "*" {
boardDirs := strings.Split(ban.boards, ",")
for _, dir := range boardDirs {
dir = strings.TrimSpace(dir)
boardID, err := gcsql.GetBoardIDFromDir(dir)
if err != nil {
if errors.Is(err, gcsql.ErrBoardDoesNotExist) {
common.Logger().Warn().Str("board", dir).Msg("Found unrecognized ban board")
continue
} else {
errEv.Err(err).Caller().Str("board", dir).Msg("Failed getting board ID from dir")
return err
}
}
ban.boardIDs = append(ban.boardIDs, boardID)
}
}
migrationUser, err := m.getMigrationUser(errEv)
if err != nil {
return err
}
ban.staffID, err = gcsql.GetStaffID(ban.staff)
if errors.Is(err, gcsql.ErrUnrecognizedUsername) {
// username not found after staff were migrated, use a stand-in account to be updated by the admin later
common.LogWarning().
Str("username", ban.staff).
Str("migrationUser", migrationUser.Username).
Msg("Ban staff not found in migrated staff table, using migration user instead")
ban.staffID = migrationUser.ID
} else if err != nil {
errEv.Err(err).Caller().Str("username", ban.staff).Msg("Failed to get staff from username")
return err
}
if ban.ip == "" && ban.name == "" && ban.fileChecksum == "" && ban.filename == "" {
common.LogWarning().Int("banID", ban.oldID).Msg("Found invalid ban (no IP, name, file checksum, or filename set)")
continue
}
if ban.ip != "" {
if net.ParseIP(ban.ip) == nil {
gcutil.LogWarning().
Int("oldID", ban.oldID).
Str("ip", ban.ip).
Msg("Found ban with invalid IP address, skipping")
continue
}
if len(ban.boardIDs) == 0 {
if err = m.migrateBan(tx, &ban, nil, errEv); err != nil {
return err
}
} else {
for b := range ban.boardIDs {
if err = m.migrateBan(tx, &ban, &ban.boardIDs[b], errEv); err != nil {
return err
}
}
}
}
if ban.name != "" || ban.fileChecksum != "" || ban.filename != "" {
filter := &gcsql.Filter{
StaffID: &ban.staffID,
StaffNote: ban.staffNote,
IsActive: true,
HandleIfAny: true,
MatchAction: "reject",
MatchDetail: ban.reason,
}
var conditions []gcsql.FilterCondition
if ban.name != "" {
nameCondition := gcsql.FilterCondition{
Field: "name",
Search: ban.name,
MatchMode: gcsql.ExactMatch,
}
if ban.nameIsRegex {
nameCondition.MatchMode = gcsql.RegexMatch
}
conditions = append(conditions, nameCondition)
}
if ban.fileChecksum != "" {
conditions = append(conditions, gcsql.FilterCondition{
Field: "checksum",
MatchMode: gcsql.ExactMatch,
Search: ban.fileChecksum,
})
}
if ban.filename != "" {
filenameCondition := gcsql.FilterCondition{
Field: "filename",
Search: ban.filename,
MatchMode: gcsql.ExactMatch,
}
if ban.nameIsRegex {
filenameCondition.MatchMode = gcsql.RegexMatch
}
conditions = append(conditions, filenameCondition)
}
if err = gcsql.ApplyFilterTx(context.Background(), tx, filter, conditions, ban.boardIDs); err != nil {
errEv.Err(err).Caller().Int("banID", ban.oldID).Msg("Failed to migrate ban to filter")
return err
}
}
}
return tx.Commit()
}
func (m *Pre2021Migrator) MigrateBans() error {
if m.IsMigratingInPlace() {
return m.migrateBansInPlace()
}
return m.migrateBansToNewDB()
}

View file

@ -0,0 +1,58 @@
package pre2021
import (
"testing"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/stretchr/testify/assert"
)
func TestMigrateBans(t *testing.T) {
outDir := t.TempDir()
migrator := setupMigrationTest(t, outDir, false)
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
t.FailNow()
}
if !assert.NoError(t, migrator.MigrateBoards()) {
t.FailNow()
}
if !assert.NoError(t, migrator.MigratePosts()) {
t.FailNow()
}
if !assert.NoError(t, migrator.MigrateStaff()) {
t.FailNow()
}
if !assert.NoError(t, migrator.MigrateBans()) {
t.FailNow()
}
validateBanMigration(t)
}
func validateBanMigration(t *testing.T) {
bans, err := gcsql.GetIPBans(0, 200, false)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, 6, len(bans), "Expected to have 4 valid bans")
assert.NotZero(t, bans[0].StaffID, "Expected ban staff ID field to be set")
var numInvalidBans int
assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXip_ban WHERE message = ?", []any{"Full ban on 8.8.0.0/16"}, []any{&numInvalidBans}))
assert.Equal(t, 0, numInvalidBans, "Expected the invalid test to not be migrated")
filters, err := gcsql.GetAllFilters(gcsql.TrueOrFalse)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, 1, len(filters))
conditions, err := filters[0].Conditions()
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, 3, len(conditions), "Expected filter to have three conditions")
}

View file

@ -1,133 +1,205 @@
package pre2021
import (
"log"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql"
)
func (m *Pre2021Migrator) MigrateBoards() error {
if m.oldBoards == nil {
m.oldBoards = map[int]string{}
}
if m.newBoards == nil {
m.newBoards = map[int]string{}
}
// get all boards from new db
err := gcsql.ResetBoardSectionArrays()
type migrationBoard struct {
oldSectionID int
oldID int
gcsql.Board
}
type migrationSection struct {
oldID int
gcsql.Section
}
func (m *Pre2021Migrator) migrateSections() error {
// creates sections in the new db if they don't exist, and also creates a migration section that
// boards will be set to, to be moved to the correct section by the admin after migration
errEv := common.LogError()
defer errEv.Discard()
// populate m.sections with all sections from the new db
currentAllSections, err := gcsql.GetAllSections(false)
if err != nil {
return nil
errEv.Err(err).Caller().Msg("Failed to get all sections from new db")
return err
}
// get boards from old db
rows, err := m.db.QuerySQL(boardsQuery)
for _, section := range currentAllSections {
m.sections = append(m.sections, migrationSection{
oldID: -1,
Section: section,
})
}
var sectionsToBeCreated []gcsql.Section
rows, err := m.db.Query(nil, sectionsQuery)
if err != nil {
errEv.Err(err).Caller().Msg("Failed to query old database sections")
return err
}
defer rows.Close()
for rows.Next() {
var id int
var dir string
var title string
var subtitle string
var description string
var section int
var max_file_size int
var max_pages int
var default_style string
var locked bool
var anonymous string
var forced_anon bool
var max_age int
var autosage_after int
var no_images_after int
var max_message_length int
var embeds_allowed bool
var redirect_to_thread bool
var require_file bool
var enable_catalog bool
if err = rows.Scan(
&id,
&dir,
&title,
&subtitle,
&description,
&section,
&max_file_size,
&max_pages,
&default_style,
&locked,
&anonymous,
&forced_anon,
&max_age,
&autosage_after,
&no_images_after,
&max_message_length,
&embeds_allowed,
&redirect_to_thread,
&require_file,
&enable_catalog); err != nil {
var section gcsql.Section
if err = rows.Scan(&section.ID, &section.Position, &section.Hidden, &section.Name, &section.Abbreviation); err != nil {
errEv.Err(err).Caller().Msg("Failed to scan row into section")
return err
}
found := false
for b := range gcsql.AllBoards {
if _, ok := m.oldBoards[id]; !ok {
m.oldBoards[id] = dir
}
if gcsql.AllBoards[b].Dir == dir {
log.Printf("Board /%s/ already exists in new db, moving on\n", dir)
var found bool
for s, newSection := range m.sections {
if section.Name == newSection.Name {
// section already exists, update values
m.sections[s].oldID = section.ID
m.sections[s].Abbreviation = section.Abbreviation
m.sections[s].Hidden = section.Hidden
m.sections[s].Position = section.Position
common.LogInfo().
Int("sectionID", section.ID).
Int("oldSectionID", m.sections[s].oldID).
Str("sectionName", section.Name).
Str("sectionAbbreviation", section.Abbreviation).
Msg("Section already exists in new db, values will be updated")
found = true
break
}
}
if !found {
sectionsToBeCreated = append(sectionsToBeCreated, section)
}
}
if err = rows.Close(); err != nil {
errEv.Caller().Msg("Failed to close section rows")
return err
}
for _, section := range sectionsToBeCreated {
migratedSection, err := gcsql.NewSection(section.Name, section.Abbreviation, section.Hidden, section.Position)
if err != nil {
errEv.Err(err).Caller().Str("sectionName", section.Name).Msg("Failed to migrate section")
return err
}
m.sections = append(m.sections, migrationSection{
Section: *migratedSection,
})
}
for s, section := range m.sections {
if err = m.sections[s].UpdateValues(); err != nil {
errEv.Err(err).Caller().Str("sectionName", section.Name).Msg("Failed to update pre-existing section values")
}
}
return nil
}
func (m *Pre2021Migrator) MigrateBoards() error {
m.boards = nil
errEv := common.LogError()
defer errEv.Discard()
// get all boards from new db
err := gcsql.ResetBoardSectionArrays()
if err != nil {
errEv.Err(err).Caller().Msg("Failed to reset board section arrays")
return nil
}
if err = m.migrateSections(); err != nil {
// error should already be logged by migrateSectionsToNewDB
return err
}
allBoards, err := gcsql.GetAllBoards(false)
if err != nil {
errEv.Err(err).Caller().Msg("Failed to get all boards from new db")
return err
}
for _, board := range allBoards {
m.boards = append(m.boards, migrationBoard{
oldSectionID: -1,
oldID: -1,
Board: board,
})
}
// get boards from old db
rows, err := m.db.Query(nil, boardsQuery)
if err != nil {
errEv.Err(err).Caller().Msg("Failed to query old database boards")
return err
}
defer rows.Close()
var boardsTmp []migrationBoard
for rows.Next() {
var board migrationBoard
var maxPages int
if err = rows.Scan(
&board.oldID, &board.NavbarPosition, &board.Dir, &board.Title, &board.Subtitle, &board.Description,
&board.SectionID, &board.MaxFilesize, &maxPages, &board.DefaultStyle, &board.Locked, &board.CreatedAt,
&board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength,
&board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog,
); err != nil {
errEv.Err(err).Caller().Msg("Failed to scan row into board")
return err
}
board.MaxThreads = maxPages * config.GetBoardConfig(board.Dir).ThreadsPerPage
boardsTmp = append(boardsTmp, board)
}
for _, board := range boardsTmp {
found := false
for b, newBoard := range m.boards {
if newBoard.Dir == board.Dir {
m.boards[b].oldID = board.oldID
m.boards[b].oldSectionID = board.SectionID
common.LogInfo().
Str("board", board.Dir).
Int("oldBoardID", board.ID).
Int("migratedBoardID", newBoard.ID).
Msg("Board already exists in new db, updating values")
// don't update other values in the array since they don't affect migrating threads or posts
if _, err = gcsql.Exec(nil, `UPDATE DBPREFIXboards
SET uri = ?, navbar_position = ?, title = ?, subtitle = ?, description = ?,
max_file_size = ?, max_threads = ?, default_style = ?, locked = ?,
anonymous_name = ?, force_anonymous = ?, autosage_after = ?, no_images_after = ?, max_message_length = ?,
min_message_length = ?, allow_embeds = ?, redirect_to_thread = ?, require_file = ?, enable_catalog = ?
WHERE id = ?`,
board.Dir, board.NavbarPosition, board.Title, board.Subtitle, board.Description,
board.MaxFilesize, board.MaxThreads, board.DefaultStyle, board.Locked,
board.AnonymousName, board.ForceAnonymous, board.AutosageAfter, board.NoImagesAfter, board.MaxMessageLength,
board.MinMessageLength, board.AllowEmbeds, board.RedirectToThread, board.RequireFile, board.EnableCatalog,
newBoard.ID); err != nil {
errEv.Err(err).Caller().Str("board", board.Dir).Msg("Failed to update board values")
return err
}
found = true
break
}
}
if found {
continue
}
// create new board using the board data from the old db
// omitting things like ID and creation date since we don't really care
if err = gcsql.CreateBoard(&gcsql.Board{
Dir: dir,
Title: title,
Subtitle: subtitle,
Description: description,
SectionID: section,
MaxFilesize: max_file_size,
// MaxPages: max_pages,
DefaultStyle: default_style,
Locked: locked,
AnonymousName: anonymous,
ForceAnonymous: forced_anon,
// MaxAge: max_age,
AutosageAfter: autosage_after,
NoImagesAfter: no_images_after,
MaxMessageLength: max_message_length,
AllowEmbeds: embeds_allowed,
RedirectToThread: redirect_to_thread,
RequireFile: require_file,
EnableCatalog: enable_catalog,
}, false); err != nil {
if err = gcsql.CreateBoard(&board.Board, board.IsHidden(false)); err != nil {
errEv.Err(err).Caller().Str("board", board.Dir).Msg("Failed to create board")
return err
}
m.newBoards[id] = dir
log.Printf("/%s/ successfully migrated in the database", dir)
// Automatic directory migration has the potential to go horribly wrong, so I'm leaving this
// commented out for now
// switch m.options.DirAction {
// case common.DirCopy:
// case common.DirMove:
// // move the old directory (probably should copy instead) to the new one
// newDocumentRoot := config.GetSystemCriticalConfig().DocumentRoot
// log.Println("Old board path:", path.Join(m.config.DocumentRoot, dir))
// log.Println("Old board path:", path.Join(newDocumentRoot, dir))
// if err = os.Rename(
// path.Join(m.config.DocumentRoot, dir),
// path.Join(newDocumentRoot, dir),
// ); err != nil {
// return err
// }
// log.Printf("/%s/ directory/files successfully moved")
// }
m.boards = append(m.boards, board)
common.LogInfo().
Str("dir", board.Dir).
Int("boardID", board.ID).
Msg("Board successfully created")
}
if err = gcsql.ResetBoardSectionArrays(); err != nil {
errEv.Err(err).Caller().Msg("Failed to reset board and section arrays")
return err
}
return nil
}

View file

@ -0,0 +1,70 @@
package pre2021
import (
"testing"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/stretchr/testify/assert"
)
func TestMigrateBoards(t *testing.T) {
outDir := t.TempDir()
migrator := setupMigrationTest(t, outDir, false)
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
t.FailNow()
}
assert.NoError(t, gcsql.ResetBoardSectionArrays())
numBoards := len(gcsql.AllBoards)
numSections := len(gcsql.AllSections)
assert.Equal(t, 1, numBoards, "Expected to have 1 board pre-migration (/test/ is automatically created during provisioning)")
assert.Equal(t, 1, numSections, "Expected to have 1 section pre-migration (Main is automatically created during provisioning)")
if !assert.NoError(t, migrator.MigrateBoards()) {
t.FailNow()
}
validateBoardMigration(t)
}
func validateBoardMigration(t *testing.T) {
migratedBoards, err := gcsql.GetAllBoards(false)
if !assert.NoError(t, err) {
t.FailNow()
}
migratedSections, err := gcsql.GetAllSections(false)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, len(migratedBoards), 3, "Expected updated boards list to have three boards")
assert.Equal(t, len(migratedSections), 2, "Expected updated sections list to have two sections")
// Test migrated sections
mainSection, err := gcsql.GetSectionFromName("Main")
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, "mainmigration", mainSection.Abbreviation, "Expected Main section to have updated abbreviation name 'mainmigration'")
// Test migrated boards
testBoard, err := gcsql.GetBoardFromDir("test")
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Greater(t, testBoard.ID, 0)
assert.Equal(t, "Testing Board", testBoard.Title)
assert.Equal(t, "Board for testing pre-2021 migration", testBoard.Subtitle)
assert.Equal(t, "Board for testing pre-2021 migration description", testBoard.Description)
testBoardSection, err := gcsql.GetSectionFromID(testBoard.SectionID)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, "Main", testBoardSection.Name, "Expected /test/ board to be in Main section")
hiddenBoard, err := gcsql.GetBoardFromDir("hidden")
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, "Hidden Board", hiddenBoard.Title)
}

View file

@ -2,171 +2,186 @@ package pre2021
import (
"database/sql"
"fmt"
"log"
"time"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gcutil"
"github.com/rs/zerolog"
)
type postTable struct {
id int
boardid int
parentid int
name string
tripcode string
email string
subject string
message string
message_raw string
password string
filename string
filename_original string
file_checksum string
filesize int
image_w int
image_h int
thumb_w int
thumb_h int
ip string
tag string
timestamp time.Time
autosage bool
deleted_timestamp time.Time
bumped time.Time
stickied bool
locked bool
reviewed bool
type migrationPost struct {
gcsql.Post
autosage bool
bumped time.Time
stickied bool
locked bool
newBoardID int
// oldParentID int
filename string
filenameOriginal string
fileChecksum string
filesize int
imageW int
imageH int
thumbW int
thumbH int
oldID int
boardID int
oldBoardID int
oldParentID int
}
func (*Pre2021Migrator) migratePost(tx *sql.Tx, post *migrationPost, errEv *zerolog.Event) error {
var err error
opts := &gcsql.RequestOptions{Tx: tx}
if post.oldParentID == 0 {
// migrating post was a thread OP, create the row in the threads table
if post.ThreadID, err = gcsql.CreateThread(opts, post.boardID, false, post.stickied, post.autosage, false); err != nil {
errEv.Err(err).Caller().
Int("boardID", post.boardID).
Msg("Failed to create thread")
}
}
// insert thread top post
if err = post.Insert(true, post.boardID, false, post.stickied, post.autosage, false, opts); err != nil {
errEv.Err(err).Caller().
Int("boardID", post.boardID).
Int("threadID", post.ThreadID).
Msg("Failed to insert thread OP")
}
if post.filename != "" {
if err = post.AttachFile(&gcsql.Upload{
PostID: post.ID,
OriginalFilename: post.filenameOriginal,
Filename: post.filename,
Checksum: post.fileChecksum,
FileSize: post.filesize,
ThumbnailWidth: post.thumbW,
ThumbnailHeight: post.thumbH,
Width: post.imageW,
Height: post.imageH,
}, opts); err != nil {
errEv.Err(err).Caller().
Int("oldPostID", post.oldID).
Msg("Failed to attach upload to migrated post")
return err
}
}
return nil
}
func (m *Pre2021Migrator) MigratePosts() error {
var err error
if err = m.migrateThreads(); err != nil {
return err
}
return m.migratePostsUtil()
}
errEv := common.LogError()
defer errEv.Discard()
func (m *Pre2021Migrator) migrateThreads() error {
tx, err := m.db.Begin()
tx, err := gcsql.BeginTx()
if err != nil {
errEv.Err(err).Caller().Msg("Failed to start transaction")
return err
}
defer tx.Rollback()
stmt, err := m.db.PrepareSQL(postsQuery, tx)
rows, err := m.db.Query(nil, threadsQuery)
if err != nil {
tx.Rollback()
errEv.Err(err).Caller().Msg("Failed to get threads")
return err
}
rows, err := stmt.Query()
if err != nil && err != sql.ErrNoRows {
tx.Rollback()
return err
}
defer stmt.Close()
defer rows.Close()
var threadIDsWithInvalidBoards []int
var missingBoardIDs []int
var migratedThreads int
for rows.Next() {
var post postTable
var thread migrationPost
if err = rows.Scan(
&post.id,
&post.boardid,
&post.parentid,
&post.name,
&post.tripcode,
&post.email,
&post.subject,
&post.message,
&post.message_raw,
&post.password,
&post.filename,
&post.filename_original,
&post.file_checksum,
&post.filesize,
&post.image_w,
&post.image_h,
&post.thumb_w,
&post.thumb_h,
&post.ip,
&post.tag,
&post.timestamp,
&post.autosage,
&post.deleted_timestamp,
&post.bumped,
&post.stickied,
&post.locked,
&post.reviewed,
&thread.oldID, &thread.oldBoardID, &thread.oldParentID, &thread.Name, &thread.Tripcode, &thread.Email,
&thread.Subject, &thread.Message, &thread.MessageRaw, &thread.Password, &thread.filename,
&thread.filenameOriginal, &thread.fileChecksum, &thread.filesize, &thread.imageW, &thread.imageH,
&thread.thumbW, &thread.thumbH, &thread.IP, &thread.CreatedOn, &thread.autosage,
&thread.bumped, &thread.stickied, &thread.locked,
); err != nil {
tx.Rollback()
errEv.Err(err).Caller().Msg("Failed to scan thread")
return err
}
_, ok := m.oldBoards[post.boardid]
if !ok {
// board doesn't exist
log.Printf(
"Pre-migrated post #%d has an invalid boardid %d (board doesn't exist), skipping",
post.id, post.boardid)
var foundBoard bool
for _, board := range m.boards {
if board.oldID == thread.oldBoardID {
thread.boardID = board.ID
foundBoard = true
break
}
}
if !foundBoard {
threadIDsWithInvalidBoards = append(threadIDsWithInvalidBoards, thread.oldID)
missingBoardIDs = append(missingBoardIDs, thread.oldBoardID)
continue
}
// var stmt *sql.Stmt
// var err error
preparedStr, _ := gcsql.SetupSQLString(`SELECT id FROM DBPREFIXboards WHERE ui = ?`, m.db)
stmt, err := tx.Prepare(preparedStr)
if err != nil {
tx.Rollback()
if err = m.migratePost(tx, &thread, errEv); err != nil {
return err
}
stmt.QueryRow(post.boardid).Scan(&post.newBoardID)
// gcsql.QueryRowSQL(`SELECT id FROM DBPREFIXboards WHERE uri = ?`, []interface{}{})
if post.parentid == 0 {
// post is a thread, save it to the DBPREFIXthreads table
// []interfaceP{{post.newParentID}
if err = gcsql.QueryRowSQL(
`SELECT board_id FROM DBPREFIXthreads ORDER BY board_id LIMIT 1`,
nil,
[]interface{}{&post.newBoardID},
); err != nil {
tx.Rollback()
return err
}
fmt.Println("Current board ID:", post.newBoardID)
prepareStr, _ := gcsql.SetupSQLString(
`INSERT INTO DBPREFIXthreads
(board_id, locked, stickied)
VALUES(?, ?, ?)`, m.db)
stmt, err = tx.Prepare(prepareStr)
if err != nil {
tx.Rollback()
return err
}
stmt.Exec(post.newBoardID, post.locked, post.stickied)
// // stmt, err := db.Prepare("INSERT table SET unique_id=? ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)")
// gcsql.ExecSQL(`INSERT INTO DBPREFIXthreads (board_id) VALUES(?)`, post.newBoardID)
// /*
// id
// board_id
// locked
// stickied
// anchored
// cyclical
// last_bump
// deleted_at
// is_deleted
// */
// get and insert replies
replyRows, err := m.db.Query(nil, postsQuery+" AND parentid = ?", thread.oldID)
if err != nil {
errEv.Err(err).Caller().
Int("parentID", thread.oldID).
Msg("Failed to get reply rows")
return err
}
m.posts = append(m.posts, post)
}
return tx.Commit()
}
defer replyRows.Close()
func (*Pre2021Migrator) migratePostsUtil() error {
for replyRows.Next() {
var reply migrationPost
if err = replyRows.Scan(
&reply.oldID, &reply.oldBoardID, &reply.oldParentID, &reply.Name, &reply.Tripcode, &reply.Email,
&reply.Subject, &reply.Message, &reply.MessageRaw, &reply.Password, &reply.filename,
&reply.filenameOriginal, &reply.fileChecksum, &reply.filesize, &reply.imageW, &reply.imageH,
&reply.thumbW, &reply.thumbH, &reply.IP, &reply.CreatedOn, &reply.autosage,
&reply.bumped, &reply.stickied, &reply.locked,
); err != nil {
errEv.Err(err).Caller().
Int("parentID", thread.oldID).
Msg("Failed to scan reply")
return err
}
reply.ThreadID = thread.ThreadID
if err = m.migratePost(tx, &reply, errEv); err != nil {
return err
}
}
if thread.locked {
if _, err = gcsql.Exec(&gcsql.RequestOptions{Tx: tx}, "UPDATE DBPREFIXthreads SET locked = TRUE WHERE id = ?", thread.ThreadID); err != nil {
errEv.Err(err).Caller().
Int("threadID", thread.ThreadID).
Msg("Unable to re-lock migrated thread")
}
}
migratedThreads++
}
if err = rows.Close(); err != nil {
errEv.Err(err).Caller().Msg("Failed to close posts rows")
return err
}
if len(threadIDsWithInvalidBoards) > 0 {
errEv.Caller().
Ints("threadIDs", threadIDsWithInvalidBoards).
Ints("boardIDs", missingBoardIDs).
Msg("Failed to find boards for threads")
return common.NewMigrationError("pre2021", "Found threads with missing boards")
}
if err = tx.Commit(); err != nil {
errEv.Err(err).Caller().Msg("Failed to commit transaction")
return err
}
gcutil.LogInfo().
Int("migratedThreads", migratedThreads).
Msg("Migrated threads successfully")
return nil
}

View file

@ -0,0 +1,59 @@
package pre2021
import (
"testing"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/stretchr/testify/assert"
)
func TestMigratePosts(t *testing.T) {
outDir := t.TempDir()
migrator := setupMigrationTest(t, outDir, false)
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
t.FailNow()
}
if !assert.NoError(t, migrator.MigrateBoards()) {
t.FailNow()
}
var numThreads int
if !assert.NoError(t, migrator.db.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXposts WHERE parentid = 0 AND deleted_timestamp IS NULL", nil, []any{&numThreads}), "Failed to get number of threads") {
t.FailNow()
}
assert.Equal(t, 2, numThreads, "Expected to have two threads pre-migration")
if !assert.NoError(t, migrator.MigratePosts()) {
t.FailNow()
}
validatePostMigration(t)
}
func validatePostMigration(t *testing.T) {
var numThreads int
if !assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXthreads", nil, []any{&numThreads}), "Failed to get number of threads") {
t.FailNow()
}
assert.Equal(t, 2, numThreads, "Expected to have two threads pre-migration")
var numUploadPosts int
assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXfiles", nil, []any{&numUploadPosts}))
assert.Equal(t, 1, numUploadPosts, "Expected to have 1 upload post")
var ip string
assert.NoError(t, gcsql.QueryRow(nil, "SELECT IP_NTOA FROM DBPREFIXposts WHERE id = 1", nil, []any{&ip}))
assert.Equal(t, "192.168.56.1", ip, "Expected to have the correct IP address")
var numMigratedThreads int
if !assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXthreads", nil, []any{&numMigratedThreads}), "Failed to get number of migrated threads") {
t.FailNow()
}
assert.Equal(t, 2, numMigratedThreads, "Expected to have three migrated threads")
var locked bool
if !assert.NoError(t, gcsql.QueryRow(nil, "SELECT locked FROM DBPREFIXthreads WHERE id = 1", nil, []any{&locked})) {
t.FailNow()
}
assert.True(t, locked, "Expected thread ID 1 to be locked")
}

View file

@ -2,8 +2,11 @@
package pre2021
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
"github.com/gochan-org/gochan/pkg/config"
@ -12,12 +15,6 @@ import (
type Pre2021Config struct {
config.SQLConfig
// DBtype string
// DBhost string
// DBname string
// DBusername string
// DBpassword string
// DBprefix string
DocumentRoot string
}
@ -26,9 +23,16 @@ type Pre2021Migrator struct {
options *common.MigrationOptions
config Pre2021Config
posts []postTable
oldBoards map[int]string // map[boardid]dir
newBoards map[int]string // map[board]dir
migrationUser *gcsql.Staff
boards []migrationBoard
sections []migrationSection
staff []migrationStaff
}
// IsMigratingInPlace implements common.DBMigrator.
func (m *Pre2021Migrator) IsMigratingInPlace() bool {
sqlConfig := config.GetSQLConfig()
return m.config.DBname == sqlConfig.DBname && m.config.DBhost == sqlConfig.DBhost && m.config.DBprefix == sqlConfig.DBprefix
}
func (m *Pre2021Migrator) readConfig() error {
@ -36,84 +40,115 @@ func (m *Pre2021Migrator) readConfig() error {
if err != nil {
return err
}
m.config.SQLConfig = config.GetSQLConfig()
return json.Unmarshal(ba, &m.config)
}
func (m *Pre2021Migrator) Init(options *common.MigrationOptions) error {
m.options = options
var err error
if err = m.readConfig(); err != nil {
return err
}
m.config.DBTimeoutSeconds = config.DefaultSQLTimeout
m.config.DBMaxOpenConnections = config.DefaultSQLMaxConns
m.config.DBMaxIdleConnections = config.DefaultSQLMaxConns
m.config.DBConnMaxLifetimeMin = config.DefaultSQLConnMaxLifetimeMin
m.db, err = gcsql.Open(&m.config.SQLConfig)
return err
}
func (m *Pre2021Migrator) IsMigrated() (bool, error) {
var migrated bool
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(m.config.DBTimeoutSeconds)*time.Second)
defer cancel()
var sqlConfig config.SQLConfig
if m.IsMigratingInPlace() {
sqlConfig = config.GetSQLConfig()
} else {
sqlConfig = m.config.SQLConfig
}
return common.TableExists(ctx, m.db, nil, "DBPREFIXdatabase_version", &sqlConfig)
}
func (m *Pre2021Migrator) renameTablesForInPlace() error {
var err error
var query string
switch m.config.DBtype {
case "mysql":
fallthrough
case "postgres":
query = `SELECT COUNT(*) > 0 FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = ?`
default:
return false, gcsql.ErrUnsupportedDB
errEv := common.LogError()
defer errEv.Discard()
if _, err = m.db.Exec(nil, "DROP TABLE DBPREFIXinfo"); err != nil {
errEv.Err(err).Caller().Msg("Error dropping info table")
return err
}
if err = m.db.QueryRowSQL(query,
[]interface{}{m.config.DBprefix + "migrated", m.config.DBname},
[]interface{}{&migrated}); err != nil {
return migrated, err
for _, table := range renameTables {
if _, err = m.db.Exec(nil, fmt.Sprintf(renameTableStatementTemplate, table, table)); err != nil {
errEv.Caller().Err(err).
Str("table", table).
Msg("Error renaming table")
return err
}
}
return migrated, err
if err = gcsql.CheckAndInitializeDatabase(m.config.DBtype, "4"); err != nil {
errEv.Caller().Err(err).Msg("Error checking and initializing database")
return err
}
if err = m.Close(); err != nil {
errEv.Err(err).Caller().Msg("Error closing database")
return err
}
m.config.SQLConfig.DBprefix = "_tmp_" + m.config.DBprefix
m.db, err = gcsql.Open(&m.config.SQLConfig)
if err != nil {
errEv.Err(err).Caller().Msg("Error reopening database with new prefix")
return err
}
common.LogInfo().Msg("Renamed tables for in-place migration")
return err
}
func (m *Pre2021Migrator) MigrateDB() (bool, error) {
errEv := common.LogError()
defer errEv.Discard()
migrated, err := m.IsMigrated()
if err != nil {
errEv.Caller().Err(err).Msg("Error checking if database is migrated")
return false, err
}
if migrated {
// db is already migrated, stop
return true, nil
}
if m.IsMigratingInPlace() {
if err = m.renameTablesForInPlace(); err != nil {
return false, err
}
}
if err := m.MigrateBoards(); err != nil {
return false, err
}
// if err = m.MigratePosts(); err != nil {
// return false, err
// }
// if err = m.MigrateStaff("password"); err != nil {
// return false, err
// }
// if err = m.MigrateBans(); err != nil {
// return false, err
// }
// if err = m.MigrateAnnouncements(); err != nil {
// return false, err
// }
common.LogInfo().Msg("Migrated boards successfully")
return true, nil
}
if err = m.MigratePosts(); err != nil {
return false, err
}
common.LogInfo().Msg("Migrated threads, posts, and uploads successfully")
func (*Pre2021Migrator) MigrateStaff(_ string) error {
return nil
}
if err = m.MigrateStaff(); err != nil {
return false, err
}
common.LogInfo().Msg("Migrated staff successfully")
func (*Pre2021Migrator) MigrateBans() error {
return nil
}
if err = m.MigrateBans(); err != nil {
return false, err
}
common.LogInfo().Msg("Migrated bans and filters successfully")
func (*Pre2021Migrator) MigrateAnnouncements() error {
return nil
if err = m.MigrateAnnouncements(); err != nil {
return false, err
}
common.LogInfo().Msg("Migrated staff announcements successfully")
return false, nil
}
func (m *Pre2021Migrator) Close() error {

View file

@ -0,0 +1,127 @@
package pre2021
import (
"io"
"os"
"path"
"testing"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
"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"
)
const (
sqlite3DBDir = "tools/" // relative to gochan project root
)
func setupMigrationTest(t *testing.T, outDir string, migrateInPlace bool) *Pre2021Migrator {
dir, err := testutil.GoToGochanRoot(t)
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.NoError(t, common.InitTestMigrationLog(t)) {
t.FailNow()
}
dbName := "gochan-pre2021.sqlite3db"
dbHost := path.Join(dir, sqlite3DBDir, dbName)
migratedDBName := "gochan-migrated.sqlite3db"
migratedDBHost := path.Join(outDir, migratedDBName)
if migrateInPlace {
oldDbFile, err := os.Open(dbHost)
if !assert.NoError(t, err) {
t.FailNow()
}
defer oldDbFile.Close()
newDbFile, err := os.OpenFile(migratedDBHost, os.O_CREATE|os.O_WRONLY, 0600)
if !assert.NoError(t, err) {
t.FailNow()
}
defer newDbFile.Close()
_, err = io.Copy(newDbFile, oldDbFile)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.NoError(t, oldDbFile.Close())
assert.NoError(t, newDbFile.Close())
dbHost = migratedDBHost
dbName = migratedDBName
}
oldSQLConfig := config.SQLConfig{
DBtype: "sqlite3",
DBname: dbName,
DBhost: dbHost,
DBprefix: "gc_",
DBusername: "gochan",
DBpassword: "password",
DBTimeoutSeconds: 600,
}
migrator := &Pre2021Migrator{
config: Pre2021Config{
SQLConfig: oldSQLConfig,
},
}
db, err := gcsql.Open(&oldSQLConfig)
if !assert.NoError(t, err) {
t.FailNow()
}
migrator.db = db
config.SetTestDBConfig("sqlite3", migratedDBHost, migratedDBName, "gochan", "password", "gc_")
sqlConfig := config.GetSQLConfig()
sqlConfig.DBTimeoutSeconds = 600
if !assert.NoError(t, gcsql.ConnectToDB(&sqlConfig)) {
t.FailNow()
}
if !migrateInPlace {
if !assert.NoError(t, gcsql.CheckAndInitializeDatabase("sqlite3", "4")) {
t.FailNow()
}
}
return migrator
}
func TestPre2021MigrationToNewDB(t *testing.T) {
outDir := t.TempDir()
migrator := setupMigrationTest(t, outDir, false)
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
t.FailNow()
}
migrated, err := migrator.MigrateDB()
if !assert.NoError(t, err) {
t.FailNow()
}
assert.False(t, migrated)
validateBoardMigration(t)
validatePostMigration(t)
validateBanMigration(t)
validateStaffMigration(t)
}
func TestPre2021MigrationInPlace(t *testing.T) {
outDir := t.TempDir()
migrator := setupMigrationTest(t, outDir, true)
if !assert.True(t, migrator.IsMigratingInPlace(), "This test should be migrating in place") {
t.FailNow()
}
migrated, err := migrator.MigrateDB()
if !assert.NoError(t, err) {
t.FailNow()
}
assert.False(t, migrated)
validateBoardMigration(t)
validatePostMigration(t)
validateBanMigration(t)
validateStaffMigration(t)
}

View file

@ -1,57 +1,33 @@
package pre2021
const (
boardsQuery = `SELECT
id,
dir,
type,
upload_type,
title,
subtitle,
description,
section,
max_file_size,
max_pages,
default_style,
locked,
anonymous,
forced_anon,
max_age,
autosage_after,
no_images_after,
max_message_length,
embeds_allowed,
redirect_to_thread,
require_file,
enable_catalog
FROM DBPREFIXboards`
sectionsQuery = "SELECT id, list_order, hidden, name, abbreviation FROM DBPREFIXsections"
postsQuery = `SELECT
id,
boardid,
parentid,
name,
tripcode,
email,
subject,
message,
message_raw,
password,
filename,
filename_original,
file_checksum,
filesize,
image_w,
image_h,
thumb_w,
thumb_h,
ip,
tag,
timestamp,
autosage,
deleted_timestamp,
bumped,
stickied,
locked,
reviewed from DBPREFIXposts WHERE deleted_timestamp = NULL`
boardsQuery = `SELECT id, list_order, dir, title, subtitle, description, section, max_file_size, max_pages,
default_style, locked, created_on, anonymous, forced_anon, autosage_after, no_images_after, max_message_length, embeds_allowed,
redirect_to_thread, require_file, enable_catalog
FROM DBPREFIXboards`
postsQuery = `SELECT id, boardid, parentid, name, tripcode, email, subject, message, message_raw, password, filename,
filename_original, file_checksum, filesize, image_w, image_h, thumb_w, thumb_h, ip, timestamp, autosage,
bumped, stickied, locked FROM DBPREFIXposts WHERE deleted_timestamp IS NULL`
threadsQuery = postsQuery + " AND parentid = 0"
staffQuery = `SELECT id, username, rank, boards, added_on, last_active FROM DBPREFIXstaff`
bansQuery = `SELECT id, allow_read, COALESCE(ip, '') as ip, name, name_is_regex, filename, file_checksum, boards, staff,
timestamp, expires, permaban, reason, type, staff_note, appeal_at, can_appeal FROM DBPREFIXbanlist`
announcementsQuery = "SELECT id, subject, message, poster, timestamp FROM DBPREFIXannouncements"
renameTableStatementTemplate = "ALTER TABLE %s RENAME TO _tmp_%s"
)
var (
// tables to be renamed to _tmp_DBPREFIX* to work around SQLite's lack of support for changing/removing columns
renameTables = []string{
"DBPREFIXannouncements", "DBPREFIXappeals", "DBPREFIXbanlist", "DBPREFIXboards", "DBPREFIXembeds", "DBPREFIXlinks",
"DBPREFIXposts", "DBPREFIXreports", "DBPREFIXsections", "DBPREFIXsessions", "DBPREFIXstaff", "DBPREFIXwordfilters",
}
)

View file

@ -0,0 +1,121 @@
package pre2021
import (
"errors"
"strings"
"time"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gcutil"
"github.com/rs/zerolog"
)
type migrationStaff struct {
gcsql.Staff
boards string
oldID int
}
func (m *Pre2021Migrator) getMigrationUser(errEv *zerolog.Event) (*gcsql.Staff, error) {
if m.migrationUser != nil {
return m.migrationUser, nil
}
user := &gcsql.Staff{
Username: "pre2021-migration" + gcutil.RandomString(8),
AddedOn: time.Now(),
}
_, err := gcsql.Exec(nil, "INSERT INTO DBPREFIXstaff(username,password_checksum,global_rank,is_active) values(?,'',0,0)", user.Username)
if err != nil {
errEv.Err(err).Caller().Str("username", user.Username).Msg("Failed to create migration user")
return nil, err
}
if err = gcsql.QueryRow(nil, "SELECT id FROM DBPREFIXstaff WHERE username = ?", []any{user.Username}, []any{&user.ID}); err != nil {
errEv.Err(err).Caller().Str("username", user.Username).Msg("Failed to get migration user ID")
return nil, err
}
m.migrationUser = user
m.staff = append(m.staff, migrationStaff{Staff: *user})
return user, nil
}
func (m *Pre2021Migrator) MigrateStaff() error {
errEv := common.LogError()
defer errEv.Discard()
_, err := m.getMigrationUser(errEv)
if err != nil {
return err
}
rows, err := m.db.Query(nil, staffQuery)
if err != nil {
errEv.Err(err).Caller().Msg("Failed to get ban rows")
return err
}
defer rows.Close()
for rows.Next() {
var staff migrationStaff
if err = rows.Scan(&staff.oldID, &staff.Username, &staff.Rank, &staff.boards, &staff.AddedOn, &staff.LastLogin); err != nil {
errEv.Err(err).Caller().Msg("Failed to scan staff row")
return err
}
m.staff = append(m.staff, staff)
}
for _, staff := range m.staff {
newStaff, err := gcsql.GetStaffByUsername(staff.Username, false)
if err == nil {
// found staff
gcutil.LogInfo().Str("username", staff.Username).Int("rank", staff.Rank).Msg("Found matching staff account")
staff.ID = newStaff.ID
} else if errors.Is(err, gcsql.ErrUnrecognizedUsername) {
// staff doesn't exist, create it (with invalid checksum to be updated by the admin)
if _, err := gcsql.Exec(nil,
"INSERT INTO DBPREFIXstaff(username,password_checksum,global_rank,added_on,last_login,is_active) values(?,'',?,?,?,1)",
staff.Username, staff.Rank, staff.AddedOn, staff.LastLogin,
); err != nil {
errEv.Err(err).Caller().Str("username", staff.Username).Int("rank", staff.Rank).Msg("Failed to migrate staff account")
return err
}
if staff.ID, err = gcsql.GetStaffID(staff.Username); err != nil {
errEv.Err(err).Caller().Str("username", staff.Username).Msg("Failed to get staff account ID")
return err
}
gcutil.LogInfo().Str("username", staff.Username).Int("rank", staff.Rank).Msg("Successfully migrated staff account")
} else {
errEv.Err(err).Caller().Str("username", staff.Username).Msg("Failed to get staff account info")
return err
}
if staff.boards != "" && staff.boards != "*" {
boardsArr := strings.Split(staff.boards, ",")
for _, board := range boardsArr {
board = strings.TrimSpace(board)
boardID, err := gcsql.GetBoardIDFromDir(board)
if err != nil {
errEv.Err(err).Caller().
Str("username", staff.Username).
Str("board", board).
Msg("Failed to get board ID")
return err
}
if _, err = gcsql.Exec(nil, "INSERT INTO DBPREFIXboard_staff(board_id,staff_id) VALUES(?,?)", boardID, staff.ID); err != nil {
errEv.Err(err).Caller().
Str("username", staff.Username).
Str("board", board).
Msg("Failed to apply staff board info")
return err
}
}
}
}
if err = rows.Close(); err != nil {
errEv.Err(err).Caller().Msg("Failed to close staff rows")
return err
}
return nil
}

View file

@ -0,0 +1,33 @@
package pre2021
import (
"testing"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/stretchr/testify/assert"
)
func TestMigrateStaff(t *testing.T) {
outDir := t.TempDir()
migrator := setupMigrationTest(t, outDir, false)
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
t.FailNow()
}
if !assert.NoError(t, migrator.MigrateBoards()) {
t.FailNow()
}
if !assert.NoError(t, migrator.MigrateStaff()) {
t.FailNow()
}
validateStaffMigration(t)
}
func validateStaffMigration(t *testing.T) {
migratedAdmin, err := gcsql.GetStaffByUsername("migratedadmin", true)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, 3, migratedAdmin.Rank)
}

View file

@ -56,9 +56,14 @@ func main() {
fatalEv.Discard()
}()
if !updateDB && (options.ChanType == "" || options.OldChanConfig == "") {
flag.PrintDefaults()
fatalEv.Msg("Missing required oldchan value")
if !updateDB {
if options.ChanType == "" {
flag.PrintDefaults()
fatalEv.Msg("Missing required oldchan value")
} else if options.OldChanConfig == "" {
flag.PrintDefaults()
fatalEv.Msg("Missing required oldconfig value")
}
} else if updateDB {
options.ChanType = "gcupdate"
}
@ -82,9 +87,22 @@ func main() {
default:
fatalEv.Msg("Unsupported chan type, Currently only pre2021 database migration is supported")
}
migratingInPlace := migrator.IsMigratingInPlace()
common.LogInfo().
Str("oldChanType", options.ChanType).
Str("oldChanConfig", options.OldChanConfig).
Bool("migratingInPlace", migratingInPlace).
Msg("Starting database migration")
config.InitConfig(versionStr)
if !updateDB {
sqlCfg := config.GetSQLConfig()
sqlCfg := config.GetSQLConfig()
if migratingInPlace && sqlCfg.DBtype == "sqlite3" && !updateDB {
common.LogWarning().
Str("dbType", sqlCfg.DBtype).
Bool("migrateInPlace", migratingInPlace).
Msg("SQLite has limitations with table column changes")
}
if !migratingInPlace {
err = gcsql.ConnectToDB(&sqlCfg)
if err != nil {
fatalEv.Err(err).Caller().Msg("Failed to connect to the database")
@ -103,7 +121,7 @@ func main() {
fatalEv.Msg("Unable to migrate database")
}
if migrated {
common.LogInfo().Msg("Database is already migrated")
common.LogWarning().Msg("Database is already migrated")
} else {
common.LogInfo().Str("chanType", options.ChanType).Msg("Database migration complete")
}

View file

@ -118,23 +118,26 @@ func getAllPostsToDelete(postIDs []any, fileOnly bool) ([]delPost, []any, error)
params := postIDs
if fileOnly {
// only deleting this post's file, not subfiles if it's an OP
query = "SELECT * FROM DBPREFIXv_posts_to_delete_file_only WHERE postid IN " + setPart
query = "SELECT post_id, thread_id, op_id, is_top_post, filename, dir FROM DBPREFIXv_posts_to_delete_file_only WHERE post_id IN " + setPart
} else {
// deleting everything, including subfiles
params = append(params, postIDs...)
query = "SELECT * FROM DBPREFIXv_posts_to_delete WHERE postid IN " + setPart +
` OR thread_id IN (SELECT thread_id from DBPREFIXposts op WHERE opid IN ` + setPart + ` AND is_top_post)`
query = "SELECT post_id, thread_id, op_id, is_top_post, filename, dir FROM DBPREFIXv_posts_to_delete WHERE post_id IN " +
setPart + " OR thread_id IN (SELECT thread_id from DBPREFIXposts op WHERE op_id IN " + setPart + " AND is_top_post)"
}
rows, err := gcsql.QuerySQL(query, params...)
rows, cancel, err := gcsql.QueryTimeoutSQL(nil, query, params...)
if err != nil {
return nil, nil, err
}
defer func() {
rows.Close()
cancel()
}()
var posts []delPost
var postIDsAny []any
for rows.Next() {
var post delPost
if err = rows.Scan(&post.postID, &post.threadID, &post.opID, &post.isOP, &post.filename, &post.boardDir); err != nil {
rows.Close()
return nil, nil, err
}
posts = append(posts, post)
@ -258,7 +261,7 @@ func validatePostPasswords(posts []any, passwordMD5 string) (bool, error) {
params = append(params, posts[p])
}
err := gcsql.QueryRowSQL(queryPosts, params, []any{&count})
err := gcsql.QueryRow(nil, queryPosts, params, []any{&count})
return count == len(posts), err
}
@ -281,21 +284,24 @@ func markPostsAsDeleted(posts []any, request *http.Request, writer http.Response
defer cancel()
tx, err := gcsql.BeginContextTx(ctx)
opts := &gcsql.RequestOptions{Context: ctx, Tx: tx, Cancel: cancel}
wantsJSON := serverutil.IsRequestingJSON(request)
if err != nil {
errEv.Err(err).Caller().Msg("Unable to start deletion transaction")
serveError(writer, "Unable to delete posts", http.StatusInternalServerError, wantsJSON, errEv.Err(err).Caller())
return false
}
defer tx.Rollback()
const postsError = "Unable to mark post(s) as deleted"
const threadsError = "Unable to mark thread(s) as deleted"
if _, err = gcsql.ExecTxSQL(tx, deletePostsSQL, posts...); err != nil {
const postsError = "Unable to delete post(s)"
const threadsError = "Unable to delete thread(s)"
if _, err = gcsql.Exec(opts, deletePostsSQL, posts...); err != nil {
serveError(writer, postsError, http.StatusInternalServerError, wantsJSON, errEv.Err(err).Caller())
return false
}
if _, err = gcsql.ExecTxSQL(tx, deleteThreadSQL, posts...); err != nil {
if _, err = gcsql.Exec(opts, deleteThreadSQL, posts...); err != nil {
errEv.Err(err).Caller().Msg("Unable to mark thread(s) as deleted")
serveError(writer, threadsError, http.StatusInternalServerError, wantsJSON, errEv.Err(err).Caller())
return false
}
@ -351,7 +357,7 @@ func deletePostFiles(posts []delPost, deleteIDs []any, permDelete bool, request
http.StatusInternalServerError, wantsJSON, errEv.Array("errors", errArr))
return false
}
_, err = gcsql.ExecSQL(deleteFilesSQL, deleteIDs...)
_, err = gcsql.ExecTimeoutSQL(nil, deleteFilesSQL, deleteIDs...)
if err != nil {
serveError(writer, "Unable to delete file entries from database",
http.StatusInternalServerError, wantsJSON, errEv.Err(err).Caller())

View file

@ -6,6 +6,7 @@ import (
"os"
"path"
"strconv"
"strings"
"github.com/gochan-org/gochan/pkg/building"
"github.com/gochan-org/gochan/pkg/config"
@ -29,16 +30,16 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
if editBtn == "Edit post" {
var err error
if len(checkedPosts) == 0 {
server.ServeErrorPage(writer, "You need to select one post to edit.")
server.ServeError(writer, server.NewServerError("You need to select one post to edit", http.StatusBadRequest), wantsJSON, nil)
return
} else if len(checkedPosts) > 1 {
server.ServeErrorPage(writer, "You can only edit one post at a time.")
server.ServeError(writer, server.NewServerError("You can only edit one post at a time", http.StatusBadRequest), wantsJSON, nil)
return
}
rank := manage.GetStaffRank(request)
if password == "" && rank == 0 {
server.ServeErrorPage(writer, "Password required for post editing")
server.ServeError(writer, server.NewServerError("Password required for post editing", http.StatusUnauthorized), wantsJSON, nil)
return
}
passwordMD5 := gcutil.Md5Sum(password)
@ -47,30 +48,36 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
if err != nil {
errEv.Err(err).Caller().
Msg("Error getting post information")
server.ServeError(writer, server.NewServerError("Error getting post information", http.StatusInternalServerError), wantsJSON, nil)
return
}
errEv.Int("postID", post.ID)
if post.Password != passwordMD5 && rank == 0 {
server.ServeErrorPage(writer, "Wrong password")
server.ServeError(writer, server.NewServerError("Wrong password", http.StatusUnauthorized), wantsJSON, nil)
return
}
board, err := post.GetBoard()
if err != nil {
errEv.Err(err).Caller().Msg("Unable to get board ID from post")
server.ServeErrorPage(writer, "Unable to get board ID from post: "+err.Error())
server.ServeError(writer, server.NewServerError("Unable to get board ID from post", http.StatusInternalServerError), wantsJSON, nil)
return
}
if strings.Contains(string(post.Message), `<span class="dice-roll">`) && !config.GetBoardConfig(board.Dir).AllowDiceRerolls {
server.ServeError(writer, server.NewServerError("Unable to edit post, dice rerolls are not allowed on this board", http.StatusForbidden), wantsJSON, nil)
return
}
errEv.Str("board", board.Dir)
upload, err := post.GetUpload()
if err != nil {
errEv.Err(err).Caller().Send()
server.ServeErrorPage(writer, "Error getting post upload info: "+err.Error())
server.ServeError(writer, server.NewServerError("Error getting post upload info: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
data := map[string]interface{}{
data := map[string]any{
"boards": gcsql.AllBoards,
"systemCritical": config.GetSystemCriticalConfig(),
"siteConfig": config.GetSiteConfig(),
@ -87,7 +94,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
if err = serverutil.MinifyTemplate(gctemplates.PostEdit, data, &buf, "text/html"); err != nil {
errEv.Err(err).Caller().
Msg("Error executing edit post template")
server.ServeError(writer, "Error executing edit post template: "+err.Error(), wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Error executing edit post template: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
writer.Write(buf.Bytes())
@ -101,7 +108,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
errEv.Err(err).Caller().
Str("postid", postIDstr).
Msg("Invalid form data")
server.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, server.NewServerError("Invalid form data: "+err.Error(), http.StatusBadRequest), wantsJSON, map[string]any{
"postid": postid,
})
return
@ -110,7 +117,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
post, err := gcsql.GetPostFromID(postid, true)
if err != nil {
errEv.Err(err).Caller().Msg("Unable to find post")
server.ServeError(writer, "Unable to find post", wantsJSON, map[string]interface{}{
server.ServeError(writer, server.NewServerError("Unable to find post", http.StatusBadRequest), wantsJSON, map[string]any{
"postid": postid,
})
return
@ -121,7 +128,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
errEv.Err(err).Caller().
Str("boardID", boardIDstr).
Msg("Invalid form data")
server.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Invalid form data: "+err.Error(), http.StatusBadRequest), wantsJSON, nil)
return
}
gcutil.LogInt("boardID", boardid, infoEv, errEv)
@ -129,13 +136,13 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
rank := manage.GetStaffRank(request)
passwordMD5 := gcutil.Md5Sum(password)
if post.Password != passwordMD5 && rank == 0 {
server.ServeError(writer, "Wrong password", wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Wrong password", http.StatusUnauthorized), wantsJSON, nil)
return
}
board, err := gcsql.GetBoardFromID(boardid)
if err != nil {
server.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, server.NewServerError("Invalid form data: "+err.Error(), http.StatusBadRequest), wantsJSON, map[string]any{
"boardid": boardid,
})
errEv.Err(err).Caller().Msg("Invalid form data")
@ -145,18 +152,18 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
if doEdit == "upload" {
oldUpload, err := post.GetUpload()
if err != nil {
errEv.Err(err).Caller().Send()
server.ServeError(writer, err.Error(), wantsJSON, nil)
errEv.Err(err).Caller().Msg("Unable to get post upload")
server.ServeError(writer, server.NewServerError("Error getting post upload info: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
upload, err := uploads.AttachUploadFromRequest(request, writer, post, board, gcutil.LogInfo(), errEv)
if err != nil {
server.ServeError(writer, err.Error(), wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Unable to attach upload:"+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
if upload == nil {
server.ServeError(writer, "Missing upload replacement", wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Missing upload replacement", http.StatusBadRequest), wantsJSON, nil)
return
}
documentRoot := config.GetSystemCriticalConfig().DocumentRoot
@ -167,7 +174,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
path.Join(documentRoot, board.Dir, "thumb", oldUpload.Filename))
if err = post.UnlinkUploads(false); err != nil {
errEv.Err(err).Caller().Send()
server.ServeError(writer, "Error unlinking old upload from post: "+err.Error(), wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Error unlinking old upload from post: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
if oldUpload.Filename != "deleted" {
@ -184,7 +191,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
Str("newFilename", upload.Filename).
Str("newOriginalFilename", upload.OriginalFilename).
Send()
server.ServeError(writer, "Error attaching new upload: "+err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, server.NewServerError("Error attaching new upload: "+err.Error(), http.StatusInternalServerError), wantsJSON, map[string]any{
"filename": upload.OriginalFilename,
})
filePath = path.Join(documentRoot, board.Dir, "src", upload.Filename)
@ -200,17 +207,15 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
var recovered bool
_, err, recovered = events.TriggerEvent("message-pre-format", post, request)
if recovered {
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Recovered from a panic in an event handler (message-pre-format)", wantsJSON, map[string]interface{}{
"postid": post.ID,
})
server.ServeError(writer, server.NewServerError("Recovered from a panic in an event handler (message-pre-format)",
http.StatusInternalServerError), wantsJSON, map[string]any{"postid": post.ID})
return
}
if err != nil {
errEv.Err(err).Caller().
Str("triggeredEvent", "message-pre-format").
Send()
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, server.NewServerError(err.Error(), http.StatusInternalServerError), wantsJSON, map[string]any{
"postid": post.ID,
})
return
@ -219,8 +224,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
// trigger the pre-format event
_, err, recovered := events.TriggerEvent("message-pre-format", post, request)
if recovered {
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Recovered from a panic in an event handler (message-pre-format)", wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Recovered from a panic in an event handler (message-pre-format)", http.StatusInternalServerError), wantsJSON, nil)
return
}
if err != nil {
@ -259,7 +263,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
); err != nil {
errEv.Err(err).Caller().
Msg("Unable to edit post")
server.ServeError(writer, "Unable to edit post: "+err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, "Unable to edit post: "+err.Error(), wantsJSON, map[string]any{
"postid": post.ID,
})
return
@ -285,9 +289,9 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
}
if err = building.BuildBoards(false, boardid); err != nil {
server.ServeErrorPage(writer, "Error rebuilding boards: "+err.Error())
server.ServeError(writer, "Error rebuilding boards: "+err.Error(), wantsJSON, nil)
} else if err = building.BuildFrontPage(); err != nil {
server.ServeErrorPage(writer, "Error rebuilding front page: "+err.Error())
server.ServeError(writer, "Error rebuilding front page: "+err.Error(), wantsJSON, nil)
} else {
http.Redirect(writer, request, post.WebPath(), http.StatusFound)
infoEv.Msg("Post edited")

View file

@ -78,7 +78,7 @@ func main() {
gcutil.LogFatal().Err(err).Msg("Failed to initialize the database")
}
events.TriggerEvent("db-initialized")
events.RegisterEvent([]string{"db-views-reset"}, func(trigger string, i ...interface{}) error {
events.RegisterEvent([]string{"db-views-reset"}, func(_ string, _ ...any) error {
gcutil.LogInfo().Msg("SQL views reset")
return nil
})

View file

@ -59,7 +59,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
return
}
if !post.IsTopPost {
server.ServeError(writer, "You appear to be trying to move a post that is not the top post in the thread", wantsJSON, map[string]interface{}{
server.ServeError(writer, "You appear to be trying to move a post that is not the top post in the thread", wantsJSON, map[string]any{
"postid": checkedPosts[0],
})
return
@ -69,7 +69,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
if err != nil {
errEv.Err(err).Caller().
Str("srcBoardIDstr", request.PostFormValue("boardid")).Send()
server.ServeError(writer, fmt.Sprintf("Invalid or missing boarid: %q", request.PostFormValue("boardid")), wantsJSON, map[string]interface{}{
server.ServeError(writer, fmt.Sprintf("Invalid or missing boarid: %q", request.PostFormValue("boardid")), wantsJSON, map[string]any{
"boardid": srcBoardID,
})
return
@ -85,7 +85,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
}
gcutil.LogStr("srcBoard", srcBoard.Dir, errEv, infoEv)
buf := bytes.NewBufferString("")
if err = serverutil.MinifyTemplate(gctemplates.MoveThreadPage, map[string]interface{}{
if err = serverutil.MinifyTemplate(gctemplates.MoveThreadPage, map[string]any{
"boardConfig": config.GetBoardConfig(srcBoard.Dir),
"postid": post.ID,
"destBoards": destBoards,
@ -106,7 +106,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
errEv.Err(err).Caller().
Str("postIDstr", postIDstr).Send()
writer.WriteHeader(http.StatusBadRequest)
server.ServeError(writer, fmt.Sprintf("Error parsing postid value: %q: %s", postIDstr, err.Error()), wantsJSON, map[string]interface{}{
server.ServeError(writer, fmt.Sprintf("Error parsing postid value: %q: %s", postIDstr, err.Error()), wantsJSON, map[string]any{
"postid": postIDstr,
})
return
@ -119,7 +119,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
errEv.Err(err).Caller().
Str("srcBoardIDstr", srcBoardIDstr).Send()
writer.WriteHeader(http.StatusBadRequest)
server.ServeError(writer, fmt.Sprintf("Error parsing srcboardid value: %q: %s", srcBoardIDstr, err.Error()), wantsJSON, map[string]interface{}{
server.ServeError(writer, fmt.Sprintf("Error parsing srcboardid value: %q: %s", srcBoardIDstr, err.Error()), wantsJSON, map[string]any{
"srcboardid": srcBoardIDstr,
})
return
@ -129,7 +129,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
errEv.Err(err).Caller().
Int("srcBoardID", srcBoardID).Send()
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, err.Error(), wantsJSON, map[string]any{
"srcboardid": srcBoardID,
})
return
@ -142,7 +142,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
errEv.Err(err).Caller().
Str("destBoardIDstr", destBoardIDstr).Send()
writer.WriteHeader(http.StatusBadRequest)
server.ServeError(writer, fmt.Sprintf("Error parsing destboardid value: %q: %s", destBoardIDstr, err.Error()), wantsJSON, map[string]interface{}{
server.ServeError(writer, fmt.Sprintf("Error parsing destboardid value: %q: %s", destBoardIDstr, err.Error()), wantsJSON, map[string]any{
"destboardid": destBoardIDstr,
})
return
@ -153,7 +153,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
errEv.Err(err).Caller().
Int("destBoardID", destBoardID).Send()
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, err.Error(), wantsJSON, map[string]any{
"destboardid": destBoardID,
})
return
@ -164,7 +164,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
if err != nil {
errEv.Err(err).Caller().Send()
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, err.Error(), wantsJSON, map[string]any{
"postid": postID,
})
return
@ -178,7 +178,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
if err = post.ChangeBoardID(destBoardID); err != nil {
errEv.Err(err).Caller().Msg("Failed changing thread board ID")
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, err.Error(), wantsJSON, map[string]any{
"postID": postID,
"destBoardID": destBoardID,
})
@ -189,7 +189,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
if err != nil {
errEv.Err(err).Caller().Msg("Unable to get upload info")
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Error getting list of files in thread", wantsJSON, map[string]interface{}{
server.ServeError(writer, "Error getting list of files in thread", wantsJSON, map[string]any{
"postid": post.ID,
})
}
@ -245,7 +245,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
if err != nil {
// got at least one error while trying to move files (if there were any)
server.ServeError(writer, "Error while moving post upload: "+err.Error(), wantsJSON,
map[string]interface{}{
map[string]any{
"postID": postID,
"srcBoard": srcBoard.Dir,
"destBoard": destBoard.Dir,
@ -258,7 +258,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
errEv.Err(err).Caller().
Msg("Failed deleting thread page")
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Failed deleting thread page: "+err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, "Failed deleting thread page: "+err.Error(), wantsJSON, map[string]any{
"postID": postID,
"srcBoard": srcBoard.Dir,
})
@ -269,7 +269,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
errEv.Err(err).Caller().
Msg("Failed deleting thread JSON file")
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Failed deleting thread JSON file: "+err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, "Failed deleting thread JSON file: "+err.Error(), wantsJSON, map[string]any{
"postID": postID,
"srcBoard": srcBoard.Dir,
})
@ -278,27 +278,27 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
if err = building.BuildThreadPages(post); err != nil {
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Failed building thread page: "+err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, "Failed building thread page: "+err.Error(), wantsJSON, map[string]any{
"postid": postID,
})
return
}
if err = building.BuildBoardPages(srcBoard, errEv); err != nil {
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Failed building board page: "+err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, "Failed building board page: "+err.Error(), wantsJSON, map[string]any{
"srcBoardID": srcBoardID,
})
return
}
if err = building.BuildBoardPages(destBoard, errEv); err != nil {
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Failed building destination board page: "+err.Error(), wantsJSON, map[string]interface{}{
server.ServeError(writer, "Failed building destination board page: "+err.Error(), wantsJSON, map[string]any{
"destBoardID": destBoardID,
})
return
}
if wantsJSON {
server.ServeJSON(writer, map[string]interface{}{
server.ServeJSON(writer, map[string]any{
"status": "success",
"postID": postID,
"srcBoard": srcBoard.Dir,

View file

@ -20,6 +20,7 @@
"DBprefix": "gc_",
"_DBprefix_info": "The prefix automataically applied to tables when the database is being provisioned and queried",
"CheckRequestReferer": true,
"Lockdown": false,
"LockdownMessage": "This imageboard has temporarily disabled posting. We apologize for the inconvenience",
"Modboard": "staff",
@ -91,12 +92,17 @@
"somemod:blue"
],
"BanMessage": "USER WAS BANNED FOR THIS POST",
"EnableCyclicThreads": true,
"CyclicThreadNumPosts": 500,
"EnableEmbeds": true,
"EnableNoFlag": true,
"EmbedWidth": 200,
"EmbedHeight": 164,
"ImagesOpenNewTab": true,
"NewTabOnOutlinks": true,
"DisableBBcode": false,
"AllowDiceRerolls": false,
"MinifyHTML": true,
"MinifyJS": true,
@ -108,8 +114,8 @@
"SiteKey": "your site key goes here (if you want a captcha, make sure to replace '_Captcha' with 'Captcha'",
"AccountSecret": "your account secret key goes here"
},
"GeoIPType": "mmdb",
"GeoIPOptions": {
"_GeoIPType": "mmdb",
"_GeoIPOptions": {
"dbLocation": "/usr/share/geoip/GeoIP2.mmdb",
"isoCode": "en"
},

View file

@ -0,0 +1,5 @@
local bbcode = require("bbcode")
bbcode.set_tag("rcv", function(node)
return {name="span", attrs={class="rcv"}}
end)

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "gochan.js",
"version": "4.0.1",
"version": "4.1.0",
"description": "",
"main": "./ts/main.ts",
"private": true,
@ -19,28 +19,28 @@
"license": "BSD-2-Clause",
"dependencies": {
"jquery": "^3.7.1",
"jquery-ui": "^1.13.2",
"jquery-ui": "^1.14.1",
"path-browserify": "^1.0.1",
"webstorage-polyfill": "^1.0.1"
},
"devDependencies": {
"@babel/preset-env": "^7.21.5",
"@babel/preset-typescript": "^7.21.5",
"@jest/globals": "^29.5.0",
"@types/jquery": "^3.5.29",
"@types/jqueryui": "^1.12.21",
"@types/path-browserify": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"eslint": "^8.55.0",
"jest": "^29.5.0",
"jest-config": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"sass": "^1.69.5",
"ts-jest": "^29.1.1",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.3",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4"
"@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.26.0",
"@jest/globals": "^29.7.0",
"@types/jquery": "^3.5.32",
"@types/jqueryui": "^1.12.23",
"@types/path-browserify": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^8.24.1",
"eslint": "^9.20.1",
"jest": "^29.7.0",
"jest-config": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"sass": "^1.85.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1"
}
}

View file

@ -18,4 +18,16 @@
-ms-border-radius: $properties;
-webkit-border-radius: $properties;
border-radius: $properties;
}
@mixin upload-box($bg, $a, $a-visited) {
div#upload-box {
background: $bg;
a {
color: $a;
}
a:hover, a:target, a:focus {
color: $a-visited;
}
}
}

View file

@ -1,4 +1,4 @@
@use 'util';
@mixin yotsuba(
$fadepath,
@ -101,4 +101,10 @@
color: $subjectcol;
font-weight: 700;
}
.dice-roll {
border: 1px dashed #222;
}
@include util.upload-box(#aaa, #444, #666);
}

View file

@ -1,12 +1,12 @@
@import 'bunkerchan/colors';
@import 'bunkerchan/front';
@import 'bunkerchan/img';
@import 'util';
@use 'bunkerchan/colors';
@use 'bunkerchan/front';
@use 'bunkerchan/img';
@use 'util';
body {
background: $bgcol;
color: $color;
font-family: $font-family;
background: colors.$bgcol;
color: colors.$color;
font-family: colors.$font-family;
font-size: 70%;
}
@ -19,20 +19,30 @@ hr {
}
a, a:visited {
color: $topborder;
color: colors.$topborder;
text-decoration: none;
}
div#staff, select.post-actions {
background: $topbarbg;
border: 1px solid $topborder;
background: colors.$topbarbg;
border: 1px solid colors.$topborder;
}
div#topbar,
div#topbar a,
div#topbar a:visited {
background: $topbarbg;
border-bottom: 1px solid $topborder;
div#topbar a:visited,
div.dropdown-menu {
background: colors.$topbarbg;
border-bottom: 1px solid colors.$topborder;
}
div.dropdown-menu {
border-left: 1px solid colors.$topborder;
border-right: 1px solid colors.$topborder;
}
div.dropdown-menu a:hover, div.dropdown-menu a:active {
background: colors.$bgcol;
}
table#pages, table#pages * {

View file

@ -2,6 +2,7 @@ $bgcol: #1D1F21;
$color: #ACACAC;
$hcol: #663E11;
$inputbg: #282A2E;
$inputbg2: #16171A;
$topborder: #B0790A;
$linkcol: #FFB300;
$bordercol: #117743;

View file

@ -1,8 +1,8 @@
@import 'colors';
@use 'colors';
div.section-title-block {
background: $topbarbg;
border: 1px solid $topborder;
background: colors.$topbarbg;
border: 1px solid colors.$topborder;
border-radius: 5px 5px 0px 0px;
}

View file

@ -1,11 +1,17 @@
@import 'colors';
@use 'colors';
%formstyle {
background: $inputbg;
border: 1px double $inputborder;
color: $color;
background: colors.$inputbg;
border: 1px double colors.$inputborder;
color: colors.$color;
}
%darkselect {
background: colors.$inputbg2;
border-radius: 5px;
color: colors.$color;
@extend %formstyle;
}
div.reply,
th.postblock,
@ -17,20 +23,25 @@ table.mgmt-table tr:first-of-type th {
border-radius: 5px;
}
table#postbox-static, div#report-delbox {
table#postbox-static, div#report-delbox, form#filterform {
input,
select,
textarea,
input[type="file"]::file-selector-button,
input[type="file"]::-webkit-file-upload-button {
border-radius: 5px;
@extend %formstyle;
}
select {
@extend %darkselect;
}
button, input[type=submit] {
background:#16171A;
background: colors.$inputbg2;
}
}
select#changepage {
@extend %formstyle;
button.hideblock-button,
select#changepage,
select.post-actions,
select#boardsearch,
select[name="show"] {
@extend %darkselect;
}

View file

@ -1,17 +1,17 @@
@import 'util';
@import 'burichan/colors';
@import 'burichan/img';
@import 'burichan/front';
@import 'burichan/manage';
@use 'util';
@use 'burichan/colors';
@use 'burichan/img';
@use 'burichan/front';
@use 'burichan/manage';
body {
font: $font;
background: $bgcol;
font: colors.$font;
background: colors.$bgcol;
margin: 8px;
}
a, a:visited {
color: $linkcol;
color: colors.$linkcol;
text-decoration: none;
}
@ -32,13 +32,11 @@ h2 a {
h3 {
margin: 0px;
text-align: center;
font-size: medium;
}
h1,h2 {
// background:#D6DAF0;
font-family: $hfont-family;
h1, h2 {
font-family: colors.$hfont-family;
}
h1, h2, h3 {
@ -47,21 +45,32 @@ h1, h2, h3 {
}
div#topbar {
// @include box-shadow(3px 3px 5px 6px $shadowcol);
@include shadow-filter(3px 5px 6px $shadowcol);
height: 30px;
@include util.shadow-filter(3px 5px 6px colors.$shadowcol);
min-height: 1.5em;
}
div#topbar,
.topbar-item,
.topbar-item:visited,
a.topbar-item,
a.topbar-item:visited,
.dropdown-button,
.dropdown-menu/* ,
.dropdown-menu a */{
background: #000A89;
color: $bgcol;
div.dropdown-menu {
background: #080e5e;
color: colors.$bgcol;
}
div#footer {
font-size: 8pt;
div.dropdown-menu {
@include util.shadow-filter(3px 5px 6px colors.$shadowcol);
a, h3 {
color: colors.$bgcol;
}
}
a.topbar-item:hover,
a.topbar-item:active,
div.dropdown-menu a:hover {
background: #0a127b;
}
footer {
font-size: 0.75em;
}

View file

@ -1,26 +1,27 @@
@import 'colors';
@import '../global/colors';
@use 'colors';
@use '../global/colors' as global-colors;
@use "../util";
h1#board-title {
font-family: serif;
font-size: 24pt;
color: $headercol;
font-size: 2em;
color: global-colors.$headercol;
}
.postblock {
background:$postblock;
background:colors.$postblock;
}
div.file-info {
font-size:12px;
font-family:sans-serif;
font-size: 1em;
font-family: sans-serif;
}
span.postername {
font-size:12px;
font-family:serif;
color: $namecol;
font-weight:800;
font-size: 1em;
font-family: serif;
color: global-colors.$namecol;
font-weight: 800;
}
div.reply,
@ -31,7 +32,14 @@ div.inlinepostprev {
}
select.post-actions,
div.reply,
div.postprev,
div.inlinepostprev {
border: 1px solid $bordercol;
}
border: 1px solid colors.$bordercol;
}
.dice-roll {
border: 1px dashed #222;
}
@include util.upload-box(#aaa, #444, #666);

View file

@ -1,17 +1,17 @@
@import 'colors';
@import '../util';
@use 'colors';
@use '../util';
.loginbox input {
height: 20%;
}
.manage-header {
background: $bgcol;
background: colors.$bgcol;
border-radius: 8px;
}
table.mgmt-table {
tr:first-of-type th {
background: $postblock;
background: colors.$postblock;
}
}

View file

@ -1,51 +1,51 @@
@import 'clear/colors';
@import './util';
@use 'clear/colors';
@use 'util';
body {
background: $bgcol;
background: colors.$bgcol;
font-family: monospace, sans-serif;
font-size: 80%;
}
div#topbar {
background: $topbarbg;
border-bottom: 1px solid $topbarborder;
background: colors.$topbarbg;
border-bottom: 1px solid colors.$topbarborder;
}
a {
color: $linkcol;
color: colors.$linkcol;
text-decoration: none;
&:hover {
text-shadow: 0px 0px 5px $hrcol;
text-shadow: 0px 0px 5px colors.$hrcol;
}
}
header {
color: $headercol;
color: colors.$headercol;
}
hr {
border: none;
border-top: 1px solid $hrcol;
border-top: 1px solid colors.$hrcol;
height: 0;
}
div#content {
input, select, textarea {
border: 1px double $inputshadow;
border: 1px double colors.$inputshadow;
border-radius: 5px;
background: $inputcol;
background: colors.$inputcol;
color: #000;
}
input:active, select:active, textarea:active {
@include shadow-filter(0px 0px 5px $replyborder);
@include util.shadow-filter(0px 0px 5px colors.$replyborder);
}
input[type="button"],
input[type="submit"],
input[type="file"]::file-selector-button,
input[type="file"]::webkit-file-upload-button {
background: #A7A7A7;
border: 3px double $inputshadow;
border: 3px double colors.$inputshadow;
border-radius: 5px;
color: #000;
}
@ -58,14 +58,14 @@ th.postblock,
div.postprev,
div.inlinepostprev,
table.mgmt-table tr:first-of-type th {
background: $inputcol;
border: 1px solid $replyborder;
background: colors.$inputcol;
border: 1px solid colors.$replyborder;
border-radius: 5px;
}
span.subject {
font-weight: bold;
color: $subjectcol;
font-weight: 800;
color: colors.$subjectcol;
}
table#pages, table#pages * {
@ -74,5 +74,7 @@ table#pages, table#pages * {
div.section-title-block {
background: #A7A7A7;
border-bottom: 1px solid $replyborder;
}
border-bottom: 1px solid colors.$replyborder;
}
@include util.upload-box(#aaa, #444, #666);

View file

@ -1,60 +1,55 @@
@import 'dark/colors';
@use 'dark/colors';
body {
background: $bgcol;
font-family: $font-family;
color: $color;
font-size: 80%;
background: colors.$bgcol;
font-family: colors.$font-family;
color: colors.$color;
// font-size: 80%;
}
a {
text-decoration: none;
color: $linkcol;
color: colors.$linkcol;
}
div#topbar {
background: $topbarbg;
border-bottom: 1px solid $color;
background: colors.$topbarbg;
border-bottom: 1px solid colors.$color;
a {
text-shadow: black 1px 1px 1px, black -1px -1px 1px, black -1px 1px 1px, black 1px -1px 1px;
}
}
header {
color: $headercol;
color: colors.$headercol;
}
div#content {
// input:not([type=submit]):not([type=button]),
input:not(div#qrbuttons input),
textarea, select {
background: $inputbg;
border: 1px solid $topbarbg;
color: $linkcol;
background: colors.$inputbg;
border: 1px solid colors.$topbarbg;
color: colors.$linkcol;
}
}
th.postblock, table.mgmt-table tr:first-of-type th {
background: $blockcol;
border: 1px solid $blockborder;
background: colors.$blockcol;
border: 1px solid colors.$blockborder;
border-radius: 5px;
text-align: left;
}
// select.post-actions {
// color: $color;
// }
div.reply,
div.postprev,
div.inlinepostprev {
background: $inputbg;
border: 1px solid $replyborder;
background: colors.$inputbg;
border: 1px solid colors.$replyborder;
border-radius: 5px;
}
span.postername {
color: $headercol;
color: colors.$headercol;
font-weight: bold;
a {
text-decoration: underline;
@ -62,11 +57,18 @@ span.postername {
}
span.tripcode {
color: $headercol;
color: colors.$headercol;
}
.dropdown-menu {
background: colors.$topbarbg!important;
b {
color: black;
}
}
span.subject {
color: $subjectcol;
color: colors.$subjectcol;
}
table#pages, table#pages * {

View file

@ -1,59 +1,59 @@
@import './darkbunker/img';
@import './darkbunker/vars';
@import './util';
@use 'darkbunker/img';
@use 'darkbunker/vars';
@use 'util';
body {
background: $bgcol;
color: $txtcol;
background: vars.$bgcol;
color: vars.$txtcol;
font-family: arial,helvetica,sans-serif;
font-size: 10pt;
}
div#topbar {
background: $topbarcol;
border-bottom: 2px solid $linkcol;
background: vars.$topbarcol;
border-bottom: 2px solid vars.$linkcol;
* {
padding: 0px;
}
}
hr {
color: $bordercol;
color: vars.$bordercol;
}
a {
color: $linkcol;
color: vars.$linkcol;
text-decoration: none;
}
a:hover {
background: inherit;
color: $linklight;
color: vars.$linklight;
}
header {
h1, div#board-subtitle {
font-family: tahoma;
color: $headercol;
color: vars.$headercol;
}
}
th, div.reply {
background: $gridcol;
border: 1px solid $bordercol;
background: vars.$gridcol;
border: 1px solid vars.$bordercol;
}
div.section-block {
border: 1px solid white;
div.section-title-block {
background: $blocktitle;
background: vars.$blocktitle;
border: 1px solid white;
font-weight: bold;
}
}
div#frontpage div.section-block:first-child {
background: $gridcol;
background: vars.$gridcol;
// border: none;
}

View file

@ -1,23 +1,23 @@
@import './vars';
@use 'vars';
div#content, div#qr-box {
color: $txtcol;
border: 1px double $inputshadow;
color: vars.$txtcol;
border: 1px double vars.$inputshadow;
border-radius: 5px;
textarea,
input:not([type="file"]):not([type="checkbox"]),
[type="submit"],
select {
color: $txtcol;
background: $gridcol;
border: 1px solid $inputshadow;
color: vars.$txtcol;
background: vars.$gridcol;
border: 1px solid vars.$inputshadow;
border-radius: 5px;
}
}
div#qr-box {
background: $gridcol;
background: vars.$gridcol;
}
form#postform, form#qrpostform {
@ -29,5 +29,5 @@ form#postform, form#qrpostform {
div#staffmenu, div#watchermenu {
background: black;
border: 1px solid $txtcol;
border: 1px solid vars.$txtcol;
}

View file

@ -1,25 +1,32 @@
@import 'global/img';
@import 'global/front';
@import 'global/manage';
@import 'global/lightbox';
@import 'global/qr';
@import "global/watcher";
@import 'global/bans';
@use 'global/img';
@use 'global/front';
@use 'global/manage';
@use 'global/lightbox';
@use 'global/qr';
@use "global/watcher";
@use 'global/bans';
@use 'global/animations';
.increase-line-height {
header, .post, .reply {
line-height: 1.5;
}
}
header {
margin-top:50px;
text-align:center;
margin-top: 50px;
text-align: center;
h1 {
margin: 0px;
}
}
div#topbar {
left:0;
position:fixed;
top:0;
width:100%;
margin-top:0px;
left: 0;
position: fixed;
top: 0;
width: 100%;
margin-top: 0px;
* {
padding: 4px;
@ -63,15 +70,15 @@ div#staffmenu {
div.section-block {
margin-bottom: 8px;
div.section-title-block {
display:block;
padding:4px 8px 4px 8px;
display: block;
padding: 4px 8px 4px 8px;
}
div.section-body {
overflow-y: hidden;
margin-right: 0px;
margin-bottom:8px;
min-height: 48px;
padding:8px;
padding: 8px;
}
}
@ -80,13 +87,13 @@ div.section-block {
text-align: center;
}
#footer {
bottom:0px;
clear:both;
left:0px;
position:static;
text-align:center;
width:100%;
footer {
bottom: 0px;
clear: both;
left: 0px;
position: static;
text-align: center;
width: 100%;
}
select.post-actions {

View file

@ -0,0 +1,21 @@
@keyframes slideopen {
from {
transform: scale(1, 0);
transform-origin: top center;
}
to {
transform: scale(1, 1);
transform-origin: top center;
}
}
@keyframes slideclose {
from {
transform: scale(1, 1);
transform-origin: top center;
}
to {
transform: scale(1, 0);
transform-origin: top center;
}
}

View file

@ -1,3 +1,5 @@
@use 'animations';
#boardmenu-bottom {
margin-top: 16px;
}
@ -218,4 +220,30 @@ div.inlinepostprev {
overflow: hidden;
margin:8px;
padding: 4px 8px;
}
.hideblock {
border: 1px solid black;
background: rgba(0, 0, 0, 0.1);
}
.hideblock.open {
animation: slideopen 0.25s;
}
.hideblock.close {
animation: slideclose 0.25s;
}
.hideblock.hidden {
display: none;
}
.hideblock-button {
display: block;
}
.dice-roll {
border: 1px dashed #aaa;
font-weight: bold;
}

View file

@ -1,4 +1,4 @@
@import '../util';
@use '../util';
.lightbox {
background:#CDCDCD;
@ -12,12 +12,10 @@
top:5%;
z-index:9001;
overflow: auto;
// display:hidden;
}
.lightbox * {
// box-shadow: 0px 0px 0px 0px #000000;
@include shadow-filter(0px 0px 0px #000);
@include util.shadow-filter(0px 0px 0px #000);
color:#000000;
}
@ -45,19 +43,17 @@
}
.lightbox-title {
font-size:42px;
font-weight:700;
text-align:center;
margin-top: 0px;
}
.lightbox-x {
color: #000!important;
float: right;
font-size: 32px;
font-weight: 700;
font-size: inherit;
}
.lightbox-x:hover {
.lightbox-x:hover,.lightbox-x:active {
color:#555;
}
@ -70,14 +66,14 @@
.lightbox input[type=password],
.lightbox input[type=file],
.lightbox textarea {
background:#FFF;
border:1px solid #000;
color:#000;
background: #FFF;
border: 1px solid #000;
color: #000;
}
.lightbox textarea#sql-statement {
width:95%;
height:300px;
width: 95%;
height: 300px;
margin-left: 0px;
clear: both;
background: #FFF;
@ -92,8 +88,7 @@
border: 1px solid #000;
border-radius: 0px;
background: #777;
// box-shadow: 0px 0px 0px 0px #000000;
@include shadow-filter(0px 0px 0px #000);
@include util.shadow-filter(0px 0px 0px #000);
}
#settings-container table textarea {

View file

@ -1,19 +1,19 @@
@import '../util';
@use '../util';
div#qr-box {
padding:1px;
@include box-sizing(border-box);
@include util.box-sizing(border-box);
min-width: 300px;
background:lightgray;
border: 1px solid #000;
input[type=text],textarea {
display: table-cell;
min-width:300px;
min-width:320px;
background: #FFF;
color: #000;
width:100%;
@include box-sizing(border-box);
@include util.box-sizing(border-box);
}
input[type=file] {
background: lightgray;

View file

@ -1,10 +1,10 @@
@import 'photon/colors';
@import 'photon/img';
@import 'util';
@use 'photon/colors';
@use 'photon/img';
@use 'util';
body {
background: $bgcol;
font: 15px $font;
background: colors.$bgcol;
font: 15px colors.$font;
}
a {
@ -18,23 +18,25 @@ div#topbar, div.dropdown-menu {
}
div#topbar {
@include shadow-filter(0px 2px 2px $shadowcol);
@include util.shadow-filter(0px 2px 2px colors.$shadowcol);
}
.dropdown-menu {
color: #FFF;
background: #000!important;
@include shadow-filter(2px 2px 3px $shadowcol);
@include util.shadow-filter(2px 2px 3px colors.$shadowcol);
z-index: 0;
div:hover {
background: $dropdowncol;
background: colors.$dropdowncol;
}
}
header, div#top-pane, a {
color: $linkcol;
color: colors.$linkcol;
}
#site-title, #board-title {
font-weight: 800;
}
}
@include util.upload-box(#aaa, #444, #666);

View file

@ -1,25 +1,25 @@
@import '../util';
@import 'colors';
@use '../util';
@use 'colors';
div.reply,
div.postprev,
div.inlinepostprev,
th.postblock {
background: $replycol;
border: 1px solid $replyborder;
@include border-radius(5px);
background: colors.$replycol;
border: 1px solid colors.$replyborder;
@include util.border-radius(5px);
}
span.subject {
color: $subjectcol;
color: colors.$subjectcol;
}
div.section-title-block {
background: $replyborder;
background: colors.$replyborder;
}
div.section-block {
@include border-radius(5px);
background: $sectionbg;
border: 1px solid $sectionborder;
@include util.border-radius(5px);
background: colors.$sectionbg;
border: 1px solid colors.$sectionborder;
}

View file

@ -1,73 +1,73 @@
@import 'pipes/colors';
@import 'pipes/front';
@import 'pipes/manage';
@import 'pipes/img';
@import 'util';
@use 'pipes/colors';
@use 'pipes/front';
@use 'pipes/manage';
@use 'pipes/img';
@use 'util';
* {
outline: none;
}
body {
background: $bgcol;
background: colors.$bgcol;
background-image: url(res/pipes_bg.png);
color: #d8d0b9;
font: $font;
font: colors.$font;
}
header h1, #site-title {
color: $headercol;
color: colors.$headercol;
}
a {
color: $linkcol;
font: $font;
color: colors.$linkcol;
font: colors.$font;
text-decoration: none;
}
a:hover {
background: inherit;
color: $linklight;
color: colors.$linklight;
}
a.topbar-item:hover {
background: $bglight;
background: colors.$bglight;
}
div#footer, div#footer * {
footer, footer * {
font-size: 9pt;
}
div#topbar {
background: $topbarcol;
background: colors.$topbarcol;
// @include box-shadow(0px 2px 2px 3px $shadowcol);
@include shadow-filter(0px 4px 2px $shadowcol);
@include util.shadow-filter(0px 4px 2px colors.$shadowcol);
li:hover {
background: $bglight;
background: colors.$bglight;
}
}
.dropdown-menu {
background: $topbarcol!important;
@include shadow-filter(2px 2px 3px $shadowcol);
background: colors.$topbarcol!important;
@include util.shadow-filter(2px 2px 3px colors.$shadowcol);
z-index: 0;
div:hover {
background: $bglight;
background: colors.$bglight;
}
}
.ui-tabs-tab {
background: $topbarcol;
border: 1px solid $bglight;
background: colors.$topbarcol;
border: 1px solid colors.$bglight;
}
.ui-tabs-active {
background: $bgcol;
background: colors.$bgcol;
}
.ui-tabs-panel {
background: $bgcol;
border: 1px solid $topbarcol;
background: colors.$bgcol;
border: 1px solid colors.$topbarcol;
padding: 8px;
}

View file

@ -1,8 +1,8 @@
@import 'colors';
@import '../util';
@use 'colors';
@use '../util';
.dropdown-button:hover {
background: $bglight;
background: colors.$bglight;
}
.section-body {
@ -10,27 +10,27 @@
}
.section-title-block {
background: $topbarcol;
background: colors.$topbarcol;
border-radius: 4px 4px 0px 0px;
}
.tab {
background: $bglight;
border: 1px solid $bgcol;
background: colors.$bglight;
border: 1px solid colors.$bgcol;
}
#current-tab {
background: $bgcol;
background: colors.$bgcol;
}
div#recent-posts-header {
// @include box-shadow(0px 2px 2px 3px $shadowcol);
@include shadow-filter(0px 2px 3px $shadowcol);
@include util.shadow-filter(0px 2px 3px colors.$shadowcol);
margin-bottom: 8px;
padding: 4px 8px 4px 8px;
}
.postblock {
background: $bgcol;
background: colors.$bgcol;
font-weight: 700;
}

View file

@ -1,8 +1,8 @@
@import '_colors';
@import '../util';
@use '_colors';
@use '../util';
.dropdown-button:hover {
background: $bglight;
background: colors.$bglight;
}
img.thumbnail {
@ -12,13 +12,13 @@ img.thumbnail {
}
.reply, .inlinepostprev, .postprev {
background: $postblock;
border: 1px solid $postblockcol;
background: colors.$postblock;
border: 1px solid colors.$postblockcol;
}
.postblock,
table.mgmt-table tr:first-of-type th {
background: $postblock;
background: colors.$postblock;
font-weight: 700;
}
@ -45,8 +45,8 @@ div#content {
textarea,
select#changepage,
select.post-actions {
background: $postblock;
border: 1px solid $postblockcol;
background: colors.$postblock;
border: 1px solid colors.$postblockcol;
color: #FFF;
}
}

View file

@ -1,13 +1,13 @@
@import 'colors';
@import '../util';
@use 'colors';
@use '../util';
.loginbox input {
height: 20%;
}
.manage-header {
background: $topbarcol;
background: colors.$topbarcol;
// @include box-shadow(2px 2px 3px 4px $shadowcol);
@include shadow-filter(4px 4px 4px $shadowcol);
@include util.shadow-filter(4px 4px 4px colors.$shadowcol);
border-radius: 8px;
}

View file

@ -1,27 +1,27 @@
@import 'win9x/vars';
@use 'win9x/vars';
@font-face {
@include mssans-font(400, normal);
@include vars.mssans-font(400, normal);
}
@font-face {
@include mssans-font(700, normal);
@include vars.mssans-font(700, normal);
}
body {
font-size: $fontsize;
font-size: vars.$fontsize;
color: white;
background: $bgcol;
background: vars.$bgcol;
}
a {
color: $txtcol;
color: vars.$txtcol;
text-decoration: none;
}
input {
color: $txtcol;
font-size: $fontsize!important;
color: vars.$txtcol;
font-size: vars.$fontsize!important;
font-family: "Pixelated MS Sans Serif", Arial;
outline: none;
}
@ -30,7 +30,7 @@ input {
div#topbar {
height: 26px;
background: silver!important;
box-shadow: $bar-shadow;
box-shadow: vars.$bar-shadow;
box-sizing: border-box;
border: none!important;
border-radius: 0!important;
@ -39,9 +39,9 @@ div#topbar {
}
a[href="/"] {
font-family: "Pixelated MS Sans Serif", Arial;
font-size: $fontsize;
font-size: vars.$fontsize;
content: "Start"!important;
background: $start_path!important;
background: vars.$start_path!important;
background-repeat: no-repeat!important;
background-position-x: 3px!important;
background-position-y: 4px!important;
@ -54,12 +54,12 @@ div#topbar {
div#topbar,
div#topbar a {
background: silver!important;
box-shadow: $bar-shadow;
box-shadow: vars.$bar-shadow;
box-sizing: border-box;
border: none!important;
border-radius: 0!important;
height:24px;
color: $txtcol!important;
color: vars.$txtcol!important;
font-family: "Pixelated MS Sans Serif", Arial;
font-size: 11px;
cursor: default;
@ -70,10 +70,10 @@ div#content {
input[type=button],
input[role=pushbutton],
input[type=submit] {
color: $txtcol!important;
color: vars.$txtcol!important;
/* font-size: 12px!important; */
background: silver!important;
box-shadow: $bar-shadow;
box-shadow: vars.$bar-shadow;
box-sizing: border-box;
border: none!important;
border-radius: 0!important;
@ -95,7 +95,7 @@ div#qr-title {
background: linear-gradient(90deg,navy,#1084d0);
a {
font-family: Arial, Helvetica, sans-serif;
box-shadow: $bar-shadow;
box-shadow: vars.$bar-shadow;
min-width: 16px;
min-height: 14px;
display: block;
@ -105,7 +105,7 @@ div#qr-title {
background-position: top 3px left 40px!important;
}
a:hover {
color:$txtcol;
color:vars.$txtcol;
}
span#qr-buttons * {
display: inline-block;
@ -125,7 +125,7 @@ div#qr-box {
box-shadow: inset -1px -1px #fff,inset 1px 1px grey,inset -2px -2px #dfdfdf,inset 2px 2px #0a0a0a;
margin: 0;
font-family: "Pixelated MS Sans Serif",Arial;
color: $txtcol;
color: vars.$txtcol;
appearance: none;
}
}
@ -140,7 +140,7 @@ input[type=text] {
} */
a.boardlistactive {
color: $txtcol!important;
color: vars.$txtcol!important;
padding: 6px 3px!important;
outline: 1px dotted #000;
outline-offset: -4px;
@ -148,7 +148,7 @@ a.boardlistactive {
h1, h2, div.subtitle, a#qrDisplayButton {
color: $txtcol;
color: vars.$txtcol;
font-family: Arial, sans-serif;
}
@ -172,7 +172,7 @@ div#content select {
}
div.pages {
background: $bgcol;
background: vars.$bgcol;
border: none;
color: white;
/* border-right: black;

View file

@ -1,23 +1,23 @@
@import 'global/colors';
@import 'yotsuba/colors';
@import 'yotsubacommon';
@use 'global/colors' as global-colors;
@use 'yotsuba/colors' as yotsuba-colors;
@use 'yotsubacommon';
@include yotsuba(
@include yotsubacommon.yotsuba(
'res/yotsuba_bg.png',
$bgcol, /* $bodybg */
yotsuba-colors.$bgcol, /* $bodybg */
maroon, /* $bodycol */
$topbarbg,
$topbarborder,
$headercol,
$postblockbg,
$postblockborder,
$postblockborder, /* $sectiontitlecol */
yotsuba-colors.$topbarbg,
yotsuba-colors.$topbarborder,
global-colors.$headercol,
yotsuba-colors.$postblockbg,
yotsuba-colors.$postblockborder,
yotsuba-colors.$postblockborder, /* $sectiontitlecol */
#fca, /* $sectiontitlebg */
$borderbotright,
yotsuba-colors.$borderbotright,
maroon, /* $linkcol */
#D9BfB7, /* $hrcol */
#CC1105, /* $subjectcol */
$namecol,
$replybg,
global-colors.$namecol,
yotsuba-colors.$replybg,
navy /* $postlinkcol */
);

View file

@ -1,23 +1,23 @@
@import 'global/colors';
@import 'yotsubab/colors';
@import 'yotsubacommon';
@use 'global/colors' as global-colors;
@use 'yotsubab/colors' as yotsubab-colors;
@use 'yotsubacommon';
@include yotsuba(
@include yotsubacommon.yotsuba(
'res/yotsubab_bg.png',
$bgcol, /* $bodybg */
yotsubab-colors.$bgcol, /* $bodybg */
#000, /* $bodycol */
#D6DAF0, /* $topbarbg */
$topbarborder,
$headercol,
$postblockbg,
$postblockborder,
yotsubab-colors.$topbarborder,
global-colors.$headercol,
yotsubab-colors.$postblockbg,
yotsubab-colors.$postblockborder,
#000, /* $sectiontitlecol */
$postblockbg, /* $sectiontitlebg */
$borderbotright,
yotsubab-colors.$postblockbg, /* $sectiontitlebg */
yotsubab-colors.$borderbotright,
#34345C, /* $linkcol */
#B7C5D9, /* $hrcol */
#0F0C5D, /* $subjectcol */
$namecol,
$replybg,
global-colors.$namecol,
yotsubab-colors.$replybg,
navy /* $postlinkcol */
);

View file

@ -9,43 +9,42 @@ import { applyBBCode, handleKeydown } from "../ts/boardevents";
document.documentElement.innerHTML = simpleHTML;
function doBBCode(keycode: number, text: string, start: number, end: number) {
function doBBCode(key:string, text: string, start: number, end: number) {
const $ta = $<HTMLTextAreaElement>("<textarea/>");
$ta.text(text);
const e = $.Event("keydown");
e.ctrlKey = true;
$ta[0].selectionStart = start;
$ta[0].selectionEnd = end;
e.keyCode = keycode;
e.which = keycode;
e.key = key;
$ta.first().trigger(e);
applyBBCode(e as JQuery.KeyDownEvent);
return $ta.text();
}
test("Tests BBCode events", () => {
let text = doBBCode(66, "bold", 0, 4);
let text = doBBCode("b", "bold", 0, 4);
expect(text).toEqual("[b]bold[/b]");
text += "italics";
text = doBBCode(73, text, text.length - 7, text.length);
text = doBBCode("i", text, text.length - 7, text.length);
expect(text).toEqual("[b]bold[/b][i]italics[/i]");
text = doBBCode(82, "strike" + text, 0, 6);
text = doBBCode("r", "strike" + text, 0, 6);
expect(text).toEqual("[s]strike[/s][b]bold[/b][i]italics[/i]");
text = doBBCode(83, text, 0, 13);
text = doBBCode("s", text, 0, 13);
expect(text).toEqual("[?][s]strike[/s][/?][b]bold[/b][i]italics[/i]");
text = doBBCode(85, text, text.length, text.length);
text = doBBCode("u", text, text.length, text.length);
expect(text).toEqual("[?][s]strike[/s][/?][b]bold[/b][i]italics[/i][u][/u]");
const invalidKeyCode = doBBCode(0, text, 0, 1); // passes an invalid keycode to applyBBCode, no change
const invalidKeyCode = doBBCode("x", text, 0, 1); // passes an invalid keycode to applyBBCode, no change
expect(invalidKeyCode).toEqual(text);
});
test("Tests proper form submission via JS", () => {
const $form = $("form#postform");
const text = doBBCode(83, "text", 0, 4);
const text = doBBCode("s", "text", 0, 4);
$form.find("textarea#postmsg").text(text);
let submitted = false;
$form.on("submit", () => {
@ -54,7 +53,7 @@ test("Tests proper form submission via JS", () => {
});
const e = $.Event("keydown");
e.ctrlKey = true;
e.keyCode = 10;
e.key = "Enter";
$form.find("textarea#postmsg").first().trigger(e);
handleKeydown(e as JQuery.KeyDownEvent);
expect(submitted).toBeTruthy();

View file

@ -9,7 +9,7 @@ export function removeLightbox(...customs: any) {
export function showLightBox(title: string, innerHTML: string) {
$(document.body).prepend(
`<div class="lightbox-bg"></div><div class="lightbox"><div class="lightbox-title">${title}<a href="javascript:;" class="lightbox-x">X</a><hr /></div>${innerHTML}</div>`
`<div class="lightbox-bg"></div><div class="lightbox"><h1 class="lightbox-title">${title}<a href="javascript:;" class="lightbox-x">X</a><hr /></h1>${innerHTML}</div>`
);
$("a.lightbox-x, .lightbox-bg").on("click", removeLightbox);
}

View file

@ -97,10 +97,10 @@ function deletePost(id: number, board: string, fileOnly = false) {
alertLightbox(`Delete failed: ${data.error}`, "Error");
}).done(data => {
if(data.error === undefined || data === "") {
if(location.href.indexOf(`/${board}/res/${id}.html`) > -1) {
alertLightbox("Thread deleted", "Success");
} else if(fileOnly) {
if(fileOnly) {
deletePostFile(id);
} else if(location.href.indexOf(`/${board}/res/${id}.html`) > -1) {
alertLightbox("Thread deleted", "Success");
} else {
deletePostElement(id);
}

View file

@ -2,6 +2,7 @@ import $ from "jquery";
import { extname } from "path";
import { formatDateString, formatFileSize } from "../formatting";
import { getThumbFilename } from "../postinfo";
import { getBooleanStorageVal } from "../storage";
/**
* creates an element from the given post data
@ -117,7 +118,35 @@ export function shrinkOriginalFilenames(elem = $(document.body)) {
});
}
export function prepareHideBlocks() {
$("div.hideblock").each((_i,el) => {
const $el = $(el);
const $button = $("<button />").prop({
class: "hideblock-button",
}).text($el.hasClass("open") ? "Hide" : "Show").on("click", e => {
e.preventDefault();
const hidden = $el.hasClass("hidden");
$button.text(hidden ? "Hide" : "Show");
if(el.onanimationend === undefined || !getBooleanStorageVal("smoothhidetoggle", true)) {
$el.toggleClass("hidden");
} else {
$el.removeClass("close");
if(hidden) {
$el.removeClass("hidden").addClass("open");
} else {
$el.addClass("close").removeClass("open");
}
}
}).insertBefore($el);
$el.on("animationend", () => {
if($el.hasClass("close")) {
$el.addClass("hidden").removeClass("close");
}
});
});
}
$(() => {
prepareHideBlocks();
shrinkOriginalFilenames();
});

View file

@ -1,11 +1,9 @@
import $ from "jquery";
import "jquery-ui/ui/version";
import "jquery-ui/ui/plugin";
import "jquery-ui/ui/safe-active-element";
import "jquery-ui/ui/widget";
import "jquery-ui/ui/scroll-parent";
import "jquery-ui/ui/widgets/mouse";
import "jquery-ui/ui/safe-blur";
import "jquery-ui/ui/widgets/draggable";
import { upArrow, downArrow } from "../vars";
@ -93,6 +91,22 @@ function setButtonTimeout(prefix = "", cooldown = 5) {
timeoutCB();
}
function fixFileList() {
let files:FileList = null;
const $browseBtns = $<HTMLInputElement>("input[name=imagefile]");
$browseBtns.each((_i, el) => {
if(el.files?.length > 0 && !files) {
files = el.files;
return false;
}
return null;
});
$browseBtns.each((_i, el) => {
if(files)
el.files = files;
});
}
export function initQR() {
if($qr !== null) {
// QR box already initialized
@ -204,6 +218,7 @@ export function initQR() {
resetSubmitButtonText();
$postform.on("submit", function(e) {
fixFileList();
const $form = $<HTMLFormElement>(this);
e.preventDefault();
copyCaptchaResponse($form);

View file

@ -54,7 +54,7 @@ function onReaderLoad(name:string, e:ProgressEvent<FileReader>) {
"href": "#"
}).text("X").on("click", (e:JQuery.ClickEvent) => {
const $target = $(e.target);
const $browseBtn = $target.parents<HTMLInputElement>("#upload-box").siblings("input[name=imagefile]");
const $browseBtn = $target.parents<HTMLInputElement>("#upload-box").siblings<HTMLInputElement>("input[name=imagefile]");
$browseBtn.each((_, el) => {
el.value = null;
});
@ -86,13 +86,19 @@ function replaceBrowseButton() {
$("<a/>").addClass("browse-text")
.attr("href", "#")
.text("Select/drop/paste upload here")
.on("click", () => $browseBtn.trigger("click"))
.on("click", e => {
e.preventDefault();
$browseBtn.trigger("click");
})
).on("dragenter dragover drop", dragAndDrop).insertBefore($browseBtn);
$("form#postform, form#qrpostform").on("paste", e => {
const clipboardData = (e.originalEvent as ClipboardEvent).clipboardData;
if(clipboardData.items.length < 1 || clipboardData.items[0].kind !== "file") {
console.log("No files in clipboard");
if(clipboardData.items.length < 1) {
alertLightbox("No files in clipboard", "Unable to paste");
return;
}
if(clipboardData.items[0].kind !== "file") {
return;
}
const clipboardFile = clipboardData.items[0].getAsFile();

View file

@ -4,7 +4,7 @@ import "./vars";
import "./cookies";
import "./notifications";
import { setPageBanner } from "./dom/banners";
import { setCustomCSS, setCustomJS, setTheme } from "./settings";
import { setCustomCSS, setCustomJS, setTheme, updateExternalLinks } from "./settings";
import { handleKeydown } from "./boardevents";
import { initStaff, createStaffMenu, addStaffThreadOptions } from "./management/manage";
import { getPageThread } from "./postinfo";
@ -57,6 +57,7 @@ $(() => {
});
$(document).on("keydown", handleKeydown);
initFlags();
updateExternalLinks();
setCustomCSS();
setCustomJS();
});

View file

@ -8,11 +8,10 @@ function canNotify() {
&& (typeof Notification !== "undefined");
}
export function notify(title: string, body: string, img = noteIcon) {
export function notify(title: string, body: string, icon = noteIcon) {
const n = new Notification(title, {
body: body,
image: img,
icon: noteIcon
icon: icon
});
setTimeout(() => {
n.close();

View file

@ -107,6 +107,7 @@ interface MinMax {
min?: number;
max?: number;
}
class NumberSetting extends Setting<number, HTMLInputElement> {
constructor(key: string, title: string, defaultVal = 0, minMax: MinMax = {min: null, max: null}, onSave?:()=>any) {
super(key, title, defaultVal, onSave);
@ -205,6 +206,28 @@ export function setTheme() {
else
themeElem.setAttribute("href", path.join(webroot ?? "/", "css", style));
}
setLineHeight();
}
function setLineHeight() {
if(getBooleanStorageVal("increaselineheight", false)) {
document.body.classList.add("increase-line-height");
} else {
document.body.classList.remove("increase-line-height");
}
}
export function updateExternalLinks(post?: JQuery<HTMLElement>) {
const $src = post ?? $(".post-text");
const extPostLinks = $src.find<HTMLAnchorElement>("a:not(.postref)");
const newTab = getBooleanStorageVal("extlinksnewtab", true);
for(const link of extPostLinks) {
if(link.hostname !== location.hostname) {
link.target = newTab?"_blank":"_self";
} else {
link.target = "_self";
}
}
}
/**
@ -249,21 +272,23 @@ $(() => {
}
}) as Setting);
settings.set("pintopbar", new BooleanSetting("pintopbar", "Pin top bar", true, initTopBar));
settings.set("increaselineheight", new BooleanSetting("increaselineheight", "Increase line height", false, setLineHeight));
settings.set("enableposthover", new BooleanSetting("enableposthover", "Preview post on hover", true, initPostPreviews));
settings.set("enablepostclick", new BooleanSetting("enablepostclick", "Preview post on click", true, initPostPreviews));
settings.set("useqr", new BooleanSetting("useqr", "Use Quick Reply box", true, () => {
if(getBooleanStorageVal("useqr", true)) initQR();
else closeQR();
}));
settings.set("extlinksnewtab", new BooleanSetting("extlinksnewtab", "Open external links in new tab", true, updateExternalLinks));
settings.set("persistentqr", new BooleanSetting("persistentqr", "Persistent Quick Reply", false));
settings.set("watcherseconds", new NumberSetting("watcherseconds", "Check watched threads every # seconds", 15, {
min: 2
}, initWatcher));
settings.set("persistentqr", new BooleanSetting("persistentqr", "Persistent Quick Reply", false));
settings.set("newuploader", new BooleanSetting("newuploader", "Use new upload element", true, updateBrowseButton));
settings.set("smoothhidetoggle", new BooleanSetting("smoothhidetoggle", "Smooth hide block toggle", true));
settings.set("customjs", new TextSetting("customjs", "Custom JavaScript", ""));
settings.set("customcss", new TextSetting("customcss", "Custom CSS", "", setCustomCSS));
if($settingsButton === null)
$settingsButton = new TopBarButton("Settings", createLightbox, {before: "a#watcher"});
$settingsButton ??= new TopBarButton("Settings", createLightbox, {before: "a#watcher"});
});

2
go.mod
View file

@ -24,7 +24,7 @@ require (
github.com/yuin/gopher-lua v1.1.1
golang.org/x/crypto v0.31.0
golang.org/x/image v0.23.0
golang.org/x/net v0.32.0
golang.org/x/net v0.33.0
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf
layeh.com/gopher-luar v1.0.11
)

4
go.sum
View file

@ -189,8 +189,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View file

@ -17,20 +17,35 @@ div.section-body {
background: #282A2E;
}
select#changepage, table#postbox-static input,
table#postbox-static select,
table#postbox-static input,
table#postbox-static textarea,
table#postbox-static input[type=file]::file-selector-button,
table#postbox-static input[type=file]::-webkit-file-upload-button, div#report-delbox input,
div#report-delbox select,
div#report-delbox textarea,
div#report-delbox input[type=file]::file-selector-button,
div#report-delbox input[type=file]::-webkit-file-upload-button {
div#report-delbox input[type=file]::-webkit-file-upload-button, form#filterform input,
form#filterform textarea,
form#filterform input[type=file]::file-selector-button,
form#filterform input[type=file]::-webkit-file-upload-button, button.hideblock-button,
select#changepage,
select.post-actions,
select#boardsearch,
select[name=show], table#postbox-static select, div#report-delbox select, form#filterform select {
background: #282A2E;
border: 1px double #07371F;
color: #ACACAC;
}
button.hideblock-button,
select#changepage,
select.post-actions,
select#boardsearch,
select[name=show], table#postbox-static select, div#report-delbox select, form#filterform select {
background: #16171A;
border-radius: 5px;
color: #ACACAC;
}
div.reply,
th.postblock,
div.postprev,
@ -41,18 +56,7 @@ table.mgmt-table tr:first-of-type th {
border-radius: 5px;
}
table#postbox-static input,
table#postbox-static select,
table#postbox-static textarea,
table#postbox-static input[type=file]::file-selector-button,
table#postbox-static input[type=file]::-webkit-file-upload-button, div#report-delbox input,
div#report-delbox select,
div#report-delbox textarea,
div#report-delbox input[type=file]::file-selector-button,
div#report-delbox input[type=file]::-webkit-file-upload-button {
border-radius: 5px;
}
table#postbox-static button, table#postbox-static input[type=submit], div#report-delbox button, div#report-delbox input[type=submit] {
table#postbox-static button, table#postbox-static input[type=submit], div#report-delbox button, div#report-delbox input[type=submit], form#filterform button, form#filterform input[type=submit] {
background: #16171A;
}
@ -83,11 +87,21 @@ div#staff, select.post-actions {
div#topbar,
div#topbar a,
div#topbar a:visited {
div#topbar a:visited,
div.dropdown-menu {
background: #151515;
border-bottom: 1px solid #B0790A;
}
div.dropdown-menu {
border-left: 1px solid #B0790A;
border-right: 1px solid #B0790A;
}
div.dropdown-menu a:hover, div.dropdown-menu a:active {
background: #1D1F21;
}
table#pages, table#pages * {
border: none;
}

View file

@ -1,6 +1,6 @@
h1#board-title {
font-family: serif;
font-size: 24pt;
font-size: 2em;
color: #AF0A0F;
}
@ -9,12 +9,12 @@ h1#board-title {
}
div.file-info {
font-size: 12px;
font-size: 1em;
font-family: sans-serif;
}
span.postername {
font-size: 12px;
font-size: 1em;
font-family: serif;
color: #117743;
font-weight: 800;
@ -28,11 +28,26 @@ div.inlinepostprev {
}
select.post-actions,
div.reply,
div.postprev,
div.inlinepostprev {
border: 1px solid #9295a4;
}
.dice-roll {
border: 1px dashed #222;
}
div#upload-box {
background: #aaa;
}
div#upload-box a {
color: #444;
}
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
color: #666;
}
#site-title {
font-family: sans-serif;
padding-top: 88px;
@ -98,7 +113,6 @@ h2 a {
h3 {
margin: 0px;
text-align: center;
font-size: medium;
}
@ -114,18 +128,32 @@ h1, h2, h3 {
div#topbar {
-webkit-filter: drop-shadow(3px 5px 6px #555555);
filter: drop-shadow(3px 5px 6px #555555);
height: 30px;
min-height: 1.5em;
}
div#topbar,
.topbar-item,
.topbar-item:visited,
a.topbar-item,
a.topbar-item:visited,
.dropdown-button,
.dropdown-menu {
background: #000A89;
div.dropdown-menu {
background: #080e5e;
color: #EEF2FF;
}
div#footer {
font-size: 8pt;
div.dropdown-menu {
-webkit-filter: drop-shadow(3px 5px 6px #555555);
filter: drop-shadow(3px 5px 6px #555555);
}
div.dropdown-menu a, div.dropdown-menu h3 {
color: #EEF2FF;
}
a.topbar-item:hover,
a.topbar-item:active,
div.dropdown-menu a:hover {
background: #0a127b;
}
footer {
font-size: 0.75em;
}

View file

@ -59,7 +59,7 @@ table.mgmt-table tr:first-of-type th {
}
span.subject {
font-weight: bold;
font-weight: 800;
color: #34ED3A;
}
@ -71,3 +71,13 @@ div.section-title-block {
background: #A7A7A7;
border-bottom: 1px solid #117743;
}
div#upload-box {
background: #aaa;
}
div#upload-box a {
color: #444;
}
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
color: #666;
}

View file

@ -2,7 +2,6 @@ body {
background: #1E1E1E;
font-family: sans-serif;
color: #999;
font-size: 80%;
}
a {
@ -56,6 +55,13 @@ span.tripcode {
color: #32DD72;
}
.dropdown-menu {
background: #666 !important;
}
.dropdown-menu b {
color: black;
}
span.subject {
color: #446655;
}

View file

@ -1,3 +1,23 @@
@keyframes slideopen {
from {
transform: scale(1, 0);
transform-origin: top center;
}
to {
transform: scale(1, 1);
transform-origin: top center;
}
}
@keyframes slideclose {
from {
transform: scale(1, 1);
transform-origin: top center;
}
to {
transform: scale(1, 0);
transform-origin: top center;
}
}
#boardmenu-bottom {
margin-top: 16px;
}
@ -220,6 +240,32 @@ div.inlinepostprev {
padding: 4px 8px;
}
.hideblock {
border: 1px solid black;
background: rgba(0, 0, 0, 0.1);
}
.hideblock.open {
animation: slideopen 0.25s;
}
.hideblock.close {
animation: slideclose 0.25s;
}
.hideblock.hidden {
display: none;
}
.hideblock-button {
display: block;
}
.dice-roll {
border: 1px dashed #aaa;
font-weight: bold;
}
div.section-block {
margin-bottom: 8px;
}
@ -439,19 +485,17 @@ form#login-box input[type=submit] {
}
.lightbox-title {
font-size: 42px;
font-weight: 700;
text-align: center;
margin-top: 0px;
}
.lightbox-x {
color: #000 !important;
float: right;
font-size: 32px;
font-weight: 700;
font-size: inherit;
}
.lightbox-x:hover {
.lightbox-x:hover, .lightbox-x:active {
color: #555;
}
@ -506,7 +550,7 @@ div#qr-box {
}
div#qr-box input[type=text], div#qr-box textarea {
display: table-cell;
min-width: 300px;
min-width: 320px;
background: #FFF;
color: #000;
width: 100%;
@ -597,6 +641,10 @@ img#banpage-image {
margin: 4px 8px 8px 4px;
}
.increase-line-height header, .increase-line-height .post, .increase-line-height .reply {
line-height: 1.5;
}
header {
margin-top: 50px;
text-align: center;
@ -668,7 +716,7 @@ div.section-block div.section-body {
text-align: center;
}
#footer {
footer {
bottom: 0px;
clear: both;
left: 0px;

View file

@ -65,3 +65,13 @@ header, div#top-pane, a {
#site-title, #board-title {
font-weight: 800;
}
div#upload-box {
background: #aaa;
}
div#upload-box a {
color: #444;
}
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
color: #666;
}

View file

@ -121,7 +121,7 @@ a.topbar-item:hover {
background: #404040;
}
div#footer, div#footer * {
footer, footer * {
font-size: 9pt;
}

View file

@ -93,3 +93,17 @@ span.subject {
color: #CC1105;
font-weight: 700;
}
.dice-roll {
border: 1px dashed #222;
}
div#upload-box {
background: #aaa;
}
div#upload-box a {
color: #444;
}
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
color: #666;
}

View file

@ -93,3 +93,17 @@ span.subject {
color: #0F0C5D;
font-weight: 700;
}
.dice-roll {
border: 1px dashed #222;
}
div#upload-box {
background: #aaa;
}
div#upload-box a {
color: #444;
}
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
color: #666;
}

View file

@ -7,6 +7,6 @@
<h1>404: File not found</h1>
<img src="/error/lol 404.gif" alt="lol 404">
<p>The requested file could not be found on this server.</p>
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.0.1
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.1.0
</body>
</html>

View file

@ -7,6 +7,6 @@
<h1>Error 500: Internal Server error</h1>
<img src="/error/server500.gif" alt="server burning">
<p>The server encountered an error while trying to serve the page, and we apologize for the inconvenience. The <a href="https://en.wikipedia.org/wiki/Idiot">system administrator</a> will try to fix things as soon they get around to it, whenever that is. Hopefully soon.</p>
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.0.1
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.1.0
</body>
</html>

View file

@ -7,6 +7,6 @@
<h1>Error 502: Bad gateway</h1>
<img src="/error/server500.gif" alt="server burning">
<p>The server encountered an error while trying to serve the page, and we apologize for the inconvenience. The <a href="https://en.wikipedia.org/wiki/Idiot">system administrator</a> will try to fix things as soon they get around to it, whenever that is. Hopefully soon.</p>
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.0.1
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.1.0
</body>
</html>

View file

Before

Width:  |  Height:  |  Size: 753 B

After

Width:  |  Height:  |  Size: 753 B

Before After
Before After

View file

@ -7,11 +7,11 @@
viewBox="0 0 120 120"
version="1.1"
id="svg5"
inkscape:export-filename="cyclical.png"
inkscape:export-filename="cyclic.png"
inkscape:export-xdpi="12.8"
inkscape:export-ydpi="12.8"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="cyclical.svg"
sodipodi:docname="cyclic.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

View file

@ -21,7 +21,7 @@ import (
const (
dirIsAFileStr = `unable to create %q, path exists and is a file`
genericErrStr = `unable to create %q: %s`
genericErrStr = `unable to create %q: %w`
pathExistsStr = `unable to create %q, path already exists`
)
@ -63,12 +63,12 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
if err != nil {
errEv.Err(err).Caller().
Msg("Failed getting board threads")
return fmt.Errorf("error getting threads for /%s/: %s", board.Dir, err.Error())
return fmt.Errorf("error getting threads for /%s/: %w", board.Dir, err)
}
topPosts, err := getBoardTopPosts(board.Dir)
if err != nil {
errEv.Err(err).Caller().Msg("Failed getting board threads")
return fmt.Errorf("error getting OP posts for /%s/: %s", board.Dir, err.Error())
return fmt.Errorf("error getting OP posts for /%s/: %w", board.Dir, err)
}
opMap := make(map[int]*Post)
for _, post := range topPosts {
@ -102,13 +102,13 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
catalogThread.Replies, err = thread.GetReplyCount()
if err != nil {
errEv.Err(err).Caller().Msg("Failed getting reply count")
return errors.New("Error getting reply count: " + err.Error())
return fmt.Errorf("error getting reply count: %w", err)
}
catalogThread.Posts, err = getThreadPosts(&thread)
if err != nil {
errEv.Err(err).Caller().Msg("Failed getting replies")
return errors.New("Failed getting replies: " + err.Error())
return fmt.Errorf("failed getting replies: %w", err)
}
if len(catalogThread.Posts) == 0 {
continue
@ -122,7 +122,7 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
catalogThread.uploads, err = thread.GetUploads()
if err != nil {
errEv.Err(err).Caller().Msg("Failed getting thread uploads")
return errors.New("Failed getting thread uploads: " + err.Error())
return fmt.Errorf("failed getting thread uploads: %w", err)
}
var imagesOnBoardPage int
@ -157,19 +157,19 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
errEv.Err(err).Caller().
Str("page", "board.html").
Msg("Failed getting board page")
return fmt.Errorf("failed opening /%s/board.html: %s", board.Dir, err.Error())
return fmt.Errorf("failed opening /%s/board.html: %w", board.Dir, err)
}
defer boardPageFile.Close()
if err = config.TakeOwnershipOfFile(boardPageFile); err != nil {
errEv.Err(err).Caller().
Msg("Unable to take ownership of board.html")
return fmt.Errorf("unable to take ownership of /%s/board.html: %s", board.Dir, err.Error())
return fmt.Errorf("unable to take ownership of /%s/board.html: %w", board.Dir, err)
}
// Render board page template to the file,
// packaging the board/section list, threads, and board info
captchaCfg := config.GetSiteConfig().Captcha
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]any{
"boards": gcsql.AllBoards,
"sections": gcsql.AllSections,
"threads": threads,
@ -183,7 +183,7 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
errEv.Err(err).Caller().
Str("page", "board.html").
Msg("Failed building board")
return fmt.Errorf("failed building /%s/: %s", board.Dir, err.Error())
return fmt.Errorf("failed building /%s/: %w", board.Dir, err)
}
if err = boardPageFile.Close(); err != nil {
@ -207,14 +207,14 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
if err != nil {
errEv.Err(err).Caller().
Msg("Failed opening catalog.json")
return fmt.Errorf("failed opening /%s/catalog.json: %s", board.Dir, err.Error())
return fmt.Errorf("failed opening /%s/catalog.json: %w", board.Dir, err)
}
defer catalogJSONFile.Close()
if err = config.TakeOwnershipOfFile(catalogJSONFile); err != nil {
errEv.Err(err).Caller().
Msg("Unable to take ownership of catalog.json")
return fmt.Errorf("unable to take ownership of /%s/catalog.json: %s", board.Dir, err.Error())
return fmt.Errorf("unable to take ownership of /%s/catalog.json: %w", board.Dir, err)
}
for _, page := range catalog.pages {
catalog.currentPage++
@ -243,7 +243,7 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
if (numThreads % boardConfig.ThreadsPerPage) > 0 {
numPages++
}
data := map[string]interface{}{
data := map[string]any{
"boards": gcsql.AllBoards,
"sections": gcsql.AllSections,
"threads": page.Threads,
@ -262,7 +262,7 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
}
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, data, currentPageFile, "text/html"); err != nil {
errEv.Err(err).Caller().Send()
return fmt.Errorf("failed building /%s/ boardpage: %s", board.Dir, err.Error())
return fmt.Errorf("failed building /%s/ boardpage: %w", board.Dir, err)
}
if err = currentPageFile.Close(); err != nil {
errEv.Err(err).Caller().Send()
@ -307,7 +307,7 @@ func BuildBoards(verbose bool, which ...int) error {
errEv.Err(err).Caller().
Int("boardid", boardID).
Msg("Unable to get board information")
return fmt.Errorf("unable to get board information (ID: %d): %s", boardID, err.Error())
return fmt.Errorf("unable to get board information (ID: %d): %w", boardID, err)
}
boards = append(boards, *board)
}
@ -446,12 +446,12 @@ func buildBoard(board *gcsql.Board, force bool) error {
} else if err = os.Mkdir(dirPath, config.DirFileMode); err != nil {
errEv.Err(os.ErrExist).Caller().
Str("dirPath", dirPath).Send()
return fmt.Errorf(genericErrStr, dirPath, err.Error())
return fmt.Errorf(genericErrStr, dirPath, err)
}
if err = config.TakeOwnership(dirPath); err != nil {
errEv.Err(err).Caller().
Str("dirPath", dirPath).Send()
return fmt.Errorf(genericErrStr, dirPath, err.Error())
return fmt.Errorf(genericErrStr, dirPath, err)
}
if resInfo != nil {
@ -468,15 +468,15 @@ func buildBoard(board *gcsql.Board, force bool) error {
return err
}
} else if err = os.Mkdir(resPath, config.DirFileMode); err != nil {
err = fmt.Errorf(genericErrStr, resPath, err.Error())
err = fmt.Errorf(genericErrStr, resPath, err)
errEv.Err(err).Caller().
Str("resPath", resPath).Send()
return fmt.Errorf(genericErrStr, resPath, err.Error())
return fmt.Errorf(genericErrStr, resPath, err)
}
if err = config.TakeOwnership(resPath); err != nil {
errEv.Err(err).Caller().
Str("resPath", resPath).Send()
return fmt.Errorf(genericErrStr, resPath, err.Error())
return fmt.Errorf(genericErrStr, resPath, err)
}
if srcInfo != nil {
@ -493,7 +493,7 @@ func buildBoard(board *gcsql.Board, force bool) error {
return err
}
} else if err = os.Mkdir(srcPath, config.DirFileMode); err != nil {
err = fmt.Errorf(genericErrStr, srcPath, err.Error())
err = fmt.Errorf(genericErrStr, srcPath, err)
errEv.Err(err).Caller().
Str("srcPath", srcPath).Send()
return err
@ -501,7 +501,7 @@ func buildBoard(board *gcsql.Board, force bool) error {
if config.TakeOwnership(srcPath); err != nil {
errEv.Err(err).Caller().
Str("srcPath", srcPath).Send()
return fmt.Errorf(genericErrStr, srcPath, err.Error())
return fmt.Errorf(genericErrStr, srcPath, err)
}
if thumbInfo != nil {
@ -514,12 +514,12 @@ func buildBoard(board *gcsql.Board, force bool) error {
} else if err = os.Mkdir(thumbPath, config.DirFileMode); err != nil {
errEv.Err(err).Caller().
Str("thumbPath", thumbPath).Send()
return fmt.Errorf(genericErrStr, thumbPath, err.Error())
return fmt.Errorf(genericErrStr, thumbPath, err)
}
if config.TakeOwnership(thumbPath); err != nil {
errEv.Err(err).Caller().
Str("thumbPath", thumbPath).Send()
return fmt.Errorf(genericErrStr, thumbPath, err.Error())
return fmt.Errorf(genericErrStr, thumbPath, err)
}
if err = BuildBoardPages(board, errEv); err != nil {
@ -562,12 +562,12 @@ func BuildBoardListJSON() error {
defer errEv.Discard()
if err != nil {
errEv.Err(err).Caller().Send()
return errors.New("unable to open boards.json for writing: " + err.Error())
return fmt.Errorf("unable to open boards.json for writing: %w", err)
}
if err = config.TakeOwnershipOfFile(boardListFile); err != nil {
errEv.Err(err).Caller().Send()
return errors.New("unable to update boards.json ownership: " + err.Error())
return fmt.Errorf("unable to update boards.json ownership: %w", err)
}
boardsListJSONData := boardsListJSON{
@ -585,15 +585,15 @@ func BuildBoardListJSON() error {
boardJSON, err := json.Marshal(boardsListJSONData)
if err != nil {
errEv.Err(err).Caller().Send()
return errors.New("Failed to create boards.json " + err.Error())
return fmt.Errorf("failed to create boards.json: %w", err)
}
if _, err = serverutil.MinifyWriter(boardListFile, boardJSON, "application/json"); err != nil {
errEv.Err(err).Caller().Send()
errEv.Err(err).Caller().Msg("Failed writing to boards.json")
return errors.New("failed writing boards.json file")
}
if err = boardListFile.Close(); err != nil {
errEv.Err(err).Caller().Send()
errEv.Err(err).Caller().Msg("Failed closing boards.json")
return errors.New("failed closing boards.json")
}
return nil

View file

@ -1,7 +1,6 @@
package building
import (
"errors"
"fmt"
"io"
"os"
@ -36,9 +35,9 @@ func getFrontPagePosts() ([]frontPagePost, error) {
if siteCfg.RecentPostsWithNoFile {
// get recent posts, including those with no file
query = "SELECT * FROM DBPREFIXv_front_page_posts"
query = "SELECT id, message_raw, dir, filename, op_id FROM DBPREFIXv_front_page_posts"
} else {
query = "SELECT * FROM DBPREFIXv_front_page_posts_with_file"
query = "SELECT id, message_raw, dir, filename, op_id FROM DBPREFIXv_front_page_posts_with_file"
}
query += " ORDER BY id DESC LIMIT " + strconv.Itoa(siteCfg.MaxRecentPosts)
@ -84,18 +83,18 @@ func BuildFrontPage() error {
err := gctemplates.InitTemplates(gctemplates.FrontPage)
if err != nil {
errEv.Err(err).Caller().Send()
return errors.New("Error loading front page template: " + err.Error())
return fmt.Errorf("failed loading front page template: %w", err)
}
criticalCfg := config.GetSystemCriticalConfig()
frontFile, err := os.OpenFile(path.Join(criticalCfg.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, config.NormalFileMode)
if err != nil {
errEv.Err(err).Caller().Send()
return errors.New("Failed opening front page for writing: " + err.Error())
return fmt.Errorf("failed opening front page for writing: %w", err)
}
if err = config.TakeOwnershipOfFile(frontFile); err != nil {
errEv.Err(err).Caller().Send()
return errors.New("Failed setting file ownership for front page: " + err.Error())
return fmt.Errorf("failed setting file ownership for front page: %w", err)
}
var recentPostsArr []frontPagePost
@ -103,9 +102,9 @@ func BuildFrontPage() error {
recentPostsArr, err = getFrontPagePosts()
if err != nil {
errEv.Err(err).Caller().Send()
return errors.New("Failed loading recent posts: " + err.Error())
return fmt.Errorf("failed loading recent posts: %w", err)
}
if err = serverutil.MinifyTemplate(gctemplates.FrontPage, map[string]interface{}{
if err = serverutil.MinifyTemplate(gctemplates.FrontPage, map[string]any{
"siteConfig": siteCfg,
"sections": gcsql.AllSections,
"boards": gcsql.AllBoards,
@ -113,15 +112,15 @@ func BuildFrontPage() error {
"recentPosts": recentPostsArr,
}, frontFile, "text/html"); err != nil {
errEv.Err(err).Caller().Send()
return errors.New("Failed executing front page template: " + err.Error())
return fmt.Errorf("failed executing front page template: %w", err)
}
return frontFile.Close()
}
// BuildPageHeader is a convenience function for automatically generating the top part
// of every normal HTML page
func BuildPageHeader(writer io.Writer, pageTitle string, board string, misc map[string]interface{}) error {
phMap := map[string]interface{}{
func BuildPageHeader(writer io.Writer, pageTitle string, board string, misc map[string]any) error {
phMap := map[string]any{
"pageTitle": pageTitle,
"documentTitle": pageTitle + " - " + config.GetSiteConfig().SiteName,
"siteConfig": config.GetSiteConfig(),
@ -139,7 +138,7 @@ func BuildPageHeader(writer io.Writer, pageTitle string, board string, misc map[
// of every normal HTML page
func BuildPageFooter(writer io.Writer) (err error) {
return serverutil.MinifyTemplate(gctemplates.PageFooter,
map[string]interface{}{}, writer, "text/html")
map[string]any{}, writer, "text/html")
}
// BuildJS minifies (if enabled) consts.js, which is built from a template
@ -150,7 +149,7 @@ func BuildJS() error {
defer errEv.Discard()
if err != nil {
errEv.Err(err).Caller().Send()
return errors.New("Error loading consts.js template:" + err.Error())
return fmt.Errorf("failed loading consts.js template: %w", err)
}
boardCfg := config.GetBoardConfig("")
@ -159,12 +158,12 @@ func BuildJS() error {
constsJSFile, err := os.OpenFile(constsJSPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, config.NormalFileMode)
if err != nil {
errEv.Err(err).Caller().Send()
return fmt.Errorf("error opening consts.js for writing: %s", err.Error())
return fmt.Errorf("failed opening consts.js for writing: %w", err)
}
if err = config.TakeOwnershipOfFile(constsJSFile); err != nil {
errEv.Err(err).Caller().Send()
return fmt.Errorf("unable to update file ownership for consts.js: %s", err.Error())
return fmt.Errorf("unable to update file ownership for consts.js: %w", err)
}
if err = serverutil.MinifyTemplate(gctemplates.JsConsts, map[string]any{
@ -175,7 +174,7 @@ func BuildJS() error {
"fileTypes": boardCfg.AllowOtherExtensions,
}, constsJSFile, "text/javascript"); err != nil {
errEv.Err(err).Caller().Send()
return fmt.Errorf("error building consts.js: %s", err.Error())
return fmt.Errorf("failed building consts.js: %w", err)
}
return constsJSFile.Close()
}

View file

@ -25,14 +25,14 @@ func TestBuildJS(t *testing.T) {
}
outDir := t.TempDir()
config.SetVersion("3.11.0")
config.SetVersion("4.0.2")
systemCriticalCfg := config.GetSystemCriticalConfig()
systemCriticalCfg.DocumentRoot = path.Join(outDir, "html")
systemCriticalCfg.TemplateDir = path.Join(testRoot, "templates")
systemCriticalCfg.LogDir = path.Join(outDir, "logs")
systemCriticalCfg.WebRoot = "/chan"
systemCriticalCfg.TimeZone = 8
config.SetSystemCriticalConfig(&systemCriticalCfg)
config.SetSystemCriticalConfig(systemCriticalCfg)
boardCfg := config.GetBoardConfig("")
boardCfg.Styles = []config.Style{
@ -109,7 +109,7 @@ func doFrontBuildingTest(t *testing.T, mock sqlmock.Sqlmock, expectOut string) {
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "abbreviation", "position", "hidden"}).
AddRows([]driver.Value{1, "Main", "main", 1, false}))
mock.ExpectPrepare(`SELECT \* FROM v_front_page_posts_with_file ORDER BY id DESC LIMIT 15`).ExpectQuery().WillReturnRows(
mock.ExpectPrepare(`SELECT id, message_raw, dir, filename, op_id FROM v_front_page_posts_with_file ORDER BY id DESC LIMIT 15`).ExpectQuery().WillReturnRows(
sqlmock.NewRows([]string{"posts.id", "posts.message_raw", "dir", "filename", "op.id"}).
AddRows(
[]driver.Value{1, "message_raw", "test", "filename", 1},
@ -147,14 +147,14 @@ func TestBuildFrontPage(t *testing.T) {
}
t.Run(driver, func(t *testing.T) {
outDir := t.TempDir()
config.SetVersion("3.11.0")
config.SetVersion("4.0.2")
systemCriticalCfg := config.GetSystemCriticalConfig()
systemCriticalCfg.DocumentRoot = path.Join(outDir, "html")
systemCriticalCfg.TemplateDir = path.Join(testRoot, "templates")
systemCriticalCfg.LogDir = path.Join(outDir, "logs")
systemCriticalCfg.WebRoot = "/chan"
systemCriticalCfg.TimeZone = 8
config.SetSystemCriticalConfig(&systemCriticalCfg)
config.SetSystemCriticalConfig(systemCriticalCfg)
boardCfg := config.GetBoardConfig("")
boardCfg.Styles = []config.Style{{Name: "test1", Filename: "test1.css"}}

View file

@ -7,7 +7,7 @@ const defaultStyle = "test1.css";
const webroot = "/chan";
const serverTZ = 8;
const fileTypes = [];`
expectedMinifiedFront = `<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Gochan</title><link rel=stylesheet href=/chan/css/global.css><link id=theme rel=stylesheet href=/chan/css/test1.css><link rel="shortcut icon" href=/chan/favicon.png><script src=/chan/js/consts.js></script><script src=/chan/js/gochan.js></script><div id=topbar><div class=topbar-section><a href=/chan/ class=topbar-item>home</a></div><div class=topbar-section><a href=/chan/test/ class=topbar-item title="Testing board">/test/</a><a href=/chan/test2/ class=topbar-item title="Testing board 2">/test2/</a></div></div><div id=content><div id=top-pane><h1 id=site-title>Gochan</h1><span id=site-slogan></span></div><br><div id=frontpage><div class=section-block style="margin: 16px 64px 16px 64px;"><div class="section-body front-intro">Welcome to Gochan!</div></div><div class=section-block><div class=section-title-block><b>Boards</b></div><div class=section-body><ul style="float:left; list-style: none"><li style="text-align: center; font-weight: bold"><b><u>Main</u></b><li><a href=/chan/test/ title="Board for testing description">/test/</a> — Testing board<li><a href=/chan/test2/ title="Board for testing description 2">/test2/</a> — Testing board 2</ul></div></div><div class=section-block><div class=section-title-block><b>Recent Posts</b></div><div class=section-body><div id=recent-posts><div class=recent-post><a href=/chan/test/res/1.html#1 class=front-reply target=_blank><img src=/chan/test/thumb alt="post thumbnail"></a><br><br><a href=/chan/test/>/test/</a><hr>message_raw</div><div class=recent-post><a href=/chan/test/res/1.html#2 class=front-reply target=_blank><img src=/chan/test/thumb alt="post thumbnail"></a><br><br><a href=/chan/test/>/test/</a><hr>message_raw</div></div></div></div></div><div id=footer>Powered by <a href=http://github.com/gochan-org/gochan/>Gochan 3.11</a><br></div></div>`
expectedMinifiedFront = `<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Gochan</title><link rel=stylesheet href=/chan/css/global.css><link id=theme rel=stylesheet href=/chan/css/test1.css><link rel="shortcut icon" href=/chan/favicon.png><script src=/chan/js/consts.js></script><script src=/chan/js/gochan.js defer></script><div id=topbar><div class=topbar-section><a href=/chan/ class=topbar-item>home</a></div><div class=topbar-section><a href=/chan/test/ class=topbar-item title="Testing board">/test/</a><a href=/chan/test2/ class=topbar-item title="Testing board 2">/test2/</a></div></div><div id=content><div id=top-pane><h1 id=site-title>Gochan</h1><span id=site-slogan></span></div><br><div id=frontpage><div class=section-block style="margin: 16px 64px 16px 64px;"><div class="section-body front-intro">Welcome to Gochan!</div></div><div class=section-block><div class=section-title-block><b>Boards</b></div><div class=section-body><ul style="float:left; list-style: none"><li style="text-align: center; font-weight: bold"><b><u>Main</u></b><li><a href=/chan/test/ title="Board for testing description">/test/</a> — Testing board<li><a href=/chan/test2/ title="Board for testing description 2">/test2/</a> — Testing board 2</ul></div></div><div class=section-block><div class=section-title-block><b>Recent Posts</b></div><div class=section-body><div id=recent-posts><div class=recent-post><a href=/chan/test/res/1.html#1 class=front-reply target=_blank><img src=/chan/test/thumb alt="post thumbnail"></a><br><br><a href=/chan/test/>/test/</a><hr>message_raw</div><div class=recent-post><a href=/chan/test/res/1.html#2 class=front-reply target=_blank><img src=/chan/test/thumb alt="post thumbnail"></a><br><br><a href=/chan/test/>/test/</a><hr>message_raw</div></div></div></div></div><footer>Powered by <a href=http://github.com/gochan-org/gochan/>Gochan 4.0.2</a><br></footer></div>`
expectedUnminifiedFront = `<!DOCTYPE html>
<html lang="en">
<head>
@ -17,7 +17,7 @@ const fileTypes = [];`
<link rel="stylesheet" href="/chan/css/global.css" />
<link id="theme" rel="stylesheet" href="/chan/css/test1.css" />
<link rel="shortcut icon" href="/chan/favicon.png"><script type="text/javascript" src="/chan/js/consts.js"></script>
<script type="text/javascript" src="/chan/js/gochan.js"></script>
<script type="text/javascript" src="/chan/js/gochan.js" defer></script>
</head>
<body>
<div id="topbar">
@ -73,9 +73,9 @@ const fileTypes = [];`
</div>
</div>
</div>
<div id="footer">
Powered by <a href="http://github.com/gochan-org/gochan/">Gochan 3.11</a><br />
</div>
<footer>
Powered by <a href="http://github.com/gochan-org/gochan/">Gochan 4.0.2</a><br />
</footer>
</div>
</body>
</html>

View file

@ -63,7 +63,10 @@ func (catalog *boardCatalog) fillPages(threadsPerPage int, threads []catalogThre
}
func getBoardTopPosts(board string) ([]*Post, error) {
const query = "SELECT * FROM DBPREFIXv_building_posts WHERE id = parent_id AND dir = ?"
const query = `SELECT id, thread_id, ip, name, tripcode, email, subject, created_on, last_modified, parent_id,
last_bump, message, message_raw, board_id, dir, original_filename, filename, checksum, filesize, tw, th,
width, height, locked, stickied, cyclical, flag, country, is_deleted
FROM DBPREFIXv_building_posts WHERE id = parent_id AND dir = ?`
var posts []*Post
err := QueryPosts(query, []any{board}, func(p *Post) error {
@ -95,7 +98,7 @@ func BuildCatalog(boardID int) error {
catalogFile, err := os.OpenFile(catalogPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, config.NormalFileMode)
if err != nil {
errEv.Err(err).Caller().Send()
return fmt.Errorf("failed opening /%s/catalog.html: %s", board.Dir, err.Error())
return fmt.Errorf("failed opening /%s/catalog.html: %w", board.Dir, err)
}
if err = config.TakeOwnershipOfFile(catalogFile); err != nil {
@ -110,7 +113,7 @@ func BuildCatalog(boardID int) error {
}
boardConfig := config.GetBoardConfig(board.Dir)
if err = serverutil.MinifyTemplate(gctemplates.Catalog, map[string]interface{}{
if err = serverutil.MinifyTemplate(gctemplates.Catalog, map[string]any{
"boards": gcsql.AllBoards,
"board": board,
"boardConfig": boardConfig,

View file

@ -99,8 +99,8 @@ func (p *Post) Stickied() bool {
return p.thread.Stickied
}
func (p *Post) Cyclical() bool {
return p.thread.Cyclical
func (p *Post) Cyclic() bool {
return p.thread.Cyclic
}
// Select all from v_building_posts (and queries with the same columns) and call the callback function on each Post
@ -131,7 +131,7 @@ func QueryPosts(query string, params []any, cb func(*Post) error) error {
&post.LastModified, &post.ParentID, &lastBump, &post.Message, &post.MessageRaw, &post.BoardID,
&post.BoardDir, &post.OriginalFilename, &post.Filename, &post.Checksum, &post.Filesize,
&post.ThumbnailWidth, &post.ThumbnailHeight, &post.UploadWidth, &post.UploadHeight,
&post.thread.Locked, &post.thread.Stickied, &post.thread.Cyclical, &post.Country.Flag, &post.Country.Name,
&post.thread.Locked, &post.thread.Stickied, &post.thread.Cyclic, &post.Country.Flag, &post.Country.Name,
&post.IsDeleted)
if err = rows.Scan(dest...); err != nil {
@ -155,7 +155,10 @@ func QueryPosts(query string, params []any, cb func(*Post) error) error {
}
func GetBuildablePostsByIP(ip string, limit int) ([]*Post, error) {
query := "SELECT * FROM DBPREFIXv_building_posts WHERE ip = PARAM_ATON ORDER BY id DESC"
query := `SELECT id, thread_id, ip, name, tripcode, email, subject, created_on, last_modified, parent_id,
last_bump, message, message_raw, board_id, dir, original_filename, filename, checksum, filesize, tw, th,
width, height, locked, stickied, cyclical, flag, country, is_deleted
FROM DBPREFIXv_building_posts WHERE ip = PARAM_ATON ORDER BY id DESC`
if limit > 0 {
query += " LIMIT " + strconv.Itoa(limit)
}
@ -169,7 +172,10 @@ func GetBuildablePostsByIP(ip string, limit int) ([]*Post, error) {
}
func getThreadPosts(thread *gcsql.Thread) ([]*Post, error) {
const query = "SELECT * FROM DBPREFIXv_building_posts WHERE thread_id = ? ORDER BY id ASC"
const query = `SELECT id, thread_id, ip, name, tripcode, email, subject, created_on, last_modified, parent_id,
last_bump, message, message_raw, board_id, dir, original_filename, filename, checksum, filesize, tw, th,
width, height, locked, stickied, cyclical, flag, country, is_deleted
FROM DBPREFIXv_building_posts WHERE thread_id = ? ORDER BY id ASC`
var posts []*Post
err := QueryPosts(query, []any{thread.ID}, func(p *Post) error {
posts = append(posts, p)
@ -179,7 +185,10 @@ func getThreadPosts(thread *gcsql.Thread) ([]*Post, error) {
}
func GetRecentPosts(boardid int, limit int) ([]*Post, error) {
query := `SELECT * FROM DBPREFIXv_building_posts`
query := `SELECT id, thread_id, ip, name, tripcode, email, subject, created_on, last_modified, parent_id,
last_bump, message, message_raw, board_id, dir, original_filename, filename, checksum, filesize, tw, th,
width, height, locked, stickied, cyclical, flag, country, is_deleted
FROM DBPREFIXv_building_posts`
var args []any
if boardid > 0 {

View file

@ -84,18 +84,18 @@ func BuildThreadPages(op *gcsql.Post) error {
threadPageFile, err = os.OpenFile(threadPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, config.NormalFileMode)
if err != nil {
errEv.Err(err).Caller().Send()
return fmt.Errorf("unable to open /%s/res/%d.html: %s", board.Dir, op.ID, err.Error())
return fmt.Errorf("unable to open /%s/res/%d.html: %w", board.Dir, op.ID, err)
}
if err = config.TakeOwnershipOfFile(threadPageFile); err != nil {
errEv.Err(err).Caller().Send()
return fmt.Errorf("unable to set file permissions for /%s/res/%d.html: %s", board.Dir, op.ID, err.Error())
return fmt.Errorf("unable to set file permissions for /%s/res/%d.html: %w", board.Dir, op.ID, err)
}
errEv.Int("op", posts[0].ID)
// render thread page
captchaCfg := config.GetSiteConfig().Captcha
if err = serverutil.MinifyTemplate(gctemplates.ThreadPage, map[string]interface{}{
if err = serverutil.MinifyTemplate(gctemplates.ThreadPage, map[string]any{
"boards": gcsql.AllBoards,
"board": board,
"boardConfig": config.GetBoardConfig(board.Dir),
@ -107,7 +107,7 @@ func BuildThreadPages(op *gcsql.Post) error {
"captcha": captchaCfg,
}, threadPageFile, "text/html"); err != nil {
errEv.Err(err).Caller().Send()
return fmt.Errorf("failed building /%s/res/%d threadpage: %s", board.Dir, posts[0].ID, err.Error())
return fmt.Errorf("failed building /%s/res/%d threadpage: %w", board.Dir, posts[0].ID, err)
}
if err = threadPageFile.Close(); err != nil {
errEv.Err(err).Caller().Send()

View file

@ -41,11 +41,15 @@ type GochanConfig struct {
// ValidateValues checks to make sure that the configuration options are usable
// (e.g., ListenIP is a valid IP address, Port isn't a negative number, etc)
func (gcfg *GochanConfig) ValidateValues() error {
// if net.ParseIP(gcfg.ListenIP) == nil {
// return &InvalidValueError{Field: "ListenIP", Value: gcfg.ListenIP}
// }
changed := false
if gcfg.SiteDomain == "" {
return &InvalidValueError{Field: "SiteDomain", Value: gcfg.SiteDomain, Details: "must be set"}
}
if strings.Contains(gcfg.SiteDomain, " ") || strings.Contains(gcfg.SiteDomain, "://") {
return &InvalidValueError{Field: "SiteDomain", Value: gcfg.SiteDomain, Details: "must be a host (port optional)"}
}
_, err := durationutil.ParseLongerDuration(gcfg.CookieMaxAge)
if errors.Is(err, durationutil.ErrInvalidDurationString) {
return &InvalidValueError{Field: "CookieMaxAge", Value: gcfg.CookieMaxAge, Details: err.Error() + cookieMaxAgeEx}
@ -162,10 +166,11 @@ type SystemCriticalConfig struct {
SQLConfig
Verbose bool `json:"DebugMode"`
RandomSeed string
Version *GochanVersion `json:"-"`
TimeZone int `json:"-"`
CheckRequestReferer bool
Verbose bool `json:"DebugMode"`
RandomSeed string
Version *GochanVersion `json:"-"`
TimeZone int `json:"-"`
}
// SiteConfig contains information about the site/community, e.g. the name of the site, the slogan (if set),
@ -321,7 +326,8 @@ type PostConfig struct {
RepliesOnBoardPage int
StickyRepliesOnBoardPage int
NewThreadsRequireUpload bool
CyclicalThreadNumPosts int
EnableCyclicThreads bool
CyclicThreadNumPosts int
BanColors []string
BanMessage string
@ -331,6 +337,7 @@ type PostConfig struct {
ImagesOpenNewTab bool
NewTabOnOutlinks bool
DisableBBcode bool
AllowDiceRerolls bool
}
func WriteConfig() error {
@ -345,8 +352,8 @@ func GetSQLConfig() SQLConfig {
// GetSystemCriticalConfig returns system-critical configuration options like listening IP
// It returns a value instead of a pointer, because it is not usually safe to edit while Gochan is running.
func GetSystemCriticalConfig() SystemCriticalConfig {
return cfg.SystemCriticalConfig
func GetSystemCriticalConfig() *SystemCriticalConfig {
return &cfg.SystemCriticalConfig
}
// GetSiteConfig returns the global site configuration (site name, slogan, etc)

View file

@ -10,6 +10,7 @@ var (
DBMaxIdleConnections: DefaultSQLMaxConns,
DBConnMaxLifetimeMin: DefaultSQLConnMaxLifetimeMin,
},
CheckRequestReferer: true,
},
SiteConfig: SiteConfig{
FirstPage: []string{"index.html", "firstrun.html", "1.html"},
@ -48,7 +49,8 @@ var (
ThreadsPerPage: 20,
RepliesOnBoardPage: 3,
StickyRepliesOnBoardPage: 1,
CyclicalThreadNumPosts: 500,
EnableCyclicThreads: true,
CyclicThreadNumPosts: 500,
BanMessage: "USER WAS BANNED FOR THIS POST",
EmbedWidth: 200,
EmbedHeight: 164,

View file

@ -13,18 +13,18 @@ var (
InvalidArgumentErrorStr = "invalid argument(s) passed to event %q"
)
type EventHandler func(string, ...interface{}) error
type EventHandler func(string, ...any) error
// RegisterEvent registers a new event handler to be called when any of the elements of triggers are passed
// to TriggerEvent
func RegisterEvent(triggers []string, handler func(trigger string, i ...interface{}) error) {
func RegisterEvent(triggers []string, handler func(trigger string, i ...any) error) {
for _, t := range triggers {
registeredEvents[t] = append(registeredEvents[t], handler)
}
}
// TriggerEvent triggers the event handler registered to trigger
func TriggerEvent(trigger string, data ...interface{}) (handled bool, err error, recovered bool) {
func TriggerEvent(trigger string, data ...any) (handled bool, err error, recovered bool) {
errEv := gcutil.LogError(nil).Caller(1)
defer func() {
if a := recover(); a != nil {

View file

@ -7,7 +7,7 @@ import (
)
func TestPanicRecover(t *testing.T) {
RegisterEvent([]string{"TestPanicRecoverEvt"}, func(tr string, i ...interface{}) error {
RegisterEvent([]string{"TestPanicRecoverEvt"}, func(_ string, i ...any) error {
t.Log("Testing panic recover")
t.Log(i[0])
return nil
@ -21,7 +21,7 @@ func TestPanicRecover(t *testing.T) {
}
func TestEventEditValue(t *testing.T) {
RegisterEvent([]string{"TestEventEditValue"}, func(tr string, i ...interface{}) error {
RegisterEvent([]string{"TestEventEditValue"}, func(_ string, i ...any) error {
p := i[0].(*int)
*p += 1
return nil
@ -35,7 +35,7 @@ func TestEventEditValue(t *testing.T) {
func TestMultipleEventTriggers(t *testing.T) {
triggered := map[string]bool{}
RegisterEvent([]string{"a", "b"}, func(tr string, i ...interface{}) error {
RegisterEvent([]string{"a", "b"}, func(tr string, _ ...any) error {
triggered[tr] = true
return nil
})

View file

@ -9,7 +9,7 @@ import (
)
func luaEventRegisterHandlerAdapter(l *lua.LState, fn *lua.LFunction) EventHandler {
return func(trigger string, data ...interface{}) error {
return func(trigger string, data ...any) error {
args := []lua.LValue{
luar.New(l, trigger),
}
@ -35,7 +35,7 @@ func PreloadModule(l *lua.LState) int {
"register_event": func(l *lua.LState) int {
table := l.CheckTable(-2)
var triggers []string
table.ForEach(func(i, val lua.LValue) {
table.ForEach(func(_, val lua.LValue) {
triggers = append(triggers, val.String())
})
fn := l.CheckFunction(-1)
@ -45,7 +45,7 @@ func PreloadModule(l *lua.LState) int {
"trigger_event": func(l *lua.LState) int {
trigger := l.CheckString(1)
numArgs := l.GetTop()
var data []interface{}
var data []any
for i := 2; i <= numArgs; i++ {
v := l.CheckAny(i)
data = append(data, luautil.LValueToInterface(l, v))

Some files were not shown because too many files have changed in this diff Show more