mirror of
https://github.com/writefreely/writefreely
synced 2024-11-24 09:33:11 +00:00
Merge branch 'develop' into draft-list-paging
This commit is contained in:
commit
27b43ac2f1
119 changed files with 19266 additions and 677 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
node_modules
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
|||
[submodule "static/js/mathjax"]
|
||||
path = static/js/mathjax
|
||||
url = https://github.com/mathjax/MathJax.git
|
|
@ -4,7 +4,7 @@ Welcome! We're glad you're interested in contributing to WriteFreely.
|
|||
|
||||
For **questions**, **help**, **feature requests**, and **general discussion**, please use [our forum](https://discuss.write.as).
|
||||
|
||||
For **bug reports**, please [open a GitHub issue](https://github.com/writeas/writefreely/issues/new). See our guide on [submitting bug reports](https://writefreely.org/contribute#bugs).
|
||||
For **bug reports**, please [open a GitHub issue](https://github.com/writefreely/writefreely/issues/new). See our guide on [submitting bug reports](https://writefreely.org/contribute#bugs).
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
@ -80,9 +80,9 @@ We highly value commit messages that follow established form within the project.
|
|||
|
||||
When in doubt, look to our existing git history for examples of good commit messages. Here are a few:
|
||||
|
||||
* [Rename Suspend status to Silence](https://github.com/writeas/writefreely/commit/7e014ca65958750ab703e317b1ce8cfc4aad2d6e)
|
||||
* [Show 404 when remote user not found](https://github.com/writeas/writefreely/commit/867eb53b3596bd7b3f2be3c53a3faf857f4cd36d)
|
||||
* [Fix post deletion on Pleroma](https://github.com/writeas/writefreely/commit/fe82cbb96e3d5c57cfde0db76c28c4ea6dabfe50)
|
||||
* [Rename Suspend status to Silence](https://github.com/writefreely/writefreely/commit/7e014ca65958750ab703e317b1ce8cfc4aad2d6e)
|
||||
* [Show 404 when remote user not found](https://github.com/writefreely/writefreely/commit/867eb53b3596bd7b3f2be3c53a3faf857f4cd36d)
|
||||
* [Fix post deletion on Pleroma](https://github.com/writefreely/writefreely/commit/fe82cbb96e3d5c57cfde0db76c28c4ea6dabfe50)
|
||||
|
||||
### Submitting pull requests
|
||||
|
||||
|
|
26
Dockerfile
26
Dockerfile
|
@ -1,28 +1,30 @@
|
|||
# Build image
|
||||
FROM golang:1.13-alpine as build
|
||||
FROM golang:1.14-alpine as build
|
||||
|
||||
RUN apk add --update nodejs nodejs-npm make g++ git sqlite-dev
|
||||
RUN apk add --update nodejs nodejs-npm make g++ git
|
||||
RUN npm install -g less less-plugin-clean-css
|
||||
RUN go get -u github.com/jteeuwen/go-bindata/...
|
||||
RUN go get -u github.com/go-bindata/go-bindata/...
|
||||
|
||||
RUN mkdir -p /go/src/github.com/writefreely/writefreely
|
||||
WORKDIR /go/src/github.com/writefreely/writefreely
|
||||
|
||||
RUN mkdir -p /go/src/github.com/writeas/writefreely
|
||||
WORKDIR /go/src/github.com/writeas/writefreely
|
||||
COPY . .
|
||||
|
||||
ENV GO111MODULE=on
|
||||
|
||||
RUN make build \
|
||||
&& make ui
|
||||
&& make ui
|
||||
RUN mkdir /stage && \
|
||||
cp -R /go/bin \
|
||||
/go/src/github.com/writeas/writefreely/templates \
|
||||
/go/src/github.com/writeas/writefreely/static \
|
||||
/go/src/github.com/writeas/writefreely/pages \
|
||||
/go/src/github.com/writeas/writefreely/keys \
|
||||
/go/src/github.com/writeas/writefreely/cmd \
|
||||
/go/src/github.com/writefreely/writefreely/templates \
|
||||
/go/src/github.com/writefreely/writefreely/static \
|
||||
/go/src/github.com/writefreely/writefreely/pages \
|
||||
/go/src/github.com/writefreely/writefreely/keys \
|
||||
/go/src/github.com/writefreely/writefreely/cmd \
|
||||
/stage
|
||||
|
||||
# Final image
|
||||
FROM alpine:3.11
|
||||
FROM alpine:3.12
|
||||
|
||||
RUN apk add --no-cache openssl ca-certificates
|
||||
COPY --from=build --chown=daemon:daemon /stage /go
|
||||
|
|
3
Makefile
3
Makefile
|
@ -1,5 +1,5 @@
|
|||
GITREV=`git describe | cut -c 2-`
|
||||
LDFLAGS=-ldflags="-X 'github.com/writeas/writefreely.softwareVer=$(GITREV)'"
|
||||
LDFLAGS=-ldflags="-X 'github.com/writefreely/writefreely.softwareVer=$(GITREV)'"
|
||||
|
||||
GOCMD=go
|
||||
GOINSTALL=$(GOCMD) install $(LDFLAGS)
|
||||
|
@ -131,6 +131,7 @@ release-docker :
|
|||
|
||||
ui : force_look
|
||||
cd less/; $(MAKE) $(MFLAGS)
|
||||
cd prose/; $(MAKE) $(MFLAGS)
|
||||
|
||||
assets : generate
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql
|
||||
|
|
16
README.md
16
README.md
|
@ -4,17 +4,17 @@
|
|||
</p>
|
||||
<hr />
|
||||
<p align="center">
|
||||
<a href="https://github.com/writeas/writefreely/releases/">
|
||||
<a href="https://github.com/writefreely/writefreely/releases/">
|
||||
<img src="https://img.shields.io/github/release/writeas/writefreely.svg" alt="Latest release" />
|
||||
</a>
|
||||
<a href="https://travis-ci.org/writeas/writefreely">
|
||||
<img src="https://travis-ci.org/writeas/writefreely.svg" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/writeas/writefreely/releases/latest">
|
||||
<a href="https://github.com/writefreely/writefreely/releases/latest">
|
||||
<img src="https://img.shields.io/github/downloads/writeas/writefreely/total.svg" />
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/github.com/writeas/writefreely">
|
||||
<img src="https://goreportcard.com/badge/github.com/writeas/writefreely" alt="Go Report Card" />
|
||||
<a href="https://goreportcard.com/report/github.com/writefreely/writefreely">
|
||||
<img src="https://goreportcard.com/badge/github.com/writefreely/writefreely" alt="Go Report Card" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/writeas/writefreely/">
|
||||
<img src="https://img.shields.io/docker/pulls/writeas/writefreely.svg" />
|
||||
|
@ -62,7 +62,7 @@ The quickest way to deploy WriteFreely is with [Write.as](https://write.as/write
|
|||
|
||||
WriteFreely deploys as a static binary on any platform and architecture that Go supports. Just use our built-in SQLite support, or add a MySQL database, and you'll be up and running!
|
||||
|
||||
For common platforms, start with our [pre-built binaries](https://github.com/writeas/writefreely/releases/) and head over to our [installation guide](https://writefreely.org/start) to get started.
|
||||
For common platforms, start with our [pre-built binaries](https://github.com/writefreely/writefreely/releases/) and head over to our [installation guide](https://writefreely.org/start) to get started.
|
||||
|
||||
### Packages
|
||||
|
||||
|
@ -80,10 +80,10 @@ Start hacking on WriteFreely with our [developer setup guide](https://writefreel
|
|||
|
||||
## Contributing
|
||||
|
||||
We gladly welcome contributions to WriteFreely, whether in the form of [code](https://github.com/writeas/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely), [bug reports](https://github.com/writeas/writefreely/issues/new?template=bug_report.md), [feature requests](https://discuss.write.as/c/feedback/feature-requests), [translations](https://poeditor.com/join/project/TIZ6HFRFdE), or [documentation](https://github.com/writefreely/documentation) improvements.
|
||||
We gladly welcome contributions to WriteFreely, whether in the form of [code](https://github.com/writefreely/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely), [bug reports](https://github.com/writefreely/writefreely/issues/new?template=bug_report.md), [feature requests](https://discuss.write.as/c/feedback/feature-requests), [translations](https://poeditor.com/join/project/TIZ6HFRFdE), or [documentation](https://github.com/writefreely/documentation) improvements.
|
||||
|
||||
Before contributing anything, please read our [Contributing Guide](https://github.com/writeas/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely). It describes the correct channels for submitting contributions and any potential requirements.
|
||||
Before contributing anything, please read our [Contributing Guide](https://github.com/writefreely/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely). It describes the correct channels for submitting contributions and any potential requirements.
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2018-2020 [A Bunch Tell LLC](https://abunchtell.com) and contributing authors. Licensed under the [AGPL](https://github.com/writeas/writefreely/blob/develop/LICENSE).
|
||||
Copyright © 2018-2021 [A Bunch Tell LLC](https://abunchtell.com) and contributing authors. Licensed under the [AGPL](https://github.com/writefreely/writefreely/blob/develop/LICENSE).
|
||||
|
|
122
account.go
122
account.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -28,10 +28,9 @@ import (
|
|||
"github.com/writeas/web-core/auth"
|
||||
"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"
|
||||
"github.com/writefreely/writefreely/author"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -50,6 +49,7 @@ type (
|
|||
Separator template.HTML
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
CollAlias string
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -87,6 +87,11 @@ func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||
if app.cfg.App.DisablePasswordAuth {
|
||||
err := ErrDisabledPasswordAuth
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqJSON := IsJSON(r)
|
||||
|
||||
// Get params
|
||||
|
@ -146,8 +151,6 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
}
|
||||
|
||||
// Handle empty optional params
|
||||
// TODO: remove this var
|
||||
createdWithPass := true
|
||||
hashedPass, err := auth.HashPass([]byte(signup.Pass))
|
||||
if err != nil {
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
|
||||
|
@ -157,7 +160,7 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
u := &User{
|
||||
Username: signup.Alias,
|
||||
HashedPass: hashedPass,
|
||||
HasPass: createdWithPass,
|
||||
HasPass: true,
|
||||
Email: prepareUserEmail(signup.Email, app.keys.EmailKey),
|
||||
Created: time.Now().Truncate(time.Second).UTC(),
|
||||
}
|
||||
|
@ -183,9 +186,6 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
resUser := &AuthUser{
|
||||
User: u,
|
||||
}
|
||||
if !createdWithPass {
|
||||
resUser.Password = signup.Pass
|
||||
}
|
||||
title := signup.Alias
|
||||
if signup.Normalize {
|
||||
title = desiredUsername
|
||||
|
@ -300,24 +300,18 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
p := &struct {
|
||||
page.StaticPage
|
||||
To string
|
||||
Message template.HTML
|
||||
Flashes []template.HTML
|
||||
LoginUsername string
|
||||
OauthSlack bool
|
||||
OauthWriteAs bool
|
||||
OauthGitlab bool
|
||||
GitlabDisplayName string
|
||||
*OAuthButtons
|
||||
To string
|
||||
Message template.HTML
|
||||
Flashes []template.HTML
|
||||
LoginUsername string
|
||||
}{
|
||||
pageForReq(app, r),
|
||||
r.FormValue("to"),
|
||||
template.HTML(""),
|
||||
[]template.HTML{},
|
||||
getTempInfo(app, "login-user", r, w),
|
||||
app.Config().SlackOauth.ClientID != "",
|
||||
app.Config().WriteAsOauth.ClientID != "",
|
||||
app.Config().GitlabOauth.ClientID != "",
|
||||
config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName),
|
||||
StaticPage: pageForReq(app, r),
|
||||
OAuthButtons: NewOAuthButtons(app.Config()),
|
||||
To: r.FormValue("to"),
|
||||
Message: template.HTML(""),
|
||||
Flashes: []template.HTML{},
|
||||
LoginUsername: getTempInfo(app, "login-user", r, w),
|
||||
}
|
||||
|
||||
if earlyError != "" {
|
||||
|
@ -392,6 +386,11 @@ func login(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
var err error
|
||||
var signin userCredentials
|
||||
|
||||
if app.cfg.App.DisablePasswordAuth {
|
||||
err := ErrDisabledPasswordAuth
|
||||
return err
|
||||
}
|
||||
|
||||
// Log in with one-time token if one is given
|
||||
if oneTimeToken != "" {
|
||||
log.Info("Login: Logging user in via token.")
|
||||
|
@ -838,6 +837,9 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
|||
return ErrCollectionNotFound
|
||||
}
|
||||
|
||||
// Add collection properties
|
||||
c.MonetizationPointer = app.db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
|
||||
silenced, err := app.db.IsUserSilenced(u.ID)
|
||||
if err != nil {
|
||||
log.Error("view edit collection %v", err)
|
||||
|
@ -853,6 +855,7 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
|||
Collection: c,
|
||||
Silenced: silenced,
|
||||
}
|
||||
obj.UserPage.CollAlias = c.Alias
|
||||
|
||||
showUserPage(w, "collection", obj)
|
||||
return nil
|
||||
|
@ -1032,6 +1035,7 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
|
|||
TopPosts: topPosts,
|
||||
Silenced: silenced,
|
||||
}
|
||||
obj.UserPage.CollAlias = c.Alias
|
||||
if app.cfg.App.Federation {
|
||||
folls, err := app.db.GetAPFollowers(c)
|
||||
if err != nil {
|
||||
|
@ -1062,13 +1066,15 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
enableOauthSlack := app.Config().SlackOauth.ClientID != ""
|
||||
enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != ""
|
||||
enableOauthGitLab := app.Config().GitlabOauth.ClientID != ""
|
||||
enableOauthGeneric := app.Config().GenericOauth.ClientID != ""
|
||||
enableOauthGitea := app.Config().GiteaOauth.ClientID != ""
|
||||
|
||||
oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get oauth accounts for settings: %s", err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
|
||||
}
|
||||
for _, oauthAccount := range oauthAccounts {
|
||||
for idx, oauthAccount := range oauthAccounts {
|
||||
switch oauthAccount.Provider {
|
||||
case "slack":
|
||||
enableOauthSlack = false
|
||||
|
@ -1076,35 +1082,49 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
enableOauthWriteAs = false
|
||||
case "gitlab":
|
||||
enableOauthGitLab = false
|
||||
case "generic":
|
||||
oauthAccounts[idx].DisplayName = app.Config().GenericOauth.DisplayName
|
||||
oauthAccounts[idx].AllowDisconnect = app.Config().GenericOauth.AllowDisconnect
|
||||
enableOauthGeneric = false
|
||||
case "gitea":
|
||||
enableOauthGitea = false
|
||||
}
|
||||
}
|
||||
|
||||
displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || len(oauthAccounts) > 0
|
||||
displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGeneric || enableOauthGitea || len(oauthAccounts) > 0
|
||||
|
||||
obj := struct {
|
||||
*UserPage
|
||||
Email string
|
||||
HasPass bool
|
||||
IsLogOut bool
|
||||
Silenced bool
|
||||
OauthSection bool
|
||||
OauthAccounts []oauthAccountInfo
|
||||
OauthSlack bool
|
||||
OauthWriteAs bool
|
||||
OauthGitLab bool
|
||||
GitLabDisplayName string
|
||||
Email string
|
||||
HasPass bool
|
||||
IsLogOut bool
|
||||
Silenced bool
|
||||
OauthSection bool
|
||||
OauthAccounts []oauthAccountInfo
|
||||
OauthSlack bool
|
||||
OauthWriteAs bool
|
||||
OauthGitLab bool
|
||||
GitLabDisplayName string
|
||||
OauthGeneric bool
|
||||
OauthGenericDisplayName string
|
||||
OauthGitea bool
|
||||
GiteaDisplayName string
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||
Email: fullUser.EmailClear(app.keys),
|
||||
HasPass: passIsSet,
|
||||
IsLogOut: r.FormValue("logout") == "1",
|
||||
Silenced: fullUser.IsSilenced(),
|
||||
OauthSection: displayOauthSection,
|
||||
OauthAccounts: oauthAccounts,
|
||||
OauthSlack: enableOauthSlack,
|
||||
OauthWriteAs: enableOauthWriteAs,
|
||||
OauthGitLab: enableOauthGitLab,
|
||||
GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName),
|
||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||
Email: fullUser.EmailClear(app.keys),
|
||||
HasPass: passIsSet,
|
||||
IsLogOut: r.FormValue("logout") == "1",
|
||||
Silenced: fullUser.IsSilenced(),
|
||||
OauthSection: displayOauthSection,
|
||||
OauthAccounts: oauthAccounts,
|
||||
OauthSlack: enableOauthSlack,
|
||||
OauthWriteAs: enableOauthWriteAs,
|
||||
OauthGitLab: enableOauthGitLab,
|
||||
GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName),
|
||||
OauthGeneric: enableOauthGeneric,
|
||||
OauthGenericDisplayName: config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName),
|
||||
OauthGitea: enableOauthGitea,
|
||||
GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName),
|
||||
}
|
||||
|
||||
showUserPage(w, "settings", obj)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -21,6 +21,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
@ -41,6 +42,19 @@ const (
|
|||
apCacheTime = time.Minute
|
||||
)
|
||||
|
||||
var instanceColl *Collection
|
||||
|
||||
func initActivityPub(app *App) {
|
||||
ur, _ := url.Parse(app.cfg.App.Host)
|
||||
instanceColl = &Collection{
|
||||
ID: 0,
|
||||
Alias: ur.Host,
|
||||
Title: ur.Host,
|
||||
db: app.db,
|
||||
hostName: app.cfg.App.Host,
|
||||
}
|
||||
}
|
||||
|
||||
type RemoteUser struct {
|
||||
ID int64
|
||||
ActorID string
|
||||
|
@ -76,12 +90,17 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
|
|||
|
||||
vars := mux.Vars(r)
|
||||
alias := vars["alias"]
|
||||
if alias == "" {
|
||||
alias = filepath.Base(r.RequestURI)
|
||||
}
|
||||
|
||||
// TODO: enforce visibility
|
||||
// Get base Collection data
|
||||
var c *Collection
|
||||
var err error
|
||||
if app.cfg.App.SingleUser {
|
||||
if alias == r.Host {
|
||||
c = instanceColl
|
||||
} else if app.cfg.App.SingleUser {
|
||||
c, err = app.db.GetCollectionByID(1)
|
||||
} else {
|
||||
c, err = app.db.GetCollection(alias)
|
||||
|
@ -89,16 +108,19 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
silenced, err := app.db.IsUserSilenced(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("fetch collection activities: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if silenced {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
if !c.IsInstanceColl() {
|
||||
silenced, err := app.db.IsUserSilenced(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("fetch collection activities: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if silenced {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
}
|
||||
|
||||
p := c.PersonObject()
|
||||
|
||||
setCacheControl(w, apCacheTime)
|
||||
|
@ -494,7 +516,7 @@ func makeActivityPost(hostName string, p *activitystreams.Person, url string, m
|
|||
|
||||
r, _ := http.NewRequest("POST", url, bytes.NewBuffer(b))
|
||||
r.Header.Add("Content-Type", "application/activity+json")
|
||||
r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
|
||||
r.Header.Set("User-Agent", ServerUserAgent(hostName))
|
||||
h := sha256.New()
|
||||
h.Write(b)
|
||||
r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil)))
|
||||
|
@ -544,7 +566,23 @@ func resolveIRI(hostName, url string) ([]byte, error) {
|
|||
|
||||
r, _ := http.NewRequest("GET", url, nil)
|
||||
r.Header.Add("Accept", "application/activity+json")
|
||||
r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
|
||||
r.Header.Set("User-Agent", ServerUserAgent(hostName))
|
||||
|
||||
p := instanceColl.PersonObject()
|
||||
h := sha256.New()
|
||||
h.Write([]byte{})
|
||||
r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil)))
|
||||
|
||||
// Sign using the 'Signature' header
|
||||
privKey, err := activitypub.DecodePrivateKey(p.GetPrivKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer := httpsig.NewSigner(p.PublicKey.ID, privKey, httpsig.RSASHA256, []string{"(request-target)", "date", "host", "digest"})
|
||||
err = signer.SignSigHeader(r)
|
||||
if err != nil {
|
||||
log.Error("Can't sign: %v", err)
|
||||
}
|
||||
|
||||
if debugging {
|
||||
dump, err := httputil.DumpRequestOut(r, true)
|
||||
|
@ -624,6 +662,16 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error {
|
|||
}
|
||||
|
||||
func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
||||
// If app is private, do not federate
|
||||
if app.cfg.App.Private {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not federate posts from private or protected blogs
|
||||
if p.Collection.Visibility == CollPrivate || p.Collection.Visibility == CollProtected {
|
||||
return nil
|
||||
}
|
||||
|
||||
if debugging {
|
||||
if isUpdate {
|
||||
log.Info("Federating updated post!")
|
||||
|
@ -631,6 +679,7 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
|||
log.Info("Federating new post!")
|
||||
}
|
||||
}
|
||||
|
||||
actor := p.Collection.PersonObject(collID)
|
||||
na := p.ActivityObject(app)
|
||||
|
||||
|
@ -699,6 +748,10 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
|||
// I don't believe we'd ever have too many mentions in a single post that this
|
||||
// could become a burden.
|
||||
remoteUser, err := getRemoteUser(app, tag.HRef)
|
||||
if err != nil {
|
||||
log.Error("Unable to find remote user %s. Skipping: %v", tag.HRef, err)
|
||||
continue
|
||||
}
|
||||
err = makeActivityPost(app.cfg.App.Host, actor, remoteUser.Inbox, activity)
|
||||
if err != nil {
|
||||
log.Error("Couldn't post! %v", err)
|
||||
|
|
10
admin.go
10
admin.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -24,8 +24,8 @@ import (
|
|||
"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"
|
||||
"github.com/writefreely/writefreely/appstats"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -328,6 +328,9 @@ func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *ht
|
|||
err = app.db.SetUserStatus(user.ID, UserActive)
|
||||
} else {
|
||||
err = app.db.SetUserStatus(user.ID, UserSilenced)
|
||||
|
||||
// reset the cache to removed silence user posts
|
||||
updateTimelineCache(app.timeline, true)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("toggle user silenced: %v", err)
|
||||
|
@ -529,6 +532,7 @@ func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *htt
|
|||
}
|
||||
apper.App().cfg.App.Federation = r.FormValue("federation") == "on"
|
||||
apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on"
|
||||
apper.App().cfg.App.Monetization = r.FormValue("monetization") == "on"
|
||||
apper.App().cfg.App.Private = r.FormValue("private") == "on"
|
||||
apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
|
||||
if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil {
|
||||
|
|
26
app.go
26
app.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -35,11 +35,11 @@ import (
|
|||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/converter"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/author"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/key"
|
||||
"github.com/writeas/writefreely/migrations"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/author"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/key"
|
||||
"github.com/writefreely/writefreely/migrations"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
|
@ -238,6 +238,7 @@ func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
p := struct {
|
||||
page.StaticPage
|
||||
*OAuthButtons
|
||||
Flashes []template.HTML
|
||||
Banner template.HTML
|
||||
Content template.HTML
|
||||
|
@ -245,6 +246,7 @@ func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
ForcedLanding bool
|
||||
}{
|
||||
StaticPage: pageForReq(app, r),
|
||||
OAuthButtons: NewOAuthButtons(app.Config()),
|
||||
ForcedLanding: forceLanding,
|
||||
}
|
||||
|
||||
|
@ -387,6 +389,8 @@ func Initialize(apper Apper, debug bool) (*App, error) {
|
|||
return nil, fmt.Errorf("connect to DB: %s", err)
|
||||
}
|
||||
|
||||
initActivityPub(apper.App())
|
||||
|
||||
// Handle local timeline, if enabled
|
||||
if apper.App().cfg.App.LocalTimeline {
|
||||
log.Info("Initializing local timeline...")
|
||||
|
@ -890,3 +894,13 @@ func adminInitDatabase(app *App) error {
|
|||
log.Info("Done.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerUserAgent returns a User-Agent string to use in external requests. The
|
||||
// hostName parameter may be left empty.
|
||||
func ServerUserAgent(hostName string) string {
|
||||
hostUAStr := ""
|
||||
if hostName != "" {
|
||||
hostUAStr = "; +" + hostName
|
||||
}
|
||||
return "Go (" + serverSoftware + "/" + softwareVer + hostUAStr + ")"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,7 +11,7 @@
|
|||
package author
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,9 +11,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,9 +11,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,9 +11,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -15,11 +15,10 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -13,9 +13,8 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,11 +11,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -30,9 +30,9 @@ import (
|
|||
"github.com/writeas/web-core/bots"
|
||||
"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"
|
||||
"github.com/writefreely/writefreely/author"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -47,6 +47,7 @@ type (
|
|||
Language string `schema:"lang" json:"lang,omitempty"`
|
||||
StyleSheet string `datastore:"style_sheet" schema:"style_sheet" json:"style_sheet"`
|
||||
Script string `datastore:"script" schema:"script" json:"script,omitempty"`
|
||||
Signature string `datastore:"post_signature" schema:"signature" json:"-"`
|
||||
Public bool `datastore:"public" json:"public"`
|
||||
Visibility collVisibility `datastore:"private" json:"-"`
|
||||
Format string `datastore:"format" json:"format,omitempty"`
|
||||
|
@ -55,6 +56,8 @@ type (
|
|||
PublicOwner bool `datastore:"public_owner" json:"-"`
|
||||
URL string `json:"url,omitempty"`
|
||||
|
||||
MonetizationPointer string `json:"monetization_pointer,omitempty"`
|
||||
|
||||
db *datastore
|
||||
hostName string
|
||||
}
|
||||
|
@ -86,13 +89,15 @@ type (
|
|||
Handle string `schema:"handle" json:"handle"`
|
||||
|
||||
// Actual collection values updated in the DB
|
||||
Alias *string `schema:"alias" json:"alias"`
|
||||
Title *string `schema:"title" json:"title"`
|
||||
Description *string `schema:"description" json:"description"`
|
||||
StyleSheet *sql.NullString `schema:"style_sheet" json:"style_sheet"`
|
||||
Script *sql.NullString `schema:"script" json:"script"`
|
||||
Visibility *int `schema:"visibility" json:"public"`
|
||||
Format *sql.NullString `schema:"format" json:"format"`
|
||||
Alias *string `schema:"alias" json:"alias"`
|
||||
Title *string `schema:"title" json:"title"`
|
||||
Description *string `schema:"description" json:"description"`
|
||||
StyleSheet *sql.NullString `schema:"style_sheet" json:"style_sheet"`
|
||||
Script *sql.NullString `schema:"script" json:"script"`
|
||||
Signature *sql.NullString `schema:"signature" json:"signature"`
|
||||
Monetization *string `schema:"monetization_pointer" json:"monetization_pointer"`
|
||||
Visibility *int `schema:"visibility" json:"public"`
|
||||
Format *sql.NullString `schema:"format" json:"format"`
|
||||
}
|
||||
CollectionFormat struct {
|
||||
Format string
|
||||
|
@ -175,6 +180,11 @@ func (c *Collection) NewFormat() *CollectionFormat {
|
|||
return cf
|
||||
}
|
||||
|
||||
func (c *Collection) IsInstanceColl() bool {
|
||||
ur, _ := url.Parse(c.hostName)
|
||||
return c.Alias == ur.Host
|
||||
}
|
||||
|
||||
func (c *Collection) IsUnlisted() bool {
|
||||
return c.Visibility == 0
|
||||
}
|
||||
|
@ -230,7 +240,7 @@ 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")
|
||||
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/writefreely/writefreely/issues/new?template=bug_report.md")
|
||||
}
|
||||
if isSingleUser {
|
||||
return c.hostName + "/"
|
||||
|
@ -550,6 +560,7 @@ type CollectionPage struct {
|
|||
IsOwner bool
|
||||
CanPin bool
|
||||
Username string
|
||||
Monetization string
|
||||
Collections *[]Collection
|
||||
PinnedPosts *[]PublicPost
|
||||
IsAdmin bool
|
||||
|
@ -721,14 +732,14 @@ func newDisplayCollection(c *Collection, cr *collectionReq, page int) *DisplayCo
|
|||
return coll
|
||||
}
|
||||
|
||||
// getCollectionPage returns the collection page as an int. If the parsed page value is not
|
||||
// greater than 0 then the default value of 1 is returned.
|
||||
func getCollectionPage(vars map[string]string) int {
|
||||
page := 1
|
||||
var p int
|
||||
p, _ = strconv.Atoi(vars["page"])
|
||||
if p > 0 {
|
||||
page = p
|
||||
if p, _ := strconv.Atoi(vars["page"]); p > 0 {
|
||||
return p
|
||||
}
|
||||
return page
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// handleViewCollection displays the requested Collection
|
||||
|
@ -827,6 +838,7 @@ 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, isOwner)
|
||||
displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer")
|
||||
|
||||
collTmpl := "collection"
|
||||
if app.cfg.App.Chorus {
|
||||
|
@ -945,6 +957,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
|
|||
// Add more data
|
||||
// TODO: fix this mess of collections inside collections
|
||||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner)
|
||||
displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer")
|
||||
|
||||
err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
[server]
|
||||
hidden_host =
|
||||
port = 8080
|
||||
|
||||
[database]
|
||||
type = mysql
|
||||
username = root
|
||||
password = changeme
|
||||
database = writefreely
|
||||
host = db
|
||||
port = 3306
|
||||
tls = false
|
||||
|
||||
[app]
|
||||
site_name = WriteFreely Example Blog!
|
||||
host = http://localhost:8080
|
||||
theme = write
|
||||
disable_js = false
|
||||
webfonts = true
|
||||
single_user = true
|
||||
open_registration = false
|
||||
min_username_len = 3
|
||||
max_blogs = 1
|
||||
federation = true
|
||||
public_stats = true
|
||||
private = false
|
||||
update_checks = true
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -81,6 +81,15 @@ type (
|
|||
CallbackProxyAPI string `ini:"callback_proxy_api"`
|
||||
}
|
||||
|
||||
GiteaOauthCfg struct {
|
||||
ClientID string `ini:"client_id"`
|
||||
ClientSecret string `ini:"client_secret"`
|
||||
Host string `ini:"host"`
|
||||
DisplayName string `ini:"display_name"`
|
||||
CallbackProxy string `ini:"callback_proxy"`
|
||||
CallbackProxyAPI string `ini:"callback_proxy_api"`
|
||||
}
|
||||
|
||||
SlackOauthCfg struct {
|
||||
ClientID string `ini:"client_id"`
|
||||
ClientSecret string `ini:"client_secret"`
|
||||
|
@ -89,6 +98,24 @@ type (
|
|||
CallbackProxyAPI string `ini:"callback_proxy_api"`
|
||||
}
|
||||
|
||||
GenericOauthCfg struct {
|
||||
ClientID string `ini:"client_id"`
|
||||
ClientSecret string `ini:"client_secret"`
|
||||
Host string `ini:"host"`
|
||||
DisplayName string `ini:"display_name"`
|
||||
CallbackProxy string `ini:"callback_proxy"`
|
||||
CallbackProxyAPI string `ini:"callback_proxy_api"`
|
||||
TokenEndpoint string `ini:"token_endpoint"`
|
||||
InspectEndpoint string `ini:"inspect_endpoint"`
|
||||
AuthEndpoint string `ini:"auth_endpoint"`
|
||||
Scope string `ini:"scope"`
|
||||
AllowDisconnect bool `ini:"allow_disconnect"`
|
||||
MapUserID string `ini:"map_user_id"`
|
||||
MapUsername string `ini:"map_username"`
|
||||
MapDisplayName string `ini:"map_display_name"`
|
||||
MapEmail string `ini:"map_email"`
|
||||
}
|
||||
|
||||
// AppCfg holds values that affect how the application functions
|
||||
AppCfg struct {
|
||||
SiteName string `ini:"site_name"`
|
||||
|
@ -115,9 +142,12 @@ type (
|
|||
MinUsernameLen int `ini:"min_username_len"`
|
||||
MaxBlogs int `ini:"max_blogs"`
|
||||
|
||||
// Options for public instances
|
||||
// Federation
|
||||
Federation bool `ini:"federation"`
|
||||
PublicStats bool `ini:"public_stats"`
|
||||
Federation bool `ini:"federation"`
|
||||
PublicStats bool `ini:"public_stats"`
|
||||
Monetization bool `ini:"monetization"`
|
||||
NotesOnly bool `ini:"notes_only"`
|
||||
|
||||
// Access
|
||||
Private bool `ini:"private"`
|
||||
|
@ -131,6 +161,9 @@ type (
|
|||
|
||||
// Check for Updates
|
||||
UpdateChecks bool `ini:"update_checks"`
|
||||
|
||||
// Disable password authentication if use only Oauth
|
||||
DisablePasswordAuth bool `ini:"disable_password_auth"`
|
||||
}
|
||||
|
||||
// Config holds the complete configuration for running a writefreely instance
|
||||
|
@ -141,6 +174,8 @@ type (
|
|||
SlackOauth SlackOauthCfg `ini:"oauth.slack"`
|
||||
WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"`
|
||||
GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"`
|
||||
GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"`
|
||||
GenericOauth GenericOauthCfg `ini:"oauth.generic"`
|
||||
}
|
||||
)
|
||||
|
||||
|
|
102
database.go
102
database.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -14,7 +14,8 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
"github.com/writeas/web-core/silobridge"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -31,9 +32,9 @@ import (
|
|||
"github.com/writeas/web-core/id"
|
||||
"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"
|
||||
"github.com/writefreely/writefreely/author"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/key"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -637,13 +638,17 @@ func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Pos
|
|||
ownerCollID.Int64 = collID
|
||||
ownerCollID.Valid = true
|
||||
var slugVal string
|
||||
if post.Title != nil && *post.Title != "" {
|
||||
slugVal = getSlug(*post.Title, post.Language.String)
|
||||
if slugVal == "" {
|
||||
if post.Slug != nil && *post.Slug != "" {
|
||||
slugVal = *post.Slug
|
||||
} else {
|
||||
if post.Title != nil && *post.Title != "" {
|
||||
slugVal = getSlug(*post.Title, post.Language.String)
|
||||
if slugVal == "" {
|
||||
slugVal = getSlug(*post.Content, post.Language.String)
|
||||
}
|
||||
} else {
|
||||
slugVal = getSlug(*post.Content, post.Language.String)
|
||||
}
|
||||
} else {
|
||||
slugVal = getSlug(*post.Content, post.Language.String)
|
||||
}
|
||||
if slugVal == "" {
|
||||
slugVal = friendlyID
|
||||
|
@ -791,10 +796,10 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll
|
|||
c := &Collection{}
|
||||
|
||||
// FIXME: change Collection to reflect database values. Add helper functions to get actual values
|
||||
var styleSheet, script, format zero.String
|
||||
row := db.QueryRow("SELECT id, alias, title, description, style_sheet, script, format, owner_id, privacy, view_count FROM collections WHERE "+condition, value)
|
||||
var styleSheet, script, signature, format zero.String
|
||||
row := db.QueryRow("SELECT id, alias, title, description, style_sheet, script, post_signature, format, owner_id, privacy, view_count FROM collections WHERE "+condition, value)
|
||||
|
||||
err := row.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &styleSheet, &script, &format, &c.OwnerID, &c.Visibility, &c.Views)
|
||||
err := row.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &styleSheet, &script, &signature, &format, &c.OwnerID, &c.Visibility, &c.Views)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return nil, impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."}
|
||||
|
@ -806,6 +811,7 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll
|
|||
}
|
||||
c.StyleSheet = styleSheet.String
|
||||
c.Script = script.String
|
||||
c.Signature = signature.String
|
||||
c.Format = format.String
|
||||
c.Public = c.IsPublic()
|
||||
|
||||
|
@ -849,7 +855,8 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
|
|||
SetStringPtr(c.Title, "title").
|
||||
SetStringPtr(c.Description, "description").
|
||||
SetNullString(c.StyleSheet, "style_sheet").
|
||||
SetNullString(c.Script, "script")
|
||||
SetNullString(c.Script, "script").
|
||||
SetNullString(c.Signature, "post_signature")
|
||||
|
||||
if c.Format != nil {
|
||||
cf := &CollectionFormat{Format: c.Format.String}
|
||||
|
@ -902,6 +909,29 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
|
|||
}
|
||||
}
|
||||
|
||||
// Update Monetization value
|
||||
if c.Monetization != nil {
|
||||
skipUpdate := false
|
||||
if *c.Monetization != "" {
|
||||
// Strip away any excess spaces
|
||||
trimmed := strings.TrimSpace(*c.Monetization)
|
||||
// Only update value when it starts with "$", per spec: https://paymentpointers.org
|
||||
if strings.HasPrefix(trimmed, "$") {
|
||||
c.Monetization = &trimmed
|
||||
} else {
|
||||
// Value appears invalid, so don't update
|
||||
skipUpdate = true
|
||||
}
|
||||
}
|
||||
if !skipUpdate {
|
||||
_, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?", collID, "monetization_pointer", *c.Monetization, *c.Monetization)
|
||||
if err != nil {
|
||||
log.Error("Unable to insert monetization_pointer value: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update rest of the collection data
|
||||
res, err = db.Exec("UPDATE collections SET "+q.Updates+" WHERE "+q.Conditions, q.Params...)
|
||||
if err != nil {
|
||||
|
@ -1150,6 +1180,7 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu
|
|||
break
|
||||
}
|
||||
p.extractData()
|
||||
p.augmentContent(c)
|
||||
p.formatContent(cfg, c, includeFuture)
|
||||
|
||||
posts = append(posts, p.processPost())
|
||||
|
@ -1214,6 +1245,7 @@ func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag strin
|
|||
break
|
||||
}
|
||||
p.extractData()
|
||||
p.augmentContent(c)
|
||||
p.formatContent(cfg, c, includeFuture)
|
||||
|
||||
posts = append(posts, p.processPost())
|
||||
|
@ -1590,6 +1622,7 @@ func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[
|
|||
break
|
||||
}
|
||||
p.extractData()
|
||||
p.augmentContent(&coll.Collection)
|
||||
|
||||
pp := p.processPost()
|
||||
pp.Collection = coll
|
||||
|
@ -2167,6 +2200,28 @@ func (db *datastore) CollectionHasAttribute(id int64, attr string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (db *datastore) GetCollectionAttribute(id int64, attr string) string {
|
||||
var v string
|
||||
err := db.QueryRow("SELECT value FROM collectionattributes WHERE collection_id = ? AND attribute = ?", id, attr).Scan(&v)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return ""
|
||||
case err != nil:
|
||||
log.Error("Couldn't SELECT value in getCollectionAttribute for attribute '%s': %v", attr, err)
|
||||
return ""
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (db *datastore) SetCollectionAttribute(id int64, attr, v string) error {
|
||||
_, err := db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?)", id, attr, v)
|
||||
if err != nil {
|
||||
log.Error("Unable to INSERT into collectionattributes: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAccount will delete the entire account for userID
|
||||
func (db *datastore) DeleteAccount(userID int64) error {
|
||||
// Get all collections
|
||||
|
@ -2632,9 +2687,11 @@ func (db *datastore) GetIDForRemoteUser(ctx context.Context, remoteUserID, provi
|
|||
}
|
||||
|
||||
type oauthAccountInfo struct {
|
||||
Provider string
|
||||
ClientID string
|
||||
RemoteUserID string
|
||||
Provider string
|
||||
ClientID string
|
||||
RemoteUserID string
|
||||
DisplayName string
|
||||
AllowDisconnect bool
|
||||
}
|
||||
|
||||
func (db *datastore) GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error) {
|
||||
|
@ -2697,6 +2754,17 @@ func handleFailedPostInsert(err error) error {
|
|||
func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) {
|
||||
handle = strings.TrimLeft(handle, "@")
|
||||
actorIRI := ""
|
||||
parts := strings.Split(handle, "@")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid handle format")
|
||||
}
|
||||
domain := parts[1]
|
||||
|
||||
// Check non-AP instances
|
||||
if siloProfileURL := silobridge.Profile(parts[0], domain); siloProfileURL != "" {
|
||||
return siloProfileURL, nil
|
||||
}
|
||||
|
||||
remoteUser, err := getRemoteUserFromHandle(app, handle)
|
||||
if err != nil {
|
||||
// can't find using handle in the table but the table may already have this user without
|
||||
|
|
|
@ -1,32 +1,47 @@
|
|||
version: "3"
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
volumes:
|
||||
- "web-data:/go/src/app"
|
||||
- "./config.ini.example:/go/src/app/config.ini"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- writefreely
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: "mariadb:latest"
|
||||
volumes:
|
||||
- "./schema.sql:/tmp/schema.sql"
|
||||
- db-data:/var/lib/mysql/data
|
||||
networks:
|
||||
- writefreely
|
||||
environment:
|
||||
- MYSQL_DATABASE=writefreely
|
||||
- MYSQL_ROOT_PASSWORD=changeme
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
web-data:
|
||||
web-keys:
|
||||
db-data:
|
||||
|
||||
networks:
|
||||
writefreely:
|
||||
external_writefreely:
|
||||
internal_writefreely:
|
||||
internal: true
|
||||
|
||||
services:
|
||||
writefreely-web:
|
||||
container_name: "writefreely-web"
|
||||
image: "writeas/writefreely:latest"
|
||||
|
||||
volumes:
|
||||
- "web-keys:/go/keys"
|
||||
- "./config.ini:/go/config.ini"
|
||||
|
||||
networks:
|
||||
- "internal_writefreely"
|
||||
- "external_writefreely"
|
||||
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
depends_on:
|
||||
- "writefreely-db"
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
writefreely-db:
|
||||
container_name: "writefreely-db"
|
||||
image: "mariadb:latest"
|
||||
|
||||
volumes:
|
||||
- "db-data:/var/lib/mysql/data"
|
||||
|
||||
networks:
|
||||
- "internal_writefreely"
|
||||
|
||||
environment:
|
||||
- MYSQL_DATABASE=writefreely
|
||||
- MYSQL_ROOT_PASSWORD=changeme
|
||||
|
||||
restart: unless-stopped
|
||||
|
|
|
@ -52,6 +52,8 @@ var (
|
|||
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
|
||||
|
||||
ErrUserSilenced = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
|
||||
|
||||
ErrDisabledPasswordAuth = impart.HTTPError{http.StatusForbidden, "Password authentication is disabled."}
|
||||
)
|
||||
|
||||
// Post operation errors
|
||||
|
|
4
feed.go
4
feed.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -104,7 +104,7 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
|||
Title: title,
|
||||
Link: &Link{Href: permalink},
|
||||
Description: "<![CDATA[" + stripmd.Strip(p.Content) + "]]>",
|
||||
Content: applyMarkdown([]byte(p.Content), "", app.cfg),
|
||||
Content: string(p.HTMLContent),
|
||||
Author: &Author{author, ""},
|
||||
Created: p.Created,
|
||||
Updated: p.Updated,
|
||||
|
|
40
go.mod
40
go.mod
|
@ -1,41 +1,34 @@
|
|||
module github.com/writeas/writefreely
|
||||
module github.com/writefreely/writefreely
|
||||
|
||||
require (
|
||||
github.com/alecthomas/gometalinter v3.0.0+incompatible // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/fatih/color v1.10.0
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-test/deep v1.0.1 // 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.1
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/gorilla/schema v1.1.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/schema v1.2.0
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/guregu/null v3.5.0+incompatible
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2
|
||||
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.7.0
|
||||
github.com/mattn/go-colorable v0.1.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.3
|
||||
github.com/mitchellh/go-wordwrap v1.0.0
|
||||
github.com/nicksnyder/go-i18n v1.10.0 // indirect
|
||||
github.com/manifoldco/promptui v0.8.0
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/microcosm-cc/bluemonday v1.0.5
|
||||
github.com/mitchellh/go-wordwrap v1.0.1
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
||||
github.com/pelletier/go-toml v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
github.com/writeas/activity v0.1.2
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
|
@ -47,16 +40,11 @@ require (
|
|||
github.com/writeas/nerds v1.0.0
|
||||
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320
|
||||
github.com/writeas/slug v1.2.0
|
||||
github.com/writeas/web-core v1.2.0
|
||||
github.com/writeas/web-core v1.3.0
|
||||
github.com/writefreely/go-nodeinfo v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
|
||||
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 // indirect
|
||||
google.golang.org/appengine v1.4.0 // indirect
|
||||
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect
|
||||
gopkg.in/ini.v1 v1.57.0
|
||||
src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b // indirect
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
|
152
go.sum
152
go.sum
|
@ -1,14 +1,6 @@
|
|||
code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs=
|
||||
code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
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=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
|
@ -26,8 +18,6 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
|||
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=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -37,64 +27,45 @@ github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0
|
|||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
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 h1:6F2OxRVnNTN4OPN+Mc2jxs2WEay9/qiHT/jphlvAwIY=
|
||||
github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe h1:U71giCx5NjRn4Lb71UuprPHqhjxGv3Jqonb9fgcaJH8=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
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-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
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/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=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
|
||||
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
|
||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
|
||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/feeds v1.1.0 h1:pcgLJhbdYgaUESnj3AmXPcB7cS3vy63+jC/TI14AGXk=
|
||||
github.com/gorilla/feeds v1.1.0/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA=
|
||||
github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
|
||||
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM=
|
||||
github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
|
||||
github.com/guregu/null v3.5.0+incompatible h1:fSdvRTQtmBA4B4YDZXhLtxTIJZYuUxBFTTHS4B9djG4=
|
||||
github.com/guregu/null v3.5.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
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/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=
|
||||
|
@ -106,47 +77,30 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec h1:ZXWuspqypleMuJy4bzYEqlMhJnGAYpLrWe5p7W3CdvI=
|
||||
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec/go.mod h1:voECJzdraJmolzPBgL9Z7ANwXf4oMXaTCsIkdiPpR/g=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/manifoldco/promptui v0.3.2 h1:rir7oByTERac6jhpHUPErHuopoRDvO3jxS+FdadEns8=
|
||||
github.com/manifoldco/promptui v0.3.2/go.mod h1:8JU+igZ+eeiiRku4T5BjtKh2ms8sziGpSYl1gN8Bazw=
|
||||
github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4=
|
||||
github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
|
||||
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o=
|
||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
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/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.3 h1:EjVH7OqbU219kdm8acbveoclh2zZFqPJTJw6VUlTLAQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/nicksnyder/go-i18n v1.10.0 h1:5AzlPKvXBH4qBzmZ09Ua9Gipyruv6uApMcrNZdo96+Q=
|
||||
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
|
||||
github.com/microcosm-cc/bluemonday v1.0.5 h1:cF59UCKMmmUgqN1baLvqU/B1ZsMori+duLVTLpgiG3w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.5/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prologic/go-gopher v0.0.0-20191226035442-664dbdb49f44 h1:q5sit1FpzEt59aM2Fd2lSBKF+nxcY1o0StRCiJa/pWo=
|
||||
github.com/prologic/go-gopher v0.0.0-20191226035442-664dbdb49f44/go.mod h1:a97DSBRiRljeRVd5CRZL5bYCIeeGjSEngGf+QMR2evA=
|
||||
github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469 h1:rAbv2gekFbUcjhUkruwo0vMJ0JqhUgg9tz7t+bxHbN4=
|
||||
github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469/go.mod h1:c61IFFAJw8ADWu54tti30Tj5VrBstVoTprmET35UEkY=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||
|
@ -159,20 +113,12 @@ github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PX
|
|||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c=
|
||||
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
|
||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY=
|
||||
github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0=
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 h1:BiSivIxLQFcKoUorpNN3rNwwFG5bITPnqUSyIccfdh0=
|
||||
|
@ -187,12 +133,9 @@ github.com/writeas/go-writeas/v2 v2.0.2 h1:akvdMg89U5oBJiCkBwOXljVLTqP354uN6qnG2
|
|||
github.com/writeas/go-writeas/v2 v2.0.2/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M=
|
||||
github.com/writeas/httpsig v1.0.0 h1:peIAoIA3DmlP8IG8tMNZqI4YD1uEnWBmkcC9OFPjt3A=
|
||||
github.com/writeas/httpsig v1.0.0/go.mod h1:7ClMGSrSVXJbmiLa17bZ1LrG1oibGZmUMlh3402flPY=
|
||||
github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE=
|
||||
github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y=
|
||||
github.com/writeas/impart v1.1.1 h1:RyA9+CqbdbDuz53k+nXCWUY+NlEkdyw6+nWanxSBl5o=
|
||||
github.com/writeas/impart v1.1.1/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y=
|
||||
github.com/writeas/import v0.2.0 h1:Ov23JW9Rnjxk06rki1Spar45bNX647HhwhAZj3flJiY=
|
||||
github.com/writeas/import v0.2.0/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM=
|
||||
github.com/writeas/import v0.2.1 h1:3k+bDNCyqaWdZinyUZtEO4je3mR6fr/nE4ozTh9/9Wg=
|
||||
github.com/writeas/import v0.2.1/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM=
|
||||
github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 h1:baEp0631C8sT2r/hqwypIw2snCFZa6h7U6TojoLHu/c=
|
||||
|
@ -202,66 +145,41 @@ github.com/writeas/nerds v1.0.0/go.mod h1:Gn2bHy1EwRcpXeB7ZhVmuUwiweK0e+JllNf66g
|
|||
github.com/writeas/openssl-go v1.0.0 h1:YXM1tDXeYOlTyJjoMlYLQH1xOloUimSR1WMF8kjFc5o=
|
||||
github.com/writeas/openssl-go v1.0.0/go.mod h1:WsKeK5jYl0B5y8ggOmtVjbmb+3rEGqSD25TppjJnETA=
|
||||
github.com/writeas/saturday v1.6.0/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ=
|
||||
github.com/writeas/saturday v1.7.1 h1:lYo1EH6CYyrFObQoA9RNWHVlpZA5iYL5Opxo7PYAnZE=
|
||||
github.com/writeas/saturday v1.7.1/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ=
|
||||
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320 h1:PozPZ29CQ/xt6ym/+FvIz+KvKEObSSc5ye+95zbTjVU=
|
||||
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ=
|
||||
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.2.0 h1:CYqvBd+byi1cK4mCr1NZ6CjILuMOFmiFecv+OACcmG0=
|
||||
github.com/writeas/web-core v1.2.0/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
|
||||
github.com/writeas/web-core v1.3.0 h1:oEVJBPZhrFMf9eya+DvFUX1i8NZGZe1eu5sQXnhuzKw=
|
||||
github.com/writeas/web-core v1.3.0/go.mod h1:DzNxa0YLV/wNeeWeHFPNa/nHmyJBFIIzXN/m9PpDm5c=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 h1:nVJ3guKA9qdkEQ3TUdXI9QSINo2CUPM/cySEvw2w8I0=
|
||||
golang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 h1:rJm0LuqUjoDhSk2zO9ISMSToQxGz7Os2jRiOL8AWu4c=
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 h1:bPP/rGuN1LUM0eaEwo6vnP6OfIWJzJBulzGUiKLjjSY=
|
||||
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
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/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/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
|
||||
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
|
|
10
gopher.go
10
gopher.go
|
@ -14,6 +14,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/prologic/go-gopher"
|
||||
|
@ -28,6 +29,11 @@ func initGopher(apper Apper) {
|
|||
gopher.ListenAndServe(fmt.Sprintf(":%d", apper.App().Config().Server.GopherPort), nil)
|
||||
}
|
||||
|
||||
// Utility function to strip the URL from the hostname provided by app.cfg.App.Host
|
||||
func stripHostProtocol(app *App) string {
|
||||
return string(regexp.MustCompile("^.*://").ReplaceAll([]byte(app.cfg.App.Host), []byte("")))
|
||||
}
|
||||
|
||||
func handleGopher(app *App, w gopher.ResponseWriter, r *gopher.Request) error {
|
||||
parts := strings.Split(r.Selector, "/")
|
||||
if app.cfg.App.SingleUser {
|
||||
|
@ -51,6 +57,8 @@ func handleGopher(app *App, w gopher.ResponseWriter, r *gopher.Request) error {
|
|||
|
||||
for _, c := range *colls {
|
||||
w.WriteItem(&gopher.Item{
|
||||
Host: stripHostProtocol(app),
|
||||
Port: app.cfg.Server.GopherPort,
|
||||
Type: gopher.DIRECTORY,
|
||||
Description: c.DisplayTitle(),
|
||||
Selector: "/" + c.Alias + "/",
|
||||
|
@ -99,6 +107,8 @@ func handleGopherCollection(app *App, w gopher.ResponseWriter, r *gopher.Request
|
|||
|
||||
for _, p := range *posts {
|
||||
w.WriteItem(&gopher.Item{
|
||||
Port: app.cfg.Server.GopherPort,
|
||||
Host: stripHostProtocol(app),
|
||||
Type: gopher.FILE,
|
||||
Description: p.CreatedDate() + " - " + p.DisplayTitle(),
|
||||
Selector: baseSel + p.Slug.String,
|
||||
|
|
16
handle.go
16
handle.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -24,8 +24,8 @@ import (
|
|||
"github.com/prologic/go-gopher"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
// UserLevel represents the required user level for accessing an endpoint
|
||||
|
@ -621,6 +621,9 @@ func (h *Handler) AllReader(f handlerFunc) http.HandlerFunc {
|
|||
log.Info(h.app.ReqLog(r, status, time.Since(start)))
|
||||
}()
|
||||
|
||||
// Allow any origin, as public endpoints are handled in here
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
if h.app.App().cfg.App.Private {
|
||||
// This instance is private, so ensure it's being accessed by a valid user
|
||||
// Check if authenticated with an access token
|
||||
|
@ -943,3 +946,10 @@ func sendRedirect(w http.ResponseWriter, code int, location string) int {
|
|||
w.WriteHeader(code)
|
||||
return code
|
||||
}
|
||||
|
||||
func cacheControl(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
12
invites.go
12
invites.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -21,7 +21,7 @@ import (
|
|||
"github.com/writeas/impart"
|
||||
"github.com/writeas/nerds/store"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
type Invite struct {
|
||||
|
@ -170,14 +170,14 @@ func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
p := struct {
|
||||
page.StaticPage
|
||||
*OAuthButtons
|
||||
Error string
|
||||
Flashes []template.HTML
|
||||
Invite string
|
||||
OAuth *OAuthButtons
|
||||
}{
|
||||
StaticPage: pageForReq(app, r),
|
||||
Invite: inviteCode,
|
||||
OAuth: NewOAuthButtons(app.cfg),
|
||||
StaticPage: pageForReq(app, r),
|
||||
OAuthButtons: NewOAuthButtons(app.cfg),
|
||||
Invite: inviteCode,
|
||||
}
|
||||
|
||||
if expired {
|
||||
|
|
4
keys.go
4
keys.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019, 2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,7 +12,7 @@ package writefreely
|
|||
|
||||
import (
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/key"
|
||||
"github.com/writefreely/writefreely/key"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
|
@ -5,6 +5,7 @@ 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
|
||||
lessc prose.less --clean-css="--s1 --advanced" $(CSSDIR)prose.css
|
||||
|
||||
install :
|
||||
./install-less.sh
|
||||
|
|
|
@ -32,6 +32,19 @@ nav#admin {
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&:not(.pages) {
|
||||
display: block;
|
||||
margin: 0.5em 0;
|
||||
a {
|
||||
margin-left: 0;
|
||||
.rounded(.25em);
|
||||
|
||||
&+a {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
font-family: @sansFont;
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
@import "admin";
|
||||
@import "login";
|
||||
@import "pages/error";
|
||||
@import "resources";
|
||||
@import "lib/elements";
|
||||
@import "lib/material";
|
||||
|
|
|
@ -1,15 +1,3 @@
|
|||
@primary: rgb(114, 120, 191);
|
||||
@secondary: rgb(114, 191, 133);
|
||||
@subheaders: #444;
|
||||
@headerTextColor: black;
|
||||
@sansFont: 'Open Sans', 'Segoe UI', Tahoma, Arial, sans-serif;
|
||||
@serifFont: Lora, 'Palatino Linotype', 'Book Antiqua', 'New York', 'DejaVu serif', serif;
|
||||
@monoFont: Hack, consolas, Menlo-Regular, Menlo, Monaco, 'ubuntu mono', monospace, monospace;
|
||||
@dangerCol: #e21d27;
|
||||
@errUrgentCol: #ecc63c;
|
||||
@proSelectedCol: #71D571;
|
||||
@textLinkColor: rgb(0, 0, 238);
|
||||
|
||||
body {
|
||||
font-family: @serifFont;
|
||||
font-size-adjust: 0.5;
|
||||
|
@ -81,7 +69,7 @@ body {
|
|||
font-size: 1.5em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.17em;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -527,7 +515,7 @@ pre, body#post article, #post .alert, #subpage .alert, body#collection article,
|
|||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
textarea, pre, body#post article, body#collection article p {
|
||||
textarea, input#title, pre, body#post article, body#collection article p {
|
||||
&.norm, &.sans, &.wrap {
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap; /* CSS 3 */
|
||||
|
@ -537,7 +525,7 @@ textarea, pre, body#post article, body#collection article p {
|
|||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
}
|
||||
textarea, pre, body#post article, body#collection article, body#subpage article, span, .font {
|
||||
textarea, input#title, pre, body#post article, body#collection article, body#subpage article, span, .font {
|
||||
&.norm {
|
||||
font-family: @serifFont;
|
||||
}
|
||||
|
@ -743,6 +731,18 @@ input, button, select.inputform, textarea.inputform, a.btn {
|
|||
}
|
||||
}
|
||||
|
||||
.btn.pager {
|
||||
border: 1px solid @lightNavBorder;
|
||||
font-size: .86em;
|
||||
padding: .5em 1em;
|
||||
white-space: nowrap;
|
||||
font-family: @sansFont;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: @lightNavBorder;
|
||||
}
|
||||
}
|
||||
|
||||
div.flat-select {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
@ -965,7 +965,12 @@ footer.contain-me {
|
|||
}
|
||||
ul {
|
||||
&.collections {
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
li {
|
||||
&.collection {
|
||||
a.title {
|
||||
|
@ -1095,7 +1100,8 @@ body#pad-sub #posts, .atoms {
|
|||
}
|
||||
.electron {
|
||||
font-weight: normal;
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.86em;
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
}
|
||||
h3, h4 {
|
||||
|
@ -1245,7 +1251,7 @@ header {
|
|||
}
|
||||
}
|
||||
&.singleuser {
|
||||
margin: 0.5em 0.25em;
|
||||
margin: 0.5em 1em 0.5em 0.25em;
|
||||
nav#user-nav {
|
||||
nav > ul > li:first-child {
|
||||
img {
|
||||
|
@ -1253,6 +1259,9 @@ header {
|
|||
}
|
||||
}
|
||||
}
|
||||
.right-side {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
}
|
||||
.dash-nav {
|
||||
font-weight: bold;
|
||||
|
@ -1547,3 +1556,26 @@ div.row {
|
|||
pre.code-block {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#org-nav {
|
||||
font-family: @sansFont;
|
||||
font-size: 1.1em;
|
||||
color: #888;
|
||||
|
||||
em, strong {
|
||||
color: #000;
|
||||
}
|
||||
&+h1 {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
a:link, a:visited, a:hover {
|
||||
color: @accent;
|
||||
}
|
||||
a:first-child {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
a.coll-name {
|
||||
font-weight: bold;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Install Less via npm
|
||||
if [ ! -e "$(which lessc)" ]; then
|
||||
sudo npm install -g less
|
||||
sudo npm install -g less@3.5.3
|
||||
sudo npm install -g less-plugin-clean-css
|
||||
else
|
||||
echo LESS $(npm view less version 2>&1 | grep -v WARN) is installed
|
||||
|
|
|
@ -9,18 +9,64 @@
|
|||
*/
|
||||
|
||||
.row.signinbtns {
|
||||
justify-content: space-evenly;
|
||||
justify-content: center;
|
||||
font-size: 1em;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.loginbtn {
|
||||
height: 40px;
|
||||
}
|
||||
margin: 0.5em;
|
||||
|
||||
#writeas-login, #gitlab-login {
|
||||
box-sizing: border-box;
|
||||
font-size: 17px;
|
||||
&.btn {
|
||||
box-sizing: border-box;
|
||||
font-size: 17px;
|
||||
white-space: nowrap;
|
||||
|
||||
img {
|
||||
height: 1.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&#writeas-login, &#slack-login {
|
||||
img {
|
||||
margin-top: -0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
&#gitlab-login {
|
||||
background-color: #fc6d26;
|
||||
border-color: #fc6d26;
|
||||
&:hover {
|
||||
background-color: darken(#fc6d26, 5%);
|
||||
border-color: darken(#fc6d26, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&#gitea-login {
|
||||
background-color: #2ecc71;
|
||||
border-color: #2ecc71;
|
||||
&:hover {
|
||||
background-color: #2cc26b;
|
||||
border-color: #2cc26b;
|
||||
}
|
||||
}
|
||||
|
||||
&#slack-login, &#gitlab-login, &#gitea-login, &#generic-oauth-login {
|
||||
font-size: 0.86em;
|
||||
font-family: @sansFont;
|
||||
}
|
||||
|
||||
&#slack-login, &#generic-oauth-login {
|
||||
color: @lightTextColor;
|
||||
background-color: @lightNavBG;
|
||||
border-color: @lightNavBorder;
|
||||
&:hover {
|
||||
background-color: @lightNavHoverBG;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,6 @@ textarea {
|
|||
&.collection {
|
||||
a.title {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,18 +188,18 @@ body#pad, body#pad-sub {
|
|||
body#pad {
|
||||
.pad-theme-transition;
|
||||
|
||||
textarea {
|
||||
textarea, #title {
|
||||
.pad-theme-transition;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
textarea {
|
||||
textarea, #title, #editor {
|
||||
background-color: @darkBG;
|
||||
color: @darkTextColor;
|
||||
}
|
||||
}
|
||||
&.light {
|
||||
textarea {
|
||||
textarea, #title, #editor {
|
||||
background-color: @lightBG;
|
||||
color: @lightTextColor;
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
&:hover {
|
||||
background: @lightNavHoverBG;
|
||||
}
|
||||
&:hover > ul {
|
||||
&:hover > ul, &.open > ul {
|
||||
display: block;
|
||||
}
|
||||
&.selected {
|
||||
|
@ -256,7 +256,7 @@ body#pad {
|
|||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
textarea {
|
||||
textarea, #title {
|
||||
position: fixed !important;
|
||||
top: 3em;
|
||||
right: 0;
|
||||
|
@ -385,6 +385,14 @@ body#pad .alert {
|
|||
top: 2.25em;
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
&.classic {
|
||||
#editor {
|
||||
top: 5.25em;
|
||||
}
|
||||
#title {
|
||||
top: 3.5rem;
|
||||
}
|
||||
}
|
||||
#tools {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
|
@ -438,8 +446,8 @@ body#pad .alert {
|
|||
}
|
||||
|
||||
@media all and (min-width: 50em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
|
@ -450,8 +458,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 60em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 15%;
|
||||
padding-right: 15%;
|
||||
}
|
||||
|
@ -462,8 +470,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 70em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 20%;
|
||||
padding-right: 20%;
|
||||
}
|
||||
|
@ -474,8 +482,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 85em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 25%;
|
||||
padding-right: 25%;
|
||||
}
|
||||
|
@ -486,8 +494,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 105em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 30%;
|
||||
padding-right: 30%;
|
||||
}
|
||||
|
|
450
less/prose-editor.less
Normal file
450
less/prose-editor.less
Normal file
|
@ -0,0 +1,450 @@
|
|||
body#pad.classic {
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
#editor {
|
||||
top: 4em;
|
||||
}
|
||||
#title {
|
||||
top: 4.25rem;
|
||||
bottom: unset;
|
||||
height: auto;
|
||||
font-weight: bold;
|
||||
font-size: 2em;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border: 0;
|
||||
}
|
||||
#tools {
|
||||
#belt {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
#target {
|
||||
ul {
|
||||
a {
|
||||
padding: 0 0.5em !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
height: calc(~"100% - 1.6em");
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
font-size: 1.2em;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
font-variant-ligatures: none;
|
||||
padding: 0.5em 0;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::-moz-selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-selectednode {
|
||||
outline: 2px solid #8cf;
|
||||
}
|
||||
|
||||
/* Make sure li selections wrap around markers */
|
||||
|
||||
li.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
li.ProseMirror-selectednode:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
right: -2px;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
border: 2px solid #8cf;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ProseMirror-textblock-dropdown {
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu {
|
||||
margin: 0 -4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ProseMirror-tooltip .ProseMirror-menu {
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
margin-right: 3px;
|
||||
display: inline-block;
|
||||
div {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-menuseparator {
|
||||
border-right: 1px solid #ddd;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown {
|
||||
vertical-align: 1px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-wrap {
|
||||
padding: 1px 0 1px 4px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown:after {
|
||||
content: "";
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 2px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
color: #666;
|
||||
border: 1px solid #aaa;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
z-index: 15;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item {
|
||||
cursor: pointer;
|
||||
padding: 2px 8px 2px 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item:hover {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap {
|
||||
position: relative;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-label:after {
|
||||
content: "";
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu {
|
||||
display: none;
|
||||
min-width: 4em;
|
||||
left: 100%;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
position: relative;
|
||||
min-height: 1em;
|
||||
color: #666;
|
||||
padding: 0.5em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 10;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ProseMirror-icon {
|
||||
display: inline-block;
|
||||
line-height: .8;
|
||||
vertical-align: -2px; /* Compensate for padding */
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled.ProseMirror-icon {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-icon svg {
|
||||
fill: currentColor;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.ProseMirror-icon span {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
border-top: 1px solid black;
|
||||
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes ProseMirror-cursor-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Add space around the hr to make clicking it easier */
|
||||
|
||||
.ProseMirror-example-setup-style hr {
|
||||
padding: 2px 10px;
|
||||
border: none;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style hr:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: silver;
|
||||
line-height: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror ul, .ProseMirror ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.ProseMirror blockquote {
|
||||
padding-left: 1em;
|
||||
border-left: 3px solid #eee;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style img {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt {
|
||||
background: white;
|
||||
padding: 1em;
|
||||
border: 1px solid silver;
|
||||
position: fixed;
|
||||
border-radius: 0.25em;
|
||||
z-index: 11;
|
||||
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.ProseMirror-prompt h5 {
|
||||
margin: 0 0 0.75em;
|
||||
font-family: @sansFont;
|
||||
font-size: 100%;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"],
|
||||
.ProseMirror-prompt textarea {
|
||||
background: #eee;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"] {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
color: #666;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close:after {
|
||||
content: "✕";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ProseMirror-invalid {
|
||||
background: #ffc;
|
||||
border: 1px solid #cc7;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-buttons {
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#editor, .editor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: black;
|
||||
background-clip: padding-box;
|
||||
padding: 5px 0;
|
||||
margin: 4em auto 23px auto;
|
||||
}
|
||||
|
||||
.ProseMirror p:first-child,
|
||||
.ProseMirror h1:first-child,
|
||||
.ProseMirror h2:first-child,
|
||||
.ProseMirror h3:first-child,
|
||||
.ProseMirror h4:first-child,
|
||||
.ProseMirror h5:first-child,
|
||||
.ProseMirror h6:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 123px;
|
||||
border: 1px solid silver;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
padding: 3px 10px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar-wrapper {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar-wrapper, #markdown textarea {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.editorreadmore {
|
||||
color: @textLinkColor;
|
||||
text-decoration: underline;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 50em) {
|
||||
#editor {
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 60em) {
|
||||
#editor {
|
||||
margin-left: 15%;
|
||||
margin-right: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 70em) {
|
||||
#editor {
|
||||
margin-left: 20%;
|
||||
margin-right: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 85em) {
|
||||
#editor {
|
||||
margin-left: 25%;
|
||||
margin-right: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 105em) {
|
||||
#editor {
|
||||
margin-left: 30%;
|
||||
margin-right: 30%;
|
||||
}
|
||||
}
|
4
less/prose.less
Normal file
4
less/prose.less
Normal file
|
@ -0,0 +1,4 @@
|
|||
@import "prose-editor";
|
||||
@import "pad-theme";
|
||||
@import "resources";
|
||||
@import "lib/elements";
|
13
less/resources.less
Normal file
13
less/resources.less
Normal file
|
@ -0,0 +1,13 @@
|
|||
@primary: rgb(114, 120, 191);
|
||||
@secondary: rgb(114, 191, 133);
|
||||
@subheaders: #444;
|
||||
@headerTextColor: black;
|
||||
@sansFont: 'Open Sans', 'Segoe UI', Tahoma, Arial, sans-serif;
|
||||
@serifFont: Lora, 'Palatino Linotype', 'Book Antiqua', 'New York', 'DejaVu serif', serif;
|
||||
@monoFont: Hack, consolas, Menlo-Regular, Menlo, Monaco, 'ubuntu mono', monospace, monospace;
|
||||
@dangerCol: #e21d27;
|
||||
@errUrgentCol: #ecc63c;
|
||||
@proSelectedCol: #71D571;
|
||||
@textLinkColor: rgb(0, 0, 238);
|
||||
|
||||
@accent: #767676;
|
|
@ -78,3 +78,10 @@ func (db *datastore) engine() string {
|
|||
}
|
||||
return " ENGINE = InnoDB"
|
||||
}
|
||||
|
||||
func (db *datastore) after(colName string) string {
|
||||
if db.driverName == driverSQLite {
|
||||
return ""
|
||||
}
|
||||
return " AFTER " + colName
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ var migrations = []Migration{
|
|||
New("support oauth attach", oauthAttach), // V6 -> V7
|
||||
New("support oauth via invite", oauthInvites), // V7 -> V8 (v0.12.0)
|
||||
New("optimize drafts retrieval", optimizeDrafts), // V8 -> V9
|
||||
New("support post signatures", supportPostSignatures), // V9 -> V10
|
||||
}
|
||||
|
||||
// CurrentVer returns the current migration version the application is on
|
||||
|
|
33
migrations/v10.go
Normal file
33
migrations/v10.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
* WriteFreely is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, included
|
||||
* in the LICENSE file in this source code package.
|
||||
*/
|
||||
|
||||
package migrations
|
||||
|
||||
func supportPostSignatures(db *datastore) error {
|
||||
t, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.Exec(`ALTER TABLE collections ADD COLUMN post_signature ` + db.typeText() + db.collateMultiByte() + ` NULL` + db.after("script"))
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
err = t.Commit()
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -14,7 +14,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
)
|
||||
|
||||
func oauth(db *datastore) error {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -14,7 +14,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
)
|
||||
|
||||
func oauthSlack(db *datastore) error {
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
/*
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
* WriteFreely is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, included
|
||||
* in the LICENSE file in this source code package.
|
||||
*/
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
)
|
||||
|
||||
func oauthAttach(db *datastore) error {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -14,7 +14,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
)
|
||||
|
||||
func oauthInvites(db *datastore) error {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019, 2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,8 +12,8 @@ package writefreely
|
|||
|
||||
import (
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/go-nodeinfo"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -45,7 +45,7 @@ func nodeInfoConfig(db *datastore, cfg *config.Config) *nodeinfo.Config {
|
|||
Private: cfg.App.Private,
|
||||
Software: nodeinfo.SoftwareMeta{
|
||||
HomePage: softwareURL,
|
||||
GitHub: "https://github.com/writeas/writefreely",
|
||||
GitHub: "https://github.com/writefreely/writefreely",
|
||||
Follow: "https://writing.exchange/@write_as",
|
||||
},
|
||||
MaxBlogs: cfg.App.MaxBlogs,
|
||||
|
|
95
oauth.go
95
oauth.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -25,24 +25,32 @@ import (
|
|||
"github.com/gorilla/sessions"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
)
|
||||
|
||||
// OAuthButtons holds display information for different OAuth providers we support.
|
||||
type OAuthButtons struct {
|
||||
SlackEnabled bool
|
||||
WriteAsEnabled bool
|
||||
GitLabEnabled bool
|
||||
GitLabDisplayName string
|
||||
SlackEnabled bool
|
||||
WriteAsEnabled bool
|
||||
GitLabEnabled bool
|
||||
GitLabDisplayName string
|
||||
GiteaEnabled bool
|
||||
GiteaDisplayName string
|
||||
GenericEnabled bool
|
||||
GenericDisplayName string
|
||||
}
|
||||
|
||||
// NewOAuthButtons creates a new OAuthButtons struct based on our app configuration.
|
||||
func NewOAuthButtons(cfg *config.Config) *OAuthButtons {
|
||||
return &OAuthButtons{
|
||||
SlackEnabled: cfg.SlackOauth.ClientID != "",
|
||||
WriteAsEnabled: cfg.WriteAsOauth.ClientID != "",
|
||||
GitLabEnabled: cfg.GitlabOauth.ClientID != "",
|
||||
GitLabDisplayName: config.OrDefaultString(cfg.GitlabOauth.DisplayName, gitlabDisplayName),
|
||||
SlackEnabled: cfg.SlackOauth.ClientID != "",
|
||||
WriteAsEnabled: cfg.WriteAsOauth.ClientID != "",
|
||||
GitLabEnabled: cfg.GitlabOauth.ClientID != "",
|
||||
GitLabDisplayName: config.OrDefaultString(cfg.GitlabOauth.DisplayName, gitlabDisplayName),
|
||||
GiteaEnabled: cfg.GiteaOauth.ClientID != "",
|
||||
GiteaDisplayName: config.OrDefaultString(cfg.GiteaOauth.DisplayName, giteaDisplayName),
|
||||
GenericEnabled: cfg.GenericOauth.ClientID != "",
|
||||
GenericDisplayName: config.OrDefaultString(cfg.GenericOauth.DisplayName, genericOauthDisplayName),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,6 +243,65 @@ func configureGitlabOauth(parentHandler *Handler, r *mux.Router, app *App) {
|
|||
}
|
||||
}
|
||||
|
||||
func configureGenericOauth(parentHandler *Handler, r *mux.Router, app *App) {
|
||||
if app.Config().GenericOauth.ClientID != "" {
|
||||
callbackLocation := app.Config().App.Host + "/oauth/callback/generic"
|
||||
|
||||
var callbackProxy *callbackProxyClient = nil
|
||||
if app.Config().GenericOauth.CallbackProxy != "" {
|
||||
callbackProxy = &callbackProxyClient{
|
||||
server: app.Config().GenericOauth.CallbackProxyAPI,
|
||||
callbackLocation: app.Config().App.Host + "/oauth/callback/generic",
|
||||
httpClient: config.DefaultHTTPClient(),
|
||||
}
|
||||
callbackLocation = app.Config().GenericOauth.CallbackProxy
|
||||
}
|
||||
|
||||
oauthClient := genericOauthClient{
|
||||
ClientID: app.Config().GenericOauth.ClientID,
|
||||
ClientSecret: app.Config().GenericOauth.ClientSecret,
|
||||
ExchangeLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.TokenEndpoint,
|
||||
InspectLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.InspectEndpoint,
|
||||
AuthLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.AuthEndpoint,
|
||||
HttpClient: config.DefaultHTTPClient(),
|
||||
CallbackLocation: callbackLocation,
|
||||
Scope: config.OrDefaultString(app.Config().GenericOauth.Scope, "read_user"),
|
||||
MapUserID: config.OrDefaultString(app.Config().GenericOauth.MapUserID, "user_id"),
|
||||
MapUsername: config.OrDefaultString(app.Config().GenericOauth.MapUsername, "username"),
|
||||
MapDisplayName: config.OrDefaultString(app.Config().GenericOauth.MapDisplayName, "-"),
|
||||
MapEmail: config.OrDefaultString(app.Config().GenericOauth.MapEmail, "email"),
|
||||
}
|
||||
configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy)
|
||||
}
|
||||
}
|
||||
|
||||
func configureGiteaOauth(parentHandler *Handler, r *mux.Router, app *App) {
|
||||
if app.Config().GiteaOauth.ClientID != "" {
|
||||
callbackLocation := app.Config().App.Host + "/oauth/callback/gitea"
|
||||
|
||||
var callbackProxy *callbackProxyClient = nil
|
||||
if app.Config().GiteaOauth.CallbackProxy != "" {
|
||||
callbackProxy = &callbackProxyClient{
|
||||
server: app.Config().GiteaOauth.CallbackProxyAPI,
|
||||
callbackLocation: app.Config().App.Host + "/oauth/callback/gitea",
|
||||
httpClient: config.DefaultHTTPClient(),
|
||||
}
|
||||
callbackLocation = app.Config().GiteaOauth.CallbackProxy
|
||||
}
|
||||
|
||||
oauthClient := giteaOauthClient{
|
||||
ClientID: app.Config().GiteaOauth.ClientID,
|
||||
ClientSecret: app.Config().GiteaOauth.ClientSecret,
|
||||
ExchangeLocation: app.Config().GiteaOauth.Host + "/login/oauth/access_token",
|
||||
InspectLocation: app.Config().GiteaOauth.Host + "/api/v1/user",
|
||||
AuthLocation: app.Config().GiteaOauth.Host + "/login/oauth/authorize",
|
||||
HttpClient: config.DefaultHTTPClient(),
|
||||
CallbackLocation: callbackLocation,
|
||||
}
|
||||
configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy)
|
||||
}
|
||||
}
|
||||
|
||||
func configureOauthRoutes(parentHandler *Handler, r *mux.Router, app *App, oauthClient oauthClient, callbackProxy *callbackProxyClient) {
|
||||
handler := &oauthHandler{
|
||||
Config: app.Config(),
|
||||
|
@ -264,6 +331,12 @@ func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http
|
|||
tokenResponse, err := h.oauthClient.exchangeOauthCode(ctx, code)
|
||||
if err != nil {
|
||||
log.Error("Unable to exchangeOauthCode: %s", err)
|
||||
// TODO: show user friendly message if needed
|
||||
// TODO: show NO message for cases like user pressing "Cancel" on authorize step
|
||||
addSessionFlash(app, w, r, err.Error(), nil)
|
||||
if attachUserID > 0 {
|
||||
return impart.HTTPError{http.StatusFound, "/me/settings"}
|
||||
}
|
||||
return impart.HTTPError{http.StatusInternalServerError, err.Error()}
|
||||
}
|
||||
|
||||
|
@ -354,7 +427,7 @@ func (r *callbackProxyClient) register(ctx context.Context, state string) error
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", "writefreely")
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
|
|
126
oauth_generic.go
Normal file
126
oauth_generic.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type genericOauthClient struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
AuthLocation string
|
||||
ExchangeLocation string
|
||||
InspectLocation string
|
||||
CallbackLocation string
|
||||
Scope string
|
||||
MapUserID string
|
||||
MapUsername string
|
||||
MapDisplayName string
|
||||
MapEmail string
|
||||
HttpClient HttpClient
|
||||
}
|
||||
|
||||
var _ oauthClient = genericOauthClient{}
|
||||
|
||||
const (
|
||||
genericOauthDisplayName = "OAuth"
|
||||
)
|
||||
|
||||
func (c genericOauthClient) GetProvider() string {
|
||||
return "generic"
|
||||
}
|
||||
|
||||
func (c genericOauthClient) GetClientID() string {
|
||||
return c.ClientID
|
||||
}
|
||||
|
||||
func (c genericOauthClient) GetCallbackLocation() string {
|
||||
return c.CallbackLocation
|
||||
}
|
||||
|
||||
func (c genericOauthClient) buildLoginURL(state string) (string, error) {
|
||||
u, err := url.Parse(c.AuthLocation)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("client_id", c.ClientID)
|
||||
q.Set("redirect_uri", c.CallbackLocation)
|
||||
q.Set("response_type", "code")
|
||||
q.Set("state", state)
|
||||
q.Set("scope", c.Scope)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func (c genericOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) {
|
||||
form := url.Values{}
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("redirect_uri", c.CallbackLocation)
|
||||
form.Add("scope", c.Scope)
|
||||
form.Add("code", code)
|
||||
req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("unable to exchange code for access token")
|
||||
}
|
||||
|
||||
var tokenResponse TokenResponse
|
||||
if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tokenResponse.Error != "" {
|
||||
return nil, errors.New(tokenResponse.Error)
|
||||
}
|
||||
return &tokenResponse, nil
|
||||
}
|
||||
|
||||
func (c genericOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) {
|
||||
req, err := http.NewRequest("GET", c.InspectLocation, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("unable to inspect access token")
|
||||
}
|
||||
|
||||
// since we don't know what the JSON from the server will look like, we create a
|
||||
// generic interface and then map manually to values set in the config
|
||||
var genericInterface map[string]interface{}
|
||||
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &genericInterface); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// map each relevant field in inspectResponse to the mapped field from the config
|
||||
var inspectResponse InspectResponse
|
||||
inspectResponse.UserID, _ = genericInterface[c.MapUserID].(string)
|
||||
inspectResponse.Username, _ = genericInterface[c.MapUsername].(string)
|
||||
inspectResponse.DisplayName, _ = genericInterface[c.MapDisplayName].(string)
|
||||
inspectResponse.Email, _ = genericInterface[c.MapEmail].(string)
|
||||
|
||||
return &inspectResponse, nil
|
||||
}
|
114
oauth_gitea.go
Normal file
114
oauth_gitea.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type giteaOauthClient struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
AuthLocation string
|
||||
ExchangeLocation string
|
||||
InspectLocation string
|
||||
CallbackLocation string
|
||||
HttpClient HttpClient
|
||||
}
|
||||
|
||||
var _ oauthClient = giteaOauthClient{}
|
||||
|
||||
const (
|
||||
giteaDisplayName = "Gitea"
|
||||
)
|
||||
|
||||
func (c giteaOauthClient) GetProvider() string {
|
||||
return "gitea"
|
||||
}
|
||||
|
||||
func (c giteaOauthClient) GetClientID() string {
|
||||
return c.ClientID
|
||||
}
|
||||
|
||||
func (c giteaOauthClient) GetCallbackLocation() string {
|
||||
return c.CallbackLocation
|
||||
}
|
||||
|
||||
func (c giteaOauthClient) buildLoginURL(state string) (string, error) {
|
||||
u, err := url.Parse(c.AuthLocation)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("client_id", c.ClientID)
|
||||
q.Set("redirect_uri", c.CallbackLocation)
|
||||
q.Set("response_type", "code")
|
||||
q.Set("state", state)
|
||||
// q.Set("scope", "read_user")
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func (c giteaOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) {
|
||||
form := url.Values{}
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("redirect_uri", c.CallbackLocation)
|
||||
// form.Add("scope", "read_user")
|
||||
form.Add("code", code)
|
||||
req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("unable to exchange code for access token")
|
||||
}
|
||||
|
||||
var tokenResponse TokenResponse
|
||||
if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tokenResponse.Error != "" {
|
||||
return nil, errors.New(tokenResponse.Error)
|
||||
}
|
||||
return &tokenResponse, nil
|
||||
}
|
||||
|
||||
func (c giteaOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) {
|
||||
req, err := http.NewRequest("GET", c.InspectLocation, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("unable to inspect access token")
|
||||
}
|
||||
|
||||
var inspectResponse InspectResponse
|
||||
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inspectResponse.Error != "" {
|
||||
return nil, errors.New(inspectResponse.Error)
|
||||
}
|
||||
return &inspectResponse, nil
|
||||
}
|
|
@ -63,7 +63,7 @@ func (c gitlabOauthClient) exchangeOauthCode(ctx context.Context, code string) (
|
|||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", "writefreely")
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
||||
|
@ -92,7 +92,7 @@ func (c gitlabOauthClient) inspectOauthAccessToken(ctx context.Context, accessTo
|
|||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", "writefreely")
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -17,7 +17,7 @@ import (
|
|||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
|
|
@ -111,7 +111,7 @@ func (c slackOauthClient) exchangeOauthCode(ctx context.Context, code string) (*
|
|||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", "writefreely")
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
||||
|
@ -140,7 +140,7 @@ func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessTok
|
|||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", "writefreely")
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
/*
|
||||
* Copyright © 2019-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
* WriteFreely is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, included
|
||||
* in the LICENSE file in this source code package.
|
||||
*/
|
||||
|
||||
package writefreely
|
||||
|
||||
import (
|
||||
|
@ -7,7 +17,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/nerds/store"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
@ -244,7 +254,7 @@ func TestViewOauthCallback(t *testing.T) {
|
|||
req, err := http.NewRequest("GET", "/oauth/callback", nil)
|
||||
assert.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
err = h.viewOauthCallback(nil, rr, req)
|
||||
err = h.viewOauthCallback(&App{cfg: app.Config(), sessionStore: app.SessionStore()}, rr, req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
|
||||
})
|
||||
|
|
|
@ -62,7 +62,7 @@ func (c writeAsOauthClient) exchangeOauthCode(ctx context.Context, code string)
|
|||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", "writefreely")
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
||||
|
@ -91,7 +91,7 @@ func (c writeAsOauthClient) inspectOauthAccessToken(ctx context.Context, accessT
|
|||
return nil, err
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
req.Header.Set("User-Agent", "writefreely")
|
||||
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
|
|
4
pad.go
4
pad.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -17,7 +17,7 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019, 2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,7 +12,7 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
4
pages.go
4
pages.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019, 2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,7 +12,7 @@ package writefreely
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{{define "content"}}
|
||||
<div class="content-container tight">
|
||||
<h1>Server error 😵</h1>
|
||||
<p>Please <a href="https://github.com/writeas/writefreely/issues/new">contact the human authors</a> of this software and remind them of their many shortcomings.</p>
|
||||
<p>Please <a href="https://github.com/writefreely/writefreely/issues/new">contact the human authors</a> of this software and remind them of their many shortcomings.</p>
|
||||
<p>Be gentle, though. They are fragile mortal beings.</p>
|
||||
<p style="margin-top:2em">Also, unlike the AI that will soon replace them, you will need to include an error log from the server in your report. (Utterly <em>primitive</em>, we know.)</p>
|
||||
<p>– {{.SiteName}} 🤖</p>
|
||||
|
|
|
@ -60,6 +60,9 @@ form dd {
|
|||
margin-top: 0;
|
||||
max-width: 8em;
|
||||
}
|
||||
.or {
|
||||
margin-bottom: 2.5em !important;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
|
@ -73,6 +76,8 @@ form dd {
|
|||
|
||||
<div{{if not .OpenRegistration}} style="padding: 2em 0;"{{end}}>
|
||||
{{ if .OpenRegistration }}
|
||||
{{template "oauth-buttons" .}}
|
||||
{{if not .DisablePasswordAuth}}
|
||||
{{if .Flashes}}<ul class="errors">
|
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>{{end}}
|
||||
|
@ -101,6 +106,7 @@ form dd {
|
|||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
{{ else }}
|
||||
<p style="font-size: 1.3em; margin: 1rem 0;">Registration is currently closed.</p>
|
||||
<p>You can always sign up on <a href="https://writefreely.org/instances">another instance</a>.</p>
|
||||
|
|
|
@ -13,25 +13,9 @@ input{margin-bottom:0.5em;}
|
|||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>{{end}}
|
||||
|
||||
{{ if or .OauthSlack .OauthWriteAs .OauthGitlab }}
|
||||
<div class="row content-container signinbtns">
|
||||
{{ if .OauthSlack }}
|
||||
<a class="loginbtn" href="/oauth/slack"><img alt="Sign in with Slack" height="40" width="172" src="/img/sign_in_with_slack.png" srcset="/img/sign_in_with_slack.png 1x, /img/sign_in_with_slack@2x.png 2x" /></a>
|
||||
{{ end }}
|
||||
{{ if .OauthWriteAs }}
|
||||
<a class="btn cta loginbtn" id="writeas-login" href="/oauth/write.as">Sign in with <strong>Write.as</strong></a>
|
||||
{{ end }}
|
||||
{{ if .OauthGitlab }}
|
||||
<a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab">Sign in with <strong>{{.GitlabDisplayName}}</strong></a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="or">
|
||||
<p>or</p>
|
||||
<hr class="short" />
|
||||
</div>
|
||||
{{ end }}
|
||||
{{template "oauth-buttons" .}}
|
||||
|
||||
{{if not .DisablePasswordAuth}}
|
||||
<form action="/auth/login" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()">
|
||||
<input type="text" name="alias" placeholder="Username" value="{{.LoginUsername}}" {{if not .LoginUsername}}autofocus{{end}} /><br />
|
||||
<input type="password" name="pass" placeholder="Password" {{if .LoginUsername}}autofocus{{end}} /><br />
|
||||
|
@ -41,11 +25,12 @@ input{margin-bottom:0.5em;}
|
|||
|
||||
{{if and (not .SingleUser) .OpenRegistration}}<p style="text-align:center;font-size:0.9em;margin:3em auto;max-width:26em;">{{if .Message}}{{.Message}}{{else}}<em>No account yet?</em> <a href="{{.SignupPath}}">Sign up</a> to start a blog.{{end}}</p>{{end}}
|
||||
|
||||
<script type="text/javascript">
|
||||
function disableSubmit() {
|
||||
var $btn = document.getElementById("btn-login");
|
||||
$btn.value = "Logging in...";
|
||||
$btn.disabled = true;
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
function disableSubmit() {
|
||||
var $btn = document.getElementById("btn-login");
|
||||
$btn.value = "Logging in...";
|
||||
$btn.disabled = true;
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
@ -70,25 +70,9 @@ form dd {
|
|||
</ul>{{end}}
|
||||
|
||||
<div id="billing">
|
||||
{{ if or .OAuth.SlackEnabled .OAuth.WriteAsEnabled .OAuth.GitLabEnabled }}
|
||||
<div class="row content-container signinbtns">
|
||||
{{ if .OAuth.SlackEnabled }}
|
||||
<a class="loginbtn" href="/oauth/slack{{if .Invite}}?invite_code={{.Invite}}{{end}}"><img alt="Sign in with Slack" height="40" width="172" src="/img/sign_in_with_slack.png" srcset="/img/sign_in_with_slack.png 1x, /img/sign_in_with_slack@2x.png 2x" /></a>
|
||||
{{ end }}
|
||||
{{ if .OAuth.WriteAsEnabled }}
|
||||
<a class="btn cta loginbtn" id="writeas-login" href="/oauth/write.as{{if .Invite}}?invite_code={{.Invite}}{{end}}">Sign in with <strong>Write.as</strong></a>
|
||||
{{ end }}
|
||||
{{ if .OAuth.GitLabEnabled }}
|
||||
<a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab{{if .Invite}}?invite_code={{.Invite}}{{end}}">Sign in with <strong>{{.OAuth.GitLabDisplayName}}</strong></a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="or">
|
||||
<p>or</p>
|
||||
<hr class="short" />
|
||||
</div>
|
||||
{{ end }}
|
||||
{{template "oauth-buttons" .}}
|
||||
|
||||
{{if not .DisablePasswordAuth}}
|
||||
<form action="/auth/signup" method="POST" id="signup-form" onsubmit="return signup()">
|
||||
<input type="hidden" name="invite_code" value="{{.Invite}}" />
|
||||
<dl class="billing">
|
||||
|
@ -112,6 +96,7 @@ form dd {
|
|||
</dt>
|
||||
</dl>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -57,6 +57,11 @@ func PostLede(t string, includePunc bool) string {
|
|||
c := []rune(t)
|
||||
t = string(c[:punc+iAdj])
|
||||
}
|
||||
punc = stringmanip.IndexRune(t, '?')
|
||||
if punc > -1 {
|
||||
c := []rune(t)
|
||||
t = string(c[:punc+iAdj])
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -16,6 +16,7 @@ import (
|
|||
"html"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
@ -27,8 +28,8 @@ import (
|
|||
blackfriday "github.com/writeas/saturday"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/stringmanip"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/parse"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/parse"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -58,10 +59,48 @@ func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool) {
|
|||
p.Post.formatContent(cfg, &p.Collection.Collection, isOwner)
|
||||
}
|
||||
|
||||
func (p *Post) augmentContent(c *Collection) {
|
||||
if p.PinnedPosition.Valid {
|
||||
// Don't augment posts that are pinned
|
||||
return
|
||||
}
|
||||
if strings.Index(p.Content, "<!--nosig-->") > -1 {
|
||||
// Don't augment posts with the special "nosig" shortcode
|
||||
return
|
||||
}
|
||||
// Add post signatures
|
||||
if c.Signature != "" {
|
||||
p.Content += "\n\n" + c.Signature
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PublicPost) augmentContent() {
|
||||
p.Post.augmentContent(&p.Collection.Collection)
|
||||
}
|
||||
|
||||
func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string {
|
||||
return applyMarkdownSpecial(data, false, baseURL, cfg)
|
||||
}
|
||||
|
||||
func disableYoutubeAutoplay(outHTML string) string {
|
||||
for _, match := range youtubeReg.FindAllString(outHTML, -1) {
|
||||
u, err := url.Parse(match)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
u.RawQuery = html.UnescapeString(u.RawQuery)
|
||||
q := u.Query()
|
||||
// Set Youtube autoplay url parameter, if any, to 0
|
||||
if len(q["autoplay"]) == 1 {
|
||||
q.Set("autoplay", "0")
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
cleanURL := u.String()
|
||||
outHTML = strings.Replace(outHTML, match, cleanURL, 1)
|
||||
}
|
||||
return outHTML
|
||||
}
|
||||
|
||||
func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *config.Config) string {
|
||||
mdExtensions := 0 |
|
||||
blackfriday.EXTENSION_TABLES |
|
||||
|
@ -97,10 +136,7 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *c
|
|||
// Strip newlines on certain block elements that render with them
|
||||
outHTML = blockReg.ReplaceAllString(outHTML, "<$1>")
|
||||
outHTML = endBlockReg.ReplaceAllString(outHTML, "</$1></$2>")
|
||||
// Remove all query parameters on YouTube embed links
|
||||
// TODO: make this more specific. Taking the nuclear approach here to strip ?autoplay=1
|
||||
outHTML = youtubeReg.ReplaceAllString(outHTML, "$1")
|
||||
|
||||
outHTML = disableYoutubeAutoplay(outHTML)
|
||||
return outHTML
|
||||
}
|
||||
|
||||
|
@ -129,9 +165,7 @@ func applyBasicMarkdown(data []byte) string {
|
|||
func postTitle(content, friendlyId string) string {
|
||||
const maxTitleLen = 80
|
||||
|
||||
// Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
|
||||
// entities added in by sanitizing the content.
|
||||
content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content))
|
||||
content = stripHTMLWithoutEscaping(content)
|
||||
|
||||
content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace)
|
||||
eol := strings.IndexRune(content, '\n')
|
||||
|
@ -149,9 +183,7 @@ func postTitle(content, friendlyId string) string {
|
|||
func friendlyPostTitle(content, friendlyId string) string {
|
||||
const maxTitleLen = 80
|
||||
|
||||
// Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
|
||||
// entities added in by sanitizing the content.
|
||||
content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content))
|
||||
content = stripHTMLWithoutEscaping(content)
|
||||
|
||||
content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace)
|
||||
eol := strings.IndexRune(content, '\n')
|
||||
|
@ -168,6 +200,12 @@ func friendlyPostTitle(content, friendlyId string) string {
|
|||
return title
|
||||
}
|
||||
|
||||
// Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
|
||||
// entities added in by sanitizing the content.
|
||||
func stripHTMLWithoutEscaping(content string) string {
|
||||
return html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content))
|
||||
}
|
||||
|
||||
func getSanitizationPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
policy.AllowAttrs("src", "style").OnElements("iframe", "video", "audio")
|
||||
|
|
34
posts.go
34
posts.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -36,8 +36,8 @@ import (
|
|||
"github.com/writeas/web-core/i18n"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/tags"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writeas/writefreely/parse"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
"github.com/writefreely/writefreely/parse"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -211,8 +211,7 @@ func (p Post) Summary() string {
|
|||
if p.Content == "" {
|
||||
return ""
|
||||
}
|
||||
// Strip out HTML
|
||||
p.Content = bluemonday.StrictPolicy().Sanitize(p.Content)
|
||||
p.Content = stripHTMLWithoutEscaping(p.Content)
|
||||
// and Markdown
|
||||
p.Content = stripmd.Strip(p.Content)
|
||||
|
||||
|
@ -1132,7 +1131,12 @@ func (p *PublicPost) CanonicalURL(hostName string) string {
|
|||
|
||||
func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
|
||||
cfg := app.cfg
|
||||
o := activitystreams.NewArticleObject()
|
||||
var o *activitystreams.Object
|
||||
if cfg.App.NotesOnly || strings.Index(p.Content, "\n\n") == -1 {
|
||||
o = activitystreams.NewNoteObject()
|
||||
} else {
|
||||
o = activitystreams.NewArticleObject()
|
||||
}
|
||||
o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID
|
||||
o.Published = p.Created
|
||||
o.URL = p.CanonicalURL(cfg.App.Host)
|
||||
|
@ -1141,6 +1145,7 @@ func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
|
|||
p.Collection.FederatedAccount() + "/followers",
|
||||
}
|
||||
o.Name = p.DisplayTitle()
|
||||
p.augmentContent()
|
||||
if p.HTMLContent == template.HTML("") {
|
||||
p.formatContent(cfg, false)
|
||||
}
|
||||
|
@ -1171,6 +1176,11 @@ func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
|
|||
})
|
||||
}
|
||||
}
|
||||
if len(p.Images) > 0 {
|
||||
for _, i := range p.Images {
|
||||
o.Attachment = append(o.Attachment, activitystreams.NewImageAttachment(i))
|
||||
}
|
||||
}
|
||||
// Find mentioned users
|
||||
mentionedUsers := make(map[string]string)
|
||||
|
||||
|
@ -1420,18 +1430,24 @@ Are you sure it was ever here?`,
|
|||
return err
|
||||
}
|
||||
}
|
||||
p.IsOwner = owner != nil && p.OwnerID.Valid && owner.ID == p.OwnerID.Int64
|
||||
|
||||
// Check if the authenticated user is the post owner
|
||||
p.IsOwner = u != nil && u.ID == p.OwnerID.Int64
|
||||
p.Collection = coll
|
||||
p.IsTopLevel = app.cfg.App.SingleUser
|
||||
|
||||
if !p.IsOwner && silenced {
|
||||
// Only allow a post owner or admin to view a post for silenced collections
|
||||
if silenced && !p.IsOwner && (u == nil || !u.IsAdmin()) {
|
||||
return ErrPostNotFound
|
||||
}
|
||||
|
||||
// Check if post has been unpublished
|
||||
if p.Content == "" && p.Title.String == "" {
|
||||
return impart.HTTPError{http.StatusGone, "Post was unpublished."}
|
||||
}
|
||||
|
||||
p.augmentContent()
|
||||
|
||||
// Serve collection post
|
||||
if isRaw {
|
||||
contentType := "text/plain"
|
||||
|
@ -1473,6 +1489,7 @@ Are you sure it was ever here?`,
|
|||
IsOwner bool
|
||||
IsPinned bool
|
||||
IsCustomDomain bool
|
||||
Monetization string
|
||||
PinnedPosts *[]PublicPost
|
||||
IsFound bool
|
||||
IsAdmin bool
|
||||
|
@ -1490,6 +1507,7 @@ Are you sure it was ever here?`,
|
|||
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)
|
||||
tp.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer")
|
||||
|
||||
if !postFound {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
|
45
posts_test.go
Normal file
45
posts_test.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
* WriteFreely is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, included
|
||||
* in the LICENSE file in this source code package.
|
||||
*/
|
||||
|
||||
package writefreely_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/guregu/null/zero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
func TestPostSummary(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
given writefreely.Post
|
||||
expected string
|
||||
}{
|
||||
"no special chars": {givenPost("Content."), "Content."},
|
||||
"HTML content": {givenPost("Content <p>with a</p> paragraph."), "Content with a paragraph."},
|
||||
"content with escaped char": {givenPost("Content's all OK."), "Content's all OK."},
|
||||
"multiline content": {givenPost(`Content
|
||||
in
|
||||
multiple
|
||||
lines.`), "Content in multiple lines."},
|
||||
}
|
||||
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
actual := test.given.Summary()
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func givenPost(content string) writefreely.Post {
|
||||
return writefreely.Post{Title: zero.StringFrom("Title"), Content: content}
|
||||
}
|
8
prose/.babelrc.js
Normal file
8
prose/.babelrc.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
"presets": [
|
||||
["@babel/env", {
|
||||
"modules": false
|
||||
}]
|
||||
],
|
||||
"plugins": ["@babel/plugin-syntax-dynamic-import"]
|
||||
}
|
4
prose/.prettierrc
Normal file
4
prose/.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
3
prose/Makefile
Normal file
3
prose/Makefile
Normal file
|
@ -0,0 +1,3 @@
|
|||
all :
|
||||
npm install
|
||||
npm run-script build
|
7
prose/README.md
Normal file
7
prose/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Building
|
||||
|
||||
* Run `npm install` to download dependencies.
|
||||
* Run `npm run-script build` to build a production script in `../static/js/` or run
|
||||
`npm run develop` to build and watch for changes. You can use `prose.html`
|
||||
to test your development changes.
|
||||
* Manually copy the file `prose.bundle.js` to `static/js/`. _To be automated_
|
57
prose/markdownParser.js
Normal file
57
prose/markdownParser.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { MarkdownParser } from "prosemirror-markdown";
|
||||
import markdownit from "markdown-it";
|
||||
|
||||
import { writeFreelySchema } from "./schema";
|
||||
|
||||
export const writeAsMarkdownParser = new MarkdownParser(
|
||||
writeFreelySchema,
|
||||
markdownit("commonmark", { html: true }),
|
||||
{
|
||||
// blockquote: { block: "blockquote" },
|
||||
paragraph: { block: "paragraph" },
|
||||
list_item: { block: "list_item" },
|
||||
bullet_list: { block: "bullet_list" },
|
||||
ordered_list: {
|
||||
block: "ordered_list",
|
||||
getAttrs: (tok) => ({ order: +tok.attrGet("start") || 1 }),
|
||||
},
|
||||
heading: {
|
||||
block: "heading",
|
||||
getAttrs: (tok) => ({ level: +tok.tag.slice(1) }),
|
||||
},
|
||||
code_block: { block: "code_block", noCloseToken: true },
|
||||
fence: {
|
||||
block: "code_block",
|
||||
getAttrs: (tok) => ({ params: tok.info || "" }),
|
||||
noCloseToken: true,
|
||||
},
|
||||
// hr: { node: "horizontal_rule" },
|
||||
image: {
|
||||
node: "image",
|
||||
getAttrs: (tok) => ({
|
||||
src: tok.attrGet("src"),
|
||||
title: tok.attrGet("title") || null,
|
||||
alt: tok.children?.[0].content || null,
|
||||
}),
|
||||
},
|
||||
hardbreak: { node: "hard_break" },
|
||||
|
||||
em: { mark: "em" },
|
||||
strong: { mark: "strong" },
|
||||
link: {
|
||||
mark: "link",
|
||||
getAttrs: (tok) => ({
|
||||
href: tok.attrGet("href"),
|
||||
title: tok.attrGet("title") || null,
|
||||
}),
|
||||
},
|
||||
code_inline: { mark: "code", noCloseToken: true },
|
||||
html_block: {
|
||||
node: "readmore",
|
||||
getAttrs(token) {
|
||||
// TODO: Give different attributes depending on the token content
|
||||
return {};
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
123
prose/markdownSerializer.js
Normal file
123
prose/markdownSerializer.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { MarkdownSerializer } from "prosemirror-markdown";
|
||||
|
||||
function backticksFor(node, side) {
|
||||
const ticks = /`+/g;
|
||||
let m;
|
||||
let len = 0;
|
||||
if (node.isText)
|
||||
while ((m = ticks.exec(node.text))) len = Math.max(len, m[0].length);
|
||||
let result = len > 0 && side > 0 ? " `" : "`";
|
||||
for (let i = 0; i < len; i++) result += "`";
|
||||
if (len > 0 && side < 0) result += " ";
|
||||
return result;
|
||||
}
|
||||
|
||||
function isPlainURL(link, parent, index, side) {
|
||||
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false;
|
||||
const content = parent.child(index + (side < 0 ? -1 : 0));
|
||||
if (
|
||||
!content.isText ||
|
||||
content.text != link.attrs.href ||
|
||||
content.marks[content.marks.length - 1] != link
|
||||
)
|
||||
return false;
|
||||
if (index == (side < 0 ? 1 : parent.childCount - 1)) return true;
|
||||
const next = parent.child(index + (side < 0 ? -2 : 1));
|
||||
return !link.isInSet(next.marks);
|
||||
}
|
||||
|
||||
export const writeAsMarkdownSerializer = new MarkdownSerializer(
|
||||
{
|
||||
readmore(state, node) {
|
||||
state.write("<!--more-->\n");
|
||||
state.closeBlock(node);
|
||||
},
|
||||
// blockquote(state, node) {
|
||||
// state.wrapBlock("> ", undefined, node, () => state.renderContent(node));
|
||||
// },
|
||||
code_block(state, node) {
|
||||
state.write(`\`\`\`${node.attrs.params || ""}\n`);
|
||||
state.text(node.textContent, false);
|
||||
state.ensureNewLine();
|
||||
state.write("```");
|
||||
state.closeBlock(node);
|
||||
},
|
||||
heading(state, node) {
|
||||
state.write(`${state.repeat("#", node.attrs.level)} `);
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
bullet_list(state, node) {
|
||||
state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `);
|
||||
},
|
||||
ordered_list(state, node) {
|
||||
const start = node.attrs.order || 1;
|
||||
const maxW = String(start + node.childCount - 1).length;
|
||||
const space = state.repeat(" ", maxW + 2);
|
||||
state.renderList(node, space, (i) => {
|
||||
const nStr = String(start + i);
|
||||
return `${state.repeat(" ", maxW - nStr.length) + nStr}. `;
|
||||
});
|
||||
},
|
||||
list_item(state, node) {
|
||||
state.renderContent(node);
|
||||
},
|
||||
paragraph(state, node) {
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
|
||||
image(state, node) {
|
||||
state.write(
|
||||
`![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${
|
||||
node.attrs.title ? ` ${state.quote(node.attrs.title)}` : ""
|
||||
})`
|
||||
);
|
||||
},
|
||||
hard_break(state, node, parent, index) {
|
||||
for (let i = index + 1; i < parent.childCount; i += 1)
|
||||
if (parent.child(i).type !== node.type) {
|
||||
state.write("\\\n");
|
||||
return;
|
||||
}
|
||||
},
|
||||
text(state, node) {
|
||||
state.text(node.text || "");
|
||||
},
|
||||
},
|
||||
{
|
||||
em: {
|
||||
open: "*",
|
||||
close: "*",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
strong: {
|
||||
open: "**",
|
||||
close: "**",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
link: {
|
||||
open(_state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, 1) ? "<" : "[";
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, -1)
|
||||
? ">"
|
||||
: `](${state.esc(mark.attrs.href)}${
|
||||
mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ""
|
||||
})`;
|
||||
},
|
||||
},
|
||||
code: {
|
||||
open(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index), -1);
|
||||
},
|
||||
close(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index - 1), 1);
|
||||
},
|
||||
escape: false,
|
||||
},
|
||||
}
|
||||
);
|
32
prose/menu.js
Normal file
32
prose/menu.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { MenuItem } from "prosemirror-menu";
|
||||
import { buildMenuItems } from "prosemirror-example-setup";
|
||||
|
||||
import { writeFreelySchema } from "./schema";
|
||||
|
||||
function canInsert(state, nodeType, attrs) {
|
||||
let $from = state.selection.$from;
|
||||
for (let d = $from.depth; d >= 0; d--) {
|
||||
let index = $from.index(d);
|
||||
if ($from.node(d).canReplaceWith(index, index, nodeType, attrs))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ReadMoreItem = new MenuItem({
|
||||
label: "Read more",
|
||||
select: (state) => canInsert(state, writeFreelySchema.nodes.readmore),
|
||||
run(state, dispatch) {
|
||||
dispatch(
|
||||
state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create())
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const getMenu = () => {
|
||||
const menuContent = [
|
||||
...buildMenuItems(writeFreelySchema).fullMenu,
|
||||
[ReadMoreItem],
|
||||
];
|
||||
return menuContent;
|
||||
};
|
16278
prose/package-lock.json
generated
Normal file
16278
prose/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
prose/package.json
Normal file
32
prose/package.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "prose",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "prose.js",
|
||||
"dependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"markdown-it": "^12.0.4",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-keymap": "^1.1.4",
|
||||
"prosemirror-markdown": "github:VV-EE/prosemirror-markdown",
|
||||
"prosemirror-model": "^1.9.1",
|
||||
"prosemirror-state": "^1.3.2",
|
||||
"prosemirror-view": "^1.14.2",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.7",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"prettier": "^2.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"develop": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
14
prose/prose.html
Normal file
14
prose/prose.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<link rel="stylesheet" href="../static/css/prose.css" />
|
||||
<div id="editor" style="margin-bottom: 0"></div>
|
||||
<!-- <div style="text-align: center"> -->
|
||||
<!-- <label style="border-right: 1px solid silver"> -->
|
||||
<!-- Markdown <input type=radio name=inputformat value=markdown> </label> -->
|
||||
<!-- <label> <input type=radio name=inputformat value=prosemirror checked> WYSIWYM</label> -->
|
||||
<!-- </div> -->
|
||||
|
||||
<div style="display: none">
|
||||
<textarea id="content">
|
||||
This is a comment written in [Markdown](http://commonmark.org). *You* may know the syntax for inserting a link, but does your whole audience? So you can give people the **choice** to use a more familiar, discoverable interface.</textarea
|
||||
>
|
||||
</div>
|
||||
<script src="dist/prose.bundle.js"></script>
|
118
prose/prose.js
Normal file
118
prose/prose.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
// class MarkdownView {
|
||||
// constructor(target, content) {
|
||||
// this.textarea = target.appendChild(document.createElement("textarea"))
|
||||
// this.textarea.value = content
|
||||
// }
|
||||
|
||||
// get content() { return this.textarea.value }
|
||||
// focus() { this.textarea.focus() }
|
||||
// destroy() { this.textarea.remove() }
|
||||
// }
|
||||
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { EditorState, TextSelection } from "prosemirror-state";
|
||||
import { exampleSetup } from "prosemirror-example-setup";
|
||||
import { keymap } from "prosemirror-keymap";
|
||||
|
||||
import { writeAsMarkdownParser } from "./markdownParser";
|
||||
import { writeAsMarkdownSerializer } from "./markdownSerializer";
|
||||
import { writeFreelySchema } from "./schema";
|
||||
import { getMenu } from "./menu";
|
||||
|
||||
let $title = document.querySelector("#title");
|
||||
let $content = document.querySelector("#content");
|
||||
|
||||
// Bugs:
|
||||
// 1. When there's just an empty line and a hard break is inserted with shift-enter then two enters are inserted
|
||||
// which do not show up in the markdown ( maybe bc. they are training enters )
|
||||
|
||||
class ProseMirrorView {
|
||||
constructor(target, content) {
|
||||
let typingTimer;
|
||||
let localDraft = localStorage.getItem(window.draftKey);
|
||||
if (localDraft != null) {
|
||||
content = localDraft;
|
||||
}
|
||||
if (content.indexOf("# ") === 0) {
|
||||
let eol = content.indexOf("\n");
|
||||
let title = content.substring("# ".length, eol);
|
||||
content = content.substring(eol + "\n\n".length);
|
||||
$title.value = title;
|
||||
}
|
||||
|
||||
const doc = writeAsMarkdownParser.parse(
|
||||
// Replace all "solo" \n's with \\\n for correct markdown parsing
|
||||
// Can't use lookahead or lookbehind because it's not supported on Safari
|
||||
content.replace(/([^]{0,1})(\n)([^]{0,1})/g, (match, p1, p2, p3) => {
|
||||
return p1 !== "\n" && p3 !== "\n" ? p1 + "\\\n" + p3 : match;
|
||||
})
|
||||
);
|
||||
|
||||
this.view = new EditorView(target, {
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
plugins: [
|
||||
keymap({
|
||||
"Mod-Enter": () => {
|
||||
document.getElementById("publish").click();
|
||||
return true;
|
||||
},
|
||||
"Mod-k": () => {
|
||||
const linkButton = document.querySelector(
|
||||
".ProseMirror-icon[title='Add or remove link']"
|
||||
);
|
||||
linkButton.dispatchEvent(new Event("mousedown"));
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
...exampleSetup({
|
||||
schema: writeFreelySchema,
|
||||
menuContent: getMenu(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
dispatchTransaction(transaction) {
|
||||
let newState = this.state.apply(transaction);
|
||||
const newContent = writeAsMarkdownSerializer
|
||||
.serialize(newState.doc)
|
||||
// Replace all \\\ns ( not followed by a \n ) with \n
|
||||
.replace(/(\\\n)(\n{0,1})/g, (match, p1, p2) =>
|
||||
p2 !== "\n" ? "\n" + p2 : match
|
||||
);
|
||||
$content.value = newContent;
|
||||
let draft = "";
|
||||
if ($title.value != null && $title.value !== "") {
|
||||
draft = "# " + $title.value + "\n\n";
|
||||
}
|
||||
draft += newContent;
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
this.updateState(newState);
|
||||
},
|
||||
});
|
||||
// Editor is focused to the last position. This is a workaround for a bug:
|
||||
// 1. 1 type something in an existing entry
|
||||
// 2. reload - works fine, the draft is reloaded
|
||||
// 3. reload again - the draft is somehow removed from localStorage and the original content is loaded
|
||||
// When the editor is focused the content is re-saved to localStorage
|
||||
|
||||
// This is also useful for editing, so it's not a bad thing even
|
||||
const lastPosition = this.view.state.doc.content.size;
|
||||
const selection = TextSelection.create(this.view.state.doc, lastPosition);
|
||||
this.view.dispatch(this.view.state.tr.setSelection(selection));
|
||||
this.view.focus();
|
||||
}
|
||||
|
||||
get content() {
|
||||
return defaultMarkdownSerializer.serialize(this.view.state.doc);
|
||||
}
|
||||
focus() {
|
||||
this.view.focus();
|
||||
}
|
||||
destroy() {
|
||||
this.view.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
let place = document.querySelector("#editor");
|
||||
let view = new ProseMirrorView(place, $content.value);
|
21
prose/schema.js
Normal file
21
prose/schema.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { schema } from "prosemirror-markdown";
|
||||
import { Schema } from "prosemirror-model";
|
||||
|
||||
export const writeFreelySchema = new Schema({
|
||||
nodes: schema.spec.nodes
|
||||
.remove("blockquote")
|
||||
.remove("horizontal_rule")
|
||||
.addToEnd("readmore", {
|
||||
inline: false,
|
||||
content: "",
|
||||
group: "block",
|
||||
draggable: true,
|
||||
toDOM: (node) => [
|
||||
"div",
|
||||
{ class: "editorreadmore" },
|
||||
"Read more...",
|
||||
],
|
||||
parseDOM: [{ tag: "div.editorreadmore" }],
|
||||
}),
|
||||
marks: schema.spec.marks,
|
||||
});
|
25
prose/webpack.config.js
Normal file
25
prose/webpack.config.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
entry: __dirname + '/prose.js'
|
||||
},
|
||||
output: {
|
||||
filename: 'prose.bundle.js',
|
||||
path: path.resolve('..', 'static', 'js'),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /(nodue_modules|bower_components)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
29
read.go
29
read.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -25,7 +25,7 @@ import (
|
|||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/memo"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -128,7 +128,7 @@ func (app *App) FetchPublicPosts() (interface{}, error) {
|
|||
}
|
||||
|
||||
func viewLocalTimelineAPI(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
updateTimelineCache(app.timeline)
|
||||
updateTimelineCache(app.timeline, false)
|
||||
|
||||
skip, _ := strconv.Atoi(r.FormValue("skip"))
|
||||
|
||||
|
@ -156,13 +156,19 @@ func viewLocalTimeline(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
return showLocalTimeline(app, w, r, page, vars["author"], vars["tag"])
|
||||
}
|
||||
|
||||
func updateTimelineCache(tl *localTimeline) {
|
||||
// Fetch posts if enough time has passed since last cache
|
||||
if tl.posts == nil || tl.m.Invalidate() {
|
||||
// updateTimelineCache will reset and update the cache if it is stale or
|
||||
// the boolean passed in is true.
|
||||
func updateTimelineCache(tl *localTimeline, reset bool) {
|
||||
if reset {
|
||||
tl.m.Reset()
|
||||
}
|
||||
|
||||
// Fetch posts if the cache is empty, has been reset or enough time has
|
||||
// passed since last cache.
|
||||
if tl.posts == nil || reset || tl.m.Invalidate() {
|
||||
log.Info("[READ] Updating post cache")
|
||||
var err error
|
||||
var postsInterfaces interface{}
|
||||
postsInterfaces, err = tl.m.Get()
|
||||
|
||||
postsInterfaces, err := tl.m.Get()
|
||||
if err != nil {
|
||||
log.Error("[READ] Unable to cache posts: %v", err)
|
||||
} else {
|
||||
|
@ -170,10 +176,11 @@ func updateTimelineCache(tl *localTimeline) {
|
|||
tl.posts = &castPosts
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page int, author, tag string) error {
|
||||
updateTimelineCache(app.timeline)
|
||||
updateTimelineCache(app.timeline, false)
|
||||
|
||||
pl := len(*(app.timeline.posts))
|
||||
ttlPages := int(math.Ceil(float64(pl) / float64(app.timeline.postsPerPage)))
|
||||
|
@ -286,7 +293,7 @@ func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) e
|
|||
return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."}
|
||||
}
|
||||
|
||||
updateTimelineCache(app.timeline)
|
||||
updateTimelineCache(app.timeline, false)
|
||||
|
||||
feed := &Feed{
|
||||
Title: app.cfg.App.SiteName + " Reader",
|
||||
|
|
11
routes.go
11
routes.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,6 +12,7 @@ package writefreely
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
|
@ -26,6 +27,7 @@ import (
|
|||
func (app *App) InitStaticRoutes(r *mux.Router) {
|
||||
// Handle static files
|
||||
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir)))
|
||||
fs = cacheControl(fs)
|
||||
app.shttp = http.NewServeMux()
|
||||
app.shttp.Handle("/", fs)
|
||||
r.PathPrefix("/").Handler(fs)
|
||||
|
@ -76,6 +78,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
|||
configureSlackOauth(handler, write, apper.App())
|
||||
configureWriteAsOauth(handler, write, apper.App())
|
||||
configureGitlabOauth(handler, write, apper.App())
|
||||
configureGenericOauth(handler, write, apper.App())
|
||||
configureGiteaOauth(handler, write, apper.App())
|
||||
|
||||
// Set up dyamic page handlers
|
||||
// Handle auth
|
||||
|
@ -122,9 +126,13 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
|||
|
||||
write.HandleFunc("/api/markdown", handler.All(handleRenderMarkdown)).Methods("POST")
|
||||
|
||||
instanceURL, _ := url.Parse(apper.App().Config().App.Host)
|
||||
host := instanceURL.Host
|
||||
|
||||
// Handle collections
|
||||
write.HandleFunc("/api/collections", handler.All(newCollection)).Methods("POST")
|
||||
apiColls := write.PathPrefix("/api/collections/").Subrouter()
|
||||
apiColls.HandleFunc("/"+host, handler.AllReader(fetchCollection)).Methods("GET")
|
||||
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.AllReader(fetchCollection)).Methods("GET")
|
||||
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE")
|
||||
apiColls.HandleFunc("/{alias}/posts", handler.AllReader(fetchCollectionPosts)).Methods("GET")
|
||||
|
@ -204,7 +212,6 @@ func RouteCollections(handler *Handler, r *mux.Router) {
|
|||
r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader))
|
||||
r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
|
||||
r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader))
|
||||
r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
|
||||
r.HandleFunc("/sitemap.xml", handler.AllReader(handleViewSitemap))
|
||||
r.HandleFunc("/feed/", handler.AllReader(ViewFeed))
|
||||
r.HandleFunc("/{slug}", handler.CollectionPostOrStatic)
|
||||
|
|
38
routes_test.go
Normal file
38
routes_test.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func TestCacheControlForStaticFiles(t *testing.T) {
|
||||
app := NewApp("testdata/config.ini")
|
||||
if err := app.LoadConfig(); err != nil {
|
||||
t.Fatalf("Could not create an app; %v", err)
|
||||
}
|
||||
router := mux.NewRouter()
|
||||
app.InitStaticRoutes(router)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/style.css", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
if code := rec.Result().StatusCode; code != http.StatusOK {
|
||||
t.Fatalf("Could not get /style.css, got HTTP status %d", code)
|
||||
}
|
||||
actual := rec.Result().Header.Get("Cache-Control")
|
||||
|
||||
expectedDirectives := []string{
|
||||
"public",
|
||||
"max-age",
|
||||
"immutable",
|
||||
}
|
||||
for _, expected := range expectedDirectives {
|
||||
if !strings.Contains(actual, expected) {
|
||||
t.Errorf("Expected Cache-Control header to contain '%s', but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
BIN
static/img/mark/gitea.png
Normal file
BIN
static/img/mark/gitea.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
static/img/mark/writeas-white.png
Normal file
BIN
static/img/mark/writeas-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
|
@ -17,7 +17,7 @@ Then [download an archive](https://github.com/highlightjs/highlight.js/releases)
|
|||
|
||||
version=9.13.1
|
||||
|
||||
cd $GOPATH/src/github.com/writeas/writefreely/static/js/highlightjs
|
||||
cd $GOPATH/src/github.com/writefreely/writefreely/static/js/highlightjs
|
||||
for f in $(ls ~/Downloads/highlight.js/src/languages); do
|
||||
# Use minified versions
|
||||
f=$(echo $f | sed 's/\.js/.min.js/')
|
||||
|
|
|
@ -110,12 +110,24 @@ Element.prototype.show = function() {
|
|||
|
||||
|
||||
var H = {
|
||||
getQEl: function(elementQuery) {
|
||||
return new Element(document.querySelector(elementQuery));
|
||||
},
|
||||
getEl: function(elementId) {
|
||||
return new Element(document.getElementById(elementId));
|
||||
},
|
||||
save: function($el, key) {
|
||||
localStorage.setItem(key, $el.el.value);
|
||||
},
|
||||
saveClassic: function($titleEl, $el, key) {
|
||||
var out = "";
|
||||
var title = $titleEl.el.value;
|
||||
if (title !== "") {
|
||||
out = "# "+title+"\n\n";
|
||||
}
|
||||
out += $el.el.value;
|
||||
localStorage.setItem(key, out);
|
||||
},
|
||||
load: function($el, key, onlyLoadPopulated, postUpdated) {
|
||||
var val = localStorage.getItem(key);
|
||||
if (onlyLoadPopulated && val == null) {
|
||||
|
@ -138,6 +150,20 @@ var H = {
|
|||
}
|
||||
return true;
|
||||
},
|
||||
loadClassic: function($titleEl, $el, key, onlyLoadPopulated) {
|
||||
var val = localStorage.getItem(key);
|
||||
if (onlyLoadPopulated && val == null) {
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
if (val.indexOf("# ") === 0) {
|
||||
var eol = val.indexOf("\n");
|
||||
title = val.substring("# ".length, eol);
|
||||
val = val.substring(eol+"\n\n".length);
|
||||
$titleEl.el.value = title;
|
||||
}
|
||||
$el.el.value = val;
|
||||
},
|
||||
set: function(key, value) {
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 419b0a6eee7eefc0f85e47f7d4f8227ec28b8e57
|
1
static/js/mathjax/tex-svg-full.js
Normal file
1
static/js/mathjax/tex-svg-full.js
Normal file
File diff suppressed because one or more lines are too long
34
static/js/menu.js
Normal file
34
static/js/menu.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
var menuItems = document.querySelectorAll('li.has-submenu');
|
||||
var menuTimer;
|
||||
function closeMenu($menu) {
|
||||
$menu.querySelector('a').setAttribute('aria-expanded', "false");
|
||||
$menu.className = "has-submenu";
|
||||
}
|
||||
Array.prototype.forEach.call(menuItems, function(el, i){
|
||||
el.addEventListener("mouseover", function(event){
|
||||
let $menu = document.querySelectorAll(".has-submenu.open");
|
||||
if ($menu.length > 0) {
|
||||
closeMenu($menu[0]);
|
||||
}
|
||||
this.className = "has-submenu open";
|
||||
this.querySelector('a').setAttribute('aria-expanded', "true");
|
||||
clearTimeout(menuTimer);
|
||||
});
|
||||
el.addEventListener("mouseout", function(event){
|
||||
menuTimer = setTimeout(function(event){
|
||||
let $menu = document.querySelector(".has-submenu.open");
|
||||
closeMenu($menu);
|
||||
}, 500);
|
||||
});
|
||||
el.querySelector('a').addEventListener("click", function(event){
|
||||
if (this.parentNode.className == "has-submenu") {
|
||||
this.parentNode.className = "has-submenu open";
|
||||
this.setAttribute('aria-expanded', "true");
|
||||
} else {
|
||||
this.parentNode.className = "has-submenu";
|
||||
this.setAttribute('aria-expanded', "false");
|
||||
}
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
});
|
2
static/js/prose.bundle.js
Normal file
2
static/js/prose.bundle.js
Normal file
File diff suppressed because one or more lines are too long
35
templates.go
35
templates.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,6 +11,7 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -22,7 +23,7 @@ import (
|
|||
"github.com/dustin/go-humanize"
|
||||
"github.com/writeas/web-core/l10n"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -38,6 +39,9 @@ var (
|
|||
"localhtml": localHTML,
|
||||
"tolower": strings.ToLower,
|
||||
"title": strings.Title,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
"hasSuffix": strings.HasSuffix,
|
||||
"dict": dict,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -85,12 +89,18 @@ func initPage(parentDir, path, key string) {
|
|||
log.Info(" [%s] %s", key, path)
|
||||
}
|
||||
|
||||
pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles(
|
||||
files := []string{
|
||||
path,
|
||||
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
|
||||
filepath.Join(parentDir, templatesDir, "base.tmpl"),
|
||||
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
|
||||
))
|
||||
}
|
||||
|
||||
if key == "login.tmpl" || key == "landing.tmpl" || key == "signup.tmpl" {
|
||||
files = append(files, filepath.Join(parentDir, templatesDir, "include", "oauth.tmpl"))
|
||||
}
|
||||
|
||||
pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...))
|
||||
}
|
||||
|
||||
func initUserPage(parentDir, path, key string) {
|
||||
|
@ -103,6 +113,7 @@ func initUserPage(parentDir, path, key string) {
|
|||
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
|
||||
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
|
||||
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
|
||||
filepath.Join(parentDir, templatesDir, "user", "include", "nav.tmpl"),
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -200,3 +211,19 @@ func localHTML(term, lang string) template.HTML {
|
|||
s = strings.Replace(s, "write.as", "<a href=\"https://writefreely.org\">writefreely</a>", 1)
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
// from: https://stackoverflow.com/a/18276968/1549194
|
||||
func dict(values ...interface{}) (map[string]interface{}, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, errors.New("dict: invalid number of parameters")
|
||||
}
|
||||
dict := make(map[string]interface{}, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("dict: keys must be strings")
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict, nil
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<nav id="user-nav">
|
||||
{{if .Username}}
|
||||
<nav class="dropdown-nav">
|
||||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
||||
<ul><li class="has-submenu"><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
||||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
||||
<li><a href="/me/settings">Account settings</a></li>
|
||||
<li><a href="/me/export">Export</a></li>
|
||||
|
@ -67,6 +67,7 @@
|
|||
{{ template "footer" . }}
|
||||
|
||||
{{if not .JSDisabled}}
|
||||
<script type="text/javascript" src="/js/menu.js"></script>
|
||||
<script type="text/javascript">
|
||||
{{if .WebFonts}}
|
||||
try { // Google Fonts
|
||||
|
|
|
@ -29,6 +29,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}}">
|
||||
{{template "collection-meta" .}}
|
||||
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
|
||||
<style type="text/css">
|
||||
body footer {
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<meta property="og:url" content="{{.CanonicalURL}}" />
|
||||
<meta property="og:description" content="{{.Description}}" />
|
||||
<meta property="og:image" content="{{.AvatarURL}}">
|
||||
{{template "collection-meta" .}}
|
||||
{{if .StyleSheet}}<style type="text/css">{{.StyleSheetDisplay}}</style>{{end}}
|
||||
<style type="text/css">
|
||||
body#collection header {
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
|
||||
<meta property="article:published_time" content="{{.Created8601}}">
|
||||
{{ end }}
|
||||
{{template "collection-meta" .}}
|
||||
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
|
||||
|
||||
{{if .Collection.RenderMathJax}}
|
||||
|
@ -62,7 +63,7 @@
|
|||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
<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{{if $.Collection.Format.ShowDates}} dated{{end}}">{{.FormattedDisplayTitle}}</h2>{{end}}{{if and $.Collection.Format.ShowDates (not .IsPinned) .IsFound}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time>{{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{{if and $.Collection.Format.ShowDates (not .IsPinned)}} dated{{end}}">{{.FormattedDisplayTitle}}</h2>{{end}}{{if and $.Collection.Format.ShowDates (not .IsPinned) .IsFound}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time>{{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>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="{{.CanonicalURL}}tag:{{.Tag}}" />
|
||||
<meta property="og:image" content="{{.Collection.AvatarURL}}">
|
||||
{{template "collection-meta" .}}
|
||||
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
|
||||
|
||||
{{if .Collection.RenderMathJax}}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<meta property="og:url" content="{{.CanonicalURL}}" />
|
||||
<meta property="og:description" content="{{.Description}}" />
|
||||
<meta property="og:image" content="{{.AvatarURL}}">
|
||||
{{template "collection-meta" .}}
|
||||
{{if .StyleSheet}}<style type="text/css">{{.StyleSheetDisplay}}</style>{{end}}
|
||||
|
||||
{{if .RenderMathJax}}
|
||||
|
@ -40,7 +41,7 @@
|
|||
</head>
|
||||
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
||||
{{if or .IsOwner .SingleUser}}<nav id="manage"><ul>
|
||||
<li><a onclick="void(0)">☰ Menu</a>
|
||||
<li class="has-submenu"><a onclick="void(0)">☰ Menu</a>
|
||||
<ul>
|
||||
{{ if .IsOwner }}
|
||||
{{if .SingleUser}}
|
||||
|
@ -117,6 +118,7 @@
|
|||
<script src="/js/h.js"></script>
|
||||
<script src="/js/postactions.js"></script>
|
||||
<script src="/js/localdate.js"></script>
|
||||
<script type="text/javascript" src="/js/menu.js"></script>
|
||||
<script type="text/javascript">
|
||||
var deleting = false;
|
||||
function delPost(e, id, owned) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue