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

Add partially refactored configuration changes

This commit is contained in:
Eggbertx 2021-07-11 11:51:29 -07:00
parent e9cbd89e18
commit dfdb926d71
46 changed files with 904 additions and 670 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.

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,20 @@ 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 = ""
if err = serverutil.CheckAkismetAPIKey(siteConfig.AkismetAPIKey); err != nil {
siteConfig.AkismetAPIKey = ""
}
server.namespaces["banned"] = posting.BanHandler
@ -140,7 +147,7 @@ func initServer() {
// 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
if config.Config.UseFastCGI {
if systemCritical.UseFastCGI {
err = fcgi.Serve(listener, server)
} else {
err = http.Serve(listener, server)
@ -163,10 +170,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 +215,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 +335,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)

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

View file

@ -20,8 +20,10 @@ 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()
boardCfg := config.GetBoardConfig("")
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 +32,7 @@ func BuildFrontPage() error {
defer frontFile.Close()
var recentPostsArr []gcsql.RecentPost
recentPostsArr, err = gcsql.GetRecentPostsGlobal(config.Config.MaxRecentPosts, !config.Config.RecentPostsWithNoFile)
recentPostsArr, err = gcsql.GetRecentPostsGlobal(boardCfg.MaxRecentPosts, !config.Config.RecentPostsWithNoFile)
if err != nil {
return errors.New(gclog.Print(gclog.LErrorLog,
"Failed loading recent posts: "+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,29 @@ 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",
"SillyTags": []string{},
"SiteName": "Gochan",
"SiteWebFolder": "/",
"NewThreadDelay": 30,
"ReplyDelay": 7,
"MaxLineLength": 150,
"ThreadsPerPage": 15,
"RepliesOnBoardPage": 3,
"StickyRepliesOnBoardPage": 1,
"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,
}
cfg *GochanConfig
cfgPath string
cfgDefaults = map[string]interface{}{}
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)
// 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 +49,263 @@ 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 len(gcfg.FirstPage) == 0 {
gcfg.FirstPage = cfgDefaults["FirstPage"].([]string)
changed = true
}
_, err := gcutil.ParseDurationString(cfg.CookieMaxAge)
_, err := gcutil.ParseDurationString(gcfg.CookieMaxAge)
if err == gcutil.ErrInvalidDurationString {
return &ErrInvalidValue{Field: "CookieMaxAge", Value: cfg.CookieMaxAge, Details: err.Error() + cookieMaxAgeEx}
return &ErrInvalidValue{Field: "CookieMaxAge", Value: gcfg.CookieMaxAge, Details: err.Error() + cookieMaxAgeEx}
} else if err == gcutil.ErrEmptyDurationString {
return &ErrInvalidValue{Field: "CookieMaxAge", Details: err.Error() + cookieMaxAgeEx}
} else if err != nil {
return err
}
if cfg.DBtype != "mysql" && cfg.DBtype != "postgresql" {
return &ErrInvalidValue{Field: "DBtype", Value: cfg.DBtype, Details: "currently supported values: mysql, postgresql"}
if gcfg.DBtype != "mysql" && gcfg.DBtype != "postgresql" {
return &ErrInvalidValue{Field: "DBtype", Value: gcfg.DBtype, Details: "currently supported values: mysql, postgresql"}
}
if len(cfg.Styles) == 0 {
return &ErrInvalidValue{Field: "Styles", Value: cfg.Styles}
if len(gcfg.Styles) == 0 {
return &ErrInvalidValue{Field: "Styles", Value: gcfg.Styles}
}
if cfg.DefaultStyle == "" {
cfg.DefaultStyle = cfg.Styles[0].Filename
if gcfg.DefaultStyle == "" {
gcfg.DefaultStyle = gcfg.Styles[0].Filename
changed = true
}
if cfg.NewThreadDelay == 0 {
cfg.NewThreadDelay = cfgDefaults["NewThreadDelay"].(int)
if gcfg.NewThreadDelay == 0 {
gcfg.NewThreadDelay = cfgDefaults["NewThreadDelay"].(int)
changed = true
}
if cfg.ReplyDelay == 0 {
cfg.ReplyDelay = cfgDefaults["ReplyDelay"].(int)
if gcfg.ReplyDelay == 0 {
gcfg.ReplyDelay = cfgDefaults["ReplyDelay"].(int)
changed = true
}
if cfg.MaxLineLength == 0 {
cfg.MaxLineLength = cfgDefaults["MaxLineLength"].(int)
if gcfg.MaxLineLength == 0 {
gcfg.MaxLineLength = cfgDefaults["MaxLineLength"].(int)
changed = true
}
if cfg.ThumbWidth == 0 {
cfg.ThumbWidth = cfgDefaults["ThumbWidth"].(int)
if gcfg.ThumbWidth == 0 {
gcfg.ThumbWidth = cfgDefaults["ThumbWidth"].(int)
changed = true
}
if cfg.ThumbHeight == 0 {
cfg.ThumbHeight = cfgDefaults["ThumbHeight"].(int)
if gcfg.ThumbHeight == 0 {
gcfg.ThumbHeight = cfgDefaults["ThumbHeight"].(int)
changed = true
}
if cfg.ThumbWidthReply == 0 {
cfg.ThumbWidthReply = cfgDefaults["ThumbWidthReply"].(int)
if gcfg.ThumbWidthReply == 0 {
gcfg.ThumbWidthReply = cfgDefaults["ThumbWidthReply"].(int)
changed = true
}
if cfg.ThumbHeightReply == 0 {
cfg.ThumbHeightReply = cfgDefaults["ThumbHeightReply"].(int)
if gcfg.ThumbHeightReply == 0 {
gcfg.ThumbHeightReply = cfgDefaults["ThumbHeightReply"].(int)
changed = true
}
if cfg.ThumbWidthCatalog == 0 {
cfg.ThumbWidthCatalog = cfgDefaults["ThumbWidthCatalog"].(int)
if gcfg.ThumbWidthCatalog == 0 {
gcfg.ThumbWidthCatalog = cfgDefaults["ThumbWidthCatalog"].(int)
changed = true
}
if cfg.ThumbHeightCatalog == 0 {
cfg.ThumbHeightCatalog = cfgDefaults["ThumbHeightCatalog"].(int)
if gcfg.ThumbHeightCatalog == 0 {
gcfg.ThumbHeightCatalog = cfgDefaults["ThumbHeightCatalog"].(int)
changed = true
}
if cfg.ThreadsPerPage == 0 {
cfg.ThreadsPerPage = cfgDefaults["ThreadsPerPage"].(int)
if gcfg.ThreadsPerPage == 0 {
gcfg.ThreadsPerPage = cfgDefaults["ThreadsPerPage"].(int)
changed = true
}
if cfg.RepliesOnBoardPage == 0 {
cfg.RepliesOnBoardPage = cfgDefaults["RepliesOnBoardPage"].(int)
if gcfg.RepliesOnBoardPage == 0 {
gcfg.RepliesOnBoardPage = cfgDefaults["RepliesOnBoardPage"].(int)
changed = true
}
if cfg.StickyRepliesOnBoardPage == 0 {
cfg.StickyRepliesOnBoardPage = cfgDefaults["StickyRepliesOnBoardPage"].(int)
if gcfg.StickyRepliesOnBoardPage == 0 {
gcfg.StickyRepliesOnBoardPage = cfgDefaults["StickyRepliesOnBoardPage"].(int)
changed = true
}
if cfg.BanMsg == "" {
cfg.BanMsg = cfgDefaults["BanMsg"].(string)
if gcfg.BanMsg == "" {
gcfg.BanMsg = cfgDefaults["BanMsg"].(string)
changed = true
}
if cfg.DateTimeFormat == "" {
cfg.DateTimeFormat = cfgDefaults["DateTimeFormat"].(string)
if gcfg.DateTimeFormat == "" {
gcfg.DateTimeFormat = cfgDefaults["DateTimeFormat"].(string)
changed = true
}
if cfg.CaptchaWidth == 0 {
cfg.CaptchaWidth = cfgDefaults["CaptchaWidth"].(int)
if gcfg.CaptchaWidth == 0 {
gcfg.CaptchaWidth = cfgDefaults["CaptchaWidth"].(int)
changed = true
}
if cfg.CaptchaHeight == 0 {
cfg.CaptchaHeight = cfgDefaults["CaptchaHeight"].(int)
if gcfg.CaptchaHeight == 0 {
gcfg.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 = cfgDefaults["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
Port int
UseFastCGI bool
DocumentRoot string
TemplateDir string
LogDir string
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
DBhost string
DBname string
DBusername string
DBpassword string
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."`
AllowVideoUploads bool
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
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"`
}
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" },

View file

@ -98,11 +98,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 +117,63 @@ 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()
}

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,10 +155,11 @@ 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)
@ -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.BanMsg = request.PostFormValue("BanMsg")
// 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.ExpandButton = (request.PostFormValue("ExpandButton") == "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,8 @@ 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, criticalCfg); 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,13 @@ 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{},
// "config": config.Config,
"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

@ -16,7 +16,7 @@ func CheckAkismetAPIKey(key string) error {
if key == "" {
return errors.New("blank key given, Akismet spam checking won't be used")
}
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
}
@ -39,13 +39,15 @@ func CheckAkismetAPIKey(key string) error {
// 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 +86,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 +96,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" },

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="{{.config.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="{{$.config.WebRoot}}{{$.board.Dir}}/src/{{$op.Filename}}" target="_blank">{{$op.Filename}}</a> - ({{formatFilesize $op.Filesize}} , {{$op.ImageW}}x{{$op.ImageH}}, {{$op.FilenameOriginal}})</div>
<a class="upload-container" href="{{$.config.WebRoot}}{{$.board.Dir}}/src/{{$op.Filename}}"><img src="{{$.config.WebRoot}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $op.Filename}}" alt="{{$.config.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="{{$.config.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="{{$.config.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="{{$.config.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="{{$.config.WebRoot}}{{$.board.Dir}}/src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}})</span><br />
<a class="upload-container" href="{{$.config.WebRoot}}{{$.board.Dir}}/src/{{$reply.Filename}}"><img src="{{$.config.WebRoot}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $reply.Filename}}" alt="{{$.config.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="{{$.config.WebRoot}}{{$.board.Dir}}">Return</a>]
[<a href="{{$.config.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="{{$.config.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="{{$.config.WebRoot}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $thread.Filename}}" alt="{{$.config.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

@ -9,5 +9,5 @@ var styles = [
{{- end -}}
];
var defaultStyle = "{{js .DefaultStyle}}";
var webroot = "{{js .SiteWebfolder}}";
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

@ -18,7 +18,7 @@
<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>
<li><a href="{{$.config.WebRoot}}{{$board.Dir}}/" title="{{$board.Description}}">/{{$board.Dir}}/</a> — {{$board.Title}}</li>
{{end}}
{{end}}
</ul>
@ -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="{{$.config.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="{{$.config.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="{{$.systemCritical.WebRoot}}">Home</a> | <a href="{{$.systemCritical.WebRoot}}#boards">Boards</a> | <a href="{{$.config.WebRoot}}#rules">Rules</a> | <a href="{{$.config.WebRoot}}#faq">FAQ</a><br />
Powered by <a href="http://github.com/eggbertx/gochan/">Gochan {{version}}</a><br />
</div>
</body>

View file

@ -10,13 +10,13 @@
{{- 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>
<link rel="stylesheet" href="{{.config.WebRoot}}css/global.css" />
<link id="theme" rel="stylesheet" href="{{.config.WebRoot}}css/{{.config.DefaultStyle}}" />
<link rel="shortcut icon" href="{{.config.WebRoot}}favicon.png">
<script type="text/javascript" src="{{$.config.WebRoot}}js/consts.js"></script>
<script type="text/javascript" src="{{$.config.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="{{$.config.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_"
}