mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-20 09:26:23 -07:00
Make config loading more flexible and powerful
Improve value validation, allow for defaults and set critical fields
This commit is contained in:
parent
6bd77b7c34
commit
cb7913398c
10 changed files with 630 additions and 227 deletions
2
go.mod
2
go.mod
|
@ -15,5 +15,5 @@ require (
|
|||
github.com/tdewolff/parse v2.3.4+incompatible // indirect
|
||||
github.com/tdewolff/test v1.0.6 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
||||
golang.org/x/net v0.0.0-20210222171744-9060382bd457
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -59,6 +59,8 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y
|
|||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210222171744-9060382bd457 h1:hMm9lBjyNLe/c9C6bElQxp4wsrleaJn1vXMZIQkNN44=
|
||||
golang.org/x/net v0.0.0-20210222171744-9060382bd457/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
|
@ -2,19 +2,68 @@ package config
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
"net"
|
||||
"reflect"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
)
|
||||
|
||||
const (
|
||||
randomStringSize = 16
|
||||
)
|
||||
|
||||
var (
|
||||
Config GochanConfig
|
||||
cfgPath string
|
||||
Config *GochanConfig
|
||||
cfgPath string
|
||||
cfgDefaults = map[string]interface{}{
|
||||
"Port": 8080,
|
||||
"FirstPage": []string{"index.html", "board.html", "firstrun.html"},
|
||||
"DocumentRoot": "html",
|
||||
"TemplateDir": "templates",
|
||||
"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,
|
||||
}
|
||||
)
|
||||
|
||||
// Style represents a theme (Pipes, Dark, etc)
|
||||
|
@ -23,87 +72,102 @@ type Style struct {
|
|||
Filename string
|
||||
}
|
||||
|
||||
// GochanConfig stores crucial info and is read from/written to gochan.json
|
||||
// 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
|
||||
Port int
|
||||
FirstPage []string
|
||||
Username string
|
||||
UseFastCGI bool
|
||||
DebugMode bool `description:"Disables several spam/browser checks that can cause problems when hosting an instance locally."`
|
||||
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."`
|
||||
|
||||
DocumentRoot string
|
||||
TemplateDir string
|
||||
LogDir string
|
||||
DocumentRoot string `critical:"true"`
|
||||
TemplateDir string `critical:"true"`
|
||||
LogDir string `critical:"true"`
|
||||
|
||||
DBtype string
|
||||
DBhost string
|
||||
DBname string
|
||||
DBusername string
|
||||
DBpassword string
|
||||
DBprefix string
|
||||
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"`
|
||||
|
||||
Lockdown bool `description:"Disables posting." default:"unchecked"`
|
||||
LockdownMessage string `description:"Message displayed when someone tries to post while the site is on lockdown."`
|
||||
Sillytags []string `description:"List of randomly selected staff tags separated by line, e.g. <span style=\"color: red;\">## Mod</span>, to be randomly assigned to posts if UseSillytags is checked. Don't include the \"## \""`
|
||||
UseSillytags bool `description:"Use Sillytags" default:"unchecked"`
|
||||
Modboard string `description:"A super secret clubhouse board that only staff can view/post to." default:"staff"`
|
||||
|
||||
SiteName string `description:"The name of the site that appears in the header of the front page." default:"Gochan"`
|
||||
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"`
|
||||
SiteHeaderURL string `description:"To be honest, I'm not even sure what this does. It'll probably be removed later."`
|
||||
SiteWebfolder string `description:"The HTTP root appearing in the browser (e.g. https://gochan.org/<SiteWebFolder>" default:"/"`
|
||||
SiteDomain string `description:"The server's domain (duh). Do not edit this unless you know what you are doing or BAD THINGS WILL HAPPEN!" default:"127.0.0.1" critical:"true"`
|
||||
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!"`
|
||||
|
||||
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. This should appear in the list above or bad things might 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."`
|
||||
|
||||
AllowDuplicateImages bool `description:"Disabling this will cause gochan to reject a post if the image has already been uploaded for another post.<br />This may end up being removed or being made board-specific in the future." default:"checked"`
|
||||
AllowVideoUploads bool `description:"Allows users to upload .webm videos. <br />This 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." default:"30"`
|
||||
ReplyDelay int `description:"Same as the above, but for replies." default:"7"`
|
||||
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." default:"150"`
|
||||
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"`
|
||||
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."`
|
||||
|
||||
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." default:"200"`
|
||||
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." default:"200"`
|
||||
ThumbWidthReply int `description:"Same as ThumbWidth and ThumbHeight but for reply images." default:"125"`
|
||||
ThumbHeightReply int `description:"Same as ThumbWidth and ThumbHeight but for reply images." default:"125"`
|
||||
ThumbWidthCatalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images." default:"50"`
|
||||
ThumbHeightCatalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images." default:"50"`
|
||||
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"`
|
||||
|
||||
ThreadsPerPage int `default:"15"`
|
||||
RepliesOnBoardPage int `description:"Number of replies to a thread to show on the board page." default:"3"`
|
||||
StickyRepliesOnBoardPage int `description:"Same as above for stickied threads." default:"1"`
|
||||
BanColors []string `description:"Colors to be used for public ban messages (e.g. USER WAS BANNED FOR THIS POST).<br />Each entry should be on its own line, and should look something like this:<br />username1:#FF0000<br />username2:#FAF00F<br />username3:blue<br />Invalid entries/nonexistent usernames will show a warning and use the default red."`
|
||||
BanMsg string `description:"The default public ban message." default:"USER WAS BANNED FOR THIS POST"`
|
||||
EmbedWidth int `description:"The width for inline/expanded webm videos." default:"200"`
|
||||
EmbedHeight int `description:"The height for inline/expanded webm videos." default:"164"`
|
||||
ExpandButton bool `description:"If checked, adds [Embed] after a Youtube, Vimeo, etc link to toggle an inline video frame." default:"checked"`
|
||||
ImagesOpenNewTab bool `description:"If checked, thumbnails will open the respective image/video in a new tab instead of expanding them." default:"unchecked"`
|
||||
MakeURLsHyperlinked bool `description:"If checked, URLs in posts will be turned into a hyperlink. If unchecked, ExpandButton and NewTabOnOutlinks are ignored." default:"checked"`
|
||||
NewTabOnOutlinks bool `description:"If checked, links to external sites will open in a new tab." default:"checked"`
|
||||
DisableBBcode bool `description:"If checked, gochan will not compile bbcode into HTML" default:"unchecked"`
|
||||
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."`
|
||||
|
||||
MinifyHTML bool `description:"If checked, gochan will minify html files when building" default:"checked"`
|
||||
MinifyJS bool `description:"If checked, gochan will minify js and json files when building" default:"checked"`
|
||||
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 webm videos."`
|
||||
EmbedHeight int `description:"The height for inline/expanded webm 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"`
|
||||
|
||||
DateTimeFormat string `description:"The format used for dates. See <a href=\"https://golang.org/pkg/time/#Time.Format\">here</a> for more info." default:"Mon, January 02, 2006 15:04 PM"`
|
||||
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" default:"240"`
|
||||
CaptchaHeight int `description:"Height of the generated captcha image" default:"80"`
|
||||
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." default:"15"`
|
||||
EnableGeoIP bool `description:"If checked, this enables the usage of GeoIP for posts." default:"checked"`
|
||||
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." default:"/usr/share/GeoIP/GeoIP.dat"`
|
||||
MaxRecentPosts int `description:"The maximum number of posts to show on the Recent Posts list on the front page." default:"3"`
|
||||
RecentPostsWithNoFile bool `description:"If checked, recent posts with no image/upload are shown on the front page (as well as those with images" default:"unchecked"`
|
||||
EnableAppeals bool `description:"If checked, allow banned users to appeal their bans.<br />This will likely be removed (permanently allowing appeals) or made board-specific in the future." default:"checked"`
|
||||
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"`
|
||||
|
||||
TimeZone int `json:"-"`
|
||||
Version *GochanVersion `json:"-"`
|
||||
jsonLocation string `json:"-"`
|
||||
TimeZone int `json:"-"`
|
||||
Version *GochanVersion `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)
|
||||
numFields := cType.NumField()
|
||||
out := make(map[string]interface{})
|
||||
for f := 0; f < numFields; f++ {
|
||||
field := cVal.Field(f)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
out[cType.Field(f).Name] = field.Elem().Interface()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (cfg *GochanConfig) checkString(val, defaultVal string, critical bool, msg string) string {
|
||||
|
@ -134,152 +198,116 @@ func (cfg *GochanConfig) checkInt(val, defaultVal int, critical bool, msg string
|
|||
return val
|
||||
}
|
||||
|
||||
// InitConfig loads and parses gochan.json and verifies its contents
|
||||
func InitConfig(versionStr string) {
|
||||
cfgPath = findResource("gochan.json", "/etc/gochan/gochan.json")
|
||||
if cfgPath == "" {
|
||||
fmt.Println("gochan.json not found")
|
||||
os.Exit(1)
|
||||
// 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}
|
||||
}
|
||||
|
||||
jfile, err := ioutil.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading %s: %s\n", cfgPath, err.Error())
|
||||
os.Exit(1)
|
||||
changed := false
|
||||
if len(cfg.FirstPage) == 0 {
|
||||
cfg.FirstPage = cfgDefaults["FirstPage"].([]string)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(jfile, &Config); err != nil {
|
||||
fmt.Printf("Error parsing %s: %s\n", cfgPath, err.Error())
|
||||
os.Exit(1)
|
||||
if cfg.DBtype != "mysql" && cfg.DBtype != "postgresql" {
|
||||
return &ErrInvalidValue{Field: "DBtype", Value: cfg.DBtype, Details: "currently supported values: mysql, postgresql"}
|
||||
}
|
||||
|
||||
Config.LogDir = findResource(Config.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 {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
if len(cfg.Styles) == 0 {
|
||||
return &ErrInvalidValue{Field: "Styles", Value: cfg.Styles}
|
||||
}
|
||||
|
||||
Config.checkString(Config.ListenIP, "", true,
|
||||
"ListenIP not set in gochan.json, halting.")
|
||||
|
||||
if Config.Port == 0 {
|
||||
Config.Port = 80
|
||||
if cfg.DefaultStyle == "" {
|
||||
cfg.DefaultStyle = cfg.Styles[0].Filename
|
||||
changed = true
|
||||
}
|
||||
|
||||
if len(Config.FirstPage) == 0 {
|
||||
Config.FirstPage = []string{"index.html", "1.html", "firstrun.html"}
|
||||
if cfg.NewThreadDelay == 0 {
|
||||
cfg.NewThreadDelay = cfgDefaults["NewThreadDelay"].(int)
|
||||
changed = true
|
||||
}
|
||||
|
||||
Config.Username = Config.checkString(Config.Username, "gochan", false,
|
||||
"Username not set in gochan.json, using 'gochan' as default")
|
||||
Config.DocumentRoot = Config.checkString(Config.DocumentRoot, "gochan", true,
|
||||
"DocumentRoot not set in gochan.json, halting.")
|
||||
|
||||
wd, wderr := os.Getwd()
|
||||
if wderr == nil {
|
||||
_, staterr := os.Stat(path.Join(wd, Config.DocumentRoot, "css"))
|
||||
if staterr == nil {
|
||||
Config.DocumentRoot = path.Join(wd, Config.DocumentRoot)
|
||||
if cfg.ReplyDelay == 0 {
|
||||
cfg.ReplyDelay = cfgDefaults["ReplyDelay"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.MaxLineLength == 0 {
|
||||
cfg.MaxLineLength = cfgDefaults["MaxLineLength"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbWidth == 0 {
|
||||
cfg.ThumbWidth = cfgDefaults["ThumbWidth"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbHeight == 0 {
|
||||
cfg.ThumbHeight = cfgDefaults["ThumbHeight"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbWidthReply == 0 {
|
||||
cfg.ThumbWidthReply = cfgDefaults["ThumbWidthReply"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbHeightReply == 0 {
|
||||
cfg.ThumbHeightReply = cfgDefaults["ThumbHeightReply"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbWidthCatalog == 0 {
|
||||
cfg.ThumbWidthCatalog = cfgDefaults["ThumbWidthCatalog"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThumbHeightCatalog == 0 {
|
||||
cfg.ThumbHeightCatalog = cfgDefaults["ThumbHeightCatalog"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.ThreadsPerPage == 0 {
|
||||
cfg.ThreadsPerPage = cfgDefaults["ThreadsPerPage"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.RepliesOnBoardPage == 0 {
|
||||
cfg.RepliesOnBoardPage = cfgDefaults["RepliesOnBoardPage"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.StickyRepliesOnBoardPage == 0 {
|
||||
cfg.StickyRepliesOnBoardPage = cfgDefaults["StickyRepliesOnBoardPage"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.BanMsg == "" {
|
||||
cfg.BanMsg = cfgDefaults["BanMsg"].(string)
|
||||
changed = true
|
||||
}
|
||||
if cfg.DateTimeFormat == "" {
|
||||
cfg.DateTimeFormat = cfgDefaults["DateTimeFormat"].(string)
|
||||
changed = true
|
||||
}
|
||||
if cfg.CaptchaWidth == 0 {
|
||||
cfg.CaptchaWidth = cfgDefaults["CaptchaWidth"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.CaptchaHeight == 0 {
|
||||
cfg.CaptchaHeight = cfgDefaults["CaptchaHeight"].(int)
|
||||
changed = true
|
||||
}
|
||||
if cfg.EnableGeoIP {
|
||||
if cfg.GeoIPDBlocation == "" {
|
||||
return &ErrInvalidValue{Field: "GeoIPDBlocation", Value: "", Details: "GeoIPDBlocation must be set in gochan.json if EnableGeoIP is true"}
|
||||
}
|
||||
}
|
||||
|
||||
Config.TemplateDir = Config.checkString(
|
||||
findResource(Config.TemplateDir, "templates", "/usr/local/share/gochan/templates/", "/usr/share/gochan/templates/"), "", true,
|
||||
"TemplateDir not set in gochan.json or unable to locate template directory, halting.")
|
||||
|
||||
Config.checkString(Config.DBtype, "", true,
|
||||
"DBtype not set in gochan.json, halting (currently supported values: mysql,postgresql)")
|
||||
Config.checkString(Config.DBhost, "", true,
|
||||
"DBhost not set in gochan.json, halting.")
|
||||
Config.DBname = Config.checkString(Config.DBname, "gochan", false,
|
||||
"DBname not set in gochan.json, setting to 'gochan'")
|
||||
|
||||
Config.checkString(Config.DBusername, "", true,
|
||||
"DBusername not set in gochan, halting.")
|
||||
Config.checkString(Config.DBpassword, "", true,
|
||||
"DBpassword not set in gochan, halting.")
|
||||
Config.LockdownMessage = Config.checkString(Config.LockdownMessage,
|
||||
"The administrator has temporarily disabled posting. We apologize for the inconvenience", false, "")
|
||||
|
||||
Config.checkString(Config.SiteName, "", true,
|
||||
"SiteName not set in gochan.json, halting.")
|
||||
Config.checkString(Config.SiteDomain, "", true,
|
||||
"SiteName not set in gochan.json, halting.")
|
||||
|
||||
if Config.SiteWebfolder == "" {
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog, "SiteWebFolder not set in gochan.json, using / as default.")
|
||||
} else if string(Config.SiteWebfolder[0]) != "/" {
|
||||
Config.SiteWebfolder = "/" + Config.SiteWebfolder
|
||||
}
|
||||
if Config.SiteWebfolder[len(Config.SiteWebfolder)-1:] != "/" {
|
||||
Config.SiteWebfolder += "/"
|
||||
if cfg.MaxLogDays == 0 {
|
||||
cfg.MaxLogDays = cfgDefaults["MaxLogDays"].(int)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if Config.Styles == nil {
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog|gclog.LFatal, "Styles not set in gochan.json, halting.")
|
||||
if cfg.RandomSeed == "" {
|
||||
cfg.RandomSeed = randomString(randomStringSize)
|
||||
changed = true
|
||||
}
|
||||
|
||||
Config.DefaultStyle = Config.checkString(Config.DefaultStyle, Config.Styles[0].Filename, false, "")
|
||||
|
||||
Config.NewThreadDelay = Config.checkInt(Config.NewThreadDelay, 30, false, "")
|
||||
Config.ReplyDelay = Config.checkInt(Config.ReplyDelay, 7, false, "")
|
||||
Config.MaxLineLength = Config.checkInt(Config.MaxLineLength, 150, false, "")
|
||||
//ReservedTrips string //eventually this will be map[string]string
|
||||
|
||||
Config.ThumbWidth = Config.checkInt(Config.ThumbWidth, 200, false, "")
|
||||
Config.ThumbHeight = Config.checkInt(Config.ThumbHeight, 200, false, "")
|
||||
Config.ThumbWidthReply = Config.checkInt(Config.ThumbWidthReply, 125, false, "")
|
||||
Config.ThumbHeightReply = Config.checkInt(Config.ThumbHeightReply, 125, false, "")
|
||||
Config.ThumbWidthCatalog = Config.checkInt(Config.ThumbWidthCatalog, 50, false, "")
|
||||
Config.ThumbHeightCatalog = Config.checkInt(Config.ThumbHeightCatalog, 50, false, "")
|
||||
|
||||
Config.ThreadsPerPage = Config.checkInt(Config.ThreadsPerPage, 10, false, "")
|
||||
Config.RepliesOnBoardPage = Config.checkInt(Config.RepliesOnBoardPage, 3, false, "")
|
||||
Config.StickyRepliesOnBoardPage = Config.checkInt(Config.StickyRepliesOnBoardPage, 1, false, "")
|
||||
|
||||
/*config.BanColors, err = c.GetString("threads", "ban_colors") //eventually this will be map[string] string
|
||||
if err != nil {
|
||||
config.BanColors = "admin:#CC0000"
|
||||
}*/
|
||||
|
||||
Config.BanMsg = Config.checkString(Config.BanMsg, "(USER WAS BANNED FOR THIS POST)", false, "")
|
||||
Config.DateTimeFormat = Config.checkString(Config.DateTimeFormat, "Mon, January 02, 2006 15:04 PM", false, "")
|
||||
|
||||
Config.CaptchaWidth = Config.checkInt(Config.CaptchaWidth, 240, false, "")
|
||||
Config.CaptchaHeight = Config.checkInt(Config.CaptchaHeight, 80, false, "")
|
||||
|
||||
if Config.EnableGeoIP {
|
||||
if Config.GeoIPDBlocation == "" {
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog, "GeoIPDBlocation not set in gochan.json, disabling EnableGeoIP")
|
||||
Config.EnableGeoIP = false
|
||||
}
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if Config.MaxLogDays == 0 {
|
||||
Config.MaxLogDays = 15
|
||||
}
|
||||
|
||||
if Config.RandomSeed == "" {
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog, "RandomSeed not set in gochan.json, Generating a random one.")
|
||||
for i := 0; i < 8; i++ {
|
||||
num := rand.Intn(127-32) + 32
|
||||
Config.RandomSeed += fmt.Sprintf("%c", num)
|
||||
}
|
||||
configJSON, _ := json.MarshalIndent(Config, "", "\t")
|
||||
if err = ioutil.WriteFile(cfgPath, configJSON, 0777); err != nil {
|
||||
gclog.Printf(gclog.LErrorLog|gclog.LStdLog|gclog.LFatal, "Unable to write %s with randomly generated seed: %s", cfgPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
_, zoneOffset := time.Now().Zone()
|
||||
Config.TimeZone = zoneOffset / 60 / 60
|
||||
|
||||
// msgfmtr.InitBBcode()
|
||||
|
||||
Config.Version = ParseVersion(versionStr)
|
||||
Config.Version.Normalize()
|
||||
return cfg.Write()
|
||||
}
|
||||
|
||||
func (cfg *GochanConfig) Write() error {
|
||||
str, err := json.MarshalIndent(cfg, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(cfg.jsonLocation, str, 0777)
|
||||
}
|
||||
|
|
73
pkg/config/config_test.go
Normal file
73
pkg/config/config_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBadTypes(t *testing.T) {
|
||||
_, _, err := ParseJSON([]byte(badTypeJSON))
|
||||
if err == nil {
|
||||
t.Fatal(`"successfully" parsed JSON file with incorrect value type`)
|
||||
}
|
||||
_, ok := err.(*json.UnmarshalTypeError)
|
||||
if !ok {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingRequired(t *testing.T) {
|
||||
_, missing, err := ParseJSON([]byte(missingRequiredJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
t.Fatal("JSON string with deliberately missing fields passed validation (this shouldn't happen for this test)")
|
||||
}
|
||||
fieldsStr := "Missing fields:\n"
|
||||
for _, field := range missing {
|
||||
fieldsStr += fmt.Sprintf("field name: %s\ndescription: %s\ncritical: %t\n\n", field.Name, field.Description, field.Critical)
|
||||
}
|
||||
t.Log(fieldsStr)
|
||||
}
|
||||
|
||||
func TestBareMinimumJSON(t *testing.T) {
|
||||
_, missing, err := ParseJSON([]byte(bareMinimumJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return
|
||||
}
|
||||
fieldsStr := "Missing fields:\n"
|
||||
for _, field := range missing {
|
||||
fieldsStr += fmt.Sprintf("field name: %s\ndescription: %s\ncritical: %t\n\n", field.Name, field.Description, field.Critical)
|
||||
}
|
||||
t.Fatal(fieldsStr)
|
||||
}
|
||||
|
||||
func TestValidJSON(t *testing.T) {
|
||||
_, missing, err := ParseJSON([]byte(validCfgJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return
|
||||
}
|
||||
fieldsStr := "Missing fields:\n"
|
||||
for _, field := range missing {
|
||||
fieldsStr += fmt.Sprintf("field name: %s\ndescription: %s\ncritical: %t\n\n", field.Name, field.Description, field.Critical)
|
||||
}
|
||||
t.Fatal(fieldsStr)
|
||||
}
|
||||
|
||||
func TestValidValues(t *testing.T) {
|
||||
cfg, _, err := ParseJSON([]byte(bareMinimumJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if err := cfg.ValidateValues(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
123
pkg/config/jsonvars_test.go
Normal file
123
pkg/config/jsonvars_test.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
// the bare minimum fields required to pass GochanConfig.validate.
|
||||
// this doesn't mean that the values are valid, just that they exist
|
||||
bareMinimumJSON = `{
|
||||
"ListenIP": "127.0.0.1",
|
||||
"Port": 8080,
|
||||
"Username": "gochan",
|
||||
"UseFastCGI": true,
|
||||
"DBtype": "mysql",
|
||||
"DBhost": "127.0.0.1:3306",
|
||||
"DBname": "gochan",
|
||||
"DBusername": "gochan",
|
||||
"DBpassword": "",
|
||||
"SiteDomain": "127.0.0.1",
|
||||
"SiteWebfolder": "/",
|
||||
|
||||
"Styles": [
|
||||
{ "Name": "Pipes", "Filename": "pipes.css" },
|
||||
{ "Name": "Burichan", "Filename": "burichan.css" },
|
||||
{ "Name": "Dark", "Filename": "dark.css" },
|
||||
{ "Name": "Photon", "Filename": "photon.css" }
|
||||
],
|
||||
"RandomSeed": "jeiwohaeiogpehwgui"
|
||||
}`
|
||||
validCfgJSON = `{
|
||||
"ListenIP": "127.0.0.1",
|
||||
"Port": 8080,
|
||||
"FirstPage": ["index.html","firstrun.html","1.html"],
|
||||
"Username": "gochan",
|
||||
"UseFastCGI": false,
|
||||
"DebugMode": false,
|
||||
|
||||
"DocumentRoot": "html",
|
||||
"TemplateDir": "templates",
|
||||
"LogDir": "log",
|
||||
|
||||
"DBtype": "mysql",
|
||||
"DBtype_alt": "postgres",
|
||||
"DBhost": "127.0.0.1:3306",
|
||||
"_comment": "gochan can use either a URL or a UNIX socket for MySQL connections",
|
||||
"DBhost_alt": "unix(/var/run/mysqld/mysqld.sock)",
|
||||
"DBname": "gochan",
|
||||
"DBusername": "gochan",
|
||||
"DBpassword": "",
|
||||
"DBprefix": "gc_",
|
||||
|
||||
"Lockdown": false,
|
||||
"LockdownMessage": "This imageboard has temporarily disabled posting. We apologize for the inconvenience",
|
||||
"Sillytags": ["Admin","Mod","Janitor","Faget","Kick me","Derpy","Troll","worst pony"],
|
||||
"UseSillytags": false,
|
||||
"Modboard": "staff",
|
||||
|
||||
"SiteName": "Gochan",
|
||||
"SiteSlogan": "",
|
||||
"SiteDomain": "127.0.0.1",
|
||||
"SiteWebfolder": "/",
|
||||
|
||||
"Styles": [
|
||||
{ "Name": "Pipes", "Filename": "pipes.css" },
|
||||
{ "Name": "Burichan", "Filename": "burichan.css" },
|
||||
{ "Name": "Dark", "Filename": "dark.css" },
|
||||
{ "Name": "Photon", "Filename": "photon.css" }
|
||||
],
|
||||
"DefaultStyle": "pipes.css",
|
||||
|
||||
"RejectDuplicateImages": true,
|
||||
"NewThreadDelay": 30,
|
||||
"ReplyDelay": 7,
|
||||
"MaxLineLength": 150,
|
||||
"ReservedTrips": [
|
||||
"thischangesto##this",
|
||||
"andthischangesto##this"
|
||||
],
|
||||
|
||||
"ThumbWidth": 200,
|
||||
"ThumbHeight": 200,
|
||||
"ThumbWidthReply": 125,
|
||||
"ThumbHeightReply": 125,
|
||||
"ThumbWidthCatalog": 50,
|
||||
"ThumbHeightCatalog": 50,
|
||||
|
||||
"ThreadsPerPage": 15,
|
||||
"PostsPerThreadPage": 50,
|
||||
"RepliesOnBoardPage": 3,
|
||||
"StickyRepliesOnBoardPage": 1,
|
||||
"BanMsg": "USER WAS BANNED FOR THIS POST",
|
||||
"EmbedWidth": 200,
|
||||
"EmbedHeight": 164,
|
||||
"ExpandButton": true,
|
||||
"ImagesOpenNewTab": true,
|
||||
"MakeURLsHyperlinked": true,
|
||||
"NewTabOnOutlinks": true,
|
||||
|
||||
"MinifyHTML": true,
|
||||
"MinifyJS": true,
|
||||
|
||||
"DateTimeFormat": "Mon, January 02, 2006 15:04 PM",
|
||||
"AkismetAPIKey": "",
|
||||
"UseCaptcha": false,
|
||||
"CaptchaWidth": 240,
|
||||
"CaptchaHeight": 80,
|
||||
"CaptchaMinutesExpire": 15,
|
||||
"EnableGeoIP": true,
|
||||
"_comment": "set GeoIPDBlocation to cf to use Cloudflare's GeoIP",
|
||||
"GeoIPDBlocation": "/usr/share/GeoIP/GeoIP.dat",
|
||||
"MaxRecentPosts": 3,
|
||||
"RecentPostsWithNoFile": false,
|
||||
"Verbosity": 0,
|
||||
"EnableAppeals": true,
|
||||
"MaxLogDays": 14,
|
||||
"_comment": "Set RandomSeed to a (preferrably large) string of letters and numbers",
|
||||
"RandomSeed": ""
|
||||
}`
|
||||
)
|
||||
|
||||
var (
|
||||
missingRequiredJSON = strings.ReplaceAll(validCfgJSON, `"ListenIP": "127.0.0.1",`, "")
|
||||
badTypeJSON = strings.ReplaceAll(validCfgJSON, `"RandomSeed": ""`, `"RandomSeed": 32`)
|
||||
)
|
|
@ -1,6 +1,39 @@
|
|||
package config
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
)
|
||||
|
||||
// MissingField represents a field missing from the configuration file
|
||||
type MissingField struct {
|
||||
Name string
|
||||
Critical bool
|
||||
Description string
|
||||
}
|
||||
|
||||
// ErrInvalidValue represents a GochanConfig field with a bad value
|
||||
type ErrInvalidValue struct {
|
||||
Field string
|
||||
Value interface{}
|
||||
Details string
|
||||
}
|
||||
|
||||
func (iv *ErrInvalidValue) Error() string {
|
||||
str := fmt.Sprintf("invalid %s value: %#v", iv.Field, iv.Value)
|
||||
if iv.Details != "" {
|
||||
str += " - " + iv.Details
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// copied from gcutil to avoid import loop
|
||||
func findResource(paths ...string) string {
|
||||
|
@ -12,3 +45,159 @@ func findResource(paths ...string) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ParseJSON loads and parses JSON data, returning a GochanConfig pointer, any critical missing
|
||||
// fields that don't have defaults, and any error from parsing the file. This doesn't mean that the
|
||||
// values are valid, just that they exist
|
||||
func ParseJSON(ba []byte) (*GochanConfig, []MissingField, error) {
|
||||
var missing []MissingField
|
||||
cfg := &GochanConfig{}
|
||||
err := json.Unmarshal(ba, cfg)
|
||||
if err != nil {
|
||||
// checking for malformed JSON, invalid field types
|
||||
return cfg, nil, err
|
||||
}
|
||||
|
||||
var checker map[string]interface{} // using this for checking for missing fields
|
||||
json.Unmarshal(ba, &checker)
|
||||
|
||||
cVal := reflect.ValueOf(cfg).Elem()
|
||||
cType := reflect.TypeOf(*cfg)
|
||||
numFields := cType.NumField()
|
||||
for f := 0; f < numFields; f++ {
|
||||
fType := cType.Field(f)
|
||||
fVal := cVal.Field(f)
|
||||
critical := fType.Tag.Get("critical") == "true"
|
||||
if !fVal.CanSet() || fType.Tag.Get("json") == "-" {
|
||||
// field is unexported and isn't read from the JSON file
|
||||
continue
|
||||
}
|
||||
|
||||
if checker[fType.Name] != nil {
|
||||
// field is in the JSON file
|
||||
continue
|
||||
}
|
||||
if cfgDefaults[fType.Name] != nil {
|
||||
// the field isn't in the JSON file but has a default value that we can use
|
||||
fVal.Set(reflect.ValueOf(cfgDefaults[fType.Name]))
|
||||
continue
|
||||
}
|
||||
if critical {
|
||||
// the field isn't in the JSON file and has no default value
|
||||
missing = append(missing, MissingField{
|
||||
Name: fType.Name,
|
||||
Description: fType.Tag.Get("description"),
|
||||
Critical: critical,
|
||||
})
|
||||
}
|
||||
}
|
||||
return cfg, missing, err
|
||||
}
|
||||
|
||||
// InitConfig loads and parses gochan.json on startup and verifies its contents
|
||||
func InitConfig(versionStr string) {
|
||||
cfgPath = findResource("gochan.json", "/etc/gochan/gochan.json")
|
||||
if cfgPath == "" {
|
||||
fmt.Println("gochan.json not found")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
jfile, err := ioutil.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading %s: %s\n", cfgPath, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var fields []MissingField
|
||||
Config, fields, err = ParseJSON(jfile)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing %s: %s", cfgPath, err.Error())
|
||||
}
|
||||
Config.jsonLocation = cfgPath
|
||||
|
||||
numMissing := 0
|
||||
for _, missing := range fields {
|
||||
fmt.Println("Missing field:", missing.Name)
|
||||
if missing.Description != "" {
|
||||
fmt.Println("Description:", missing.Description)
|
||||
}
|
||||
numMissing++
|
||||
}
|
||||
if numMissing > 0 {
|
||||
fmt.Println("gochan failed to load the configuration file because there are fields missing.\nSee gochan.example.json in sample-configs for an example configuration file")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = Config.ValidateValues(); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err = os.Stat(Config.DocumentRoot); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err = os.Stat(Config.TemplateDir); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err = os.Stat(Config.LogDir); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Config.LogDir = findResource(Config.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 {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if Config.Port == 0 {
|
||||
Config.Port = 80
|
||||
}
|
||||
|
||||
if len(Config.FirstPage) == 0 {
|
||||
Config.FirstPage = []string{"index.html", "1.html", "firstrun.html"}
|
||||
}
|
||||
|
||||
if Config.SiteWebfolder == "" {
|
||||
Config.SiteWebfolder = "/"
|
||||
}
|
||||
|
||||
if Config.SiteWebfolder[0] != '/' {
|
||||
Config.SiteWebfolder = "/" + Config.SiteWebfolder
|
||||
}
|
||||
if Config.SiteWebfolder[len(Config.SiteWebfolder)-1] != '/' {
|
||||
Config.SiteWebfolder += "/"
|
||||
}
|
||||
|
||||
if Config.EnableGeoIP {
|
||||
if _, err = os.Stat(Config.GeoIPDBlocation); err != nil {
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog, "Unable to find GeoIP file location set in gochan.json, disabling GeoIP")
|
||||
}
|
||||
Config.EnableGeoIP = false
|
||||
}
|
||||
|
||||
_, zoneOffset := time.Now().Zone()
|
||||
Config.TimeZone = zoneOffset / 60 / 60
|
||||
|
||||
Config.Version = ParseVersion(versionStr)
|
||||
Config.Version.Normalize()
|
||||
}
|
||||
|
||||
// reimplemented from gcutil.RandomString to avoid a dependency cycle
|
||||
func randomString(length int) string {
|
||||
var str string
|
||||
for i := 0; i < length; i++ {
|
||||
num := rand.Intn(127)
|
||||
if num < 32 {
|
||||
num += 32
|
||||
}
|
||||
str += fmt.Sprintf("%c", num)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
|
|
@ -46,6 +46,6 @@ func TestGochanLog(t *testing.T) {
|
|||
Println(LErrorLog, "Error log", "(Println)")
|
||||
Println(LStaffLog, "Staff log", "(Println)")
|
||||
Println(LAccessLog|LErrorLog, "Access and error log", "(Println)")
|
||||
Println(LAccessLog|LStaffLog|LFatal, "Fatal access and staff log", "(Println)")
|
||||
Println(LAccessLog, "This shouldn't be here", "(Println)")
|
||||
// Println(LAccessLog|LStaffLog|LFatal, "Fatal access and staff log", "(Println)")
|
||||
// Println(LAccessLog, "This shouldn't be here", "(Println)")
|
||||
}
|
||||
|
|
|
@ -238,7 +238,7 @@ var funcMap = template.FuncMap{
|
|||
return loopArr
|
||||
},
|
||||
"generateConfigTable": func() string {
|
||||
configType := reflect.TypeOf(config.Config)
|
||||
configType := reflect.TypeOf(*config.Config)
|
||||
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++ {
|
||||
|
|
|
@ -108,7 +108,6 @@ var actions = map[string]Action{
|
|||
config.Config.Modboard = request.PostFormValue("Modboard")
|
||||
config.Config.SiteName = request.PostFormValue("SiteName")
|
||||
config.Config.SiteSlogan = request.PostFormValue("SiteSlogan")
|
||||
config.Config.SiteHeaderURL = request.PostFormValue("SiteHeaderURL")
|
||||
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")
|
||||
|
@ -118,8 +117,7 @@ var actions = map[string]Action{
|
|||
}
|
||||
config.Styles = Styles */
|
||||
config.Config.DefaultStyle = request.PostFormValue("DefaultStyle")
|
||||
config.Config.AllowDuplicateImages = (request.PostFormValue("AllowDuplicateImages") == "on")
|
||||
config.Config.AllowVideoUploads = (request.PostFormValue("AllowVideoUploads") == "on")
|
||||
config.Config.RejectDuplicateImages = (request.PostFormValue("RejectDuplicateImages") == "on")
|
||||
NewThreadDelay, err := strconv.Atoi(request.PostFormValue("NewThreadDelay"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
|
@ -205,14 +203,6 @@ var actions = map[string]Action{
|
|||
config.Config.StickyRepliesOnBoardPage = StickyRepliesOnBoardPage
|
||||
}
|
||||
|
||||
BanColorsArr := strings.Split(request.PostFormValue("BanColors"), "\n")
|
||||
var BanColors []string
|
||||
for _, color := range BanColorsArr {
|
||||
BanColors = append(BanColors, strings.Trim(color, " \n\r"))
|
||||
|
||||
}
|
||||
config.Config.BanColors = BanColors
|
||||
|
||||
config.Config.BanMsg = request.PostFormValue("BanMsg")
|
||||
EmbedWidth, err := strconv.Atoi(request.PostFormValue("EmbedWidth"))
|
||||
if err != nil {
|
||||
|
@ -230,7 +220,6 @@ var actions = map[string]Action{
|
|||
|
||||
config.Config.ExpandButton = (request.PostFormValue("ExpandButton") == "on")
|
||||
config.Config.ImagesOpenNewTab = (request.PostFormValue("ImagesOpenNewTab") == "on")
|
||||
config.Config.MakeURLsHyperlinked = (request.PostFormValue("MakeURLsHyperlinked") == "on")
|
||||
config.Config.NewTabOnOutlinks = (request.PostFormValue("NewTabOnOutlinks") == "on")
|
||||
config.Config.MinifyHTML = (request.PostFormValue("MinifyHTML") == "on")
|
||||
config.Config.MinifyJS = (request.PostFormValue("MinifyJS") == "on")
|
||||
|
@ -267,7 +256,6 @@ var actions = map[string]Action{
|
|||
config.Config.MaxRecentPosts = MaxRecentPosts
|
||||
}
|
||||
|
||||
config.Config.EnableAppeals = (request.PostFormValue("EnableAppeals") == "on")
|
||||
MaxLogDays, err := strconv.Atoi(request.PostFormValue("MaxLogDays"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
|
@ -288,7 +276,7 @@ var actions = map[string]Action{
|
|||
}
|
||||
manageConfigBuffer := bytes.NewBufferString("")
|
||||
if err = gctemplates.ManageConfig.Execute(manageConfigBuffer,
|
||||
map[string]interface{}{"config": config.Config, "status": status}); err != nil {
|
||||
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
|
||||
|
|
|
@ -243,7 +243,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
}
|
||||
|
||||
if filetype == "webm" {
|
||||
if !allowsVids || !config.Config.AllowVideoUploads {
|
||||
if !allowsVids {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LAccessLog,
|
||||
"Video uploading is not currently enabled for this board."))
|
||||
os.Remove(filePath)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue