1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-02 10:56:25 -07:00

refactor configuration

This commit is contained in:
Eggbertx 2021-08-01 22:49:53 -07:00
commit 225d7fb6d3
47 changed files with 1130 additions and 707 deletions

View file

@ -1,5 +1,8 @@
Gochan Gochan
======= =======
**NOTE**: I am currently working on refactoring configuration, use `master` for non-broken code until this branch is merged into it.
A semi-standalone imageboard server written in Go A semi-standalone imageboard server written in Go
Gochan works in a manner similar to Kusaba X, Tinyboard and others. As such, Gochan generates static HTML files which can optionally be served by a separate web server. Gochan works in a manner similar to Kusaba X, Tinyboard and others. As such, Gochan generates static HTML files which can optionally be served by a separate web server.
@ -17,11 +20,7 @@ Demo installation: https://gochan.org
5. Go to http://[gochan url]/manage?action=staff, log in (default username/password is admin/password), and create a new admin user (and any other staff users as necessary). Then delete the admin user for security. 5. Go to http://[gochan url]/manage?action=staff, log in (default username/password is admin/password), and create a new admin user (and any other staff users as necessary). Then delete the admin user for security.
## Configuration ## Configuration
1. Make sure to set `DBtype`, `DBhost`, `DBname`, `DBusername`, and `DBpassword`, since these are required to connect to your SQL database. Valid `DBtype` values are "mysql" and "postgres" (sqlite3 is no longer supported for stability reasons). See [config.md](config.md)
1. To connect to a MySQL database, set `DBhost` to "ip:3306" or a different port, if necessary.
2. To connect to a PostgreSQL database, set `DBhost` to the IP address or hostname. Using a UNIX socket may work as well, but it is currently untested.
2. Set `DomainRegex`,`SiteDomain`, since these are necessary in order to post and log in as a staff member.
3. If you want to see debugging info/noncritical warnings, set verbosity to 1.
## Installation using Docker ## Installation using Docker
See [`docker/README.md`](docker/README.md) See [`docker/README.md`](docker/README.md)

View file

