mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-02 02:36:24 -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
|
||||
=======
|
||||
**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
|
||||
|
||||
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.
|
||||
|
||||
## 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).
|
||||
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.
|
||||
See [config.md](config.md)
|
||||
|
||||
## Installation using Docker
|
||||
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
|
||||
func Entry(targetVersion int) error {
|
||||
cfg := config.GetSystemCriticalConfig()
|
||||
gcsql.ConnectToDB(
|
||||
config.Config.DBhost, config.Config.DBtype, config.Config.DBname,
|
||||
config.Config.DBusername, config.Config.DBpassword, config.Config.DBprefix)
|
||||
cfg.DBhost, cfg.DBtype, cfg.DBname,
|
||||
cfg.DBusername, cfg.DBpassword, cfg.DBprefix)
|
||||
|
||||
return runMigration(targetVersion)
|
||||
}
|
||||
|
@ -24,6 +25,7 @@ func runMigration(targetVersion int) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
criticalCfg := config.GetSystemCriticalConfig()
|
||||
switch dbFlags {
|
||||
case gcsql.DBCorrupted:
|
||||
gclog.Println(stdFatalFlag, "Database found is corrupted, please contact the devs.")
|
||||
|
@ -35,7 +37,7 @@ func runMigration(targetVersion int) error {
|
|||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
gclog.Println(gclog.LStdLog, "Finish migrating to version 1.")
|
||||
|
|
|
@ -24,7 +24,7 @@ func (me *MigrationError) OldChanType() string {
|
|||
func (me *MigrationError) Error() string {
|
||||
from := me.oldChanType
|
||||
if from != "" {
|
||||
from = " from " + from + " "
|
||||
from = " from " + from
|
||||
}
|
||||
return "unable to migrate" + from + ": " + me.errMessage
|
||||
}
|
||||
|
@ -34,13 +34,14 @@ func NewMigrationError(oldChanType string, errMessage string) *MigrationError {
|
|||
}
|
||||
|
||||
type DBOptions struct {
|
||||
Host string
|
||||
DBType string
|
||||
Username string
|
||||
Password string
|
||||
OldDBName string
|
||||
OldChanType string
|
||||
NewDBName string
|
||||
Host string `json:"dbhost"`
|
||||
DBType string `json:"dbtype"`
|
||||
Username string `json:"dbusername"`
|
||||
Password string `json:"dbpassword"`
|
||||
OldDBName string `json:"olddbname"`
|
||||
OldChanType string `json:"oldchan"`
|
||||
NewDBName string `json:"newdbname"`
|
||||
TablePrefix string `json:"tableprefix"`
|
||||
}
|
||||
|
||||
// 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 (
|
||||
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"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 {
|
||||
db *gcsql.GCDB
|
||||
options common.DBOptions
|
||||
|
@ -15,12 +25,39 @@ func (m *Pre2021Migrator) Init(options common.DBOptions) error {
|
|||
m.options = options
|
||||
var err error
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -28,5 +65,5 @@ func (m *Pre2021Migrator) Close() error {
|
|||
if m.db != nil {
|
||||
return m.db.Close()
|
||||
}
|
||||
return nil
|
||||
return gcsql.Close()
|
||||
}
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"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/pre2021"
|
||||
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/tinyboard"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -23,6 +25,7 @@ the README and/or the -h command line flag before you use it.
|
|||
|
||||
var (
|
||||
versionStr string
|
||||
bufIn = bufio.NewReader(os.Stdin)
|
||||
)
|
||||
|
||||
func fatalPrintln(args ...interface{}) {
|
||||
|
@ -30,17 +33,43 @@ func fatalPrintln(args ...interface{}) {
|
|||
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() {
|
||||
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.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.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.NewDBName, "newdbname", "", "The name of the new database")
|
||||
flag.StringVar(&options.TablePrefix, "tableprefix", "", "Prefix for the SQL tables' names")
|
||||
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)
|
||||
|
||||
var migrator common.DBMigrator
|
||||
|
@ -61,18 +90,17 @@ func main() {
|
|||
}
|
||||
defer migrator.Close()
|
||||
|
||||
config.InitConfig(versionStr)
|
||||
// config.InitConfig(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
|
||||
if err != nil {
|
||||
gclog.Printf(gclog.LErrorLog, "Error while migrating: %s", err)
|
||||
} */
|
||||
if options.OldDBName == config.Config.DBname {
|
||||
fatalPrintln(
|
||||
"The old database name must not be the same as the new one set in gochan.json")
|
||||
if options.OldDBName == options.NewDBName {
|
||||
fatalPrintln("The old database name must not be the same as the new one.")
|
||||
}
|
||||
if err = migrator.MigrateDB(); err != nil {
|
||||
fatalPrintln("Error migrating database:", err)
|
||||
fatalPrintln(err)
|
||||
}
|
||||
fmt.Println("Database migration successful!")
|
||||
}
|
||||
|
|
|
@ -34,10 +34,12 @@ func main() {
|
|||
gclog.Printf(gclog.LStdLog, "Starting gochan v%s", versionStr)
|
||||
config.InitConfig(versionStr)
|
||||
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
|
||||
gcsql.ConnectToDB(
|
||||
config.Config.DBhost, config.Config.DBtype, config.Config.DBname,
|
||||
config.Config.DBusername, config.Config.DBpassword, config.Config.DBprefix)
|
||||
gcsql.CheckAndInitializeDatabase(config.Config.DBtype)
|
||||
systemCritical.DBhost, systemCritical.DBtype, systemCritical.DBname,
|
||||
systemCritical.DBusername, systemCritical.DBpassword, systemCritical.DBprefix)
|
||||
gcsql.CheckAndInitializeDatabase(systemCritical.DBtype)
|
||||
parseCommandLine()
|
||||
serverutil.InitMinifier()
|
||||
|
||||
|
|
|
@ -32,7 +32,11 @@ type gochanServer struct {
|
|||
}
|
||||
|
||||
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
|
||||
results, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
|
@ -46,7 +50,7 @@ func (s gochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
|
|||
if results.IsDir() {
|
||||
//check to see if one of the specified index pages exists
|
||||
var found bool
|
||||
for _, value := range config.Config.FirstPage {
|
||||
for _, value := range siteConfig.FirstPage {
|
||||
newPath := path.Join(filePath, value)
|
||||
_, err := os.Stat(newPath)
|
||||
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) {
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
for name, namespaceFunction := range s.namespaces {
|
||||
if request.URL.Path == config.Config.SiteWebfolder+name {
|
||||
// writer.WriteHeader(200)
|
||||
if request.URL.Path == systemCritical.WebRoot+name {
|
||||
namespaceFunction(writer, request)
|
||||
return
|
||||
}
|
||||
|
@ -114,17 +118,24 @@ func (s gochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Reques
|
|||
}
|
||||
|
||||
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 {
|
||||
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.namespaces = make(map[string]func(http.ResponseWriter, *http.Request))
|
||||
|
||||
// Check if Akismet API key is usable at startup.
|
||||
if err = serverutil.CheckAkismetAPIKey(config.Config.AkismetAPIKey); err != nil {
|
||||
config.Config.AkismetAPIKey = ""
|
||||
err = serverutil.CheckAkismetAPIKey(siteConfig.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
|
||||
|
@ -137,10 +148,10 @@ func initServer() {
|
|||
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)
|
||||
// or they will be restricted to something like /plugin
|
||||
// Eventually plugins will be able to register new namespaces or they will be restricted to something
|
||||
// like /plugin
|
||||
|
||||
if config.Config.UseFastCGI {
|
||||
if systemCritical.UseFastCGI {
|
||||
err = fcgi.Serve(listener, server)
|
||||
} else {
|
||||
err = http.Serve(listener, server)
|
||||
|
@ -163,10 +174,11 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
reportBtn := request.PostFormValue("report_btn")
|
||||
editBtn := request.PostFormValue("edit_btn")
|
||||
doEdit := request.PostFormValue("doedit")
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
|
||||
if action == "" && deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit" && doEdit != "1" {
|
||||
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
|
||||
}
|
||||
var postsArr []string
|
||||
|
@ -207,9 +219,11 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
}
|
||||
|
||||
if err = gctemplates.PostEdit.Execute(writer, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"post": post,
|
||||
"referrer": request.Referer(),
|
||||
"systemCritical": config.GetSystemCriticalConfig(),
|
||||
"siteConfig": config.GetSiteConfig(),
|
||||
"boardConfig": config.GetBoardConfig(""),
|
||||
"post": post,
|
||||
"referrer": request.Referer(),
|
||||
}); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error executing edit post template: ", err.Error()))
|
||||
|
@ -325,7 +339,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
}
|
||||
if post.ParentID == 0 {
|
||||
os.Remove(path.Join(
|
||||
config.Config.DocumentRoot, board, "/res/"+strconv.Itoa(post.ID)+".html"))
|
||||
systemCritical.DocumentRoot, board, "/res/"+strconv.Itoa(post.ID)+".html"))
|
||||
} else {
|
||||
_board, _ := gcsql.GetBoardFromID(post.BoardID)
|
||||
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
|
||||
|
||||
var numRepliesOnBoardPage int
|
||||
|
||||
// postCfg := config.getpo
|
||||
postCfg := config.GetBoardConfig("").PostConfig
|
||||
if op.Stickied {
|
||||
// If the thread is stickied, limit replies on the archive page to the
|
||||
// configured value for stickied threads.
|
||||
numRepliesOnBoardPage = config.Config.StickyRepliesOnBoardPage
|
||||
numRepliesOnBoardPage = postCfg.StickyRepliesOnBoardPage
|
||||
} else {
|
||||
// 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)
|
||||
|
@ -118,8 +119,8 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
nonStickiedThreads = append(nonStickiedThreads, thread)
|
||||
}
|
||||
}
|
||||
|
||||
gcutil.DeleteMatchingFiles(path.Join(config.Config.DocumentRoot, board.Dir), "\\d.html$")
|
||||
criticalCfg := config.GetSystemCriticalConfig()
|
||||
gcutil.DeleteMatchingFiles(path.Join(criticalCfg.DocumentRoot, board.Dir), "\\d.html$")
|
||||
// Order the threads, stickied threads first, then nonstickied threads.
|
||||
threads = append(stickiedThreads, nonStickiedThreads...)
|
||||
|
||||
|
@ -129,7 +130,7 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
board.CurrentPage = 1
|
||||
|
||||
// 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 {
|
||||
return errors.New(gclog.Printf(gclog.LErrorLog,
|
||||
"Failed opening /%s/board.html: %s",
|
||||
|
@ -139,7 +140,7 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
// Render board page template to the file,
|
||||
// packaging the board/section list, threads, and board info
|
||||
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"webroot": criticalCfg.WebRoot,
|
||||
"boards": gcsql.AllBoards,
|
||||
"sections": gcsql.AllSections,
|
||||
"threads": threads,
|
||||
|
@ -152,13 +153,14 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
}
|
||||
|
||||
// Create the archive pages.
|
||||
threadPages = paginate(config.Config.ThreadsPerPage, threads)
|
||||
boardCfg := config.GetBoardConfig(board.Dir)
|
||||
threadPages = paginate(boardCfg.ThreadsPerPage, threads)
|
||||
board.NumPages = len(threadPages)
|
||||
|
||||
// Create array of page wrapper objects, and open the file.
|
||||
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 {
|
||||
return errors.New(gclog.Printf(gclog.LErrorLog,
|
||||
"Failed opening /%s/catalog.json: %s", board.Dir, err.Error()))
|
||||
|
@ -170,7 +172,7 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
board.CurrentPage++
|
||||
var currentPageFilepath string
|
||||
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)
|
||||
if err != nil {
|
||||
err = errors.New(gclog.Printf(gclog.LErrorLog,
|
||||
|
@ -181,7 +183,7 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
|
||||
// Render the boardpage template
|
||||
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"webroot": criticalCfg.WebRoot,
|
||||
"boards": gcsql.AllBoards,
|
||||
"sections": gcsql.AllSections,
|
||||
"threads": pageThreads,
|
||||
|
@ -259,8 +261,8 @@ func BuildCatalog(boardID int) string {
|
|||
if err = board.PopulateData(boardID); err != nil {
|
||||
return gclog.Printf(gclog.LErrorLog, "Error getting board information (ID: %d)", boardID)
|
||||
}
|
||||
|
||||
catalogPath := path.Join(config.Config.DocumentRoot, board.Dir, "catalog.html")
|
||||
criticalCfg := config.GetSystemCriticalConfig()
|
||||
catalogPath := path.Join(criticalCfg.DocumentRoot, board.Dir, "catalog.html")
|
||||
catalogFile, err := os.OpenFile(catalogPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return gclog.Printf(gclog.LErrorLog,
|
||||
|
@ -285,7 +287,7 @@ func BuildCatalog(boardID int) string {
|
|||
|
||||
if err = serverutil.MinifyTemplate(gctemplates.Catalog, map[string]interface{}{
|
||||
"boards": gcsql.AllBoards,
|
||||
"config": config.Config,
|
||||
"webroot": criticalCfg.WebRoot,
|
||||
"board": board,
|
||||
"sections": gcsql.AllSections,
|
||||
"threads": threadInterfaces,
|
||||
|
|
|
@ -20,8 +20,9 @@ func BuildFrontPage() error {
|
|||
return errors.New(gclog.Print(gclog.LErrorLog,
|
||||
"Error loading front page template: ", err.Error()))
|
||||
}
|
||||
os.Remove(path.Join(config.Config.DocumentRoot, "index.html"))
|
||||
frontFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
criticalCfg := config.GetSystemCriticalConfig()
|
||||
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 {
|
||||
return errors.New(gclog.Print(gclog.LErrorLog,
|
||||
|
@ -30,7 +31,8 @@ func BuildFrontPage() error {
|
|||
defer frontFile.Close()
|
||||
|
||||
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 {
|
||||
return errors.New(gclog.Print(gclog.LErrorLog,
|
||||
"Failed loading recent posts: "+err.Error()))
|
||||
|
@ -43,9 +45,11 @@ func BuildFrontPage() error {
|
|||
}
|
||||
|
||||
if err = serverutil.MinifyTemplate(gctemplates.FrontPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"webroot": criticalCfg.WebRoot,
|
||||
"site_config": siteCfg,
|
||||
"sections": gcsql.AllSections,
|
||||
"boards": gcsql.AllBoards,
|
||||
"board_config": config.GetBoardConfig(""),
|
||||
"recent_posts": recentPostsArr,
|
||||
}, frontFile, "text/html"); err != nil {
|
||||
return errors.New(gclog.Print(gclog.LErrorLog,
|
||||
|
@ -56,7 +60,8 @@ func BuildFrontPage() error {
|
|||
|
||||
// BuildBoardListJSON generates a JSON file with info about the boards
|
||||
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 {
|
||||
return errors.New(
|
||||
gclog.Print(gclog.LErrorLog, "Failed opening boards.json for writing: ", err.Error()))
|
||||
|
@ -67,11 +72,12 @@ func BuildBoardListJSON() error {
|
|||
"boards": []gcsql.Board{},
|
||||
}
|
||||
|
||||
boardCfg := config.GetBoardConfig("")
|
||||
// Our cooldowns are site-wide currently.
|
||||
cooldowns := gcsql.BoardCooldowns{
|
||||
NewThread: config.Config.NewThreadDelay,
|
||||
Reply: config.Config.ReplyDelay,
|
||||
ImageReply: config.Config.ReplyDelay}
|
||||
NewThread: boardCfg.NewThreadDelay,
|
||||
Reply: boardCfg.ReplyDelay,
|
||||
ImageReply: boardCfg.ReplyDelay}
|
||||
|
||||
for b := range gcsql.AllBoards {
|
||||
gcsql.AllBoards[b].Cooldowns = cooldowns
|
||||
|
@ -99,7 +105,10 @@ func BuildJS() error {
|
|||
return errors.New(gclog.Println(gclog.LErrorLog,
|
||||
"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)
|
||||
if err != nil {
|
||||
return errors.New(gclog.Printf(gclog.LErrorLog,
|
||||
|
@ -107,7 +116,14 @@ func BuildJS() error {
|
|||
}
|
||||
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,
|
||||
"Error building %q: %s", constsJSPath, err.Error()))
|
||||
}
|
||||
|
|
|
@ -58,14 +58,15 @@ func BuildThreadPages(op *gcsql.Post) error {
|
|||
return errors.New(gclog.Printf(gclog.LErrorLog,
|
||||
"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{}
|
||||
for _, reply := range replies {
|
||||
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)
|
||||
if err != nil {
|
||||
return errors.New(gclog.Printf(gclog.LErrorLog,
|
||||
|
@ -74,7 +75,7 @@ func BuildThreadPages(op *gcsql.Post) error {
|
|||
|
||||
// render thread page
|
||||
if err = serverutil.MinifyTemplate(gctemplates.ThreadPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"webroot": criticalCfg.WebRoot,
|
||||
"boards": gcsql.AllBoards,
|
||||
"board": board,
|
||||
"sections": gcsql.AllSections,
|
||||
|
@ -86,7 +87,7 @@ func BuildThreadPages(op *gcsql.Post) error {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return errors.New(gclog.Printf(gclog.LErrorLog,
|
||||
"Failed opening /%s/res/%d.json: %s", board.Dir, op.ID, err.Error()))
|
||||
|
|
|
@ -12,155 +12,89 @@ import (
|
|||
const (
|
||||
randomStringSize = 16
|
||||
cookieMaxAgeEx = ` (example: "1 year 2 months 3 days 4 hours", or "1y2mo3d4h"`
|
||||
/* currentConfig = iota
|
||||
oldConfig
|
||||
invalidConfig */
|
||||
)
|
||||
|
||||
var (
|
||||
Config *GochanConfig
|
||||
cfgPath string
|
||||
cfgDefaults = map[string]interface{}{
|
||||
"Port": 8080,
|
||||
"FirstPage": []string{"index.html", "board.html", "firstrun.html"},
|
||||
"DocumentRoot": "html",
|
||||
"TemplateDir": "templates",
|
||||
"CookieMaxAge": "1y",
|
||||
"LogDir": "log",
|
||||
cfg *GochanConfig
|
||||
cfgPath string
|
||||
defaults = map[string]interface{}{
|
||||
"WebRoot": "/",
|
||||
// SiteConfig
|
||||
"FirstPage": []string{"index.html", "firstrun.html", "1.html"},
|
||||
"CookieMaxAge": "1y",
|
||||
"LockdownMessage": "This imageboard has temporarily disabled posting. We apologize for the inconvenience",
|
||||
"SiteName": "Gochan",
|
||||
"MinifyHTML": true,
|
||||
"MinifyJS": true,
|
||||
"MaxRecentPosts": 3,
|
||||
"EnableAppeals": true,
|
||||
"MaxLogDays": 14,
|
||||
|
||||
"SillyTags": []string{},
|
||||
|
||||
"SiteName": "Gochan",
|
||||
"SiteWebFolder": "/",
|
||||
|
||||
"NewThreadDelay": 30,
|
||||
"ReplyDelay": 7,
|
||||
|
||||
"MaxLineLength": 150,
|
||||
|
||||
"ThreadsPerPage": 15,
|
||||
// BoardConfig
|
||||
"DateTimeFormat": "Mon, January 02, 2006 15:04 PM",
|
||||
"CaptchaWidth": 240,
|
||||
"CaptchaHeight": 80,
|
||||
"CaptchaMinutesTimeout": 15,
|
||||
|
||||
// PostConfig
|
||||
"NewThreadDelay": 30,
|
||||
"ReplyDelay": 7,
|
||||
"MaxLineLength": 150,
|
||||
"ThreadsPerPage": 15,
|
||||
"PostsPerThreadPage": 50,
|
||||
"RepliesOnBoardPage": 3,
|
||||
"StickyRepliesOnBoardPage": 1,
|
||||
"BanMessage": "USER WAS BANNED FOR THIS POST",
|
||||
"EmbedWidth": 200,
|
||||
"EmbedHeight": 164,
|
||||
"EnableEmbeds": true,
|
||||
"ImagesOpenNewTab": true,
|
||||
"NewTabOnOutlinks": true,
|
||||
|
||||
// UploadConfig
|
||||
"ThumbWidth": 200,
|
||||
"ThumbHeight": 200,
|
||||
"ThumbWidthReply": 125,
|
||||
"ThumbHeightReply": 125,
|
||||
"ThumbWidthCatalog": 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 {
|
||||
ListenIP string `critical:"true"`
|
||||
Port int `critical:"true"`
|
||||
FirstPage []string `critical:"true"`
|
||||
Username string `critical:"true"`
|
||||
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:"-"`
|
||||
SystemCriticalConfig
|
||||
SiteConfig
|
||||
BoardConfig
|
||||
jsonLocation string `json:"-"`
|
||||
}
|
||||
|
||||
// ToMap returns the configuration file as a map
|
||||
func (cfg *GochanConfig) ToMap() map[string]interface{} {
|
||||
cVal := reflect.ValueOf(cfg).Elem()
|
||||
cType := reflect.TypeOf(*cfg)
|
||||
func (gcfg *GochanConfig) setField(field string, value interface{}) {
|
||||
structValue := reflect.ValueOf(gcfg).Elem()
|
||||
structFieldValue := structValue.FieldByName(field)
|
||||
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()
|
||||
out := make(map[string]interface{})
|
||||
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
|
||||
// (e.g., ListenIP is a valid IP address, Port isn't a negative number, etc)
|
||||
func (cfg *GochanConfig) ValidateValues() error {
|
||||
if net.ParseIP(cfg.ListenIP) == nil {
|
||||
return &ErrInvalidValue{Field: "ListenIP", Value: cfg.ListenIP}
|
||||
func (gcfg *GochanConfig) ValidateValues() error {
|
||||
if net.ParseIP(gcfg.ListenIP) == nil {
|
||||
return &ErrInvalidValue{Field: "ListenIP", Value: gcfg.ListenIP}
|
||||
}
|
||||
changed := false
|
||||
if len(cfg.FirstPage) == 0 {
|
||||
cfg.FirstPage = cfgDefaults["FirstPage"].([]string)
|
||||
|
||||
if gcfg.WebRoot == "" {
|
||||
gcfg.WebRoot = "/"
|
||||
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 {
|
||||
return &ErrInvalidValue{Field: "CookieMaxAge", Value: cfg.CookieMaxAge, Details: err.Error() + cookieMaxAgeEx}
|
||||
} else if err == gcutil.ErrEmptyDurationString {
|
||||
return &ErrInvalidValue{Field: "CookieMaxAge", Details: err.Error() + cookieMaxAgeEx}
|
||||
return &ErrInvalidValue{Field: "CookieMaxAge", Value: gcfg.CookieMaxAge, Details: err.Error() + cookieMaxAgeEx}
|
||||
} else if err != nil {
|
||||
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 == "" {
|
||||
cfg.DefaultStyle = cfg.Styles[0].Filename
|
||||
if len(gcfg.Styles) == 0 {
|
||||
return &ErrInvalidValue{Field: "Styles", Value: gcfg.Styles}
|
||||
}
|
||||
if gcfg.DefaultStyle == "" {
|
||||
gcfg.DefaultStyle = gcfg.Styles[0].Filename
|
||||
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
|
||||
}
|
||||
if cfg.ReplyDelay == 0 {
|
||||
cfg.ReplyDelay = cfgDefaults["ReplyDelay"].(int)
|
||||
if gcfg.ThumbWidth == 0 {
|
||||
gcfg.ThumbWidth = defaults["ThumbWidth"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.MaxLineLength == 0 {
|
||||
cfg.MaxLineLength = cfgDefaults["MaxLineLength"].(int)
|
||||
if gcfg.ThumbHeight == 0 {
|
||||
gcfg.ThumbHeight = defaults["ThumbHeight"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbWidth == 0 {
|
||||
cfg.ThumbWidth = cfgDefaults["ThumbWidth"].(int)
|
||||
if gcfg.ThumbWidthReply == 0 {
|
||||
gcfg.ThumbWidthReply = defaults["ThumbWidthReply"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbHeight == 0 {
|
||||
cfg.ThumbHeight = cfgDefaults["ThumbHeight"].(int)
|
||||
if gcfg.ThumbHeightReply == 0 {
|
||||
gcfg.ThumbHeightReply = defaults["ThumbHeightReply"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbWidthReply == 0 {
|
||||
cfg.ThumbWidthReply = cfgDefaults["ThumbWidthReply"].(int)
|
||||
|
||||
if gcfg.ThumbWidthCatalog == 0 {
|
||||
gcfg.ThumbWidthCatalog = defaults["ThumbWidthCatalog"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbHeightReply == 0 {
|
||||
cfg.ThumbHeightReply = cfgDefaults["ThumbHeightReply"].(int)
|
||||
if gcfg.ThumbHeightCatalog == 0 {
|
||||
gcfg.ThumbHeightCatalog = defaults["ThumbHeightCatalog"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbWidthCatalog == 0 {
|
||||
cfg.ThumbWidthCatalog = cfgDefaults["ThumbWidthCatalog"].(int)
|
||||
if gcfg.ThreadsPerPage == 0 {
|
||||
gcfg.ThreadsPerPage = defaults["ThreadsPerPage"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbHeightCatalog == 0 {
|
||||
cfg.ThumbHeightCatalog = cfgDefaults["ThumbHeightCatalog"].(int)
|
||||
if gcfg.RepliesOnBoardPage == 0 {
|
||||
gcfg.RepliesOnBoardPage = defaults["RepliesOnBoardPage"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThreadsPerPage == 0 {
|
||||
cfg.ThreadsPerPage = cfgDefaults["ThreadsPerPage"].(int)
|
||||
if gcfg.StickyRepliesOnBoardPage == 0 {
|
||||
gcfg.StickyRepliesOnBoardPage = defaults["StickyRepliesOnBoardPage"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.RepliesOnBoardPage == 0 {
|
||||
cfg.RepliesOnBoardPage = cfgDefaults["RepliesOnBoardPage"].(int)
|
||||
if gcfg.BanMessage == "" {
|
||||
gcfg.BanMessage = defaults["BanMessage"].(string)
|
||||
changed = true
|
||||
}
|
||||
if cfg.StickyRepliesOnBoardPage == 0 {
|
||||
cfg.StickyRepliesOnBoardPage = cfgDefaults["StickyRepliesOnBoardPage"].(int)
|
||||
if gcfg.DateTimeFormat == "" {
|
||||
gcfg.DateTimeFormat = defaults["DateTimeFormat"].(string)
|
||||
changed = true
|
||||
}
|
||||
if cfg.BanMsg == "" {
|
||||
cfg.BanMsg = cfgDefaults["BanMsg"].(string)
|
||||
if gcfg.CaptchaWidth == 0 {
|
||||
gcfg.CaptchaWidth = defaults["CaptchaWidth"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.DateTimeFormat == "" {
|
||||
cfg.DateTimeFormat = cfgDefaults["DateTimeFormat"].(string)
|
||||
if gcfg.CaptchaHeight == 0 {
|
||||
gcfg.CaptchaHeight = defaults["CaptchaHeight"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.CaptchaWidth == 0 {
|
||||
cfg.CaptchaWidth = cfgDefaults["CaptchaWidth"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.CaptchaHeight == 0 {
|
||||
cfg.CaptchaHeight = cfgDefaults["CaptchaHeight"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.EnableGeoIP {
|
||||
if cfg.GeoIPDBlocation == "" {
|
||||
if gcfg.EnableGeoIP {
|
||||
if gcfg.GeoIPDBlocation == "" {
|
||||
return &ErrInvalidValue{Field: "GeoIPDBlocation", Value: "", Details: "GeoIPDBlocation must be set in gochan.json if EnableGeoIP is true"}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.MaxLogDays == 0 {
|
||||
cfg.MaxLogDays = cfgDefaults["MaxLogDays"].(int)
|
||||
if gcfg.MaxLogDays == 0 {
|
||||
gcfg.MaxLogDays = defaults["MaxLogDays"].(int)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if cfg.RandomSeed == "" {
|
||||
cfg.RandomSeed = gcutil.RandomString(randomStringSize)
|
||||
if gcfg.RandomSeed == "" {
|
||||
gcfg.RandomSeed = gcutil.RandomString(randomStringSize)
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
return cfg.Write()
|
||||
return gcfg.Write()
|
||||
}
|
||||
|
||||
func (cfg *GochanConfig) Write() error {
|
||||
str, err := json.MarshalIndent(cfg, "", "\t")
|
||||
func (gcfg *GochanConfig) Write() error {
|
||||
str, err := json.MarshalIndent(gcfg, "", "\t")
|
||||
if err != nil {
|
||||
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",
|
||||
"DBpassword": "",
|
||||
"SiteDomain": "127.0.0.1",
|
||||
"SiteWebfolder": "/",
|
||||
"SiteWebFolder": "/",
|
||||
|
||||
"Styles": [
|
||||
{ "Name": "Pipes", "Filename": "pipes.css" },
|
||||
|
@ -57,7 +57,7 @@ const (
|
|||
"SiteName": "Gochan",
|
||||
"SiteSlogan": "",
|
||||
"SiteDomain": "127.0.0.1",
|
||||
"SiteWebfolder": "/",
|
||||
"SiteWebFolder": "/",
|
||||
|
||||
"Styles": [
|
||||
{ "Name": "Pipes", "Filename": "pipes.css" },
|
||||
|
@ -87,10 +87,10 @@ const (
|
|||
"PostsPerThreadPage": 50,
|
||||
"RepliesOnBoardPage": 3,
|
||||
"StickyRepliesOnBoardPage": 1,
|
||||
"BanMsg": "USER WAS BANNED FOR THIS POST",
|
||||
"BanMessage": "USER WAS BANNED FOR THIS POST",
|
||||
"EmbedWidth": 200,
|
||||
"EmbedHeight": 164,
|
||||
"ExpandButton": true,
|
||||
"EnableEmbeds": true,
|
||||
"ImagesOpenNewTab": true,
|
||||
"MakeURLsHyperlinked": true,
|
||||
"NewTabOnOutlinks": true,
|
||||
|
|
|
@ -13,6 +13,13 @@ import (
|
|||
"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
|
||||
type MissingField struct {
|
||||
Name string
|
||||
|
@ -35,6 +42,39 @@ func (iv *ErrInvalidValue) Error() string {
|
|||
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
|
||||
// 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
|
||||
|
@ -66,9 +106,9 @@ func ParseJSON(ba []byte) (*GochanConfig, []MissingField, error) {
|
|||
// field is in the JSON file
|
||||
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
|
||||
fVal.Set(reflect.ValueOf(cfgDefaults[fType.Name]))
|
||||
fVal.Set(reflect.ValueOf(defaults[fType.Name]))
|
||||
continue
|
||||
}
|
||||
if critical {
|
||||
|
@ -98,11 +138,11 @@ func InitConfig(versionStr string) {
|
|||
}
|
||||
|
||||
var fields []MissingField
|
||||
Config, fields, err = ParseJSON(jfile)
|
||||
cfg, fields, err = ParseJSON(jfile)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing %s: %s", cfgPath, err.Error())
|
||||
}
|
||||
Config.jsonLocation = cfgPath
|
||||
cfg.jsonLocation = cfgPath
|
||||
|
||||
numMissing := 0
|
||||
for _, missing := range fields {
|
||||
|
@ -117,63 +157,90 @@ func InitConfig(versionStr string) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = Config.ValidateValues(); err != nil {
|
||||
if err = cfg.ValidateValues(); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err = os.Stat(Config.DocumentRoot); err != nil {
|
||||
if _, err = os.Stat(cfg.DocumentRoot); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err = os.Stat(Config.TemplateDir); err != nil {
|
||||
if _, err = os.Stat(cfg.TemplateDir); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err = os.Stat(Config.LogDir); err != nil {
|
||||
if _, err = os.Stat(cfg.LogDir); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
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(
|
||||
path.Join(Config.LogDir, "access.log"),
|
||||
path.Join(Config.LogDir, "error.log"),
|
||||
path.Join(Config.LogDir, "staff.log"),
|
||||
Config.DebugMode); err != nil {
|
||||
path.Join(cfg.LogDir, "access.log"),
|
||||
path.Join(cfg.LogDir, "error.log"),
|
||||
path.Join(cfg.LogDir, "staff.log"),
|
||||
cfg.DebugMode); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if Config.Port == 0 {
|
||||
Config.Port = 80
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 80
|
||||
}
|
||||
|
||||
if len(Config.FirstPage) == 0 {
|
||||
Config.FirstPage = []string{"index.html", "1.html", "firstrun.html"}
|
||||
if len(cfg.FirstPage) == 0 {
|
||||
cfg.FirstPage = []string{"index.html", "1.html", "firstrun.html"}
|
||||
}
|
||||
|
||||
if Config.SiteWebfolder == "" {
|
||||
Config.SiteWebfolder = "/"
|
||||
if cfg.WebRoot == "" {
|
||||
cfg.WebRoot = "/"
|
||||
}
|
||||
|
||||
if Config.SiteWebfolder[0] != '/' {
|
||||
Config.SiteWebfolder = "/" + Config.SiteWebfolder
|
||||
if cfg.WebRoot[0] != '/' {
|
||||
cfg.WebRoot = "/" + cfg.WebRoot
|
||||
}
|
||||
if Config.SiteWebfolder[len(Config.SiteWebfolder)-1] != '/' {
|
||||
Config.SiteWebfolder += "/"
|
||||
if cfg.WebRoot[len(cfg.WebRoot)-1] != '/' {
|
||||
cfg.WebRoot += "/"
|
||||
}
|
||||
|
||||
if Config.EnableGeoIP {
|
||||
if _, err = os.Stat(Config.GeoIPDBlocation); err != nil {
|
||||
if cfg.EnableGeoIP {
|
||||
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")
|
||||
}
|
||||
Config.EnableGeoIP = false
|
||||
cfg.EnableGeoIP = false
|
||||
}
|
||||
|
||||
_, zoneOffset := time.Now().Zone()
|
||||
Config.TimeZone = zoneOffset / 60 / 60
|
||||
cfg.TimeZone = zoneOffset / 60 / 60
|
||||
|
||||
Config.Version = ParseVersion(versionStr)
|
||||
Config.Version.Normalize()
|
||||
cfg.Version = ParseVersion(versionStr)
|
||||
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), " ")
|
||||
sqlArr := strings.Split(gcdb.replacer.Replace(sqlStr), ";")
|
||||
|
||||
debugMode := config.GetSystemCriticalConfig().DebugMode
|
||||
for _, statement := range sqlArr {
|
||||
statement = strings.Trim(statement, " \n\r\t")
|
||||
if len(statement) > 0 {
|
||||
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, "Length sql: %d\n", len(statement))
|
||||
gclog.Printf(gclog.LStdLog, "Statement: %s\n", statement)
|
||||
|
|
|
@ -18,24 +18,28 @@ const (
|
|||
)
|
||||
|
||||
type GCDB struct {
|
||||
db *sql.DB
|
||||
connStr string
|
||||
driver string
|
||||
nilTimestamp string
|
||||
replacer *strings.Replacer
|
||||
db *sql.DB
|
||||
connStr string
|
||||
driver string
|
||||
replacer *strings.Replacer
|
||||
// nilTimestamp string
|
||||
}
|
||||
|
||||
func (db *GCDB) ConnectionString() string {
|
||||
return db.connStr
|
||||
}
|
||||
|
||||
func (db *GCDB) Connection() *sql.DB {
|
||||
return db.db
|
||||
}
|
||||
|
||||
func (db *GCDB) SQLDriver() string {
|
||||
return db.driver
|
||||
}
|
||||
|
||||
func (db *GCDB) NilSQLTimestamp() string {
|
||||
/* func (db *GCDB) NilSQLTimestamp() string {
|
||||
return db.nilTimestamp
|
||||
}
|
||||
} */
|
||||
|
||||
func (db *GCDB) Close() error {
|
||||
if db.db != nil {
|
||||
|
@ -145,10 +149,10 @@ func Open(host, dbDriver, dbName, username, password, prefix string) (db *GCDB,
|
|||
switch dbDriver {
|
||||
case "mysql":
|
||||
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":
|
||||
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:
|
||||
return nil, ErrUnsupportedDB
|
||||
}
|
||||
|
@ -172,7 +176,7 @@ func sqlVersionError(err error, dbDriver string, query *string) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if config.Config.DebugMode {
|
||||
if config.GetSystemCriticalConfig().DebugMode {
|
||||
return fmt.Errorf(UnsupportedSQLVersionMsg+"\nQuery: "+*query, errText)
|
||||
}
|
||||
return fmt.Errorf(UnsupportedSQLVersionMsg, errText)
|
||||
|
|
|
@ -54,7 +54,7 @@ func GetCompleteDatabaseVersion() (dbVersion, dbFlag int, err error) {
|
|||
return 0, DBIsPreApril, nil
|
||||
}
|
||||
//No old or current database versioning tables found.
|
||||
if config.Config.DBprefix != "" {
|
||||
if config.GetSystemCriticalConfig().DBprefix != "" {
|
||||
//Check if any gochan tables exist
|
||||
gochanTableExists, err := doesGochanPrefixTableExist()
|
||||
if err != nil {
|
||||
|
|
|
@ -795,6 +795,8 @@ func DeleteFilesFromPost(postID int) error {
|
|||
filenames = append(filenames, filename)
|
||||
}
|
||||
|
||||
systemCriticalCfg := config.GetSystemCriticalConfig()
|
||||
|
||||
//Remove files from disk
|
||||
for _, fileName := range filenames {
|
||||
fileName = fileName[:strings.Index(fileName, ".")]
|
||||
|
@ -804,9 +806,9 @@ func DeleteFilesFromPost(postID int) error {
|
|||
thumbType = "jpg"
|
||||
}
|
||||
|
||||
os.Remove(path.Join(config.Config.DocumentRoot, board, "/src/"+fileName+"."+fileType))
|
||||
os.Remove(path.Join(config.Config.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, "/src/"+fileName+"."+fileType))
|
||||
os.Remove(path.Join(systemCriticalCfg.DocumentRoot, board, "/thumb/"+fileName+"t."+thumbType))
|
||||
os.Remove(path.Join(systemCriticalCfg.DocumentRoot, board, "/thumb/"+fileName+"c."+thumbType))
|
||||
}
|
||||
|
||||
const removeFilesSQL = `DELETE FROM DBPREFIXfiles WHERE post_id = ?`
|
||||
|
@ -1005,7 +1007,7 @@ func doesTableExist(tableName string) (bool, error) {
|
|||
WHERE TABLE_NAME = ?`
|
||||
|
||||
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 {
|
||||
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.
|
||||
//Returns false if the prefix is an empty string
|
||||
func doesGochanPrefixTableExist() (bool, error) {
|
||||
if config.Config.DBprefix == "" {
|
||||
if config.GetSystemCriticalConfig().DBprefix == "" {
|
||||
return false, nil
|
||||
}
|
||||
var prefixTableExist = `SELECT count(*)
|
||||
|
|
|
@ -140,24 +140,26 @@ type Board struct {
|
|||
|
||||
// AbsolutePath returns the full filepath of the board directory
|
||||
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
|
||||
// fileType should be "boardPage", "threadPage", "upload", or "thumb"
|
||||
func (board *Board) WebPath(fileName, fileType string) string {
|
||||
var filePath string
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
|
||||
switch fileType {
|
||||
case "":
|
||||
fallthrough
|
||||
case "boardPage":
|
||||
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, fileName)
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, fileName)
|
||||
case "threadPage":
|
||||
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, "res", fileName)
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, "res", fileName)
|
||||
case "upload":
|
||||
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, "src", fileName)
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, "src", fileName)
|
||||
case "thumb":
|
||||
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, "thumb", fileName)
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, "thumb", fileName)
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
@ -188,7 +190,7 @@ func (board *Board) SetDefaults() {
|
|||
board.Section = 1
|
||||
board.MaxFilesize = 4096
|
||||
board.MaxPages = 11
|
||||
board.DefaultStyle = config.Config.DefaultStyle
|
||||
board.DefaultStyle = config.GetBoardConfig("").DefaultStyle
|
||||
board.Locked = false
|
||||
board.Anonymous = "Anonymous"
|
||||
board.ForcedAnon = false
|
||||
|
@ -251,15 +253,16 @@ type Post struct {
|
|||
|
||||
func (p *Post) GetURL(includeDomain bool) string {
|
||||
postURL := ""
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
if includeDomain {
|
||||
postURL += config.Config.SiteDomain
|
||||
postURL += systemCritical.SiteDomain
|
||||
}
|
||||
var board Board
|
||||
if err := board.PopulateData(p.BoardID); err != nil {
|
||||
return postURL
|
||||
}
|
||||
|
||||
postURL += config.Config.SiteWebfolder + board.Dir + "/res/"
|
||||
postURL += systemCritical.WebRoot + board.Dir + "/res/"
|
||||
if p.ParentID == 0 {
|
||||
postURL += fmt.Sprintf("%d.html#%d", p.ID, p.ID)
|
||||
} 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
|
||||
func (p *RecentPost) GetURL(includeDomain bool) string {
|
||||
postURL := ""
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
if includeDomain {
|
||||
postURL += config.Config.SiteDomain
|
||||
postURL += systemCritical.SiteDomain
|
||||
}
|
||||
idStr := strconv.Itoa(p.PostID)
|
||||
postURL += config.Config.SiteWebfolder + p.BoardName + "/res/"
|
||||
postURL += systemCritical.WebRoot + p.BoardName + "/res/"
|
||||
if p.ParentID == 0 {
|
||||
postURL += idStr + ".html#" + idStr
|
||||
} else {
|
||||
|
|
|
@ -74,7 +74,7 @@ var funcMap = template.FuncMap{
|
|||
return fmt.Sprintf("%0.2f GB", size/1024/1024/1024)
|
||||
},
|
||||
"formatTimestamp": func(t time.Time) string {
|
||||
return t.Format(config.Config.DateTimeFormat)
|
||||
return t.Format(config.GetBoardConfig("").DateTimeFormat)
|
||||
},
|
||||
"stringAppend": func(strings ...string) string {
|
||||
var appended string
|
||||
|
@ -155,13 +155,14 @@ var funcMap = template.FuncMap{
|
|||
return
|
||||
},
|
||||
"getPostURL": func(postInterface interface{}, typeOf string, withDomain bool) (postURL string) {
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
if withDomain {
|
||||
postURL = config.Config.SiteDomain
|
||||
postURL = systemCritical.SiteDomain
|
||||
}
|
||||
postURL += config.Config.SiteWebfolder
|
||||
postURL += systemCritical.WebRoot
|
||||
|
||||
if typeOf == "recent" {
|
||||
post, ok := postInterface.(*gcsql.RecentPost)
|
||||
post, ok := postInterface.(gcsql.RecentPost)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -240,61 +241,77 @@ var funcMap = template.FuncMap{
|
|||
return loopArr
|
||||
},
|
||||
"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>`
|
||||
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()
|
||||
switch kind {
|
||||
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 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>"
|
||||
tableOut += configTable(siteCfg) +
|
||||
configTable(boardCfg) +
|
||||
"</table>"
|
||||
return template.HTML(tableOut)
|
||||
},
|
||||
"isStyleDefault": func(style string) bool {
|
||||
return style == config.Config.DefaultStyle
|
||||
return style == config.GetBoardConfig("").DefaultStyle
|
||||
},
|
||||
"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) {
|
||||
var templates []string
|
||||
templateDir := config.GetSystemCriticalConfig().TemplateDir
|
||||
for i, file := range files {
|
||||
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) {
|
||||
files[i] = tmplPath
|
||||
} 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 {
|
||||
return nil
|
||||
}
|
||||
templateDir := config.GetSystemCriticalConfig().TemplateDir
|
||||
|
||||
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,
|
||||
|
|
|
@ -2,11 +2,9 @@ package manage
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -85,216 +83,220 @@ var actions = map[string]Action{
|
|||
Title: "Configuration",
|
||||
Permissions: AdminPerms,
|
||||
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
|
||||
config.Config.UseSillytags = (request.PostFormValue("UseSillytags") == "on")
|
||||
config.Config.Modboard = request.PostFormValue("Modboard")
|
||||
config.Config.SiteName = request.PostFormValue("SiteName")
|
||||
config.Config.SiteSlogan = request.PostFormValue("SiteSlogan")
|
||||
config.Config.SiteWebfolder = request.PostFormValue("SiteWebfolder")
|
||||
// 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 */
|
||||
config.Config.DefaultStyle = request.PostFormValue("DefaultStyle")
|
||||
config.Config.RejectDuplicateImages = (request.PostFormValue("RejectDuplicateImages") == "on")
|
||||
NewThreadDelay, err := strconv.Atoi(request.PostFormValue("NewThreadDelay"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.NewThreadDelay = NewThreadDelay
|
||||
}
|
||||
// do := request.FormValue("do")
|
||||
// siteCfg := config.GetSiteConfig()
|
||||
// boardCfg := config.GetBoardConfig("")
|
||||
// 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 {
|
||||
// siteCfg.CookieMaxAge = request.PostFormValue("CookieMaxAge")
|
||||
// if _, err = gcutil.ParseDurationString(config.Config.CookieMaxAge); err != nil {
|
||||
// status += err.Error()
|
||||
// siteCfg.CookieMaxAge = "1y"
|
||||
// }
|
||||
// siteCfg.Lockdown = (request.PostFormValue("Lockdown") == "on")
|
||||
// siteCfg.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"))
|
||||
// }
|
||||
|
||||
ReplyDelay, err := strconv.Atoi(request.PostFormValue("ReplyDelay"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ReplyDelay = ReplyDelay
|
||||
}
|
||||
// boardCfg.Sillytags = Sillytags
|
||||
// boardCfg.UseSillytags = (request.PostFormValue("UseSillytags") == "on")
|
||||
// siteCfg.Modboard = request.PostFormValue("Modboard")
|
||||
// siteCfg.SiteName = request.PostFormValue("SiteName")
|
||||
// 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"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.MaxLineLength = MaxLineLength
|
||||
}
|
||||
// ReplyDelay, err := strconv.Atoi(request.PostFormValue("ReplyDelay"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.ReplyDelay = ReplyDelay
|
||||
// }
|
||||
|
||||
ReservedTripsArr := strings.Split(request.PostFormValue("ReservedTrips"), "\n")
|
||||
var ReservedTrips []string
|
||||
for _, trip := range ReservedTripsArr {
|
||||
ReservedTrips = append(ReservedTrips, strings.Trim(trip, " \n\r"))
|
||||
// MaxLineLength, err := strconv.Atoi(request.PostFormValue("MaxLineLength"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.MaxLineLength = MaxLineLength
|
||||
// }
|
||||
|
||||
}
|
||||
config.Config.ReservedTrips = ReservedTrips
|
||||
// ReservedTripsArr := strings.Split(request.PostFormValue("ReservedTrips"), "\n")
|
||||
// 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 {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbWidth = ThumbWidth
|
||||
}
|
||||
// }
|
||||
// boardCfg.ReservedTrips = ReservedTrips
|
||||
|
||||
ThumbHeight, err := strconv.Atoi(request.PostFormValue("ThumbHeight"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbHeight = ThumbHeight
|
||||
}
|
||||
// ThumbWidth, err := strconv.Atoi(request.PostFormValue("ThumbWidth"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.ThumbWidth = ThumbWidth
|
||||
// }
|
||||
|
||||
ThumbWidthReply, err := strconv.Atoi(request.PostFormValue("ThumbWidthReply"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbWidthReply = ThumbWidthReply
|
||||
}
|
||||
// ThumbHeight, err := strconv.Atoi(request.PostFormValue("ThumbHeight"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.ThumbHeight = ThumbHeight
|
||||
// }
|
||||
|
||||
ThumbHeightReply, err := strconv.Atoi(request.PostFormValue("ThumbHeightReply"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbHeightReply = ThumbHeightReply
|
||||
}
|
||||
// ThumbWidthReply, err := strconv.Atoi(request.PostFormValue("ThumbWidthReply"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.ThumbWidthReply = ThumbWidthReply
|
||||
// }
|
||||
|
||||
ThumbWidthCatalog, err := strconv.Atoi(request.PostFormValue("ThumbWidthCatalog"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbWidthCatalog = ThumbWidthCatalog
|
||||
}
|
||||
// ThumbHeightReply, err := strconv.Atoi(request.PostFormValue("ThumbHeightReply"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.ThumbHeightReply = ThumbHeightReply
|
||||
// }
|
||||
|
||||
ThumbHeightCatalog, err := strconv.Atoi(request.PostFormValue("ThumbHeightCatalog"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbHeightCatalog = ThumbHeightCatalog
|
||||
}
|
||||
// ThumbWidthCatalog, err := strconv.Atoi(request.PostFormValue("ThumbWidthCatalog"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.ThumbWidthCatalog = ThumbWidthCatalog
|
||||
// }
|
||||
|
||||
RepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("RepliesOnBoardPage"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.RepliesOnBoardPage = RepliesOnBoardPage
|
||||
}
|
||||
// ThumbHeightCatalog, err := strconv.Atoi(request.PostFormValue("ThumbHeightCatalog"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.ThumbHeightCatalog = ThumbHeightCatalog
|
||||
// }
|
||||
|
||||
StickyRepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("StickyRepliesOnBoardPage"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.StickyRepliesOnBoardPage = StickyRepliesOnBoardPage
|
||||
}
|
||||
// RepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("RepliesOnBoardPage"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.RepliesOnBoardPage = RepliesOnBoardPage
|
||||
// }
|
||||
|
||||
config.Config.BanMsg = request.PostFormValue("BanMsg")
|
||||
EmbedWidth, err := strconv.Atoi(request.PostFormValue("EmbedWidth"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.EmbedWidth = EmbedWidth
|
||||
}
|
||||
// StickyRepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("StickyRepliesOnBoardPage"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.StickyRepliesOnBoardPage = StickyRepliesOnBoardPage
|
||||
// }
|
||||
|
||||
EmbedHeight, err := strconv.Atoi(request.PostFormValue("EmbedHeight"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.EmbedHeight = EmbedHeight
|
||||
}
|
||||
// boardCfg.BanMessage = request.PostFormValue("BanMessage")
|
||||
// EmbedWidth, err := strconv.Atoi(request.PostFormValue("EmbedWidth"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.EmbedWidth = EmbedWidth
|
||||
// }
|
||||
|
||||
config.Config.ExpandButton = (request.PostFormValue("ExpandButton") == "on")
|
||||
config.Config.ImagesOpenNewTab = (request.PostFormValue("ImagesOpenNewTab") == "on")
|
||||
config.Config.NewTabOnOutlinks = (request.PostFormValue("NewTabOnOutlinks") == "on")
|
||||
config.Config.MinifyHTML = (request.PostFormValue("MinifyHTML") == "on")
|
||||
config.Config.MinifyJS = (request.PostFormValue("MinifyJS") == "on")
|
||||
config.Config.DateTimeFormat = request.PostFormValue("DateTimeFormat")
|
||||
AkismetAPIKey := request.PostFormValue("AkismetAPIKey")
|
||||
// EmbedHeight, err := strconv.Atoi(request.PostFormValue("EmbedHeight"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// boardCfg.EmbedHeight = EmbedHeight
|
||||
// }
|
||||
|
||||
if err = serverutil.CheckAkismetAPIKey(AkismetAPIKey); err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.AkismetAPIKey = AkismetAPIKey
|
||||
}
|
||||
// boardCfg.EnableEmbeds = (request.PostFormValue("EnableEmbeds") == "on")
|
||||
// boardCfg.ImagesOpenNewTab = (request.PostFormValue("ImagesOpenNewTab") == "on")
|
||||
// boardCfg.NewTabOnOutlinks = (request.PostFormValue("NewTabOnOutlinks") == "on")
|
||||
// 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")
|
||||
CaptchaWidth, err := strconv.Atoi(request.PostFormValue("CaptchaWidth"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.CaptchaWidth = CaptchaWidth
|
||||
}
|
||||
CaptchaHeight, err := strconv.Atoi(request.PostFormValue("CaptchaHeight"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.CaptchaHeight = CaptchaHeight
|
||||
}
|
||||
// if err = serverutil.CheckAkismetAPIKey(AkismetAPIKey); err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// siteCfg.AkismetAPIKey = AkismetAPIKey
|
||||
// }
|
||||
|
||||
config.Config.EnableGeoIP = (request.PostFormValue("EnableGeoIP") == "on")
|
||||
config.Config.GeoIPDBlocation = request.PostFormValue("GeoIPDBlocation")
|
||||
// boardCfg.UseCaptcha = (request.PostFormValue("UseCaptcha") == "on")
|
||||
// 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"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.MaxRecentPosts = MaxRecentPosts
|
||||
}
|
||||
// boardCfg.EnableGeoIP = (request.PostFormValue("EnableGeoIP") == "on")
|
||||
// siteCfg.GeoIPDBlocation = request.PostFormValue("GeoIPDBlocation")
|
||||
|
||||
MaxLogDays, err := strconv.Atoi(request.PostFormValue("MaxLogDays"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.MaxLogDays = MaxLogDays
|
||||
}
|
||||
// MaxRecentPosts, err := strconv.Atoi(request.PostFormValue("MaxRecentPosts"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// siteCfg.MaxRecentPosts = MaxRecentPosts
|
||||
// }
|
||||
|
||||
configJSON, err = json.MarshalIndent(config.Config, "", "\t")
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else if err = ioutil.WriteFile("gochan.json", configJSON, 0777); err != nil {
|
||||
status = gclog.Print(gclog.LErrorLog, "Error writing gochan.json: ", err.Error())
|
||||
} else {
|
||||
status = "Wrote gochan.json successfully<br />"
|
||||
building.BuildJS()
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
"Error executing config management page: ", err.Error()))
|
||||
return htmlOut + err.Error(), err
|
||||
}
|
||||
htmlOut += manageConfigBuffer.String()
|
||||
return htmlOut, nil
|
||||
// MaxLogDays, err := strconv.Atoi(request.PostFormValue("MaxLogDays"))
|
||||
// if err != nil {
|
||||
// status += err.Error() + "<br />"
|
||||
// } else {
|
||||
// siteCfg.MaxLogDays = MaxLogDays
|
||||
// }
|
||||
|
||||
// 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{}{
|
||||
// "siteCfg": siteCfg,
|
||||
// "boardCfg": boardCfg,
|
||||
// "status": status,
|
||||
// }); 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": {
|
||||
Title: "Login",
|
||||
Permissions: NoPerms,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (htmlOut string, err error) {
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
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")
|
||||
password := request.FormValue("password")
|
||||
|
@ -304,16 +306,16 @@ var actions = map[string]Action{
|
|||
}
|
||||
if username == "" || password == "" {
|
||||
//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="text" name="username" class="logindata" /><br />` +
|
||||
`<input type="password" name="password" class="logindata" /><br />` +
|
||||
`<input type="submit" value="Login" />` +
|
||||
`</form>`
|
||||
} 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)
|
||||
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
|
||||
}},
|
||||
|
@ -342,9 +344,10 @@ var actions = map[string]Action{
|
|||
if len(announcements) == 0 {
|
||||
htmlOut += "No announcements"
|
||||
} else {
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
for _, announcement := range announcements {
|
||||
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>`
|
||||
}
|
||||
}
|
||||
|
@ -426,7 +429,11 @@ var actions = map[string]Action{
|
|||
manageBansBuffer := bytes.NewBufferString("")
|
||||
|
||||
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 {
|
||||
return "", errors.New("Error executing ban management page template: " + err.Error())
|
||||
}
|
||||
|
@ -452,6 +459,7 @@ var actions = map[string]Action{
|
|||
var done bool
|
||||
board := new(gcsql.Board)
|
||||
var boardCreationStatus string
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
|
||||
for !done {
|
||||
switch {
|
||||
|
@ -529,31 +537,32 @@ var actions = map[string]Action{
|
|||
board.EnableCatalog = (request.FormValue("enablecatalog") == "on")
|
||||
|
||||
//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 = ""
|
||||
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/ already exists.",
|
||||
config.Config.DocumentRoot, board.Dir)
|
||||
systemCritical.DocumentRoot, board.Dir)
|
||||
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 = ""
|
||||
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/res/ already exists.",
|
||||
config.Config.DocumentRoot, board.Dir)
|
||||
systemCritical.DocumentRoot, board.Dir)
|
||||
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 = ""
|
||||
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/src/ already exists.",
|
||||
config.Config.DocumentRoot, board.Dir)
|
||||
systemCritical.DocumentRoot, board.Dir)
|
||||
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 = ""
|
||||
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/thumb/ already exists.",
|
||||
config.Config.DocumentRoot, board.Dir)
|
||||
systemCritical.DocumentRoot, board.Dir)
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -572,6 +581,7 @@ var actions = map[string]Action{
|
|||
case do == "edit":
|
||||
// resetBoardSectionArrays()
|
||||
default:
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
// put the default column values in the text boxes
|
||||
board.Section = 1
|
||||
board.MaxFilesize = 4718592
|
||||
|
@ -583,7 +593,7 @@ var actions = map[string]Action{
|
|||
board.EmbedsAllowed = true
|
||||
board.EnableCatalog = 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>`
|
||||
|
@ -604,8 +614,9 @@ var actions = map[string]Action{
|
|||
manageBoardsBuffer := bytes.NewBufferString("")
|
||||
gcsql.AllSections, _ = gcsql.GetAllSectionsOrCreateDefault()
|
||||
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
if err = gctemplates.ManageBoards.Execute(manageBoardsBuffer, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"boardConfig": boardConfig,
|
||||
"board": board,
|
||||
"section_arr": gcsql.AllSections,
|
||||
}); err != nil {
|
||||
|
@ -702,6 +713,7 @@ var actions = map[string]Action{
|
|||
Title: "Recent posts",
|
||||
Permissions: JanitorPerms,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (htmlOut string, err error) {
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
limit := request.FormValue("limit")
|
||||
if limit == "" {
|
||||
limit = "50"
|
||||
|
@ -722,7 +734,7 @@ var actions = map[string]Action{
|
|||
for _, recentpost := range recentposts {
|
||||
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>`,
|
||||
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.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()))
|
||||
return "", err
|
||||
}
|
||||
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
for _, staff := range allStaff {
|
||||
username := request.FormValue("username")
|
||||
password := request.FormValue("password")
|
||||
|
@ -789,7 +801,7 @@ var actions = map[string]Action{
|
|||
}
|
||||
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>`,
|
||||
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>` +
|
||||
|
|
|
@ -65,7 +65,10 @@ func CallManageFunction(writer http.ResponseWriter, request *http.Request) {
|
|||
|
||||
if !handler.isJSON {
|
||||
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,
|
||||
"Error executing manage page header template: ", err.Error()))
|
||||
return
|
||||
|
|
|
@ -42,15 +42,16 @@ func createSession(key, username, password string, request *http.Request, writer
|
|||
}
|
||||
|
||||
// successful login, add cookie that expires in one month
|
||||
|
||||
maxAge, err := gcutil.ParseDurationString(config.Config.CookieMaxAge)
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
siteConfig := config.GetSiteConfig()
|
||||
maxAge, err := gcutil.ParseDurationString(siteConfig.CookieMaxAge)
|
||||
if err != nil {
|
||||
maxAge = gcutil.DefaultMaxAge
|
||||
}
|
||||
http.SetCookie(writer, &http.Cookie{
|
||||
Name: "sessiondata",
|
||||
Value: key,
|
||||
Path: "/",
|
||||
Path: systemCritical.WebRoot,
|
||||
Domain: domain,
|
||||
MaxAge: int(maxAge),
|
||||
})
|
||||
|
|
|
@ -29,7 +29,9 @@ func BanHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
// banStatus, err := getBannedStatus(request) TODO refactor to use ipban
|
||||
var banStatus gcsql.BanInfo
|
||||
var err error
|
||||
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
siteConfig := config.GetSiteConfig()
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
if appealMsg != "" {
|
||||
if banStatus.BannedForever() {
|
||||
fmt.Fprint(writer, "No.")
|
||||
|
@ -40,7 +42,7 @@ func BanHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
serverutil.ServeErrorPage(writer, err.Error())
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -52,7 +54,12 @@ func BanHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
}
|
||||
|
||||
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 {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error minifying page template: ", err.Error()))
|
||||
|
|
|
@ -31,18 +31,20 @@ type captchaJSON struct {
|
|||
|
||||
// InitCaptcha prepares the captcha driver for use
|
||||
func InitCaptcha() {
|
||||
if !config.Config.UseCaptcha {
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
if !boardConfig.UseCaptcha {
|
||||
return
|
||||
}
|
||||
driver = base64Captcha.NewDriverString(
|
||||
config.Config.CaptchaHeight, config.Config.CaptchaWidth, 0, 0, 6,
|
||||
boardConfig.CaptchaHeight, boardConfig.CaptchaWidth, 0, 0, 6,
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
&color.RGBA{0, 0, 0, 0}, nil).ConvertFonts()
|
||||
}
|
||||
|
||||
// ServeCaptcha handles requests to /captcha if UseCaptcha is enabled in gochan.json
|
||||
func ServeCaptcha(writer http.ResponseWriter, request *http.Request) {
|
||||
if !config.Config.UseCaptcha {
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
if !boardConfig.UseCaptcha {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
|
@ -109,7 +111,8 @@ func ServeCaptcha(writer http.ResponseWriter, request *http.Request) {
|
|||
}
|
||||
|
||||
func getCaptchaImage() (captchaID, chaptchaB64 string) {
|
||||
if !config.Config.UseCaptcha {
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
if !boardConfig.UseCaptcha {
|
||||
return
|
||||
}
|
||||
captcha := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)
|
||||
|
|
|
@ -32,7 +32,7 @@ type MessageFormatter struct {
|
|||
}
|
||||
|
||||
func (mf *MessageFormatter) InitBBcode() {
|
||||
if config.Config.DisableBBcode {
|
||||
if config.GetBoardConfig("").DisableBBcode {
|
||||
return
|
||||
}
|
||||
mf.bbCompiler = bbcode.NewCompiler(true, true)
|
||||
|
@ -45,7 +45,7 @@ func (mf *MessageFormatter) InitBBcode() {
|
|||
}
|
||||
|
||||
func (mf *MessageFormatter) Compile(msg string) string {
|
||||
if config.Config.DisableBBcode {
|
||||
if config.GetBoardConfig("").DisableBBcode {
|
||||
return msg
|
||||
}
|
||||
return mf.bbCompiler.Compile(msg)
|
||||
|
@ -59,6 +59,7 @@ func FormatMessage(message string) template.HTML {
|
|||
trimmedLine := strings.TrimSpace(line)
|
||||
lineWords := strings.Split(trimmedLine, " ")
|
||||
isGreentext := false // if true, append </span> to end of line
|
||||
WebRoot := config.GetSystemCriticalConfig().WebRoot
|
||||
for w, word := range lineWords {
|
||||
if strings.LastIndex(word, ">>") == 0 {
|
||||
//word is a backlink
|
||||
|
@ -79,9 +80,9 @@ func FormatMessage(message string) template.HTML {
|
|||
if !boardIDFound {
|
||||
lineWords[w] = `<a href="javascript:;"><strike>` + word + `</strike></a>`
|
||||
} 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 {
|
||||
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 {
|
||||
|
|
|
@ -42,8 +42,11 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
var nameCookie string
|
||||
var formEmail string
|
||||
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
|
||||
if request.Method == "GET" {
|
||||
http.Redirect(writer, request, config.Config.SiteWebfolder, http.StatusFound)
|
||||
http.Redirect(writer, request, systemCritical.WebRoot, http.StatusFound)
|
||||
return
|
||||
}
|
||||
// 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)
|
||||
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.")
|
||||
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.")
|
||||
return
|
||||
}
|
||||
|
@ -164,7 +167,11 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
var banpageBuffer bytes.Buffer
|
||||
|
||||
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 {
|
||||
serverutil.ServeErrorPage(writer,
|
||||
gclog.Print(gclog.LErrorLog, "Error minifying page: ", err.Error()))
|
||||
|
@ -176,7 +183,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
|
||||
post.Sanitize()
|
||||
|
||||
if config.Config.UseCaptcha {
|
||||
if boardConfig.UseCaptcha {
|
||||
captchaID := request.FormValue("captchaid")
|
||||
captchaAnswer := request.FormValue("captchaanswer")
|
||||
if captchaID == "" && captchaAnswer == "" {
|
||||
|
@ -234,9 +241,9 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
return
|
||||
}
|
||||
boardDir := _board.Dir
|
||||
filePath = path.Join(config.Config.DocumentRoot, "/"+boardDir+"/src/", post.Filename)
|
||||
thumbPath = path.Join(config.Config.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))
|
||||
filePath = path.Join(systemCritical.DocumentRoot, "/"+boardDir+"/src/", post.Filename)
|
||||
thumbPath = path.Join(systemCritical.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "t."+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 {
|
||||
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",
|
||||
handler.Filename, post.IP, request.Referer())
|
||||
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,
|
||||
"Error creating video thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
} 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,
|
||||
"Error creating video thumbnail: ", err.Error()))
|
||||
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,
|
||||
"Error creating video thumbnail: ", err.Error()))
|
||||
return
|
||||
|
@ -345,15 +352,15 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
|
||||
if request.FormValue("spoiler") == "on" {
|
||||
// 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")
|
||||
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())
|
||||
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
|
||||
post.ThumbW = img.Bounds().Max.X
|
||||
post.ThumbH = img.Bounds().Max.Y
|
||||
|
@ -402,11 +409,11 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
|
||||
if emailCommand == "noko" {
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
gclog.Printf(errStdLogs,
|
||||
"Error pruning temporary upload for %q: %s", fileSrc, err.Error())
|
||||
|
|
|
@ -16,17 +16,18 @@ import (
|
|||
func createImageThumbnail(imageObj image.Image, size string) image.Image {
|
||||
var thumbWidth int
|
||||
var thumbHeight int
|
||||
boardCfg := config.GetBoardConfig("")
|
||||
|
||||
switch size {
|
||||
case "op":
|
||||
thumbWidth = config.Config.ThumbWidth
|
||||
thumbHeight = config.Config.ThumbHeight
|
||||
thumbWidth = boardCfg.ThumbWidth
|
||||
thumbHeight = boardCfg.ThumbHeight
|
||||
case "reply":
|
||||
thumbWidth = config.Config.ThumbWidthReply
|
||||
thumbHeight = config.Config.ThumbHeightReply
|
||||
thumbWidth = boardCfg.ThumbWidthReply
|
||||
thumbHeight = boardCfg.ThumbHeightReply
|
||||
case "catalog":
|
||||
thumbWidth = config.Config.ThumbWidthCatalog
|
||||
thumbHeight = config.Config.ThumbHeightCatalog
|
||||
thumbWidth = boardCfg.ThumbWidthCatalog
|
||||
thumbHeight = boardCfg.ThumbHeightCatalog
|
||||
}
|
||||
oldRect := imageObj.Bounds()
|
||||
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) {
|
||||
var thumbWidth int
|
||||
var thumbHeight int
|
||||
|
||||
boardCfg := config.GetBoardConfig("")
|
||||
switch {
|
||||
case size == "op":
|
||||
thumbWidth = config.Config.ThumbWidth
|
||||
thumbHeight = config.Config.ThumbHeight
|
||||
thumbWidth = boardCfg.ThumbWidth
|
||||
thumbHeight = boardCfg.ThumbHeight
|
||||
case size == "reply":
|
||||
thumbWidth = config.Config.ThumbWidthReply
|
||||
thumbHeight = config.Config.ThumbHeightReply
|
||||
thumbWidth = boardCfg.ThumbWidthReply
|
||||
thumbHeight = boardCfg.ThumbHeightReply
|
||||
case size == "catalog":
|
||||
thumbWidth = config.Config.ThumbWidthCatalog
|
||||
thumbHeight = config.Config.ThumbHeightCatalog
|
||||
thumbWidth = boardCfg.ThumbWidthCatalog
|
||||
thumbHeight = boardCfg.ThumbHeightCatalog
|
||||
}
|
||||
if w == h {
|
||||
newWidth = thumbWidth
|
||||
|
|
|
@ -11,12 +11,17 @@ import (
|
|||
"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.
|
||||
func CheckAkismetAPIKey(key string) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -30,22 +35,22 @@ func CheckAkismetAPIKey(key string) error {
|
|||
}
|
||||
if string(body) == "invalid" {
|
||||
// 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."
|
||||
gclog.Print(gclog.LErrorLog, errmsg)
|
||||
return errors.New(errmsg)
|
||||
return ErrInvalidAkismetKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if config.Config.AkismetAPIKey != "" {
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
siteCfg := config.GetSiteConfig()
|
||||
if siteCfg.AkismetAPIKey != "" {
|
||||
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_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()))
|
||||
if err != nil {
|
||||
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)
|
||||
func ValidReferer(request *http.Request) bool {
|
||||
if config.Config.DebugMode {
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
if systemCritical.DebugMode {
|
||||
return true
|
||||
}
|
||||
rURL, err := url.ParseRequestURI(request.Referer())
|
||||
|
@ -93,5 +99,5 @@ func ValidReferer(request *http.Request) bool {
|
|||
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
|
||||
func InitMinifier() {
|
||||
if !config.Config.MinifyHTML && !config.Config.MinifyJS {
|
||||
siteConfig := config.GetSiteConfig()
|
||||
if !siteConfig.MinifyHTML && !siteConfig.MinifyJS {
|
||||
return
|
||||
}
|
||||
minifier = minify.New()
|
||||
if config.Config.MinifyHTML {
|
||||
if siteConfig.MinifyHTML {
|
||||
minifier.AddFunc("text/html", minifyHTML.Minify)
|
||||
}
|
||||
if config.Config.MinifyJS {
|
||||
if siteConfig.MinifyJS {
|
||||
minifier.AddFunc("text/javascript", minifyJS.Minify)
|
||||
minifier.AddFunc("application/json", minifyJSON.Minify)
|
||||
}
|
||||
}
|
||||
|
||||
func canMinify(mediaType string) bool {
|
||||
if mediaType == "text/html" && config.Config.MinifyHTML {
|
||||
siteConfig := config.GetSiteConfig()
|
||||
if mediaType == "text/html" && siteConfig.MinifyHTML {
|
||||
return true
|
||||
}
|
||||
if (mediaType == "application/json" || mediaType == "text/javascript") && config.Config.MinifyJS {
|
||||
if (mediaType == "application/json" || mediaType == "text/javascript") && siteConfig.MinifyJS {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
|
@ -13,8 +13,10 @@ import (
|
|||
// ServeErrorPage shows a general error page if something goes wrong
|
||||
func ServeErrorPage(writer http.ResponseWriter, err string) {
|
||||
MinifyTemplate(gctemplates.ErrorPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"ErrorTitle": "Error :c",
|
||||
"systemCritical": config.GetSystemCriticalConfig(),
|
||||
"siteConfig": config.GetSiteConfig(),
|
||||
"boardConfig": config.GetBoardConfig(""),
|
||||
"ErrorTitle": "Error :c",
|
||||
// "ErrorImage": "/error/lol 404.gif",
|
||||
"ErrorHeader": "Error",
|
||||
"ErrorText": err,
|
||||
|
@ -25,7 +27,8 @@ func ServeErrorPage(writer http.ResponseWriter, err string) {
|
|||
func ServeNotFound(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
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 {
|
||||
writer.Write([]byte("Requested page not found, and /error/404.html not found"))
|
||||
} else {
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
"SiteSlogan": "",
|
||||
"SiteDomain": "127.0.0.1",
|
||||
"SiteHeaderURL": "",
|
||||
"SiteWebfolder": "/",
|
||||
"WebRoot": "/",
|
||||
|
||||
"Styles": [
|
||||
{ "Name": "Pipes", "Filename": "pipes.css" },
|
||||
|
@ -55,10 +55,10 @@
|
|||
|
||||
"ThumbWidth": 200,
|
||||
"ThumbHeight": 200,
|
||||
"ThumbWidth_reply": 125,
|
||||
"ThumbHeight_reply": 125,
|
||||
"ThumbWidth_catalog": 50,
|
||||
"ThumbHeight_catalog": 50,
|
||||
"ThumbWidthReply": 125,
|
||||
"ThumbHeightReply": 125,
|
||||
"ThumbWidthCatalog": 50,
|
||||
"ThumbHeightCatalog": 50,
|
||||
|
||||
"ThreadsPerPage": 15,
|
||||
"PostsPerThreadPage": 50,
|
||||
|
@ -68,12 +68,11 @@
|
|||
"admin:#0000A0",
|
||||
"somemod:blue"
|
||||
],
|
||||
"BanMsg": "USER WAS BANNED FOR THIS POST",
|
||||
"BanMessage": "USER WAS BANNED FOR THIS POST",
|
||||
"EnableEmbeds": true,
|
||||
"EmbedWidth": 200,
|
||||
"EmbedHeight": 164,
|
||||
"ExpandButton": true,
|
||||
"ImagesOpenNewTab": true,
|
||||
"MakeURLsHyperlinked": true,
|
||||
"NewTabOnOutlinks": true,
|
||||
|
||||
"MinifyHTML": true,
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Banned</title>
|
||||
<link rel="shortcut icon" href="{{.config.SiteWebfolder}}favicon.png">
|
||||
<link rel="stylesheet" href="{{.config.SiteWebfolder}}css/global.css" />
|
||||
<link id="theme" rel="stylesheet" href="{{.config.SiteWebfolder}}css/{{.config.DefaultStyle}}" />
|
||||
<script type="text/javascript" src="{{.config.SiteWebfolder}}js/consts.js"></script>
|
||||
<script type="text/javascript" src="{{.config.SiteWebfolder}}js/gochan.js"></script>
|
||||
<link rel="shortcut icon" href="{{.systemCritical.WebRoot}}favicon.png">
|
||||
<link rel="stylesheet" href="{{.systemCritical.WebRoot}}css/global.css" />
|
||||
<link id="theme" rel="stylesheet" href="{{.systemCritical.WebRoot}}css/{{.boardConfig.DefaultStyle}}" />
|
||||
<script type="text/javascript" src="{{.systemCritical.WebRoot}}js/consts.js"></script>
|
||||
<script type="text/javascript" src="{{.systemCritical.WebRoot}}js/gochan.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="top-pane">
|
||||
<span id="site-title">{{.config.SiteName}}</span><br />
|
||||
<span id="site-slogan">{{.config.SiteSlogan}}</span>
|
||||
<span id="site-title">{{.siteConfig.SiteName}}</span><br />
|
||||
<span id="site-slogan">{{.siteConfig.SiteSlogan}}</span>
|
||||
</div><br />
|
||||
<div class="section-block" style="margin: 0px 26px 0px 24px">
|
||||
<div class="section-title-block">
|
||||
|
@ -35,9 +35,9 @@
|
|||
</div>{{if bannedForever .ban}}
|
||||
<img id="banpage-image" src="/permabanned.jpg" style="float:right; margin: 4px 8px 8px 4px"/><br />
|
||||
<audio id="jack" preload="auto" autobuffer loop>
|
||||
<source src="{{.config.SiteWebfolder}}hittheroad.ogg" />
|
||||
<source src="{{.config.SiteWebfolder}}hittheroad.wav" />
|
||||
<source src="{{.config.SiteWebfolder}}hittheroad.mp3" />
|
||||
<source src="{{.systemCritical.WebRoot}}hittheroad.ogg" />
|
||||
<source src="{{.systemCritical.WebRoot}}hittheroad.wav" />
|
||||
<source src="{{.systemCritical.WebRoot}}hittheroad.mp3" />
|
||||
</audio>
|
||||
<script type="text/javascript">
|
||||
document.getElementById("jack").play();
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<span id="board-subtitle">{{$.board.Subtitle}}</span>
|
||||
</header><hr />
|
||||
<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>
|
||||
{{- template "postbox.html" .}}
|
||||
<hr />
|
||||
|
@ -15,13 +15,13 @@
|
|||
<div class="op-post" id="op{{$op.ID}}">
|
||||
{{- if ne $op.Filename "" -}}
|
||||
{{- 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>
|
||||
<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>
|
||||
<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="{{$.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}}
|
||||
<div class="file-deleted-box" style="text-align:center;">File removed</div>
|
||||
{{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>
|
||||
{{- if gt $thread.NumReplies 3}}
|
||||
<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}}">
|
||||
<a class="anchor" id="{{$reply.ID}}"></a>
|
||||
<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 "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 />
|
||||
<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>
|
||||
<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="{{$.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}}
|
||||
<div class="file-deleted-box" style="text-align:center;">File removed</div>
|
||||
{{end}}
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<span id="board-subtitle">Catalog</span>
|
||||
</header><hr />
|
||||
<div id="catalog-links" style="float: left;">
|
||||
[<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}">Return</a>]
|
||||
[<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/catalog.html">Refresh</a>]
|
||||
[<a href="{{$.webroot}}{{$.board.Dir}}">Return</a>]
|
||||
[<a href="{{$.webroot}}{{$.board.Dir}}/catalog.html">Refresh</a>]
|
||||
</div>
|
||||
<div id="catalog-controls" style="float: right;">
|
||||
Sort by: <select>
|
||||
|
@ -16,9 +16,9 @@
|
|||
</div><hr />
|
||||
<div id="content">{{range $_,$thread := .threads}}
|
||||
<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}}
|
||||
<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 />
|
||||
<b>{{if eq $thread.Name ""}}Anonymous{{else}}{{$thread.Name}}{{end}}</b> | <b>R:</b> {{numReplies $.board.ID $thread.ID}}<br />
|
||||
{{$thread.MessageHTML}}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
instead of loading them on every HTML page.
|
||||
*/ -}}
|
||||
var styles = [
|
||||
{{- range $ii, $style := .Styles -}}
|
||||
{{- range $ii, $style := .styles -}}
|
||||
{{if gt $ii 0}},{{end -}}
|
||||
{Name: "{{js $style.Name}}", Filename: "{{js $style.Filename}}"}
|
||||
{{- end -}}
|
||||
];
|
||||
var defaultStyle = "{{js .DefaultStyle}}";
|
||||
var webroot = "{{js .SiteWebfolder}}";
|
||||
var serverTZ = {{.TimeZone}};
|
||||
var defaultStyle = "{{js .default_style}}";
|
||||
var webroot = "{{js .webroot}}";
|
||||
var serverTZ = {{.timezone}};
|
||||
|
|
|
@ -8,6 +8,6 @@
|
|||
<h1>{{.ErrorHeader}}</h1>
|
||||
{{/*<img src="{{.ErrorImage}}" border="0" alt="">*/}}
|
||||
<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>
|
||||
</html>
|
|
@ -1,7 +1,7 @@
|
|||
{{- template "page_header.html" .}}
|
||||
<div id="top-pane">
|
||||
<span id="site-title">{{.config.SiteName}}</span><br />
|
||||
<span id="site-slogan">{{.config.SiteSlogan}}</span>
|
||||
<span id="site-title">{{.site_config.SiteName}}</span><br />
|
||||
<span id="site-slogan">{{.site_config.SiteSlogan}}</span>
|
||||
</div><br />
|
||||
<div id="frontpage">
|
||||
<div class="section-block" style="margin: 16px 64px 16px 64px;">
|
||||
|
@ -17,8 +17,8 @@
|
|||
<ul style="float:left; list-style: none">
|
||||
<li style="text-align: center; font-weight: bold"><b><u>{{$section.Name}}</u></b></li>
|
||||
{{range $_, $board := $.boards}}
|
||||
{{if and (eq $board.Section $section.ID) (ne $board.Dir $.config.Modboard)}}
|
||||
<li><a href="{{$.config.SiteWebfolder}}{{$board.Dir}}/" title="{{$board.Description}}">/{{$board.Dir}}/</a> — {{$board.Title}}</li>
|
||||
{{if and (eq $board.Section $section.ID) (ne $board.Dir $.site_config.Modboard)}}
|
||||
<li><a href="{{$.webroot}}{{$board.Dir}}/" title="{{$board.Description}}">/{{$board.Dir}}/</a> — {{$board.Title}}</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
|
@ -26,7 +26,7 @@
|
|||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{- if gt .config.MaxRecentPosts 0}}
|
||||
{{- if gt .site_config.MaxRecentPosts 0}}
|
||||
<div class="section-block">
|
||||
<div class="section-title-block"><b>Recent Posts</b></div>
|
||||
<div class="section-body">
|
||||
|
@ -34,11 +34,11 @@
|
|||
{{- range $i, $post := $.recent_posts}}{{$postURL := getPostURL $post "recent" false}}
|
||||
<div class="recent-post">
|
||||
{{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}}
|
||||
<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 />
|
||||
<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}}
|
||||
</div>{{end}}
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<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 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}}
|
||||
</select></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>
|
||||
<link rel="stylesheet" href="{{.SiteWebfolder}}css/global.css" />
|
||||
<link id="theme" rel="stylesheet" href="{{.SiteWebfolder}}css/{{.DefaultStyle}}" />
|
||||
<link rel="shortcut icon" href="{{.SiteWebfolder}}favicon.png" />
|
||||
<script type="text/javascript" src="{{.SiteWebfolder}}js/consts.js"></script>
|
||||
<script type="text/javascript" src="{{.SiteWebfolder}}js/gochan.js"></script>
|
||||
<link rel="stylesheet" href="{{.WebRoot}}css/global.css" />
|
||||
<link id="theme" rel="stylesheet" href="{{.WebRoot}}css/{{.DefaultStyle}}" />
|
||||
<link rel="shortcut icon" href="{{.WebRoot}}favicon.png" />
|
||||
<script type="text/javascript" src="{{.WebRoot}}js/consts.js"></script>
|
||||
<script type="text/javascript" src="{{.WebRoot}}js/gochan.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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 />
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
{{- 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}}/ - {{$.board.Title}}</title>{{end}}
|
||||
{{- else}}<title>{{.config.SiteName}}</title>{{end}}
|
||||
<link rel="stylesheet" href="{{.config.SiteWebfolder}}css/global.css" />
|
||||
<link id="theme" rel="stylesheet" href="{{.config.SiteWebfolder}}css/{{.config.DefaultStyle}}" />
|
||||
<link rel="shortcut icon" href="{{.config.SiteWebfolder}}favicon.png">
|
||||
<script type="text/javascript" src="{{$.config.SiteWebfolder}}js/consts.js"></script>
|
||||
<script type="text/javascript" src="{{$.config.SiteWebfolder}}js/gochan.js"></script>
|
||||
{{- else}}<title>{{.site_config.SiteName}}</title>{{end}}
|
||||
<link rel="stylesheet" href="{{$.webroot}}css/global.css" />
|
||||
<link id="theme" rel="stylesheet" href="{{.webroot}}css/{{.board_config.DefaultStyle}}" />
|
||||
<link rel="shortcut icon" href="{{.webroot}}favicon.png">
|
||||
<script type="text/javascript" src="{{$.webroot}}js/consts.js"></script>
|
||||
<script type="text/javascript" src="{{$.webroot}}js/gochan.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
|
@ -4,10 +4,10 @@
|
|||
<span id="board-subtitle">{{$.board.Subtitle}}</span>
|
||||
</header><hr />
|
||||
<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 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>
|
||||
{{template "postbox.html" .}}<hr />
|
||||
<div id="content">
|
||||
|
@ -17,23 +17,23 @@
|
|||
{{if ne $.op.Filename ""}}
|
||||
{{- 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>
|
||||
<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}}
|
||||
<div class="file-deleted-box" style="text-align:center;">File removed</div>
|
||||
{{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>
|
||||
{{range $reply_num,$reply := .posts -}}
|
||||
<div class="reply-container" id="replycontainer{{$reply.ID}}">
|
||||
<a class="anchor" id="{{$reply.ID}}"></a>
|
||||
<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 "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 />
|
||||
<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}}
|
||||
<div class="file-deleted-box" style="text-align:center;">File removed</div>
|
||||
{{end}}{{end}}
|
||||
|
@ -53,7 +53,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<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">
|
||||
[{{range $i, $boardlink := .boards -}}
|
||||
{{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