Merge branch 'develop' into modestly-writefreely
This commit is contained in:
commit
1c9438e305
|
@ -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
|
||||
}
|
||||
|
||||
|
|
1
admin.go
1
admin.go
|
@ -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
68
app.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
31
database.go
31
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/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
3
go.mod
|
@ -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
3
go.sum
|
@ -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=
|
||||
|
|
28
handle.go
28
handle.go
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
47
posts.go
47
posts.go
|
@ -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)
|
||||
}
|
||||
|
|
32
sitemap.go
32
sitemap.go
|
@ -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())
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue