Merge branch 'develop' into modestly-writefreely

This commit is contained in:
Matt Baer 2019-08-07 18:02:54 -04:00 committed by GitHub
commit 1c9438e305
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 246 additions and 108 deletions

View File

@ -163,7 +163,7 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
}
// Create actual user
if err := app.db.CreateUser(u, desiredUsername); err != nil {
if err := app.db.CreateUser(app.cfg, u, desiredUsername); err != nil {
return nil, err
}

View File

@ -391,6 +391,7 @@ func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *htt
if apper.App().cfg.App.UserInvites == "none" {
apper.App().cfg.App.UserInvites = ""
}
apper.App().cfg.App.DefaultVisibility = r.FormValue("default_visibility")
m := "?cm=Configuration+saved."
err = apper.SaveConfig(apper.App().cfg)

68
app.go
View File

@ -11,6 +11,7 @@
package writefreely
import (
"crypto/tls"
"database/sql"
"fmt"
"html/template"
@ -39,6 +40,7 @@ import (
"github.com/writeas/writefreely/key"
"github.com/writeas/writefreely/migrations"
"github.com/writeas/writefreely/page"
"golang.org/x/crypto/acme/autocert"
)
const (
@ -118,6 +120,8 @@ type Apper interface {
SaveConfig(*config.Config) error
LoadKeys() error
ReqLog(r *http.Request, status int, timeSince time.Duration) string
}
// App returns the App
@ -177,6 +181,10 @@ func (app *App) LoadKeys() error {
return nil
}
func (app *App) ReqLog(r *http.Request, status int, timeSince time.Duration) string {
return fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, timeSince, r.UserAgent())
}
// 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 {
@ -380,19 +388,55 @@ func Serve(app *App, r *mux.Router) {
}
var err error
if app.cfg.IsSecureStandalone() {
log.Info("Serving redirects on http://%s:80", bindAddress)
go func() {
err = http.ListenAndServe(
fmt.Sprintf("%s:80", bindAddress), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if app.cfg.Server.Autocert {
m := &autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(app.cfg.Server.TLSCertPath),
}
host, err := url.Parse(app.cfg.App.Host)
if err != nil {
log.Error("[WARNING] Unable to parse configured host! %s", err)
log.Error(`[WARNING] ALL hosts are allowed, which can open you to an attack where
clients connect to a server by IP address and pretend to be asking for an
incorrect host name, and cause you to reach the CA's rate limit for certificate
requests. We recommend supplying a valid host name.`)
log.Info("Using autocert on ANY host")
} else {
log.Info("Using autocert on host %s", host.Host)
m.HostPolicy = autocert.HostWhitelist(host.Host)
}
s := &http.Server{
Addr: ":https",
Handler: r,
TLSConfig: &tls.Config{
GetCertificate: m.GetCertificate,
},
}
s.SetKeepAlivesEnabled(false)
go func() {
log.Info("Serving redirects on http://%s:80", bindAddress)
err = http.ListenAndServe(":80", m.HTTPHandler(nil))
log.Error("Unable to start redirect server: %v", err)
}()
log.Info("Serving on https://%s:443", bindAddress)
log.Info("---")
err = s.ListenAndServeTLS("", "")
} else {
go func() {
log.Info("Serving redirects on http://%s:80", bindAddress)
err = http.ListenAndServe(fmt.Sprintf("%s:80", bindAddress), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, app.cfg.App.Host, http.StatusMovedPermanently)
}))
log.Error("Unable to start redirect server: %v", err)
}()
log.Error("Unable to start redirect server: %v", err)
}()
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, r)
log.Info("Serving on https://%s:443", bindAddress)
log.Info("Using manual certificates")
log.Info("---")
err = http.ListenAndServeTLS(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("---")
@ -507,7 +551,7 @@ func DoConfig(app *App, configSections string) {
// Create blog
log.Info("Creating user %s...\n", u.Username)
err = app.db.CreateUser(u, app.cfg.App.SiteName)
err = app.db.CreateUser(app.cfg, u, app.cfg.App.SiteName)
if err != nil {
log.Error("Unable to create user: %s", err)
os.Exit(1)
@ -702,7 +746,7 @@ func CreateUser(apper Apper, username, password string, isAdmin bool) error {
userType = "admin"
}
log.Info("Creating %s %s...", userType, usernameDesc)
err = apper.App().db.CreateUser(u, desiredUsername)
err = apper.App().db.CreateUser(apper.App().Config(), u, desiredUsername)
if err != nil {
return fmt.Errorf("Unable to create user: %s", err)
}

View File

@ -31,6 +31,7 @@ import (
"github.com/writeas/web-core/log"
waposts "github.com/writeas/web-core/posts"
"github.com/writeas/writefreely/author"
"github.com/writeas/writefreely/config"
"github.com/writeas/writefreely/page"
)
@ -126,6 +127,21 @@ const (
CollProtected
)
var collVisibilityStrings = map[string]collVisibility{
"unlisted": CollUnlisted,
"public": CollPublic,
"private": CollPrivate,
"protected": CollProtected,
}
func defaultVisibility(cfg *config.Config) collVisibility {
vis, ok := collVisibilityStrings[cfg.App.DefaultVisibility]
if !ok {
vis = CollUnlisted
}
return vis
}
func (cf *CollectionFormat) Ascending() bool {
return cf.Format == "novel"
}
@ -211,6 +227,10 @@ func (c *Collection) DisplayCanonicalURL() string {
}
func (c *Collection) RedirectingCanonicalURL(isRedir bool) string {
if c.hostName == "" {
// If this is true, the human programmers screwed up. So ask for a bug report and fail, fail, fail
log.Error("[PROGRAMMER ERROR] WARNING: Collection.hostName is empty! Federation and many other things will fail! If you're seeing this in the wild, please report this bug and let us know what you were doing just before this: https://github.com/writeas/writefreely/issues/new?template=bug_report.md")
}
if isSingleUser {
return c.hostName + "/"
}
@ -358,36 +378,32 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
return impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Parameter(s) %srequired.", missingParams)}
}
var userID int64
if reqJSON && !c.Web {
accessToken = r.Header.Get("Authorization")
if accessToken == "" {
return ErrNoAccessToken
}
userID = app.db.GetUserID(accessToken)
if userID == -1 {
return ErrBadAccessToken
}
} else {
u = getUserSession(app, r)
if u == nil {
return ErrNotLoggedIn
}
userID = u.ID
}
if !author.IsValidUsername(app.cfg, c.Alias) {
return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."}
}
var coll *Collection
var err error
if accessToken != "" {
coll, err = app.db.CreateCollectionFromToken(c.Alias, c.Title, accessToken)
if err != nil {
// TODO: handle this
return err
}
} else {
coll, err = app.db.CreateCollection(c.Alias, c.Title, u.ID)
if err != nil {
// TODO: handle this
return err
}
coll, err := app.db.CreateCollection(app.cfg, c.Alias, c.Title, userID)
if err != nil {
// TODO: handle this
return err
}
res := &CollectionObj{Collection: *coll}

View File

@ -35,6 +35,7 @@ type (
TLSCertPath string `ini:"tls_cert_path"`
TLSKeyPath string `ini:"tls_key_path"`
Autocert bool `ini:"autocert"`
TemplatesParentDir string `ini:"templates_parent_dir"`
StaticParentDir string `ini:"static_parent_dir"`
@ -84,6 +85,9 @@ type (
// Additional functions
LocalTimeline bool `ini:"local_timeline"`
UserInvites string `ini:"user_invites"`
// Defaults
DefaultVisibility string `ini:"default_visibility"`
}
// Config holds the complete configuration for running a writefreely instance

View File

@ -101,39 +101,48 @@ func Configure(fname string, configSections string) (*SetupData, error) {
selPrompt = promptui.Select{
Templates: selTmpls,
Label: "Web server mode",
Items: []string{"Insecure (port 80)", "Secure (port 443)"},
Items: []string{"Insecure (port 80)", "Secure (port 443), manual certificate", "Secure (port 443), auto certificate"},
}
sel, _, err := selPrompt.Run()
if err != nil {
return data, err
}
if sel == 0 {
data.Config.Server.Autocert = false
data.Config.Server.Port = 80
data.Config.Server.TLSCertPath = ""
data.Config.Server.TLSKeyPath = ""
} else if sel == 1 {
} else if sel == 1 || sel == 2 {
data.Config.Server.Port = 443
data.Config.Server.Autocert = sel == 2
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Certificate path",
Validate: validateNonEmpty,
Default: data.Config.Server.TLSCertPath,
}
data.Config.Server.TLSCertPath, err = prompt.Run()
if err != nil {
return data, err
}
if sel == 1 {
// Manual certificate configuration
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Certificate path",
Validate: validateNonEmpty,
Default: data.Config.Server.TLSCertPath,
}
data.Config.Server.TLSCertPath, err = prompt.Run()
if err != nil {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Key path",
Validate: validateNonEmpty,
Default: data.Config.Server.TLSKeyPath,
}
data.Config.Server.TLSKeyPath, err = prompt.Run()
if err != nil {
return data, err
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Key path",
Validate: validateNonEmpty,
Default: data.Config.Server.TLSKeyPath,
}
data.Config.Server.TLSKeyPath, err = prompt.Run()
if err != nil {
return data, err
}
} else {
// Automatic certificate
data.Config.Server.TLSCertPath = "certs"
data.Config.Server.TLSKeyPath = "certs"
}
}
} else {

View File

@ -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/config"
"github.com/writeas/writefreely/key"
)
@ -44,7 +45,7 @@ var (
)
type writestore interface {
CreateUser(*User, string) error
CreateUser(*config.Config, *User, string) error
UpdateUserEmail(keys *key.Keychain, userID int64, email string) error
UpdateEncryptedUserEmail(int64, []byte) error
GetUserByID(int64) (*User, error)
@ -73,7 +74,7 @@ type writestore interface {
GetAnonymousPosts(u *User) (*[]PublicPost, error)
GetUserPosts(u *User) (*[]PublicPost, error)
CreateOwnedPost(post *SubmittedPost, accessToken, collAlias string) (*PublicPost, error)
CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error)
CreatePost(userID, collID int64, post *SubmittedPost) (*Post, error)
UpdateOwnedPost(post *AuthenticatedPost, userID int64) error
GetEditablePost(id, editToken string) (*PublicPost, error)
@ -82,8 +83,8 @@ type writestore interface {
GetOwnedPost(id string, ownerID int64) (*PublicPost, error)
GetPostProperty(id string, collectionID int64, property string) (interface{}, error)
CreateCollectionFromToken(string, string, string) (*Collection, error)
CreateCollection(string, string, int64) (*Collection, error)
CreateCollectionFromToken(*config.Config, string, string, string) (*Collection, error)
CreateCollection(*config.Config, string, string, int64) (*Collection, error)
GetCollectionBy(condition string, value interface{}) (*Collection, error)
GetCollection(alias string) (*Collection, error)
GetCollectionForPad(alias string) (*Collection, error)
@ -102,7 +103,7 @@ type writestore interface {
CanCollect(cpr *ClaimPostRequest, userID int64) bool
AttemptClaim(p *ClaimPostRequest, query string, params []interface{}, slugIdx int) (sql.Result, error)
DispersePosts(userID int64, postIDs []string) (*[]ClaimPostResult, error)
ClaimPosts(userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error)
ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error)
GetPostsCount(c *CollectionObj, includeFuture bool)
GetPosts(c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error)
@ -162,7 +163,7 @@ func (db *datastore) dateSub(l int, unit string) string {
return fmt.Sprintf("DATE_SUB(NOW(), INTERVAL %d %s)", l, unit)
}
func (db *datastore) CreateUser(u *User, collectionTitle string) error {
func (db *datastore) CreateUser(cfg *config.Config, u *User, collectionTitle string) error {
if db.PostIDExists(u.Username) {
return impart.HTTPError{http.StatusConflict, "Invalid collection name."}
}
@ -196,7 +197,7 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error {
if collectionTitle == "" {
collectionTitle = u.Username
}
res, err = t.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", u.Username, collectionTitle, "", CollUnlisted, u.ID, 0)
res, err = t.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", u.Username, collectionTitle, "", defaultVisibility(cfg), u.ID, 0)
if err != nil {
t.Rollback()
if db.isDuplicateKeyErr(err) {
@ -238,13 +239,13 @@ func (db *datastore) UpdateEncryptedUserEmail(userID int64, encEmail []byte) err
return nil
}
func (db *datastore) CreateCollectionFromToken(alias, title, accessToken string) (*Collection, error) {
func (db *datastore) CreateCollectionFromToken(cfg *config.Config, alias, title, accessToken string) (*Collection, error) {
userID := db.GetUserID(accessToken)
if userID == -1 {
return nil, ErrBadAccessToken
}
return db.CreateCollection(alias, title, userID)
return db.CreateCollection(cfg, alias, title, userID)
}
func (db *datastore) GetUserCollectionCount(userID int64) (uint64, error) {
@ -261,13 +262,13 @@ func (db *datastore) GetUserCollectionCount(userID int64) (uint64, error) {
return collCount, nil
}
func (db *datastore) CreateCollection(alias, title string, userID int64) (*Collection, error) {
func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, userID int64) (*Collection, error) {
if db.PostIDExists(alias) {
return nil, impart.HTTPError{http.StatusConflict, "Invalid collection name."}
}
// All good, so create new collection
res, err := db.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", alias, title, "", CollUnlisted, userID, 0)
res, err := db.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", alias, title, "", defaultVisibility(cfg), userID, 0)
if err != nil {
if db.isDuplicateKeyErr(err) {
return nil, impart.HTTPError{http.StatusConflict, "Collection already exists."}
@ -281,6 +282,7 @@ func (db *datastore) CreateCollection(alias, title string, userID int64) (*Colle
Title: title,
OwnerID: userID,
PublicOwner: false,
Public: defaultVisibility(cfg) == CollPublic,
}
c.ID, err = res.LastInsertId()
@ -541,7 +543,7 @@ func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int,
return u.String(), nil
}
func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias string) (*PublicPost, error) {
func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error) {
var userID, collID int64 = -1, -1
var coll *Collection
var err error
@ -555,6 +557,7 @@ func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias
if err != nil {
return nil, err
}
coll.hostName = hostName
if coll.OwnerID != userID {
return nil, ErrForbiddenCollection
}
@ -1324,7 +1327,7 @@ func (db *datastore) DispersePosts(userID int64, postIDs []string) (*[]ClaimPost
return &res, nil
}
func (db *datastore) ClaimPosts(userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error) {
func (db *datastore) ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error) {
postClaimReqs := map[string]bool{}
res := []ClaimPostResult{}
postCollAlias := collAlias
@ -1381,7 +1384,7 @@ func (db *datastore) ClaimPosts(userID int64, collAlias string, posts *[]ClaimPo
// This is a new collection
// TODO: consider removing this. This seriously complicates this
// method and adds another (unnecessary?) logic path.
coll, err = db.CreateCollection(postCollAlias, "", userID)
coll, err = db.CreateCollection(cfg, postCollAlias, "", userID)
if err != nil {
if err, ok := err.(impart.HTTPError); ok {
r.Code = err.Status

3
go.mod
View File

@ -6,13 +6,11 @@ require (
github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966 // indirect
github.com/alecthomas/gometalinter v3.0.0+incompatible // indirect
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 // indirect
github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.7.0
github.com/fatih/structs v1.1.0 // indirect
github.com/go-macaron/cache v0.0.0-20151013081102-561735312776 // indirect
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 // indirect
github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193 // indirect
@ -31,6 +29,7 @@ require (
github.com/gorilla/sessions v1.1.3
github.com/guregu/null v3.4.0+incompatible
github.com/ikeikeikeike/go-sitemap-generator v1.0.1
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2
github.com/imdario/mergo v0.3.7 // indirect
github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect

3
go.sum
View File

@ -21,6 +21,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/mxj v1.8.3/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
@ -81,6 +82,8 @@ github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfF
github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
github.com/ikeikeikeike/go-sitemap-generator v1.0.1 h1:49Fn8gro/B12vCY8pf5/+/Jpr3kwB9TvP0MSymo69SY=
github.com/ikeikeikeike/go-sitemap-generator v1.0.1/go.mod h1:QI+zWsz6yQyxkG9LWNcnu0f7aiAE5tPdsZOsICgmd1c=
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 h1:wIdDEle9HEy7vBPjC6oKz6ejs3Ut+jmsYvuOoAW2pSM=
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2/go.mod h1:WtaVKD9TeruTED9ydiaOJU08qGoEPP/LyzTKiD3jEsw=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts=

View File

@ -137,7 +137,7 @@ func (h *Handler) User(f userHandlerFunc) http.HandlerFunc {
status = http.StatusInternalServerError
}
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
u := getUserSession(h.app.App(), r)
@ -175,7 +175,7 @@ func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc {
status = http.StatusInternalServerError
}
log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()))
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
u := getUserSession(h.app.App(), r)
@ -213,7 +213,7 @@ func (h *Handler) AdminApper(f userApperHandlerFunc) http.HandlerFunc {
status = http.StatusInternalServerError
}
log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()))
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
u := getUserSession(h.app.App(), r)
@ -295,7 +295,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF
status = 500
}
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
u, err := a(h.app.App(), r)
@ -381,7 +381,7 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevelFunc) http.HandlerFunc {
status = 500
}
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
var session *sessions.Session
@ -440,7 +440,7 @@ func (h *Handler) CollectionPostOrStatic(w http.ResponseWriter, r *http.Request)
start := time.Now()
status := 200
defer func() {
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
// Serve static file
@ -472,7 +472,7 @@ func (h *Handler) Web(f handlerFunc, ul UserLevelFunc) http.HandlerFunc {
status = 500
}
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
if ul(h.app.App().cfg) != UserLevelNoneType {
@ -530,7 +530,7 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc {
status = 500
}
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
// TODO: do any needed authentication
@ -562,7 +562,7 @@ func (h *Handler) AllReader(f handlerFunc) http.HandlerFunc {
status = 500
}
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
if h.app.App().cfg.App.Private {
@ -619,7 +619,7 @@ func (h *Handler) Download(f dataHandlerFunc, ul UserLevelFunc) http.HandlerFunc
status = 500
}
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
data, filename, err := f(h.app.App(), w, r)
@ -682,7 +682,7 @@ func (h *Handler) Redirect(url string, ul UserLevelFunc) http.HandlerFunc {
status = sendRedirect(w, http.StatusFound, url)
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
return nil
}())
@ -721,6 +721,10 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er
return
} else if err.Status == http.StatusNotFound {
w.WriteHeader(err.Status)
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
// This is a fediverse request; simply return the header
return
}
h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
return
} else if err.Status == http.StatusInternalServerError {
@ -799,7 +803,7 @@ func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc {
}
// TODO: log actual status code returned
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
log.Info(h.app.ReqLog(r, status, time.Since(start)))
}()
if h.app.App().cfg.App.Private {

View File

@ -108,9 +108,13 @@ body {
code {
.article-code;
}
img, video {
img, video, audio {
max-width: 100%;
}
audio {
width: 100%;
white-space: initial;
}
pre {
.code-block;

View File

@ -156,10 +156,12 @@ func friendlyPostTitle(content, friendlyId string) string {
func getSanitizationPolicy() *bluemonday.Policy {
policy := bluemonday.UGCPolicy()
policy.AllowAttrs("src", "style").OnElements("iframe", "video")
policy.AllowAttrs("src", "style").OnElements("iframe", "video", "audio")
policy.AllowAttrs("src", "type").OnElements("source")
policy.AllowAttrs("frameborder", "width", "height").Matching(bluemonday.Integer).OnElements("iframe")
policy.AllowAttrs("allowfullscreen").OnElements("iframe")
policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video")
policy.AllowAttrs("controls", "loop", "muted", "autoplay", "preload").OnElements("audio")
policy.AllowAttrs("target").OnElements("a")
policy.AllowAttrs("style", "class", "id").Globally()
policy.AllowURLSchemes("http", "https", "mailto", "xmpp")

View File

@ -556,7 +556,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
var coll *Collection
var err error
if accessToken != "" {
newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias)
newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host)
} else {
//return ErrNotLoggedIn
// TODO: verify user is logged in
@ -869,7 +869,7 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
collAlias := vars["alias"]
// Update all given posts
res, err := app.db.ClaimPosts(ownerID, collAlias, claims)
res, err := app.db.ClaimPosts(app.cfg, ownerID, collAlias, claims)
if err != nil {
return err
}
@ -1295,14 +1295,32 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
coll.Owner = owner
}
postFound := true
p, err := app.db.GetPost(slug, coll.ID)
if err != nil {
if err == ErrCollectionPageNotFound && slug == "feed" {
// User tried to access blog feed without a trailing slash, and
// there's no post with a slug "feed"
return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "/feed/"}
if err == ErrCollectionPageNotFound {
postFound = false
if slug == "feed" {
// User tried to access blog feed without a trailing slash, and
// there's no post with a slug "feed"
return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "/feed/"}
}
po := &Post{
Slug: null.NewString(slug, true),
Font: "norm",
Language: zero.NewString("en", true),
RTL: zero.NewBool(false, true),
Content: `<p class="msg">This page is missing.</p>
Are you sure it was ever here?`,
}
pp := po.processPost()
p = &pp
} else {
return err
}
return err
}
p.IsOwner = owner != nil && p.OwnerID.Valid && owner.ID == p.OwnerID.Int64
p.Collection = coll
@ -1324,11 +1342,20 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
contentType = "text/markdown"
}
w.Header().Set("Content-Type", fmt.Sprintf("%s; charset=utf-8", contentType))
if !postFound {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Post not found.")
// TODO: return error instead, so status is correctly reflected in logs
return nil
}
if isMarkdown && p.Title.String != "" {
fmt.Fprintf(w, "# %s\n\n", p.Title.String)
}
fmt.Fprint(w, p.Content)
} else if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
if !postFound {
return ErrCollectionPageNotFound
}
p.extractData()
ap := p.ActivityObject()
ap.Context = []interface{}{activitystreams.Namespace}
@ -1345,14 +1372,20 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
IsPinned bool
IsCustomDomain bool
PinnedPosts *[]PublicPost
IsFound bool
}{
PublicPost: p,
StaticPage: pageForReq(app, r),
IsOwner: cr.isCollOwner,
IsCustomDomain: cr.isCustomDomain,
IsFound: postFound,
}
tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll)
tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p)
if !postFound {
w.WriteHeader(http.StatusNotFound)
}
if err := templates["collection-post"].ExecuteTemplate(w, "post", tp); err != nil {
log.Error("Error in collection-post template: %v", err)
}

View File

@ -12,15 +12,16 @@ package writefreely
import (
"fmt"
"github.com/gorilla/mux"
"github.com/ikeikeikeike/go-sitemap-generator/stm"
"github.com/writeas/web-core/log"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/ikeikeikeike/go-sitemap-generator/v2/stm"
"github.com/writeas/web-core/log"
)
func buildSitemap(host, alias string) *stm.Sitemap {
sm := stm.NewSitemap()
sm := stm.NewSitemap(0)
sm.SetDefaultHost(host)
if alias != "/" {
sm.SetSitemapsPath(alias)
@ -76,27 +77,30 @@ func handleViewSitemap(app *App, w http.ResponseWriter, r *http.Request) error {
lastSiteMod = p.Updated
}
u := stm.URL{
"loc": p.Slug.String,
"changefreq": "weekly",
"mobile": true,
"lastmod": p.Updated,
{"loc", p.Slug.String},
{"changefreq", "weekly"},
{"mobile", true},
{"lastmod", p.Updated},
}
if len(p.Images) > 0 {
imgs := []stm.URL{}
for _, i := range p.Images {
imgs = append(imgs, stm.URL{"loc": i, "title": ""})
imgs = append(imgs, stm.URL{
{"loc", i},
{"title", ""},
})
}
u["image"] = imgs
u = append(u, []interface{}{"image", imgs})
}
sm.Add(u)
}
// Add top URL
sm.Add(stm.URL{
"loc": pre,
"changefreq": "daily",
"priority": "1.0",
"lastmod": lastSiteMod,
{"loc", pre},
{"changefreq", "daily"},
{"priority", "1.0"},
{"lastmod", lastSiteMod},
})
w.Write(sm.XMLContent())

View File

@ -8,6 +8,7 @@
<link rel="stylesheet" type="text/css" href="/css/write.css" />
<link rel="shortcut icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{ if .IsFound }}
<link rel="canonical" href="{{.CanonicalURL}}" />
<meta name="generator" content="WriteFreely">
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
@ -29,6 +30,7 @@
<meta property="og:updated_time" content="{{.Created8601}}" />
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
<meta property="article:published_time" content="{{.Created8601}}">
{{ end }}
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
{{if .Collection.RenderMathJax}}
@ -50,14 +52,14 @@
{{if .PinnedPosts}}
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
{{end}}
{{ if .IsOwner }}<span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span>
{{ if and .IsOwner .IsFound }}<span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span>
<a class="xtra-feature" href="/{{if not .SingleUser}}{{.Collection.Alias}}/{{end}}{{.Slug.String}}/edit" dir="{{.Direction}}">Edit</a>
{{if .IsPinned}}<a class="xtra-feature unpin" href="/{{.Collection.Alias}}/{{.Slug.String}}/unpin" dir="{{.Direction}}" onclick="unpinPost(event, '{{.ID}}')">Unpin</a>{{end}}
{{ end }}
</nav>
</header>
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
<article id="post-body" class="{{.Font}} h-entry {{if not .IsFound}}error-page{{end}}">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
{{ if .Collection.ShowFooterBranding }}
<footer dir="ltr"><hr><nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language.String}}</p></nav></footer>

View File

@ -141,7 +141,7 @@ function pinPost(e, postID, slug, title) {
var $header = document.getElementsByTagName('header')[0];
var $pinnedNavs = $header.getElementsByTagName('nav');
// Add link to nav
var link = '<a class="pinned" href="/{{.Alias}}/'+slug+'">'+title+'</a>';
var link = '<a class="pinned" href="{{if not .SingleUser}}/{{.Alias}}/{{end}}'+slug+'">'+title+'</a>';
if ($pinnedNavs.length == 0) {
$header.insertAdjacentHTML("beforeend", '<nav>'+link+'</nav>');
} else {

View File

@ -176,7 +176,7 @@ function pinPost(e, postID, slug, title) {
var $header = document.getElementsByTagName('header')[0];
var $pinnedNavs = $header.getElementsByTagName('nav');
// Add link to nav
var link = '<a class="pinned" href="/{{.Alias}}/'+slug+'">'+title+'</a>';
var link = '<a class="pinned" href="{{if not .SingleUser}}/{{.Alias}}/{{end}}'+slug+'">'+title+'</a>';
if ($pinnedNavs.length == 0) {
$header.insertAdjacentHTML("beforeend", '<nav>'+link+'</nav>');
} else {

View File

@ -95,6 +95,14 @@ p.docs {
<option value="admin" {{if eq .Config.UserInvites "admin"}}selected="selected"{{end}}>Admins</option>
</select>
</dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="default_visibility">Default blog visibility</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}>
<select name="default_visibility" id="default_visibility">
<option value="unlisted" {{if eq .Config.DefaultVisibility "unlisted"}}selected="selected"{{end}}>Unlisted</option>
<option value="public" {{if eq .Config.DefaultVisibility "public"}}selected="selected"{{end}}>Public</option>
<option value="private" {{if eq .Config.DefaultVisibility "private"}}selected="selected"{{end}}>Private</option>
</select>
</dd>
</dl>
<input type="submit" value="Save Configuration" />
</div>

View File

@ -34,8 +34,8 @@
{{if .Summary}}<p>{{.Summary}}</p>{{end}}
</div>{{end}}
</div>{{ else }}<div id="no-posts-published"><p>You haven't saved any drafts yet.</p>
<p>They'll show up here once you do. Find your blog posts from the <a href="/me/c/">Blogs</a> page.</p>
<p class="text-cta"><a href="/">Start writing</a></p></div>{{ end }}
<p>They'll show up here once you do. {{if not .SingleUser}}Find your blog posts from the <a href="/me/c/">Blogs</a> page.{{end}}</p>
<p class="text-cta"><a href="{{if .SingleUser}}/me/new{{else}}/{{end}}">Start writing</a></p></div>{{ end }}
<div id="moving"></div>

View File

@ -58,6 +58,7 @@
</label>
<p>A password is required to read this blog.</p>
</li>
{{if not .SingleUser}}
<li>
<label class="option-text{{if not .LocalTimeline}} disabled{{end}}"><input type="radio" name="visibility" id="visibility-public" value="1" {{if .IsPublic}}checked="checked"{{end}} {{if not .LocalTimeline}}disabled="disabled"{{end}} />
Public
@ -65,6 +66,7 @@
{{if .LocalTimeline}}<p>This blog is displayed on the public <a href="/read">reader</a>, and is visible to {{if .Private}}any registered user on this instance{{else}}anyone with its link{{end}}.</p>
{{else}}<p>The public reader is currently turned off for this community.</p>{{end}}
</li>
{{end}}
</ul>
</div>
</div>

View File

@ -8,11 +8,11 @@
<hr />
<nav>
<a class="home" href="/">{{.SiteName}}</a>
<a href="/about">about</a>
{{if not .SingleUser}}<a href="/about">about</a>{{end}}
{{if and (not .SingleUser) .LocalTimeline}}<a href="/read">reader</a>{{end}}
<a href="https://writefreely.org/guide/{{.OfficialVersion}}" target="guide">writer's guide</a>
<a href="/privacy">privacy</a>
{{if .WFModesty}}
{{if not .SingleUser}}<a href="/privacy">privacy</a>{{end}}
{{if .WFModesty}}
<p style="font-size: 0.9em">powered by <a href="https://writefreely.org">writefreely</a></p>
{{else}}
<a href="https://writefreely.org">writefreely {{.Version}}</a>