mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-02 10:56:25 -07:00
refactor configuration
This commit is contained in:
commit
225d7fb6d3
47 changed files with 1130 additions and 707 deletions
|
@ -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)
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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
|
||||||
|
|
47
cmd/gochan-migration/internal/common/sqlutil.go
Normal file
47
cmd/gochan-migration/internal/common/sqlutil.go
Normal 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)
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
37
config.md
Normal 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.
|
|
@ -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,
|
||||||
|
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(*)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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&do=del&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&do=del&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>` +
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
})
|
})
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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, ">>") == 0 {
|
if strings.LastIndex(word, ">>") == 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, ">") == 0 && w == 0 {
|
} else if strings.Index(word, ">") == 0 && w == 0 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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)">▼</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)">▼</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)">▼</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)">▼</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}}
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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}};
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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)">▼</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)">▼</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)">▼</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)">▼</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>
|
||||||
|
|
10
vagrant/migrationtest/migrationconfig-pre2012.json
Normal file
10
vagrant/migrationtest/migrationconfig-pre2012.json
Normal 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_"
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue