Scheduled
{{end}}{{if .Title.String}}diff --git a/README.md b/README.md index 4f0b6bb..68da89b 100644 --- a/README.md +++ b/README.md @@ -47,15 +47,15 @@ It's designed to be flexible and share your writing widely, so it's built around ## Hosting -We offer two kinds of hosting services that make WriteFreely deployment painless: [Write.as](https://write.as) for individuals, and [WriteFreely.host](https://writefreely.host) for communities. Besides saving you time, as a customer you directly help fund WriteFreely development. +We offer two kinds of hosting services that make WriteFreely deployment painless: [Write.as Pro](https://write.as/pro) for individuals, and [Write.as for Teams](https://write.as/for/teams) for businesses. Besides saving you time and effort, both services directly fund WriteFreely development and ensure the long-term sustainability of our open source work. -### [![Write.as](https://write.as/img/writeas-wf-readme.png)](https://write.as/) +### [![Write.as Pro](https://writefreely.org/img/writeas-pro-readme.png)](https://write.as/pro) -Start a personal blog on [Write.as](https://write.as), our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. [Read more here](https://write.as/pricing). +Start a personal blog on [Write.as](https://write.as), our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. [Read more here](https://write.as/pro). -### [![WriteFreely.host](https://writefreely.host/img/wfhost-wf-readme.png)](https://writefreely.host) +### [![Write.as for Teams](https://writefreely.org/img/writeas-for-teams-readme.png)](https://write.as/for/teams) -[WriteFreely.host](https://writefreely.host) makes it easy to start a close-knit community — to share knowledge, complement your Mastodon instance, or publish updates in your organization. We take care of the hosting, upgrades, backups, and maintenance so you can focus on writing. +[Write.as for Teams](https://write.as/for/teams) gives your organization, business, or [open source project](https://write.as/for/open-source) a clutter-free space to share updates or proposals and build your collective knowledge. We take care of hosting, upgrades, backups, and maintenance so your team can focus on writing. ## Quick start diff --git a/account.go b/account.go index 1cf259b..180e9b0 100644 --- a/account.go +++ b/account.go @@ -13,6 +13,13 @@ package writefreely import ( "encoding/json" "fmt" + "html/template" + "net/http" + "regexp" + "strings" + "sync" + "time" + "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/guregu/null/zero" @@ -21,13 +28,8 @@ import ( "github.com/writeas/web-core/data" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/author" + "github.com/writeas/writefreely/config" "github.com/writeas/writefreely/page" - "html/template" - "net/http" - "regexp" - "strings" - "sync" - "time" ) type ( @@ -58,11 +60,15 @@ func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []str up.Flashes = flashes up.Path = r.URL.Path up.IsAdmin = u.IsAdmin() - up.CanInvite = app.cfg.App.UserInvites != "" && - (up.IsAdmin || app.cfg.App.UserInvites != "admin") + up.CanInvite = canUserInvite(app.cfg, up.IsAdmin) return up } +func canUserInvite(cfg *config.Config, isAdmin bool) bool { + return cfg.App.UserInvites != "" && + (isAdmin || cfg.App.UserInvites != "admin") +} + func (up *UserPage) SetMessaging(u *User) { //up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID) } @@ -79,7 +85,7 @@ func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error { } func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { - reqJSON := IsJSON(r.Header.Get("Content-Type")) + reqJSON := IsJSON(r) // Get params var ur userRegistration @@ -114,7 +120,7 @@ func signup(app *App, 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")) + reqJSON := IsJSON(r) // Validate required params (alias) if signup.Alias == "" { @@ -304,10 +310,10 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { p := &struct { page.StaticPage - To string - Message template.HTML - Flashes []template.HTML - Username string + To string + Message template.HTML + Flashes []template.HTML + LoginUsername string }{ pageForReq(app, r), r.FormValue("to"), @@ -371,7 +377,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 { - reqJSON := IsJSON(r.Header.Get("Content-Type")) + reqJSON := IsJSON(r) oneTimeToken := r.FormValue("with") verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "") @@ -546,7 +552,7 @@ func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser if err != nil { log.Error("Login: Unable to get user posts: %v", err) } - colls, err := app.db.GetCollections(u) + colls, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("Login: Unable to get user collections: %v", err) } @@ -574,7 +580,7 @@ func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request 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")) + reqJSON := IsJSON(r) if reqJSON { // Use given Authorization header accessToken := r.Header.Get("Authorization") @@ -619,7 +625,7 @@ func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, // Export as CSV if strings.HasSuffix(r.URL.Path, ".csv") { - data = exportPostsCSV(u, posts) + data = exportPostsCSV(app.cfg.App.Host, u, posts) return data, filename, err } if strings.HasSuffix(r.URL.Path, ".zip") { @@ -656,7 +662,7 @@ func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, s } func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error { - reqJSON := IsJSON(r.Header.Get("Content-Type")) + reqJSON := IsJSON(r) uObj := struct { ID int64 `json:"id,omitempty"` Username string `json:"username,omitempty"` @@ -680,7 +686,7 @@ func viewMeAPI(app *App, 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")) + reqJSON := IsJSON(r) if !reqJSON { return ErrBadRequestedType } @@ -711,12 +717,12 @@ func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) e } func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error { - reqJSON := IsJSON(r.Header.Get("Content-Type")) + reqJSON := IsJSON(r) if !reqJSON { return ErrBadRequestedType } - p, err := app.db.GetCollections(u) + p, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { return err } @@ -739,7 +745,7 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err log.Error("unable to fetch flashes: %v", err) } - c, err := app.db.GetPublishableCollections(u) + c, err := app.db.GetPublishableCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } @@ -762,7 +768,7 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err } func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error { - c, err := app.db.GetCollections(u) + c, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) return fmt.Errorf("No collections") @@ -816,7 +822,7 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques } func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error { - reqJSON := IsJSON(r.Header.Get("Content-Type")) + reqJSON := IsJSON(r) var s userSettings var u *User diff --git a/activitypub.go b/activitypub.go index 997609d..80d484e 100644 --- a/activitypub.go +++ b/activitypub.go @@ -129,10 +129,10 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "outbox", res.TotalPosts, p) ocp.OrderedItems = []interface{}{} - posts, err := app.db.GetPosts(c, p, false, true, false) + posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false) for _, pp := range *posts { pp.Collection = res - o := pp.ActivityObject() + o := pp.ActivityObject(app.cfg) a := activitystreams.NewCreateActivity(o) ocp.OrderedItems = append(ocp.OrderedItems, *a) } @@ -375,11 +375,11 @@ 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 !app.db.isDuplicateKeyErr(err) { - t.Rollback() - log.Error("Couldn't add new remoteuser in DB: %v\n", err) - return - } + // if duplicate key, res will be nil and panic on + // res.LastInsertId below + t.Rollback() + log.Error("Couldn't add new remoteuser in DB: %v\n", err) + return } followerID, err = res.LastInsertId() @@ -524,7 +524,7 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error { } p.Collection.hostName = app.cfg.App.Host actor := p.Collection.PersonObject(collID) - na := p.ActivityObject() + na := p.ActivityObject(app.cfg) // Add followers p.Collection.ID = collID @@ -570,7 +570,7 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { } } actor := p.Collection.PersonObject(collID) - na := p.ActivityObject() + na := p.ActivityObject(app.cfg) // Add followers p.Collection.ID = collID diff --git a/admin.go b/admin.go index 9436ff5..a1c2dac 100644 --- a/admin.go +++ b/admin.go @@ -18,11 +18,11 @@ import ( "strconv" "time" - "github.com/gogits/gogs/pkg/tool" "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/appstats" "github.com/writeas/writefreely/config" ) @@ -195,7 +195,7 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque p.LastPost = lp.Format("January 2, 2006, 3:04 PM") } - colls, err := app.db.GetCollections(p.User) + colls, err := app.db.GetCollections(p.User, app.cfg.App.Host) if err != nil { return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's collections: %v", err)} } @@ -319,6 +319,8 @@ func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Reque } p.Content, err = getLandingBody(app) p.Content.ID = "landing" + } else if slug == "reader" { + p.Content, err = getReaderSection(app) } else { p.Content, err = app.db.GetDynamicContent(slug) } @@ -342,7 +344,7 @@ func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Req id := vars["page"] // Validate - if id != "about" && id != "privacy" && id != "landing" { + if id != "about" && id != "privacy" && id != "landing" && id != "reader" { return impart.HTTPError{http.StatusNotFound, "No such page."} } @@ -356,6 +358,9 @@ func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Req return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m} } err = app.db.UpdateDynamicContent("landing-body", "", r.FormValue("content"), "section") + } else if id == "reader" { + // Update sections with titles + err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "section") } else { // Update page err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page") @@ -402,37 +407,37 @@ func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *htt } func updateAppStats() { - sysStatus.Uptime = tool.TimeSincePro(appStartTime) + sysStatus.Uptime = appstats.TimeSincePro(appStartTime) m := new(runtime.MemStats) runtime.ReadMemStats(m) sysStatus.NumGoroutine = runtime.NumGoroutine() - sysStatus.MemAllocated = tool.FileSize(int64(m.Alloc)) - sysStatus.MemTotal = tool.FileSize(int64(m.TotalAlloc)) - sysStatus.MemSys = tool.FileSize(int64(m.Sys)) + sysStatus.MemAllocated = appstats.FileSize(int64(m.Alloc)) + sysStatus.MemTotal = appstats.FileSize(int64(m.TotalAlloc)) + sysStatus.MemSys = appstats.FileSize(int64(m.Sys)) sysStatus.Lookups = m.Lookups sysStatus.MemMallocs = m.Mallocs sysStatus.MemFrees = m.Frees - sysStatus.HeapAlloc = tool.FileSize(int64(m.HeapAlloc)) - sysStatus.HeapSys = tool.FileSize(int64(m.HeapSys)) - sysStatus.HeapIdle = tool.FileSize(int64(m.HeapIdle)) - sysStatus.HeapInuse = tool.FileSize(int64(m.HeapInuse)) - sysStatus.HeapReleased = tool.FileSize(int64(m.HeapReleased)) + sysStatus.HeapAlloc = appstats.FileSize(int64(m.HeapAlloc)) + sysStatus.HeapSys = appstats.FileSize(int64(m.HeapSys)) + sysStatus.HeapIdle = appstats.FileSize(int64(m.HeapIdle)) + sysStatus.HeapInuse = appstats.FileSize(int64(m.HeapInuse)) + sysStatus.HeapReleased = appstats.FileSize(int64(m.HeapReleased)) sysStatus.HeapObjects = m.HeapObjects - sysStatus.StackInuse = tool.FileSize(int64(m.StackInuse)) - sysStatus.StackSys = tool.FileSize(int64(m.StackSys)) - sysStatus.MSpanInuse = tool.FileSize(int64(m.MSpanInuse)) - sysStatus.MSpanSys = tool.FileSize(int64(m.MSpanSys)) - sysStatus.MCacheInuse = tool.FileSize(int64(m.MCacheInuse)) - sysStatus.MCacheSys = tool.FileSize(int64(m.MCacheSys)) - sysStatus.BuckHashSys = tool.FileSize(int64(m.BuckHashSys)) - sysStatus.GCSys = tool.FileSize(int64(m.GCSys)) - sysStatus.OtherSys = tool.FileSize(int64(m.OtherSys)) - - sysStatus.NextGC = tool.FileSize(int64(m.NextGC)) + sysStatus.StackInuse = appstats.FileSize(int64(m.StackInuse)) + sysStatus.StackSys = appstats.FileSize(int64(m.StackSys)) + sysStatus.MSpanInuse = appstats.FileSize(int64(m.MSpanInuse)) + sysStatus.MSpanSys = appstats.FileSize(int64(m.MSpanSys)) + sysStatus.MCacheInuse = appstats.FileSize(int64(m.MCacheInuse)) + sysStatus.MCacheSys = appstats.FileSize(int64(m.MCacheSys)) + sysStatus.BuckHashSys = appstats.FileSize(int64(m.BuckHashSys)) + sysStatus.GCSys = appstats.FileSize(int64(m.GCSys)) + sysStatus.OtherSys = appstats.FileSize(int64(m.OtherSys)) + + sysStatus.NextGC = appstats.FileSize(int64(m.NextGC)) sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000) sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000) sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000) diff --git a/app.go b/app.go index dea31bf..5cdaac2 100644 --- a/app.go +++ b/app.go @@ -186,8 +186,8 @@ func (app *App) ReqLog(r *http.Request, status int, timeSince time.Duration) str 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. +// handleViewHome shows page at root path. It checks the configuration and +// authentication state to show the correct page. func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { if app.cfg.App.SingleUser { // Render blog index @@ -199,6 +199,15 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { if !forceLanding { // Show correct page based on user auth status and configured landing path u := getUserSession(app, r) + + if app.cfg.App.Chorus { + // This instance is focused on reading, so show Reader on home route if not + // private or a private-instance user is logged in. + if !app.cfg.App.Private || u != nil { + return viewLocalTimeline(app, w, r) + } + } + if u != nil { // User is logged in, so show the Pad return handleViewPad(app, w, r) @@ -209,6 +218,12 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { } } + return handleViewLanding(app, w, r) +} + +func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error { + forceLanding := r.FormValue("landing") == "1" + p := struct { page.StaticPage Flashes []template.HTML @@ -226,14 +241,14 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { log.Error("unable to get landing banner: %v", err) return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get banner: %v", err)} } - p.Banner = template.HTML(applyMarkdown([]byte(banner.Content), "")) + p.Banner = template.HTML(applyMarkdown([]byte(banner.Content), "", app.cfg)) content, err := getLandingBody(app) if err != nil { log.Error("unable to get landing content: %v", err) return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get content: %v", err)} } - p.Content = template.HTML(applyMarkdown([]byte(content.Content), "")) + p.Content = template.HTML(applyMarkdown([]byte(content.Content), "", app.cfg)) // Get error messages session, err := app.sessionStore.Get(r, cookieName) @@ -281,7 +296,7 @@ func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *te return err } p.ContentTitle = c.Title.String - p.Content = template.HTML(applyMarkdown([]byte(c.Content), "")) + p.Content = template.HTML(applyMarkdown([]byte(c.Content), "", app.cfg)) p.PlainContent = shortPostDescription(stripmd.Strip(c.Content)) if !c.Updated.IsZero() { p.Updated = c.Updated.Format("January 2, 2006") @@ -319,6 +334,8 @@ func pageForReq(app *App, r *http.Request) page.StaticPage { u = getUserSession(app, r) if u != nil { p.Username = u.Username + p.IsAdmin = u != nil && u.IsAdmin() + p.CanInvite = canUserInvite(app.cfg, p.IsAdmin) } } p.CanViewReader = !app.cfg.App.Private || u != nil diff --git a/appstats/appstats.go b/appstats/appstats.go new file mode 100644 index 0000000..bf27c7b --- /dev/null +++ b/appstats/appstats.go @@ -0,0 +1,128 @@ +// Copyright 2014-2018 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file of the Gogs project (github.com/gogs/gogs). + +package appstats + +import ( + "fmt" + "math" + "strings" + "time" +) + +// Borrowed from github.com/gogs/gogs/pkg/tool + +// Seconds-based time units +const ( + Minute = 60 + Hour = 60 * Minute + Day = 24 * Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month +) + +func computeTimeDiff(diff int64) (int64, string) { + diffStr := "" + switch { + case diff <= 0: + diff = 0 + diffStr = "now" + case diff < 2: + diff = 0 + diffStr = "1 second" + case diff < 1*Minute: + diffStr = fmt.Sprintf("%d seconds", diff) + diff = 0 + + case diff < 2*Minute: + diff -= 1 * Minute + diffStr = "1 minute" + case diff < 1*Hour: + diffStr = fmt.Sprintf("%d minutes", diff/Minute) + diff -= diff / Minute * Minute + + case diff < 2*Hour: + diff -= 1 * Hour + diffStr = "1 hour" + case diff < 1*Day: + diffStr = fmt.Sprintf("%d hours", diff/Hour) + diff -= diff / Hour * Hour + + case diff < 2*Day: + diff -= 1 * Day + diffStr = "1 day" + case diff < 1*Week: + diffStr = fmt.Sprintf("%d days", diff/Day) + diff -= diff / Day * Day + + case diff < 2*Week: + diff -= 1 * Week + diffStr = "1 week" + case diff < 1*Month: + diffStr = fmt.Sprintf("%d weeks", diff/Week) + diff -= diff / Week * Week + + case diff < 2*Month: + diff -= 1 * Month + diffStr = "1 month" + case diff < 1*Year: + diffStr = fmt.Sprintf("%d months", diff/Month) + diff -= diff / Month * Month + + case diff < 2*Year: + diff -= 1 * Year + diffStr = "1 year" + default: + diffStr = fmt.Sprintf("%d years", diff/Year) + diff = 0 + } + return diff, diffStr +} + +// TimeSincePro calculates the time interval and generate full user-friendly string. +func TimeSincePro(then time.Time) string { + now := time.Now() + diff := now.Unix() - then.Unix() + + if then.After(now) { + return "future" + } + + var timeStr, diffStr string + for { + if diff == 0 { + break + } + + diff, diffStr = computeTimeDiff(diff) + timeStr += ", " + diffStr + } + return strings.TrimPrefix(timeStr, ", ") +} + +func logn(n, b float64) float64 { + return math.Log(n) / math.Log(b) +} + +func humanateBytes(s uint64, base float64, sizes []string) string { + if s < 10 { + return fmt.Sprintf("%d B", s) + } + e := math.Floor(logn(float64(s), base)) + suffix := sizes[int(e)] + val := float64(s) / math.Pow(base, math.Floor(e)) + f := "%.0f" + if val < 10 { + f = "%.1f" + } + + return fmt.Sprintf(f+" %s", val, suffix) +} + +// FileSize calculates the file size and generate user-friendly string. +func FileSize(s int64) string { + sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} + return humanateBytes(uint64(s), 1024, sizes) +} diff --git a/collections.go b/collections.go index aee74f7..cdf3d5c 100644 --- a/collections.go +++ b/collections.go @@ -338,7 +338,7 @@ func (c *Collection) RenderMathJax() bool { } func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { - reqJSON := IsJSON(r.Header.Get("Content-Type")) + reqJSON := IsJSON(r) alias := r.FormValue("alias") title := r.FormValue("title") @@ -454,7 +454,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error { c.hostName = app.cfg.App.Host // Redirect users who aren't requesting JSON - reqJSON := IsJSON(r.Header.Get("Content-Type")) + reqJSON := IsJSON(r) if !reqJSON { return impart.HTTPError{http.StatusFound, c.CanonicalURL()} } @@ -512,7 +512,7 @@ func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) erro } } - posts, err := app.db.GetPosts(c, page, isCollOwner, false, false) + posts, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false) if err != nil { return err } @@ -541,6 +541,8 @@ type CollectionPage struct { Username string Collections *[]Collection PinnedPosts *[]PublicPost + IsAdmin bool + CanInvite bool } func (c *CollectionObj) ScriptDisplay() template.JS { @@ -724,6 +726,8 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro return err } + c.hostName = app.cfg.App.Host + // Serve ActivityStreams data now, if requested if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { ac := c.PersonObject() @@ -744,7 +748,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro return impart.HTTPError{http.StatusFound, redirURL} } - coll.Posts, _ = app.db.GetPosts(c, page, cr.isCollOwner, false, false) + coll.Posts, _ = app.db.GetPosts(app.cfg, c, page, cr.isCollOwner, false, false) // Serve collection displayPage := CollectionPage{ @@ -753,6 +757,8 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro IsCustomDomain: cr.isCustomDomain, IsWelcome: r.FormValue("greeting") != "", } + displayPage.IsAdmin = u != nil && u.IsAdmin() + displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin) var owner *User if u != nil { displayPage.Username = u.Username @@ -762,14 +768,15 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro owner = u displayPage.CanPin = true - pubColls, err := app.db.GetPublishableCollections(owner) + pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } displayPage.Collections = pubColls } } - if owner == nil { + isOwner := owner != nil + if !isOwner { // Current user doesn't own collection; retrieve owner information owner, err = app.db.GetUserByID(coll.OwnerID) if err != nil { @@ -782,9 +789,13 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro // Add more data // TODO: fix this mess of collections inside collections - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj) + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) - err = templates["collection"].ExecuteTemplate(w, "collection", displayPage) + collTmpl := "collection" + if app.cfg.App.Chorus { + collTmpl = "chorus-collection" + } + err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage) if err != nil { log.Error("Unable to render collection index: %v", err) } @@ -833,7 +844,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e coll := newDisplayCollection(c, cr, page) - coll.Posts, _ = app.db.GetPostsTagged(c, tag, page, cr.isCollOwner) + coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, page, cr.isCollOwner) if coll.Posts != nil && len(*coll.Posts) == 0 { return ErrCollectionPageNotFound } @@ -859,14 +870,15 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e owner = u displayPage.CanPin = true - pubColls, err := app.db.GetPublishableCollections(owner) + pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } displayPage.Collections = pubColls } } - if owner == nil { + isOwner := owner != nil + if !isOwner { // Current user doesn't own collection; retrieve owner information owner, err = app.db.GetUserByID(coll.OwnerID) if err != nil { @@ -878,7 +890,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e coll.Owner = displayPage.Owner // Add more data // TODO: fix this mess of collections inside collections - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj) + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage) if err != nil { @@ -907,7 +919,7 @@ func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Reque } func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error { - reqJSON := IsJSON(r.Header.Get("Content-Type")) + reqJSON := IsJSON(r) vars := mux.Vars(r) collAlias := vars["alias"] isWeb := r.FormValue("web") == "1" diff --git a/config/config.go b/config/config.go index f915431..80e2565 100644 --- a/config/config.go +++ b/config/config.go @@ -69,8 +69,13 @@ type ( JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` Landing string `ini:"landing"` + SimpleNav bool `ini:"simple_nav"` WFModesty bool `ini:"wf_modesty"` + // Site functionality + Chorus bool `ini:"chorus"` + DisableDrafts bool `ini:"disable_drafts"` + // Users SingleUser bool `ini:"single_user"` OpenRegistration bool `ini:"open_registration"` diff --git a/database.go b/database.go index 34c5234..a3235b6 100644 --- a/database.go +++ b/database.go @@ -65,8 +65,8 @@ type writestore interface { ChangeSettings(app *App, u *User, s *userSettings) error ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error - GetCollections(u *User) (*[]Collection, error) - GetPublishableCollections(u *User) (*[]Collection, error) + GetCollections(u *User, hostName string) (*[]Collection, error) + GetPublishableCollections(u *User, hostName string) (*[]Collection, error) GetMeStats(u *User) userMeStats GetTotalCollections() (int64, error) GetTotalPosts() (int64, error) @@ -94,7 +94,7 @@ type writestore interface { UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error GetLastPinnedPostPos(collID int64) int64 - GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) + GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) RemoveCollectionRedirect(t *sql.Tx, alias string) error GetCollectionRedirect(alias string) (new string) IsCollectionAttributeOn(id int64, attr string) bool @@ -106,8 +106,8 @@ type writestore interface { 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) - GetPostsTagged(c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) + GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) + GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) GetAPFollowers(c *Collection) (*[]RemoteUser, error) GetAPActorKeys(collectionID int64) ([]byte, []byte) @@ -1070,7 +1070,7 @@ func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) { // It will return future posts if `includeFuture` is true. // It will include only standard (non-pinned) posts unless `includePinned` is true. // TODO: change includeFuture to isOwner, since that's how it's used -func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) { +func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) { collID := c.ID cf := c.NewFormat() @@ -1115,7 +1115,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecen break } p.extractData() - p.formatContent(c, includeFuture) + p.formatContent(cfg, c, includeFuture) posts = append(posts, p.processPost()) } @@ -1131,7 +1131,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecen // given tag. // It will return future posts if `includeFuture` is true. // TODO: change includeFuture to isOwner, since that's how it's used -func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) { +func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) { collID := c.ID cf := c.NewFormat() @@ -1179,7 +1179,7 @@ func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, include break } p.extractData() - p.formatContent(c, includeFuture) + p.formatContent(cfg, c, includeFuture) posts = append(posts, p.processPost()) } @@ -1533,9 +1533,13 @@ func (db *datastore) GetLastPinnedPostPos(collID int64) int64 { return lastPos.Int64 } -func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) { +func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) { // FIXME: sqlite-backed instances don't include ellipsis on truncated titles - rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID) + timeCondition := "" + if !includeFuture { + timeCondition = "AND created <= " + db.now() + } + rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL "+timeCondition+" ORDER BY pinned_position ASC", coll.ID) if err != nil { log.Error("Failed selecting pinned posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."} @@ -1559,7 +1563,7 @@ func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) return &posts, nil } -func (db *datastore) GetCollections(u *User) (*[]Collection, error) { +func (db *datastore) GetCollections(u *User, hostName string) (*[]Collection, error) { rows, err := db.Query("SELECT id, alias, title, description, privacy, view_count FROM collections WHERE owner_id = ? ORDER BY id ASC", u.ID) if err != nil { log.Error("Failed selecting from collections: %v", err) @@ -1575,6 +1579,7 @@ func (db *datastore) GetCollections(u *User) (*[]Collection, error) { log.Error("Failed scanning row: %v", err) break } + c.hostName = hostName c.URL = c.CanonicalURL() c.Public = c.IsPublic() @@ -1588,8 +1593,8 @@ func (db *datastore) GetCollections(u *User) (*[]Collection, error) { return &colls, nil } -func (db *datastore) GetPublishableCollections(u *User) (*[]Collection, error) { - c, err := db.GetCollections(u) +func (db *datastore) GetPublishableCollections(u *User, hostName string) (*[]Collection, error) { + c, err := db.GetCollections(u, hostName) if err != nil { return nil, err } @@ -2252,6 +2257,19 @@ func (db *datastore) GetUserInvite(id string) (*Invite, error) { return &i, nil } +// IsUsersInvite returns true if the user with ID created the invite with code +// and an error other than sql no rows, if any. Will return false in the event +// of an error. +func (db *datastore) IsUsersInvite(code string, userID int64) (bool, error) { + var id string + err := db.QueryRow("SELECT id FROM userinvites WHERE id = ? AND owner_id = ?", code, userID).Scan(&id) + if err != nil && err != sql.ErrNoRows { + log.Error("Failed selecting invite: %v", err) + return false, err + } + return id != "", nil +} + func (db *datastore) GetUsersInvitedCount(id string) int64 { var count int64 err := db.QueryRow("SELECT COUNT(*) FROM usersinvited WHERE invite_id = ?", id).Scan(&count) diff --git a/export.go b/export.go index 47a2603..592bc0c 100644 --- a/export.go +++ b/export.go @@ -20,7 +20,7 @@ import ( "github.com/writeas/web-core/log" ) -func exportPostsCSV(u *User, posts *[]PublicPost) []byte { +func exportPostsCSV(hostName string, u *User, posts *[]PublicPost) []byte { var b bytes.Buffer r := [][]string{ @@ -30,8 +30,9 @@ func exportPostsCSV(u *User, posts *[]PublicPost) []byte { var blog string if p.Collection != nil { blog = p.Collection.Alias + p.Collection.hostName = hostName } - f := []string{p.ID, p.Slug.String, blog, p.CanonicalURL(), p.Created8601(), p.Title.String, strings.Replace(p.Content, "\n", "\\n", -1)} + f := []string{p.ID, p.Slug.String, blog, p.CanonicalURL(hostName), p.Created8601(), p.Title.String, strings.Replace(p.Content, "\n", "\\n", -1)} r = append(r, f) } @@ -104,7 +105,7 @@ func compileFullExport(app *App, u *User) *ExportUser { User: u, } - colls, err := app.db.GetCollections(u) + colls, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } @@ -118,7 +119,7 @@ func compileFullExport(app *App, u *User) *ExportUser { var collObjs []CollectionObj for _, c := range *colls { co := &CollectionObj{Collection: c} - co.Posts, err = app.db.GetPosts(&c, 0, true, false, true) + co.Posts, err = app.db.GetPosts(app.cfg, &c, 0, true, false, true) if err != nil { log.Error("unable to get collection posts: %v", err) } diff --git a/feed.go b/feed.go index dd82c33..32feb82 100644 --- a/feed.go +++ b/feed.go @@ -55,9 +55,9 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { tag := mux.Vars(req)["tag"] if tag != "" { - coll.Posts, _ = app.db.GetPostsTagged(c, tag, 1, false) + coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, 1, false) } else { - coll.Posts, _ = app.db.GetPosts(c, 1, false, true, false) + coll.Posts, _ = app.db.GetPosts(app.cfg, c, 1, false, true, false) } author := "" @@ -94,7 +94,7 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { Title: title, Link: &Link{Href: permalink}, Description: "", - Content: applyMarkdown([]byte(p.Content), ""), + Content: applyMarkdown([]byte(p.Content), "", app.cfg), Author: &Author{author, ""}, Created: p.Created, Updated: p.Updated, diff --git a/go.mod b/go.mod index bde8334..5e27956 100644 --- a/go.mod +++ b/go.mod @@ -2,25 +2,14 @@ module github.com/writeas/writefreely require ( github.com/BurntSushi/toml v0.3.1 // indirect - github.com/Unknwon/com v0.0.0-20181010210213-41959bdd855f // indirect - 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/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/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 github.com/go-sql-driver/mysql v1.4.1 github.com/go-test/deep v1.0.1 // indirect - github.com/gogits/gogs v0.11.86 - github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 // indirect - github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09 // indirect - github.com/gogs/gogs v0.11.86 // indirect - github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a // indirect github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/gorilla/feeds v1.1.0 @@ -28,17 +17,13 @@ require ( github.com/gorilla/schema v1.0.2 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 github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/manifoldco/promptui v0.3.2 github.com/mattn/go-colorable v0.1.0 // indirect github.com/mattn/go-sqlite3 v1.10.0 - github.com/mcuadros/go-version v0.0.0-20180611085657-6d5863ca60fa // indirect github.com/microcosm-cc/bluemonday v1.0.2 github.com/mitchellh/go-wordwrap v1.0.0 github.com/nicksnyder/go-i18n v1.10.0 // indirect @@ -46,7 +31,6 @@ require ( github.com/pelletier/go-toml v1.2.0 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect - github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect @@ -70,11 +54,7 @@ require ( golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 google.golang.org/appengine v1.4.0 // indirect gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect - gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e // indirect - gopkg.in/clog.v1 v1.2.0 // indirect gopkg.in/ini.v1 v1.41.0 - gopkg.in/macaron.v1 v1.3.2 // indirect - gopkg.in/redis.v2 v2.3.2 // indirect gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index 8898bec..ec1e19d 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Unknwon/com v0.0.0-20181010210213-41959bdd855f h1:m1tYqjD/N0vF/S8s/ZKz/eccUr8RAAcrOK2MhXeTegA= -github.com/Unknwon/com v0.0.0-20181010210213-41959bdd855f/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= -github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966 h1:Mp8GNJ/tdTZIEdLdZfykEJaL3mTyEYrSzYNcdoQKpJk= -github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966/go.mod h1:SFtfq0zFPsENI7DpE87QM2hcYu5QQ0fRdCgP+P1Hrqo= github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= github.com/alecthomas/gometalinter v3.0.0+incompatible h1:e9Zfvfytsw/e6Kd/PYd75wggK+/kX5Xn8IYDUKyc5fU= github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= @@ -11,8 +7,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZq github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= -github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g= -github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 h1:jWNY1NDg6a/c8RSXkai7IX6UOhir0LD39I4Dukg+4Ks= github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49/go.mod h1:EIlIeMufZ8nqdUhnesledB15xLRl4wIJUppwDLPrdrQ= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= @@ -36,26 +30,10 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= -github.com/go-macaron/cache v0.0.0-20151013081102-561735312776 h1:UYIHS1r0WotqB5cIa0PAiV0m6GzD9rDBcn4alp5JgCw= -github.com/go-macaron/cache v0.0.0-20151013081102-561735312776/go.mod h1:hHAsZm/oBZVcY+S7qdQL6Vbg5VrXF6RuKGuqsszt3Ok= -github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI= -github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw= -github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193 h1:z/nqwd+ql/r6Q3QGnwNd6B89UjPytM0be5pDQV9TuWw= -github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193/go.mod h1:ScEJm9Gk+ez5JJTml5WlBIqavAfuE5nF8e4Gvyz/X+A= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/gogits/gogs v0.11.86 h1:IujCpA+F/mYDXTcqdy593rl2donWakAWoL2HYZn7spw= -github.com/gogits/gogs v0.11.86/go.mod h1:H8FMbPPb+o/TgI6YnmQmT8nmEIHypXDau+f2CChYoCk= -github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs= -github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= -github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09 h1:UdOSIHZpkYcajRbfebBYzFDsL3SuqObH3bvKYBqgKmI= -github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09/go.mod h1:Zas3BtO88pk1cwUfEYlvnl/CRwh0ybDxRWSwRjG8I3w= -github.com/gogs/gogs v0.11.86 h1:D+dXuY/6XjJ2t74W/dxo7ogx5+xW05Va8sJiQSS4WXA= -github.com/gogs/gogs v0.11.86/go.mod h1:qlbvdn16XTC6q7eR+thjW+OLdN+mi2PBZ8KqVT39T88= -github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a h1:8DZwxETOVWIinYxDK+i6L+rMb7eGATGaakD6ZucfHVk= -github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a/go.mod h1:TUIZ+29jodWQ8Gk6Pvtg4E09aMsc3C/VLZiVYfUhWQU= github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 h1:6DVPu65tee05kY0/rciBQ47ue+AnuY8KTayV6VHikIo= github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= @@ -80,14 +58,8 @@ github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9R github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM= 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= -github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= @@ -113,8 +85,6 @@ github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mcuadros/go-version v0.0.0-20180611085657-6d5863ca60fa h1:XvNrttGMJfVrUqblGju4IkjYXwx6l5OAAyjaIsydzsk= -github.com/mcuadros/go-version v0.0.0-20180611085657-6d5863ca60fa/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= @@ -131,8 +101,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= -github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= -github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= @@ -167,8 +135,6 @@ github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g= github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ= github.com/writeas/web-core v1.0.0 h1:5VKkCakQgdKZcbfVKJXtRpc5VHrkflusCl/KRCPzpQ0= github.com/writeas/web-core v1.0.0/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE= -github.com/writefreely/go-nodeinfo v1.1.0 h1:dp/ieEu0/gTeNKFvJTYhzBBouyFn7aiWtWzkb8J1JLg= -github.com/writefreely/go-nodeinfo v1.1.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg= github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss= github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo= @@ -195,19 +161,11 @@ google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO50 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c h1:vTxShRUnK60yd8DZU+f95p1zSLj814+5CuEh7NjF2/Y= gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA= -gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e h1:wGA78yza6bu/mWcc4QfBuIEHEtc06xdiU0X8sY36yUU= -gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfzYWe577fCe7Ceci+6qjO2Rdc0Z4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/clog.v1 v1.2.0 h1:BHfwHRNQy497iBNsRBassPixSAxRbn2z5KVkdBFbwxc= -gopkg.in/clog.v1 v1.2.0/go.mod h1:L6fgdpdhFgKX4eGuDvt+N6X2GwZE160NRrIHzvaF8ZM= gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE= gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/macaron.v1 v1.3.2 h1:AvWIaPmwBUA87/OWzePkoxeaw6YJWDfBt1pDFPBnLf8= -gopkg.in/macaron.v1 v1.3.2/go.mod h1:PrsiawTWAGZs6wFbT5hlr7SQ2Ns9h7cUVtcUu4lQOVo= -gopkg.in/redis.v2 v2.3.2 h1:GPVIIB/JnL1wvfULefy3qXmPu1nfNu2d0yA09FHgwfs= -gopkg.in/redis.v2 v2.3.2/go.mod h1:4wl9PJ/CqzeHk3LVq1hNLHH8krm3+AXEgut4jVc++LU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/handle.go b/handle.go index 99c23ae..7e410f5 100644 --- a/handle.go +++ b/handle.go @@ -772,7 +772,7 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error) return } - if IsJSON(r.Header.Get("Content-Type")) { + if IsJSON(r) { impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."}) return } diff --git a/invites.go b/invites.go index 561255f..4e1f5fa 100644 --- a/invites.go +++ b/invites.go @@ -12,15 +12,16 @@ package writefreely import ( "database/sql" + "html/template" + "net/http" + "strconv" + "time" + "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/nerds/store" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/page" - "html/template" - "net/http" - "strconv" - "time" ) type Invite struct { @@ -114,6 +115,36 @@ func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error { return err } + expired := i.Expired() + if !expired && i.MaxUses.Valid && i.MaxUses.Int64 > 0 { + // Invite has a max-use number, so check if we're past that limit + i.uses = app.db.GetUsersInvitedCount(inviteCode) + expired = i.uses >= i.MaxUses.Int64 + } + + if u := getUserSession(app, r); u != nil { + // check if invite belongs to another user + // error can be ignored as not important in this case + if ownInvite, _ := app.db.IsUsersInvite(inviteCode, u.ID); !ownInvite { + addSessionFlash(app, w, r, "You're already registered and logged in.", nil) + // show homepage + return impart.HTTPError{http.StatusFound, "/me/settings"} + } + + // show invite instructions + p := struct { + *UserPage + Invite *Invite + Expired bool + }{ + UserPage: NewUserPage(app, r, u, "Invite to "+app.cfg.App.SiteName, nil), + Invite: i, + Expired: expired, + } + showUserPage(w, "invite-help", p) + return nil + } + p := struct { page.StaticPage Error string @@ -124,16 +155,10 @@ func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error { Invite: inviteCode, } - if i.Expired() { + if expired { p.Error = "This invite link has expired." } - if i.MaxUses.Valid && i.MaxUses.Int64 > 0 { - if c := app.db.GetUsersInvitedCount(inviteCode); c >= i.MaxUses.Int64 { - p.Error = "This invite link has expired." - } - } - // Get error messages session, err := app.sessionStore.Get(r, cookieName) if err != nil { diff --git a/less/Makefile b/less/Makefile index e81258a..117e9b2 100644 --- a/less/Makefile +++ b/less/Makefile @@ -1,20 +1,10 @@ -ifeq ($(shell which lessc),/usr/bin/lessc) - LESSC=/usr/bin/lessc -else ifeq ($(shell which lessc),/usr/local/bin/lessc) - LESSC=/usr/local/bin/lessc -else ifeq ($(shell which lessc),/bin/lessc) - LESSC=/bin/lessc -else - LESSC=node_modules/.bin/lessc -endif -export LESSC - CSSDIR=../static/css/ all : - $(LESSC) app.less --clean-css="--s1 --advanced" $(CSSDIR)write.css - $(LESSC) fonts.less --clean-css="--s1 --advanced" $(CSSDIR)fonts.css - $(LESSC) icons.less --clean-css="--s1 --advanced" $(CSSDIR)icons.css + @command -v lessc >/dev/null 2>&1 || { echo >&2 "lessc is not installed, please run: make install or: less/install-less.sh"; exit 1; } + lessc app.less --clean-css="--s1 --advanced" $(CSSDIR)write.css + lessc fonts.less --clean-css="--s1 --advanced" $(CSSDIR)fonts.css + lessc icons.less --clean-css="--s1 --advanced" $(CSSDIR)icons.css install : ./install-less.sh diff --git a/less/core.less b/less/core.less index 7e39f35..f4332a9 100644 --- a/less/core.less +++ b/less/core.less @@ -405,6 +405,31 @@ body { } } +nav#full-nav { + margin: 0; + + .left-side { + display: inline-block; + + a:first-child { + margin-left: 0; + } + } + + .right-side { + float: right; + } +} + +nav#full-nav a.simple-btn, .tool button { + font-family: @sansFont; + border: 1px solid #ccc !important; + padding: .5rem 1rem; + margin: 0; + .rounded(.25em); + text-decoration: none; +} + .post-title { a { &:link { diff --git a/less/pad-theme.less b/less/pad-theme.less index af1f95c..a8f668e 100644 --- a/less/pad-theme.less +++ b/less/pad-theme.less @@ -63,7 +63,7 @@ body#pad, body#pad-sub { } } #belt { - a { + a, button { color: #000; } } @@ -100,7 +100,7 @@ body#pad, body#pad-sub { } } #belt { - a { + a, button { color: white; } } diff --git a/less/pad.less b/less/pad.less index d37c6bc..a132b30 100644 --- a/less/pad.less +++ b/less/pad.less @@ -222,6 +222,13 @@ body#pad, body#pad-sub { font-style: italic; } } + button { + font-family: @sansFont; + background-color: transparent; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + border: 0; + } } } } diff --git a/pad.go b/pad.go index 1545b4f..3cb7f37 100644 --- a/pad.go +++ b/pad.go @@ -11,12 +11,13 @@ package writefreely import ( + "net/http" + "strings" + "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/page" - "net/http" - "strings" ) func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { @@ -47,23 +48,20 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { } var err error if appData.User != nil { - appData.Blogs, err = app.db.GetPublishableCollections(appData.User) + appData.Blogs, err = app.db.GetPublishableCollections(appData.User, app.cfg.App.Host) if err != nil { log.Error("Unable to get user's blogs for Pad: %v", err) } } padTmpl := app.cfg.App.Editor - if padTmpl == "" { + if templates[padTmpl] == nil { + log.Info("No template '%s' found. Falling back to default 'pad' template.", padTmpl) padTmpl = "pad" } if action == "" && slug == "" { // Not editing any post; simply render the Pad - if templates[padTmpl] == nil { - log.Info("No template '%s' found. Falling back to default 'pad' template.", padTmpl) - padTmpl = "pad" - } if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil { log.Error("Unable to execute template: %v", err) } diff --git a/page/page.go b/page/page.go index 2af5322..15f09a9 100644 --- a/page/page.go +++ b/page/page.go @@ -28,6 +28,8 @@ type StaticPage struct { Values map[string]string Flashes []string CanViewReader bool + IsAdmin bool + CanInvite bool } // SanitizeHost alters the StaticPage to contain a real hostname. This is diff --git a/pages.go b/pages.go index 405b34f..d8f034b 100644 --- a/pages.go +++ b/pages.go @@ -135,3 +135,30 @@ WriteFreely can communicate with other federated platforms like Mastodon, so peo } return "" } + +func getReaderSection(app *App) (*instanceContent, error) { + c, err := app.db.GetDynamicContent("reader") + if err != nil { + return nil, err + } + if c == nil { + c = &instanceContent{ + ID: "reader", + Type: "section", + Content: defaultReaderBanner(app.cfg), + Updated: defaultPageUpdatedTime, + } + } + if !c.Title.Valid { + c.Title = defaultReaderTitle(app.cfg) + } + return c, nil +} + +func defaultReaderTitle(cfg *config.Config) sql.NullString { + return sql.NullString{String: "Reader", Valid: true} +} + +func defaultReaderBanner(cfg *config.Config) string { + return "Read the latest posts from " + cfg.App.SiteName + "." +} diff --git a/pages/login.tmpl b/pages/login.tmpl index 9b58523..1c8e862 100644 --- a/pages/login.tmpl +++ b/pages/login.tmpl @@ -12,8 +12,8 @@ {{end}}
diff --git a/postrender.go b/postrender.go index af715be..83fb5ad 100644 --- a/postrender.go +++ b/postrender.go @@ -12,17 +12,19 @@ package writefreely import ( "fmt" - "github.com/microcosm-cc/bluemonday" - stripmd "github.com/writeas/go-strip-markdown" - "github.com/writeas/saturday" - "github.com/writeas/web-core/stringmanip" - "github.com/writeas/writefreely/parse" "html" "html/template" "regexp" "strings" "unicode" "unicode/utf8" + + "github.com/microcosm-cc/bluemonday" + stripmd "github.com/writeas/go-strip-markdown" + blackfriday "github.com/writeas/saturday" + "github.com/writeas/web-core/stringmanip" + "github.com/writeas/writefreely/config" + "github.com/writeas/writefreely/parse" ) var ( @@ -34,27 +36,28 @@ var ( markeddownReg = regexp.MustCompile("(.+)
") ) -func (p *Post) formatContent(c *Collection, isOwner bool) { +func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) { baseURL := c.CanonicalURL() + // TODO: redundant if !isSingleUser { baseURL = "/" + c.Alias + "/" } p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String))) - p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL)) + p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL, cfg)) if exc := strings.Index(string(p.Content), ""); exc > -1 { - p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL)) + p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL, cfg)) } } -func (p *PublicPost) formatContent(isOwner bool) { - p.Post.formatContent(&p.Collection.Collection, isOwner) +func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool) { + p.Post.formatContent(cfg, &p.Collection.Collection, isOwner) } -func applyMarkdown(data []byte, baseURL string) string { - return applyMarkdownSpecial(data, false, baseURL) +func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string { + return applyMarkdownSpecial(data, false, baseURL, cfg) } -func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string) string { +func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *config.Config) string { mdExtensions := 0 | blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | @@ -74,7 +77,11 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string) string md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) if baseURL != "" { // Replace special text generated by Markdown parser - md = []byte(hashtagReg.ReplaceAll(md, []byte("#$1"))) + tagPrefix := baseURL + "tag:" + if cfg.App.Chorus { + tagPrefix = "/read/t/" + } + md = []byte(hashtagReg.ReplaceAll(md, []byte("#$1"))) } // Strip out bad HTML policy := getSanitizationPolicy() @@ -163,6 +170,7 @@ func getSanitizationPolicy() *bluemonday.Policy { policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video") policy.AllowAttrs("controls", "loop", "muted", "autoplay", "preload").OnElements("audio") policy.AllowAttrs("target").OnElements("a") + policy.AllowAttrs("title").OnElements("abbr") policy.AllowAttrs("style", "class", "id").Globally() policy.AllowURLSchemes("http", "https", "mailto", "xmpp") return policy diff --git a/posts.go b/posts.go index 2f3606f..d004296 100644 --- a/posts.go +++ b/posts.go @@ -35,6 +35,7 @@ import ( "github.com/writeas/web-core/i18n" "github.com/writeas/web-core/log" "github.com/writeas/web-core/tags" + "github.com/writeas/writefreely/config" "github.com/writeas/writefreely/page" "github.com/writeas/writefreely/parse" ) @@ -376,7 +377,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { Direction: d, } if !isRaw { - post.HTMLContent = template.HTML(applyMarkdown([]byte(content), "")) + post.HTMLContent = template.HTML(applyMarkdown([]byte(content), "", app.cfg)) } } @@ -471,7 +472,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { // /posts?collection={alias} // ? /collections/{alias}/posts func newPost(app *App, w http.ResponseWriter, r *http.Request) error { - reqJSON := IsJSON(r.Header.Get("Content-Type")) + reqJSON := IsJSON(r) vars := mux.Vars(r) collAlias := vars["alias"] if collAlias == "" { @@ -597,7 +598,7 @@ func newPost(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")) + reqJSON := IsJSON(r) vars := mux.Vars(r) postID := vars["post"] @@ -1032,7 +1033,7 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { } p.Collection = &CollectionObj{Collection: *coll} - po := p.ActivityObject() + po := p.ActivityObject(app.cfg) po.Context = []interface{}{activitystreams.Namespace} return impart.RenderActivityJSON(w, po, http.StatusOK) } @@ -1060,25 +1061,25 @@ func (p *Post) processPost() PublicPost { return *res } -func (p *PublicPost) CanonicalURL() string { +func (p *PublicPost) CanonicalURL(hostName string) string { if p.Collection == nil || p.Collection.Alias == "" { - return p.Collection.hostName + "/" + p.ID + return hostName + "/" + p.ID } return p.Collection.CanonicalURL() + p.Slug.String } -func (p *PublicPost) ActivityObject() *activitystreams.Object { +func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object { o := activitystreams.NewArticleObject() o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID o.Published = p.Created - o.URL = p.CanonicalURL() + o.URL = p.CanonicalURL(cfg.App.Host) o.AttributedTo = p.Collection.FederatedAccount() o.CC = []string{ p.Collection.FederatedAccount() + "/followers", } o.Name = p.DisplayTitle() if p.HTMLContent == template.HTML("") { - p.formatContent(false) + p.formatContent(cfg, false) } o.Content = string(p.HTMLContent) if p.Language.Valid { @@ -1093,7 +1094,11 @@ func (p *PublicPost) ActivityObject() *activitystreams.Object { if isSingleUser { tagBaseURL = p.Collection.CanonicalURL() + "tag:" } else { - tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias) + if cfg.App.Chorus { + tagBaseURL = fmt.Sprintf("%s/read/t/", p.Collection.hostName) + } else { + tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias) + } } for _, t := range p.Tags { o.Tag = append(o.Tag, activitystreams.Tag{ @@ -1357,14 +1362,14 @@ Are you sure it was ever here?`, return ErrCollectionPageNotFound } p.extractData() - ap := p.ActivityObject() + ap := p.ActivityObject(app.cfg) ap.Context = []interface{}{activitystreams.Namespace} return impart.RenderActivityJSON(w, ap, http.StatusOK) } else { p.extractData() p.Content = strings.Replace(p.Content, "", "", 1) // TODO: move this to function - p.formatContent(cr.isCollOwner) + p.formatContent(app.cfg, cr.isCollOwner) tp := struct { *PublicPost page.StaticPage @@ -1373,6 +1378,8 @@ Are you sure it was ever here?`, IsCustomDomain bool PinnedPosts *[]PublicPost IsFound bool + IsAdmin bool + CanInvite bool }{ PublicPost: p, StaticPage: pageForReq(app, r), @@ -1380,13 +1387,19 @@ Are you sure it was ever here?`, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, } - tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll) + tp.IsAdmin = u != nil && u.IsAdmin() + tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) + tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, p.IsOwner) 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 { + postTmpl := "collection-post" + if app.cfg.App.Chorus { + postTmpl = "chorus-collection-post" + } + if err := templates[postTmpl].ExecuteTemplate(w, "post", tp); err != nil { log.Error("Error in collection-post template: %v", err) } } diff --git a/read.go b/read.go index 3bc91c7..df24621 100644 --- a/read.go +++ b/read.go @@ -47,6 +47,13 @@ type readPublication struct { Posts *[]PublicPost CurrentPage int TotalPages int + SelTopic string + IsAdmin bool + CanInvite bool + + // Customizable page content + ContentTitle string + Content template.HTML } func initLocalTimeline(app *App) { @@ -97,7 +104,7 @@ func (app *App) FetchPublicPosts() (interface{}, error) { } p.extractData() - p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), "")) + p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), "", app.cfg)) fp := p.processPost() if isCollectionPost { fp.Collection = &CollectionObj{Collection: *c} @@ -197,13 +204,25 @@ func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page in } d := &readPublication{ - pageForReq(app, r), - &posts, - page, - ttlPages, + StaticPage: pageForReq(app, r), + Posts: &posts, + CurrentPage: page, + TotalPages: ttlPages, + SelTopic: tag, + } + if app.cfg.App.Chorus { + u := getUserSession(app, r) + d.IsAdmin = u != nil && u.IsAdmin() + d.CanInvite = canUserInvite(app.cfg, d.IsAdmin) + } + c, err := getReaderSection(app) + if err != nil { + return err } + d.ContentTitle = c.Title.String + d.Content = template.HTML(applyMarkdown([]byte(c.Content), "", app.cfg)) - err := templates["read"].ExecuteTemplate(w, "base", d) + err = templates["read"].ExecuteTemplate(w, "base", d) if err != nil { log.Error("Unable to render reader: %v", err) fmt.Fprintf(w, ":(") @@ -274,7 +293,7 @@ func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) e } title = p.PlainDisplayTitle() - permalink = p.CanonicalURL() + permalink = p.CanonicalURL(app.cfg.App.Host) if p.Collection != nil { author = p.Collection.Title } else { @@ -286,7 +305,7 @@ func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) e Title: title, Link: &Link{Href: permalink}, Description: "", - Content: applyMarkdown([]byte(p.Content), ""), + Content: applyMarkdown([]byte(p.Content), "", app.cfg), Author: &Author{author, ""}, Created: p.Created, Updated: p.Updated, diff --git a/request.go b/request.go index 4939f9c..2eb29f5 100644 --- a/request.go +++ b/request.go @@ -10,9 +10,13 @@ package writefreely -import "mime" +import ( + "mime" + "net/http" +) -func IsJSON(h string) bool { - ct, _, _ := mime.ParseMediaType(h) - return ct == "application/json" +func IsJSON(r *http.Request) bool { + ct, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + accept := r.Header.Get("Accept") + return ct == "application/json" || accept == "application/json" } diff --git a/routes.go b/routes.go index 937f8bc..9dc6c9d 100644 --- a/routes.go +++ b/routes.go @@ -152,7 +152,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { // Handle special pages first write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) - write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelNoneRequired)).Methods("GET") + write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired)) + write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelOptional)).Methods("GET") // TODO: show a reader-specific 404 page if the function is disabled write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader)) RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter()) diff --git a/sitemap.go b/sitemap.go index 4dfd953..00e148f 100644 --- a/sitemap.go +++ b/sitemap.go @@ -66,7 +66,7 @@ func handleViewSitemap(app *App, w http.ResponseWriter, r *http.Request) error { host = c.CanonicalURL() sm := buildSitemap(host, pre) - posts, err := app.db.GetPosts(c, 0, false, false, false) + posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false) if err != nil { log.Error("Error getting posts: %v", err) return err diff --git a/templates.go b/templates.go index 7a45c45..6e9a008 100644 --- a/templates.go +++ b/templates.go @@ -64,11 +64,14 @@ func initTemplate(parentDir, name string) { filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), } - if name == "collection" || name == "collection-tags" { + if name == "collection" || name == "collection-tags" || name == "chorus-collection" { // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl")) } - if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" { + if name == "chorus-collection" || name == "chorus-collection-post" { + files = append(files, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl")) + } + if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" || name == "chorus-collection" || name == "chorus-collection-post" { files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl")) } templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) diff --git a/templates/bare.tmpl b/templates/bare.tmpl new file mode 100644 index 0000000..a4194c9 --- /dev/null +++ b/templates/bare.tmpl @@ -0,0 +1,235 @@ +{{define "pad"}} + + + +Scheduled
{{end}}{{if .Title.String}}{{.Description}}
{{end}} + {{/*if not .Public/*}} + + {{/*end*/}} + {{if .PinnedPosts}}This is your new blog.
+Start writing, or customize your blog.
+Check out our writing guide to see what else you can do, and get in touch anytime with questions or feedback.
+Read the latest posts from {{.SiteName}}. {{if .Username}}To showcase your writing here, go to your blog settings and select the Public option.{{end}}
+{{if .SelTopic}}#{{.SelTopic}} posts{{else}}{{.Content}}{{end}}
{{if .Collection}}from {{.Collection.DisplayTitle}}{{else}}Anonymous{{end}}
{{if .Excerpt}}{{.Content}}
{{ else }}{{.HTMLContent}}{{ end }}{{.Content}}
{{ else }}{{.HTMLContent}}{{ end }}Describe what your instance is about.
{{else if eq .Content.ID "privacy"}}Outline your privacy policy.
+ {{else if eq .Content.ID "reader"}} +Customize your Reader page.
{{else if eq .Content.ID "landing"}}Customize your home page.
{{end}} @@ -38,7 +40,7 @@ input[type=text] { {{if .Message}}{{.Message}}
{{end}}