From 9023d4eb3f0311fd40f3f146544b3464b6b0c76f Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 10 May 2019 11:40:35 -0400 Subject: [PATCH 01/23] Move flag parsing to main package This exposes setup and admin functions in the writefreely package, and uses them in the main application, initialized by the flag parsing that now happens there. This is the first step towards making `writefreely` usable as a standalone package. --- app.go | 345 +++++++++++++++++++--------------------- cmd/writefreely/main.go | 84 +++++++++- 2 files changed, 246 insertions(+), 183 deletions(-) diff --git a/app.go b/app.go index 540b496..154943a 100644 --- a/app.go +++ b/app.go @@ -12,7 +12,6 @@ package writefreely import ( "database/sql" - "flag" "fmt" "html/template" "net/http" @@ -190,184 +189,8 @@ func pageForReq(app *app, r *http.Request) page.StaticPage { var shttp = http.NewServeMux() var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") -func Serve() { - // General options usable with other commands - debugPtr := flag.Bool("debug", false, "Enables debug logging.") - configFile := flag.String("c", "config.ini", "The configuration file to use") - - // Setup actions - createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits") - doConfig := flag.Bool("config", false, "Run the configuration process") - genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys") - createSchema := flag.Bool("init-db", false, "Initialize app database") - migrate := flag.Bool("migrate", false, "Migrate the database") - - // Admin actions - createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password") - createUser := flag.String("create-user", "", "Create a regular user with the given username:password") - resetPassUser := flag.String("reset-pass", "", "Reset the given user's password") - outputVersion := flag.Bool("v", false, "Output the current version") - flag.Parse() - - debugging = *debugPtr - - app := &app{ - cfgFile: *configFile, - } - - if *outputVersion { - fmt.Println(serverSoftware + " " + softwareVer) - os.Exit(0) - } else if *createConfig { - log.Info("Creating configuration...") - c := config.New() - log.Info("Saving configuration %s...", app.cfgFile) - err := config.Save(c, app.cfgFile) - if err != nil { - log.Error("Unable to save configuration: %v", err) - os.Exit(1) - } - os.Exit(0) - } else if *doConfig { - d, err := config.Configure(app.cfgFile) - if err != nil { - log.Error("Unable to configure: %v", err) - os.Exit(1) - } - if d.User != nil { - app.cfg = d.Config - connectToDatabase(app) - defer shutdown(app) - - if !app.db.DatabaseInitialized() { - err = adminInitDatabase(app) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } - } - - u := &User{ - Username: d.User.Username, - HashedPass: d.User.HashedPass, - Created: time.Now().Truncate(time.Second).UTC(), - } - - // Create blog - log.Info("Creating user %s...\n", u.Username) - err = app.db.CreateUser(u, app.cfg.App.SiteName) - if err != nil { - log.Error("Unable to create user: %s", err) - os.Exit(1) - } - log.Info("Done!") - } - os.Exit(0) - } else if *genKeys { - errStatus := 0 - - // Read keys path from config - loadConfig(app) - - // Create keys dir if it doesn't exist yet - fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir) - if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) { - err = os.Mkdir(fullKeysDir, 0700) - if err != nil { - log.Error("%s", err) - os.Exit(1) - } - } - - // Generate keys - initKeyPaths(app) - err := generateKey(emailKeyPath) - if err != nil { - errStatus = 1 - } - err = generateKey(cookieAuthKeyPath) - if err != nil { - errStatus = 1 - } - err = generateKey(cookieKeyPath) - if err != nil { - errStatus = 1 - } - - os.Exit(errStatus) - } else if *createSchema { - loadConfig(app) - connectToDatabase(app) - defer shutdown(app) - err := adminInitDatabase(app) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } - os.Exit(0) - } else if *createAdmin != "" { - err := adminCreateUser(app, *createAdmin, true) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } - os.Exit(0) - } else if *createUser != "" { - err := adminCreateUser(app, *createUser, false) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } - os.Exit(0) - } else if *resetPassUser != "" { - // Connect to the database - loadConfig(app) - connectToDatabase(app) - defer shutdown(app) - - // Fetch user - u, err := app.db.GetUserForAuth(*resetPassUser) - if err != nil { - log.Error("Get user: %s", err) - os.Exit(1) - } - - // Prompt for new password - prompt := promptui.Prompt{ - Templates: &promptui.PromptTemplates{ - Success: "{{ . | bold | faint }}: ", - }, - Label: "New password", - Mask: '*', - } - newPass, err := prompt.Run() - if err != nil { - log.Error("%s", err) - os.Exit(1) - } - - // Do the update - log.Info("Updating...") - err = adminResetPassword(app, u, newPass) - if err != nil { - log.Error("%s", err) - os.Exit(1) - } - log.Info("Success.") - os.Exit(0) - } else if *migrate { - loadConfig(app) - connectToDatabase(app) - defer shutdown(app) - - err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName)) - if err != nil { - log.Error("migrate: %s", err) - os.Exit(1) - } - - os.Exit(0) - } +func Serve(app *app, debug bool) { + debugging = debug log.Info("Initializing...") @@ -375,7 +198,7 @@ func Serve() { hostName = app.cfg.App.Host isSingleUser = app.cfg.App.SingleUser - app.cfg.Server.Dev = *debugPtr + app.cfg.Server.Dev = debugging err := initTemplates(app.cfg) if err != nil { @@ -488,6 +311,165 @@ func Serve() { } } +// OutputVersion prints out the version of the application. +func OutputVersion() { + fmt.Println(serverSoftware + " " + softwareVer) +} + +// NewApp creates a new app instance. +func NewApp(cfgFile string) *app { + return &app{ + cfgFile: cfgFile, + } +} + +// CreateConfig creates a default configuration and saves it to the app's cfgFile. +func CreateConfig(app *app) error { + log.Info("Creating configuration...") + c := config.New() + log.Info("Saving configuration %s...", app.cfgFile) + err := config.Save(c, app.cfgFile) + if err != nil { + return fmt.Errorf("Unable to save configuration: %v", err) + } + return nil +} + +// DoConfig runs the interactive configuration process. +func DoConfig(app *app) { + d, err := config.Configure(app.cfgFile) + if err != nil { + log.Error("Unable to configure: %v", err) + os.Exit(1) + } + if d.User != nil { + app.cfg = d.Config + connectToDatabase(app) + defer shutdown(app) + + if !app.db.DatabaseInitialized() { + err = adminInitDatabase(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + } + + u := &User{ + Username: d.User.Username, + HashedPass: d.User.HashedPass, + Created: time.Now().Truncate(time.Second).UTC(), + } + + // Create blog + log.Info("Creating user %s...\n", u.Username) + err = app.db.CreateUser(u, app.cfg.App.SiteName) + if err != nil { + log.Error("Unable to create user: %s", err) + os.Exit(1) + } + log.Info("Done!") + } + os.Exit(0) +} + +// GenerateKeys creates app encryption keys and saves them into the configured KeysParentDir. +func GenerateKeys(app *app) error { + // Read keys path from config + loadConfig(app) + + // Create keys dir if it doesn't exist yet + fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir) + if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) { + err = os.Mkdir(fullKeysDir, 0700) + if err != nil { + return err + } + } + + // Generate keys + initKeyPaths(app) + var keyErrs error + err := generateKey(emailKeyPath) + if err != nil { + keyErrs = err + } + err = generateKey(cookieAuthKeyPath) + if err != nil { + keyErrs = err + } + err = generateKey(cookieKeyPath) + if err != nil { + keyErrs = err + } + + return keyErrs +} + +// CreateSchema creates all database tables needed for the application. +func CreateSchema(app *app) error { + loadConfig(app) + connectToDatabase(app) + defer shutdown(app) + err := adminInitDatabase(app) + if err != nil { + return err + } + return nil +} + +// Migrate runs all necessary database migrations. +func Migrate(app *app) error { + loadConfig(app) + connectToDatabase(app) + defer shutdown(app) + + err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName)) + if err != nil { + return fmt.Errorf("migrate: %s", err) + } + return nil +} + +// ResetPassword runs the interactive password reset process. +func ResetPassword(app *app, username string) error { + // Connect to the database + loadConfig(app) + connectToDatabase(app) + defer shutdown(app) + + // Fetch user + u, err := app.db.GetUserForAuth(username) + if err != nil { + log.Error("Get user: %s", err) + os.Exit(1) + } + + // Prompt for new password + prompt := promptui.Prompt{ + Templates: &promptui.PromptTemplates{ + Success: "{{ . | bold | faint }}: ", + }, + Label: "New password", + Mask: '*', + } + newPass, err := prompt.Run() + if err != nil { + log.Error("%s", err) + os.Exit(1) + } + + // Do the update + log.Info("Updating...") + err = adminResetPassword(app, u, newPass) + if err != nil { + log.Error("%s", err) + os.Exit(1) + } + log.Info("Success.") + return nil +} + func loadConfig(app *app) { log.Info("Loading %s configuration...", app.cfgFile) cfg, err := config.Load(app.cfgFile) @@ -533,7 +515,8 @@ func shutdown(app *app) { app.db.Close() } -func adminCreateUser(app *app, credStr string, isAdmin bool) error { +// CreateUser creates a new admin or normal user from the given username:password string. +func CreateUser(app *app, credStr string, isAdmin bool) error { // Create an admin user with --create-admin creds := strings.Split(credStr, ":") if len(creds) != 2 { diff --git a/cmd/writefreely/main.go b/cmd/writefreely/main.go index e0a293c..73866cb 100644 --- a/cmd/writefreely/main.go +++ b/cmd/writefreely/main.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -11,9 +11,89 @@ package main import ( + "flag" + "github.com/writeas/web-core/log" "github.com/writeas/writefreely" + "os" ) func main() { - writefreely.Serve() + // General options usable with other commands + debugPtr := flag.Bool("debug", false, "Enables debug logging.") + configFile := flag.String("c", "config.ini", "The configuration file to use") + + // Setup actions + createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits") + doConfig := flag.Bool("config", false, "Run the configuration process") + genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys") + createSchema := flag.Bool("init-db", false, "Initialize app database") + migrate := flag.Bool("migrate", false, "Migrate the database") + + // Admin actions + createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password") + createUser := flag.String("create-user", "", "Create a regular user with the given username:password") + resetPassUser := flag.String("reset-pass", "", "Reset the given user's password") + outputVersion := flag.Bool("v", false, "Output the current version") + flag.Parse() + + app := writefreely.NewApp(*configFile) + + if *outputVersion { + writefreely.OutputVersion() + os.Exit(0) + } else if *createConfig { + err := writefreely.CreateConfig(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *doConfig { + writefreely.DoConfig(app) + os.Exit(0) + } else if *genKeys { + err := writefreely.GenerateKeys(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *createSchema { + err := writefreely.CreateSchema(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *createAdmin != "" { + err := writefreely.CreateUser(app, *createAdmin, true) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *createUser != "" { + err := writefreely.CreateUser(app, *createUser, false) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *resetPassUser != "" { + err := writefreely.ResetPassword(app, *resetPassUser) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *migrate { + err := writefreely.Migrate(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } + + writefreely.Serve(app, *debugPtr) } From d8937e89a8647fd9a98f51dd0228cc3dfc12a812 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 12 May 2019 16:55:30 -0400 Subject: [PATCH 02/23] Make App struct public --- account.go | 52 ++++++++++++++++++++++---------------------- activitypub.go | 20 ++++++++--------- admin.go | 18 +++++++-------- app.go | 39 +++++++++++++++++---------------- collections.go | 24 ++++++++++---------- database.go | 4 ++-- export.go | 4 ++-- feed.go | 4 ++-- handle.go | 18 +++++++-------- hostmeta.go | 4 ++-- invites.go | 6 ++--- keys.go | 6 ++--- pad.go | 6 ++--- pages.go | 6 ++--- posts.go | 26 +++++++++++----------- read.go | 14 ++++++------ session.go | 16 +++++++------- sitemap.go | 4 ++-- unregisteredusers.go | 8 +++---- 19 files changed, 140 insertions(+), 139 deletions(-) diff --git a/account.go b/account.go index 06151c9..f05e94c 100644 --- a/account.go +++ b/account.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -49,7 +49,7 @@ type ( } ) -func NewUserPage(app *app, r *http.Request, u *User, title string, flashes []string) *UserPage { +func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []string) *UserPage { up := &UserPage{ StaticPage: pageForReq(app, r), PageTitle: title, @@ -73,12 +73,12 @@ const ( var actuallyUsernameReg = regexp.MustCompile("username is actually ([a-z0-9\\-]+)\\. Please try that, instead") -func apiSignup(app *app, w http.ResponseWriter, r *http.Request) error { +func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error { _, err := signup(app, w, r) return err } -func signup(app *app, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { +func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { reqJSON := IsJSON(r.Header.Get("Content-Type")) // Get params @@ -113,7 +113,7 @@ func signup(app *app, w http.ResponseWriter, r *http.Request) (*AuthUser, error) return signupWithRegistration(app, ur, w, r) } -func signupWithRegistration(app *app, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { +func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { reqJSON := IsJSON(r.Header.Get("Content-Type")) // Validate required params (alias) @@ -229,7 +229,7 @@ func signupWithRegistration(app *app, signup userRegistration, w http.ResponseWr return resUser, nil } -func viewLogout(app *app, w http.ResponseWriter, r *http.Request) error { +func viewLogout(app *App, w http.ResponseWriter, r *http.Request) error { session, err := app.sessionStore.Get(r, cookieName) if err != nil { return ErrInternalCookieSession @@ -268,7 +268,7 @@ func viewLogout(app *app, w http.ResponseWriter, r *http.Request) error { return impart.HTTPError{http.StatusFound, "/"} } -func handleAPILogout(app *app, w http.ResponseWriter, r *http.Request) error { +func handleAPILogout(app *App, w http.ResponseWriter, r *http.Request) error { accessToken := r.Header.Get("Authorization") if accessToken == "" { return ErrNoAccessToken @@ -284,7 +284,7 @@ func handleAPILogout(app *app, w http.ResponseWriter, r *http.Request) error { return impart.HTTPError{Status: http.StatusNoContent} } -func viewLogin(app *app, w http.ResponseWriter, r *http.Request) error { +func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { var earlyError string oneTimeToken := r.FormValue("with") if oneTimeToken != "" { @@ -333,7 +333,7 @@ func viewLogin(app *app, w http.ResponseWriter, r *http.Request) error { return nil } -func webLogin(app *app, w http.ResponseWriter, r *http.Request) error { +func webLogin(app *App, w http.ResponseWriter, r *http.Request) error { err := login(app, w, r) if err != nil { username := r.FormValue("alias") @@ -370,7 +370,7 @@ func webLogin(app *app, w http.ResponseWriter, r *http.Request) error { var loginAttemptUsers = sync.Map{} -func login(app *app, w http.ResponseWriter, r *http.Request) error { +func login(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) oneTimeToken := r.FormValue("with") verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "") @@ -534,7 +534,7 @@ func login(app *app, w http.ResponseWriter, r *http.Request) error { return nil } -func getVerboseAuthUser(app *app, token string, u *User, verbose bool) *AuthUser { +func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser { resUser := &AuthUser{ AccessToken: token, User: u, @@ -563,7 +563,7 @@ func getVerboseAuthUser(app *app, token string, u *User, verbose bool) *AuthUser return resUser } -func viewExportOptions(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request) error { // Fetch extra user data p := NewUserPage(app, r, u, "Export", nil) @@ -571,7 +571,7 @@ func viewExportOptions(app *app, u *User, w http.ResponseWriter, r *http.Request return nil } -func viewExportPosts(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error) { +func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) { var filename string var u = &User{} reqJSON := IsJSON(r.Header.Get("Content-Type")) @@ -635,7 +635,7 @@ func viewExportPosts(app *app, w http.ResponseWriter, r *http.Request) ([]byte, return data, filename, err } -func viewExportFull(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error) { +func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) { var err error filename := "" u := getUserSession(app, r) @@ -655,7 +655,7 @@ func viewExportFull(app *app, w http.ResponseWriter, r *http.Request) ([]byte, s return data, filename, err } -func viewMeAPI(app *app, w http.ResponseWriter, r *http.Request) error { +func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) uObj := struct { ID int64 `json:"id,omitempty"` @@ -679,7 +679,7 @@ func viewMeAPI(app *app, w http.ResponseWriter, r *http.Request) error { return impart.WriteSuccess(w, uObj, http.StatusOK) } -func viewMyPostsAPI(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) if !reqJSON { return ErrBadRequestedType @@ -710,7 +710,7 @@ func viewMyPostsAPI(app *app, u *User, w http.ResponseWriter, r *http.Request) e return impart.WriteSuccess(w, p, http.StatusOK) } -func viewMyCollectionsAPI(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) if !reqJSON { return ErrBadRequestedType @@ -724,7 +724,7 @@ func viewMyCollectionsAPI(app *app, u *User, w http.ResponseWriter, r *http.Requ return impart.WriteSuccess(w, p, http.StatusOK) } -func viewArticles(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error { p, err := app.db.GetAnonymousPosts(u) if err != nil { log.Error("unable to fetch anon posts: %v", err) @@ -761,7 +761,7 @@ func viewArticles(app *app, u *User, w http.ResponseWriter, r *http.Request) err return nil } -func viewCollections(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error { c, err := app.db.GetCollections(u) if err != nil { log.Error("unable to fetch collections: %v", err) @@ -792,7 +792,7 @@ func viewCollections(app *app, u *User, w http.ResponseWriter, r *http.Request) return nil } -func viewEditCollection(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) c, err := app.db.GetCollection(vars["collection"]) if err != nil { @@ -815,7 +815,7 @@ func viewEditCollection(app *app, u *User, w http.ResponseWriter, r *http.Reques return nil } -func updateSettings(app *app, w http.ResponseWriter, r *http.Request) error { +func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) var s userSettings @@ -904,7 +904,7 @@ func updateSettings(app *app, w http.ResponseWriter, r *http.Request) error { return nil } -func updatePassphrase(app *app, w http.ResponseWriter, r *http.Request) error { +func updatePassphrase(app *App, w http.ResponseWriter, r *http.Request) error { accessToken := r.Header.Get("Authorization") if accessToken == "" { return ErrNoAccessToken @@ -943,7 +943,7 @@ func updatePassphrase(app *app, w http.ResponseWriter, r *http.Request) error { return impart.WriteSuccess(w, struct{}{}, http.StatusOK) } -func viewStats(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error { var c *Collection var err error vars := mux.Vars(r) @@ -994,7 +994,7 @@ func viewStats(app *app, u *User, w http.ResponseWriter, r *http.Request) error return nil } -func viewSettings(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error { fullUser, err := app.db.GetUserByID(u.ID) if err != nil { log.Error("Unable to get user for settings: %s", err) @@ -1025,7 +1025,7 @@ func viewSettings(app *app, u *User, w http.ResponseWriter, r *http.Request) err return nil } -func saveTempInfo(app *app, key, val string, r *http.Request, w http.ResponseWriter) error { +func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error { session, err := app.sessionStore.Get(r, "t") if err != nil { return ErrInternalCookieSession @@ -1039,7 +1039,7 @@ func saveTempInfo(app *app, key, val string, r *http.Request, w http.ResponseWri return err } -func getTempInfo(app *app, key string, r *http.Request, w http.ResponseWriter) string { +func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) string { session, err := app.sessionStore.Get(r, "t") if err != nil { return "" diff --git a/activitypub.go b/activitypub.go index 3b0ac82..70343ca 100644 --- a/activitypub.go +++ b/activitypub.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -62,7 +62,7 @@ func (ru *RemoteUser) AsPerson() *activitystreams.Person { } } -func handleFetchCollectionActivities(app *app, w http.ResponseWriter, r *http.Request) error { +func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) @@ -86,7 +86,7 @@ func handleFetchCollectionActivities(app *app, w http.ResponseWriter, r *http.Re return impart.RenderActivityJSON(w, p, http.StatusOK) } -func handleFetchCollectionOutbox(app *app, w http.ResponseWriter, r *http.Request) error { +func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) @@ -138,7 +138,7 @@ func handleFetchCollectionOutbox(app *app, w http.ResponseWriter, r *http.Reques return impart.RenderActivityJSON(w, ocp, http.StatusOK) } -func handleFetchCollectionFollowers(app *app, w http.ResponseWriter, r *http.Request) error { +func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) @@ -183,7 +183,7 @@ func handleFetchCollectionFollowers(app *app, w http.ResponseWriter, r *http.Req return impart.RenderActivityJSON(w, ocp, http.StatusOK) } -func handleFetchCollectionFollowing(app *app, w http.ResponseWriter, r *http.Request) error { +func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) @@ -218,7 +218,7 @@ func handleFetchCollectionFollowing(app *app, w http.ResponseWriter, r *http.Req return impart.RenderActivityJSON(w, ocp, http.StatusOK) } -func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request) error { +func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) @@ -531,7 +531,7 @@ func resolveIRI(url string) ([]byte, error) { return body, nil } -func deleteFederatedPost(app *app, p *PublicPost, collID int64) error { +func deleteFederatedPost(app *App, p *PublicPost, collID int64) error { if debugging { log.Info("Deleting federated post!") } @@ -569,7 +569,7 @@ func deleteFederatedPost(app *app, p *PublicPost, collID int64) error { return nil } -func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error { +func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { if debugging { if isUpdate { log.Info("Federating updated post!") @@ -619,7 +619,7 @@ func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error { return nil } -func getRemoteUser(app *app, actorID string) (*RemoteUser, error) { +func getRemoteUser(app *App, actorID string) (*RemoteUser, error) { u := RemoteUser{ActorID: actorID} err := app.db.QueryRow("SELECT id, inbox, shared_inbox FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox) switch { @@ -633,7 +633,7 @@ func getRemoteUser(app *app, actorID string) (*RemoteUser, error) { return &u, nil } -func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser, error) { +func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) { log.Info("Fetching actor %s locally", actorIRI) actor := &activitystreams.Person{} remoteUser, err := getRemoteUser(app, actorIRI) diff --git a/admin.go b/admin.go index 29f3501..b2eab1f 100644 --- a/admin.go +++ b/admin.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -96,7 +96,7 @@ func (c instanceContent) UpdatedFriendly() string { return c.Updated.Format("January 2, 2006, 3:04 PM") } -func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error { updateAppStats() p := struct { *UserPage @@ -117,7 +117,7 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque return nil } -func handleViewAdminUsers(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Request) error { p := struct { *UserPage Config config.AppCfg @@ -157,7 +157,7 @@ func handleViewAdminUsers(app *app, u *User, w http.ResponseWriter, r *http.Requ return nil } -func handleViewAdminUser(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) username := vars["username"] if username == "" { @@ -229,7 +229,7 @@ func handleViewAdminUser(app *app, u *User, w http.ResponseWriter, r *http.Reque return nil } -func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error { p := struct { *UserPage Config config.AppCfg @@ -287,7 +287,7 @@ func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Requ return nil } -func handleViewAdminPage(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) slug := vars["slug"] if slug == "" { @@ -329,7 +329,7 @@ func handleViewAdminPage(app *app, u *User, w http.ResponseWriter, r *http.Reque return nil } -func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) id := vars["page"] @@ -347,7 +347,7 @@ func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Req return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m} } -func handleAdminUpdateConfig(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func handleAdminUpdateConfig(app *App, u *User, w http.ResponseWriter, r *http.Request) error { app.cfg.App.SiteName = r.FormValue("site_name") app.cfg.App.SiteDesc = r.FormValue("site_desc") app.cfg.App.OpenRegistration = r.FormValue("open_registration") == "on" @@ -418,7 +418,7 @@ func updateAppStats() { sysStatus.NumGC = m.NumGC } -func adminResetPassword(app *app, u *User, newPass string) error { +func adminResetPassword(app *App, u *User, newPass string) error { hashedPass, err := auth.HashPass([]byte(newPass)) if err != nil { return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)} diff --git a/app.go b/app.go index 154943a..8f8d3c5 100644 --- a/app.go +++ b/app.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -62,7 +62,8 @@ var ( isSingleUser bool ) -type app struct { +// App holds data and configuration for an individual WriteFreely instance. +type App struct { router *mux.Router db *datastore cfg *config.Config @@ -76,7 +77,7 @@ type app struct { // handleViewHome shows page at root path. Will be the Pad if logged in and the // catch-all landing page otherwise. -func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { +func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { if app.cfg.App.SingleUser { // Render blog index return handleViewCollection(app, w, r) @@ -111,7 +112,7 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { return renderPage(w, "landing.tmpl", p) } -func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error { +func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *template.Template) error { p := struct { page.StaticPage ContentTitle string @@ -157,7 +158,7 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te return nil } -func pageForReq(app *app, r *http.Request) page.StaticPage { +func pageForReq(app *App, r *http.Request) page.StaticPage { p := page.StaticPage{ AppCfg: app.cfg.App, Path: r.URL.Path, @@ -189,7 +190,7 @@ func pageForReq(app *app, r *http.Request) page.StaticPage { var shttp = http.NewServeMux() var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") -func Serve(app *app, debug bool) { +func Serve(app *App, debug bool) { debugging = debug log.Info("Initializing...") @@ -317,14 +318,14 @@ func OutputVersion() { } // NewApp creates a new app instance. -func NewApp(cfgFile string) *app { - return &app{ +func NewApp(cfgFile string) *App { + return &App{ cfgFile: cfgFile, } } // CreateConfig creates a default configuration and saves it to the app's cfgFile. -func CreateConfig(app *app) error { +func CreateConfig(app *App) error { log.Info("Creating configuration...") c := config.New() log.Info("Saving configuration %s...", app.cfgFile) @@ -336,7 +337,7 @@ func CreateConfig(app *app) error { } // DoConfig runs the interactive configuration process. -func DoConfig(app *app) { +func DoConfig(app *App) { d, err := config.Configure(app.cfgFile) if err != nil { log.Error("Unable to configure: %v", err) @@ -374,7 +375,7 @@ func DoConfig(app *app) { } // GenerateKeys creates app encryption keys and saves them into the configured KeysParentDir. -func GenerateKeys(app *app) error { +func GenerateKeys(app *App) error { // Read keys path from config loadConfig(app) @@ -407,7 +408,7 @@ func GenerateKeys(app *app) error { } // CreateSchema creates all database tables needed for the application. -func CreateSchema(app *app) error { +func CreateSchema(app *App) error { loadConfig(app) connectToDatabase(app) defer shutdown(app) @@ -419,7 +420,7 @@ func CreateSchema(app *app) error { } // Migrate runs all necessary database migrations. -func Migrate(app *app) error { +func Migrate(app *App) error { loadConfig(app) connectToDatabase(app) defer shutdown(app) @@ -432,7 +433,7 @@ func Migrate(app *app) error { } // ResetPassword runs the interactive password reset process. -func ResetPassword(app *app, username string) error { +func ResetPassword(app *App, username string) error { // Connect to the database loadConfig(app) connectToDatabase(app) @@ -470,7 +471,7 @@ func ResetPassword(app *app, username string) error { return nil } -func loadConfig(app *app) { +func loadConfig(app *App) { log.Info("Loading %s configuration...", app.cfgFile) cfg, err := config.Load(app.cfgFile) if err != nil { @@ -480,7 +481,7 @@ func loadConfig(app *app) { app.cfg = cfg } -func connectToDatabase(app *app) { +func connectToDatabase(app *App) { log.Info("Connecting to %s database...", app.cfg.Database.Type) var db *sql.DB @@ -510,13 +511,13 @@ func connectToDatabase(app *app) { app.db = &datastore{db, app.cfg.Database.Type} } -func shutdown(app *app) { +func shutdown(app *App) { log.Info("Closing database connection...") app.db.Close() } // CreateUser creates a new admin or normal user from the given username:password string. -func CreateUser(app *app, credStr string, isAdmin bool) error { +func CreateUser(app *App, credStr string, isAdmin bool) error { // Create an admin user with --create-admin creds := strings.Split(credStr, ":") if len(creds) != 2 { @@ -587,7 +588,7 @@ func CreateUser(app *app, credStr string, isAdmin bool) error { return nil } -func adminInitDatabase(app *app) error { +func adminInitDatabase(app *App) error { schemaFileName := "schema.sql" if app.cfg.Database.Type == driverSQLite { schemaFileName = "sqlite.sql" diff --git a/collections.go b/collections.go index cd7be58..677e4bc 100644 --- a/collections.go +++ b/collections.go @@ -316,7 +316,7 @@ func (c *Collection) RenderMathJax() bool { return c.db.CollectionHasAttribute(c.ID, "render_mathjax") } -func newCollection(app *app, w http.ResponseWriter, r *http.Request) error { +func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) alias := r.FormValue("alias") title := r.FormValue("title") @@ -399,7 +399,7 @@ func newCollection(app *app, w http.ResponseWriter, r *http.Request) error { return impart.HTTPError{http.StatusFound, redirectTo} } -func apiCheckCollectionPermissions(app *app, r *http.Request, c *Collection) (int64, error) { +func apiCheckCollectionPermissions(app *App, r *http.Request, c *Collection) (int64, error) { accessToken := r.Header.Get("Authorization") var userID int64 = -1 if accessToken != "" { @@ -419,7 +419,7 @@ func apiCheckCollectionPermissions(app *app, r *http.Request, c *Collection) (in } // fetchCollection handles the API endpoint for retrieving collection data. -func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error { +func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error { accept := r.Header.Get("Accept") if strings.Contains(accept, "application/activity+json") { return handleFetchCollectionActivities(app, w, r) @@ -467,7 +467,7 @@ func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error { // fetchCollectionPosts handles an API endpoint for retrieving a collection's // posts. -func fetchCollectionPosts(app *app, w http.ResponseWriter, r *http.Request) error { +func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) alias := vars["alias"] @@ -563,7 +563,7 @@ func processCollectionRequest(cr *collectionReq, vars map[string]string, w http. // domain that doesn't yet have a collection associated, or if a collection // requires a password. In either case, this will return nil, nil -- thus both // values should ALWAYS be checked to determine whether or not to continue. -func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.ResponseWriter, r *http.Request) (*Collection, error) { +func processCollectionPermissions(app *App, cr *collectionReq, u *User, w http.ResponseWriter, r *http.Request) (*Collection, error) { // Display collection if this is a collection var c *Collection var err error @@ -654,7 +654,7 @@ func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.R return c, nil } -func checkUserForCollection(app *app, cr *collectionReq, r *http.Request, isPostReq bool) (*User, error) { +func checkUserForCollection(app *App, cr *collectionReq, r *http.Request, isPostReq bool) (*User, error) { u := getUserSession(app, r) return u, nil } @@ -682,7 +682,7 @@ func getCollectionPage(vars map[string]string) int { } // handleViewCollection displays the requested Collection -func handleViewCollection(app *app, w http.ResponseWriter, r *http.Request) error { +func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) cr := &collectionReq{} @@ -788,7 +788,7 @@ func handleViewCollection(app *app, w http.ResponseWriter, r *http.Request) erro return err } -func handleViewCollectionTag(app *app, w http.ResponseWriter, r *http.Request) error { +func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) tag := vars["tag"] @@ -867,7 +867,7 @@ func handleViewCollectionTag(app *app, w http.ResponseWriter, r *http.Request) e return nil } -func handleCollectionPostRedirect(app *app, w http.ResponseWriter, r *http.Request) error { +func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) slug := vars["slug"] @@ -885,7 +885,7 @@ func handleCollectionPostRedirect(app *app, w http.ResponseWriter, r *http.Reque return impart.HTTPError{http.StatusFound, loc} } -func existingCollection(app *app, w http.ResponseWriter, r *http.Request) error { +func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) vars := mux.Vars(r) collAlias := vars["alias"] @@ -980,7 +980,7 @@ func collectionAliasFromReq(r *http.Request) string { return alias } -func handleWebCollectionUnlock(app *app, w http.ResponseWriter, r *http.Request) error { +func handleWebCollectionUnlock(app *App, w http.ResponseWriter, r *http.Request) error { var readReq struct { Alias string `schema:"alias" json:"alias"` Pass string `schema:"password" json:"password"` @@ -1047,7 +1047,7 @@ func handleWebCollectionUnlock(app *app, w http.ResponseWriter, r *http.Request) return impart.HTTPError{http.StatusFound, next} } -func isAuthorizedForCollection(app *app, alias string, r *http.Request) bool { +func isAuthorizedForCollection(app *App, alias string, r *http.Request) bool { authd := false session, err := app.sessionStore.Get(r, blogPassCookieName) if err == nil { diff --git a/database.go b/database.go index 4e64dbb..1c1d303 100644 --- a/database.go +++ b/database.go @@ -60,7 +60,7 @@ type writestore interface { GetTemporaryAccessToken(userID int64, validSecs int) (string, error) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error) DeleteAccount(userID int64) (l *string, err error) - ChangeSettings(app *app, u *User, s *userSettings) error + ChangeSettings(app *App, u *User, s *userSettings) error ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error GetCollections(u *User) (*[]Collection, error) @@ -1774,7 +1774,7 @@ func (db *datastore) GetUserPostsCount(userID int64) int64 { // ChangeSettings takes a User and applies the changes in the given // userSettings, MODIFYING THE USER with successful changes. -func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error { +func (db *datastore) ChangeSettings(app *App, u *User, s *userSettings) error { var errPass error q := query.NewUpdate() diff --git a/export.go b/export.go index e23b850..3029c9b 100644 --- a/export.go +++ b/export.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -92,7 +92,7 @@ func exportPostsZip(u *User, posts *[]PublicPost) []byte { return b.Bytes() } -func compileFullExport(app *app, u *User) *ExportUser { +func compileFullExport(app *App, u *User) *ExportUser { exportUser := &ExportUser{ User: u, } diff --git a/feed.go b/feed.go index 89043d3..68432dc 100644 --- a/feed.go +++ b/feed.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -20,7 +20,7 @@ import ( "time" ) -func ViewFeed(app *app, w http.ResponseWriter, req *http.Request) error { +func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { alias := collectionAliasFromReq(req) // Display collection if this is a collection diff --git a/handle.go b/handle.go index 706a2fa..946487f 100644 --- a/handle.go +++ b/handle.go @@ -36,16 +36,16 @@ const ( ) type ( - handlerFunc func(app *app, w http.ResponseWriter, r *http.Request) error - userHandlerFunc func(app *app, u *User, w http.ResponseWriter, r *http.Request) error - dataHandlerFunc func(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error) - authFunc func(app *app, r *http.Request) (*User, error) + handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error + userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error + dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) + authFunc func(app *App, r *http.Request) (*User, error) ) type Handler struct { errors *ErrorPages sessionStore *sessions.CookieStore - app *app + app *App } // ErrorPages hold template HTML error pages for displaying errors to the user. @@ -59,7 +59,7 @@ type ErrorPages struct { // NewHandler returns a new Handler instance, using the given StaticPage data, // and saving alias to the application's CookieStore. -func NewHandler(app *app) *Handler { +func NewHandler(app *App) *Handler { h := &Handler{ errors: &ErrorPages{ NotFound: template.Must(template.New("").Parse("{{define \"base\"}}404

Not found.

{{end}}")), @@ -160,7 +160,7 @@ func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc { // UserAPI handles requests made in the API by the authenticated user. // This provides user-friendly HTML pages and actions that work in the browser. func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc { - return h.UserAll(false, f, func(app *app, r *http.Request) (*User, error) { + return h.UserAll(false, f, func(app *App, r *http.Request) (*User, error) { // Authorize user from Authorization header t := r.Header.Get("Authorization") if t == "" { @@ -222,7 +222,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF } func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc { - return func(app *app, w http.ResponseWriter, r *http.Request) error { + return func(app *App, w http.ResponseWriter, r *http.Request) error { err := f(app, w, r) if err != nil { if ie, ok := err.(impart.HTTPError); ok { @@ -239,7 +239,7 @@ func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc { } func (h *Handler) Page(n string) http.HandlerFunc { - return h.Web(func(app *app, w http.ResponseWriter, r *http.Request) error { + return h.Web(func(app *App, w http.ResponseWriter, r *http.Request) error { t, ok := pages[n] if !ok { return impart.HTTPError{http.StatusNotFound, "Page not found."} diff --git a/hostmeta.go b/hostmeta.go index 70a8856..4f452c3 100644 --- a/hostmeta.go +++ b/hostmeta.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -15,7 +15,7 @@ import ( "net/http" ) -func handleViewHostMeta(app *app, w http.ResponseWriter, r *http.Request) error { +func handleViewHostMeta(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) w.Header().Set("Content-Type", "application/xrd+xml; charset=utf-8") diff --git a/invites.go b/invites.go index 54d6619..561255f 100644 --- a/invites.go +++ b/invites.go @@ -45,7 +45,7 @@ func (i Invite) ExpiresFriendly() string { return i.Expires.Format("January 2, 2006, 3:04 PM") } -func handleViewUserInvites(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func handleViewUserInvites(app *App, u *User, w http.ResponseWriter, r *http.Request) error { // Don't show page if instance doesn't allow it if !(app.cfg.App.UserInvites != "" && (u.IsAdmin() || app.cfg.App.UserInvites != "admin")) { return impart.HTTPError{http.StatusNotFound, ""} @@ -73,7 +73,7 @@ func handleViewUserInvites(app *app, u *User, w http.ResponseWriter, r *http.Req return nil } -func handleCreateUserInvite(app *app, u *User, w http.ResponseWriter, r *http.Request) error { +func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Request) error { muVal := r.FormValue("uses") expVal := r.FormValue("expires") @@ -106,7 +106,7 @@ func handleCreateUserInvite(app *app, u *User, w http.ResponseWriter, r *http.Re return impart.HTTPError{http.StatusFound, "/me/invites"} } -func handleViewInvite(app *app, w http.ResponseWriter, r *http.Request) error { +func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error { inviteCode := mux.Vars(r)["code"] i, err := app.db.GetUserInvite(inviteCode) diff --git a/keys.go b/keys.go index 3b9c360..84f7d6a 100644 --- a/keys.go +++ b/keys.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -34,13 +34,13 @@ type keychain struct { emailKey, cookieAuthKey, cookieKey []byte } -func initKeyPaths(app *app) { +func initKeyPaths(app *App) { emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath) cookieAuthKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieAuthKeyPath) cookieKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieKeyPath) } -func initKeys(app *app) error { +func initKeys(app *App) error { var err error app.keys = &keychain{} diff --git a/pad.go b/pad.go index 1a602dd..0057299 100644 --- a/pad.go +++ b/pad.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -19,7 +19,7 @@ import ( "strings" ) -func handleViewPad(app *app, w http.ResponseWriter, r *http.Request) error { +func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) action := vars["action"] slug := vars["slug"] @@ -102,7 +102,7 @@ func handleViewPad(app *app, w http.ResponseWriter, r *http.Request) error { return nil } -func handleViewMeta(app *app, w http.ResponseWriter, r *http.Request) error { +func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) action := vars["action"] slug := vars["slug"] diff --git a/pages.go b/pages.go index ddbd132..29ba07a 100644 --- a/pages.go +++ b/pages.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -18,7 +18,7 @@ import ( var defaultPageUpdatedTime = time.Date(2018, 11, 8, 12, 0, 0, 0, time.Local) -func getAboutPage(app *app) (*instanceContent, error) { +func getAboutPage(app *App) (*instanceContent, error) { c, err := app.db.GetDynamicContent("about") if err != nil { return nil, err @@ -40,7 +40,7 @@ func defaultAboutTitle(cfg *config.Config) sql.NullString { return sql.NullString{String: "About " + cfg.App.SiteName, Valid: true} } -func getPrivacyPage(app *app) (*instanceContent, error) { +func getPrivacyPage(app *App) (*instanceContent, error) { c, err := app.db.GetDynamicContent("privacy") if err != nil { return nil, err diff --git a/posts.go b/posts.go index b21588c..a7b7476 100644 --- a/posts.go +++ b/posts.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -260,7 +260,7 @@ func (p *Post) HasTitleLink() bool { return hasLink } -func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error { +func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) friendlyID := vars["post"] @@ -466,7 +466,7 @@ func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error { // /posts // /posts?collection={alias} // ? /collections/{alias}/posts -func newPost(app *app, w http.ResponseWriter, r *http.Request) error { +func newPost(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) vars := mux.Vars(r) collAlias := vars["alias"] @@ -591,7 +591,7 @@ func newPost(app *app, w http.ResponseWriter, r *http.Request) error { return response } -func existingPost(app *app, w http.ResponseWriter, r *http.Request) error { +func existingPost(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) vars := mux.Vars(r) postID := vars["post"] @@ -711,7 +711,7 @@ func existingPost(app *app, w http.ResponseWriter, r *http.Request) error { return nil } -func deletePost(app *app, w http.ResponseWriter, r *http.Request) error { +func deletePost(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) friendlyID := vars["post"] editToken := r.FormValue("token") @@ -830,7 +830,7 @@ func deletePost(app *app, w http.ResponseWriter, r *http.Request) error { } // addPost associates a post with the authenticated user. -func addPost(app *app, w http.ResponseWriter, r *http.Request) error { +func addPost(app *App, w http.ResponseWriter, r *http.Request) error { var ownerID int64 // Authenticate user @@ -879,7 +879,7 @@ func addPost(app *app, w http.ResponseWriter, r *http.Request) error { return impart.WriteSuccess(w, res, http.StatusOK) } -func dispersePost(app *app, w http.ResponseWriter, r *http.Request) error { +func dispersePost(app *App, w http.ResponseWriter, r *http.Request) error { var ownerID int64 // Authenticate user @@ -923,7 +923,7 @@ type ( ) // pinPost pins a post to a blog -func pinPost(app *app, w http.ResponseWriter, r *http.Request) error { +func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { var userID int64 // Authenticate user @@ -981,7 +981,7 @@ func pinPost(app *app, w http.ResponseWriter, r *http.Request) error { return impart.WriteSuccess(w, res, http.StatusOK) } -func fetchPost(app *app, w http.ResponseWriter, r *http.Request) error { +func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { var collID int64 var coll *Collection var err error @@ -1030,7 +1030,7 @@ func fetchPost(app *app, w http.ResponseWriter, r *http.Request) error { return impart.WriteSuccess(w, p, http.StatusOK) } -func fetchPostProperty(app *app, w http.ResponseWriter, r *http.Request) error { +func fetchPostProperty(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) p, err := app.db.GetPostProperty(vars["post"], 0, vars["property"]) if err != nil { @@ -1128,7 +1128,7 @@ func (p *SubmittedPost) isFontValid() bool { return valid } -func getRawPost(app *app, friendlyID string) *RawPost { +func getRawPost(app *App, friendlyID string) *RawPost { var content, font, title string var isRTL sql.NullBool var lang sql.NullString @@ -1148,7 +1148,7 @@ func getRawPost(app *app, friendlyID string) *RawPost { } // TODO; return a Post! -func getRawCollectionPost(app *app, slug, collAlias string) *RawPost { +func getRawCollectionPost(app *App, slug, collAlias string) *RawPost { var id, title, content, font string var isRTL sql.NullBool var lang sql.NullString @@ -1185,7 +1185,7 @@ func getRawCollectionPost(app *app, slug, collAlias string) *RawPost { } } -func viewCollectionPost(app *app, w http.ResponseWriter, r *http.Request) error { +func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) slug := vars["slug"] diff --git a/read.go b/read.go index 5098935..13add9e 100644 --- a/read.go +++ b/read.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -49,7 +49,7 @@ type readPublication struct { TotalPages int } -func initLocalTimeline(app *app) { +func initLocalTimeline(app *App) { app.timeline = &localTimeline{ postsPerPage: tlPostsPerPage, m: memo.New(app.db.FetchPublicPosts, 10*time.Minute), @@ -108,7 +108,7 @@ func (db *datastore) FetchPublicPosts() (interface{}, error) { return posts, nil } -func viewLocalTimelineAPI(app *app, w http.ResponseWriter, r *http.Request) error { +func viewLocalTimelineAPI(app *App, w http.ResponseWriter, r *http.Request) error { updateTimelineCache(app.timeline) skip, _ := strconv.Atoi(r.FormValue("skip")) @@ -121,7 +121,7 @@ func viewLocalTimelineAPI(app *app, w http.ResponseWriter, r *http.Request) erro return impart.WriteSuccess(w, posts, http.StatusOK) } -func viewLocalTimeline(app *app, w http.ResponseWriter, r *http.Request) error { +func viewLocalTimeline(app *App, w http.ResponseWriter, r *http.Request) error { if !app.cfg.App.LocalTimeline { return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."} } @@ -153,7 +153,7 @@ func updateTimelineCache(tl *localTimeline) { } } -func showLocalTimeline(app *app, w http.ResponseWriter, r *http.Request, page int, author, tag string) error { +func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page int, author, tag string) error { updateTimelineCache(app.timeline) pl := len(*(app.timeline.posts)) @@ -226,7 +226,7 @@ func (c *readPublication) PrevPageURL(n int) string { // handlePostIDRedirect handles a route where a post ID is given and redirects // the user to the canonical post URL. -func handlePostIDRedirect(app *app, w http.ResponseWriter, r *http.Request) error { +func handlePostIDRedirect(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) postID := vars["post"] p, err := app.db.GetPost(postID, 0) @@ -249,7 +249,7 @@ func handlePostIDRedirect(app *app, w http.ResponseWriter, r *http.Request) erro return impart.HTTPError{http.StatusFound, c.CanonicalURL() + p.Slug.String} } -func viewLocalTimelineFeed(app *app, w http.ResponseWriter, req *http.Request) error { +func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) error { if !app.cfg.App.LocalTimeline { return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."} } diff --git a/session.go b/session.go index 6140b05..a3a7aaa 100644 --- a/session.go +++ b/session.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -29,7 +29,7 @@ const ( // initSession creates the cookie store. It depends on the keychain already // being loaded. -func initSession(app *app) *sessions.CookieStore { +func initSession(app *App) *sessions.CookieStore { // Register complex data types we'll be storing in cookies gob.Register(&User{}) @@ -44,7 +44,7 @@ func initSession(app *app) *sessions.CookieStore { return store } -func getSessionFlashes(app *app, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) { +func getSessionFlashes(app *App, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) { var err error if session == nil { session, err = app.sessionStore.Get(r, cookieName) @@ -66,7 +66,7 @@ func getSessionFlashes(app *app, w http.ResponseWriter, r *http.Request, session return f, nil } -func addSessionFlash(app *app, w http.ResponseWriter, r *http.Request, m string, session *sessions.Session) error { +func addSessionFlash(app *App, w http.ResponseWriter, r *http.Request, m string, session *sessions.Session) error { var err error if session == nil { session, err = app.sessionStore.Get(r, cookieName) @@ -82,7 +82,7 @@ func addSessionFlash(app *app, w http.ResponseWriter, r *http.Request, m string, return nil } -func getUserAndSession(app *app, r *http.Request) (*User, *sessions.Session) { +func getUserAndSession(app *App, r *http.Request) (*User, *sessions.Session) { session, err := app.sessionStore.Get(r, cookieName) if err == nil { // Got the currently logged-in user @@ -97,12 +97,12 @@ func getUserAndSession(app *app, r *http.Request) (*User, *sessions.Session) { return nil, nil } -func getUserSession(app *app, r *http.Request) *User { +func getUserSession(app *App, r *http.Request) *User { u, _ := getUserAndSession(app, r) return u } -func saveUserSession(app *app, r *http.Request, w http.ResponseWriter) error { +func saveUserSession(app *App, r *http.Request, w http.ResponseWriter) error { session, err := app.sessionStore.Get(r, cookieName) if err != nil { return ErrInternalCookieSession @@ -127,7 +127,7 @@ func saveUserSession(app *app, r *http.Request, w http.ResponseWriter) error { return err } -func getFullUserSession(app *app, r *http.Request) *User { +func getFullUserSession(app *App, r *http.Request) *User { u := getUserSession(app, r) if u == nil { return nil diff --git a/sitemap.go b/sitemap.go index 9bb168a..ae13dfd 100644 --- a/sitemap.go +++ b/sitemap.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -34,7 +34,7 @@ func buildSitemap(host, alias string) *stm.Sitemap { return sm } -func handleViewSitemap(app *app, w http.ResponseWriter, r *http.Request) error { +func handleViewSitemap(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) // Determine canonical blog URL diff --git a/unregisteredusers.go b/unregisteredusers.go index fa20426..91daba5 100644 --- a/unregisteredusers.go +++ b/unregisteredusers.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -18,7 +18,7 @@ import ( "net/http" ) -func handleWebSignup(app *app, w http.ResponseWriter, r *http.Request) error { +func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) // Get params @@ -67,7 +67,7 @@ func handleWebSignup(app *app, w http.ResponseWriter, r *http.Request) error { // { "username": "asdf" } // result: { code: 204 } -func handleUsernameCheck(app *app, w http.ResponseWriter, r *http.Request) error { +func handleUsernameCheck(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r.Header.Get("Content-Type")) // Get params @@ -112,7 +112,7 @@ func handleUsernameCheck(app *app, w http.ResponseWriter, r *http.Request) error return impart.HTTPError{http.StatusConflict, "Username is already taken."} } -func getValidUsername(app *app, reqName, prevName string) (string, *impart.HTTPError) { +func getValidUsername(app *App, reqName, prevName string) (string, *impart.HTTPError) { // Check if username is okay finalUsername := getSlug(reqName, "") if finalUsername == "" { From 9e43b04f04c431e6933739859d10e7487b91b595 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 12 May 2019 17:20:24 -0400 Subject: [PATCH 03/23] Make Keychain struct public --- app.go | 2 +- database.go | 4 ++-- keys.go | 4 ++-- users.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app.go b/app.go index 8f8d3c5..3cd416a 100644 --- a/app.go +++ b/app.go @@ -68,7 +68,7 @@ type App struct { db *datastore cfg *config.Config cfgFile string - keys *keychain + keys *Keychain sessionStore *sessions.CookieStore formDecoder *schema.Decoder diff --git a/database.go b/database.go index 1c1d303..8ec93f7 100644 --- a/database.go +++ b/database.go @@ -44,7 +44,7 @@ var ( type writestore interface { CreateUser(*User, string) error - UpdateUserEmail(keys *keychain, userID int64, email string) error + UpdateUserEmail(keys *Keychain, userID int64, email string) error UpdateEncryptedUserEmail(int64, []byte) error GetUserByID(int64) (*User, error) GetUserForAuth(string) (*User, error) @@ -219,7 +219,7 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error { // FIXME: We're returning errors inconsistently in this file. Do we use Errorf // for returned value, or impart? -func (db *datastore) UpdateUserEmail(keys *keychain, userID int64, email string) error { +func (db *datastore) UpdateUserEmail(keys *Keychain, userID int64, email string) error { encEmail, err := data.Encrypt(keys.emailKey, email) if err != nil { return fmt.Errorf("Couldn't encrypt email %s: %s\n", email, err) diff --git a/keys.go b/keys.go index 84f7d6a..219e9c7 100644 --- a/keys.go +++ b/keys.go @@ -30,7 +30,7 @@ var ( cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256") ) -type keychain struct { +type Keychain struct { emailKey, cookieAuthKey, cookieKey []byte } @@ -42,7 +42,7 @@ func initKeyPaths(app *App) { func initKeys(app *App) error { var err error - app.keys = &keychain{} + app.keys = &Keychain{} if debugging { log.Info(" %s", emailKeyPath) diff --git a/users.go b/users.go index 39ab385..6b245d5 100644 --- a/users.go +++ b/users.go @@ -79,7 +79,7 @@ type ( // EmailClear decrypts and returns the user's email, caching it in the user // object. -func (u *User) EmailClear(keys *keychain) string { +func (u *User) EmailClear(keys *Keychain) string { if u.clearEmail != "" { return u.clearEmail } From 77c81527868e6ca2770ab54b772a853f98bba103 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 12 May 2019 18:42:57 -0400 Subject: [PATCH 04/23] Break up CreateUser credentials into two args Now the func takes a username and password instead of a single string. --- app.go | 16 ++-------------- cmd/writefreely/main.go | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/app.go b/app.go index 3cd416a..fbdd63b 100644 --- a/app.go +++ b/app.go @@ -516,18 +516,9 @@ func shutdown(app *App) { app.db.Close() } -// CreateUser creates a new admin or normal user from the given username:password string. -func CreateUser(app *App, credStr string, isAdmin bool) error { +// CreateUser creates a new admin or normal user from the given credentials. +func CreateUser(app *App, username, password string, isAdmin bool) error { // Create an admin user with --create-admin - creds := strings.Split(credStr, ":") - if len(creds) != 2 { - c := "user" - if isAdmin { - c = "admin" - } - return fmt.Errorf("usage: writefreely --create-%s username:password", c) - } - loadConfig(app) connectToDatabase(app) defer shutdown(app) @@ -547,9 +538,6 @@ func CreateUser(app *App, credStr string, isAdmin bool) error { } // Create the user - username := creds[0] - password := creds[1] - // Normalize and validate username desiredUsername := username username = getSlug(username, "") diff --git a/cmd/writefreely/main.go b/cmd/writefreely/main.go index 73866cb..6a32ad9 100644 --- a/cmd/writefreely/main.go +++ b/cmd/writefreely/main.go @@ -12,9 +12,11 @@ package main import ( "flag" + "fmt" "github.com/writeas/web-core/log" "github.com/writeas/writefreely" "os" + "strings" ) func main() { @@ -66,14 +68,24 @@ func main() { } os.Exit(0) } else if *createAdmin != "" { - err := writefreely.CreateUser(app, *createAdmin, true) + username, password, err := userPass(*createAdmin, true) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + err = writefreely.CreateUser(app, username, password, true) if err != nil { log.Error(err.Error()) os.Exit(1) } os.Exit(0) } else if *createUser != "" { - err := writefreely.CreateUser(app, *createUser, false) + username, password, err := userPass(*createUser, false) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + err = writefreely.CreateUser(app, username, password, false) if err != nil { log.Error(err.Error()) os.Exit(1) @@ -97,3 +109,19 @@ func main() { writefreely.Serve(app, *debugPtr) } + +func userPass(credStr string, isAdmin bool) (user string, pass string, err error) { + creds := strings.Split(credStr, ":") + if len(creds) != 2 { + c := "user" + if isAdmin { + c = "admin" + } + err = fmt.Errorf("usage: writefreely --create-%s username:password", c) + return + } + + user = creds[0] + pass = creds[1] + return +} From c95ee0abe10baaf22fb798b6cf3a7f8c11ecb85b Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 12 May 2019 20:11:53 -0400 Subject: [PATCH 05/23] Support changing default landing path This adds a new `landing` value in the [app] section of config.ini. If non-empty, unauthenticated users on multi-user instances will be redirected to the path given there. This closes T574 --- app.go | 5 +++++ config/config.go | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/app.go b/app.go index 540b496..8d065fb 100644 --- a/app.go +++ b/app.go @@ -32,6 +32,7 @@ import ( "github.com/gorilla/sessions" "github.com/manifoldco/promptui" "github.com/writeas/go-strip-markdown" + "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/converter" "github.com/writeas/web-core/log" @@ -90,6 +91,10 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { return handleViewPad(app, w, r) } + if land := app.cfg.App.LandingPath(); land != "/" { + return impart.HTTPError{http.StatusFound, land} + } + p := struct { page.StaticPage Flashes []template.HTML diff --git a/config/config.go b/config/config.go index 2b07bed..add5447 100644 --- a/config/config.go +++ b/config/config.go @@ -13,6 +13,7 @@ package config import ( "gopkg.in/ini.v1" + "strings" ) const ( @@ -64,6 +65,7 @@ type ( Theme string `ini:"theme"` JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` + Landing string `ini:"landing"` // Users SingleUser bool `ini:"single_user"` @@ -134,6 +136,13 @@ 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 == "" { From 584fe4fb93ff34fa904a3aaffebf552fd66e654b Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 12 May 2019 20:11:53 -0400 Subject: [PATCH 06/23] Support changing default landing path This adds a new `landing` value in the [app] section of config.ini. If non-empty, unauthenticated users on multi-user instances will be redirected to the path given there. This closes T574 --- app.go | 5 +++++ config/config.go | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/app.go b/app.go index fbdd63b..67a2fec 100644 --- a/app.go +++ b/app.go @@ -31,6 +31,7 @@ import ( "github.com/gorilla/sessions" "github.com/manifoldco/promptui" "github.com/writeas/go-strip-markdown" + "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/converter" "github.com/writeas/web-core/log" @@ -90,6 +91,10 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { return handleViewPad(app, w, r) } + if land := app.cfg.App.LandingPath(); land != "/" { + return impart.HTTPError{http.StatusFound, land} + } + p := struct { page.StaticPage Flashes []template.HTML diff --git a/config/config.go b/config/config.go index 2b07bed..add5447 100644 --- a/config/config.go +++ b/config/config.go @@ -13,6 +13,7 @@ package config import ( "gopkg.in/ini.v1" + "strings" ) const ( @@ -64,6 +65,7 @@ type ( Theme string `ini:"theme"` JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` + Landing string `ini:"landing"` // Users SingleUser bool `ini:"single_user"` @@ -134,6 +136,13 @@ 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 == "" { From 758269e3d8fc3122cbb7460c827397bcc0fbfa9a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 13 Jun 2019 10:14:35 -0400 Subject: [PATCH 07/23] Move key generation and Keychain to key pkg Ref T613 --- account.go | 2 +- app.go | 7 +++++- database.go | 9 ++++---- key/key.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++ keys.go | 28 ++++++------------------ session.go | 2 +- users.go | 5 +++-- 7 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 key/key.go diff --git a/account.go b/account.go index f05e94c..d8ea0df 100644 --- a/account.go +++ b/account.go @@ -154,7 +154,7 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr Created: time.Now().Truncate(time.Second).UTC(), } if signup.Email != "" { - encEmail, err := data.Encrypt(app.keys.emailKey, signup.Email) + encEmail, err := data.Encrypt(app.keys.EmailKey, signup.Email) if err != nil { log.Error("Unable to encrypt email: %s\n", err) } else { diff --git a/app.go b/app.go index 67a2fec..74a8598 100644 --- a/app.go +++ b/app.go @@ -37,6 +37,7 @@ import ( "github.com/writeas/web-core/log" "github.com/writeas/writefreely/author" "github.com/writeas/writefreely/config" + "github.com/writeas/writefreely/key" "github.com/writeas/writefreely/migrations" "github.com/writeas/writefreely/page" ) @@ -69,13 +70,17 @@ type App struct { db *datastore cfg *config.Config cfgFile string - keys *Keychain + keys *key.Keychain sessionStore *sessions.CookieStore formDecoder *schema.Decoder timeline *localTimeline } +func (app *App) SetKeys(k *key.Keychain) { + app.keys = k +} + // handleViewHome shows page at root path. Will be the Pad if logged in and the // catch-all landing page otherwise. func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { diff --git a/database.go b/database.go index 8ec93f7..72d2780 100644 --- a/database.go +++ b/database.go @@ -29,6 +29,7 @@ import ( "github.com/writeas/web-core/log" "github.com/writeas/web-core/query" "github.com/writeas/writefreely/author" + "github.com/writeas/writefreely/key" ) const ( @@ -44,7 +45,7 @@ var ( type writestore interface { CreateUser(*User, string) error - UpdateUserEmail(keys *Keychain, userID int64, email string) error + UpdateUserEmail(keys *key.Keychain, userID int64, email string) error UpdateEncryptedUserEmail(int64, []byte) error GetUserByID(int64) (*User, error) GetUserForAuth(string) (*User, error) @@ -219,8 +220,8 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error { // FIXME: We're returning errors inconsistently in this file. Do we use Errorf // for returned value, or impart? -func (db *datastore) UpdateUserEmail(keys *Keychain, userID int64, email string) error { - encEmail, err := data.Encrypt(keys.emailKey, email) +func (db *datastore) UpdateUserEmail(keys *key.Keychain, userID int64, email string) error { + encEmail, err := data.Encrypt(keys.EmailKey, email) if err != nil { return fmt.Errorf("Couldn't encrypt email %s: %s\n", email, err) } @@ -1780,7 +1781,7 @@ func (db *datastore) ChangeSettings(app *App, u *User, s *userSettings) error { // Update email if given if s.Email != "" { - encEmail, err := data.Encrypt(app.keys.emailKey, s.Email) + encEmail, err := data.Encrypt(app.keys.EmailKey, s.Email) if err != nil { log.Error("Couldn't encrypt email %s: %s\n", s.Email, err) return impart.HTTPError{http.StatusInternalServerError, "Unable to encrypt email address."} diff --git a/key/key.go b/key/key.go new file mode 100644 index 0000000..14b0784 --- /dev/null +++ b/key/key.go @@ -0,0 +1,62 @@ +/* + * Copyright © 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 key holds application keys and utilities around generating them. +package key + +import ( + "crypto/rand" +) + +const ( + EncKeysBytes = 32 +) + +type Keychain struct { + EmailKey, CookieAuthKey, CookieKey []byte +} + +// GenerateKeys generates necessary keys for the app on the given Keychain, +// skipping any that already exist. +func (keys *Keychain) GenerateKeys() error { + // Generate keys only if they don't already exist + var err, keyErrs error + if len(keys.EmailKey) == 0 { + keys.EmailKey, err = GenerateBytes(EncKeysBytes) + if err != nil { + keyErrs = err + } + } + if len(keys.CookieAuthKey) == 0 { + keys.CookieAuthKey, err = GenerateBytes(EncKeysBytes) + if err != nil { + keyErrs = err + } + } + if len(keys.CookieKey) == 0 { + keys.CookieKey, err = GenerateBytes(EncKeysBytes) + if err != nil { + keyErrs = err + } + } + + return keyErrs +} + +// GenerateBytes returns securely generated random bytes. +func GenerateBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/keys.go b/keys.go index 219e9c7..067908e 100644 --- a/keys.go +++ b/keys.go @@ -11,8 +11,8 @@ package writefreely import ( - "crypto/rand" "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/key" "io/ioutil" "os" "path/filepath" @@ -20,8 +20,6 @@ import ( const ( keysDir = "keys" - - encKeysBytes = 32 ) var ( @@ -30,9 +28,6 @@ var ( cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256") ) -type Keychain struct { - emailKey, cookieAuthKey, cookieKey []byte -} func initKeyPaths(app *App) { emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath) @@ -42,12 +37,12 @@ func initKeyPaths(app *App) { func initKeys(app *App) error { var err error - app.keys = &Keychain{} + app.keys = &key.Keychain{} if debugging { log.Info(" %s", emailKeyPath) } - app.keys.emailKey, err = ioutil.ReadFile(emailKeyPath) + app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath) if err != nil { return err } @@ -55,7 +50,7 @@ func initKeys(app *App) error { if debugging { log.Info(" %s", cookieAuthKeyPath) } - app.keys.cookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath) + app.keys.CookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath) if err != nil { return err } @@ -63,7 +58,7 @@ func initKeys(app *App) error { if debugging { log.Info(" %s", cookieKeyPath) } - app.keys.cookieKey, err = ioutil.ReadFile(cookieKeyPath) + app.keys.CookieKey, err = ioutil.ReadFile(cookieKeyPath) if err != nil { return err } @@ -85,7 +80,7 @@ func generateKey(path string) error { } log.Info("Generating %s.", path) - b, err := generateBytes(encKeysBytes) + b, err := key.GenerateBytes(key.EncKeysBytes) if err != nil { log.Error("FAILED. %s. Run writefreely --gen-keys again.", err) return err @@ -98,14 +93,3 @@ func generateKey(path string) error { log.Info("Success.") return nil } - -// generateBytes returns securely generated random bytes. -func generateBytes(n int) ([]byte, error) { - b := make([]byte, n) - _, err := rand.Read(b) - if err != nil { - return nil, err - } - - return b, nil -} diff --git a/session.go b/session.go index a3a7aaa..95bfb18 100644 --- a/session.go +++ b/session.go @@ -34,7 +34,7 @@ func initSession(app *App) *sessions.CookieStore { gob.Register(&User{}) // Create the cookie store - store := sessions.NewCookieStore(app.keys.cookieAuthKey, app.keys.cookieKey) + store := sessions.NewCookieStore(app.keys.CookieAuthKey, app.keys.CookieKey) store.Options = &sessions.Options{ Path: "/", MaxAge: sessionLength, diff --git a/users.go b/users.go index 6b245d5..d5e9a91 100644 --- a/users.go +++ b/users.go @@ -16,6 +16,7 @@ import ( "github.com/guregu/null/zero" "github.com/writeas/web-core/data" "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/key" ) type ( @@ -79,13 +80,13 @@ type ( // EmailClear decrypts and returns the user's email, caching it in the user // object. -func (u *User) EmailClear(keys *Keychain) string { +func (u *User) EmailClear(keys *key.Keychain) string { if u.clearEmail != "" { return u.clearEmail } if u.Email.Valid && u.Email.String != "" { - email, err := data.Decrypt(keys.emailKey, []byte(u.Email.String)) + email, err := data.Decrypt(keys.EmailKey, []byte(u.Email.String)) if err != nil { log.Error("Error decrypting user email: %v", err) } else { From 23acabaeb338d9b623ff789a06f68387aa2f427a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 13 Jun 2019 13:05:05 -0400 Subject: [PATCH 08/23] Use db.isDuplicateKeyErr() in activitypub.go (instead of writing out the logic of that helper function) Ref T613 --- activitypub.go | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/activitypub.go b/activitypub.go index 70343ca..c48a7ca 100644 --- a/activitypub.go +++ b/activitypub.go @@ -17,7 +17,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/go-sql-driver/mysql" "github.com/gorilla/mux" "github.com/writeas/activity/streams" "github.com/writeas/httpsig" @@ -370,13 +369,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request // Add follower locally, since it wasn't found before res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox) if err != nil { - if mysqlErr, ok := err.(*mysql.MySQLError); ok { - if mysqlErr.Number != mySQLErrDuplicateKey { - t.Rollback() - log.Error("Couldn't add new remoteuser in DB: %v\n", err) - return - } - } else { + if !app.db.isDuplicateKeyErr(err) { t.Rollback() log.Error("Couldn't add new remoteuser in DB: %v\n", err) return @@ -393,13 +386,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request // Add in key _, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, followerID, fullActor.PublicKey.PublicKeyPEM) if err != nil { - if mysqlErr, ok := err.(*mysql.MySQLError); ok { - if mysqlErr.Number != mySQLErrDuplicateKey { - t.Rollback() - log.Error("Couldn't add follower keys in DB: %v\n", err) - return - } - } else { + if !app.db.isDuplicateKeyErr(err) { t.Rollback() log.Error("Couldn't add follower keys in DB: %v\n", err) return @@ -410,13 +397,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request // Add follow _, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, "+app.db.now()+")", c.ID, followerID) if err != nil { - if mysqlErr, ok := err.(*mysql.MySQLError); ok { - if mysqlErr.Number != mySQLErrDuplicateKey { - t.Rollback() - log.Error("Couldn't add follower in DB: %v\n", err) - return - } - } else { + if !app.db.isDuplicateKeyErr(err) { t.Rollback() log.Error("Couldn't add follower in DB: %v\n", err) return From eb6349f93ad467a96f61df067f45349d1f02ad08 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 13 Jun 2019 13:25:01 -0400 Subject: [PATCH 09/23] Allow compiling without go-sql-driver/mysql pkg This ensures the writefreely pkg can be used in other applications that need to load mysql themselves -- this can be done by building with the tag: wflib Ref T613 --- app.go | 2 -- database-lib.go | 20 ++++++++++++++++++++ database-no-sqlite.go | 2 +- database-sqlite.go | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 database-lib.go diff --git a/app.go b/app.go index 74a8598..cbff814 100644 --- a/app.go +++ b/app.go @@ -24,8 +24,6 @@ import ( "syscall" "time" - _ "github.com/go-sql-driver/mysql" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/gorilla/sessions" diff --git a/database-lib.go b/database-lib.go new file mode 100644 index 0000000..58beb05 --- /dev/null +++ b/database-lib.go @@ -0,0 +1,20 @@ +// +build wflib + +/* + * Copyright © 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. + */ + +// This file contains dummy database funcs for when writefreely is used as a +// library. + +package writefreely + +func (db *datastore) isDuplicateKeyErr(err error) bool { + return false +} diff --git a/database-no-sqlite.go b/database-no-sqlite.go index 10db7d5..a3d50fc 100644 --- a/database-no-sqlite.go +++ b/database-no-sqlite.go @@ -1,4 +1,4 @@ -// +build !sqlite +// +build !sqlite,!wflib /* * Copyright © 2019 A Bunch Tell LLC. diff --git a/database-sqlite.go b/database-sqlite.go index 5fa3f6c..3741169 100644 --- a/database-sqlite.go +++ b/database-sqlite.go @@ -1,4 +1,4 @@ -// +build sqlite +// +build sqlite,!wflib /* * Copyright © 2019 A Bunch Tell LLC. From ed4aacd1ac6a3ada63cee447e8d722637d5d1e42 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 13 Jun 2019 18:22:18 -0400 Subject: [PATCH 10/23] Move static file ServeMux to App struct --- app.go | 6 +----- posts.go | 4 ++-- routes.go | 13 ++++++++++++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index cbff814..abc34a3 100644 --- a/app.go +++ b/app.go @@ -65,6 +65,7 @@ var ( // App holds data and configuration for an individual WriteFreely instance. type App struct { router *mux.Router + shttp *http.ServeMux db *datastore cfg *config.Config cfgFile string @@ -195,7 +196,6 @@ func pageForReq(app *App, r *http.Request) page.StaticPage { return p } -var shttp = http.NewServeMux() var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") func Serve(app *App, debug bool) { @@ -272,10 +272,6 @@ func Serve(app *App, debug bool) { initLocalTimeline(app) } - // Handle static files - fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir))) - shttp.Handle("/", fs) - r.PathPrefix("/").Handler(fs) // Handle shutdown c := make(chan os.Signal, 2) diff --git a/posts.go b/posts.go index a7b7476..8156748 100644 --- a/posts.go +++ b/posts.go @@ -275,7 +275,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { return handleTemplatedPage(app, w, r, t) } else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" { // Serve static file - shttp.ServeHTTP(w, r) + app.shttp.ServeHTTP(w, r) return nil } @@ -1196,7 +1196,7 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error if strings.Contains(r.URL.Path, ".") && !isRaw { // Serve static file - shttp.ServeHTTP(w, r) + app.shttp.ServeHTTP(w, r) return nil } diff --git a/routes.go b/routes.go index bd0136f..f7d2451 100644 --- a/routes.go +++ b/routes.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -17,9 +17,20 @@ import ( "github.com/writeas/writefreely/config" "github.com/writefreely/go-nodeinfo" "net/http" + "path/filepath" "strings" ) +// InitStaticRoutes adds routes for serving static files. +// TODO: this should just be a func, not method +func (app *App) InitStaticRoutes(r *mux.Router) { + // Handle static files + fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir))) + app.shttp = http.NewServeMux() + app.shttp.Handle("/", fs) + r.PathPrefix("/").Handler(fs) +} + func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datastore) { hostSubroute := cfg.App.Host[strings.Index(cfg.App.Host, "://")+3:] if cfg.App.SingleUser { From 034db22f8c29c8cf94898e3f1b1dfab2338ff000 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 13 Jun 2019 18:50:23 -0400 Subject: [PATCH 11/23] Break functionality out of Serve() func - Adds a new interface, Apper, that enables loading and persisting instance-level data in new ways - Converts some initialization funcs to methods - Exports funcs and methods needed for intialization - In general, moves a ton of stuff around Overall, this should maintain all existing functionality, but with the ability to now better manage a WF instance. Ref T613 --- app.go | 294 ++++++++++++++++++++++++++-------------- cmd/writefreely/main.go | 19 ++- handle.go | 13 ++ keys.go | 40 ++---- routes.go | 27 ++-- session.go | 6 +- templates.go | 3 +- 7 files changed, 255 insertions(+), 147 deletions(-) diff --git a/app.go b/app.go index abc34a3..e970a77 100644 --- a/app.go +++ b/app.go @@ -14,6 +14,7 @@ import ( "database/sql" "fmt" "html/template" + "io/ioutil" "net/http" "net/url" "os" @@ -76,10 +77,109 @@ type App struct { timeline *localTimeline } +// DB returns the App's datastore +func (app *App) DB() *datastore { + return app.db +} + +// Router returns the App's router +func (app *App) Router() *mux.Router { + return app.router +} + +// Config returns the App's current configuration. +func (app *App) Config() *config.Config { + return app.cfg +} + +// SetConfig updates the App's Config to the given value. +func (app *App) SetConfig(cfg *config.Config) { + app.cfg = cfg +} + +// SetKeys updates the App's Keychain to the given value. func (app *App) SetKeys(k *key.Keychain) { app.keys = k } +// Apper is the interface for getting data into and out of a WriteFreely +// instance (or "App"). +// +// App returns the App for the current instance. +// +// LoadConfig reads an app configuration into the App, returning any error +// encountered. +// +// SaveConfig persists the current App configuration. +// +// LoadKeys reads the App's encryption keys and loads them into its +// key.Keychain. +type Apper interface { + App() *App + + LoadConfig() error + SaveConfig(*config.Config) error + + LoadKeys() error +} + +// App returns the App +func (app *App) App() *App { + return app +} + +// LoadConfig loads and parses a config file. +func (app *App) LoadConfig() error { + log.Info("Loading %s configuration...", app.cfgFile) + cfg, err := config.Load(app.cfgFile) + if err != nil { + log.Error("Unable to load configuration: %v", err) + os.Exit(1) + return err + } + app.cfg = cfg + return nil +} + +// SaveConfig saves the given Config to disk -- namely, to the App's cfgFile. +func (app *App) SaveConfig(c *config.Config) error { + return config.Save(c, app.cfgFile) +} + +// LoadKeys reads all needed keys from disk into the App. In order to use the +// configured `Server.KeysParentDir`, you must call initKeyPaths(App) before +// this. +func (app *App) LoadKeys() error { + var err error + app.keys = &key.Keychain{} + + if debugging { + log.Info(" %s", emailKeyPath) + } + app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath) + if err != nil { + return err + } + + if debugging { + log.Info(" %s", cookieAuthKeyPath) + } + app.keys.CookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath) + if err != nil { + return err + } + + if debugging { + log.Info(" %s", cookieKeyPath) + } + app.keys.CookieKey, err = ioutil.ReadFile(cookieKeyPath) + if err != nil { + return err + } + + return nil +} + // handleViewHome shows page at root path. Will be the Pad if logged in and the // catch-all landing page otherwise. func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { @@ -198,81 +298,50 @@ func pageForReq(app *App, r *http.Request) page.StaticPage { var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") -func Serve(app *App, debug bool) { +// Initialize loads the app configuration and initializes templates, keys, +// session, route handlers, and the database connection. +func Initialize(apper Apper, debug bool) (*App, error) { debugging = debug - log.Info("Initializing...") + apper.LoadConfig() - loadConfig(app) + // Load templates + err := InitTemplates(apper.App().Config()) + if err != nil { + return nil, fmt.Errorf("load templates: %s", err) + } + + // Load keys and set up session + initKeyPaths(apper.App()) // TODO: find a better way to do this, since it's unneeded in all Apper implementations + err = InitKeys(apper) + if err != nil { + return nil, fmt.Errorf("init keys: %s", err) + } + apper.App().InitSession() + + apper.App().InitDecoder() + + err = ConnectToDatabase(apper.App()) + if err != nil { + return nil, fmt.Errorf("connect to DB: %s", err) + } + + // Handle local timeline, if enabled + if apper.App().cfg.App.LocalTimeline { + log.Info("Initializing local timeline...") + initLocalTimeline(apper.App()) + } + + return apper.App(), nil +} + +func Serve(app *App, r *mux.Router) { + log.Info("Going to serve...") hostName = app.cfg.App.Host isSingleUser = app.cfg.App.SingleUser app.cfg.Server.Dev = debugging - err := initTemplates(app.cfg) - if err != nil { - log.Error("load templates: %s", err) - os.Exit(1) - } - - // Load keys - log.Info("Loading encryption keys...") - initKeyPaths(app) - err = initKeys(app) - if err != nil { - log.Error("\n%s\n", err) - } - - // Initialize modules - app.sessionStore = initSession(app) - app.formDecoder = schema.NewDecoder() - app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString) - app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool) - app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString) - app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool) - app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64) - app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64) - - // Check database configuration - if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") { - log.Error("Database user or password not set.") - os.Exit(1) - } - if app.cfg.Database.Host == "" { - app.cfg.Database.Host = "localhost" - } - if app.cfg.Database.Database == "" { - app.cfg.Database.Database = "writefreely" - } - - connectToDatabase(app) - defer shutdown(app) - - // Test database connection - err = app.db.Ping() - if err != nil { - log.Error("Database ping failed: %s", err) - } - - r := mux.NewRouter() - handler := NewHandler(app) - handler.SetErrorPages(&ErrorPages{ - NotFound: pages["404-general.tmpl"], - Gone: pages["410.tmpl"], - InternalServerError: pages["500.tmpl"], - Blank: pages["blank.tmpl"], - }) - - // Handle app routes - initRoutes(handler, r, app.cfg, app.db) - - // Handle local timeline, if enabled - if app.cfg.App.LocalTimeline { - log.Info("Initializing local timeline...") - initLocalTimeline(app) - } - - // Handle shutdown c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) @@ -284,13 +353,12 @@ func Serve(app *App, debug bool) { os.Exit(0) }() - http.Handle("/", r) - // Start web application server var bindAddress = app.cfg.Server.Bind if bindAddress == "" { bindAddress = "localhost" } + var err error if app.cfg.IsSecureStandalone() { log.Info("Serving redirects on http://%s:80", bindAddress) go func() { @@ -304,11 +372,11 @@ func Serve(app *App, debug bool) { log.Info("Serving on https://%s:443", bindAddress) log.Info("---") err = http.ListenAndServeTLS( - fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, nil) + fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, r) } else { log.Info("Serving on http://%s:%d\n", bindAddress, app.cfg.Server.Port) log.Info("---") - err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), nil) + err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), r) } if err != nil { log.Error("Unable to start: %v", err) @@ -316,6 +384,44 @@ func Serve(app *App, debug bool) { } } +func (app *App) InitDecoder() { + // TODO: do this at the package level, instead of the App level + // Initialize modules + app.formDecoder = schema.NewDecoder() + app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString) + app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool) + app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString) + app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool) + app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64) + app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64) +} + +// ConnectToDatabase validates and connects to the configured database, then +// tests the connection. +func ConnectToDatabase(app *App) error { + // Check database configuration + if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") { + return fmt.Errorf("Database user or password not set.") + } + if app.cfg.Database.Host == "" { + app.cfg.Database.Host = "localhost" + } + if app.cfg.Database.Database == "" { + app.cfg.Database.Database = "writefreely" + } + + // TODO: check err + connectToDatabase(app) + + // Test database connection + err := app.db.Ping() + if err != nil { + return fmt.Errorf("Database ping failed: %s", err) + } + + return nil +} + // OutputVersion prints out the version of the application. func OutputVersion() { fmt.Println(serverSoftware + " " + softwareVer) @@ -378,10 +484,10 @@ func DoConfig(app *App) { os.Exit(0) } -// GenerateKeys creates app encryption keys and saves them into the configured KeysParentDir. -func GenerateKeys(app *App) error { +// GenerateKeyFiles creates app encryption keys and saves them into the configured KeysParentDir. +func GenerateKeyFiles(app *App) error { // Read keys path from config - loadConfig(app) + app.LoadConfig() // Create keys dir if it doesn't exist yet fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir) @@ -412,11 +518,11 @@ func GenerateKeys(app *App) error { } // CreateSchema creates all database tables needed for the application. -func CreateSchema(app *App) error { - loadConfig(app) - connectToDatabase(app) - defer shutdown(app) - err := adminInitDatabase(app) +func CreateSchema(apper Apper) error { + apper.LoadConfig() + connectToDatabase(apper.App()) + defer shutdown(apper.App()) + err := adminInitDatabase(apper.App()) if err != nil { return err } @@ -425,7 +531,7 @@ func CreateSchema(app *App) error { // Migrate runs all necessary database migrations. func Migrate(app *App) error { - loadConfig(app) + app.LoadConfig() connectToDatabase(app) defer shutdown(app) @@ -439,7 +545,7 @@ func Migrate(app *App) error { // ResetPassword runs the interactive password reset process. func ResetPassword(app *App, username string) error { // Connect to the database - loadConfig(app) + app.LoadConfig() connectToDatabase(app) defer shutdown(app) @@ -475,16 +581,6 @@ func ResetPassword(app *App, username string) error { return nil } -func loadConfig(app *App) { - log.Info("Loading %s configuration...", app.cfgFile) - cfg, err := config.Load(app.cfgFile) - if err != nil { - log.Error("Unable to load configuration: %v", err) - os.Exit(1) - } - app.cfg = cfg -} - func connectToDatabase(app *App) { log.Info("Connecting to %s database...", app.cfg.Database.Type) @@ -521,14 +617,14 @@ func shutdown(app *App) { } // CreateUser creates a new admin or normal user from the given credentials. -func CreateUser(app *App, username, password string, isAdmin bool) error { +func CreateUser(apper Apper, username, password string, isAdmin bool) error { // Create an admin user with --create-admin - loadConfig(app) - connectToDatabase(app) - defer shutdown(app) + apper.LoadConfig() + connectToDatabase(apper.App()) + defer shutdown(apper.App()) // Ensure an admin / first user doesn't already exist - firstUser, _ := app.db.GetUserByID(1) + firstUser, _ := apper.App().db.GetUserByID(1) if isAdmin { // Abort if trying to create admin user, but one already exists if firstUser != nil { @@ -551,8 +647,8 @@ func CreateUser(app *App, username, password string, isAdmin bool) error { usernameDesc += " (originally: " + desiredUsername + ")" } - if !author.IsValidUsername(app.cfg, username) { - return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, app.cfg.App.MinUsernameLen) + if !author.IsValidUsername(apper.App().cfg, username) { + return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, apper.App().cfg.App.MinUsernameLen) } // Hash the password @@ -572,7 +668,7 @@ func CreateUser(app *App, username, password string, isAdmin bool) error { userType = "admin" } log.Info("Creating %s %s...", userType, usernameDesc) - err = app.db.CreateUser(u, desiredUsername) + err = apper.App().db.CreateUser(u, desiredUsername) if err != nil { return fmt.Errorf("Unable to create user: %s", err) } diff --git a/cmd/writefreely/main.go b/cmd/writefreely/main.go index 6a32ad9..1ddb3da 100644 --- a/cmd/writefreely/main.go +++ b/cmd/writefreely/main.go @@ -13,6 +13,7 @@ package main import ( "flag" "fmt" + "github.com/gorilla/mux" "github.com/writeas/web-core/log" "github.com/writeas/writefreely" "os" @@ -54,7 +55,7 @@ func main() { writefreely.DoConfig(app) os.Exit(0) } else if *genKeys { - err := writefreely.GenerateKeys(app) + err := writefreely.GenerateKeyFiles(app) if err != nil { log.Error(err.Error()) os.Exit(1) @@ -107,7 +108,21 @@ func main() { os.Exit(0) } - writefreely.Serve(app, *debugPtr) + // Initialize the application + var err error + app, err = writefreely.Initialize(app, *debugPtr) + if err != nil { + log.Error("%s", err) + os.Exit(1) + } + + // Set app routes + r := mux.NewRouter() + app.InitRoutes(r) + app.InitStaticRoutes(r) + + // Serve the application + writefreely.Serve(app, r) } func userPass(credStr string, isAdmin bool) (user string, pass string, err error) { diff --git a/handle.go b/handle.go index 946487f..acde1a1 100644 --- a/handle.go +++ b/handle.go @@ -74,6 +74,19 @@ func NewHandler(app *App) *Handler { return h } +// NewWFHandler returns a new Handler instance, using WriteFreely template files. +// You MUST call writefreely.InitTemplates() before this. +func NewWFHandler(app *App) *Handler { + h := NewHandler(app) + h.SetErrorPages(&ErrorPages{ + NotFound: pages["404-general.tmpl"], + Gone: pages["410.tmpl"], + InternalServerError: pages["500.tmpl"], + Blank: pages["blank.tmpl"], + }) + return h +} + // SetErrorPages sets the given set of ErrorPages as templates for any errors // that come up. func (h *Handler) SetErrorPages(e *ErrorPages) { diff --git a/keys.go b/keys.go index 067908e..5cc63a3 100644 --- a/keys.go +++ b/keys.go @@ -28,6 +28,15 @@ var ( cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256") ) +// InitKeys loads encryption keys into memory via the given Apper interface +func InitKeys(apper Apper) error { + log.Info("Loading encryption keys...") + err := apper.LoadKeys() + if err != nil { + return err + } + return nil +} func initKeyPaths(app *App) { emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath) @@ -35,37 +44,6 @@ func initKeyPaths(app *App) { cookieKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieKeyPath) } -func initKeys(app *App) error { - var err error - app.keys = &key.Keychain{} - - if debugging { - log.Info(" %s", emailKeyPath) - } - app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath) - if err != nil { - return err - } - - if debugging { - log.Info(" %s", cookieAuthKeyPath) - } - app.keys.CookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath) - if err != nil { - return err - } - - if debugging { - log.Info(" %s", cookieKeyPath) - } - app.keys.CookieKey, err = ioutil.ReadFile(cookieKeyPath) - if err != nil { - return err - } - - return nil -} - // generateKey generates a key at the given path used for the encryption of // certain user data. Because user data becomes unrecoverable without these // keys, this won't overwrite any existing key, and instead outputs a message. diff --git a/routes.go b/routes.go index f7d2451..a136970 100644 --- a/routes.go +++ b/routes.go @@ -14,7 +14,6 @@ import ( "github.com/gorilla/mux" "github.com/writeas/go-webfinger" "github.com/writeas/web-core/log" - "github.com/writeas/writefreely/config" "github.com/writefreely/go-nodeinfo" "net/http" "path/filepath" @@ -31,9 +30,14 @@ func (app *App) InitStaticRoutes(r *mux.Router) { r.PathPrefix("/").Handler(fs) } -func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datastore) { - hostSubroute := cfg.App.Host[strings.Index(cfg.App.Host, "://")+3:] - if cfg.App.SingleUser { +// InitRoutes adds dynamic routes for the given mux.Router. +func (app *App) InitRoutes(r *mux.Router) *mux.Router { + // Create handler + handler := NewWFHandler(app) + + // Set up routes + hostSubroute := app.cfg.App.Host[strings.Index(app.cfg.App.Host, "://")+3:] + if app.cfg.App.SingleUser { hostSubroute = "{domain}" } else { if strings.HasPrefix(hostSubroute, "localhost") { @@ -41,7 +45,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto } } - if cfg.App.SingleUser { + if app.cfg.App.SingleUser { log.Info("Adding %s routes (single user)...", hostSubroute) } else { log.Info("Adding %s routes (multi-user)...", hostSubroute) @@ -51,7 +55,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto write := r.PathPrefix("/").Subrouter() // Federation endpoint configurations - wf := webfinger.Default(wfResolver{db, cfg}) + wf := webfinger.Default(wfResolver{app.db, app.cfg}) wf.NoTLSHandler = nil // Federation endpoints @@ -60,15 +64,15 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto // webfinger write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger))) // nodeinfo - niCfg := nodeInfoConfig(db, cfg) - ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{cfg, db}) + niCfg := nodeInfoConfig(app.db, app.cfg) + ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{app.cfg, app.db}) write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover))) write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo))) // Set up dyamic page handlers // Handle auth auth := write.PathPrefix("/api/auth/").Subrouter() - if cfg.App.OpenRegistration { + if app.cfg.App.OpenRegistration { auth.HandleFunc("/signup", handler.All(apiSignup)).Methods("POST") } auth.HandleFunc("/login", handler.All(login)).Methods("POST") @@ -155,7 +159,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto RouteRead(handler, readPerm, write.PathPrefix("/read").Subrouter()) draftEditPrefix := "" - if cfg.App.SingleUser { + if app.cfg.App.SingleUser { draftEditPrefix = "/d" write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") } else { @@ -166,7 +170,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET") // Collections - if cfg.App.SingleUser { + if app.cfg.App.SingleUser { RouteCollections(handler, write.PathPrefix("/").Subrouter()) } else { write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional)) @@ -176,6 +180,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto } write.HandleFunc(draftEditPrefix+"/{post}", handler.Web(handleViewPost, UserLevelOptional)) write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional)) + return r } func RouteCollections(handler *Handler, r *mux.Router) { diff --git a/session.go b/session.go index 95bfb18..e379496 100644 --- a/session.go +++ b/session.go @@ -27,9 +27,9 @@ const ( blogPassCookieName = "ub" ) -// initSession creates the cookie store. It depends on the keychain already +// InitSession creates the cookie store. It depends on the keychain already // being loaded. -func initSession(app *App) *sessions.CookieStore { +func (app *App) InitSession() { // Register complex data types we'll be storing in cookies gob.Register(&User{}) @@ -41,7 +41,7 @@ func initSession(app *App) *sessions.CookieStore { HttpOnly: true, Secure: strings.HasPrefix(app.cfg.App.Host, "https://"), } - return store + app.sessionStore = store } func getSessionFlashes(app *App, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) { diff --git a/templates.go b/templates.go index 0f93cb9..7a45c45 100644 --- a/templates.go +++ b/templates.go @@ -98,7 +98,8 @@ func initUserPage(parentDir, path, key string) { )) } -func initTemplates(cfg *config.Config) error { +// InitTemplates loads all template files from the configured parent dir. +func InitTemplates(cfg *config.Config) error { log.Info("Loading templates...") tmplFiles, err := ioutil.ReadDir(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir)) if err != nil { From be0547a62ced91b4eb577d8964507d5eccace717 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 13 Jun 2019 20:25:30 -0400 Subject: [PATCH 12/23] Include schema.sql when built with wflib tag This ensures outside application builds will succeed when including the writefreely pkg. Ref T613 --- Makefile | 11 ++++-- bindata-lib.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 bindata-lib.go diff --git a/Makefile b/Makefile index 0aba10b..86195f3 100644 --- a/Makefile +++ b/Makefile @@ -100,13 +100,16 @@ ui : force_look cd less/; $(MAKE) $(MFLAGS) assets : generate - go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql sqlite.sql + go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql assets-no-sqlite: generate - go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql + go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql dev-assets : generate - go-bindata -pkg writefreely -ignore=\\.gitignore -debug schema.sql sqlite.sql + go-bindata -pkg writefreely -ignore=\\.gitignore -debug -tags="!wflib" schema.sql sqlite.sql + +lib-assets : generate + go-bindata -pkg writefreely -ignore=\\.gitignore -o bindata-lib.go -tags="wflib" schema.sql generate : @hash go-bindata > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @@ -123,7 +126,7 @@ $(TMPBIN)/xgo: deps $(TMPBIN) $(GOBUILD) -o $(TMPBIN)/xgo github.com/karalabe/xgo ci-assets : $(TMPBIN)/go-bindata - $(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql sqlite.sql + $(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql clean : -rm -rf build diff --git a/bindata-lib.go b/bindata-lib.go new file mode 100644 index 0000000..74429dc --- /dev/null +++ b/bindata-lib.go @@ -0,0 +1,105 @@ +// +build wflib + +package writefreely + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "strings" +) + +func bindata_read(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + return buf.Bytes(), nil +} + +var _schema_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x59\x5f\x6f\xa3\x38\x10\x7f\xef\xa7\xf0\xdb\xa6\x52\x23\x6d\x7a\xdd\xaa\xba\xd3\x3e\x64\x53\x76\x2f\xba\x94\xee\x25\x44\xba\x7d\x02\x03\x93\xd4\xaa\xb1\x91\x6d\x92\xe6\xdb\x9f\x8c\x49\x08\x86\x24\xd0\xdb\x3b\x71\x7d\x2a\xcc\x6f\x8c\xfd\x9b\x3f\x9e\x99\x0c\x87\x57\xc3\x21\x7a\xc4\x0a\x87\x58\xc2\xaf\x28\xd8\x0a\xa2\x60\x25\x00\xe8\x2e\xb8\x1a\x0e\xaf\xb4\x78\xf8\xce\x3f\xad\xac\xf5\x3d\x1c\x52\x40\x52\x89\x2c\x52\x99\x00\xb4\xe2\x02\xa9\xfc\x5d\x80\xa3\x08\xa4\x54\xfc\x15\x98\x34\xdf\x9b\xcc\x9d\xb1\xe7\x20\x6f\xfc\x65\xe6\xa0\xe9\x57\xe4\x3e\x7b\xc8\xf9\x6b\xba\xf0\x16\x16\x1a\x0d\xae\x10\x0a\xf2\x87\x00\x85\x84\x61\xb1\x1b\x8c\xee\xaf\x73\x05\x77\x39\x9b\xdd\x68\x71\x26\x41\xf8\x24\x0e\x10\x61\x6a\x60\x0b\x65\x16\xf3\x00\x29\xc2\x76\x5a\x3a\x2a\xa5\xe8\xd1\xf9\x3a\x5e\xce\x3c\xf4\xe1\xe3\x87\x1c\xc9\x19\xf8\x8a\x24\xd0\x0e\x1d\x09\xc0\x0a\xe2\x00\xc5\x58\x81\x56\xab\x43\x27\xcb\xf9\xdc\x71\x3d\xdf\x9b\x3e\x39\x0b\x6f\xfc\xf4\x3d\x57\x84\xb7\x94\x08\x90\x47\x8a\x7b\x7c\xf5\x40\x78\x0d\x4c\x05\x68\x83\x45\xf4\x82\xc5\xe0\xf6\xd3\xa7\xeb\x1a\xf2\xfb\x7c\xfa\x34\x9e\xff\x40\x7f\x38\x3f\xd0\xa0\xa0\xe9\xfa\xea\x1a\x39\xee\xb7\xa9\xeb\x7c\x9e\x32\xc6\x1f\xbf\x94\xfb\xf9\x7d\x3c\x5f\x38\xde\x67\x8a\x15\x61\xa3\xdf\xfe\x75\xb3\xa7\x69\xc4\x99\xd2\xa7\xb8\x6c\xf4\x12\x6b\x4c\xae\xcd\xb9\x3f\xfa\x2f\xb6\x4d\x0f\xd0\x04\x62\x92\x25\x0a\xde\x54\x7e\xb8\xf1\xc4\x73\xe6\x68\xe1\x78\x28\x53\xab\x07\x34\x79\x9e\xcd\xf4\x17\xf5\x83\x1f\x12\x66\x79\x4d\x1a\xbf\xcb\x80\x55\xce\x49\xdc\x2b\xc2\x13\xb2\x16\x58\x11\xde\x18\x68\x16\xc0\x10\xbd\x01\x21\x09\x67\x26\x78\x46\x23\x8b\x69\x03\x6f\x64\x29\x97\x0b\x90\x19\x55\x01\xca\x4d\xb0\x97\xf4\x85\x8f\x88\x53\x0a\x91\x3e\x2c\x56\x4a\x90\x30\x53\xd0\x22\xff\x34\x6a\x19\xae\x4a\xd1\xc9\x74\x73\xd0\x29\xdd\x77\x74\xfb\x60\x81\x36\x98\x66\x60\x85\x76\xdd\x7f\x93\xf0\xae\xe2\xc2\x49\x78\x57\xf3\xe2\xaa\x33\x56\xf7\x77\x73\xb4\x99\xde\xf8\x68\xb9\xc5\x57\xd8\x75\xb2\x46\x8e\x6f\x6d\x87\x34\x0b\x29\x89\xfc\x57\xd8\x05\x28\xa4\x3c\xb4\xa4\x82\x6c\xb0\x82\x13\xe2\x73\xa4\xf6\x90\xc8\x14\x4b\xb9\xe5\x22\xee\xc4\x66\xa9\xd4\x9e\xd2\x42\x25\x40\xb9\xd7\xde\x7f\xbc\xfe\x3f\xb3\x26\x20\x26\x02\x22\xd5\x89\xb5\x52\xc9\xb0\x96\x0a\xd8\xf8\x98\x12\x2c\x8f\xc2\xfd\xa3\x45\x4c\xc0\x60\x7b\x11\x54\x65\xef\x68\xdd\x1e\x52\xd7\x89\x32\x79\x74\xa1\x5b\x5e\x85\xc6\x4b\xef\xd9\x9f\xba\x93\xb9\xf3\xe4\xb8\x9e\xc9\x9f\x0d\x3c\xb5\x4f\x8d\xb5\x4a\x4a\x11\x45\x7f\x4e\xa6\x0d\x62\x90\x91\x20\xa9\xca\x2f\xcb\xc3\xfe\xee\x3b\xed\xaf\x5a\x99\xaa\x1d\x05\x5f\xbe\x00\x14\x17\xa8\x79\x9b\x7f\xa4\xb8\x51\x5b\xaf\x9c\xab\xae\xb8\x48\xf0\x51\xc9\xf8\x50\x2f\x18\x4d\xe6\x8b\x76\x8d\x35\xae\xa9\x82\xb7\xec\x4c\x35\xbd\x21\xb0\xf5\x23\x9e\xe9\xe2\xab\x41\x5e\xaf\x8d\xf4\xdb\xa5\x3b\xfd\x73\xe9\xe4\x2f\xf7\xf6\x1d\x04\x3d\xf3\xee\x94\xcb\x36\xa9\xc0\xc0\x4a\x8f\x2e\x9c\xc0\xee\x39\x68\xb6\xb6\x7c\xb8\x66\x88\x84\xc7\x64\xb5\xf3\x8b\xd6\xc6\xd4\xb9\xb7\x0d\x38\xed\x07\x3e\x4e\x53\xc0\x02\xb3\x08\x0a\xe8\x5d\x53\x67\xc2\xb8\x48\x4c\x73\x42\x31\x5b\x67\x78\xbd\x47\x37\xad\x2b\x14\xad\x38\xc1\x4f\xf0\x94\xda\x12\xcd\x97\x4a\xfd\x4b\x84\x31\x88\xfd\x94\x4b\x62\xa2\xeb\xe8\x8b\x4b\x77\x31\xfd\xe6\x3a\x8f\x0d\x8b\xef\x1b\x30\x5d\x95\x4a\x85\x93\xb4\x6d\x07\x76\xa8\xfc\x3b\x6b\x5e\x70\x7f\x3b\xdd\xfc\x93\xec\x70\xe8\x71\xba\x25\x82\x8e\xe1\x48\x62\xdf\x38\x6b\xbd\x78\xcc\xdf\xd7\x14\x4a\xa3\x0f\xca\xff\x6f\x0e\x6b\xe7\x98\xc2\x73\x0a\xd4\xde\x8f\x6e\x7a\xd5\x2b\x09\x48\xb8\x82\x15\xa7\x94\x6f\x5b\xc4\x7d\x15\x7e\xb2\x64\xaa\xf5\x4f\x46\xcf\xaf\x4c\x28\x6a\xa0\xd3\xa3\x84\xcb\x25\xbe\xf5\x81\x9e\xf1\xab\xb7\xd5\xae\xce\xb7\xf0\xf5\x21\x40\x7e\x75\x77\xe7\xf6\x6c\x1f\x70\x39\x3e\x8c\xc5\x0f\x1e\xdf\x7f\xb6\x3b\x51\x6d\xd7\x66\xc7\xec\x35\x16\x67\x91\xe2\x86\x8a\xd3\x56\x21\x2c\xe4\x6f\xe7\x00\xf2\x05\x0b\x88\xfd\x4b\xb8\xcb\xb6\xb1\xe2\x6f\x50\x6e\xaf\x37\x76\xd1\x24\x77\x99\x3d\x58\x78\x63\x9d\xb3\xe3\xcd\x86\x79\xc3\xfd\xdd\x7f\x34\x6e\xd8\x6f\xac\x97\x83\x06\xbd\x39\xc2\x36\xa4\x99\xf7\x8a\xd8\x2a\xe7\x6c\x8a\xab\x75\x4e\x7d\x44\x86\xdf\x74\x42\x90\x01\x92\x09\xa6\xf4\x64\x2d\x74\x36\xc9\xb7\x99\x0a\x13\x86\x23\x45\x36\xcd\xf3\xe9\x3e\xd1\xde\xd2\xd1\x3b\x76\x86\x5a\x85\xe1\x04\xde\xdd\x1c\x5e\x1a\x66\x54\x57\x32\x7c\x1d\x16\x32\x8f\xf5\x75\x20\xc1\x84\xe6\x5b\x2a\x7e\x9d\x68\x9c\xd3\xbf\xfb\xd7\x82\xcb\x59\xb0\xa4\x65\x50\xfe\xdf\xab\x28\x94\x26\xce\xe2\x53\x61\x78\x90\x17\xee\x90\x3f\xf9\x27\xc3\xf1\xe4\x7d\xdf\xfa\xcc\x7f\x07\x00\x00\xff\xff\xbe\x79\x68\xa8\x10\x1b\x00\x00") + +func schema_sql() ([]byte, error) { + return bindata_read( + _schema_sql, + "schema.sql", + ) +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + return f() + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() ([]byte, error){ + "schema.sql": schema_sql, +} +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for name := range node.Children { + rv = append(rv, name) + } + return rv, nil +} + +type _bintree_t struct { + Func func() ([]byte, error) + Children map[string]*_bintree_t +} +var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ + "schema.sql": &_bintree_t{schema_sql, map[string]*_bintree_t{ + }}, +}} From 830b859421fd14780809593c19fd3998d84ba6cb Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 13 Jun 2019 20:44:13 -0400 Subject: [PATCH 13/23] Fix activitypub.go imports --- activitypub.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/activitypub.go b/activitypub.go index 8b1b129..bbcd3bb 100644 --- a/activitypub.go +++ b/activitypub.go @@ -17,6 +17,13 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "time" + "github.com/gorilla/mux" "github.com/writeas/activity/streams" "github.com/writeas/httpsig" @@ -25,12 +32,6 @@ import ( "github.com/writeas/web-core/activitypub" "github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/log" - "io/ioutil" - "net/http" - "net/http/httputil" - "net/url" - "strconv" - "time" ) const ( From 26a0990014408eee55c22ef177dcbf5e73b466a1 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 13 Jun 2019 21:56:13 -0400 Subject: [PATCH 14/23] Save config via Apper interface from Admin dash Ref T613 --- admin.go | 32 ++++++------ cmd/writefreely/main.go | 2 +- handle.go | 111 +++++++++++++++++++++++++++------------- routes.go | 24 ++++----- 4 files changed, 104 insertions(+), 65 deletions(-) diff --git a/admin.go b/admin.go index b2eab1f..696d4eb 100644 --- a/admin.go +++ b/admin.go @@ -347,33 +347,33 @@ func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Req return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m} } -func handleAdminUpdateConfig(app *App, u *User, w http.ResponseWriter, r *http.Request) error { - app.cfg.App.SiteName = r.FormValue("site_name") - app.cfg.App.SiteDesc = r.FormValue("site_desc") - app.cfg.App.OpenRegistration = r.FormValue("open_registration") == "on" +func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error { + apper.App().cfg.App.SiteName = r.FormValue("site_name") + apper.App().cfg.App.SiteDesc = r.FormValue("site_desc") + apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on" mul, err := strconv.Atoi(r.FormValue("min_username_len")) if err == nil { - app.cfg.App.MinUsernameLen = mul + apper.App().cfg.App.MinUsernameLen = mul } mb, err := strconv.Atoi(r.FormValue("max_blogs")) if err == nil { - app.cfg.App.MaxBlogs = mb + apper.App().cfg.App.MaxBlogs = mb } - app.cfg.App.Federation = r.FormValue("federation") == "on" - app.cfg.App.PublicStats = r.FormValue("public_stats") == "on" - app.cfg.App.Private = r.FormValue("private") == "on" - app.cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on" - if app.cfg.App.LocalTimeline && app.timeline == nil { + apper.App().cfg.App.Federation = r.FormValue("federation") == "on" + apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on" + apper.App().cfg.App.Private = r.FormValue("private") == "on" + apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on" + if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil { log.Info("Initializing local timeline...") - initLocalTimeline(app) + initLocalTimeline(apper.App()) } - app.cfg.App.UserInvites = r.FormValue("user_invites") - if app.cfg.App.UserInvites == "none" { - app.cfg.App.UserInvites = "" + apper.App().cfg.App.UserInvites = r.FormValue("user_invites") + if apper.App().cfg.App.UserInvites == "none" { + apper.App().cfg.App.UserInvites = "" } m := "?cm=Configuration+saved." - err = config.Save(app.cfg, app.cfgFile) + err = apper.SaveConfig(apper.App().cfg) if err != nil { m = "?cm=" + err.Error() } diff --git a/cmd/writefreely/main.go b/cmd/writefreely/main.go index 1ddb3da..484135d 100644 --- a/cmd/writefreely/main.go +++ b/cmd/writefreely/main.go @@ -118,7 +118,7 @@ func main() { // Set app routes r := mux.NewRouter() - app.InitRoutes(r) + writefreely.InitRoutes(app, r) app.InitStaticRoutes(r) // Serve the application diff --git a/handle.go b/handle.go index acde1a1..81a4823 100644 --- a/handle.go +++ b/handle.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -36,16 +36,17 @@ const ( ) type ( - handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error - userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error - dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) - authFunc func(app *App, r *http.Request) (*User, error) + handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error + userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error + userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error + dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) + authFunc func(app *App, r *http.Request) (*User, error) ) type Handler struct { errors *ErrorPages sessionStore *sessions.CookieStore - app *App + app Apper } // ErrorPages hold template HTML error pages for displaying errors to the user. @@ -59,7 +60,7 @@ type ErrorPages struct { // NewHandler returns a new Handler instance, using the given StaticPage data, // and saving alias to the application's CookieStore. -func NewHandler(app *App) *Handler { +func NewHandler(apper Apper) *Handler { h := &Handler{ errors: &ErrorPages{ NotFound: template.Must(template.New("").Parse("{{define \"base\"}}404

Not found.

{{end}}")), @@ -67,8 +68,8 @@ func NewHandler(app *App) *Handler { InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}500

Internal server error.

{{end}}")), Blank: template.Must(template.New("").Parse("{{define \"base\"}}{{.Title}}

{{.Content}}

{{end}}")), }, - sessionStore: app.sessionStore, - app: app, + sessionStore: apper.App().sessionStore, + app: apper, } return h @@ -76,8 +77,8 @@ func NewHandler(app *App) *Handler { // NewWFHandler returns a new Handler instance, using WriteFreely template files. // You MUST call writefreely.InitTemplates() before this. -func NewWFHandler(app *App) *Handler { - h := NewHandler(app) +func NewWFHandler(apper Apper) *Handler { + h := NewHandler(apper) h.SetErrorPages(&ErrorPages{ NotFound: pages["404-general.tmpl"], Gone: pages["410.tmpl"], @@ -104,21 +105,21 @@ func (h *Handler) User(f userHandlerFunc) http.HandlerFunc { defer func() { if e := recover(); e != nil { log.Error("%s: %s", e, debug.Stack()) - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = http.StatusInternalServerError } log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) }() - u := getUserSession(h.app, r) + u := getUserSession(h.app.App(), r) if u == nil { err := ErrNotLoggedIn status = err.Status return err } - err := f(h.app, u, w, r) + err := f(h.app.App(), u, w, r) if err == nil { status = http.StatusOK } else if err, ok := err.(impart.HTTPError); ok { @@ -142,14 +143,52 @@ func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc { defer func() { if e := recover(); e != nil { log.Error("%s: %s", e, debug.Stack()) - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = http.StatusInternalServerError } log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())) }() - u := getUserSession(h.app, r) + u := getUserSession(h.app.App(), r) + if u == nil || !u.IsAdmin() { + err := impart.HTTPError{http.StatusNotFound, ""} + status = err.Status + return err + } + + err := f(h.app.App(), u, w, r) + if err == nil { + status = http.StatusOK + } else if err, ok := err.(impart.HTTPError); ok { + status = err.Status + } else { + status = http.StatusInternalServerError + } + + return err + }()) + } +} + +// AdminApper handles requests on /admin routes that require an Apper. +func (h *Handler) AdminApper(f userApperHandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h.handleHTTPError(w, r, func() error { + var status int + start := time.Now() + + defer func() { + if e := recover(); e != nil { + log.Error("%s: %s", e, debug.Stack()) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) + status = http.StatusInternalServerError + } + + log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())) + }() + + u := getUserSession(h.app.App(), r) if u == nil || !u.IsAdmin() { err := impart.HTTPError{http.StatusNotFound, ""} status = err.Status @@ -204,7 +243,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) }() - u, err := a(h.app, r) + u, err := a(h.app.App(), r) if err != nil { if err, ok := err.(impart.HTTPError); ok { status = err.Status @@ -214,7 +253,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF return err } - err = f(h.app, u, w, r) + err = f(h.app.App(), u, w, r) if err == nil { status = 200 } else if err, ok := err.(impart.HTTPError); ok { @@ -277,13 +316,13 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc { defer func() { if e := recover(); e != nil { - u := getUserSession(h.app, r) + u := getUserSession(h.app.App(), r) username := "None" if u != nil { username = u.Username } log.Error("User: %s\n\n%s: %s", username, e, debug.Stack()) - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } @@ -315,13 +354,13 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc { } // TODO: pass User object to function - err = f(h.app, w, r) + err = f(h.app.App(), w, r) if err == nil { status = 200 } else if httpErr, ok := err.(impart.HTTPError); ok { status = httpErr.Status if status < 300 || status > 399 { - addSessionFlash(h.app, w, r, httpErr.Message, session) + addSessionFlash(h.app.App(), w, r, httpErr.Message, session) return impart.HTTPError{http.StatusFound, r.Referer()} } } else { @@ -332,7 +371,7 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc { log.Error(e) } log.Info("Web handler internal error render") - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } @@ -351,14 +390,14 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc { defer func() { if e := recover(); e != nil { - u := getUserSession(h.app, r) + u := getUserSession(h.app.App(), r) username := "None" if u != nil { username = u.Username } log.Error("User: %s\n\n%s: %s", username, e, debug.Stack()) log.Info("Web deferred internal error render") - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } @@ -388,7 +427,7 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc { } // TODO: pass User object to function - err := f(h.app, w, r) + err := f(h.app.App(), w, r) if err == nil { status = 200 } else if httpErr, ok := err.(impart.HTTPError); ok { @@ -397,7 +436,7 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc { e := fmt.Sprintf("[Web handler] 500: %v", err) log.Error(e) log.Info("Web internal error render") - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } @@ -425,7 +464,7 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc { // TODO: do any needed authentication - err := f(h.app, w, r) + err := f(h.app.App(), w, r) if err != nil { if err, ok := err.(impart.HTTPError); ok { status = err.Status @@ -447,14 +486,14 @@ func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc { defer func() { if e := recover(); e != nil { log.Error("%s: %s", e, debug.Stack()) - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) }() - data, filename, err := f(h.app, w, r) + data, filename, err := f(h.app.App(), w, r) if err != nil { if err, ok := err.(impart.HTTPError); ok { status = err.Status @@ -543,7 +582,7 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er page.StaticPage Content *template.HTML }{ - StaticPage: pageForReq(h.app, r), + StaticPage: pageForReq(h.app.App(), r), } if err.Message != "" { co := template.HTML(err.Message) @@ -553,12 +592,12 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er return } else if err.Status == http.StatusNotFound { w.WriteHeader(err.Status) - h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) return } else if err.Status == http.StatusInternalServerError { w.WriteHeader(err.Status) log.Info("handleHTTPErorr internal error render") - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) return } else if err.Status == http.StatusAccepted { impart.WriteSuccess(w, "", err.Status) @@ -569,7 +608,7 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er Title string Content template.HTML }{ - pageForReq(h.app, r), + pageForReq(h.app.App(), r), fmt.Sprintf("Uh oh (%d)", err.Status), template.HTML(fmt.Sprintf("

%s

", err.Message)), } @@ -604,7 +643,7 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error) impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."}) return } - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) } func correctPageFromLoginAttempt(r *http.Request) string { @@ -626,7 +665,7 @@ func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc { defer func() { if e := recover(); e != nil { log.Error("Handler.LogHandlerFunc\n\n%s: %s", e, debug.Stack()) - h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } diff --git a/routes.go b/routes.go index a136970..13dd3a5 100644 --- a/routes.go +++ b/routes.go @@ -31,13 +31,13 @@ func (app *App) InitStaticRoutes(r *mux.Router) { } // InitRoutes adds dynamic routes for the given mux.Router. -func (app *App) InitRoutes(r *mux.Router) *mux.Router { +func InitRoutes(apper Apper, r *mux.Router) *mux.Router { // Create handler - handler := NewWFHandler(app) + handler := NewWFHandler(apper) // Set up routes - hostSubroute := app.cfg.App.Host[strings.Index(app.cfg.App.Host, "://")+3:] - if app.cfg.App.SingleUser { + hostSubroute := apper.App().cfg.App.Host[strings.Index(apper.App().cfg.App.Host, "://")+3:] + if apper.App().cfg.App.SingleUser { hostSubroute = "{domain}" } else { if strings.HasPrefix(hostSubroute, "localhost") { @@ -45,7 +45,7 @@ func (app *App) InitRoutes(r *mux.Router) *mux.Router { } } - if app.cfg.App.SingleUser { + if apper.App().cfg.App.SingleUser { log.Info("Adding %s routes (single user)...", hostSubroute) } else { log.Info("Adding %s routes (multi-user)...", hostSubroute) @@ -55,7 +55,7 @@ func (app *App) InitRoutes(r *mux.Router) *mux.Router { write := r.PathPrefix("/").Subrouter() // Federation endpoint configurations - wf := webfinger.Default(wfResolver{app.db, app.cfg}) + wf := webfinger.Default(wfResolver{apper.App().db, apper.App().cfg}) wf.NoTLSHandler = nil // Federation endpoints @@ -64,15 +64,15 @@ func (app *App) InitRoutes(r *mux.Router) *mux.Router { // webfinger write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger))) // nodeinfo - niCfg := nodeInfoConfig(app.db, app.cfg) - ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{app.cfg, app.db}) + niCfg := nodeInfoConfig(apper.App().db, apper.App().cfg) + ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{apper.App().cfg, apper.App().db}) write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover))) write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo))) // Set up dyamic page handlers // Handle auth auth := write.PathPrefix("/api/auth/").Subrouter() - if app.cfg.App.OpenRegistration { + if apper.App().cfg.App.OpenRegistration { auth.HandleFunc("/signup", handler.All(apiSignup)).Methods("POST") } auth.HandleFunc("/login", handler.All(login)).Methods("POST") @@ -145,7 +145,7 @@ func (app *App) InitRoutes(r *mux.Router) *mux.Router { write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET") write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET") write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET") - write.HandleFunc("/admin/update/config", handler.Admin(handleAdminUpdateConfig)).Methods("POST") + write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST") write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST") // Handle special pages first @@ -159,7 +159,7 @@ func (app *App) InitRoutes(r *mux.Router) *mux.Router { RouteRead(handler, readPerm, write.PathPrefix("/read").Subrouter()) draftEditPrefix := "" - if app.cfg.App.SingleUser { + if apper.App().cfg.App.SingleUser { draftEditPrefix = "/d" write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") } else { @@ -170,7 +170,7 @@ func (app *App) InitRoutes(r *mux.Router) *mux.Router { write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET") // Collections - if app.cfg.App.SingleUser { + if apper.App().cfg.App.SingleUser { RouteCollections(handler, write.PathPrefix("/").Subrouter()) } else { write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional)) From 0c44fe1c2eadc1441f71b036ad7dd8b5e6e7da61 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 14 Jun 2019 09:52:10 -0700 Subject: [PATCH 15/23] add update script this bash script will download a new version, if any, and exrtact it to a temporary location. Then copy the new files to the current directory and restart the systemd service. --- scripts/upgrade-server.sh | 88 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100755 scripts/upgrade-server.sh diff --git a/scripts/upgrade-server.sh b/scripts/upgrade-server.sh new file mode 100755 index 0000000..5246d89 --- /dev/null +++ b/scripts/upgrade-server.sh @@ -0,0 +1,88 @@ +#! /bin/bash +############################################################################### +## writefreely update script ## +## ## +## WARNING: running this script will overwrite any modifed assets or ## +## template files. If you have any custom changes to these files you ## +## should back them up FIRST. ## +## ## +## This must be run from the web application root directory ## +## i.e. /var/www/writefreely, and operates under the assumption that you ## +## have not installed the binary `writefreely` in another location. ## +############################################################################### +# +# Copyright © 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. +# + + +# only execute as root, or use sudo + +if [[ `id -u` -ne 0 ]]; then + echo "You must login as root, or execute this script with sudo" + exit 10 +fi + +# go ahead and check for the latest release on linux +echo "Checking for updates.." + +url=`curl -s https://api.github.com/repos/writeas/writefreely/releases/latest | grep 'browser_' | grep linux | cut -d\" -f4` + +# check current version + +current=`./writefreely -v` + +echo "Current version is v${current:(-5):5}" + +# grab latest version number +IFS='/' +read -ra parts <<< "$url" + +latest=${parts[-2]} +echo "Latest release is $latest" + + +IFS='.' +read -ra cv <<< "${current:(-5):5}" +read -ra lv <<< "${latest#v}" + +IFS=' ' +tempdir=$(mktemp -d) + + +if [[ ${lv[0]} -gt ${cv[0]} ]]; then + echo "New major version" + echo "Downloading.." + `wget -P $tempdir -q --show-progress $url` +elif [[ ${lv[0]} -eq ${cv[0]} ]] && [[ ${lv[1]} -gt ${cv[1]} ]]; then + echo "New minor version" + echo "Downloading.." + `wget -P $tempdir -q --show-progress $url` +elif [[ ${lv[2]} -gt ${cv[2]} ]]; then + echo "New patch version" + echo "Downloading.." + `wget -P $tempdir -q --show-progress $url` +else + echo "Nothing to change" + exit 0 +fi + +filename=${parts[-1]} + +# extract +echo "Extracing files.." +tar -zxf $tempdir/$filename -C $tempdir + +# copy files +echo "Copying files.." +cp -r $tempdir/{pages,static,templates,writefreely} . + +# restart service +echo "Restarting writefreely systemd service.." +`systemctl restart writefreely` +echo "Done, version has been upgraded to $latest." From f6ba1fc9c8a041380aae7bfab0fe937cbb54d769 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 14 Jun 2019 10:05:08 -0700 Subject: [PATCH 16/23] update script: error check restarting service basic error check with descriptive output in update_server.sh --- scripts/upgrade-server.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/upgrade-server.sh b/scripts/upgrade-server.sh index 5246d89..1bfab71 100755 --- a/scripts/upgrade-server.sh +++ b/scripts/upgrade-server.sh @@ -84,5 +84,9 @@ cp -r $tempdir/{pages,static,templates,writefreely} . # restart service echo "Restarting writefreely systemd service.." -`systemctl restart writefreely` -echo "Done, version has been upgraded to $latest." +if `systemctl restart writefreely`; then + echo "Success, version has been upgraded to $latest." +else + echo "Upgrade complete, but failed to restart service" + exit 1 +fi From 4c34b3473662d5b367b1c7f75cbe9e9f2e79835d Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 14 Jun 2019 16:43:18 -0400 Subject: [PATCH 17/23] Exit when no writefreely executable found (or `writefreely -v` otherwise doesn't output anything) Ref T555 --- scripts/upgrade-server.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/upgrade-server.sh b/scripts/upgrade-server.sh index 1bfab71..039fde8 100755 --- a/scripts/upgrade-server.sh +++ b/scripts/upgrade-server.sh @@ -36,6 +36,9 @@ url=`curl -s https://api.github.com/repos/writeas/writefreely/releases/latest | # check current version current=`./writefreely -v` +if [ -z "$current" ]; then + exit 1 +fi echo "Current version is v${current:(-5):5}" From f38a135bfa7202d65aa627c31bd38db1880bcdf9 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 14 Jun 2019 18:54:04 -0400 Subject: [PATCH 18/23] Remove global hostName var This moves `hostName` to the `Collection` struct, where it's needed. The field is populated after successful `GetCollection...()` calls. This isn't the cleanest way to do things, but it accomplishes the goal. Eventually, we should accept the AppCfg to `GetCollection...()` calls, or make them `App` methods, instead of `datastore` methods. Ref T613 --- activitypub.go | 17 +++++++++++------ app.go | 4 ---- collections.go | 15 ++++++++++----- feed.go | 1 + posts.go | 6 ++++-- read.go | 11 +++++++---- sitemap.go | 1 + webfinger.go | 1 + 8 files changed, 35 insertions(+), 21 deletions(-) diff --git a/activitypub.go b/activitypub.go index 0308b6c..0ac4d0c 100644 --- a/activitypub.go +++ b/activitypub.go @@ -80,6 +80,7 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re if err != nil { return err } + c.hostName = app.cfg.App.Host p := c.PersonObject() @@ -104,6 +105,7 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques if err != nil { return err } + c.hostName = app.cfg.App.Host if app.cfg.App.SingleUser { if alias != c.Alias { @@ -156,6 +158,7 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req if err != nil { return err } + c.hostName = app.cfg.App.Host accountRoot := c.FederatedAccount() @@ -201,6 +204,7 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req if err != nil { return err } + c.hostName = app.cfg.App.Host accountRoot := c.FederatedAccount() @@ -234,6 +238,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request // TODO: return Reject? return err } + c.hostName = app.cfg.App.Host if debugging { dump, err := httputil.DumpRequest(r, true) @@ -349,7 +354,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request log.Error("No to! %v", err) return } - err = makeActivityPost(p, fullActor.Inbox, am) + err = makeActivityPost(app.cfg.App.Host, p, fullActor.Inbox, am) if err != nil { log.Error("Unable to make activity POST: %v", err) return @@ -423,7 +428,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request return nil } -func makeActivityPost(p *activitystreams.Person, url string, m interface{}) error { +func makeActivityPost(hostName string, p *activitystreams.Person, url string, m interface{}) error { log.Info("POST %s", url) b, err := json.Marshal(m) if err != nil { @@ -477,7 +482,7 @@ func makeActivityPost(p *activitystreams.Person, url string, m interface{}) erro return nil } -func resolveIRI(url string) ([]byte, error) { +func resolveIRI(hostName, url string) ([]byte, error) { log.Info("GET %s", url) r, _ := http.NewRequest("GET", url, nil) @@ -547,7 +552,7 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error { na.CC = append(na.CC, f) } - err = makeActivityPost(actor, si, activitystreams.NewDeleteActivity(na)) + err = makeActivityPost(app.cfg.App.Host, actor, si, activitystreams.NewDeleteActivity(na)) if err != nil { log.Error("Couldn't delete post! %v", err) } @@ -601,7 +606,7 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { activity.To = na.To activity.CC = na.CC } - err = makeActivityPost(actor, si, activity) + err = makeActivityPost(app.cfg.App.Host, actor, si, activity) if err != nil { log.Error("Couldn't post! %v", err) } @@ -632,7 +637,7 @@ func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, if iErr.Status == http.StatusNotFound { // Fetch remote actor log.Info("Not found; fetching actor %s remotely", actorIRI) - actorResp, err := resolveIRI(actorIRI) + actorResp, err := resolveIRI(app.cfg.App.Host, actorIRI) if err != nil { log.Error("Unable to get actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."} diff --git a/app.go b/app.go index e970a77..a0864c4 100644 --- a/app.go +++ b/app.go @@ -57,9 +57,6 @@ var ( softwareVer = "0.9.0" // DEPRECATED VARS - // TODO: pass app.cfg into GetCollection* calls so we can get these values - // from Collection methods and we no longer need these. - hostName string isSingleUser bool ) @@ -338,7 +335,6 @@ func Initialize(apper Apper, debug bool) (*App, error) { func Serve(app *App, r *mux.Router) { log.Info("Going to serve...") - hostName = app.cfg.App.Host isSingleUser = app.cfg.App.SingleUser app.cfg.Server.Dev = debugging diff --git a/collections.go b/collections.go index eb9fcab..1a8ceca 100644 --- a/collections.go +++ b/collections.go @@ -54,7 +54,8 @@ type ( PublicOwner bool `datastore:"public_owner" json:"-"` URL string `json:"url,omitempty"` - db *datastore + db *datastore + hostName string } CollectionObj struct { Collection @@ -211,10 +212,10 @@ func (c *Collection) DisplayCanonicalURL() string { func (c *Collection) RedirectingCanonicalURL(isRedir bool) string { if isSingleUser { - return hostName + "/" + return c.hostName + "/" } - return fmt.Sprintf("%s/%s/", hostName, c.Alias) + return fmt.Sprintf("%s/%s/", c.hostName, c.Alias) } // PrevPageURL provides a full URL for the previous page of collection posts, @@ -300,11 +301,11 @@ func (c *Collection) AvatarURL() string { if !isAvatarChar(fl) { return "" } - return hostName + "/img/avatars/" + fl + ".png" + return c.hostName + "/img/avatars/" + fl + ".png" } func (c *Collection) FederatedAPIBase() string { - return hostName + "/" + return c.hostName + "/" } func (c *Collection) FederatedAccount() string { @@ -434,6 +435,8 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error { if err != nil { return err } + c.hostName = app.cfg.App.Host + // Redirect users who aren't requesting JSON reqJSON := IsJSON(r.Header.Get("Content-Type")) if !reqJSON { @@ -475,6 +478,7 @@ func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) erro if err != nil { return err } + c.hostName = app.cfg.App.Host // Check permissions userID, err := apiCheckCollectionPermissions(app, r, c) @@ -600,6 +604,7 @@ func processCollectionPermissions(app *App, cr *collectionReq, u *User, w http.R } return nil, err } + c.hostName = app.cfg.App.Host // Update CollectionRequest to reflect owner status cr.isCollOwner = u != nil && u.ID == c.OwnerID diff --git a/feed.go b/feed.go index bf3444b..dd82c33 100644 --- a/feed.go +++ b/feed.go @@ -34,6 +34,7 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { if err != nil { return nil } + c.hostName = app.cfg.App.Host if c.IsPrivate() || c.IsProtected() { return ErrCollectionNotFound diff --git a/posts.go b/posts.go index 35cb6b9..0efa5ec 100644 --- a/posts.go +++ b/posts.go @@ -996,6 +996,7 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { if err != nil { return err } + coll.hostName = app.cfg.App.Host _, err = apiCheckCollectionPermissions(app, r, coll) if err != nil { return err @@ -1056,7 +1057,7 @@ func (p *Post) processPost() PublicPost { func (p *PublicPost) CanonicalURL() string { if p.Collection == nil || p.Collection.Alias == "" { - return hostName + "/" + p.ID + return p.Collection.hostName + "/" + p.ID } return p.Collection.CanonicalURL() + p.Slug.String } @@ -1087,7 +1088,7 @@ func (p *PublicPost) ActivityObject() *activitystreams.Object { if isSingleUser { tagBaseURL = p.Collection.CanonicalURL() + "tag:" } else { - tagBaseURL = fmt.Sprintf("%s/%s/tag:", hostName, p.Collection.Alias) + tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias) } for _, t := range p.Tags { o.Tag = append(o.Tag, activitystreams.Tag{ @@ -1244,6 +1245,7 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error } return err } + c.hostName = app.cfg.App.Host // Check collection permissions if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) { diff --git a/read.go b/read.go index 13add9e..3bc91c7 100644 --- a/read.go +++ b/read.go @@ -52,17 +52,17 @@ type readPublication struct { func initLocalTimeline(app *App) { app.timeline = &localTimeline{ postsPerPage: tlPostsPerPage, - m: memo.New(app.db.FetchPublicPosts, 10*time.Minute), + m: memo.New(app.FetchPublicPosts, 10*time.Minute), } } // satisfies memo.Func -func (db *datastore) FetchPublicPosts() (interface{}, error) { +func (app *App) FetchPublicPosts() (interface{}, error) { // Finds all public posts and posts in a public collection published during the owner's active subscription period and within the last 3 months - rows, err := db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated + rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated FROM collections c LEFT JOIN posts p ON p.collection_id = c.id - WHERE c.privacy = 1 AND (p.created >= ` + db.dateSub(3, "month") + ` AND p.created <= ` + db.now() + ` AND pinned_position IS NULL) + WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) ORDER BY p.created DESC`) if err != nil { log.Error("Failed selecting from posts: %v", err) @@ -82,6 +82,8 @@ func (db *datastore) FetchPublicPosts() (interface{}, error) { log.Error("[READ] Unable to scan row, skipping: %v", err) continue } + c.hostName = app.cfg.App.Host + isCollectionPost := alias.Valid if isCollectionPost { c.Alias = alias.String @@ -244,6 +246,7 @@ func handlePostIDRedirect(app *App, w http.ResponseWriter, r *http.Request) erro if err != nil { return err } + c.hostName = app.cfg.App.Host // Retrieve collection information and send user to canonical URL return impart.HTTPError{http.StatusFound, c.CanonicalURL() + p.Slug.String} diff --git a/sitemap.go b/sitemap.go index efc772b..5c37366 100644 --- a/sitemap.go +++ b/sitemap.go @@ -57,6 +57,7 @@ func handleViewSitemap(app *App, w http.ResponseWriter, r *http.Request) error { if err != nil { return err } + c.hostName = app.cfg.App.Host if !isSubdomain { pre += alias + "/" diff --git a/webfinger.go b/webfinger.go index f8a0d7b..c95d88e 100644 --- a/webfinger.go +++ b/webfinger.go @@ -37,6 +37,7 @@ func (wfr wfResolver) FindUser(username string, host, requestHost string, r []we log.Error("Unable to get blog: %v", err) return nil, err } + c.hostName = wfr.cfg.App.Host if wfr.cfg.App.SingleUser { // Ensure handle matches user-chosen one on single-user blogs if username != c.Alias { From 36b160b706c766ed2762019ad7fdad99cb330e65 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 14 Jun 2019 19:11:03 -0400 Subject: [PATCH 19/23] Add TODO for multierror --- app.go | 1 + key/key.go | 1 + 2 files changed, 2 insertions(+) diff --git a/app.go b/app.go index a0864c4..8aeedce 100644 --- a/app.go +++ b/app.go @@ -496,6 +496,7 @@ func GenerateKeyFiles(app *App) error { // Generate keys initKeyPaths(app) + // TODO: use something like https://github.com/hashicorp/go-multierror to return errors var keyErrs error err := generateKey(emailKeyPath) if err != nil { diff --git a/key/key.go b/key/key.go index 14b0784..1cb3cf8 100644 --- a/key/key.go +++ b/key/key.go @@ -27,6 +27,7 @@ type Keychain struct { // skipping any that already exist. func (keys *Keychain) GenerateKeys() error { // Generate keys only if they don't already exist + // TODO: use something like https://github.com/hashicorp/go-multierror to return errors var err, keyErrs error if len(keys.EmailKey) == 0 { keys.EmailKey, err = GenerateBytes(EncKeysBytes) From 872ec4809b71833d38953d818473339fcec950cb Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 14 Jun 2019 19:50:41 -0400 Subject: [PATCH 20/23] Tweak status wording And fix a typo Ref T555 --- scripts/upgrade-server.sh | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/upgrade-server.sh b/scripts/upgrade-server.sh index 039fde8..e02945d 100755 --- a/scripts/upgrade-server.sh +++ b/scripts/upgrade-server.sh @@ -29,7 +29,7 @@ if [[ `id -u` -ne 0 ]]; then fi # go ahead and check for the latest release on linux -echo "Checking for updates.." +echo "Checking for updates..." url=`curl -s https://api.github.com/repos/writeas/writefreely/releases/latest | grep 'browser_' | grep linux | cut -d\" -f4` @@ -59,37 +59,37 @@ tempdir=$(mktemp -d) if [[ ${lv[0]} -gt ${cv[0]} ]]; then - echo "New major version" - echo "Downloading.." + echo "New major version available." + echo "Downloading..." `wget -P $tempdir -q --show-progress $url` elif [[ ${lv[0]} -eq ${cv[0]} ]] && [[ ${lv[1]} -gt ${cv[1]} ]]; then - echo "New minor version" - echo "Downloading.." + echo "New minor version available." + echo "Downloading..." `wget -P $tempdir -q --show-progress $url` elif [[ ${lv[2]} -gt ${cv[2]} ]]; then - echo "New patch version" - echo "Downloading.." + echo "New patch version available." + echo "Downloading..." `wget -P $tempdir -q --show-progress $url` else - echo "Nothing to change" + echo "Up to date." exit 0 fi filename=${parts[-1]} # extract -echo "Extracing files.." +echo "Extracting files..." tar -zxf $tempdir/$filename -C $tempdir # copy files -echo "Copying files.." +echo "Copying files..." cp -r $tempdir/{pages,static,templates,writefreely} . # restart service -echo "Restarting writefreely systemd service.." +echo "Restarting writefreely systemd service..." if `systemctl restart writefreely`; then echo "Success, version has been upgraded to $latest." else - echo "Upgrade complete, but failed to restart service" + echo "Upgrade complete, but failed to restart service." exit 1 fi From 075f25b829dd0ad6599dac1026ac15c6375e534d Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 14 Jun 2019 17:26:56 -0700 Subject: [PATCH 21/23] fix: update script: non-standard version numbers store version output in new variable slice output indexed from beginning instead of end. allowing for custom build version numbers. also fix weird spacing from tabs being two spaces wide, sorry :) --- scripts/upgrade-server.sh | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/scripts/upgrade-server.sh b/scripts/upgrade-server.sh index e02945d..c8e004a 100755 --- a/scripts/upgrade-server.sh +++ b/scripts/upgrade-server.sh @@ -1,23 +1,23 @@ #! /bin/bash ############################################################################### -## writefreely update script ## -## ## -## WARNING: running this script will overwrite any modifed assets or ## -## template files. If you have any custom changes to these files you ## -## should back them up FIRST. ## -## ## -## This must be run from the web application root directory ## -## i.e. /var/www/writefreely, and operates under the assumption that you ## -## have not installed the binary `writefreely` in another location. ## +## writefreely update script ## +## ## +## WARNING: running this script will overwrite any modifed assets or ## +## template files. If you have any custom changes to these files you ## +## should back them up FIRST. ## +## ## +## This must be run from the web application root directory ## +## i.e. /var/www/writefreely, and operates under the assumption that you## +## have not installed the binary `writefreely` in another location. ## ############################################################################### # -# Copyright © 2019 A Bunch Tell LLC. +# Copyright © 2019 A Bunch Tell LLC. # -# This file is part of WriteFreely. +# 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. +# 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. # @@ -35,12 +35,13 @@ url=`curl -s https://api.github.com/repos/writeas/writefreely/releases/latest | # check current version -current=`./writefreely -v` -if [ -z "$current" ]; then +bin_output=`./writefreely -v` +if [ -z "$bin_output" ]; then exit 1 fi -echo "Current version is v${current:(-5):5}" +current=${bin_output:12:5} +echo "Current version is v$current" # grab latest version number IFS='/' @@ -51,7 +52,7 @@ echo "Latest release is $latest" IFS='.' -read -ra cv <<< "${current:(-5):5}" +read -ra cv <<< "$current" read -ra lv <<< "${latest#v}" IFS=' ' From 161f7a8de29892d721cdb5f40197ae0dc4583503 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 16 Jun 2019 17:38:34 -0400 Subject: [PATCH 22/23] Support changing landing conf val from Admin UI Closes T651 --- admin.go | 1 + templates/user/admin.tmpl | 2 ++ 2 files changed, 3 insertions(+) diff --git a/admin.go b/admin.go index 696d4eb..7d21abd 100644 --- a/admin.go +++ b/admin.go @@ -350,6 +350,7 @@ func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Req func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error { apper.App().cfg.App.SiteName = r.FormValue("site_name") apper.App().cfg.App.SiteDesc = r.FormValue("site_desc") + apper.App().cfg.App.Landing = r.FormValue("landing") apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on" mul, err := strconv.Atoi(r.FormValue("min_username_len")) if err == nil { diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl index 5ce11a2..90b5d70 100644 --- a/templates/user/admin.tmpl +++ b/templates/user/admin.tmpl @@ -71,6 +71,8 @@ p.docs {
{{.Config.Host}}
User Mode
{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}
+