@ -12,9 +12,10 @@ const (
//Entry runs all the migration logic until the database matches the given database version //Entry runs all the migration logic until the database matches the given database version
func Entry(targetVersion int) error { func Entry(targetVersion int) error {
cfg := config.GetSystemCriticalConfig()
gcsql.ConnectToDB( gcsql.ConnectToDB(
config.Config.DBhost, config.Config.DBtype, config.Config.DBname, cfg.DBhost, cfg.DBtype, cfg.DBname,
config.Config.DBusername, config.Config.DBpassword, config.Config.DBprefix) cfg.DBusername, cfg.DBpassword, cfg.DBprefix)
return runMigration(targetVersion) return runMigration(targetVersion)
} }
@ -24,6 +25,7 @@ func runMigration(targetVersion int) error {
if err != nil { if err != nil {
return err return err
} }
criticalCfg := config.GetSystemCriticalConfig()
switch dbFlags { switch dbFlags {
case gcsql.DBCorrupted: case gcsql.DBCorrupted:
gclog.Println(stdFatalFlag, "Database found is corrupted, please contact the devs.") gclog.Println(stdFatalFlag, "Database found is corrupted, please contact the devs.")
@ -35,7 +37,7 @@ func runMigration(targetVersion int) error {
return err return err
} }
gclog.Println(gclog.LStdLog, "Migrating pre april 2020 version to version 1 of modern system.") gclog.Println(gclog.LStdLog, "Migrating pre april 2020 version to version 1 of modern system.")
if err = migratePreApril2020Database(config.Config.DBtype); err != nil { if err = migratePreApril2020Database(criticalCfg.DBtype); err != nil {
return err return err
} }
gclog.Println(gclog.LStdLog, "Finish migrating to version 1.") gclog.Println(gclog.LStdLog, "Finish migrating to version 1.")

View file

@ -24,7 +24,7 @@ func (me *MigrationError) OldChanType() string {
func (me *MigrationError) Error() string { func (me *MigrationError) Error() string {
from := me.oldChanType from := me.oldChanType
if from != "" { if from != "" {
from = " from " + from + " " from = " from " + from
} }
return "unable to migrate" + from + ": " + me.errMessage return "unable to migrate" + from + ": " + me.errMessage
} }
@ -34,13 +34,14 @@ func NewMigrationError(oldChanType string, errMessage string) *MigrationError {
} }
type DBOptions struct { type DBOptions struct {
Host string Host string `json:"dbhost"`
DBType string DBType string `json:"dbtype"`
Username string Username string `json:"dbusername"`
Password string Password string `json:"dbpassword"`
OldDBName string OldDBName string `json:"olddbname"`
OldChanType string OldChanType string `json:"oldchan"`
NewDBName string NewDBName string `json:"newdbname"`
TablePrefix string `json:"tableprefix"`
} }
// DBMigrator is used for handling the migration from one database type to a // DBMigrator is used for handling the migration from one database type to a

View file

@ -0,0 +1,47 @@
package common
import (
"fmt"
"io/ioutil"
"regexp"
"strings"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gcutil"
)
var (
commentRemover = regexp.MustCompile("--.*\n?")
)
func RunSQLFile(path string, db *gcsql.GCDB) error {
sqlBytes, err := ioutil.ReadFile(path)
if err != nil {
return err
}
sqlStr := commentRemover.ReplaceAllString(string(sqlBytes), " ")
sqlArr := strings.Split(sqlStr, ";")
for _, statement := range sqlArr {
statement = strings.Trim(statement, " \n\r\t")
if len(statement) > 0 {
if _, err = db.ExecSQL(statement); err != nil {
return err
}
}
}
return nil
}
func InitDB(initFile string, db *gcsql.GCDB) error {
filePath := gcutil.FindResource(initFile,
"/usr/local/share/gochan/"+initFile,
"/usr/share/gochan/"+initFile)
if filePath == "" {
return fmt.Errorf(
"SQL database initialization file (%s) missing. Please reinstall gochan-migration", initFile)
}
return RunSQLFile(filePath, db)
}

View file

@ -3,9 +3,19 @@ package pre2021
import ( import (
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common" "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/gcsql"
) )
const (
// check to see if the old db exists, if the new db exists, and the number of tables
// in the new db
dbInfoSQL = `SELECT
(SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?) AS olddb,
(SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?) as newdb,
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?) as num_tables`
)
type Pre2021Migrator struct { type Pre2021Migrator struct {
db *gcsql.GCDB db *gcsql.GCDB
options common.DBOptions options common.DBOptions
@ -15,12 +25,39 @@ func (m *Pre2021Migrator) Init(options common.DBOptions) error {
m.options = options m.options = options
var err error var err error
m.db, err = gcsql.Open( m.db, err = gcsql.Open(
m.options.Host, m.options.DBType, "", m.options.Username, m.options.Password, "", m.options.Host, m.options.DBType, "", m.options.Username,
) m.options.Password, options.TablePrefix)
return err return err
} }
func (m *Pre2021Migrator) MigrateDB() error { func (m *Pre2021Migrator) MigrateDB() error {
chkDbStmt, err := m.db.PrepareSQL(dbInfoSQL)
if err != nil {
return err
}
defer chkDbStmt.Close()
var olddb []byte
var newdb []byte
var numTables int
if err = chkDbStmt.QueryRow(m.options.OldDBName, m.options.NewDBName, m.options.NewDBName).Scan(&olddb, &newdb, &numTables); err != nil {
return common.NewMigrationError("pre2021", err.Error())
}
if olddb == nil {
return common.NewMigrationError("pre2021", "old database doesn't exist")
}
if newdb == nil {
return common.NewMigrationError("pre2021", "new database doesn't exist")
}
if numTables > 0 {
return common.NewMigrationError("pre2021", "new database must be empty")
}
gcsql.ConnectToDB(
m.options.Host, m.options.DBType, m.options.NewDBName,
m.options.Username, m.options.Password, m.options.TablePrefix)
cfg := config.GetSystemCriticalConfig()
gcsql.CheckAndInitializeDatabase(cfg.DBtype)
return nil return nil
} }
@ -28,5 +65,5 @@ func (m *Pre2021Migrator) Close() error {
if m.db != nil { if m.db != nil {
return m.db.Close() return m.db.Close()
} }
return nil return gcsql.Close()
} }

View file

@ -1,15 +1,17 @@
package main package main
import ( import (
"bufio"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common" "github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/kusabax" "github.com/gochan-org/gochan/cmd/gochan-migration/internal/kusabax"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/pre2021" "github.com/gochan-org/gochan/cmd/gochan-migration/internal/pre2021"
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/tinyboard" "github.com/gochan-org/gochan/cmd/gochan-migration/internal/tinyboard"
"github.com/gochan-org/gochan/pkg/config"
) )
const ( const (
@ -23,6 +25,7 @@ the README and/or the -h command line flag before you use it.
var ( var (
versionStr string versionStr string
bufIn = bufio.NewReader(os.Stdin)
) )
func fatalPrintln(args ...interface{}) { func fatalPrintln(args ...interface{}) {
@ -30,17 +33,43 @@ func fatalPrintln(args ...interface{}) {
os.Exit(1) os.Exit(1)
} }
func readConfig(filename string, options *common.DBOptions) {
ba, err := ioutil.ReadFile(filename)
if err != nil {
fatalPrintln(err)
return
}
if err = json.Unmarshal(ba, options); err != nil {
fatalPrintln(err)
}
}
func main() { func main() {
var options common.DBOptions var options common.DBOptions
var migrationConfigFile string
flag.StringVar(&migrationConfigFile, "migrationconfig", "", "a JSON file to use for supplying the required migration information (ignores all other set arguments if used)")
flag.StringVar(&options.OldChanType, "oldchan", "", "The imageboard we are migrating from (currently only pre2021 is supported, but more are coming") flag.StringVar(&options.OldChanType, "oldchan", "", "The imageboard we are migrating from (currently only pre2021 is supported, but more are coming")
flag.StringVar(&options.Host, "dbhost", "", "The database host or socket file to connect to") flag.StringVar(&options.Host, "dbhost", "", "The database host or socket file to connect to")
flag.StringVar(&options.DBType, "dbtype", "mysql", "The kind of database server we are connecting to (currently only mysql is supported)") flag.StringVar(&options.DBType, "dbtype", "mysql", "The kind of database server we are connecting to (currently only mysql is supported)")
flag.StringVar(&options.Username, "dbusername", "", "The database username") flag.StringVar(&options.Username, "dbusername", "", "The database username")
flag.StringVar(&options.Password, "dbpassword", "", "The database password (if required)") flag.StringVar(&options.Password, "dbpassword", "", "The database password (if required by SQL account)")
flag.StringVar(&options.OldDBName, "olddbname", "", "The name of the old database") flag.StringVar(&options.OldDBName, "olddbname", "", "The name of the old database")
flag.StringVar(&options.NewDBName, "newdbname", "", "The name of the new database")
flag.StringVar(&options.TablePrefix, "tableprefix", "", "Prefix for the SQL tables' names")
flag.Parse() flag.Parse()
if migrationConfigFile != "" {
readConfig(migrationConfigFile, &options)
}
if options.OldChanType == "" || options.Host == "" || options.DBType == "" || options.Username == "" || options.OldDBName == "" || options.NewDBName == "" {
flag.PrintDefaults()
fmt.Println("Missing required database connection info")
os.Exit(1)
return
}
fmt.Printf(banner, versionStr) fmt.Printf(banner, versionStr)
var migrator common.DBMigrator var migrator common.DBMigrator
@ -61,18 +90,17 @@ func main() {
} }
defer migrator.Close() defer migrator.Close()
config.InitConfig(versionStr) // config.InitConfig(versionStr)
/* gclog.Printf(gclog.LStdLog, "Starting gochan migration (gochan v%s)", versionStr) /* gclog.Printf(gclog.LStdLog, "Starting gochan migration (gochan v%s)", versionStr)
err := gcmigrate.Entry(1) //TEMP, get correct database version from command line or some kind of table. 1 Is the current version we are working towards err := gcmigrate.Entry(1) //TEMP, get correct database version from command line or some kind of table. 1 Is the current version we are working towards
if err != nil { if err != nil {
gclog.Printf(gclog.LErrorLog, "Error while migrating: %s", err) gclog.Printf(gclog.LErrorLog, "Error while migrating: %s", err)
} */ } */
if options.OldDBName == config.Config.DBname { if options.OldDBName == options.NewDBName {
fatalPrintln( fatalPrintln("The old database name must not be the same as the new one.")
"The old database name must not be the same as the new one set in gochan.json")
} }
if err = migrator.MigrateDB(); err != nil { if err = migrator.MigrateDB(); err != nil {
fatalPrintln("Error migrating database:", err) fatalPrintln(err)
} }
fmt.Println("Database migration successful!") fmt.Println("Database migration successful!")
} }

View file

@ -34,10 +34,12 @@ func main() {
gclog.Printf(gclog.LStdLog, "Starting gochan v%s", versionStr) gclog.Printf(gclog.LStdLog, "Starting gochan v%s", versionStr)
config.InitConfig(versionStr) config.InitConfig(versionStr)
systemCritical := config.GetSystemCriticalConfig()
gcsql.ConnectToDB( gcsql.ConnectToDB(
config.Config.DBhost, config.Config.DBtype, config.Config.DBname, systemCritical.DBhost, systemCritical.DBtype, systemCritical.DBname,
config.Config.DBusername, config.Config.DBpassword, config.Config.DBprefix) systemCritical.DBusername, systemCritical.DBpassword, systemCritical.DBprefix)
gcsql.CheckAndInitializeDatabase(config.Config.DBtype) gcsql.CheckAndInitializeDatabase(systemCritical.DBtype)
parseCommandLine() parseCommandLine()
serverutil.InitMinifier() serverutil.InitMinifier()

View file

@ -32,7 +32,11 @@ type gochanServer struct {
} }
func (s gochanServer) serveFile(writer http.ResponseWriter, request *http.Request) { func (s gochanServer) serveFile(writer http.ResponseWriter, request *http.Request) {
filePath := path.Join(config.Config.DocumentRoot, request.URL.Path)
systemCritical := config.GetSystemCriticalConfig()
siteConfig := config.GetSiteConfig()
filePath := path.Join(systemCritical.DocumentRoot, request.URL.Path)
var fileBytes []byte var fileBytes []byte
results, err := os.Stat(filePath) results, err := os.Stat(filePath)
if err != nil { if err != nil {
@ -46,7 +50,7 @@ func (s gochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
if results.IsDir() { if results.IsDir() {
//check to see if one of the specified index pages exists //check to see if one of the specified index pages exists
var found bool var found bool
for _, value := range config.Config.FirstPage { for _, value := range siteConfig.FirstPage {
newPath := path.Join(filePath, value) newPath := path.Join(filePath, value)
_, err := os.Stat(newPath) _, err := os.Stat(newPath)
if err == nil { if err == nil {
@ -103,9 +107,9 @@ func (s gochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
} }
func (s gochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) { func (s gochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
systemCritical := config.GetSystemCriticalConfig()
for name, namespaceFunction := range s.namespaces { for name, namespaceFunction := range s.namespaces {
if request.URL.Path == config.Config.SiteWebfolder+name { if request.URL.Path == systemCritical.WebRoot+name {
// writer.WriteHeader(200)
namespaceFunction(writer, request) namespaceFunction(writer, request)
return return
} }
@ -114,17 +118,24 @@ func (s gochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Reques
} }
func initServer() { func initServer() {
listener, err := net.Listen("tcp", config.Config.ListenIP+":"+strconv.Itoa(config.Config.Port)) systemCritical := config.GetSystemCriticalConfig()
siteConfig := config.GetSiteConfig()
listener, err := net.Listen("tcp", systemCritical.ListenIP+":"+strconv.Itoa(systemCritical.Port))
if err != nil { if err != nil {
gclog.Printf(gclog.LErrorLog|gclog.LStdLog|gclog.LFatal, gclog.Printf(gclog.LErrorLog|gclog.LStdLog|gclog.LFatal,
"Failed listening on %s:%d: %s", config.Config.ListenIP, config.Config.Port, err.Error()) "Failed listening on %s:%d: %s", systemCritical.ListenIP, systemCritical.Port, err.Error())
} }
server = new(gochanServer) server = new(gochanServer)
server.namespaces = make(map[string]func(http.ResponseWriter, *http.Request)) server.namespaces = make(map[string]func(http.ResponseWriter, *http.Request))
// Check if Akismet API key is usable at startup. // Check if Akismet API key is usable at startup.
if err = serverutil.CheckAkismetAPIKey(config.Config.AkismetAPIKey); err != nil { err = serverutil.CheckAkismetAPIKey(siteConfig.AkismetAPIKey)
config.Config.AkismetAPIKey = "" if err == serverutil.ErrBlankAkismetKey {
gclog.Print(gclog.LErrorLog, err.Error(), ". Akismet spam protection won't be used.")
} else if err != nil {
gclog.Print(gclog.LErrorLog|gclog.LAccessLog, ". Akismet spam protection will be disabled.")
siteConfig.AkismetAPIKey = ""
} }
server.namespaces["banned"] = posting.BanHandler server.namespaces["banned"] = posting.BanHandler
@ -137,10 +148,10 @@ func initServer() {
http.Redirect(writer, request, "https://www.youtube.com/watch?v=dQw4w9WgXcQ", http.StatusFound) http.Redirect(writer, request, "https://www.youtube.com/watch?v=dQw4w9WgXcQ", http.StatusFound)
} }
} }
// Eventually plugins will be able to register new namespaces (assuming they ever get it working on Windows or macOS) // Eventually plugins will be able to register new namespaces or they will be restricted to something
// or they will be restricted to something like /plugin // like /plugin
if config.Config.UseFastCGI { if systemCritical.UseFastCGI {
err = fcgi.Serve(listener, server) err = fcgi.Serve(listener, server)
} else { } else {
err = http.Serve(listener, server) err = http.Serve(listener, server)
@ -163,10 +174,11 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
reportBtn := request.PostFormValue("report_btn") reportBtn := request.PostFormValue("report_btn")
editBtn := request.PostFormValue("edit_btn") editBtn := request.PostFormValue("edit_btn")
doEdit := request.PostFormValue("doedit") doEdit := request.PostFormValue("doedit")
systemCritical := config.GetSystemCriticalConfig()
if action == "" && deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit" && doEdit != "1" { if action == "" && deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit" && doEdit != "1" {
gclog.Printf(gclog.LAccessLog, "Received invalid /util request from %q", request.Host) gclog.Printf(gclog.LAccessLog, "Received invalid /util request from %q", request.Host)
http.Redirect(writer, request, path.Join(config.Config.SiteWebfolder, "/"), http.StatusFound) http.Redirect(writer, request, path.Join(systemCritical.WebRoot, "/"), http.StatusFound)
return return
} }
var postsArr []string var postsArr []string
@ -207,9 +219,11 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
} }
if err = gctemplates.PostEdit.Execute(writer, map[string]interface{}{ if err = gctemplates.PostEdit.Execute(writer, map[string]interface{}{
"config": config.Config, "systemCritical": config.GetSystemCriticalConfig(),
"post": post, "siteConfig": config.GetSiteConfig(),
"referrer": request.Referer(), "boardConfig": config.GetBoardConfig(""),
"post": post,
"referrer": request.Referer(),
}); err != nil { }); err != nil {
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog, serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
"Error executing edit post template: ", err.Error())) "Error executing edit post template: ", err.Error()))
@ -325,7 +339,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
} }
if post.ParentID == 0 { if post.ParentID == 0 {
os.Remove(path.Join( os.Remove(path.Join(
config.Config.DocumentRoot, board, "/res/"+strconv.Itoa(post.ID)+".html")) systemCritical.DocumentRoot, board, "/res/"+strconv.Itoa(post.ID)+".html"))
} else { } else {
_board, _ := gcsql.GetBoardFromID(post.BoardID) _board, _ := gcsql.GetBoardFromID(post.BoardID)
building.BuildBoardPages(&_board) building.BuildBoardPages(&_board)

37
config.md Normal file
View file

@ -0,0 +1,37 @@
# gochan configuration
See [gochan.example.json](sample-configs/gochan.example.json) for an example gochan.json.
## Server-critical stuff
* You'll need to edit some of the values (like `ListenIP` and `UseFastCGI` based on your system's setup. For an example nginx configuration, see [gochan-fastcgi.nginx](sample-configs/gochan-fastcgi.nginx) for FastCGI and [gochan-http.nginx](sample-configs/gochan-http.nginx) for passing through HTTP.
* `DocumentRoot` refers to the root directory on your filesystem where gochan will look for requested files.
* `TemplateDir` refers to the directory where gochan will load the templates from.
* `LogDir` refers to the directory where gochan will write the logs to.
**Make sure gochan has read-write permission for `DocumentRoot` and `LogDir` and read permission for `TemplateDir`**
## Database configuration
Valid `DBtype` values are "mysql" and "postgres" (sqlite3 is no longer supported for stability reasons, though that may or may not come back).
1. To connect to a MySQL database, set `DBhost` to "x.x.x.x:3306" (replacing x.x.x.x with your database server's IP or domain) or a different port, if necessary. You can also use a UNIX socket if you have it set up, like "unix(/var/run/mysqld/mysqld.sock)".
2. To connect to a PostgreSQL database, set `DBhost` to the IP address or hostname. Using a UNIX socket may work as well, but it is currently untested.
3. Set `SiteDomain`, since these are necessary in order to post and log in as a staff member.
3. If you want to see debugging info/noncritical warnings, set verbosity to 1.
4. If `DBprefix` is set (not required), all gochan table names will be prefixed with the `DBprefix` value. Once you run gochan for the first time, you really shouldn't edit this value, since gochan will assume the tables are missing.
## Website configuration
* `SiteName` is used for the name displayed on the home page.
* `SiteSlogan` is used for the slogan (if set) on the home page.
* `SiteDomain` is used for links throughout the site.
* `WebRoot` is used as the prefix for boards, files, and pretty much everything on the site. If it isn't set, "/" will be used.
## Styles
* `Styles` is an array, with each element representing a theme selectable by the user from the frontend settings screen. Each element should have `Name` string value and a `Filename` string value. Example:
```JSON
"Styles": [
{ "Name": "Pipes", "Filename": "pipes.css" }
]
```
* If `DefaultStyle` is not set, the first element in `Styles` will be used.
## Misc
* `ReservedTrips` is used for reserving secure tripcodes. It should be an array of strings. For example, if you have `abcd##ABCD` and someone posts with the name ##abcd, their name will instead show up as !!ABCD on the site.
* `BanColors` is used for the color of the text set by `BanMessage`, and can be used for setting per-user colors, if desired. It should be a string array, with each element being of the form `"username:color"`, where color is a valid HTML color (#000A0, green, etc) and username is the staff member who set the ban. If a color isn't set for the user, the style will be used to set the color.

View file

@ -73,14 +73,15 @@ func BuildBoardPages(board *gcsql.Board) error {
thread.OP = op thread.OP = op
var numRepliesOnBoardPage int var numRepliesOnBoardPage int
// postCfg := config.getpo
postCfg := config.GetBoardConfig("").PostConfig
if op.Stickied { if op.Stickied {
// If the thread is stickied, limit replies on the archive page to the // If the thread is stickied, limit replies on the archive page to the
// configured value for stickied threads. // configured value for stickied threads.
numRepliesOnBoardPage = config.Config.StickyRepliesOnBoardPage numRepliesOnBoardPage = postCfg.StickyRepliesOnBoardPage
} else { } else {
// Otherwise, limit the replies to the configured value for normal threads. // Otherwise, limit the replies to the configured value for normal threads.
numRepliesOnBoardPage = config.Config.RepliesOnBoardPage numRepliesOnBoardPage = postCfg.RepliesOnBoardPage
} }
postsInThread, err = gcsql.GetExistingRepliesLimitedRev(op.ID, numRepliesOnBoardPage) postsInThread, err = gcsql.GetExistingRepliesLimitedRev(op.ID, numRepliesOnBoardPage)
@ -118,8 +119,8 @@ func BuildBoardPages(board *gcsql.Board) error {
nonStickiedThreads = append(nonStickiedThreads, thread) nonStickiedThreads = append(nonStickiedThreads, thread)
} }
} }
criticalCfg := config.GetSystemCriticalConfig()
gcutil.DeleteMatchingFiles(path.Join(config.Config.DocumentRoot, board.Dir), "\\d.html$") gcutil.DeleteMatchingFiles(path.Join(criticalCfg.DocumentRoot, board.Dir), "\\d.html$")
// Order the threads, stickied threads first, then nonstickied threads. // Order the threads, stickied threads first, then nonstickied threads.
threads = append(stickiedThreads, nonStickiedThreads...) threads = append(stickiedThreads, nonStickiedThreads...)
@ -129,7 +130,7 @@ func BuildBoardPages(board *gcsql.Board) error {
board.CurrentPage = 1 board.CurrentPage = 1
// Open 1.html for writing to the first page. // Open 1.html for writing to the first page.
boardPageFile, err = os.OpenFile(path.Join(config.Config.DocumentRoot, board.Dir, "1.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777) boardPageFile, err = os.OpenFile(path.Join(criticalCfg.DocumentRoot, board.Dir, "1.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil { if err != nil {
return errors.New(gclog.Printf(gclog.LErrorLog, return errors.New(gclog.Printf(gclog.LErrorLog,
"Failed opening /%s/board.html: %s", "Failed opening /%s/board.html: %s",
@ -139,7 +140,7 @@ func BuildBoardPages(board *gcsql.Board) error {
// Render board page template to the file, // Render board page template to the file,
// packaging the board/section list, threads, and board info // packaging the board/section list, threads, and board info
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{ if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
"config": config.Config, "webroot": criticalCfg.WebRoot,
"boards": gcsql.AllBoards, "boards": gcsql.AllBoards,
"sections": gcsql.AllSections, "sections": gcsql.AllSections,
"threads": threads, "threads": threads,
@ -152,13 +153,14 @@ func BuildBoardPages(board *gcsql.Board) error {
} }
// Create the archive pages. // Create the archive pages.
threadPages = paginate(config.Config.ThreadsPerPage, threads) boardCfg := config.GetBoardConfig(board.Dir)
threadPages = paginate(boardCfg.ThreadsPerPage, threads)
board.NumPages = len(threadPages) board.NumPages = len(threadPages)
// Create array of page wrapper objects, and open the file. // Create array of page wrapper objects, and open the file.
pagesArr := make([]map[string]interface{}, board.NumPages) pagesArr := make([]map[string]interface{}, board.NumPages)
catalogJSONFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, board.Dir, "catalog.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777) catalogJSONFile, err := os.OpenFile(path.Join(criticalCfg.DocumentRoot, board.Dir, "catalog.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil { if err != nil {
return errors.New(gclog.Printf(gclog.LErrorLog, return errors.New(gclog.Printf(gclog.LErrorLog,
"Failed opening /%s/catalog.json: %s", board.Dir, err.Error())) "Failed opening /%s/catalog.json: %s", board.Dir, err.Error()))
@ -170,7 +172,7 @@ func BuildBoardPages(board *gcsql.Board) error {
board.CurrentPage++ board.CurrentPage++
var currentPageFilepath string var currentPageFilepath string
pageFilename := strconv.Itoa(board.CurrentPage) + ".html" pageFilename := strconv.Itoa(board.CurrentPage) + ".html"
currentPageFilepath = path.Join(config.Config.DocumentRoot, board.Dir, pageFilename) currentPageFilepath = path.Join(criticalCfg.DocumentRoot, board.Dir, pageFilename)
currentPageFile, err = os.OpenFile(currentPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777) currentPageFile, err = os.OpenFile(currentPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil { if err != nil {
err = errors.New(gclog.Printf(gclog.LErrorLog, err = errors.New(gclog.Printf(gclog.LErrorLog,
@ -181,7 +183,7 @@ func BuildBoardPages(board *gcsql.Board) error {
// Render the boardpage template // Render the boardpage template
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{ if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
"config": config.Config, "webroot": criticalCfg.WebRoot,
"boards": gcsql.AllBoards, "boards": gcsql.AllBoards,
"sections": gcsql.AllSections, "sections": gcsql.AllSections,
"threads": pageThreads, "threads": pageThreads,
@ -259,8 +261,8 @@ func BuildCatalog(boardID int) string {
if err = board.PopulateData(boardID); err != nil { if err = board.PopulateData(boardID); err != nil {
return gclog.Printf(gclog.LErrorLog, "Error getting board information (ID: %d)", boardID) return gclog.Printf(gclog.LErrorLog, "Error getting board information (ID: %d)", boardID)
} }
criticalCfg := config.GetSystemCriticalConfig()
catalogPath := path.Join(config.Config.DocumentRoot, board.Dir, "catalog.html") catalogPath := path.Join(criticalCfg.DocumentRoot, board.Dir, "catalog.html")
catalogFile, err := os.OpenFile(catalogPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777) catalogFile, err := os.OpenFile(catalogPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil { if err != nil {
return gclog.Printf(gclog.LErrorLog, return gclog.Printf(gclog.LErrorLog,
@ -285,7 +287,7 @@ func BuildCatalog(boardID int) string {
if err = serverutil.MinifyTemplate(gctemplates.Catalog, map[string]interface{}{ if err = serverutil.MinifyTemplate(gctemplates.Catalog, map[string]interface{}{
"boards": gcsql.AllBoards, "boards": gcsql.AllBoards,
"config": config.Config, "webroot": criticalCfg.WebRoot,
"board": board, "board": board,
"sections": gcsql.AllSections, "sections": gcsql.AllSections,
"threads": threadInterfaces, "threads": threadInterfaces,

View file

@ -20,8 +20,9 @@ func BuildFrontPage() error {
return errors.New(gclog.Print(gclog.LErrorLog, return errors.New(gclog.Print(gclog.LErrorLog,
"Error loading front page template: ", err.Error())) "Error loading front page template: ", err.Error()))
} }
os.Remove(path.Join(config.Config.DocumentRoot, "index.html")) criticalCfg := config.GetSystemCriticalConfig()
frontFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777) os.Remove(path.Join(criticalCfg.DocumentRoot, "index.html"))
frontFile, err := os.OpenFile(path.Join(criticalCfg.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil { if err != nil {
return errors.New(gclog.Print(gclog.LErrorLog, return errors.New(gclog.Print(gclog.LErrorLog,
@ -30,7 +31,8 @@ func BuildFrontPage() error {
defer frontFile.Close() defer frontFile.Close()
var recentPostsArr []gcsql.RecentPost var recentPostsArr []gcsql.RecentPost
recentPostsArr, err = gcsql.GetRecentPostsGlobal(config.Config.MaxRecentPosts, !config.Config.RecentPostsWithNoFile) siteCfg := config.GetSiteConfig()
recentPostsArr, err = gcsql.GetRecentPostsGlobal(siteCfg.MaxRecentPosts, !siteCfg.RecentPostsWithNoFile)
if err != nil { if err != nil {
return errors.New(gclog.Print(gclog.LErrorLog, return errors.New(gclog.Print(gclog.LErrorLog,
"Failed loading recent posts: "+err.Error())) "Failed loading recent posts: "+err.Error()))
@ -43,9 +45,11 @@ func BuildFrontPage() error {
} }
if err = serverutil.MinifyTemplate(gctemplates.FrontPage, map[string]interface{}{ if err = serverutil.MinifyTemplate(gctemplates.FrontPage, map[string]interface{}{
"config": config.Config, "webroot": criticalCfg.WebRoot,
"site_config": siteCfg,
"sections": gcsql.AllSections, "sections": gcsql.AllSections,
"boards": gcsql.AllBoards, "boards": gcsql.AllBoards,
"board_config": config.GetBoardConfig(""),
"recent_posts": recentPostsArr, "recent_posts": recentPostsArr,
}, frontFile, "text/html"); err != nil { }, frontFile, "text/html"); err != nil {
return errors.New(gclog.Print(gclog.LErrorLog, return errors.New(gclog.Print(gclog.LErrorLog,
@ -56,7 +60,8 @@ func BuildFrontPage() error {
// BuildBoardListJSON generates a JSON file with info about the boards // BuildBoardListJSON generates a JSON file with info about the boards
func BuildBoardListJSON() error { func BuildBoardListJSON() error {
boardListFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, "boards.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777) criticalCfg := config.GetSystemCriticalConfig()
boardListFile, err := os.OpenFile(path.Join(criticalCfg.DocumentRoot, "boards.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil { if err != nil {
return errors.New( return errors.New(
gclog.Print(gclog.LErrorLog, "Failed opening boards.json for writing: ", err.Error())) gclog.Print(gclog.LErrorLog, "Failed opening boards.json for writing: ", err.Error()))
@ -67,11 +72,12 @@ func BuildBoardListJSON() error {
"boards": []gcsql.Board{}, "boards": []gcsql.Board{},
} }
boardCfg := config.GetBoardConfig("")
// Our cooldowns are site-wide currently. // Our cooldowns are site-wide currently.
cooldowns := gcsql.BoardCooldowns{ cooldowns := gcsql.BoardCooldowns{
NewThread: config.Config.NewThreadDelay, NewThread: boardCfg.NewThreadDelay,
Reply: config.Config.ReplyDelay, Reply: boardCfg.ReplyDelay,
ImageReply: config.Config.ReplyDelay} ImageReply: boardCfg.ReplyDelay}
for b := range gcsql.AllBoards { for b := range gcsql.AllBoards {
gcsql.AllBoards[b].Cooldowns = cooldowns gcsql.AllBoards[b].Cooldowns = cooldowns
@ -99,7 +105,10 @@ func BuildJS() error {
return errors.New(gclog.Println(gclog.LErrorLog, return errors.New(gclog.Println(gclog.LErrorLog,
"Error loading consts.js template:", err.Error())) "Error loading consts.js template:", err.Error()))
} }
constsJSPath := path.Join(config.Config.DocumentRoot, "js", "consts.js")
boardCfg := config.GetBoardConfig("")
criticalCfg := config.GetSystemCriticalConfig()
constsJSPath := path.Join(criticalCfg.DocumentRoot, "js", "consts.js")
constsJSFile, err := os.OpenFile(constsJSPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) constsJSFile, err := os.OpenFile(constsJSPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return errors.New(gclog.Printf(gclog.LErrorLog, return errors.New(gclog.Printf(gclog.LErrorLog,
@ -107,7 +116,14 @@ func BuildJS() error {
} }
defer constsJSFile.Close() defer constsJSFile.Close()
if err = serverutil.MinifyTemplate(gctemplates.JsConsts, config.Config, constsJSFile, "text/javascript"); err != nil { if err = serverutil.MinifyTemplate(gctemplates.JsConsts,
map[string]interface{}{
"webroot": criticalCfg.WebRoot,
"styles": boardCfg.Styles,
"default_style": boardCfg.DefaultStyle,
"timezone": criticalCfg.TimeZone,
},
constsJSFile, "text/javascript"); err != nil {
return errors.New(gclog.Printf(gclog.LErrorLog, return errors.New(gclog.Printf(gclog.LErrorLog,
"Error building %q: %s", constsJSPath, err.Error())) "Error building %q: %s", constsJSPath, err.Error()))
} }

View file

@ -58,14 +58,15 @@ func BuildThreadPages(op *gcsql.Post) error {
return errors.New(gclog.Printf(gclog.LErrorLog, return errors.New(gclog.Printf(gclog.LErrorLog,
"Error building thread %d: %s", op.ID, err.Error())) "Error building thread %d: %s", op.ID, err.Error()))
} }
os.Remove(path.Join(config.Config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html")) criticalCfg := config.GetSystemCriticalConfig()
os.Remove(path.Join(criticalCfg.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html"))
var repliesInterface []interface{} var repliesInterface []interface{}
for _, reply := range replies { for _, reply := range replies {
repliesInterface = append(repliesInterface, reply) repliesInterface = append(repliesInterface, reply)
} }
threadPageFilepath := path.Join(config.Config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html") threadPageFilepath := path.Join(criticalCfg.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html")
threadPageFile, err = os.OpenFile(threadPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777) threadPageFile, err = os.OpenFile(threadPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil { if err != nil {
return errors.New(gclog.Printf(gclog.LErrorLog, return errors.New(gclog.Printf(gclog.LErrorLog,
@ -74,7 +75,7 @@ func BuildThreadPages(op *gcsql.Post) error {
// render thread page // render thread page
if err = serverutil.MinifyTemplate(gctemplates.ThreadPage, map[string]interface{}{ if err = serverutil.MinifyTemplate(gctemplates.ThreadPage, map[string]interface{}{
"config": config.Config, "webroot": criticalCfg.WebRoot,
"boards": gcsql.AllBoards, "boards": gcsql.AllBoards,
"board": board, "board": board,
"sections": gcsql.AllSections, "sections": gcsql.AllSections,
@ -86,7 +87,7 @@ func BuildThreadPages(op *gcsql.Post) error {
} }
// Put together the thread JSON // Put together the thread JSON
threadJSONFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777) threadJSONFile, err := os.OpenFile(path.Join(criticalCfg.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil { if err != nil {
return errors.New(gclog.Printf(gclog.LErrorLog, return errors.New(gclog.Printf(gclog.LErrorLog,
"Failed opening /%s/res/%d.json: %s", board.Dir, op.ID, err.Error())) "Failed opening /%s/res/%d.json: %s", board.Dir, op.ID, err.Error()))

View file

@ -12,155 +12,89 @@ import (
const ( const (
randomStringSize = 16 randomStringSize = 16
cookieMaxAgeEx = ` (example: "1 year 2 months 3 days 4 hours", or "1y2mo3d4h"` cookieMaxAgeEx = ` (example: "1 year 2 months 3 days 4 hours", or "1y2mo3d4h"`
/* currentConfig = iota
oldConfig
invalidConfig */
) )
var ( var (
Config *GochanConfig cfg *GochanConfig
cfgPath string cfgPath string
cfgDefaults = map[string]interface{}{ defaults = map[string]interface{}{
"Port": 8080, "WebRoot": "/",
"FirstPage": []string{"index.html", "board.html", "firstrun.html"}, // SiteConfig
"DocumentRoot": "html", "FirstPage": []string{"index.html", "firstrun.html", "1.html"},
"TemplateDir": "templates", "CookieMaxAge": "1y",
"CookieMaxAge": "1y", "LockdownMessage": "This imageboard has temporarily disabled posting. We apologize for the inconvenience",
"LogDir": "log", "SiteName": "Gochan",
"MinifyHTML": true,
"MinifyJS": true,
"MaxRecentPosts": 3,
"EnableAppeals": true,
"MaxLogDays": 14,
"SillyTags": []string{}, // BoardConfig
"DateTimeFormat": "Mon, January 02, 2006 15:04 PM",
"SiteName": "Gochan", "CaptchaWidth": 240,
"SiteWebFolder": "/", "CaptchaHeight": 80,
"CaptchaMinutesTimeout": 15,
"NewThreadDelay": 30,
"ReplyDelay": 7,
"MaxLineLength": 150,
"ThreadsPerPage": 15,
// PostConfig
"NewThreadDelay": 30,
"ReplyDelay": 7,
"MaxLineLength": 150,
"ThreadsPerPage": 15,
"PostsPerThreadPage": 50,
"RepliesOnBoardPage": 3, "RepliesOnBoardPage": 3,
"StickyRepliesOnBoardPage": 1, "StickyRepliesOnBoardPage": 1,
"BanMessage": "USER WAS BANNED FOR THIS POST",
"EmbedWidth": 200,
"EmbedHeight": 164,
"EnableEmbeds": true,
"ImagesOpenNewTab": true,
"NewTabOnOutlinks": true,
// UploadConfig
"ThumbWidth": 200, "ThumbWidth": 200,
"ThumbHeight": 200, "ThumbHeight": 200,
"ThumbWidthReply": 125, "ThumbWidthReply": 125,
"ThumbHeightReply": 125, "ThumbHeightReply": 125,
"ThumbWidthCatalog": 50, "ThumbWidthCatalog": 50,
"ThumbHeightCatalog": 50, "ThumbHeightCatalog": 50,
"BanMsg": "USER WAS BANNED FOR THIS POST",
"EmbedWidth": 200,
"EmbedHeight": 164,
"ExpandButton": true,
"NewTabOnOutlinks": true,
"MinifyHTML": true,
"MinifyJS": true,
"CaptchaWidth": 240,
"CaptchaHeight": 80,
"DateTimeFormat": "Mon, January 02, 2006 15:04 PM",
"CaptchaMinutesExpire": 15,
"EnableGeoIP": true,
"GeoIPDBlocation": "/usr/share/GeoIP/GeoIP.dat",
"MaxRecentPosts": 3,
"MaxLogDays": 15,
} }
boardConfigs = map[string]BoardConfig{}
) )
// Style represents a theme (Pipes, Dark, etc)
type Style struct {
Name string
Filename string
}
// GochanConfig stores important info and is read from/written to gochan.json.
// If a field has an entry in the defaults map, that value will be used here.
// If a field has a critical struct tag set to "true", a warning will be printed
// if it exists in the defaults map and an error will be printed if it doesn't.
type GochanConfig struct { type GochanConfig struct {
ListenIP string `critical:"true"` SystemCriticalConfig
Port int `critical:"true"` SiteConfig
FirstPage []string `critical:"true"` BoardConfig
Username string `critical:"true"` jsonLocation string `json:"-"`
UseFastCGI bool `critical:"true"`
DebugMode bool `description:"Disables several spam/browser checks that can cause problems when hosting an instance locally."`
CookieMaxAge string `description:"The amount of time that session cookies will exist before they expire (ex: 1y2mo3d4h or 1 year 2 months 3 days 4 hours). Default is 1 year"`
DocumentRoot string `critical:"true"`
TemplateDir string `critical:"true"`
LogDir string `critical:"true"`
DBtype string `critical:"true"`
DBhost string `critical:"true"`
DBname string `critical:"true"`
DBusername string `critical:"true"`
DBpassword string `critical:"true"`
DBprefix string `description:"Each table's name in the database will start with this, if it is set"`
SiteName string `description:"The name of the site that appears in the header of the front page."`
SiteSlogan string `description:"The text that appears below SiteName on the home page"`
SiteWebfolder string `critical:"true" description:"The HTTP root appearing in the browser (e.g. https://gochan.org/<SiteWebFolder>"`
SiteDomain string `critical:"true" description:"The server's domain. Do not edit this unless you know what you are doing or BAD THINGS WILL HAPPEN!"`
Lockdown bool `description:"Disables posting."`
LockdownMessage string `description:"Message displayed when someone tries to post while the site is on lockdown."`
Sillytags []string `description:"List of randomly selected fake staff tags separated by line, e.g. ## Mod, to be randomly assigned to posts if UseSillytags is checked. Don't include the \"## \""`
UseSillytags bool `description:"Use Sillytags"`
Modboard string `description:"A super secret clubhouse board that only staff can view/post to."`
Styles []Style `critical:"true" description:"List of styles (one per line) that should be accessed online at <SiteWebFolder>/css/<Style>"`
DefaultStyle string `description:"Filename of the default Style. If this unset, the first entry in the Styles array will be used."`
RejectDuplicateImages bool `description:"Enabling this will cause gochan to reject a post if the image has already been uploaded for another post.\nThis may end up being removed or being made board-specific in the future."`
NewThreadDelay int `description:"The amount of time in seconds that is required before an IP can make a new thread.<br />This may end up being removed or being made board-specific in the future."`
ReplyDelay int `description:"Same as the above, but for replies."`
MaxLineLength int `description:"Any line in a post that exceeds this will be split into two (or more) lines.<br />I'm not really sure why this is here, so it may end up being removed."`
ReservedTrips []string `description:"Secure tripcodes (!!Something) can be reserved here.<br />Each reservation should go on its own line and should look like this:<br />TripPassword1##Tripcode1<br />TripPassword2##Tripcode2"`
ThumbWidth int `description:"OP thumbnails use this as their max width.<br />To keep the aspect ratio, the image will be scaled down to the ThumbWidth or ThumbHeight, whichever is larger."`
ThumbHeight int `description:"OP thumbnails use this as their max height.<br />To keep the aspect ratio, the image will be scaled down to the ThumbWidth or ThumbHeight, whichever is larger."`
ThumbWidthReply int `description:"Same as ThumbWidth and ThumbHeight but for reply images."`
ThumbHeightReply int `description:"Same as ThumbWidth and ThumbHeight but for reply images."`
ThumbWidthCatalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images."`
ThumbHeightCatalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images."`
ThreadsPerPage int
RepliesOnBoardPage int `description:"Number of replies to a thread to show on the board page."`
StickyRepliesOnBoardPage int `description:"Same as above for stickied threads."`
BanMsg string `description:"The default public ban message."`
EmbedWidth int `description:"The width for inline/expanded videos."`
EmbedHeight int `description:"The height for inline/expanded videos."`
ExpandButton bool `description:"If checked, adds [Embed] after a Youtube, Vimeo, etc link to toggle an inline video frame."`
ImagesOpenNewTab bool `description:"If checked, thumbnails will open the respective image/video in a new tab instead of expanding them." `
NewTabOnOutlinks bool `description:"If checked, links to external sites will open in a new tab."`
DisableBBcode bool `description:"If checked, gochan will not compile bbcode into HTML"`
MinifyHTML bool `description:"If checked, gochan will minify html files when building"`
MinifyJS bool `description:"If checked, gochan will minify js and json files when building"`
DateTimeFormat string `description:"The format used for dates. See <a href=\"https://golang.org/pkg/time/#Time.Format\">here</a> for more info."`
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
UseCaptcha bool `description:"If checked, a captcha will be generated"`
CaptchaWidth int `description:"Width of the generated captcha image"`
CaptchaHeight int `description:"Height of the generated captcha image"`
CaptchaMinutesExpire int `description:"Number of minutes before a user has to enter a new CAPTCHA before posting. If <1 they have to submit one for every post."`
EnableGeoIP bool `description:"If checked, this enables the usage of GeoIP for posts."`
GeoIPDBlocation string `description:"Specifies the location of the GeoIP database file. If you're using CloudFlare, you can set it to cf to rely on CloudFlare for GeoIP information."`
MaxRecentPosts int `description:"The maximum number of posts to show on the Recent Posts list on the front page."`
RecentPostsWithNoFile bool `description:"If checked, recent posts with no image/upload are shown on the front page (as well as those with images"`
MaxLogDays int `description:"The maximum number of days to keep messages in the moderation/staff log file."`
RandomSeed string `critical:"true"`
jsonLocation string `json:"-"`
TimeZone int `json:"-"`
Version *GochanVersion `json:"-"`
} }
// ToMap returns the configuration file as a map func (gcfg *GochanConfig) setField(field string, value interface{}) {
func (cfg *GochanConfig) ToMap() map[string]interface{} { structValue := reflect.ValueOf(gcfg).Elem()
cVal := reflect.ValueOf(cfg).Elem() structFieldValue := structValue.FieldByName(field)
cType := reflect.TypeOf(*cfg) if !structFieldValue.IsValid() {
return
}
if !structFieldValue.CanSet() {
return
}
structFieldType := structFieldValue.Type()
val := reflect.ValueOf(value)
if structFieldType != val.Type() {
return
}
structFieldValue.Set(val)
}
// ToMap returns the configuration file as a map. This will probably be removed
func (gcfg *GochanConfig) ToMap() map[string]interface{} {
cVal := reflect.ValueOf(gcfg).Elem()
cType := reflect.TypeOf(*gcfg)
numFields := cType.NumField() numFields := cType.NumField()
out := make(map[string]interface{}) out := make(map[string]interface{})
for f := 0; f < numFields; f++ { for f := 0; f < numFields; f++ {
@ -175,122 +109,271 @@ func (cfg *GochanConfig) ToMap() map[string]interface{} {
// ValidateValues checks to make sure that the configuration options are usable // 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) // (e.g., ListenIP is a valid IP address, Port isn't a negative number, etc)
func (cfg *GochanConfig) ValidateValues() error { func (gcfg *GochanConfig) ValidateValues() error {
if net.ParseIP(cfg.ListenIP) == nil { if net.ParseIP(gcfg.ListenIP) == nil {
return &ErrInvalidValue{Field: "ListenIP", Value: cfg.ListenIP} return &ErrInvalidValue{Field: "ListenIP", Value: gcfg.ListenIP}
} }
changed := false changed := false
if len(cfg.FirstPage) == 0 {
cfg.FirstPage = cfgDefaults["FirstPage"].([]string) if gcfg.WebRoot == "" {
gcfg.WebRoot = "/"
changed = true changed = true
} }
_, err := gcutil.ParseDurationString(cfg.CookieMaxAge) if len(gcfg.FirstPage) == 0 {
gcfg.FirstPage = defaults["FirstPage"].([]string)
changed = true
}
if gcfg.CookieMaxAge == "" {
gcfg.CookieMaxAge = defaults["CookieMaxAge"].(string)
changed = true
}
_, err := gcutil.ParseDurationString(gcfg.CookieMaxAge)
if err == gcutil.ErrInvalidDurationString { if err == gcutil.ErrInvalidDurationString {
return &ErrInvalidValue{Field: "CookieMaxAge", Value: cfg.CookieMaxAge, Details: err.Error() + cookieMaxAgeEx} return &ErrInvalidValue{Field: "CookieMaxAge", Value: gcfg.CookieMaxAge, Details: err.Error() + cookieMaxAgeEx}
} else if err == gcutil.ErrEmptyDurationString {
return &ErrInvalidValue{Field: "CookieMaxAge", Details: err.Error() + cookieMaxAgeEx}
} else if err != nil { } else if err != nil {
return err return err
} }
if cfg.DBtype != "mysql" && cfg.DBtype != "postgresql" {
return &ErrInvalidValue{Field: "DBtype", Value: cfg.DBtype, Details: "currently supported values: mysql, postgresql"} if gcfg.LockdownMessage == "" {
gcfg.LockdownMessage = defaults["LockdownMessage"].(string)
} }
if len(cfg.Styles) == 0 {
return &ErrInvalidValue{Field: "Styles", Value: cfg.Styles} if gcfg.DBtype != "mysql" && gcfg.DBtype != "postgresql" {
return &ErrInvalidValue{Field: "DBtype", Value: gcfg.DBtype, Details: "currently supported values: mysql, postgresql"}
} }
if cfg.DefaultStyle == "" { if len(gcfg.Styles) == 0 {
cfg.DefaultStyle = cfg.Styles[0].Filename return &ErrInvalidValue{Field: "Styles", Value: gcfg.Styles}
}
if gcfg.DefaultStyle == "" {
gcfg.DefaultStyle = gcfg.Styles[0].Filename
changed = true changed = true
} }
if cfg.NewThreadDelay == 0 {
cfg.NewThreadDelay = cfgDefaults["NewThreadDelay"].(int) if gcfg.SiteName == "" {
gcfg.SiteName = defaults["SiteName"].(string)
}
if gcfg.MaxLineLength == 0 {
gcfg.MaxLineLength = defaults["MaxLineLength"].(int)
changed = true changed = true
} }
if cfg.ReplyDelay == 0 { if gcfg.ThumbWidth == 0 {
cfg.ReplyDelay = cfgDefaults["ReplyDelay"].(int) gcfg.ThumbWidth = defaults["ThumbWidth"].(int)
changed = true changed = true
} }
if cfg.MaxLineLength == 0 { if gcfg.ThumbHeight == 0 {
cfg.MaxLineLength = cfgDefaults["MaxLineLength"].(int) gcfg.ThumbHeight = defaults["ThumbHeight"].(int)
changed = true changed = true
} }
if cfg.ThumbWidth == 0 { if gcfg.ThumbWidthReply == 0 {
cfg.ThumbWidth = cfgDefaults["ThumbWidth"].(int) gcfg.ThumbWidthReply = defaults["ThumbWidthReply"].(int)
changed = true changed = true
} }
if cfg.ThumbHeight == 0 { if gcfg.ThumbHeightReply == 0 {
cfg.ThumbHeight = cfgDefaults["ThumbHeight"].(int) gcfg.ThumbHeightReply = defaults["ThumbHeightReply"].(int)
changed = true changed = true
} }
if cfg.ThumbWidthReply == 0 {
cfg.ThumbWidthReply = cfgDefaults["ThumbWidthReply"].(int) if gcfg.ThumbWidthCatalog == 0 {
gcfg.ThumbWidthCatalog = defaults["ThumbWidthCatalog"].(int)
changed = true changed = true
} }
if cfg.ThumbHeightReply == 0 { if gcfg.ThumbHeightCatalog == 0 {
cfg.ThumbHeightReply = cfgDefaults["ThumbHeightReply"].(int) gcfg.ThumbHeightCatalog = defaults["ThumbHeightCatalog"].(int)
changed = true changed = true
} }
if cfg.ThumbWidthCatalog == 0 { if gcfg.ThreadsPerPage == 0 {
cfg.ThumbWidthCatalog = cfgDefaults["ThumbWidthCatalog"].(int) gcfg.ThreadsPerPage = defaults["ThreadsPerPage"].(int)
changed = true changed = true
} }
if cfg.ThumbHeightCatalog == 0 { if gcfg.RepliesOnBoardPage == 0 {
cfg.ThumbHeightCatalog = cfgDefaults["ThumbHeightCatalog"].(int) gcfg.RepliesOnBoardPage = defaults["RepliesOnBoardPage"].(int)
changed = true changed = true
} }
if cfg.ThreadsPerPage == 0 { if gcfg.StickyRepliesOnBoardPage == 0 {
cfg.ThreadsPerPage = cfgDefaults["ThreadsPerPage"].(int) gcfg.StickyRepliesOnBoardPage = defaults["StickyRepliesOnBoardPage"].(int)
changed = true changed = true
} }
if cfg.RepliesOnBoardPage == 0 { if gcfg.BanMessage == "" {
cfg.RepliesOnBoardPage = cfgDefaults["RepliesOnBoardPage"].(int) gcfg.BanMessage = defaults["BanMessage"].(string)
changed = true changed = true
} }
if cfg.StickyRepliesOnBoardPage == 0 { if gcfg.DateTimeFormat == "" {
cfg.StickyRepliesOnBoardPage = cfgDefaults["StickyRepliesOnBoardPage"].(int) gcfg.DateTimeFormat = defaults["DateTimeFormat"].(string)
changed = true changed = true
} }
if cfg.BanMsg == "" { if gcfg.CaptchaWidth == 0 {
cfg.BanMsg = cfgDefaults["BanMsg"].(string) gcfg.CaptchaWidth = defaults["CaptchaWidth"].(int)
changed = true changed = true
} }
if cfg.DateTimeFormat == "" { if gcfg.CaptchaHeight == 0 {
cfg.DateTimeFormat = cfgDefaults["DateTimeFormat"].(string) gcfg.CaptchaHeight = defaults["CaptchaHeight"].(int)
changed = true changed = true
} }
if cfg.CaptchaWidth == 0 { if gcfg.EnableGeoIP {
cfg.CaptchaWidth = cfgDefaults["CaptchaWidth"].(int) if gcfg.GeoIPDBlocation == "" {
changed = true
}
if cfg.CaptchaHeight == 0 {
cfg.CaptchaHeight = cfgDefaults["CaptchaHeight"].(int)
changed = true
}
if cfg.EnableGeoIP {
if cfg.GeoIPDBlocation == "" {
return &ErrInvalidValue{Field: "GeoIPDBlocation", Value: "", Details: "GeoIPDBlocation must be set in gochan.json if EnableGeoIP is true"} return &ErrInvalidValue{Field: "GeoIPDBlocation", Value: "", Details: "GeoIPDBlocation must be set in gochan.json if EnableGeoIP is true"}
} }
} }
if cfg.MaxLogDays == 0 { if gcfg.MaxLogDays == 0 {
cfg.MaxLogDays = cfgDefaults["MaxLogDays"].(int) gcfg.MaxLogDays = defaults["MaxLogDays"].(int)
changed = true changed = true
} }
if cfg.RandomSeed == "" { if gcfg.RandomSeed == "" {
cfg.RandomSeed = gcutil.RandomString(randomStringSize) gcfg.RandomSeed = gcutil.RandomString(randomStringSize)
changed = true changed = true
} }
if !changed { if !changed {
return nil return nil
} }
return cfg.Write() return gcfg.Write()
} }
func (cfg *GochanConfig) Write() error { func (gcfg *GochanConfig) Write() error {
str, err := json.MarshalIndent(cfg, "", "\t") str, err := json.MarshalIndent(gcfg, "", "\t")
if err != nil { if err != nil {
return err return err
} }
return ioutil.WriteFile(cfg.jsonLocation, str, 0777) return ioutil.WriteFile(gcfg.jsonLocation, str, 0777)
}
/*
SystemCriticalConfig contains configuration options that are extremely important, and fucking with them while
the server is running could have site breaking consequences. It should only be changed by modifying the configuration
file and restarting the server.
*/
type SystemCriticalConfig struct {
ListenIP string `critical:"true"`
Port int `critical:"true"`
UseFastCGI bool `critical:"true"`
DocumentRoot string `critical:"true"`
TemplateDir string `critical:"true"`
LogDir string `critical:"true"`
SiteHeaderURL string
WebRoot string `description:"The HTTP root appearing in the browser (e.g. '/', 'https://yoursite.net/', etc) that all internal links start with"`
SiteDomain string `description:"The server's domain (e.g. gochan.org, 127.0.0.1, etc)"`
DBtype string `critical:"true"`
DBhost string `critical:"true"`
DBname string `critical:"true"`
DBusername string `critical:"true"`
DBpassword string `critical:"true"`
DBprefix string `description:"Each table's name in the database will start with this, if it is set"`
DebugMode bool `description:"Disables several spam/browser checks that can cause problems when hosting an instance locally."`
RandomSeed string
Version *GochanVersion `json:"-"`
TimeZone int `json:"-"`
}
type SiteConfig struct {
FirstPage []string
Username string
CookieMaxAge string `description:"The amount of time that session cookies will exist before they expire (ex: 1y2mo3d4h or 1 year 2 months 3 days 4 hours). Default is 1 year"`
Lockdown bool `description:"Disables posting."`
LockdownMessage string `description:"Message displayed when someone tries to post while the site is on lockdown."`
SiteName string `description:"The name of the site that appears in the header of the front page."`
SiteSlogan string `description:"The text that appears below SiteName on the home page"`
Modboard string `description:"A super secret clubhouse board that only staff can view/post to."`
MaxRecentPosts int `description:"The maximum number of posts to show on the Recent Posts list on the front page."`
RecentPostsWithNoFile bool `description:"If checked, recent posts with no image/upload are shown on the front page (as well as those with images"`
Verbosity int
EnableAppeals bool
MaxLogDays int `description:"The maximum number of days to keep messages in the moderation/staff log file."`
MinifyHTML bool `description:"If checked, gochan will minify html files when building"`
MinifyJS bool `description:"If checked, gochan will minify js and json files when building"`
GeoIPDBlocation string `description:"Specifies the location of the GeoIP database file. If you're using CloudFlare, you can set it to cf to rely on CloudFlare for GeoIP information."`
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
}
type BoardConfig struct {
InheritGlobalStyles bool `description:"If checked, a board uses the global Styles array + the board config's styles (with duplicates removed)"`
Styles []Style `description:"List of styles (one per line) that should be accessed online at <SiteWebFolder>/css/<Style>"`
DefaultStyle string `description:"Filename of the default Style. If this unset, the first entry in the Styles array will be used."`
Sillytags []string `description:"List of randomly selected fake staff tags separated by line, e.g. ## Mod, to be randomly assigned to posts if UseSillytags is checked. Don't include the \"## \""`
UseSillytags bool `description:"Use Sillytags"`
PostConfig
UploadConfig
DateTimeFormat string `description:"The format used for dates. See <a href=\"https://golang.org/pkg/time/#Time.Format\">here</a> for more info."`
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
UseCaptcha bool
CaptchaWidth int
CaptchaHeight int
CaptchaMinutesTimeout int
EnableGeoIP bool
}
// Style represents a theme (Pipes, Dark, etc)
type Style struct {
Name string
Filename string
}
type UploadConfig struct {
RejectDuplicateImages bool `description:"Enabling this will cause gochan to reject a post if the image has already been uploaded for another post.\nThis may end up being removed or being made board-specific in the future."`
ThumbWidth int `description:"OP thumbnails use this as their max width.<br />To keep the aspect ratio, the image will be scaled down to the ThumbWidth or ThumbHeight, whichever is larger."`
ThumbHeight int `description:"OP thumbnails use this as their max height.<br />To keep the aspect ratio, the image will be scaled down to the ThumbWidth or ThumbHeight, whichever is larger."`
ThumbWidthReply int `description:"Same as ThumbWidth and ThumbHeight but for reply images."`
ThumbHeightReply int `description:"Same as ThumbWidth and ThumbHeight but for reply images."`
ThumbWidthCatalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images."`
ThumbHeightCatalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images."`
}
type PostConfig struct {
NewThreadDelay int `description:"The amount of time in seconds that is required before an IP can make a new thread.<br />This may end up being removed or being made board-specific in the future."`
ReplyDelay int `description:"Same as the above, but for replies."`
MaxLineLength int `description:"Any line in a post that exceeds this will be split into two (or more) lines.<br />I'm not really sure why this is here, so it may end up being removed."`
ReservedTrips []string `description:"Secure tripcodes (!!Something) can be reserved here.<br />Each reservation should go on its own line and should look like this:<br />TripPassword1##Tripcode1<br />TripPassword2##Tripcode2"`
ThreadsPerPage int
PostsPerThreadPage int
RepliesOnBoardPage int `description:"Number of replies to a thread to show on the board page."`
StickyRepliesOnBoardPage int `description:"Same as above for stickied threads."`
BanColors []string
BanMessage string `description:"The default public ban message."`
EmbedWidth int `description:"The width for inline/expanded videos."`
EmbedHeight int `description:"The height for inline/expanded videos."`
EnableEmbeds bool `description:"If checked, adds [Embed] after a Youtube, Vimeo, etc link to toggle an inline video frame."`
ImagesOpenNewTab bool `description:"If checked, thumbnails will open the respective image/video in a new tab instead of expanding them." `
NewTabOnOutlinks bool `description:"If checked, links to external sites will open in a new tab."`
DisableBBcode bool `description:"If checked, gochan will not compile bbcode into HTML"`
}
func WriteConfig() error {
return cfg.Write()
}
// GetSystemCriticalConfig returns system-critical configuration options like listening IP
func GetSystemCriticalConfig() SystemCriticalConfig {
return cfg.SystemCriticalConfig
}
// GetSiteConfig returns the global site configuration (site name, slogan, etc)
func GetSiteConfig() *SiteConfig {
return &cfg.SiteConfig
}
// GetBoardConfig returns the custom configuration for the specified board (if it exists)
// or the global board configuration if board is an empty string or it doesn't exist
func GetBoardConfig(board string) *BoardConfig {
bc, exists := boardConfigs[board]
if board == "" || !exists {
return &cfg.BoardConfig
}
return &bc
}
func GetVersion() *GochanVersion {
return cfg.Version
} }

View file

@ -16,7 +16,7 @@ const (
"DBusername": "gochan", "DBusername": "gochan",
"DBpassword": "", "DBpassword": "",
"SiteDomain": "127.0.0.1", "SiteDomain": "127.0.0.1",
"SiteWebfolder": "/", "SiteWebFolder": "/",
"Styles": [ "Styles": [
{ "Name": "Pipes", "Filename": "pipes.css" }, { "Name": "Pipes", "Filename": "pipes.css" },
@ -57,7 +57,7 @@ const (
"SiteName": "Gochan", "SiteName": "Gochan",
"SiteSlogan": "", "SiteSlogan": "",
"SiteDomain": "127.0.0.1", "SiteDomain": "127.0.0.1",
"SiteWebfolder": "/", "SiteWebFolder": "/",
"Styles": [ "Styles": [
{ "Name": "Pipes", "Filename": "pipes.css" }, { "Name": "Pipes", "Filename": "pipes.css" },
@ -87,10 +87,10 @@ const (
"PostsPerThreadPage": 50, "PostsPerThreadPage": 50,
"RepliesOnBoardPage": 3, "RepliesOnBoardPage": 3,
"StickyRepliesOnBoardPage": 1, "StickyRepliesOnBoardPage": 1,
"BanMsg": "USER WAS BANNED FOR THIS POST", "BanMessage": "USER WAS BANNED FOR THIS POST",
"EmbedWidth": 200, "EmbedWidth": 200,
"EmbedHeight": 164, "EmbedHeight": 164,
"ExpandButton": true, "EnableEmbeds": true,
"ImagesOpenNewTab": true, "ImagesOpenNewTab": true,
"MakeURLsHyperlinked": true, "MakeURLsHyperlinked": true,
"NewTabOnOutlinks": true, "NewTabOnOutlinks": true,

View file

@ -13,6 +13,13 @@ import (
"github.com/gochan-org/gochan/pkg/gcutil" "github.com/gochan-org/gochan/pkg/gcutil"
) )
var (
criticalFields = []string{
"ListenIP", "Port", "Username", "UseFastCGI", "DocumentRoot", "TemplateDir", "LogDir",
"DBtype", "DBhost", "DBname", "DBusername", "DBpassword", "SiteDomain", "Styles",
}
)
// MissingField represents a field missing from the configuration file // MissingField represents a field missing from the configuration file
type MissingField struct { type MissingField struct {
Name string Name string
@ -35,6 +42,39 @@ func (iv *ErrInvalidValue) Error() string {
return str return str
} }
func GetDefaultBool(key string) bool {
boolInterface := defaults[key]
if boolInterface == nil {
return false
}
b, ok := boolInterface.(bool)
return b && ok
}
func GetDefaultInt(key string) int {
intInterface := defaults[key]
if intInterface == nil {
return 0
}
i, ok := intInterface.(int)
if !ok {
return 0
}
return i
}
func GetDefaultString(key string) string {
i := defaults[key]
if i == nil {
return ""
}
str, ok := i.(string)
if !ok {
return ""
}
return str
}
// ParseJSON loads and parses JSON data, returning a GochanConfig pointer, any critical missing // ParseJSON loads and parses JSON data, returning a GochanConfig pointer, any critical missing
// fields that don't have defaults, and any error from parsing the file. This doesn't mean that the // fields that don't have defaults, and any error from parsing the file. This doesn't mean that the
// values are valid, just that they exist // values are valid, just that they exist
@ -66,9 +106,9 @@ func ParseJSON(ba []byte) (*GochanConfig, []MissingField, error) {
// field is in the JSON file // field is in the JSON file
continue continue
} }
if cfgDefaults[fType.Name] != nil { if defaults[fType.Name] != nil {
// the field isn't in the JSON file but has a default value that we can use // the field isn't in the JSON file but has a default value that we can use
fVal.Set(reflect.ValueOf(cfgDefaults[fType.Name])) fVal.Set(reflect.ValueOf(defaults[fType.Name]))
continue continue
} }
if critical { if critical {
@ -98,11 +138,11 @@ func InitConfig(versionStr string) {
} }
var fields []MissingField var fields []MissingField
Config, fields, err = ParseJSON(jfile) cfg, fields, err = ParseJSON(jfile)
if err != nil { if err != nil {
fmt.Printf("Error parsing %s: %s", cfgPath, err.Error()) fmt.Printf("Error parsing %s: %s", cfgPath, err.Error())
} }
Config.jsonLocation = cfgPath cfg.jsonLocation = cfgPath
numMissing := 0 numMissing := 0
for _, missing := range fields { for _, missing := range fields {
@ -117,63 +157,90 @@ func InitConfig(versionStr string) {
os.Exit(1) os.Exit(1)
} }
if err = Config.ValidateValues(); err != nil { if err = cfg.ValidateValues(); err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
os.Exit(1) os.Exit(1)
} }
if _, err = os.Stat(Config.DocumentRoot); err != nil { if _, err = os.Stat(cfg.DocumentRoot); err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
os.Exit(1) os.Exit(1)
} }
if _, err = os.Stat(Config.TemplateDir); err != nil { if _, err = os.Stat(cfg.TemplateDir); err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
os.Exit(1) os.Exit(1)
} }
if _, err = os.Stat(Config.LogDir); err != nil { if _, err = os.Stat(cfg.LogDir); err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
os.Exit(1) os.Exit(1)
} }
Config.LogDir = gcutil.FindResource(Config.LogDir, "log", "/var/log/gochan/") cfg.LogDir = gcutil.FindResource(cfg.LogDir, "log", "/var/log/gochan/")
if err = gclog.InitLogs( if err = gclog.InitLogs(
path.Join(Config.LogDir, "access.log"), path.Join(cfg.LogDir, "access.log"),
path.Join(Config.LogDir, "error.log"), path.Join(cfg.LogDir, "error.log"),
path.Join(Config.LogDir, "staff.log"), path.Join(cfg.LogDir, "staff.log"),
Config.DebugMode); err != nil { cfg.DebugMode); err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
os.Exit(1) os.Exit(1)
} }
if Config.Port == 0 { if cfg.Port == 0 {
Config.Port = 80 cfg.Port = 80
} }
if len(Config.FirstPage) == 0 { if len(cfg.FirstPage) == 0 {
Config.FirstPage = []string{"index.html", "1.html", "firstrun.html"} cfg.FirstPage = []string{"index.html", "1.html", "firstrun.html"}
} }
if Config.SiteWebfolder == "" { if cfg.WebRoot == "" {
Config.SiteWebfolder = "/" cfg.WebRoot = "/"
} }
if Config.SiteWebfolder[0] != '/' { if cfg.WebRoot[0] != '/' {
Config.SiteWebfolder = "/" + Config.SiteWebfolder cfg.WebRoot = "/" + cfg.WebRoot
} }
if Config.SiteWebfolder[len(Config.SiteWebfolder)-1] != '/' { if cfg.WebRoot[len(cfg.WebRoot)-1] != '/' {
Config.SiteWebfolder += "/" cfg.WebRoot += "/"
} }
if Config.EnableGeoIP { if cfg.EnableGeoIP {
if _, err = os.Stat(Config.GeoIPDBlocation); err != nil { if _, err = os.Stat(cfg.GeoIPDBlocation); err != nil {
gclog.Print(gclog.LErrorLog|gclog.LStdLog, "Unable to find GeoIP file location set in gochan.json, disabling GeoIP") gclog.Print(gclog.LErrorLog|gclog.LStdLog, "Unable to find GeoIP file location set in gochan.json, disabling GeoIP")
} }
Config.EnableGeoIP = false cfg.EnableGeoIP = false
} }
_, zoneOffset := time.Now().Zone() _, zoneOffset := time.Now().Zone()
Config.TimeZone = zoneOffset / 60 / 60 cfg.TimeZone = zoneOffset / 60 / 60
Config.Version = ParseVersion(versionStr) cfg.Version = ParseVersion(versionStr)
Config.Version.Normalize() cfg.Version.Normalize()
}
// TODO: use reflect to check if the field exists in SystemCriticalConfig
func fieldIsCritical(field string) bool {
for _, cF := range criticalFields {
if field == cF {
return true
}
}
return false
}
// UpdateFromMap updates the configuration with the given key->values for use in things like the
// config editor page and possibly others
func UpdateFromMap(m map[string]interface{}, validate bool) error {
for key, val := range m {
if fieldIsCritical(key) {
// don't mess with critical/read-only fields (ListenIP, DocumentRoot, etc)
// after the server has started
continue
}
cfg.setField(key, val)
}
if validate {
return cfg.ValidateValues()
}
return nil
} }

View file

@ -49,11 +49,12 @@ func RunSQLFile(path string) error {
sqlStr := regexp.MustCompile("--.*\n?").ReplaceAllString(string(sqlBytes), " ") sqlStr := regexp.MustCompile("--.*\n?").ReplaceAllString(string(sqlBytes), " ")
sqlArr := strings.Split(gcdb.replacer.Replace(sqlStr), ";") sqlArr := strings.Split(gcdb.replacer.Replace(sqlStr), ";")
debugMode := config.GetSystemCriticalConfig().DebugMode
for _, statement := range sqlArr { for _, statement := range sqlArr {
statement = strings.Trim(statement, " \n\r\t") statement = strings.Trim(statement, " \n\r\t")
if len(statement) > 0 { if len(statement) > 0 {
if _, err = gcdb.db.Exec(statement); err != nil { if _, err = gcdb.db.Exec(statement); err != nil {
if config.Config.DebugMode { if debugMode {
gclog.Printf(gclog.LStdLog, "Error excecuting sql: %s\n", err.Error()) gclog.Printf(gclog.LStdLog, "Error excecuting sql: %s\n", err.Error())
gclog.Printf(gclog.LStdLog, "Length sql: %d\n", len(statement)) gclog.Printf(gclog.LStdLog, "Length sql: %d\n", len(statement))
gclog.Printf(gclog.LStdLog, "Statement: %s\n", statement) gclog.Printf(gclog.LStdLog, "Statement: %s\n", statement)

View file

@ -18,24 +18,28 @@ const (
) )
type GCDB struct { type GCDB struct {
db *sql.DB db *sql.DB
connStr string connStr string
driver string driver string
nilTimestamp string replacer *strings.Replacer
replacer *strings.Replacer // nilTimestamp string
} }
func (db *GCDB) ConnectionString() string { func (db *GCDB) ConnectionString() string {
return db.connStr return db.connStr
} }
func (db *GCDB) Connection() *sql.DB {
return db.db
}
func (db *GCDB) SQLDriver() string { func (db *GCDB) SQLDriver() string {
return db.driver return db.driver
} }
func (db *GCDB) NilSQLTimestamp() string { /* func (db *GCDB) NilSQLTimestamp() string {
return db.nilTimestamp return db.nilTimestamp
} } */
func (db *GCDB) Close() error { func (db *GCDB) Close() error {
if db.db != nil { if db.db != nil {
@ -145,10 +149,10 @@ func Open(host, dbDriver, dbName, username, password, prefix string) (db *GCDB,
switch dbDriver { switch dbDriver {
case "mysql": case "mysql":
db.connStr = fmt.Sprintf(mysqlConnStr, username, password, host, dbName) db.connStr = fmt.Sprintf(mysqlConnStr, username, password, host, dbName)
db.nilTimestamp = "0000-00-00 00:00:00" // db.nilTimestamp = "0000-00-00 00:00:00"
case "postgres": case "postgres":
db.connStr = fmt.Sprintf(postgresConnStr, username, password, host, dbName) db.connStr = fmt.Sprintf(postgresConnStr, username, password, host, dbName)
db.nilTimestamp = "0001-01-01 00:00:00" // db.nilTimestamp = "0001-01-01 00:00:00"
default: default:
return nil, ErrUnsupportedDB return nil, ErrUnsupportedDB
} }
@ -172,7 +176,7 @@ func sqlVersionError(err error, dbDriver string, query *string) error {
return err return err
} }
} }
if config.Config.DebugMode { if config.GetSystemCriticalConfig().DebugMode {
return fmt.Errorf(UnsupportedSQLVersionMsg+"\nQuery: "+*query, errText) return fmt.Errorf(UnsupportedSQLVersionMsg+"\nQuery: "+*query, errText)
} }
return fmt.Errorf(UnsupportedSQLVersionMsg, errText) return fmt.Errorf(UnsupportedSQLVersionMsg, errText)

View file

@ -54,7 +54,7 @@ func GetCompleteDatabaseVersion() (dbVersion, dbFlag int, err error) {
return 0, DBIsPreApril, nil return 0, DBIsPreApril, nil
} }
//No old or current database versioning tables found. //No old or current database versioning tables found.
if config.Config.DBprefix != "" { if config.GetSystemCriticalConfig().DBprefix != "" {
//Check if any gochan tables exist //Check if any gochan tables exist
gochanTableExists, err := doesGochanPrefixTableExist() gochanTableExists, err := doesGochanPrefixTableExist()
if err != nil { if err != nil {

View file

@ -795,6 +795,8 @@ func DeleteFilesFromPost(postID int) error {
filenames = append(filenames, filename) filenames = append(filenames, filename)
} }
systemCriticalCfg := config.GetSystemCriticalConfig()
//Remove files from disk //Remove files from disk
for _, fileName := range filenames { for _, fileName := range filenames {
fileName = fileName[:strings.Index(fileName, ".")] fileName = fileName[:strings.Index(fileName, ".")]
@ -804,9 +806,9 @@ func DeleteFilesFromPost(postID int) error {
thumbType = "jpg" thumbType = "jpg"
} }
os.Remove(path.Join(config.Config.DocumentRoot, board, "/src/"+fileName+"."+fileType)) os.Remove(path.Join(systemCriticalCfg.DocumentRoot, board, "/src/"+fileName+"."+fileType))
os.Remove(path.Join(config.Config.DocumentRoot, board, "/thumb/"+fileName+"t."+thumbType)) os.Remove(path.Join(systemCriticalCfg.DocumentRoot, board, "/thumb/"+fileName+"t."+thumbType))
os.Remove(path.Join(config.Config.DocumentRoot, board, "/thumb/"+fileName+"c."+thumbType)) os.Remove(path.Join(systemCriticalCfg.DocumentRoot, board, "/thumb/"+fileName+"c."+thumbType))
} }
const removeFilesSQL = `DELETE FROM DBPREFIXfiles WHERE post_id = ?` const removeFilesSQL = `DELETE FROM DBPREFIXfiles WHERE post_id = ?`
@ -1005,7 +1007,7 @@ func doesTableExist(tableName string) (bool, error) {
WHERE TABLE_NAME = ?` WHERE TABLE_NAME = ?`
var count int var count int
err := QueryRowSQL(existQuery, []interface{}{config.Config.DBprefix + tableName}, []interface{}{&count}) err := QueryRowSQL(existQuery, []interface{}{config.GetSystemCriticalConfig().DBprefix + tableName}, []interface{}{&count})
if err != nil { if err != nil {
return false, err return false, err
} }
@ -1015,7 +1017,7 @@ func doesTableExist(tableName string) (bool, error) {
//doesGochanPrefixTableExist returns true if any table with a gochan prefix was found. //doesGochanPrefixTableExist returns true if any table with a gochan prefix was found.
//Returns false if the prefix is an empty string //Returns false if the prefix is an empty string
func doesGochanPrefixTableExist() (bool, error) { func doesGochanPrefixTableExist() (bool, error) {
if config.Config.DBprefix == "" { if config.GetSystemCriticalConfig().DBprefix == "" {
return false, nil return false, nil
} }
var prefixTableExist = `SELECT count(*) var prefixTableExist = `SELECT count(*)

View file

@ -140,24 +140,26 @@ type Board struct {
// AbsolutePath returns the full filepath of the board directory // AbsolutePath returns the full filepath of the board directory
func (board *Board) AbsolutePath(subpath ...string) string { func (board *Board) AbsolutePath(subpath ...string) string {
return path.Join(config.Config.DocumentRoot, board.Dir, path.Join(subpath...)) return path.Join(config.GetSystemCriticalConfig().DocumentRoot, board.Dir, path.Join(subpath...))
} }
// WebPath returns a string that represents the file's path as accessible by a browser // WebPath returns a string that represents the file's path as accessible by a browser
// fileType should be "boardPage", "threadPage", "upload", or "thumb" // fileType should be "boardPage", "threadPage", "upload", or "thumb"
func (board *Board) WebPath(fileName, fileType string) string { func (board *Board) WebPath(fileName, fileType string) string {
var filePath string var filePath string
systemCritical := config.GetSystemCriticalConfig()
switch fileType { switch fileType {
case "": case "":
fallthrough fallthrough
case "boardPage": case "boardPage":
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, fileName) filePath = path.Join(systemCritical.WebRoot, board.Dir, fileName)
case "threadPage": case "threadPage":
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, "res", fileName) filePath = path.Join(systemCritical.WebRoot, board.Dir, "res", fileName)
case "upload": case "upload":
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, "src", fileName) filePath = path.Join(systemCritical.WebRoot, board.Dir, "src", fileName)
case "thumb": case "thumb":
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, "thumb", fileName) filePath = path.Join(systemCritical.WebRoot, board.Dir, "thumb", fileName)
} }
return filePath return filePath
} }
@ -188,7 +190,7 @@ func (board *Board) SetDefaults() {
board.Section = 1 board.Section = 1
board.MaxFilesize = 4096 board.MaxFilesize = 4096
board.MaxPages = 11 board.MaxPages = 11
board.DefaultStyle = config.Config.DefaultStyle board.DefaultStyle = config.GetBoardConfig("").DefaultStyle
board.Locked = false board.Locked = false
board.Anonymous = "Anonymous" board.Anonymous = "Anonymous"
board.ForcedAnon = false board.ForcedAnon = false
@ -251,15 +253,16 @@ type Post struct {
func (p *Post) GetURL(includeDomain bool) string { func (p *Post) GetURL(includeDomain bool) string {
postURL := "" postURL := ""
systemCritical := config.GetSystemCriticalConfig()
if includeDomain { if includeDomain {
postURL += config.Config.SiteDomain postURL += systemCritical.SiteDomain
} }
var board Board var board Board
if err := board.PopulateData(p.BoardID); err != nil { if err := board.PopulateData(p.BoardID); err != nil {
return postURL return postURL
} }
postURL += config.Config.SiteWebfolder + board.Dir + "/res/" postURL += systemCritical.WebRoot + board.Dir + "/res/"
if p.ParentID == 0 { if p.ParentID == 0 {
postURL += fmt.Sprintf("%d.html#%d", p.ID, p.ID) postURL += fmt.Sprintf("%d.html#%d", p.ID, p.ID)
} else { } else {
@ -340,11 +343,12 @@ type RecentPost struct {
// GetURL returns the full URL of the recent post, or the full path if includeDomain is false // GetURL returns the full URL of the recent post, or the full path if includeDomain is false
func (p *RecentPost) GetURL(includeDomain bool) string { func (p *RecentPost) GetURL(includeDomain bool) string {
postURL := "" postURL := ""
systemCritical := config.GetSystemCriticalConfig()
if includeDomain { if includeDomain {
postURL += config.Config.SiteDomain postURL += systemCritical.SiteDomain
} }
idStr := strconv.Itoa(p.PostID) idStr := strconv.Itoa(p.PostID)
postURL += config.Config.SiteWebfolder + p.BoardName + "/res/" postURL += systemCritical.WebRoot + p.BoardName + "/res/"
if p.ParentID == 0 { if p.ParentID == 0 {
postURL += idStr + ".html#" + idStr postURL += idStr + ".html#" + idStr
} else { } else {

View file

@ -74,7 +74,7 @@ var funcMap = template.FuncMap{
return fmt.Sprintf("%0.2f GB", size/1024/1024/1024) return fmt.Sprintf("%0.2f GB", size/1024/1024/1024)
}, },
"formatTimestamp": func(t time.Time) string { "formatTimestamp": func(t time.Time) string {
return t.Format(config.Config.DateTimeFormat) return t.Format(config.GetBoardConfig("").DateTimeFormat)
}, },
"stringAppend": func(strings ...string) string { "stringAppend": func(strings ...string) string {
var appended string var appended string
@ -155,13 +155,14 @@ var funcMap = template.FuncMap{
return return
}, },
"getPostURL": func(postInterface interface{}, typeOf string, withDomain bool) (postURL string) { "getPostURL": func(postInterface interface{}, typeOf string, withDomain bool) (postURL string) {
systemCritical := config.GetSystemCriticalConfig()
if withDomain { if withDomain {
postURL = config.Config.SiteDomain postURL = systemCritical.SiteDomain
} }
postURL += config.Config.SiteWebfolder postURL += systemCritical.WebRoot
if typeOf == "recent" { if typeOf == "recent" {
post, ok := postInterface.(*gcsql.RecentPost) post, ok := postInterface.(gcsql.RecentPost)
if !ok { if !ok {
return return
} }
@ -240,61 +241,77 @@ var funcMap = template.FuncMap{
return loopArr return loopArr
}, },
"generateConfigTable": func() template.HTML { "generateConfigTable": func() template.HTML {
configType := reflect.TypeOf(*config.Config) siteCfg := config.GetSiteConfig()
boardCfg := config.GetBoardConfig("")
tableOut := `<table style="border-collapse: collapse;" id="config"><tr><th>Field name</th><th>Value</th><th>Type</th><th>Description</th></tr>` tableOut := `<table style="border-collapse: collapse;" id="config"><tr><th>Field name</th><th>Value</th><th>Type</th><th>Description</th></tr>`
numFields := configType.NumField()
for f := 17; f < numFields-2; f++ {
// starting at Lockdown because the earlier fields can't be safely edited from a web interface
field := configType.Field(f)
if field.Tag.Get("critical") != "" {
continue
}
name := field.Name
tableOut += "<tr><th>" + name + "</th><td>"
f := reflect.Indirect(reflect.ValueOf(config.Config)).FieldByName(name)
kind := f.Kind() tableOut += configTable(siteCfg) +
switch kind { configTable(boardCfg) +
case reflect.Int: "</table>"
tableOut += `<input name="` + name + `" type="number" value="` + html.EscapeString(fmt.Sprintf("%v", f)) + `" class="config-text"/>`
case reflect.String:
tableOut += `<input name="` + name + `" type="text" value="` + html.EscapeString(fmt.Sprintf("%v", f)) + `" class="config-text"/>`
case reflect.Bool:
checked := ""
if f.Bool() {
checked = "checked"
}
tableOut += `<input name="` + name + `" type="checkbox" ` + checked + " />"
case reflect.Slice:
tableOut += `<textarea name="` + name + `" rows="4" cols="28">`
arrLength := f.Len()
for s := 0; s < arrLength; s++ {
newLine := "\n"
if s == arrLength-1 {
newLine = ""
}
tableOut += html.EscapeString(f.Slice(s, s+1).Index(0).String()) + newLine
}
tableOut += "</textarea>"
default:
tableOut += fmt.Sprintf("%v", kind)
}
tableOut += "</td><td>" + kind.String() + "</td><td>"
defaultTag := field.Tag.Get("default")
var defaultTagHTML string
if defaultTag != "" {
defaultTagHTML = " <b>Default: " + defaultTag + "</b>"
}
tableOut += field.Tag.Get("description") + defaultTagHTML + "</td>"
tableOut += "</tr>"
}
tableOut += "</table>"
return template.HTML(tableOut) return template.HTML(tableOut)
}, },
"isStyleDefault": func(style string) bool { "isStyleDefault": func(style string) bool {
return style == config.Config.DefaultStyle return style == config.GetBoardConfig("").DefaultStyle
}, },
"version": func() string { "version": func() string {
return config.Config.Version.String() return config.GetVersion().String()
}, },
} }
func configTable(cfg interface{}) string {
cVal := reflect.ValueOf(cfg)
if cVal.Kind() == reflect.Ptr {
cVal = cVal.Elem()
}
var tableOut string
if cVal.Kind() != reflect.Struct {
return ""
}
cType := cVal.Type()
numFields := cVal.NumField()
for f := 0; f < numFields; f++ {
field := cType.Field(f)
name := field.Name
fVal := reflect.Indirect(cVal).FieldByName(name)
fKind := fVal.Kind()
// interf := cVal.Field(f).Interface()
switch fKind {
case reflect.Int:
tableOut += `<input name="` + name + `" type="number" value="` + html.EscapeString(fmt.Sprintf("%v", f)) + `" class="config-text"/>`
case reflect.String:
tableOut += `<input name="` + name + `" type="text" value="` + html.EscapeString(fmt.Sprintf("%v", f)) + `" class="config-text"/>`
case reflect.Bool:
checked := ""
if fVal.Bool() {
checked = "checked"
}
tableOut += `<input name="` + name + `" type="checkbox" ` + checked + " />"
case reflect.Slice:
tableOut += `<textarea name="` + name + `" rows="4" cols="28">`
arrLength := fVal.Len()
for s := 0; s < arrLength; s++ {
newLine := "\n"
if s == arrLength-1 {
newLine = ""
}
tableOut += html.EscapeString(fVal.Slice(s, s+1).Index(0).String()) + newLine
}
tableOut += "</textarea>"
default:
tableOut += fmt.Sprintf("%v", fKind)
}
tableOut += "</td><td>" + fKind.String() + "</td><td>"
defaultTag := field.Tag.Get("default")
var defaultTagHTML string
if defaultTag != "" {
defaultTagHTML = " <b>Default: " + defaultTag + "</b>"
}
tableOut += field.Tag.Get("description") + defaultTagHTML + "</td>"
tableOut += "</tr>"
}
return tableOut
}

View file

@ -28,14 +28,15 @@ var (
func loadTemplate(files ...string) (*template.Template, error) { func loadTemplate(files ...string) (*template.Template, error) {
var templates []string var templates []string
templateDir := config.GetSystemCriticalConfig().TemplateDir
for i, file := range files { for i, file := range files {
templates = append(templates, file) templates = append(templates, file)
tmplPath := path.Join(config.Config.TemplateDir, "override", file) tmplPath := path.Join(templateDir, "override", file)
if _, err := os.Stat(tmplPath); !os.IsNotExist(err) { if _, err := os.Stat(tmplPath); !os.IsNotExist(err) {
files[i] = tmplPath files[i] = tmplPath
} else { } else {
files[i] = path.Join(config.Config.TemplateDir, file) files[i] = path.Join(templateDir, file)
} }
} }
@ -46,8 +47,10 @@ func templateError(name string, err error) error {
if err == nil { if err == nil {
return nil return nil
} }
templateDir := config.GetSystemCriticalConfig().TemplateDir
return fmt.Errorf("failed loading template '%s/%s': %s", return fmt.Errorf("failed loading template '%s/%s': %s",
config.Config.TemplateDir, name, err.Error()) templateDir, name, err.Error())
} }
// InitTemplates loads the given templates by name. If no parameters are given, // InitTemplates loads the given templates by name. If no parameters are given,

View file

@ -2,11 +2,9 @@ package manage
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"html" "html"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -85,216 +83,220 @@ var actions = map[string]Action{
Title: "Configuration", Title: "Configuration",
Permissions: AdminPerms, Permissions: AdminPerms,
Callback: func(writer http.ResponseWriter, request *http.Request) (htmlOut string, err error) { Callback: func(writer http.ResponseWriter, request *http.Request) (htmlOut string, err error) {
do := request.FormValue("do")
var status string
if do == "save" {
configJSON, err := json.MarshalIndent(config.Config, "", "\t")
if err != nil {
status += gclog.Println(gclog.LErrorLog, err.Error()) + "<br />"
} else if err = ioutil.WriteFile("gochan.json", configJSON, 0777); err != nil {
status += gclog.Println(gclog.LErrorLog,
"Error backing up old gochan.json, cancelling save:", err.Error())
} else {
config.Config.CookieMaxAge = request.PostFormValue("CookieMaxAge")
if _, err = gcutil.ParseDurationString(config.Config.CookieMaxAge); err != nil {
status += err.Error()
config.Config.CookieMaxAge = "1y"
}
config.Config.Lockdown = (request.PostFormValue("Lockdown") == "on")
config.Config.LockdownMessage = request.PostFormValue("LockdownMessage")
SillytagsArr := strings.Split(request.PostFormValue("Sillytags"), "\n")
var Sillytags []string
for _, tag := range SillytagsArr {
Sillytags = append(Sillytags, strings.Trim(tag, " \n\r"))
}
config.Config.Sillytags = Sillytags // do := request.FormValue("do")
config.Config.UseSillytags = (request.PostFormValue("UseSillytags") == "on") // siteCfg := config.GetSiteConfig()
config.Config.Modboard = request.PostFormValue("Modboard") // boardCfg := config.GetBoardConfig("")
config.Config.SiteName = request.PostFormValue("SiteName") // var status string
config.Config.SiteSlogan = request.PostFormValue("SiteSlogan") // if do == "save" {
config.Config.SiteWebfolder = request.PostFormValue("SiteWebfolder") // configJSON, err := json.MarshalIndent(config.Config, "", "\t")
// TODO: Change this to match the new Style type in gochan.json // if err != nil {
/* Styles_arr := strings.Split(request.PostFormValue("Styles"), "\n") // status += gclog.Println(gclog.LErrorLog, err.Error()) + "<br />"
var Styles []string // } else if err = ioutil.WriteFile("gochan.json", configJSON, 0777); err != nil {
for _, style := range Styles_arr { // status += gclog.Println(gclog.LErrorLog,
Styles = append(Styles, strings.Trim(style, " \n\r")) // "Error backing up old gochan.json, cancelling save:", err.Error())
} // } else {
config.Styles = Styles */ // siteCfg.CookieMaxAge = request.PostFormValue("CookieMaxAge")
config.Config.DefaultStyle = request.PostFormValue("DefaultStyle") // if _, err = gcutil.ParseDurationString(config.Config.CookieMaxAge); err != nil {
config.Config.RejectDuplicateImages = (request.PostFormValue("RejectDuplicateImages") == "on") // status += err.Error()
NewThreadDelay, err := strconv.Atoi(request.PostFormValue("NewThreadDelay")) // siteCfg.CookieMaxAge = "1y"
if err != nil { // }
status += err.Error() + "<br />" // siteCfg.Lockdown = (request.PostFormValue("Lockdown") == "on")
} else { // siteCfg.LockdownMessage = request.PostFormValue("LockdownMessage")
config.Config.NewThreadDelay = NewThreadDelay // SillytagsArr := strings.Split(request.PostFormValue("Sillytags"), "\n")
} // var Sillytags []string
// for _, tag := range SillytagsArr {
// Sillytags = append(Sillytags, strings.Trim(tag, " \n\r"))
// }
ReplyDelay, err := strconv.Atoi(request.PostFormValue("ReplyDelay")) // boardCfg.Sillytags = Sillytags
if err != nil { // boardCfg.UseSillytags = (request.PostFormValue("UseSillytags") == "on")
status += err.Error() + "<br />" // siteCfg.Modboard = request.PostFormValue("Modboard")
} else { // siteCfg.SiteName = request.PostFormValue("SiteName")
config.Config.ReplyDelay = ReplyDelay // siteCfg.SiteSlogan = request.PostFormValue("SiteSlogan")
} // // boardCfg.WebRoot = request.PostFormValue("WebRoot")
// // TODO: Change this to match the new Style type in gochan.json
// /* Styles_arr := strings.Split(request.PostFormValue("Styles"), "\n")
// var Styles []string
// for _, style := range Styles_arr {
// Styles = append(Styles, strings.Trim(style, " \n\r"))
// }
// config.Styles = Styles */
// boardCfg.DefaultStyle = request.PostFormValue("DefaultStyle")
// boardCfg.RejectDuplicateImages = (request.PostFormValue("RejectDuplicateImages") == "on")
// NewThreadDelay, err := strconv.Atoi(request.PostFormValue("NewThreadDelay"))
// if err != nil {
// status += err.Error() + "<br />"
// } else {
// boardCfg.NewThreadDelay = NewThreadDelay
// }
MaxLineLength, err := strconv.Atoi(request.PostFormValue("MaxLineLength")) // ReplyDelay, err := strconv.Atoi(request.PostFormValue("ReplyDelay"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else { // } else {
config.Config.MaxLineLength = MaxLineLength // boardCfg.ReplyDelay = ReplyDelay
} // }
ReservedTripsArr := strings.Split(request.PostFormValue("ReservedTrips"), "\n") // MaxLineLength, err := strconv.Atoi(request.PostFormValue("MaxLineLength"))
var ReservedTrips []string // if err != nil {
for _, trip := range ReservedTripsArr { // status += err.Error() + "<br />"
ReservedTrips = append(ReservedTrips, strings.Trim(trip, " \n\r")) // } else {
// boardCfg.MaxLineLength = MaxLineLength
// }
} // ReservedTripsArr := strings.Split(request.PostFormValue("ReservedTrips"), "\n")
config.Config.ReservedTrips = ReservedTrips // var ReservedTrips []string
// for _, trip := range ReservedTripsArr {
// ReservedTrips = append(ReservedTrips, strings.Trim(trip, " \n\r"))
ThumbWidth, err := strconv.Atoi(request.PostFormValue("ThumbWidth")) // }
if err != nil { // boardCfg.ReservedTrips = ReservedTrips
status += err.Error() + "<br />"
} else {
config.Config.ThumbWidth = ThumbWidth
}
ThumbHeight, err := strconv.Atoi(request.PostFormValue("ThumbHeight")) // ThumbWidth, err := strconv.Atoi(request.PostFormValue("ThumbWidth"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else { // } else {
config.Config.ThumbHeight = ThumbHeight // boardCfg.ThumbWidth = ThumbWidth
} // }
ThumbWidthReply, err := strconv.Atoi(request.PostFormValue("ThumbWidthReply")) // ThumbHeight, err := strconv.Atoi(request.PostFormValue("ThumbHeight"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else { // } else {
config.Config.ThumbWidthReply = ThumbWidthReply // boardCfg.ThumbHeight = ThumbHeight
} // }
ThumbHeightReply, err := strconv.Atoi(request.PostFormValue("ThumbHeightReply")) // ThumbWidthReply, err := strconv.Atoi(request.PostFormValue("ThumbWidthReply"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else { // } else {
config.Config.ThumbHeightReply = ThumbHeightReply // boardCfg.ThumbWidthReply = ThumbWidthReply
} // }
ThumbWidthCatalog, err := strconv.Atoi(request.PostFormValue("ThumbWidthCatalog")) // ThumbHeightReply, err := strconv.Atoi(request.PostFormValue("ThumbHeightReply"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else { // } else {
config.Config.ThumbWidthCatalog = ThumbWidthCatalog // boardCfg.ThumbHeightReply = ThumbHeightReply
} // }
ThumbHeightCatalog, err := strconv.Atoi(request.PostFormValue("ThumbHeightCatalog")) // ThumbWidthCatalog, err := strconv.Atoi(request.PostFormValue("ThumbWidthCatalog"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else { // } else {
config.Config.ThumbHeightCatalog = ThumbHeightCatalog // boardCfg.ThumbWidthCatalog = ThumbWidthCatalog
} // }
RepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("RepliesOnBoardPage")) // ThumbHeightCatalog, err := strconv.Atoi(request.PostFormValue("ThumbHeightCatalog"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else { // } else {
config.Config.RepliesOnBoardPage = RepliesOnBoardPage // boardCfg.ThumbHeightCatalog = ThumbHeightCatalog
} // }
StickyRepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("StickyRepliesOnBoardPage")) // RepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("RepliesOnBoardPage"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else { // } else {
config.Config.StickyRepliesOnBoardPage = StickyRepliesOnBoardPage // boardCfg.RepliesOnBoardPage = RepliesOnBoardPage
} // }
config.Config.BanMsg = request.PostFormValue("BanMsg") // StickyRepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("StickyRepliesOnBoardPage"))
EmbedWidth, err := strconv.Atoi(request.PostFormValue("EmbedWidth")) // if err != nil {
if err != nil { // status += err.Error() + "<br />"
status += err.Error() + "<br />" // } else {
} else { // boardCfg.StickyRepliesOnBoardPage = StickyRepliesOnBoardPage
config.Config.EmbedWidth = EmbedWidth // }
}
EmbedHeight, err := strconv.Atoi(request.PostFormValue("EmbedHeight")) // boardCfg.BanMessage = request.PostFormValue("BanMessage")
if err != nil { // EmbedWidth, err := strconv.Atoi(request.PostFormValue("EmbedWidth"))
status += err.Error() + "<br />" // if err != nil {
} else { // status += err.Error() + "<br />"
config.Config.EmbedHeight = EmbedHeight // } else {
} // boardCfg.EmbedWidth = EmbedWidth
// }
config.Config.ExpandButton = (request.PostFormValue("ExpandButton") == "on") // EmbedHeight, err := strconv.Atoi(request.PostFormValue("EmbedHeight"))
config.Config.ImagesOpenNewTab = (request.PostFormValue("ImagesOpenNewTab") == "on") // if err != nil {
config.Config.NewTabOnOutlinks = (request.PostFormValue("NewTabOnOutlinks") == "on") // status += err.Error() + "<br />"
config.Config.MinifyHTML = (request.PostFormValue("MinifyHTML") == "on") // } else {
config.Config.MinifyJS = (request.PostFormValue("MinifyJS") == "on") // boardCfg.EmbedHeight = EmbedHeight
config.Config.DateTimeFormat = request.PostFormValue("DateTimeFormat") // }
AkismetAPIKey := request.PostFormValue("AkismetAPIKey")
if err = serverutil.CheckAkismetAPIKey(AkismetAPIKey); err != nil { // boardCfg.EnableEmbeds = (request.PostFormValue("EnableEmbeds") == "on")
status += err.Error() + "<br />" // boardCfg.ImagesOpenNewTab = (request.PostFormValue("ImagesOpenNewTab") == "on")
} else { // boardCfg.NewTabOnOutlinks = (request.PostFormValue("NewTabOnOutlinks") == "on")
config.Config.AkismetAPIKey = AkismetAPIKey // boardCfg.DateTimeFormat = request.PostFormValue("DateTimeFormat")
} // siteCfg.MinifyHTML = (request.PostFormValue("MinifyHTML") == "on")
// siteCfg.MinifyJS = (request.PostFormValue("MinifyJS") == "on")
// AkismetAPIKey := request.PostFormValue("AkismetAPIKey")
config.Config.UseCaptcha = (request.PostFormValue("UseCaptcha") == "on") // if err = serverutil.CheckAkismetAPIKey(AkismetAPIKey); err != nil {
CaptchaWidth, err := strconv.Atoi(request.PostFormValue("CaptchaWidth")) // status += err.Error() + "<br />"
if err != nil { // } else {
status += err.Error() + "<br />" // siteCfg.AkismetAPIKey = AkismetAPIKey
} else { // }
config.Config.CaptchaWidth = CaptchaWidth
}
CaptchaHeight, err := strconv.Atoi(request.PostFormValue("CaptchaHeight"))
if err != nil {
status += err.Error() + "<br />"
} else {
config.Config.CaptchaHeight = CaptchaHeight
}
config.Config.EnableGeoIP = (request.PostFormValue("EnableGeoIP") == "on") // boardCfg.UseCaptcha = (request.PostFormValue("UseCaptcha") == "on")
config.Config.GeoIPDBlocation = request.PostFormValue("GeoIPDBlocation") // CaptchaWidth, err := strconv.Atoi(request.PostFormValue("CaptchaWidth"))
// if err != nil {
// status += err.Error() + "<br />"
// } else {
// boardCfg.CaptchaWidth = CaptchaWidth
// }
// CaptchaHeight, err := strconv.Atoi(request.PostFormValue("CaptchaHeight"))
// if err != nil {
// status += err.Error() + "<br />"
// } else {
// boardCfg.CaptchaHeight = CaptchaHeight
// }
MaxRecentPosts, err := strconv.Atoi(request.PostFormValue("MaxRecentPosts")) // boardCfg.EnableGeoIP = (request.PostFormValue("EnableGeoIP") == "on")
if err != nil { // siteCfg.GeoIPDBlocation = request.PostFormValue("GeoIPDBlocation")
status += err.Error() + "<br />"
} else {
config.Config.MaxRecentPosts = MaxRecentPosts
}
MaxLogDays, err := strconv.Atoi(request.PostFormValue("MaxLogDays")) // MaxRecentPosts, err := strconv.Atoi(request.PostFormValue("MaxRecentPosts"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else { // } else {
config.Config.MaxLogDays = MaxLogDays // siteCfg.MaxRecentPosts = MaxRecentPosts
} // }
configJSON, err = json.MarshalIndent(config.Config, "", "\t") // MaxLogDays, err := strconv.Atoi(request.PostFormValue("MaxLogDays"))
if err != nil { // if err != nil {
status += err.Error() + "<br />" // status += err.Error() + "<br />"
} else if err = ioutil.WriteFile("gochan.json", configJSON, 0777); err != nil { // } else {
status = gclog.Print(gclog.LErrorLog, "Error writing gochan.json: ", err.Error()) // siteCfg.MaxLogDays = MaxLogDays
} else { // }
status = "Wrote gochan.json successfully<br />"
building.BuildJS() // if err = config.WriteConfig(); err != nil {
} // status = gclog.Print(gclog.LErrorLog, "Error writing gochan.json: ", err.Error()) + "<br />"
} // } else {
} // status = "Wrote gochan.json successfully<br />"
manageConfigBuffer := bytes.NewBufferString("") // }
if err = gctemplates.ManageConfig.Execute(manageConfigBuffer, // }
map[string]interface{}{"config": *config.Config, "status": status}); err != nil { // }
err = errors.New(gclog.Print(gclog.LErrorLog, // manageConfigBuffer := bytes.NewBufferString("")
"Error executing config management page: ", err.Error())) // if err = gctemplates.ManageConfig.Execute(manageConfigBuffer, map[string]interface{}{
return htmlOut + err.Error(), err // "siteCfg": siteCfg,
} // "boardCfg": boardCfg,
htmlOut += manageConfigBuffer.String() // "status": status,
return htmlOut, nil // }); err != nil {
// err = errors.New(gclog.Print(gclog.LErrorLog,
// "Error executing config management page: ", err.Error()))
// return htmlOut + err.Error(), err
// }
// htmlOut += manageConfigBuffer.String()
// return htmlOut, nil
return htmlOut + "Web-based configuration tool has been temporarily disabled", nil
}}, }},
"login": { "login": {
Title: "Login", Title: "Login",
Permissions: NoPerms, Permissions: NoPerms,
Callback: func(writer http.ResponseWriter, request *http.Request) (htmlOut string, err error) { Callback: func(writer http.ResponseWriter, request *http.Request) (htmlOut string, err error) {
systemCritical := config.GetSystemCriticalConfig()
if GetStaffRank(request) > 0 { if GetStaffRank(request) > 0 {
http.Redirect(writer, request, path.Join(config.Config.SiteWebfolder, "manage"), http.StatusFound) http.Redirect(writer, request, path.Join(systemCritical.WebRoot, "manage"), http.StatusFound)
} }
username := request.FormValue("username") username := request.FormValue("username")
password := request.FormValue("password") password := request.FormValue("password")
@ -304,16 +306,16 @@ var actions = map[string]Action{
} }
if username == "" || password == "" { if username == "" || password == "" {
//assume that they haven't logged in //assume that they haven't logged in
htmlOut = `<form method="POST" action="` + config.Config.SiteWebfolder + `manage?action=login" id="login-box" class="staff-form">` + htmlOut = `<form method="POST" action="` + systemCritical.WebRoot + `manage?action=login" id="login-box" class="staff-form">` +
`<input type="hidden" name="redirect" value="` + redirectAction + `" />` + `<input type="hidden" name="redirect" value="` + redirectAction + `" />` +
`<input type="text" name="username" class="logindata" /><br />` + `<input type="text" name="username" class="logindata" /><br />` +
`<input type="password" name="password" class="logindata" /><br />` + `<input type="password" name="password" class="logindata" /><br />` +
`<input type="submit" value="Login" />` + `<input type="submit" value="Login" />` +
`</form>` `</form>`
} else { } else {
key := gcutil.Md5Sum(request.RemoteAddr + username + password + config.Config.RandomSeed + gcutil.RandomString(3))[0:10] key := gcutil.Md5Sum(request.RemoteAddr + username + password + systemCritical.RandomSeed + gcutil.RandomString(3))[0:10]
createSession(key, username, password, request, writer) createSession(key, username, password, request, writer)
http.Redirect(writer, request, path.Join(config.Config.SiteWebfolder, "manage?action="+request.FormValue("redirect")), http.StatusFound) http.Redirect(writer, request, path.Join(systemCritical.WebRoot, "manage?action="+request.FormValue("redirect")), http.StatusFound)
} }
return return
}}, }},
@ -342,9 +344,10 @@ var actions = map[string]Action{
if len(announcements) == 0 { if len(announcements) == 0 {
htmlOut += "No announcements" htmlOut += "No announcements"
} else { } else {
boardConfig := config.GetBoardConfig("")
for _, announcement := range announcements { for _, announcement := range announcements {
htmlOut += `<div class="section-block">` + htmlOut += `<div class="section-block">` +
`<div class="section-title-block"><b>` + announcement.Subject + `</b> by ` + announcement.Poster + ` at ` + announcement.Timestamp.Format(config.Config.DateTimeFormat) + `</div>` + `<div class="section-title-block"><b>` + announcement.Subject + `</b> by ` + announcement.Poster + ` at ` + announcement.Timestamp.Format(boardConfig.DateTimeFormat) + `</div>` +
`<div class="section-body">` + announcement.Message + `</div></div>` `<div class="section-body">` + announcement.Message + `</div></div>`
} }
} }
@ -426,7 +429,11 @@ var actions = map[string]Action{
manageBansBuffer := bytes.NewBufferString("") manageBansBuffer := bytes.NewBufferString("")
if err = gctemplates.ManageBans.Execute(manageBansBuffer, if err = gctemplates.ManageBans.Execute(manageBansBuffer,
map[string]interface{}{"config": config.Config, "banlist": banlist, "post": post}, map[string]interface{}{
// "systemCritical": config.GetSystemCriticalConfig(),
"banlist": banlist,
"post": post,
},
); err != nil { ); err != nil {
return "", errors.New("Error executing ban management page template: " + err.Error()) return "", errors.New("Error executing ban management page template: " + err.Error())
} }
@ -452,6 +459,7 @@ var actions = map[string]Action{
var done bool var done bool
board := new(gcsql.Board) board := new(gcsql.Board)
var boardCreationStatus string var boardCreationStatus string
systemCritical := config.GetSystemCriticalConfig()
for !done { for !done {
switch { switch {
@ -529,31 +537,32 @@ var actions = map[string]Action{
board.EnableCatalog = (request.FormValue("enablecatalog") == "on") board.EnableCatalog = (request.FormValue("enablecatalog") == "on")
//actually start generating stuff //actually start generating stuff
if err = os.Mkdir(path.Join(config.Config.DocumentRoot, board.Dir), 0666); err != nil {
if err = os.Mkdir(path.Join(systemCritical.DocumentRoot, board.Dir), 0666); err != nil {
do = "" do = ""
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/ already exists.", boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/ already exists.",
config.Config.DocumentRoot, board.Dir) systemCritical.DocumentRoot, board.Dir)
break break
} }
if err = os.Mkdir(path.Join(config.Config.DocumentRoot, board.Dir, "res"), 0666); err != nil { if err = os.Mkdir(path.Join(systemCritical.DocumentRoot, board.Dir, "res"), 0666); err != nil {
do = "" do = ""
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/res/ already exists.", boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/res/ already exists.",
config.Config.DocumentRoot, board.Dir) systemCritical.DocumentRoot, board.Dir)
break break
} }
if err = os.Mkdir(path.Join(config.Config.DocumentRoot, board.Dir, "src"), 0666); err != nil { if err = os.Mkdir(path.Join(systemCritical.DocumentRoot, board.Dir, "src"), 0666); err != nil {
do = "" do = ""
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/src/ already exists.", boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/src/ already exists.",
config.Config.DocumentRoot, board.Dir) systemCritical.DocumentRoot, board.Dir)
break break
} }
if err = os.Mkdir(path.Join(config.Config.DocumentRoot, board.Dir, "thumb"), 0666); err != nil { if err = os.Mkdir(path.Join(systemCritical.DocumentRoot, board.Dir, "thumb"), 0666); err != nil {
do = "" do = ""
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/thumb/ already exists.", boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/thumb/ already exists.",
config.Config.DocumentRoot, board.Dir) systemCritical.DocumentRoot, board.Dir)
break break
} }
@ -572,6 +581,7 @@ var actions = map[string]Action{
case do == "edit": case do == "edit":
// resetBoardSectionArrays() // resetBoardSectionArrays()
default: default:
boardConfig := config.GetBoardConfig("")
// put the default column values in the text boxes // put the default column values in the text boxes
board.Section = 1 board.Section = 1
board.MaxFilesize = 4718592 board.MaxFilesize = 4718592
@ -583,7 +593,7 @@ var actions = map[string]Action{
board.EmbedsAllowed = true board.EmbedsAllowed = true
board.EnableCatalog = true board.EnableCatalog = true
board.Worksafe = true board.Worksafe = true
board.ThreadsPerPage = config.Config.ThreadsPerPage board.ThreadsPerPage = boardConfig.ThreadsPerPage
} }
htmlOut = `<h1 class="manage-header">Manage boards</h1><form action="/manage?action=boards" method="POST"><input type="hidden" name="do" value="existing" /><select name="boardselect"><option>Select board...</option>` htmlOut = `<h1 class="manage-header">Manage boards</h1><form action="/manage?action=boards" method="POST"><input type="hidden" name="do" value="existing" /><select name="boardselect"><option>Select board...</option>`
@ -604,8 +614,9 @@ var actions = map[string]Action{
manageBoardsBuffer := bytes.NewBufferString("") manageBoardsBuffer := bytes.NewBufferString("")
gcsql.AllSections, _ = gcsql.GetAllSectionsOrCreateDefault() gcsql.AllSections, _ = gcsql.GetAllSectionsOrCreateDefault()
boardConfig := config.GetBoardConfig("")
if err = gctemplates.ManageBoards.Execute(manageBoardsBuffer, map[string]interface{}{ if err = gctemplates.ManageBoards.Execute(manageBoardsBuffer, map[string]interface{}{
"config": config.Config, "boardConfig": boardConfig,
"board": board, "board": board,
"section_arr": gcsql.AllSections, "section_arr": gcsql.AllSections,
}); err != nil { }); err != nil {
@ -702,6 +713,7 @@ var actions = map[string]Action{
Title: "Recent posts", Title: "Recent posts",
Permissions: JanitorPerms, Permissions: JanitorPerms,
Callback: func(writer http.ResponseWriter, request *http.Request) (htmlOut string, err error) { Callback: func(writer http.ResponseWriter, request *http.Request) (htmlOut string, err error) {
systemCritical := config.GetSystemCriticalConfig()
limit := request.FormValue("limit") limit := request.FormValue("limit")
if limit == "" { if limit == "" {
limit = "50" limit = "50"
@ -722,7 +734,7 @@ var actions = map[string]Action{
for _, recentpost := range recentposts { for _, recentpost := range recentposts {
htmlOut += fmt.Sprintf( htmlOut += fmt.Sprintf(
`<tr><td><b>Post:</b> <a href="%s">%s/%d</a><br /><b>IP:</b> %s</td><td>%s</td><td>%s</td></tr>`, `<tr><td><b>Post:</b> <a href="%s">%s/%d</a><br /><b>IP:</b> %s</td><td>%s</td><td>%s</td></tr>`,
path.Join(config.Config.SiteWebfolder, recentpost.BoardName, "/res/", strconv.Itoa(recentpost.ParentID)+".html#"+strconv.Itoa(recentpost.PostID)), path.Join(systemCritical.WebRoot, recentpost.BoardName, "/res/", strconv.Itoa(recentpost.ParentID)+".html#"+strconv.Itoa(recentpost.PostID)),
recentpost.BoardName, recentpost.PostID, recentpost.IP, string(recentpost.Message), recentpost.BoardName, recentpost.PostID, recentpost.IP, string(recentpost.Message),
recentpost.Timestamp.Format("01/02/06, 15:04"), recentpost.Timestamp.Format("01/02/06, 15:04"),
) )
@ -759,7 +771,7 @@ var actions = map[string]Action{
gclog.Print(gclog.LErrorLog, "Error getting staff list: ", err.Error())) gclog.Print(gclog.LErrorLog, "Error getting staff list: ", err.Error()))
return "", err return "", err
} }
boardConfig := config.GetBoardConfig("")
for _, staff := range allStaff { for _, staff := range allStaff {
username := request.FormValue("username") username := request.FormValue("username")
password := request.FormValue("password") password := request.FormValue("password")
@ -789,7 +801,7 @@ var actions = map[string]Action{
} }
htmlOut += fmt.Sprintf( htmlOut += fmt.Sprintf(
`<tr><td>%s</td><td>%s</td><td>%s</td><td><a href="/manage?action=staff&amp;do=del&amp;username=%s" style="float:right;color:red;">X</a></td></tr>`, `<tr><td>%s</td><td>%s</td><td>%s</td><td><a href="/manage?action=staff&amp;do=del&amp;username=%s" style="float:right;color:red;">X</a></td></tr>`,
staff.Username, rank, staff.AddedOn.Format(config.Config.DateTimeFormat), staff.Username) staff.Username, rank, staff.AddedOn.Format(boardConfig.DateTimeFormat), staff.Username)
} }
htmlOut += `</table><hr /><h2 class="manage-header">Add new staff</h2>` + htmlOut += `</table><hr /><h2 class="manage-header">Add new staff</h2>` +

View file

@ -65,7 +65,10 @@ func CallManageFunction(writer http.ResponseWriter, request *http.Request) {
if !handler.isJSON { if !handler.isJSON {
managePageBuffer.WriteString("<!DOCTYPE html><html><head>") managePageBuffer.WriteString("<!DOCTYPE html><html><head>")
if err = gctemplates.ManageHeader.Execute(&managePageBuffer, config.Config); err != nil { criticalCfg := config.GetSystemCriticalConfig()
if err = gctemplates.ManageHeader.Execute(&managePageBuffer, map[string]interface{}{
"webroot": criticalCfg.WebRoot,
}); err != nil {
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog|gclog.LStaffLog, serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog|gclog.LStaffLog,
"Error executing manage page header template: ", err.Error())) "Error executing manage page header template: ", err.Error()))
return return

View file

@ -42,15 +42,16 @@ func createSession(key, username, password string, request *http.Request, writer
} }
// successful login, add cookie that expires in one month // successful login, add cookie that expires in one month
systemCritical := config.GetSystemCriticalConfig()
maxAge, err := gcutil.ParseDurationString(config.Config.CookieMaxAge) siteConfig := config.GetSiteConfig()
maxAge, err := gcutil.ParseDurationString(siteConfig.CookieMaxAge)
if err != nil { if err != nil {
maxAge = gcutil.DefaultMaxAge maxAge = gcutil.DefaultMaxAge
} }
http.SetCookie(writer, &http.Cookie{ http.SetCookie(writer, &http.Cookie{
Name: "sessiondata", Name: "sessiondata",
Value: key, Value: key,
Path: "/", Path: systemCritical.WebRoot,
Domain: domain, Domain: domain,
MaxAge: int(maxAge), MaxAge: int(maxAge),
}) })

View file

@ -29,7 +29,9 @@ func BanHandler(writer http.ResponseWriter, request *http.Request) {
// banStatus, err := getBannedStatus(request) TODO refactor to use ipban // banStatus, err := getBannedStatus(request) TODO refactor to use ipban
var banStatus gcsql.BanInfo var banStatus gcsql.BanInfo
var err error var err error
systemCritical := config.GetSystemCriticalConfig()
siteConfig := config.GetSiteConfig()
boardConfig := config.GetBoardConfig("")
if appealMsg != "" { if appealMsg != "" {
if banStatus.BannedForever() { if banStatus.BannedForever() {
fmt.Fprint(writer, "No.") fmt.Fprint(writer, "No.")
@ -40,7 +42,7 @@ func BanHandler(writer http.ResponseWriter, request *http.Request) {
serverutil.ServeErrorPage(writer, err.Error()) serverutil.ServeErrorPage(writer, err.Error())
} }
fmt.Fprint(writer, fmt.Fprint(writer,
"Appeal sent. It will (hopefully) be read by a staff member. check "+config.Config.SiteWebfolder+"banned occasionally for a response", "Appeal sent. It will (hopefully) be read by a staff member. check "+systemCritical.WebRoot+"banned occasionally for a response",
) )
return return
} }
@ -52,7 +54,12 @@ func BanHandler(writer http.ResponseWriter, request *http.Request) {
} }
if err = serverutil.MinifyTemplate(gctemplates.Banpage, map[string]interface{}{ if err = serverutil.MinifyTemplate(gctemplates.Banpage, map[string]interface{}{
"config": config.Config, "ban": banStatus, "banBoards": banStatus.Boards, "post": gcsql.Post{}, "systemCritical": systemCritical,
"siteConfig": siteConfig,
"boardConfig": boardConfig,
"ban": banStatus,
"banBoards": banStatus.Boards,
"post": gcsql.Post{},
}, writer, "text/html"); err != nil { }, writer, "text/html"); err != nil {
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog, serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
"Error minifying page template: ", err.Error())) "Error minifying page template: ", err.Error()))

View file

@ -31,18 +31,20 @@ type captchaJSON struct {
// InitCaptcha prepares the captcha driver for use // InitCaptcha prepares the captcha driver for use
func InitCaptcha() { func InitCaptcha() {
if !config.Config.UseCaptcha { boardConfig := config.GetBoardConfig("")
if !boardConfig.UseCaptcha {
return return
} }
driver = base64Captcha.NewDriverString( driver = base64Captcha.NewDriverString(
config.Config.CaptchaHeight, config.Config.CaptchaWidth, 0, 0, 6, boardConfig.CaptchaHeight, boardConfig.CaptchaWidth, 0, 0, 6,
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
&color.RGBA{0, 0, 0, 0}, nil).ConvertFonts() &color.RGBA{0, 0, 0, 0}, nil).ConvertFonts()
} }
// ServeCaptcha handles requests to /captcha if UseCaptcha is enabled in gochan.json // ServeCaptcha handles requests to /captcha if UseCaptcha is enabled in gochan.json
func ServeCaptcha(writer http.ResponseWriter, request *http.Request) { func ServeCaptcha(writer http.ResponseWriter, request *http.Request) {
if !config.Config.UseCaptcha { boardConfig := config.GetBoardConfig("")
if !boardConfig.UseCaptcha {
return return
} }
var err error var err error
@ -109,7 +111,8 @@ func ServeCaptcha(writer http.ResponseWriter, request *http.Request) {
} }
func getCaptchaImage() (captchaID, chaptchaB64 string) { func getCaptchaImage() (captchaID, chaptchaB64 string) {
if !config.Config.UseCaptcha { boardConfig := config.GetBoardConfig("")
if !boardConfig.UseCaptcha {
return return
} }
captcha := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore) captcha := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)

View file

@ -32,7 +32,7 @@ type MessageFormatter struct {
} }
func (mf *MessageFormatter) InitBBcode() { func (mf *MessageFormatter) InitBBcode() {
if config.Config.DisableBBcode { if config.GetBoardConfig("").DisableBBcode {
return return
} }
mf.bbCompiler = bbcode.NewCompiler(true, true) mf.bbCompiler = bbcode.NewCompiler(true, true)
@ -45,7 +45,7 @@ func (mf *MessageFormatter) InitBBcode() {
} }
func (mf *MessageFormatter) Compile(msg string) string { func (mf *MessageFormatter) Compile(msg string) string {
if config.Config.DisableBBcode { if config.GetBoardConfig("").DisableBBcode {
return msg return msg
} }
return mf.bbCompiler.Compile(msg) return mf.bbCompiler.Compile(msg)
@ -59,6 +59,7 @@ func FormatMessage(message string) template.HTML {
trimmedLine := strings.TrimSpace(line) trimmedLine := strings.TrimSpace(line)
lineWords := strings.Split(trimmedLine, " ") lineWords := strings.Split(trimmedLine, " ")
isGreentext := false // if true, append </span> to end of line isGreentext := false // if true, append </span> to end of line
WebRoot := config.GetSystemCriticalConfig().WebRoot
for w, word := range lineWords { for w, word := range lineWords {
if strings.LastIndex(word, "&gt;&gt;") == 0 { if strings.LastIndex(word, "&gt;&gt;") == 0 {
//word is a backlink //word is a backlink
@ -79,9 +80,9 @@ func FormatMessage(message string) template.HTML {
if !boardIDFound { if !boardIDFound {
lineWords[w] = `<a href="javascript:;"><strike>` + word + `</strike></a>` lineWords[w] = `<a href="javascript:;"><strike>` + word + `</strike></a>`
} else if linkParent == 0 { } else if linkParent == 0 {
lineWords[w] = `<a href="` + config.Config.SiteWebfolder + boardDir + `/res/` + word[8:] + `.html" class="postref">` + word + `</a>` lineWords[w] = `<a href="` + WebRoot + boardDir + `/res/` + word[8:] + `.html" class="postref">` + word + `</a>`
} else { } else {
lineWords[w] = `<a href="` + config.Config.SiteWebfolder + boardDir + `/res/` + strconv.Itoa(linkParent) + `.html#` + word[8:] + `" class="postref">` + word + `</a>` lineWords[w] = `<a href="` + WebRoot + boardDir + `/res/` + strconv.Itoa(linkParent) + `.html#` + word[8:] + `" class="postref">` + word + `</a>`
} }
} }
} else if strings.Index(word, "&gt;") == 0 && w == 0 { } else if strings.Index(word, "&gt;") == 0 && w == 0 {

View file

@ -42,8 +42,11 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
var nameCookie string var nameCookie string
var formEmail string var formEmail string
systemCritical := config.GetSystemCriticalConfig()
boardConfig := config.GetBoardConfig("")
if request.Method == "GET" { if request.Method == "GET" {
http.Redirect(writer, request, config.Config.SiteWebfolder, http.StatusFound) http.Redirect(writer, request, systemCritical.WebRoot, http.StatusFound)
return return
} }
// fix new cookie domain for when you use a port number // fix new cookie domain for when you use a port number
@ -141,10 +144,10 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
postDelay, _ := gcsql.SinceLastPost(post.ID) postDelay, _ := gcsql.SinceLastPost(post.ID)
if postDelay > -1 { if postDelay > -1 {
if post.ParentID == 0 && postDelay < config.Config.NewThreadDelay { if post.ParentID == 0 && postDelay < boardConfig.NewThreadDelay {
serverutil.ServeErrorPage(writer, "Please wait before making a new thread.") serverutil.ServeErrorPage(writer, "Please wait before making a new thread.")
return return
} else if post.ParentID > 0 && postDelay < config.Config.ReplyDelay { } else if post.ParentID > 0 && postDelay < boardConfig.ReplyDelay {
serverutil.ServeErrorPage(writer, "Please wait before making a reply.") serverutil.ServeErrorPage(writer, "Please wait before making a reply.")
return return
} }
@ -164,7 +167,11 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
var banpageBuffer bytes.Buffer var banpageBuffer bytes.Buffer
if err = serverutil.MinifyTemplate(gctemplates.Banpage, map[string]interface{}{ if err = serverutil.MinifyTemplate(gctemplates.Banpage, map[string]interface{}{
"config": config.Config, "ban": banStatus, "banBoards": boards[post.BoardID-1].Dir, "systemCritical": config.GetSystemCriticalConfig(),
"siteConfig": config.GetSiteConfig(),
"boardConfig": config.GetBoardConfig(""),
"ban": banStatus,
"banBoards": boards[post.BoardID-1].Dir,
}, writer, "text/html"); err != nil { }, writer, "text/html"); err != nil {
serverutil.ServeErrorPage(writer, serverutil.ServeErrorPage(writer,
gclog.Print(gclog.LErrorLog, "Error minifying page: ", err.Error())) gclog.Print(gclog.LErrorLog, "Error minifying page: ", err.Error()))
@ -176,7 +183,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
post.Sanitize() post.Sanitize()
if config.Config.UseCaptcha { if boardConfig.UseCaptcha {
captchaID := request.FormValue("captchaid") captchaID := request.FormValue("captchaid")
captchaAnswer := request.FormValue("captchaanswer") captchaAnswer := request.FormValue("captchaanswer")
if captchaID == "" && captchaAnswer == "" { if captchaID == "" && captchaAnswer == "" {
@ -234,9 +241,9 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
return return
} }
boardDir := _board.Dir boardDir := _board.Dir
filePath = path.Join(config.Config.DocumentRoot, "/"+boardDir+"/src/", post.Filename) filePath = path.Join(systemCritical.DocumentRoot, "/"+boardDir+"/src/", post.Filename)
thumbPath = path.Join(config.Config.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "t."+thumbFiletype, -1)) thumbPath = path.Join(systemCritical.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "t."+thumbFiletype, -1))
catalogThumbPath = path.Join(config.Config.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "c."+thumbFiletype, -1)) catalogThumbPath = path.Join(systemCritical.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "c."+thumbFiletype, -1))
if err = ioutil.WriteFile(filePath, data, 0777); err != nil { if err = ioutil.WriteFile(filePath, data, 0777); err != nil {
gclog.Printf(gclog.LErrorLog, "Couldn't write file %q: %s", post.Filename, err.Error()) gclog.Printf(gclog.LErrorLog, "Couldn't write file %q: %s", post.Filename, err.Error())
@ -265,20 +272,20 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
gclog.Printf(gclog.LAccessLog, "Receiving post with video: %s from %s, referrer: %s", gclog.Printf(gclog.LAccessLog, "Receiving post with video: %s from %s, referrer: %s",
handler.Filename, post.IP, request.Referer()) handler.Filename, post.IP, request.Referer())
if post.ParentID == 0 { if post.ParentID == 0 {
if err := createVideoThumbnail(filePath, thumbPath, config.Config.ThumbWidth); err != nil { if err := createVideoThumbnail(filePath, thumbPath, boardConfig.ThumbWidth); err != nil {
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog, serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
"Error creating video thumbnail: ", err.Error())) "Error creating video thumbnail: ", err.Error()))
return return
} }
} else { } else {
if err := createVideoThumbnail(filePath, thumbPath, config.Config.ThumbWidthReply); err != nil { if err := createVideoThumbnail(filePath, thumbPath, boardConfig.ThumbWidthReply); err != nil {
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog, serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
"Error creating video thumbnail: ", err.Error())) "Error creating video thumbnail: ", err.Error()))
return return
} }
} }
if err := createVideoThumbnail(filePath, catalogThumbPath, config.Config.ThumbWidthCatalog); err != nil { if err := createVideoThumbnail(filePath, catalogThumbPath, boardConfig.ThumbWidthCatalog); err != nil {
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog, serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
"Error creating video thumbnail: ", err.Error())) "Error creating video thumbnail: ", err.Error()))
return return
@ -345,15 +352,15 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
if request.FormValue("spoiler") == "on" { if request.FormValue("spoiler") == "on" {
// If spoiler is enabled, symlink thumbnail to spoiler image // If spoiler is enabled, symlink thumbnail to spoiler image
if _, err := os.Stat(path.Join(config.Config.DocumentRoot, "spoiler.png")); err != nil { if _, err := os.Stat(path.Join(systemCritical.DocumentRoot, "spoiler.png")); err != nil {
serverutil.ServeErrorPage(writer, "missing /spoiler.png") serverutil.ServeErrorPage(writer, "missing /spoiler.png")
return return
} }
if err = syscall.Symlink(path.Join(config.Config.DocumentRoot, "spoiler.png"), thumbPath); err != nil { if err = syscall.Symlink(path.Join(systemCritical.DocumentRoot, "spoiler.png"), thumbPath); err != nil {
serverutil.ServeErrorPage(writer, err.Error()) serverutil.ServeErrorPage(writer, err.Error())
return return
} }
} else if config.Config.ThumbWidth >= post.ImageW && config.Config.ThumbHeight >= post.ImageH { } else if boardConfig.ThumbWidth >= post.ImageW && boardConfig.ThumbHeight >= post.ImageH {
// If image fits in thumbnail size, symlink thumbnail to original // If image fits in thumbnail size, symlink thumbnail to original
post.ThumbW = img.Bounds().Max.X post.ThumbW = img.Bounds().Max.X
post.ThumbH = img.Bounds().Max.Y post.ThumbH = img.Bounds().Max.Y
@ -402,11 +409,11 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
if emailCommand == "noko" { if emailCommand == "noko" {
if post.ParentID < 1 { if post.ParentID < 1 {
http.Redirect(writer, request, config.Config.SiteWebfolder+postBoard.Dir+"/res/"+strconv.Itoa(post.ID)+".html", http.StatusFound) http.Redirect(writer, request, systemCritical.WebRoot+postBoard.Dir+"/res/"+strconv.Itoa(post.ID)+".html", http.StatusFound)
} else { } else {
http.Redirect(writer, request, config.Config.SiteWebfolder+postBoard.Dir+"/res/"+strconv.Itoa(post.ParentID)+".html#"+strconv.Itoa(post.ID), http.StatusFound) http.Redirect(writer, request, systemCritical.WebRoot+postBoard.Dir+"/res/"+strconv.Itoa(post.ParentID)+".html#"+strconv.Itoa(post.ID), http.StatusFound)
} }
} else { } else {
http.Redirect(writer, request, config.Config.SiteWebfolder+postBoard.Dir+"/", http.StatusFound) http.Redirect(writer, request, systemCritical.WebRoot+postBoard.Dir+"/", http.StatusFound)
} }
} }

View file

@ -33,7 +33,8 @@ func tempCleaner() {
continue continue
} }
fileSrc := path.Join(config.Config.DocumentRoot, board.Dir, "src", post.FilenameOriginal) systemCritical := config.GetSystemCriticalConfig()
fileSrc := path.Join(systemCritical.DocumentRoot, board.Dir, "src", post.FilenameOriginal)
if err = os.Remove(fileSrc); err != nil { if err = os.Remove(fileSrc); err != nil {
gclog.Printf(errStdLogs, gclog.Printf(errStdLogs,
"Error pruning temporary upload for %q: %s", fileSrc, err.Error()) "Error pruning temporary upload for %q: %s", fileSrc, err.Error())

View file

@ -16,17 +16,18 @@ import (
func createImageThumbnail(imageObj image.Image, size string) image.Image { func createImageThumbnail(imageObj image.Image, size string) image.Image {
var thumbWidth int var thumbWidth int
var thumbHeight int var thumbHeight int
boardCfg := config.GetBoardConfig("")
switch size { switch size {
case "op": case "op":
thumbWidth = config.Config.ThumbWidth thumbWidth = boardCfg.ThumbWidth
thumbHeight = config.Config.ThumbHeight thumbHeight = boardCfg.ThumbHeight
case "reply": case "reply":
thumbWidth = config.Config.ThumbWidthReply thumbWidth = boardCfg.ThumbWidthReply
thumbHeight = config.Config.ThumbHeightReply thumbHeight = boardCfg.ThumbHeightReply
case "catalog": case "catalog":
thumbWidth = config.Config.ThumbWidthCatalog thumbWidth = boardCfg.ThumbWidthCatalog
thumbHeight = config.Config.ThumbHeightCatalog thumbHeight = boardCfg.ThumbHeightCatalog
} }
oldRect := imageObj.Bounds() oldRect := imageObj.Bounds()
if thumbWidth >= oldRect.Max.X && thumbHeight >= oldRect.Max.Y { if thumbWidth >= oldRect.Max.X && thumbHeight >= oldRect.Max.Y {
@ -82,17 +83,17 @@ func getNewFilename() string {
func getThumbnailSize(w, h int, size string) (newWidth, newHeight int) { func getThumbnailSize(w, h int, size string) (newWidth, newHeight int) {
var thumbWidth int var thumbWidth int
var thumbHeight int var thumbHeight int
boardCfg := config.GetBoardConfig("")
switch { switch {
case size == "op": case size == "op":
thumbWidth = config.Config.ThumbWidth thumbWidth = boardCfg.ThumbWidth
thumbHeight = config.Config.ThumbHeight thumbHeight = boardCfg.ThumbHeight
case size == "reply": case size == "reply":
thumbWidth = config.Config.ThumbWidthReply thumbWidth = boardCfg.ThumbWidthReply
thumbHeight = config.Config.ThumbHeightReply thumbHeight = boardCfg.ThumbHeightReply
case size == "catalog": case size == "catalog":
thumbWidth = config.Config.ThumbWidthCatalog thumbWidth = boardCfg.ThumbWidthCatalog
thumbHeight = config.Config.ThumbHeightCatalog thumbHeight = boardCfg.ThumbHeightCatalog
} }
if w == h { if w == h {
newWidth = thumbWidth newWidth = thumbWidth

View file

@ -11,12 +11,17 @@ import (
"github.com/gochan-org/gochan/pkg/gclog" "github.com/gochan-org/gochan/pkg/gclog"
) )
var (
ErrBlankAkismetKey = errors.New("blank Akismet key")
ErrInvalidAkismetKey = errors.New("invalid Akismet key")
)
// CheckAkismetAPIKey checks the validity of the Akismet API key given in the config file. // CheckAkismetAPIKey checks the validity of the Akismet API key given in the config file.
func CheckAkismetAPIKey(key string) error { func CheckAkismetAPIKey(key string) error {
if key == "" { if key == "" {
return errors.New("blank key given, Akismet spam checking won't be used") return ErrBlankAkismetKey
} }
resp, err := http.PostForm("https://rest.akismet.com/1.1/verify-key", url.Values{"key": {key}, "blog": {"http://" + config.Config.SiteDomain}}) resp, err := http.PostForm("https://rest.akismet.com/1.1/verify-key", url.Values{"key": {key}, "blog": {"http://" + config.GetSystemCriticalConfig().SiteDomain}})
if err != nil { if err != nil {
return err return err
} }
@ -30,22 +35,22 @@ func CheckAkismetAPIKey(key string) error {
} }
if string(body) == "invalid" { if string(body) == "invalid" {
// This should disable the Akismet checks if the API key is not valid. // This should disable the Akismet checks if the API key is not valid.
errmsg := "Akismet API key is invalid, Akismet spam protection will be disabled." return ErrInvalidAkismetKey
gclog.Print(gclog.LErrorLog, errmsg)
return errors.New(errmsg)
} }
return nil return nil
} }
// CheckPostForSpam checks a given post for spam with Akismet. Only checks if Akismet API key is set. // CheckPostForSpam checks a given post for spam with Akismet. Only checks if Akismet API key is set.
func CheckPostForSpam(userIP, userAgent, referrer, author, email, postContent string) string { func CheckPostForSpam(userIP, userAgent, referrer, author, email, postContent string) string {
if config.Config.AkismetAPIKey != "" { systemCritical := config.GetSystemCriticalConfig()
siteCfg := config.GetSiteConfig()
if siteCfg.AkismetAPIKey != "" {
client := &http.Client{} client := &http.Client{}
data := url.Values{"blog": {"http://" + config.Config.SiteDomain}, "user_ip": {userIP}, "user_agent": {userAgent}, "referrer": {referrer}, data := url.Values{"blog": {"http://" + systemCritical.SiteDomain}, "user_ip": {userIP}, "user_agent": {userAgent}, "referrer": {referrer},
"comment_type": {"forum-post"}, "comment_author": {author}, "comment_author_email": {email}, "comment_type": {"forum-post"}, "comment_author": {author}, "comment_author_email": {email},
"comment_content": {postContent}} "comment_content": {postContent}}
req, err := http.NewRequest("POST", "https://"+config.Config.AkismetAPIKey+".rest.akismet.com/1.1/comment-check", req, err := http.NewRequest("POST", "https://"+siteCfg.AkismetAPIKey+".rest.akismet.com/1.1/comment-check",
strings.NewReader(data.Encode())) strings.NewReader(data.Encode()))
if err != nil { if err != nil {
gclog.Print(gclog.LErrorLog, err.Error()) gclog.Print(gclog.LErrorLog, err.Error())
@ -84,7 +89,8 @@ func CheckPostForSpam(userIP, userAgent, referrer, author, email, postContent st
// ValidReferer checks to make sure that the incoming request is from the same domain (or if debug mode is enabled) // ValidReferer checks to make sure that the incoming request is from the same domain (or if debug mode is enabled)
func ValidReferer(request *http.Request) bool { func ValidReferer(request *http.Request) bool {
if config.Config.DebugMode { systemCritical := config.GetSystemCriticalConfig()
if systemCritical.DebugMode {
return true return true
} }
rURL, err := url.ParseRequestURI(request.Referer()) rURL, err := url.ParseRequestURI(request.Referer())
@ -93,5 +99,5 @@ func ValidReferer(request *http.Request) bool {
return false return false
} }
return strings.Index(rURL.Path, config.Config.SiteWebfolder) == 0 return strings.Index(rURL.Path, systemCritical.WebRoot) == 0
} }

View file

@ -15,24 +15,26 @@ var minifier *minify.M
// InitMinifier sets up the HTML/JS/JSON minifier if enabled in gochan.json // InitMinifier sets up the HTML/JS/JSON minifier if enabled in gochan.json
func InitMinifier() { func InitMinifier() {
if !config.Config.MinifyHTML && !config.Config.MinifyJS { siteConfig := config.GetSiteConfig()
if !siteConfig.MinifyHTML && !siteConfig.MinifyJS {
return return
} }
minifier = minify.New() minifier = minify.New()
if config.Config.MinifyHTML { if siteConfig.MinifyHTML {
minifier.AddFunc("text/html", minifyHTML.Minify) minifier.AddFunc("text/html", minifyHTML.Minify)
} }
if config.Config.MinifyJS { if siteConfig.MinifyJS {
minifier.AddFunc("text/javascript", minifyJS.Minify) minifier.AddFunc("text/javascript", minifyJS.Minify)
minifier.AddFunc("application/json", minifyJSON.Minify) minifier.AddFunc("application/json", minifyJSON.Minify)
} }
} }
func canMinify(mediaType string) bool { func canMinify(mediaType string) bool {
if mediaType == "text/html" && config.Config.MinifyHTML { siteConfig := config.GetSiteConfig()
if mediaType == "text/html" && siteConfig.MinifyHTML {
return true return true
} }
if (mediaType == "application/json" || mediaType == "text/javascript") && config.Config.MinifyJS { if (mediaType == "application/json" || mediaType == "text/javascript") && siteConfig.MinifyJS {
return true return true
} }
return false return false

View file

@ -13,8 +13,10 @@ import (
// ServeErrorPage shows a general error page if something goes wrong // ServeErrorPage shows a general error page if something goes wrong
func ServeErrorPage(writer http.ResponseWriter, err string) { func ServeErrorPage(writer http.ResponseWriter, err string) {
MinifyTemplate(gctemplates.ErrorPage, map[string]interface{}{ MinifyTemplate(gctemplates.ErrorPage, map[string]interface{}{
"config": config.Config, "systemCritical": config.GetSystemCriticalConfig(),
"ErrorTitle": "Error :c", "siteConfig": config.GetSiteConfig(),
"boardConfig": config.GetBoardConfig(""),
"ErrorTitle": "Error :c",
// "ErrorImage": "/error/lol 404.gif", // "ErrorImage": "/error/lol 404.gif",
"ErrorHeader": "Error", "ErrorHeader": "Error",
"ErrorText": err, "ErrorText": err,
@ -25,7 +27,8 @@ func ServeErrorPage(writer http.ResponseWriter, err string) {
func ServeNotFound(writer http.ResponseWriter, request *http.Request) { func ServeNotFound(writer http.ResponseWriter, request *http.Request) {
writer.Header().Add("Content-Type", "text/html; charset=utf-8") writer.Header().Add("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(404) writer.WriteHeader(404)
errorPage, err := ioutil.ReadFile(config.Config.DocumentRoot + "/error/404.html") systemCritical := config.GetSystemCriticalConfig()
errorPage, err := ioutil.ReadFile(systemCritical.DocumentRoot + "/error/404.html")
if err != nil { if err != nil {
writer.Write([]byte("Requested page not found, and /error/404.html not found")) writer.Write([]byte("Requested page not found, and /error/404.html not found"))
} else { } else {

View file

@ -29,7 +29,7 @@
"SiteSlogan": "", "SiteSlogan": "",
"SiteDomain": "127.0.0.1", "SiteDomain": "127.0.0.1",
"SiteHeaderURL": "", "SiteHeaderURL": "",
"SiteWebfolder": "/", "WebRoot": "/",
"Styles": [ "Styles": [
{ "Name": "Pipes", "Filename": "pipes.css" }, { "Name": "Pipes", "Filename": "pipes.css" },
@ -55,10 +55,10 @@
"ThumbWidth": 200, "ThumbWidth": 200,
"ThumbHeight": 200, "ThumbHeight": 200,
"ThumbWidth_reply": 125, "ThumbWidthReply": 125,
"ThumbHeight_reply": 125, "ThumbHeightReply": 125,
"ThumbWidth_catalog": 50, "ThumbWidthCatalog": 50,
"ThumbHeight_catalog": 50, "ThumbHeightCatalog": 50,
"ThreadsPerPage": 15, "ThreadsPerPage": 15,
"PostsPerThreadPage": 50, "PostsPerThreadPage": 50,
@ -68,12 +68,11 @@
"admin:#0000A0", "admin:#0000A0",
"somemod:blue" "somemod:blue"
], ],
"BanMsg": "USER WAS BANNED FOR THIS POST", "BanMessage": "USER WAS BANNED FOR THIS POST",
"EnableEmbeds": true,
"EmbedWidth": 200, "EmbedWidth": 200,
"EmbedHeight": 164, "EmbedHeight": 164,
"ExpandButton": true,
"ImagesOpenNewTab": true, "ImagesOpenNewTab": true,
"MakeURLsHyperlinked": true,
"NewTabOnOutlinks": true, "NewTabOnOutlinks": true,
"MinifyHTML": true, "MinifyHTML": true,

View file

@ -2,16 +2,16 @@
<html> <html>
<head> <head>
<title>Banned</title> <title>Banned</title>
<link rel="shortcut icon" href="{{.config.SiteWebfolder}}favicon.png"> <link rel="shortcut icon" href="{{.systemCritical.WebRoot}}favicon.png">
<link rel="stylesheet" href="{{.config.SiteWebfolder}}css/global.css" /> <link rel="stylesheet" href="{{.systemCritical.WebRoot}}css/global.css" />
<link id="theme" rel="stylesheet" href="{{.config.SiteWebfolder}}css/{{.config.DefaultStyle}}" /> <link id="theme" rel="stylesheet" href="{{.systemCritical.WebRoot}}css/{{.boardConfig.DefaultStyle}}" />
<script type="text/javascript" src="{{.config.SiteWebfolder}}js/consts.js"></script> <script type="text/javascript" src="{{.systemCritical.WebRoot}}js/consts.js"></script>
<script type="text/javascript" src="{{.config.SiteWebfolder}}js/gochan.js"></script> <script type="text/javascript" src="{{.systemCritical.WebRoot}}js/gochan.js"></script>
</head> </head>
<body> <body>
<div id="top-pane"> <div id="top-pane">
<span id="site-title">{{.config.SiteName}}</span><br /> <span id="site-title">{{.siteConfig.SiteName}}</span><br />
<span id="site-slogan">{{.config.SiteSlogan}}</span> <span id="site-slogan">{{.siteConfig.SiteSlogan}}</span>
</div><br /> </div><br />
<div class="section-block" style="margin: 0px 26px 0px 24px"> <div class="section-block" style="margin: 0px 26px 0px 24px">
<div class="section-title-block"> <div class="section-title-block">
@ -35,9 +35,9 @@
</div>{{if bannedForever .ban}} </div>{{if bannedForever .ban}}
<img id="banpage-image" src="/permabanned.jpg" style="float:right; margin: 4px 8px 8px 4px"/><br /> <img id="banpage-image" src="/permabanned.jpg" style="float:right; margin: 4px 8px 8px 4px"/><br />
<audio id="jack" preload="auto" autobuffer loop> <audio id="jack" preload="auto" autobuffer loop>
<source src="{{.config.SiteWebfolder}}hittheroad.ogg" /> <source src="{{.systemCritical.WebRoot}}hittheroad.ogg" />
<source src="{{.config.SiteWebfolder}}hittheroad.wav" /> <source src="{{.systemCritical.WebRoot}}hittheroad.wav" />
<source src="{{.config.SiteWebfolder}}hittheroad.mp3" /> <source src="{{.systemCritical.WebRoot}}hittheroad.mp3" />
</audio> </audio>
<script type="text/javascript"> <script type="text/javascript">
document.getElementById("jack").play(); document.getElementById("jack").play();

View file

@ -4,7 +4,7 @@
<span id="board-subtitle">{{$.board.Subtitle}}</span> <span id="board-subtitle">{{$.board.Subtitle}}</span>
</header><hr /> </header><hr />
<div id="right-sidelinks"> <div id="right-sidelinks">
<a href="{{.config.SiteWebfolder}}{{.board.Dir}}/catalog.html">Board catalog</a><br /> <a href="{{.webroot}}{{.board.Dir}}/catalog.html">Board catalog</a><br />
</div> </div>
{{- template "postbox.html" .}} {{- template "postbox.html" .}}
<hr /> <hr />
@ -15,13 +15,13 @@
<div class="op-post" id="op{{$op.ID}}"> <div class="op-post" id="op{{$op.ID}}">
{{- if ne $op.Filename "" -}} {{- if ne $op.Filename "" -}}
{{- if ne $op.Filename "deleted"}} {{- if ne $op.Filename "deleted"}}
<div class="file-info">File: <a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$op.Filename}}" target="_blank">{{$op.Filename}}</a> - ({{formatFilesize $op.Filesize}} , {{$op.ImageW}}x{{$op.ImageH}}, {{$op.FilenameOriginal}})</div> <div class="file-info">File: <a href="{{$.webroot}}{{$.board.Dir}}/src/{{$op.Filename}}" target="_blank">{{$op.Filename}}</a> - ({{formatFilesize $op.Filesize}} , {{$op.ImageW}}x{{$op.ImageH}}, {{$op.FilenameOriginal}})</div>
<a class="upload-container" href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$op.Filename}}"><img src="{{$.config.SiteWebfolder}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $op.Filename}}" alt="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$op.Filename}}" width="{{$op.ThumbW}}" height="{{$op.ThumbH}}" class="upload" /></a> <a class="upload-container" href="{{$.webroot}}{{$.board.Dir}}/src/{{$op.Filename}}"><img src="{{$.webroot}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $op.Filename}}" alt="{{$.webroot}}{{$.board.Dir}}/src/{{$op.Filename}}" width="{{$op.ThumbW}}" height="{{$op.ThumbH}}" class="upload" /></a>
{{else}} {{else}}
<div class="file-deleted-box" style="text-align:center;">File removed</div> <div class="file-deleted-box" style="text-align:center;">File removed</div>
{{end}} {{end}}
{{end}} {{end}}
<input type="checkbox" id="check{{$op.ID}}" name="check{{$op.ID}}" /><label class="post-info" for="check{{$op.ID}}"> <span class="subject">{{$op.Subject}}</span> <span class="postername">{{if ne $op.Email ""}}<a href="mailto:{{$op.Email}}">{{end}}{{if ne $op.Name ""}}{{$op.Name}}{{else}}{{if eq $op.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $op.Email ""}}</a>{{end}}</span>{{if ne $op.Tripcode ""}}<span class="tripcode">!{{$op.Tripcode}}</span>{{end}} {{formatTimestamp $op.Timestamp}} </label><a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$op.ID}}.html#{{$op.ID}}">No.</a> <a href="javascript:quote({{$op.ID}})" class="backlink-click">{{$op.ID}}</a> <span class="post-links"> <span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span> <span>[<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$op.ID}}.html">View</a>]</span></span><br /> <input type="checkbox" id="check{{$op.ID}}" name="check{{$op.ID}}" /><label class="post-info" for="check{{$op.ID}}"> <span class="subject">{{$op.Subject}}</span> <span class="postername">{{if ne $op.Email ""}}<a href="mailto:{{$op.Email}}">{{end}}{{if ne $op.Name ""}}{{$op.Name}}{{else}}{{if eq $op.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $op.Email ""}}</a>{{end}}</span>{{if ne $op.Tripcode ""}}<span class="tripcode">!{{$op.Tripcode}}</span>{{end}} {{formatTimestamp $op.Timestamp}} </label><a href="{{$.webroot}}{{$.board.Dir}}/res/{{$op.ID}}.html#{{$op.ID}}">No.</a> <a href="javascript:quote({{$op.ID}})" class="backlink-click">{{$op.ID}}</a> <span class="post-links"> <span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span> <span>[<a href="{{$.webroot}}{{$.board.Dir}}/res/{{$op.ID}}.html">View</a>]</span></span><br />
<div class="post-text">{{truncateHTMLMessage $op.MessageHTML 2222 18}}</div> <div class="post-text">{{truncateHTMLMessage $op.MessageHTML 2222 18}}</div>
{{- if gt $thread.NumReplies 3}} {{- if gt $thread.NumReplies 3}}
<b>{{subtract $thread.NumReplies 3}} post{{if gt $thread.NumReplies 4}}s{{end}} omitted</b> <b>{{subtract $thread.NumReplies 3}} post{{if gt $thread.NumReplies 4}}s{{end}} omitted</b>
@ -31,11 +31,11 @@
<div class="reply-container" id="replycontainer{{$reply.ID}}"> <div class="reply-container" id="replycontainer{{$reply.ID}}">
<a class="anchor" id="{{$reply.ID}}"></a> <a class="anchor" id="{{$reply.ID}}"></a>
<div class="reply" id="reply{{$reply.ID}}"> <div class="reply" id="reply{{$reply.ID}}">
<input type="checkbox" id="check{{$reply.ID}}" name="check{{$reply.ID}}" /> <label class="post-info" for="check{{$reply.ID}}"> <span class="subject">{{$reply.Subject}}</span> <span class="postername">{{if ne $reply.Email ""}}<a href="mailto:{{$reply.Email}}">{{end}}{{if ne $reply.Name ""}}{{$reply.Name}}{{else}}{{if eq $reply.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $reply.Email ""}}</a>{{end}}</span>{{if ne $reply.Tripcode ""}}<span class="tripcode">!{{$reply.Tripcode}}</span>{{end}} {{formatTimestamp $reply.Timestamp}} </label><a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$op.ID}}.html#{{$reply.ID}}">No.</a> <a href="javascript:quote({{$reply.ID}})" class="backlink-click">{{$reply.ID}}</a> <span class="post-links"><span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br /> <input type="checkbox" id="check{{$reply.ID}}" name="check{{$reply.ID}}" /> <label class="post-info" for="check{{$reply.ID}}"> <span class="subject">{{$reply.Subject}}</span> <span class="postername">{{if ne $reply.Email ""}}<a href="mailto:{{$reply.Email}}">{{end}}{{if ne $reply.Name ""}}{{$reply.Name}}{{else}}{{if eq $reply.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $reply.Email ""}}</a>{{end}}</span>{{if ne $reply.Tripcode ""}}<span class="tripcode">!{{$reply.Tripcode}}</span>{{end}} {{formatTimestamp $reply.Timestamp}} </label><a href="{{$.webroot}}{{$.board.Dir}}/res/{{$op.ID}}.html#{{$reply.ID}}">No.</a> <a href="javascript:quote({{$reply.ID}})" class="backlink-click">{{$reply.ID}}</a> <span class="post-links"><span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
{{if ne $reply.Filename ""}} {{if ne $reply.Filename ""}}
{{if ne $reply.Filename "deleted" -}} {{if ne $reply.Filename "deleted" -}}
<span class="file-info">File: <a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}})</span><br /> <span class="file-info">File: <a href="{{$.webroot}}{{$.board.Dir}}/src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}})</span><br />
<a class="upload-container" href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$reply.Filename}}"><img src="{{$.config.SiteWebfolder}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $reply.Filename}}" alt="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$reply.Filename}}" width="{{$reply.ThumbW}}" height="{{$reply.ThumbH}}" class="upload" /></a> <a class="upload-container" href="{{$.webroot}}{{$.board.Dir}}/src/{{$reply.Filename}}"><img src="{{$.webroot}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $reply.Filename}}" alt="{{$.webroot}}{{$.board.Dir}}/src/{{$reply.Filename}}" width="{{$reply.ThumbW}}" height="{{$reply.ThumbH}}" class="upload" /></a>
{{else}} {{else}}
<div class="file-deleted-box" style="text-align:center;">File removed</div> <div class="file-deleted-box" style="text-align:center;">File removed</div>
{{end}} {{end}}

View file

@ -4,8 +4,8 @@
<span id="board-subtitle">Catalog</span> <span id="board-subtitle">Catalog</span>
</header><hr /> </header><hr />
<div id="catalog-links" style="float: left;"> <div id="catalog-links" style="float: left;">
[<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}">Return</a>] [<a href="{{$.webroot}}{{$.board.Dir}}">Return</a>]
[<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/catalog.html">Refresh</a>] [<a href="{{$.webroot}}{{$.board.Dir}}/catalog.html">Refresh</a>]
</div> </div>
<div id="catalog-controls" style="float: right;"> <div id="catalog-controls" style="float: right;">
Sort by: <select> Sort by: <select>
@ -16,9 +16,9 @@
</div><hr /> </div><hr />
<div id="content">{{range $_,$thread := .threads}} <div id="content">{{range $_,$thread := .threads}}
<div class="catalog-thread"> <div class="catalog-thread">
<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$thread.ID}}.html"> <a href="{{$.webroot}}{{$.board.Dir}}/res/{{$thread.ID}}.html">
{{if eq $thread.Filename ""}}(No file){{else if eq $thread.Filename "deleted"}}(File deleted){{else}} {{if eq $thread.Filename ""}}(No file){{else if eq $thread.Filename "deleted"}}(File deleted){{else}}
<img src="{{$.config.SiteWebfolder}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $thread.Filename}}" alt="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$thread.Filename}}" width="{{$thread.ThumbW}}" height="{{$thread.ThumbH}}" /> <img src="{{$.webroot}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $thread.Filename}}" alt="{{$.webroot}}{{$.board.Dir}}/src/{{$thread.Filename}}" width="{{$thread.ThumbW}}" height="{{$thread.ThumbH}}" />
{{end}}</a><br /> {{end}}</a><br />
<b>{{if eq $thread.Name ""}}Anonymous{{else}}{{$thread.Name}}{{end}}</b> | <b>R:</b> {{numReplies $.board.ID $thread.ID}}<br /> <b>{{if eq $thread.Name ""}}Anonymous{{else}}{{$thread.Name}}{{end}}</b> | <b>R:</b> {{numReplies $.board.ID $thread.ID}}<br />
{{$thread.MessageHTML}} {{$thread.MessageHTML}}

View file

@ -3,11 +3,11 @@
instead of loading them on every HTML page. instead of loading them on every HTML page.
*/ -}} */ -}}
var styles = [ var styles = [
{{- range $ii, $style := .Styles -}} {{- range $ii, $style := .styles -}}
{{if gt $ii 0}},{{end -}} {{if gt $ii 0}},{{end -}}
{Name: "{{js $style.Name}}", Filename: "{{js $style.Filename}}"} {Name: "{{js $style.Name}}", Filename: "{{js $style.Filename}}"}
{{- end -}} {{- end -}}
]; ];
var defaultStyle = "{{js .DefaultStyle}}"; var defaultStyle = "{{js .default_style}}";
var webroot = "{{js .SiteWebfolder}}"; var webroot = "{{js .webroot}}";
var serverTZ = {{.TimeZone}}; var serverTZ = {{.timezone}};

View file

@ -8,6 +8,6 @@
<h1>{{.ErrorHeader}}</h1> <h1>{{.ErrorHeader}}</h1>
{{/*<img src="{{.ErrorImage}}" border="0" alt="">*/}} {{/*<img src="{{.ErrorImage}}" border="0" alt="">*/}}
<p>{{.ErrorText}}</p> <p>{{.ErrorText}}</p>
<hr><address>http://gochan.org powered by Gochan {{version}}</address> <hr><address>http://{{.systemCritcal.SiteDomain}}{{.systemCritical.WebRoot}} powered by Gochan {{version}}</address>
</body> </body>
</html> </html>

View file

@ -1,7 +1,7 @@
{{- template "page_header.html" .}} {{- template "page_header.html" .}}
<div id="top-pane"> <div id="top-pane">
<span id="site-title">{{.config.SiteName}}</span><br /> <span id="site-title">{{.site_config.SiteName}}</span><br />
<span id="site-slogan">{{.config.SiteSlogan}}</span> <span id="site-slogan">{{.site_config.SiteSlogan}}</span>
</div><br /> </div><br />
<div id="frontpage"> <div id="frontpage">
<div class="section-block" style="margin: 16px 64px 16px 64px;"> <div class="section-block" style="margin: 16px 64px 16px 64px;">
@ -17,8 +17,8 @@
<ul style="float:left; list-style: none"> <ul style="float:left; list-style: none">
<li style="text-align: center; font-weight: bold"><b><u>{{$section.Name}}</u></b></li> <li style="text-align: center; font-weight: bold"><b><u>{{$section.Name}}</u></b></li>
{{range $_, $board := $.boards}} {{range $_, $board := $.boards}}
{{if and (eq $board.Section $section.ID) (ne $board.Dir $.config.Modboard)}} {{if and (eq $board.Section $section.ID) (ne $board.Dir $.site_config.Modboard)}}
<li><a href="{{$.config.SiteWebfolder}}{{$board.Dir}}/" title="{{$board.Description}}">/{{$board.Dir}}/</a> — {{$board.Title}}</li> <li><a href="{{$.webroot}}{{$board.Dir}}/" title="{{$board.Description}}">/{{$board.Dir}}/</a> — {{$board.Title}}</li>
{{end}} {{end}}
{{end}} {{end}}
</ul> </ul>
@ -26,7 +26,7 @@
{{end}} {{end}}
</div> </div>
</div> </div>
{{- if gt .config.MaxRecentPosts 0}} {{- if gt .site_config.MaxRecentPosts 0}}
<div class="section-block"> <div class="section-block">
<div class="section-title-block"><b>Recent Posts</b></div> <div class="section-title-block"><b>Recent Posts</b></div>
<div class="section-body"> <div class="section-body">
@ -34,11 +34,11 @@
{{- range $i, $post := $.recent_posts}}{{$postURL := getPostURL $post "recent" false}} {{- range $i, $post := $.recent_posts}}{{$postURL := getPostURL $post "recent" false}}
<div class="recent-post"> <div class="recent-post">
{{if and (ne $post.Filename "deleted") (ne $post.Filename "") -}} {{if and (ne $post.Filename "deleted") (ne $post.Filename "") -}}
<a href="{{$postURL}}" class="front-reply" target="_blank"><img src="{{$.config.SiteWebfolder}}{{$post.BoardName}}/thumb/{{getThreadThumbnail $post.Filename}}" alt="post thumbnail"/></a><br /> <a href="{{$postURL}}" class="front-reply" target="_blank"><img src="{{$.webroot}}{{$post.BoardName}}/thumb/{{getThreadThumbnail $post.Filename}}" alt="post thumbnail"/></a><br />
{{else}} {{else}}
<div class="file-deleted-box" style="text-align:center; float:none;"><a href="{{$postURL}}" class="front-reply" target="_blank">No file</a></div> <div class="file-deleted-box" style="text-align:center; float:none;"><a href="{{$postURL}}" class="front-reply" target="_blank">No file</a></div>
{{- end}}<br /> {{- end}}<br />
<a href="{{$.config.SiteWebfolder}}{{$post.BoardName}}/">/{{$post.BoardName}}/</a><hr /> <a href="{{$.webroot}}{{$post.BoardName}}/">/{{$post.BoardName}}/</a><hr />
{{truncateMessage (stripHTML $post.Message) 40 4}} {{truncateMessage (stripHTML $post.Message) 40 4}}
</div>{{end}} </div>{{end}}
</div> </div>

View file

@ -12,7 +12,7 @@
<tr><td>Description</td><td><input type="text" name="description" value="{{$.board.Description}}" /></td></tr> <tr><td>Description</td><td><input type="text" name="description" value="{{$.board.Description}}" /></td></tr>
<tr><td>Max image size</td><td><input type="text" name="maximagesize" value="{{$.board.MaxFilesize}}" /></td></tr> <tr><td>Max image size</td><td><input type="text" name="maximagesize" value="{{$.board.MaxFilesize}}" /></td></tr>
<tr><td>Max pages</td><td><input type="text" name="maxpages" value="{{$.board.MaxPages}}" /></td></tr> <tr><td>Max pages</td><td><input type="text" name="maxpages" value="{{$.board.MaxPages}}" /></td></tr>
<tr><td>Default style</td><td><select name="defaultstyle">{{range $_, $style := $.config.Styles}} <tr><td>Default style</td><td><select name="defaultstyle">{{range $_, $style := $.boardConfig.Styles}}
<option value="{{$style.Filename}}">{{$style.Name}} ({{$style.Filename}})</option>{{end}} <option value="{{$style.Filename}}">{{$style.Name}} ({{$style.Filename}})</option>{{end}}
</select></td></tr> </select></td></tr>
<tr><td>Locked</td><td><input type="checkbox" name="locked" {{if $.board.Locked}}checked{{end}}/></td></tr> <tr><td>Locked</td><td><input type="checkbox" name="locked" {{if $.board.Locked}}checked{{end}}/></td></tr>

View file

@ -1,8 +1,8 @@
<title>Gochan Manage page</title> <title>Gochan Manage page</title>
<link rel="stylesheet" href="{{.SiteWebfolder}}css/global.css" /> <link rel="stylesheet" href="{{.WebRoot}}css/global.css" />
<link id="theme" rel="stylesheet" href="{{.SiteWebfolder}}css/{{.DefaultStyle}}" /> <link id="theme" rel="stylesheet" href="{{.WebRoot}}css/{{.DefaultStyle}}" />
<link rel="shortcut icon" href="{{.SiteWebfolder}}favicon.png" /> <link rel="shortcut icon" href="{{.WebRoot}}favicon.png" />
<script type="text/javascript" src="{{.SiteWebfolder}}js/consts.js"></script> <script type="text/javascript" src="{{.WebRoot}}js/consts.js"></script>
<script type="text/javascript" src="{{.SiteWebfolder}}js/gochan.js"></script> <script type="text/javascript" src="{{.WebRoot}}js/gochan.js"></script>
</head> </head>
<body> <body>

View file

@ -1,5 +1,5 @@
<div id="footer"> <div id="footer">
<a href="{{$.config.SiteWebfolder}}">Home</a> | <a href="{{$.config.SiteWebfolder}}#boards">Boards</a> | <a href="{{$.config.SiteWebfolder}}#rules">Rules</a> | <a href="{{$.config.SiteWebfolder}}#faq">FAQ</a><br /> <a href="{{$.webroot}}">Home</a> | <a href="{{$.webroot}}#boards">Boards</a> | <a href="{{$.webroot}}#rules">Rules</a> | <a href="{{$.webroot}}#faq">FAQ</a><br />
Powered by <a href="http://github.com/eggbertx/gochan/">Gochan {{version}}</a><br /> Powered by <a href="http://github.com/eggbertx/gochan/">Gochan {{version}}</a><br />
</div> </div>
</body> </body>

View file

@ -9,14 +9,14 @@
{{- else if ne $.op.MessageHTML "" -}}<title>/{{$.board.Dir}}/ - {{truncateString $.op.MessageText 20 true}}</title> {{- else if ne $.op.MessageHTML "" -}}<title>/{{$.board.Dir}}/ - {{truncateString $.op.MessageText 20 true}}</title>
{{- else}}<title>/{{$.board.Dir}}/ - #{{$.op.ID}}</title>{{end}} {{- else}}<title>/{{$.board.Dir}}/ - #{{$.op.ID}}</title>{{end}}
{{- else}}<title>/{{$.board.Dir}}/ - {{$.board.Title}}</title>{{end}} {{- else}}<title>/{{$.board.Dir}}/ - {{$.board.Title}}</title>{{end}}
{{- else}}<title>{{.config.SiteName}}</title>{{end}} {{- else}}<title>{{.site_config.SiteName}}</title>{{end}}
<link rel="stylesheet" href="{{.config.SiteWebfolder}}css/global.css" /> <link rel="stylesheet" href="{{$.webroot}}css/global.css" />
<link id="theme" rel="stylesheet" href="{{.config.SiteWebfolder}}css/{{.config.DefaultStyle}}" /> <link id="theme" rel="stylesheet" href="{{.webroot}}css/{{.board_config.DefaultStyle}}" />
<link rel="shortcut icon" href="{{.config.SiteWebfolder}}favicon.png"> <link rel="shortcut icon" href="{{.webroot}}favicon.png">
<script type="text/javascript" src="{{$.config.SiteWebfolder}}js/consts.js"></script> <script type="text/javascript" src="{{$.webroot}}js/consts.js"></script>
<script type="text/javascript" src="{{$.config.SiteWebfolder}}js/gochan.js"></script> <script type="text/javascript" src="{{$.webroot}}js/gochan.js"></script>
</head> </head>
<body> <body>
<div id="topbar"> <div id="topbar">
{{range $i, $board := .boards}}<a href="{{$.config.SiteWebfolder}}{{$board.Dir}}/" class="topbar-item">/{{$board.Dir}}/</a>{{end}} {{range $i, $board := .boards}}<a href="{{$.webroot}}{{$board.Dir}}/" class="topbar-item">/{{$board.Dir}}/</a>{{end}}
</div> </div>

View file

@ -4,10 +4,10 @@
<span id="board-subtitle">{{$.board.Subtitle}}</span> <span id="board-subtitle">{{$.board.Subtitle}}</span>
</header><hr /> </header><hr />
<div id="threadlinks-top"> <div id="threadlinks-top">
<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/1.html" >Return</a><br /> <a href="{{$.webroot}}{{$.board.Dir}}/1.html" >Return</a><br />
</div> </div>
<div id="right-sidelinks"> <div id="right-sidelinks">
<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/catalog.html">Board catalog</a><br /> <a href="{{$.webroot}}{{$.board.Dir}}/catalog.html">Board catalog</a><br />
</div> </div>
{{template "postbox.html" .}}<hr /> {{template "postbox.html" .}}<hr />
<div id="content"> <div id="content">
@ -17,23 +17,23 @@
{{if ne $.op.Filename ""}} {{if ne $.op.Filename ""}}
{{- if ne $.op.Filename "deleted" -}} {{- if ne $.op.Filename "deleted" -}}
<div class="file-info">File: <a href="../src/{{.op.Filename}}" target="_blank">{{$.op.Filename}}</a> - ({{formatFilesize $.op.Filesize}} , {{$.op.ImageW}}x{{$.op.ImageH}}, {{$.op.FilenameOriginal}})</div> <div class="file-info">File: <a href="../src/{{.op.Filename}}" target="_blank">{{$.op.Filename}}</a> - ({{formatFilesize $.op.Filesize}} , {{$.op.ImageW}}x{{$.op.ImageH}}, {{$.op.FilenameOriginal}})</div>
<a class="upload-container" href="{{.config.SiteWebfolder}}{{.board.Dir}}/src/{{.op.Filename}}"><img src="{{.config.SiteWebfolder}}{{.board.Dir}}/thumb/{{getThreadThumbnail .op.Filename}}" alt="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{.op.Filename}}" width="{{.op.ThumbW}}" height="{{.op.ThumbH}}" class="upload" /></a> <a class="upload-container" href="{{.webroot}}{{.board.Dir}}/src/{{.op.Filename}}"><img src="{{.webroot}}{{.board.Dir}}/thumb/{{getThreadThumbnail .op.Filename}}" alt="{{$.webroot}}{{$.board.Dir}}/src/{{.op.Filename}}" width="{{.op.ThumbW}}" height="{{.op.ThumbH}}" class="upload" /></a>
{{- else}} {{- else}}
<div class="file-deleted-box" style="text-align:center;">File removed</div> <div class="file-deleted-box" style="text-align:center;">File removed</div>
{{end}} {{end}}
{{end -}} {{end -}}
<input type="checkbox" id="check{{.op.ID}}" name="check{{.op.ID}}" /><label class="post-info" for="check{{.op.ID}}"> <span class="subject">{{.op.Subject}}</span> <span class="postername">{{if ne .op.Email ""}}<a href="mailto:{{.op.Email}}">{{end}}{{if ne .op.Name ""}}{{.op.Name}}{{else}}{{if eq .op.Tripcode ""}}{{.board.Anonymous}}{{end}}{{end}}{{if ne .op.Email ""}}</a>{{end}}</span>{{if ne .op.Tripcode ""}}<span class="tripcode">!{{.op.Tripcode}}</span>{{end}} {{formatTimestamp .op.Timestamp}} </label><a href="{{$.config.SiteWebfolder}}{{.board.Dir}}/res/{{.op.ID}}.html#{{.op.ID}}">No.</a> <a href="javascript:quote({{.op.ID}})" class="backlink-click">{{.op.ID}}</a> <span class="post-links"> <span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br /> <input type="checkbox" id="check{{.op.ID}}" name="check{{.op.ID}}" /><label class="post-info" for="check{{.op.ID}}"> <span class="subject">{{.op.Subject}}</span> <span class="postername">{{if ne .op.Email ""}}<a href="mailto:{{.op.Email}}">{{end}}{{if ne .op.Name ""}}{{.op.Name}}{{else}}{{if eq .op.Tripcode ""}}{{.board.Anonymous}}{{end}}{{end}}{{if ne .op.Email ""}}</a>{{end}}</span>{{if ne .op.Tripcode ""}}<span class="tripcode">!{{.op.Tripcode}}</span>{{end}} {{formatTimestamp .op.Timestamp}} </label><a href="{{$.webroot}}{{.board.Dir}}/res/{{.op.ID}}.html#{{.op.ID}}">No.</a> <a href="javascript:quote({{.op.ID}})" class="backlink-click">{{.op.ID}}</a> <span class="post-links"> <span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
<div class="post-text">{{.op.MessageHTML}}</div> <div class="post-text">{{.op.MessageHTML}}</div>
</div> </div>
{{range $reply_num,$reply := .posts -}} {{range $reply_num,$reply := .posts -}}
<div class="reply-container" id="replycontainer{{$reply.ID}}"> <div class="reply-container" id="replycontainer{{$reply.ID}}">
<a class="anchor" id="{{$reply.ID}}"></a> <a class="anchor" id="{{$reply.ID}}"></a>
<div class="reply" id="reply{{$reply.ID}}"> <div class="reply" id="reply{{$reply.ID}}">
<input type="checkbox" id="check{{$reply.ID}}" name="check{{$reply.ID}}" /> <label class="post-info" for="check{{$reply.ID}}"> <span class="subject">{{$reply.Subject}}</span> <span class="postername">{{if ne $reply.Email ""}}<a href="mailto:{{$reply.Email}}">{{end}}{{if ne $reply.Name ""}}{{$reply.Name}}{{else}}{{if eq $reply.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $reply.Email ""}}</a>{{end}}</span>{{if ne $reply.Tripcode ""}}<span class="tripcode">!{{$reply.Tripcode}}</span>{{end}} {{formatTimestamp $reply.Timestamp}} </label><a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$.op.ID}}.html#{{$reply.ID}}">No.</a> <a href="javascript:quote({{$reply.ID}})" class="backlink-click">{{$reply.ID}}</a> <span class="post-links"><span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br /> <input type="checkbox" id="check{{$reply.ID}}" name="check{{$reply.ID}}" /> <label class="post-info" for="check{{$reply.ID}}"> <span class="subject">{{$reply.Subject}}</span> <span class="postername">{{if ne $reply.Email ""}}<a href="mailto:{{$reply.Email}}">{{end}}{{if ne $reply.Name ""}}{{$reply.Name}}{{else}}{{if eq $reply.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $reply.Email ""}}</a>{{end}}</span>{{if ne $reply.Tripcode ""}}<span class="tripcode">!{{$reply.Tripcode}}</span>{{end}} {{formatTimestamp $reply.Timestamp}} </label><a href="{{$.webroot}}{{$.board.Dir}}/res/{{$.op.ID}}.html#{{$reply.ID}}">No.</a> <a href="javascript:quote({{$reply.ID}})" class="backlink-click">{{$reply.ID}}</a> <span class="post-links"><span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
{{if ne $reply.Filename ""}} {{if ne $reply.Filename ""}}
{{if ne $reply.Filename "deleted"}} {{if ne $reply.Filename "deleted"}}
<span class="file-info">File: <a href="../src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}})</span><br /> <span class="file-info">File: <a href="../src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}})</span><br />
<a class="upload-container" href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$reply.Filename}}"><img src="{{$.config.SiteWebfolder}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $reply.Filename}}" alt="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$reply.Filename}}" width="{{$reply.ThumbW}}" height="{{$reply.ThumbH}}" class="upload" /></a> <a class="upload-container" href="{{$.webroot}}{{$.board.Dir}}/src/{{$reply.Filename}}"><img src="{{$.webroot}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $reply.Filename}}" alt="{{$.webroot}}{{$.board.Dir}}/src/{{$reply.Filename}}" width="{{$reply.ThumbW}}" height="{{$reply.ThumbH}}" class="upload" /></a>
{{else}} {{else}}
<div class="file-deleted-box" style="text-align:center;">File removed</div> <div class="file-deleted-box" style="text-align:center;">File removed</div>
{{end}}{{end}} {{end}}{{end}}
@ -53,7 +53,7 @@
</div> </div>
</form> </form>
<div id="left-bottom-content"> <div id="left-bottom-content">
<a href="{{.config.SiteWebfolder}}{{.board.Dir}}/">Return</a><br /><br /> <a href="{{.webroot}}{{.board.Dir}}/">Return</a><br /><br />
<span id="boardmenu-bottom"> <span id="boardmenu-bottom">
[{{range $i, $boardlink := .boards -}} [{{range $i, $boardlink := .boards -}}
{{if gt $i 0}}/{{end -}} <a href="/{{$boardlink.Dir}}/">{{$boardlink.Dir}}</a> {{if gt $i 0}}/{{end -}} <a href="/{{$boardlink.Dir}}/">{{$boardlink.Dir}}</a>

View file

@ -0,0 +1,10 @@
{
"dbhost": "127.0.0.1:3306",
"dbtype": "mysql",
"dbusername": "gochan",
"dbpassword": "gochan",
"olddbname": "gochan_pre2021_db",
"newdbname": "gochan",
"oldchan": "pre2021",
"tableprefix": "gc_"
}