diff --git a/config/config.go b/config/config.go index 2d86c9e..e27ffb9 100644 --- a/config/config.go +++ b/config/config.go @@ -1,184 +1,185 @@ /* * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ // Package config holds and assists in the configuration of a writefreely instance. package config import ( "gopkg.in/ini.v1" "strings" ) const ( // FileName is the default configuration file name FileName = "config.ini" UserNormal UserType = "user" UserAdmin = "admin" ) type ( UserType string // ServerCfg holds values that affect how the HTTP server runs ServerCfg struct { HiddenHost string `ini:"hidden_host"` Port int `ini:"port"` Bind string `ini:"bind"` TLSCertPath string `ini:"tls_cert_path"` TLSKeyPath string `ini:"tls_key_path"` Autocert bool `ini:"autocert"` TemplatesParentDir string `ini:"templates_parent_dir"` StaticParentDir string `ini:"static_parent_dir"` PagesParentDir string `ini:"pages_parent_dir"` KeysParentDir string `ini:"keys_parent_dir"` Dev bool `ini:"-"` } // DatabaseCfg holds values that determine how the application connects to a datastore DatabaseCfg struct { Type string `ini:"type"` FileName string `ini:"filename"` User string `ini:"username"` Password string `ini:"password"` Database string `ini:"database"` Host string `ini:"host"` Port int `ini:"port"` } // AppCfg holds values that affect how the application functions AppCfg struct { SiteName string `ini:"site_name"` SiteDesc string `ini:"site_description"` Host string `ini:"host"` // Site appearance Theme string `ini:"theme"` + Editor string `ini:"editor"` JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` Landing string `ini:"landing"` WFModesty bool `ini:"wf_modesty"` // Users SingleUser bool `ini:"single_user"` OpenRegistration bool `ini:"open_registration"` MinUsernameLen int `ini:"min_username_len"` MaxBlogs int `ini:"max_blogs"` // Federation Federation bool `ini:"federation"` PublicStats bool `ini:"public_stats"` // Access Private bool `ini:"private"` // Additional functions LocalTimeline bool `ini:"local_timeline"` UserInvites string `ini:"user_invites"` // Defaults DefaultVisibility string `ini:"default_visibility"` } // Config holds the complete configuration for running a writefreely instance Config struct { Server ServerCfg `ini:"server"` Database DatabaseCfg `ini:"database"` App AppCfg `ini:"app"` } ) // New creates a new Config with sane defaults func New() *Config { c := &Config{ Server: ServerCfg{ Port: 8080, Bind: "localhost", /* IPV6 support when not using localhost? */ }, App: AppCfg{ Host: "http://localhost:8080", Theme: "write", WebFonts: true, SingleUser: true, MinUsernameLen: 3, MaxBlogs: 1, Federation: true, PublicStats: true, }, } c.UseMySQL(true) return c } // UseMySQL resets the Config's Database to use default values for a MySQL setup. func (cfg *Config) UseMySQL(fresh bool) { cfg.Database.Type = "mysql" if fresh { cfg.Database.Host = "localhost" cfg.Database.Port = 3306 } } // UseSQLite resets the Config's Database to use default values for a SQLite setup. func (cfg *Config) UseSQLite(fresh bool) { cfg.Database.Type = "sqlite3" if fresh { cfg.Database.FileName = "writefreely.db" } } // IsSecureStandalone returns whether or not the application is running as a // standalone server with TLS enabled. func (cfg *Config) IsSecureStandalone() bool { return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != "" } func (ac *AppCfg) LandingPath() string { if !strings.HasPrefix(ac.Landing, "/") { return "/" + ac.Landing } return ac.Landing } // Load reads the given configuration file, then parses and returns it as a Config. func Load(fname string) (*Config, error) { if fname == "" { fname = FileName } cfg, err := ini.Load(fname) if err != nil { return nil, err } // Parse INI file uc := &Config{} err = cfg.MapTo(uc) if err != nil { return nil, err } return uc, nil } // Save writes the given Config to the given file. func Save(uc *Config, fname string) error { cfg := ini.Empty() err := ini.ReflectFrom(cfg, uc) if err != nil { return err } if fname == "" { fname = FileName } return cfg.SaveTo(fname) } diff --git a/pad.go b/pad.go index 0057299..1545b4f 100644 --- a/pad.go +++ b/pad.go @@ -1,168 +1,175 @@ /* * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/page" "net/http" "strings" ) func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) action := vars["action"] slug := vars["slug"] collAlias := vars["collection"] if app.cfg.App.SingleUser { // TODO: refactor all of this, especially for single-user blogs c, err := app.db.GetCollectionByID(1) if err != nil { return err } collAlias = c.Alias } appData := &struct { page.StaticPage Post *RawPost User *User Blogs *[]Collection Editing bool // True if we're modifying an existing post EditCollection *Collection // Collection of the post we're editing, if any }{ StaticPage: pageForReq(app, r), Post: &RawPost{Font: "norm"}, User: getUserSession(app, r), } var err error if appData.User != nil { appData.Blogs, err = app.db.GetPublishableCollections(appData.User) if err != nil { log.Error("Unable to get user's blogs for Pad: %v", err) } } - padTmpl := "pad" + padTmpl := app.cfg.App.Editor + if padTmpl == "" { + padTmpl = "pad" + } if action == "" && slug == "" { // Not editing any post; simply render the Pad + if templates[padTmpl] == nil { + log.Info("No template '%s' found. Falling back to default 'pad' template.", padTmpl) + padTmpl = "pad" + } if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil { log.Error("Unable to execute template: %v", err) } return nil } // Retrieve post information for editing appData.Editing = true // Make sure this isn't cached, so user doesn't accidentally lose data w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Expires", "Thu, 04 Oct 1990 20:00:00 GMT") if slug != "" { // TODO: refactor all of this, especially for single-user blogs appData.Post = getRawCollectionPost(app, slug, collAlias) if appData.Post.OwnerID != appData.User.ID { // TODO: add ErrForbiddenEditPost message to flashes return impart.HTTPError{http.StatusFound, r.URL.Path[:strings.LastIndex(r.URL.Path, "/edit")]} } appData.EditCollection, err = app.db.GetCollectionForPad(collAlias) if err != nil { return err } } else { // Editing a floating article appData.Post = getRawPost(app, action) appData.Post.Id = action } if appData.Post.Gone { return ErrPostUnpublished } else if appData.Post.Found && appData.Post.Content != "" { // Got the post } else if appData.Post.Found { return ErrPostFetchError } else { return ErrPostNotFound } if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil { log.Error("Unable to execute template: %v", err) } return nil } func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) action := vars["action"] slug := vars["slug"] collAlias := vars["collection"] appData := &struct { page.StaticPage Post *RawPost User *User EditCollection *Collection // Collection of the post we're editing, if any Flashes []string NeedsToken bool }{ StaticPage: pageForReq(app, r), Post: &RawPost{Font: "norm"}, User: getUserSession(app, r), } var err error if action == "" && slug == "" { return ErrPostNotFound } // Make sure this isn't cached, so user doesn't accidentally lose data w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Expires", "Thu, 28 Jul 1989 12:00:00 GMT") if slug != "" { appData.Post = getRawCollectionPost(app, slug, collAlias) if appData.Post.OwnerID != appData.User.ID { // TODO: add ErrForbiddenEditPost message to flashes return impart.HTTPError{http.StatusFound, r.URL.Path[:strings.LastIndex(r.URL.Path, "/meta")]} } if app.cfg.App.SingleUser { // TODO: optimize this query just like we do in GetCollectionForPad (?) appData.EditCollection, err = app.db.GetCollectionByID(1) } else { appData.EditCollection, err = app.db.GetCollectionForPad(collAlias) } if err != nil { return err } } else { // Editing a floating article appData.Post = getRawPost(app, action) appData.Post.Id = action } appData.NeedsToken = appData.User == nil || appData.User.ID != appData.Post.OwnerID if appData.Post.Gone { return ErrPostUnpublished } else if appData.Post.Found && appData.Post.Content != "" { // Got the post } else if appData.Post.Found { return ErrPostFetchError } else { return ErrPostNotFound } appData.Flashes, _ = getSessionFlashes(app, w, r, nil) if err = templates["edit-meta"].ExecuteTemplate(w, "edit-meta", appData); err != nil { log.Error("Unable to execute template: %v", err) } return nil }