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

refactor configuration

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

View file

@ -1,5 +1,8 @@
Gochan
=======
**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)

View file

@ -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.")

View file

@ -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

View file

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

View file

@ -3,9 +3,19 @@ package pre2021
import (
"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()
}

View file

@ -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!")
}

View file

@ -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()

View file

@ -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
View file

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

View file

@ -73,14 +73,15 @@ func BuildBoardPages(board *gcsql.Board) error {
thread.OP = op
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,

View file

@ -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()))
}

View file

@ -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()))

View file

@ -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
}

View file

@ -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,

View file

@ -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
}

View file

@ -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)

View file

@ -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)

View file

@ -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 {

View file

@ -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(*)

View file

@ -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 {

View file

@ -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
}

View file

@ -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,

View file

@ -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&amp;do=del&amp;username=%s" style="float:right;color:red;">X</a></td></tr>`,
staff.Username, rank, staff.AddedOn.Format(config.Config.DateTimeFormat), staff.Username)
staff.Username, rank, staff.AddedOn.Format(boardConfig.DateTimeFormat), staff.Username)
}
htmlOut += `</table><hr /><h2 class="manage-header">Add new staff</h2>` +

View file

@ -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

View file

@ -42,15 +42,16 @@ func createSession(key, username, password string, request *http.Request, writer
}
// successful login, add cookie that expires in one month
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),
})

View file

@ -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()))

View file

@ -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)

View file

@ -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, "&gt;&gt;") == 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, "&gt;") == 0 && w == 0 {

View file

@ -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)
}
}

View file

@ -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())

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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 {

View file

@ -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,

View file

@ -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();

View file

@ -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)">&#9660;</a>]</span> <span>[<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$op.ID}}.html">View</a>]</span></span><br />
<input type="checkbox" id="check{{$op.ID}}" name="check{{$op.ID}}" /><label class="post-info" for="check{{$op.ID}}"> <span class="subject">{{$op.Subject}}</span> <span class="postername">{{if ne $op.Email ""}}<a href="mailto:{{$op.Email}}">{{end}}{{if ne $op.Name ""}}{{$op.Name}}{{else}}{{if eq $op.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $op.Email ""}}</a>{{end}}</span>{{if ne $op.Tripcode ""}}<span class="tripcode">!{{$op.Tripcode}}</span>{{end}} {{formatTimestamp $op.Timestamp}} </label><a href="{{$.webroot}}{{$.board.Dir}}/res/{{$op.ID}}.html#{{$op.ID}}">No.</a> <a href="javascript:quote({{$op.ID}})" class="backlink-click">{{$op.ID}}</a> <span class="post-links"> <span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span> <span>[<a href="{{$.webroot}}{{$.board.Dir}}/res/{{$op.ID}}.html">View</a>]</span></span><br />
<div class="post-text">{{truncateHTMLMessage $op.MessageHTML 2222 18}}</div>
{{- 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)">&#9660;</a>]</span></span><br />
<input type="checkbox" id="check{{$reply.ID}}" name="check{{$reply.ID}}" /> <label class="post-info" for="check{{$reply.ID}}"> <span class="subject">{{$reply.Subject}}</span> <span class="postername">{{if ne $reply.Email ""}}<a href="mailto:{{$reply.Email}}">{{end}}{{if ne $reply.Name ""}}{{$reply.Name}}{{else}}{{if eq $reply.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $reply.Email ""}}</a>{{end}}</span>{{if ne $reply.Tripcode ""}}<span class="tripcode">!{{$reply.Tripcode}}</span>{{end}} {{formatTimestamp $reply.Timestamp}} </label><a href="{{$.webroot}}{{$.board.Dir}}/res/{{$op.ID}}.html#{{$reply.ID}}">No.</a> <a href="javascript:quote({{$reply.ID}})" class="backlink-click">{{$reply.ID}}</a> <span class="post-links"><span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
{{if ne $reply.Filename ""}}
{{if ne $reply.Filename "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}}

View file

@ -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}}

View file

@ -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}};

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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)">&#9660;</a>]</span></span><br />
<input type="checkbox" id="check{{.op.ID}}" name="check{{.op.ID}}" /><label class="post-info" for="check{{.op.ID}}"> <span class="subject">{{.op.Subject}}</span> <span class="postername">{{if ne .op.Email ""}}<a href="mailto:{{.op.Email}}">{{end}}{{if ne .op.Name ""}}{{.op.Name}}{{else}}{{if eq .op.Tripcode ""}}{{.board.Anonymous}}{{end}}{{end}}{{if ne .op.Email ""}}</a>{{end}}</span>{{if ne .op.Tripcode ""}}<span class="tripcode">!{{.op.Tripcode}}</span>{{end}} {{formatTimestamp .op.Timestamp}} </label><a href="{{$.webroot}}{{.board.Dir}}/res/{{.op.ID}}.html#{{.op.ID}}">No.</a> <a href="javascript:quote({{.op.ID}})" class="backlink-click">{{.op.ID}}</a> <span class="post-links"> <span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
<div class="post-text">{{.op.MessageHTML}}</div>
</div>
{{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)">&#9660;</a>]</span></span><br />
<input type="checkbox" id="check{{$reply.ID}}" name="check{{$reply.ID}}" /> <label class="post-info" for="check{{$reply.ID}}"> <span class="subject">{{$reply.Subject}}</span> <span class="postername">{{if ne $reply.Email ""}}<a href="mailto:{{$reply.Email}}">{{end}}{{if ne $reply.Name ""}}{{$reply.Name}}{{else}}{{if eq $reply.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $reply.Email ""}}</a>{{end}}</span>{{if ne $reply.Tripcode ""}}<span class="tripcode">!{{$reply.Tripcode}}</span>{{end}} {{formatTimestamp $reply.Timestamp}} </label><a href="{{$.webroot}}{{$.board.Dir}}/res/{{$.op.ID}}.html#{{$reply.ID}}">No.</a> <a href="javascript:quote({{$reply.ID}})" class="backlink-click">{{$reply.ID}}</a> <span class="post-links"><span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
{{if ne $reply.Filename ""}}
{{if ne $reply.Filename "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>

View file

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