Skip to content
Permalink
Browse files

Merge branch 'develop' into T661-disable-accounts

  • Loading branch information
thebaer committed Nov 11, 2019
2 parents 5644e8d + 5839c2a commit 53586d9cb8250cf43a4d0717a62ee86b89337f2a
@@ -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

@@ -85,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
@@ -120,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 == "" {
@@ -377,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 != "")

@@ -580,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")
@@ -625,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") {
@@ -662,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"`
@@ -686,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
}
@@ -717,7 +717,7 @@ 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
}
@@ -842,7 +842,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
@@ -415,11 +415,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()
@@ -16,12 +16,14 @@ import (
"net/http"
"runtime"
"strconv"
"strings"
"time"

"github.com/gorilla/mux"
"github.com/writeas/impart"
"github.com/writeas/web-core/auth"
"github.com/writeas/web-core/log"
"github.com/writeas/web-core/passgen"
"github.com/writeas/writefreely/appstats"
"github.com/writeas/writefreely/config"
)
@@ -170,11 +172,12 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
Config config.AppCfg
Message string

User *User
Colls []inspectedCollection
LastPost string

TotalPosts int64
User *User
Colls []inspectedCollection
LastPost string
NewPassword string
TotalPosts int64
ClearEmail string
}{
Config: app.cfg.App,
Message: r.FormValue("m"),
@@ -186,6 +189,14 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)}
}

flashes, _ := getSessionFlashes(app, w, r, nil)
for _, flash := range flashes {
if strings.HasPrefix(flash, "SUCCESS: ") {
p.NewPassword = strings.TrimPrefix(flash, "SUCCESS: ")
p.ClearEmail = p.User.EmailClear(app.keys)
}
}
p.UserPage = NewUserPage(app, r, u, p.User.Username, nil)
p.TotalPosts = app.db.GetUserPostsCount(p.User.ID)
lp, err := app.db.GetUserLastPostTime(p.User.ID)
@@ -254,6 +265,38 @@ func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *ht
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}
}

func handleAdminResetUserPass(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
username := vars["username"]
if username == "" {
return impart.HTTPError{http.StatusFound, "/admin/users"}
}

// Generate new random password since none supplied
pass := passgen.NewWordish()
hashedPass, err := auth.HashPass([]byte(pass))
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
}

userIDVal := r.FormValue("user")
log.Info("ADMIN: Changing user %s password", userIDVal)
id, err := strconv.Atoi(userIDVal)
if err != nil {
return impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Invalid user ID: %v", err)}
}

err = app.db.ChangePassphrase(int64(id), true, "", hashedPass)
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
}
log.Info("ADMIN: Successfully changed.")

addSessionFlash(app, w, r, fmt.Sprintf("SUCCESS: %s", pass), nil)

return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s", username)}
}

func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
p := struct {
*UserPage
2 app.go
@@ -56,7 +56,7 @@ var (
debugging bool

// Software version can be set from git env using -ldflags
softwareVer = "0.10.0"
softwareVer = "0.11.0"

// DEPRECATED VARS
isSingleUser bool
@@ -339,7 +339,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")

@@ -464,7 +464,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()}
}
@@ -943,7 +943,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"
@@ -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)
}

2 go.mod
@@ -45,7 +45,7 @@ require (
github.com/writeas/openssl-go v1.0.0 // indirect
github.com/writeas/saturday v1.7.1
github.com/writeas/slug v1.2.0
github.com/writeas/web-core v1.0.0
github.com/writeas/web-core v1.2.0
github.com/writefreely/go-nodeinfo v1.2.0
golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
2 go.sum
@@ -135,6 +135,8 @@ 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/writeas/web-core v1.2.0 h1:CYqvBd+byi1cK4mCr1NZ6CjILuMOFmiFecv+OACcmG0=
github.com/writeas/web-core v1.2.0/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
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=
@@ -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
}
@@ -483,7 +483,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 == "" {
@@ -617,7 +617,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"]

@@ -1118,9 +1118,9 @@ 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
}
@@ -1129,7 +1129,7 @@ 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",
@@ -295,7 +295,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 {
@@ -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"
}
@@ -145,6 +145,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST")
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")
write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST")
@@ -8,7 +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" />
<link rel="canonical" href="{{.CanonicalURL}}" />
<link rel="canonical" href="{{.CanonicalURL .Host}}" />
<meta name="generator" content="WriteFreely">
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
<meta name="description" content="{{.Summary}}">
@@ -25,7 +25,7 @@
<meta property="og:description" content="{{.Summary}}" />
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" />
<meta property="og:type" content="article" />
<meta property="og:url" content="{{.CanonicalURL}}" />
<meta property="og:url" content="{{.CanonicalURL .Host}}" />
<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}}">
@@ -80,7 +80,7 @@ article time.dt-published {
</p>
<nav>
{{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}}
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
{{end}}
</nav>
<hr>
@@ -71,7 +71,7 @@ body#collection header nav.tabs a:first-child {
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p-->
{{/*end*/}}
{{if .PinnedPosts}}<nav class="pinned-posts">
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
{{end}}
</header>

0 comments on commit 53586d9

Please sign in to comment.
You can’t perform that action at this time.