1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-03 19:56:22 -07:00
gochan/pkg/config/util.go

342 lines
8.6 KiB
Go
Raw Normal View History

package config
import (
"encoding/json"
"flag"
"fmt"
"os"
"path"
"reflect"
"time"
"github.com/gochan-org/gochan/pkg/gcutil"
)
2021-07-16 19:12:25 -07:00
var (
criticalFields = []string{
"ListenIP", "Port", "Username", "UseFastCGI", "DocumentRoot", "TemplateDir", "LogDir",
"DBtype", "DBhost", "DBname", "DBusername", "DBpassword", "SiteDomain", "Styles",
}
)
// MissingField represents a field missing from the configuration file
type MissingField struct {
Name string
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
}
2021-07-11 18:12:02 -07:00
func GetDefaultBool(key string) bool {
2021-07-16 19:12:25 -07:00
boolInterface := defaults[key]
2021-07-11 18:12:02 -07:00
if boolInterface == nil {
return false
}
b, ok := boolInterface.(bool)
return b && ok
}
func GetDefaultInt(key string) int {
2021-07-16 19:12:25 -07:00
intInterface := defaults[key]
2021-07-11 18:12:02 -07:00
if intInterface == nil {
return 0
}
i, ok := intInterface.(int)
if !ok {
return 0
}
return i
}
func GetDefaultString(key string) string {
2021-07-16 19:12:25 -07:00
i := defaults[key]
2021-07-11 18:12:02 -07:00
if i == nil {
return ""
}
str, ok := i.(string)
if !ok {
return ""
}
return str
}
// ParseJSON loads and parses JSON data, returning a GochanConfig pointer, any critical missing
// fields that don't have defaults, and any error from parsing the file. This doesn't mean that the
// values are valid, just that they exist
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
}
2021-07-16 19:12:25 -07:00
if defaults[fType.Name] != nil {
// the field isn't in the JSON file but has a default value that we can use
2021-07-16 19:12:25 -07:00
fVal.Set(reflect.ValueOf(defaults[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) {
if flag.Lookup("test.v") != nil {
// create a dummy config for testing if we're using go test
cfg = &GochanConfig{
testing: true,
SystemCriticalConfig: SystemCriticalConfig{
ListenIP: "127.0.0.1",
Port: 8080,
UseFastCGI: true,
DebugMode: true,
DocumentRoot: "html",
TemplateDir: "templates",
LogDir: "",
DBtype: "sqlite3",
DBhost: "./testdata/gochantest.db",
DBname: "gochan",
DBusername: "gochan",
DBpassword: "",
DBprefix: "gc_",
SiteDomain: "127.0.0.1",
WebRoot: "/",
RandomSeed: "abcd",
Version: ParseVersion(versionStr),
},
SiteConfig: SiteConfig{
Username: "gochan",
FirstPage: []string{"index.html", "firstrun.html", "1.html"},
Lockdown: false,
LockdownMessage: "This imageboard has temporarily disabled posting. We apologize for the inconvenience",
SiteName: "Gochan",
SiteSlogan: "Gochan testing",
MinifyHTML: true,
MinifyJS: true,
EnableAppeals: true,
MaxLogDays: 14,
Verbosity: 1,
MaxRecentPosts: 3,
RecentPostsWithNoFile: false,
},
BoardConfig: BoardConfig{
Sillytags: []string{"Admin", "Mod", "Janitor", "Dweeb", "Kick me", "Troll", "worst pony"},
UseSillytags: false,
Styles: []Style{
{Name: "Pipes", Filename: "pipes.css"},
{Name: "BunkerChan", Filename: "bunkerchan.css"},
{Name: "Burichan", Filename: "burichan.css"},
{Name: "Clear", Filename: "clear.css"},
{Name: "Dark", Filename: "dark.css"},
{Name: "Photon", Filename: "photon.css"},
{Name: "Yotsuba", Filename: "yotsuba.css"},
{Name: "Yotsuba B", Filename: "yotsubab.css"},
{Name: "Windows 9x", Filename: "win9x.css"},
},
DefaultStyle: "pipes.css",
PostConfig: PostConfig{
NewThreadDelay: 30,
ReplyDelay: 7,
ThreadsPerPage: 15,
PostsPerThreadPage: 50,
RepliesOnBoardPage: 3,
StickyRepliesOnBoardPage: 1,
BanColors: []string{
"admin:#0000A0",
"somemod:blue",
},
BanMessage: "USER WAS BANNED FOR THIS POST",
EnableEmbeds: true,
EmbedWidth: 200,
EmbedHeight: 164,
ImagesOpenNewTab: true,
NewTabOnOutlinks: true,
},
UploadConfig: UploadConfig{
ThumbWidth: 200,
ThumbHeight: 200,
ThumbWidthReply: 125,
ThumbHeightReply: 125,
ThumbWidthCatalog: 50,
ThumbHeightCatalog: 50,
},
DateTimeFormat: "Mon, January 02, 2006 3:04 PM",
},
}
return
}
cfgPath = gcutil.FindResource(
"gochan.json",
"/usr/local/etc/gochan/gochan.json",
"/etc/gochan/gochan.json")
if cfgPath == "" {
fmt.Println("gochan.json not found")
os.Exit(1)
}
2022-09-08 15:45:29 -07:00
cfgBytes, err := os.ReadFile(cfgPath)
if err != nil {
fmt.Printf("Error reading %s: %s\n", cfgPath, err.Error())
os.Exit(1)
}
var fields []MissingField
2022-09-08 15:45:29 -07:00
cfg, fields, err = ParseJSON(cfgBytes)
if err != nil {
fmt.Printf("Error parsing %s: %s", cfgPath, err.Error())
}
cfg.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 = cfg.ValidateValues(); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if _, err = os.Stat(cfg.DocumentRoot); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if _, err = os.Stat(cfg.TemplateDir); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if _, err = os.Stat(cfg.LogDir); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
cfg.LogDir = gcutil.FindResource(cfg.LogDir, "log", "/var/log/gochan/")
if err = gcutil.InitLog(path.Join(cfg.LogDir, "gochan.log"), cfg.DebugMode); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if err = gcutil.InitAccessLog(path.Join(cfg.LogDir, "gochan_access.log")); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if cfg.Port == 0 {
cfg.Port = 80
}
if len(cfg.FirstPage) == 0 {
cfg.FirstPage = []string{"index.html", "1.html", "firstrun.html"}
}
if cfg.WebRoot == "" {
cfg.WebRoot = "/"
}
if cfg.WebRoot[0] != '/' {
cfg.WebRoot = "/" + cfg.WebRoot
}
if cfg.WebRoot[len(cfg.WebRoot)-1] != '/' {
cfg.WebRoot += "/"
}
if cfg.EnableGeoIP {
if _, err = os.Stat(cfg.GeoIPDBlocation); err != nil {
gcutil.LogError(err).
Str("location", cfg.GeoIPDBlocation).
Msg("Unable to load GeoIP file location set in gochan.json, disabling GeoIP")
}
cfg.EnableGeoIP = false
}
_, zoneOffset := time.Now().Zone()
cfg.TimeZone = zoneOffset / 60 / 60
cfg.Version = ParseVersion(versionStr)
cfg.Version.Normalize()
}
2021-07-16 19:12:25 -07:00
// TODO: use reflect to check if the field exists in SystemCriticalConfig
func fieldIsCritical(field string) bool {
for _, cF := range criticalFields {
if field == cF {
return true
}
}
return false
}
// WebPath returns an absolute path, starting at the web root (which is "/" by default)
func WebPath(part ...string) string {
return path.Join(cfg.WebRoot, path.Join(part...))
}
2021-07-16 19:12:25 -07:00
// UpdateFromMap updates the configuration with the given key->values for use in things like the
// config editor page and possibly others
func UpdateFromMap(m map[string]interface{}, validate bool) error {
for key, val := range m {
if fieldIsCritical(key) {
// don't mess with critical/read-only fields (ListenIP, DocumentRoot, etc)
// after the server has started
continue
}
cfg.setField(key, val)
}
if validate {
return cfg.ValidateValues()
}
return nil
}