mirror of https://github.com/writeas/writefreely
commit
815500ab78
@ -0,0 +1,10 @@ |
||||
root = true |
||||
|
||||
[*] |
||||
end_of_line = lf |
||||
insert_final_newline = true |
||||
trim_trailing_whitespace = true |
||||
charset = utf-8 |
||||
|
||||
[*.go] |
||||
indent_style = tab |
@ -0,0 +1,61 @@ |
||||
name: Build container image, publish as GitHub-package |
||||
|
||||
# This workflow uses actions that are not certified by GitHub. |
||||
# They are provided by a third-party and are governed by |
||||
# separate terms of service, privacy policy, and support |
||||
# documentation. |
||||
|
||||
on: |
||||
push: |
||||
branches: [ main, develop ] |
||||
# Publish semver tags as releases. |
||||
tags: |
||||
- 'v*.*.*' |
||||
|
||||
env: |
||||
# Use docker.io for Docker Hub if empty |
||||
REGISTRY: ghcr.io |
||||
# github.repository as <account>/<repo> |
||||
IMAGE_NAME: ${{ github.repository }} |
||||
|
||||
jobs: |
||||
build: |
||||
|
||||
runs-on: ubuntu-latest |
||||
permissions: |
||||
contents: read |
||||
packages: write |
||||
|
||||
steps: |
||||
- name: Checkout repository |
||||
uses: actions/checkout@v4 |
||||
|
||||
# Login against a Docker registry except on PR |
||||
# https://github.com/docker/login-action |
||||
- name: Log into registry ${{ env.REGISTRY }} |
||||
if: github.event_name != 'pull_request' |
||||
uses: docker/login-action@v3.0.0 |
||||
with: |
||||
registry: ${{ env.REGISTRY }} |
||||
username: ${{ github.actor }} |
||||
password: ${{ secrets.GITHUB_TOKEN }} |
||||
|
||||
# Extract metadata (tags, labels) for Docker |
||||
# https://github.com/docker/metadata-action |
||||
- name: Extract Docker metadata |
||||
id: meta |
||||
uses: docker/metadata-action@v4.6.0 |
||||
with: |
||||
images: | |
||||
ghcr.io/${{ github.repository }} |
||||
flavor: latest=true |
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR) |
||||
# https://github.com/docker/build-push-action |
||||
- name: Build and push Docker images |
||||
uses: docker/build-push-action@v5.0.0 |
||||
with: |
||||
context: . |
||||
push: ${{ github.event_name != 'pull_request' }} |
||||
tags: ${{ steps.meta.outputs.tags }} |
||||
labels: ${{ steps.meta.outputs.labels }} |
@ -0,0 +1,5 @@ |
||||
# Security Policy |
||||
|
||||
## Reporting a Vulnerability |
||||
|
||||
To report a vulnerability, send an email to security@writefreely.org. |
@ -1,106 +0,0 @@ |
||||
// +build wflib
|
||||
|
||||
package writefreely |
||||
|
||||
import ( |
||||
"bytes" |
||||
"compress/gzip" |
||||
"fmt" |
||||
"io" |
||||
"strings" |
||||
) |
||||
|
||||
func bindata_read(data []byte, name string) ([]byte, error) { |
||||
gz, err := gzip.NewReader(bytes.NewBuffer(data)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Read %q: %v", name, err) |
||||
} |
||||
|
||||
var buf bytes.Buffer |
||||
_, err = io.Copy(&buf, gz) |
||||
gz.Close() |
||||
|
||||
if err != nil { |
||||
return nil, fmt.Errorf("Read %q: %v", name, err) |
||||
} |
||||
|
||||
return buf.Bytes(), nil |
||||
} |
||||
|
||||
var _schema_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x59\x5f\x6f\xa3\x38\x10\x7f\xef\xa7\xf0\xdb\xa6\x52\x23\x6d\x7a\xdd\xaa\xba\xd3\x3e\x64\x53\x76\x2f\xba\x94\xee\x25\x44\xba\x7d\x02\x03\x93\xd4\xaa\xb1\x91\x6d\x92\xe6\xdb\x9f\x8c\x49\x08\x86\x24\xd0\xdb\x3b\x71\x7d\x2a\xcc\x6f\x8c\xfd\x9b\x3f\x9e\x99\x0c\x87\x57\xc3\x21\x7a\xc4\x0a\x87\x58\xc2\xaf\x28\xd8\x0a\xa2\x60\x25\x00\xe8\x2e\xb8\x1a\x0e\xaf\xb4\x78\xf8\xce\x3f\xad\xac\xf5\x3d\x1c\x52\x40\x52\x89\x2c\x52\x99\x00\xb4\xe2\x02\xa9\xfc\x5d\x80\xa3\x08\xa4\x54\xfc\x15\x98\x34\xdf\x9b\xcc\x9d\xb1\xe7\x20\x6f\xfc\x65\xe6\xa0\xe9\x57\xe4\x3e\x7b\xc8\xf9\x6b\xba\xf0\x16\x16\x1a\x0d\xae\x10\x0a\xf2\x87\x00\x85\x84\x61\xb1\x1b\x8c\xee\xaf\x73\x05\x77\x39\x9b\xdd\x68\x71\x26\x41\xf8\x24\x0e\x10\x61\x6a\x60\x0b\x65\x16\xf3\x00\x29\xc2\x76\x5a\x3a\x2a\xa5\xe8\xd1\xf9\x3a\x5e\xce\x3c\xf4\xe1\xe3\x87\x1c\xc9\x19\xf8\x8a\x24\xd0\x0e\x1d\x09\xc0\x0a\xe2\x00\xc5\x58\x81\x56\xab\x43\x27\xcb\xf9\xdc\x71\x3d\xdf\x9b\x3e\x39\x0b\x6f\xfc\xf4\x3d\x57\x84\xb7\x94\x08\x90\x47\x8a\x7b\x7c\xf5\x40\x78\x0d\x4c\x05\x68\x83\x45\xf4\x82\xc5\xe0\xf6\xd3\xa7\xeb\x1a\xf2\xfb\x7c\xfa\x34\x9e\xff\x40\x7f\x38\x3f\xd0\xa0\xa0\xe9\xfa\xea\x1a\x39\xee\xb7\xa9\xeb\x7c\x9e\x32\xc6\x1f\xbf\x94\xfb\xf9\x7d\x3c\x5f\x38\xde\x67\x8a\x15\x61\xa3\xdf\xfe\x75\xb3\xa7\x69\xc4\x99\xd2\xa7\xb8\x6c\xf4\x12\x6b\x4c\xae\xcd\xb9\x3f\xfa\x2f\xb6\x4d\x0f\xd0\x04\x62\x92\x25\x0a\xde\x54\x7e\xb8\xf1\xc4\x73\xe6\x68\xe1\x78\x28\x53\xab\x07\x34\x79\x9e\xcd\xf4\x17\xf5\x83\x1f\x12\x66\x79\x4d\x1a\xbf\xcb\x80\x55\xce\x49\xdc\x2b\xc2\x13\xb2\x16\x58\x11\xde\x18\x68\x16\xc0\x10\xbd\x01\x21\x09\x67\x26\x78\x46\x23\x8b\x69\x03\x6f\x64\x29\x97\x0b\x90\x19\x55\x01\xca\x4d\xb0\x97\xf4\x85\x8f\x88\x53\x0a\x91\x3e\x2c\x56\x4a\x90\x30\x53\xd0\x22\xff\x34\x6a\x19\xae\x4a\xd1\xc9\x74\x73\xd0\x29\xdd\x77\x74\xfb\x60\x81\x36\x98\x66\x60\x85\x76\xdd\x7f\x93\xf0\xae\xe2\xc2\x49\x78\x57\xf3\xe2\xaa\x33\x56\xf7\x77\x73\xb4\x99\xde\xf8\x68\xb9\xc5\x57\xd8\x75\xb2\x46\x8e\x6f\x6d\x87\x34\x0b\x29\x89\xfc\x57\xd8\x05\x28\xa4\x3c\xb4\xa4\x82\x6c\xb0\x82\x13\xe2\x73\xa4\xf6\x90\xc8\x14\x4b\xb9\xe5\x22\xee\xc4\x66\xa9\xd4\x9e\xd2\x42\x25\x40\xb9\xd7\xde\x7f\xbc\xfe\x3f\xb3\x26\x20\x26\x02\x22\xd5\x89\xb5\x52\xc9\xb0\x96\x0a\xd8\xf8\x98\x12\x2c\x8f\xc2\xfd\xa3\x45\x4c\xc0\x60\x7b\x11\x54\x65\xef\x68\xdd\x1e\x52\xd7\x89\x32\x79\x74\xa1\x5b\x5e\x85\xc6\x4b\xef\xd9\x9f\xba\x93\xb9\xf3\xe4\xb8\x9e\xc9\x9f\x0d\x3c\xb5\x4f\x8d\xb5\x4a\x4a\x11\x45\x7f\x4e\xa6\x0d\x62\x90\x91\x20\xa9\xca\x2f\xcb\xc3\xfe\xee\x3b\xed\xaf\x5a\x99\xaa\x1d\x05\x5f\xbe\x00\x14\x17\xa8\x79\x9b\x7f\xa4\xb8\x51\x5b\xaf\x9c\xab\xae\xb8\x48\xf0\x51\xc9\xf8\x50\x2f\x18\x4d\xe6\x8b\x76\x8d\x35\xae\xa9\x82\xb7\xec\x4c\x35\xbd\x21\xb0\xf5\x23\x9e\xe9\xe2\xab\x41\x5e\xaf\x8d\xf4\xdb\xa5\x3b\xfd\x73\xe9\xe4\x2f\xf7\xf6\x1d\x04\x3d\xf3\xee\x94\xcb\x36\xa9\xc0\xc0\x4a\x8f\x2e\x9c\xc0\xee\x39\x68\xb6\xb6\x7c\xb8\x66\x88\x84\xc7\x64\xb5\xf3\x8b\xd6\xc6\xd4\xb9\xb7\x0d\x38\xed\x07\x3e\x4e\x53\xc0\x02\xb3\x08\x0a\xe8\x5d\x53\x67\xc2\xb8\x48\x4c\x73\x42\x31\x5b\x67\x78\xbd\x47\x37\xad\x2b\x14\xad\x38\xc1\x4f\xf0\x94\xda\x12\xcd\x97\x4a\xfd\x4b\x84\x31\x88\xfd\x94\x4b\x62\xa2\xeb\xe8\x8b\x4b\x77\x31\xfd\xe6\x3a\x8f\x0d\x8b\xef\x1b\x30\x5d\x95\x4a\x85\x93\xb4\x6d\x07\x76\xa8\xfc\x3b\x6b\x5e\x70\x7f\x3b\xdd\xfc\x93\xec\x70\xe8\x71\xba\x25\x82\x8e\xe1\x48\x62\xdf\x38\x6b\xbd\x78\xcc\xdf\xd7\x14\x4a\xa3\x0f\xca\xff\x6f\x0e\x6b\xe7\x98\xc2\x73\x0a\xd4\xde\x8f\x6e\x7a\xd5\x2b\x09\x48\xb8\x82\x15\xa7\x94\x6f\x5b\xc4\x7d\x15\x7e\xb2\x64\xaa\xf5\x4f\x46\xcf\xaf\x4c\x28\x6a\xa0\xd3\xa3\x84\xcb\x25\xbe\xf5\x81\x9e\xf1\xab\xb7\xd5\xae\xce\xb7\xf0\xf5\x21\x40\x7e\x75\x77\xe7\xf6\x6c\x1f\x70\x39\x3e\x8c\xc5\x0f\x1e\xdf\x7f\xb6\x3b\x51\x6d\xd7\x66\xc7\xec\x35\x16\x67\x91\xe2\x86\x8a\xd3\x56\x21\x2c\xe4\x6f\xe7\x00\xf2\x05\x0b\x88\xfd\x4b\xb8\xcb\xb6\xb1\xe2\x6f\x50\x6e\xaf\x37\x76\xd1\x24\x77\x99\x3d\x58\x78\x63\x9d\xb3\xe3\xcd\x86\x79\xc3\xfd\xdd\x7f\x34\x6e\xd8\x6f\xac\x97\x83\x06\xbd\x39\xc2\x36\xa4\x99\xf7\x8a\xd8\x2a\xe7\x6c\x8a\xab\x75\x4e\x7d\x44\x86\xdf\x74\x42\x90\x01\x92\x09\xa6\xf4\x64\x2d\x74\x36\xc9\xb7\x99\x0a\x13\x86\x23\x45\x36\xcd\xf3\xe9\x3e\xd1\xde\xd2\xd1\x3b\x76\x86\x5a\x85\xe1\x04\xde\xdd\x1c\x5e\x1a\x66\x54\x57\x32\x7c\x1d\x16\x32\x8f\xf5\x75\x20\xc1\x84\xe6\x5b\x2a\x7e\x9d\x68\x9c\xd3\xbf\xfb\xd7\x82\xcb\x59\xb0\xa4\x65\x50\xfe\xdf\xab\x28\x94\x26\xce\xe2\x53\x61\x78\x90\x17\xee\x90\x3f\xf9\x27\xc3\xf1\xe4\x7d\xdf\xfa\xcc\x7f\x07\x00\x00\xff\xff\xbe\x79\x68\xa8\x10\x1b\x00\x00") |
||||
|
||||
func schema_sql() ([]byte, error) { |
||||
return bindata_read( |
||||
_schema_sql, |
||||
"schema.sql", |
||||
) |
||||
} |
||||
|
||||
// Asset loads and returns the asset for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func Asset(name string) ([]byte, error) { |
||||
cannonicalName := strings.Replace(name, "\\", "/", -1) |
||||
if f, ok := _bindata[cannonicalName]; ok { |
||||
return f() |
||||
} |
||||
return nil, fmt.Errorf("Asset %s not found", name) |
||||
} |
||||
|
||||
// AssetNames returns the names of the assets.
|
||||
func AssetNames() []string { |
||||
names := make([]string, 0, len(_bindata)) |
||||
for name := range _bindata { |
||||
names = append(names, name) |
||||
} |
||||
return names |
||||
} |
||||
|
||||
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||
var _bindata = map[string]func() ([]byte, error){ |
||||
"schema.sql": schema_sql, |
||||
} |
||||
|
||||
// AssetDir returns the file names below a certain
|
||||
// directory embedded in the file by go-bindata.
|
||||
// For example if you run go-bindata on data/... and data contains the
|
||||
// following hierarchy:
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
// then AssetDir("data") would return []string{"foo.txt", "img"}
|
||||
// AssetDir("data/img") would return []string{"a.png", "b.png"}
|
||||
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
|
||||
// AssetDir("") will return []string{"data"}.
|
||||
func AssetDir(name string) ([]string, error) { |
||||
node := _bintree |
||||
if len(name) != 0 { |
||||
cannonicalName := strings.Replace(name, "\\", "/", -1) |
||||
pathList := strings.Split(cannonicalName, "/") |
||||
for _, p := range pathList { |
||||
node = node.Children[p] |
||||
if node == nil { |
||||
return nil, fmt.Errorf("Asset %s not found", name) |
||||
} |
||||
} |
||||
} |
||||
if node.Func != nil { |
||||
return nil, fmt.Errorf("Asset %s not found", name) |
||||
} |
||||
rv := make([]string, 0, len(node.Children)) |
||||
for name := range node.Children { |
||||
rv = append(rv, name) |
||||
} |
||||
return rv, nil |
||||
} |
||||
|
||||
type _bintree_t struct { |
||||
Func func() ([]byte, error) |
||||
Children map[string]*_bintree_t |
||||
} |
||||
|
||||
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ |
||||
"schema.sql": &_bintree_t{schema_sql, map[string]*_bintree_t{}}, |
||||
}} |
@ -0,0 +1,462 @@ |
||||
/* |
||||
* Copyright © 2019-2021 Musing Studio 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 ( |
||||
"database/sql" |
||||
"encoding/json" |
||||
"fmt" |
||||
"html/template" |
||||
"net/http" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/aymerick/douceur/inliner" |
||||
"github.com/gorilla/mux" |
||||
"github.com/mailgun/mailgun-go" |
||||
stripmd "github.com/writeas/go-strip-markdown/v2" |
||||
"github.com/writeas/impart" |
||||
"github.com/writeas/web-core/data" |
||||
"github.com/writeas/web-core/log" |
||||
"github.com/writefreely/writefreely/key" |
||||
"github.com/writefreely/writefreely/spam" |
||||
) |
||||
|
||||
const ( |
||||
emailSendDelay = 15 |
||||
) |
||||
|
||||
type ( |
||||
SubmittedSubscription struct { |
||||
CollAlias string |
||||
UserID int64 |
||||
|
||||
Email string `schema:"email" json:"email"` |
||||
Web bool `schema:"web" json:"web"` |
||||
Slug string `schema:"slug" json:"slug"` |
||||
From string `schema:"from" json:"from"` |
||||
} |
||||
|
||||
EmailSubscriber struct { |
||||
ID string |
||||
CollID int64 |
||||
UserID sql.NullInt64 |
||||
Email sql.NullString |
||||
Subscribed time.Time |
||||
Token string |
||||
Confirmed bool |
||||
AllowExport bool |
||||
acctEmail sql.NullString |
||||
} |
||||
) |
||||
|
||||
func (es *EmailSubscriber) FinalEmail(keys *key.Keychain) string { |
||||
if !es.UserID.Valid || es.Email.Valid { |
||||
return es.Email.String |
||||
} |
||||
|
||||
decEmail, err := data.Decrypt(keys.EmailKey, []byte(es.acctEmail.String)) |
||||
if err != nil { |
||||
log.Error("Error decrypting user email: %v", err) |
||||
return "" |
||||
} |
||||
return string(decEmail) |
||||
} |
||||
|
||||
func (es *EmailSubscriber) SubscribedFriendly() string { |
||||
return es.Subscribed.Format("January 2, 2006") |
||||
} |
||||
|
||||
func handleCreateEmailSubscription(app *App, w http.ResponseWriter, r *http.Request) error { |
||||
reqJSON := IsJSON(r) |
||||
vars := mux.Vars(r) |
||||
var err error |
||||
|
||||
ss := SubmittedSubscription{ |
||||
CollAlias: vars["alias"], |
||||
} |
||||
u := getUserSession(app, r) |
||||
if u != nil { |
||||
ss.UserID = u.ID |
||||
} |
||||
if reqJSON { |
||||
// Decode JSON request
|
||||
decoder := json.NewDecoder(r.Body) |
||||
err = decoder.Decode(&ss) |
||||
if err != nil { |
||||
log.Error("Couldn't parse new subscription JSON request: %v\n", err) |
||||
return ErrBadJSON |
||||
} |
||||
} else { |
||||
err = r.ParseForm() |
||||
if err != nil { |
||||
log.Error("Couldn't parse new subscription form request: %v\n", err) |
||||
return ErrBadFormData |
||||
} |
||||
|
||||
err = app.formDecoder.Decode(&ss, r.PostForm) |
||||
if err != nil { |
||||
log.Error("Continuing, but error decoding new subscription form request: %v\n", err) |
||||
//return ErrBadFormData
|
||||
} |
||||
} |
||||
|
||||
c, err := app.db.GetCollection(ss.CollAlias) |
||||
if err != nil { |
||||
log.Error("getCollection: %s", err) |
||||
return err |
||||
} |
||||
c.hostName = app.cfg.App.Host |
||||
|
||||
from := c.CanonicalURL() |
||||
isAuthorBanned, err := app.db.IsUserSilenced(c.OwnerID) |
||||
if isAuthorBanned { |
||||
log.Info("Author is silenced, so subscription is blocked.") |
||||
return impart.HTTPError{http.StatusFound, from} |
||||
} |
||||
|
||||
if ss.Web { |
||||
if u != nil && u.ID == c.OwnerID { |
||||
from = "/" + c.Alias + "/" |
||||
} |
||||
from += ss.Slug |
||||
} |
||||
|
||||
if r.FormValue(spam.HoneypotFieldName()) != "" || r.FormValue("fake_password") != "" { |
||||
log.Info("Honeypot field was filled out! Not subscribing.") |
||||
return impart.HTTPError{http.StatusFound, from} |
||||
} |
||||
|
||||
if ss.Email == "" && ss.UserID < 1 { |
||||
log.Info("No subscriber data. Not subscribing.") |
||||
return impart.HTTPError{http.StatusFound, from} |
||||
} |
||||
|
||||
confirmed := app.db.IsSubscriberConfirmed(ss.Email) |
||||
es, err := app.db.AddEmailSubscription(c.ID, ss.UserID, ss.Email, confirmed) |
||||
if err != nil { |
||||
log.Error("addEmailSubscription: %s", err) |
||||
return err |
||||
} |
||||
|
||||
// Send confirmation email if needed
|
||||
if !confirmed { |
||||
err = sendSubConfirmEmail(app, c, ss.Email, es.ID, es.Token) |
||||
if err != nil { |
||||
log.Error("Failed to send subscription confirmation email: %s", err) |
||||
return err |
||||
} |
||||
} |
||||
|
||||
if ss.Web { |
||||
session, err := app.sessionStore.Get(r, userEmailCookieName) |
||||
if err != nil { |
||||
// The cookie should still save, even if there's an error.
|
||||
// Source: https://github.com/gorilla/sessions/issues/16#issuecomment-143642144
|
||||
log.Error("Getting user email cookie: %v; ignoring", err) |
||||
} |
||||
if confirmed { |
||||
addSessionFlash(app, w, r, "<strong>Subscribed</strong>. You'll now receive future blog posts via email.", nil) |
||||
} else { |
||||
addSessionFlash(app, w, r, "Please check your email and <strong>click the confirmation link</strong> to subscribe.", nil) |
||||
} |
||||
session.Values[userEmailCookieVal] = ss.Email |
||||
err = session.Save(r, w) |
||||
if err != nil { |
||||
log.Error("save email cookie: %s", err) |
||||
return err |
||||
} |
||||
|
||||
return impart.HTTPError{http.StatusFound, from} |
||||
} |
||||
return impart.WriteSuccess(w, "", http.StatusAccepted) |
||||
} |
||||
|
||||
func handleDeleteEmailSubscription(app *App, w http.ResponseWriter, r *http.Request) error { |
||||
alias := collectionAliasFromReq(r) |
||||
|
||||
vars := mux.Vars(r) |
||||
subID := vars["subscriber"] |
||||
email := r.FormValue("email") |
||||
token := r.FormValue("t") |
||||
slug := r.FormValue("slug") |
||||
isWeb := r.Method == "GET" |
||||
|
||||
// Display collection if this is a collection
|
||||
var c *Collection |
||||
var err error |
||||
if app.cfg.App.SingleUser { |
||||
c, err = app.db.GetCollectionByID(1) |
||||
} else { |
||||
c, err = app.db.GetCollection(alias) |
||||
} |
||||
if err != nil { |
||||
log.Error("Get collection: %s", err) |
||||
return err |
||||
} |
||||
|
||||
from := c.CanonicalURL() |
||||
|
||||
if subID != "" { |
||||
// User unsubscribing via email, so assume action is taken by either current
|
||||
// user or not current user, and only use the request's information to
|
||||
// satisfy this unsubscribe, i.e. subscriberID and token.
|
||||
err = app.db.DeleteEmailSubscriber(subID, token) |
||||
} else { |
||||
// User unsubscribing through the web app, so assume action is taken by
|
||||
// currently-auth'd user.
|
||||
var userID int64 |
||||
u := getUserSession(app, r) |
||||
if u != nil { |
||||
// User is logged in
|
||||
userID = u.ID |
||||
if userID == c.OwnerID { |
||||
from = "/" + c.Alias + "/" |
||||
} |
||||
} |
||||
if email == "" && userID <= 0 { |
||||
// Get email address from saved cookie
|
||||
session, err := app.sessionStore.Get(r, userEmailCookieName) |
||||
if err != nil { |
||||
log.Error("Unable to get email cookie: %s", err) |
||||
} else { |
||||
email = session.Values[userEmailCookieVal].(string) |
||||
} |
||||
} |
||||
|
||||
if email == "" && userID <= 0 { |
||||
err = fmt.Errorf("No subscriber given.") |
||||
log.Error("Not deleting subscription: %s", err) |
||||
return err |
||||
} |
||||
|
||||
err = app.db.DeleteEmailSubscriberByUser(email, userID, c.ID) |
||||
} |
||||
if err != nil { |
||||
log.Error("Unable to delete subscriber: %v", err) |
||||
return err |
||||
} |
||||
|
||||
if isWeb { |
||||
from += slug |
||||
addSessionFlash(app, w, r, "<strong>Unsubscribed</strong>. You will no longer receive these blog posts via email.", nil) |
||||
return impart.HTTPError{http.StatusFound, from} |
||||
} |
||||
return impart.WriteSuccess(w, "", http.StatusAccepted) |
||||
} |
||||
|
||||
func handleConfirmEmailSubscription(app *App, w http.ResponseWriter, r *http.Request) error { |
||||
alias := collectionAliasFromReq(r) |
||||
subID := mux.Vars(r)["subscriber"] |
||||
token := r.FormValue("t") |
||||
|
||||
var c *Collection |
||||
var err error |
||||
if app.cfg.App.SingleUser { |
||||
c, err = app.db.GetCollectionByID(1) |
||||
} else { |
||||
c, err = app.db.GetCollection(alias) |
||||
} |
||||
if err != nil { |
||||
log.Error("Get collection: %s", err) |
||||
return err |
||||
} |
||||
|
||||
from := c.CanonicalURL() |
||||
|
||||
err = app.db.UpdateSubscriberConfirmed(subID, token) |
||||
if err != nil { |
||||
addSessionFlash(app, w, r, err.Error(), nil) |
||||
return impart.HTTPError{http.StatusFound, from} |
||||
} |
||||
|
||||
addSessionFlash(app, w, r, "<strong>Confirmed</strong>! Thanks. Now you'll receive future blog posts via email.", nil) |
||||
return impart.HTTPError{http.StatusFound, from} |
||||
} |
||||
|
||||
func emailPost(app *App, p *PublicPost, collID int64) error { |
||||
p.augmentContent() |
||||
|
||||
// Do some shortcode replacement.
|
||||
// Since the user is receiving this email, we can assume they're subscribed via email.
|
||||
p.Content = strings.Replace(p.Content, "<!--emailsub-->", `<p id="emailsub">You're subscribed to email updates.</p>`, -1) |
||||
|
||||
if p.HTMLContent == template.HTML("") { |
||||
p.formatContent(app.cfg, false, false) |
||||
} |
||||
p.augmentReadingDestination() |
||||
|
||||
title := p.Title.String |
||||
if title != "" { |
||||
title = p.Title.String + "\n\n" |
||||
} |
||||
plainMsg := title + "A new post from " + p.CanonicalURL(app.cfg.App.Host) + "\n\n" + stripmd.Strip(p.Content) |
||||
plainMsg += ` |
||||
|
||||
--------------------------------------------------------------------------------- |
||||
|
||||
Originally published on ` + p.Collection.DisplayTitle() + ` (` + p.Collection.CanonicalURL() + `), a blog you subscribe to. |
||||
|
||||
Sent to %recipient.to%. Unsubscribe: ` + p.Collection.CanonicalURL() + `email/unsubscribe/%recipient.id%?t=%recipient.token%` |
||||
|
||||
gun := mailgun.NewMailgun(app.cfg.Email.Domain, app.cfg.Email.MailgunPrivate) |
||||
m := mailgun.NewMessage(p.Collection.DisplayTitle()+" <"+p.Collection.Alias+"@"+app.cfg.Email.Domain+">", stripmd.Strip(p.DisplayTitle()), plainMsg) |
||||
replyTo := app.db.GetCollectionAttribute(collID, collAttrLetterReplyTo) |
||||
if replyTo != "" { |
||||
m.SetReplyTo(replyTo) |
||||
} |
||||
|
||||
subs, err := app.db.GetEmailSubscribers(collID, true) |
||||
if err != nil { |
||||
log.Error("Unable to get email subscribers: %v", err) |
||||
return err |
||||
} |
||||
if len(subs) == 0 { |
||||
return nil |
||||
} |
||||
|
||||
if title != "" { |
||||
title = string(`<h2 id="title">` + p.FormattedDisplayTitle() + `</h2>`) |
||||
} |
||||
m.AddTag("New post") |
||||
|
||||
fontFam := "Lora, Palatino, Baskerville, serif" |
||||
if p.IsSans() { |
||||
fontFam = `"Open Sans", Tahoma, Arial, sans-serif` |
||||
} else if p.IsMonospace() { |
||||
fontFam = `Hack, consolas, Menlo-Regular, Menlo, Monaco, monospace, monospace` |
||||
} |
||||
|
||||
// TODO: move this to a templated file and LESS-generated stylesheet
|
||||
fullHTML := `<html> |
||||
<head> |
||||
<style> |
||||
body { |
||||
font-size: 120%; |
||||
font-family: ` + fontFam + `; |
||||
margin: 1em 2em; |
||||
} |
||||
#article { |
||||
line-height: 1.5; |
||||
margin: 1.5em 0; |
||||
white-space: pre-wrap; |
||||
word-wrap: break-word; |
||||
} |
||||
h1, h2, h3, h4, h5, h6, p, code { |
||||
display: inline |
||||
} |
||||
img, iframe, video { |
||||
max-width: 100% |
||||
} |
||||
#title { |
||||
margin-bottom: 1em; |
||||
display: block; |
||||
} |
||||
.intro { |
||||
font-style: italic; |
||||
font-size: 0.95em; |
||||
} |
||||
div#footer { |
||||
text-align: center; |
||||
max-width: 35em; |
||||
margin: 2em auto; |
||||
} |
||||
div#footer p { |
||||
display: block; |
||||
font-size: 0.86em; |
||||
color: #666; |
||||
} |
||||
hr { |
||||
border: 1px solid #ccc; |
||||
margin: 2em 1em; |
||||
} |
||||
p#emailsub { |
||||
text-align: center; |
||||
display: inline-block !important; |
||||
width: 100%; |
||||
font-style: italic; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
<div id="article">` + title + `<p class="intro">From <a href="` + p.CanonicalURL(app.cfg.App.Host) + `">` + p.DisplayCanonicalURL() + `</a></p> |
||||
|
||||
` + string(p.HTMLContent) + `</div> |
||||
<hr /> |
||||
<div id="footer"> |
||||
<p>Originally published on <a href="` + p.Collection.CanonicalURL() + `">` + p.Collection.DisplayTitle() + `</a>, a blog you subscribe to.</p> |
||||
<p>Sent to %recipient.to%. <a href="` + p.Collection.CanonicalURL() + `email/unsubscribe/%recipient.id%?t=%recipient.token%">Unsubscribe</a>.</p> |
||||
</div> |
||||
</body> |
||||
</html>` |
||||
|
||||
// inline CSS
|
||||
html, err := inliner.Inline(fullHTML) |
||||
if err != nil { |
||||
log.Error("Unable to inline email HTML: %v", err) |
||||
return err |
||||
} |
||||
|
||||
m.SetHtml(html) |
||||
|
||||
log.Info("[email] Adding %d recipient(s)", len(subs)) |
||||
for _, s := range subs { |
||||
e := s.FinalEmail(app.keys) |
||||
log.Info("[email] Adding %s", e) |
||||
err = m.AddRecipientAndVariables(e, map[string]interface{}{ |
||||
"id": s.ID, |
||||
"to": e, |
||||
"token": s.Token, |
||||
}) |
||||
if err != nil { |
||||
log.Error("Unable to add receipient %s: %s", e, err) |
||||
} |
||||
} |
||||
|
||||
res, _, err := gun.Send(m) |
||||
log.Info("[email] Send result: %s", res) |
||||
if err != nil { |
||||
log.Error("Unable to send post email: %v", err) |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func sendSubConfirmEmail(app *App, c *Collection, email, subID, token string) error { |
||||
if email == "" { |
||||
return fmt.Errorf("You must supply an email to verify.") |
||||
} |
||||
|
||||
// Send email
|
||||
gun := mailgun.NewMailgun(app.cfg.Email.Domain, app.cfg.Email.MailgunPrivate) |
||||
|
||||
plainMsg := "Confirm your subscription to " + c.DisplayTitle() + ` (` + c.CanonicalURL() + `) to start receiving future posts. Simply click the following link (or copy and paste it into your browser): |
||||
|
||||
` + c.CanonicalURL() + "email/confirm/" + subID + "?t=" + token + ` |
||||
|
||||
If you didn't subscribe to this site or you're not sure why you're getting this email, you can delete it. You won't be subscribed or receive any future emails.` |
||||
m := mailgun.NewMessage(c.DisplayTitle()+" <"+c.Alias+"@"+app.cfg.Email.Domain+">", "Confirm your subscription to "+c.DisplayTitle(), plainMsg, fmt.Sprintf("<%s>", email)) |
||||
m.AddTag("Email Verification") |
||||
|
||||
m.SetHtml(`<html> |
||||
<body style="font-family:Lora, 'Palatino Linotype', Palatino, Baskerville, 'Book Antiqua', 'New York', 'DejaVu serif', serif; font-size: 100%%; margin:1em 2em;"> |
||||
<div style="font-size: 1.2em;"> |
||||
<p>Confirm your subscription to <a href="` + c.CanonicalURL() + `">` + c.DisplayTitle() + `</a> to start receiving future posts:</p> |
||||
<p><a href="` + c.CanonicalURL() + `email/confirm/` + subID + `?t=` + token + `">Subscribe to ` + c.DisplayTitle() + `</a></p> |
||||
<p>If you didn't subscribe to this site or you're not sure why you're getting this email, you can delete it. You won't be subscribed or receive any future emails.</p> |
||||
</div> |
||||
</body> |
||||
</html>`) |
||||
gun.Send(m) |
||||
|
||||
return nil |
||||
} |
@ -1,49 +1,92 @@ |
||||
module github.com/writefreely/writefreely |
||||
|
||||
require ( |
||||
git.mills.io/prologic/go-gopher v0.0.0-20210712135410-b7ebb55feece |
||||
github.com/PuerkitoBio/goquery v1.7.0 // indirect |
||||
github.com/aymerick/douceur v0.2.0 |
||||
github.com/clbanning/mxj v1.8.4 // indirect |
||||
github.com/dustin/go-humanize v1.0.0 |
||||
github.com/fatih/color v1.10.0 |
||||
github.com/go-sql-driver/mysql v1.6.0 |
||||
github.com/dustin/go-humanize v1.0.1 |
||||
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect |
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect |
||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect |
||||
github.com/fatih/color v1.15.0 |
||||
github.com/go-ini/ini v1.67.0 |
||||
github.com/go-sql-driver/mysql v1.7.1 |
||||
github.com/go-test/deep v1.0.1 // indirect |
||||
github.com/gobuffalo/envy v1.9.0 // indirect |
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect |
||||
github.com/gorilla/csrf v1.7.0 |
||||
github.com/gorilla/csrf v1.7.1 |
||||
github.com/gorilla/feeds v1.1.1 |
||||
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/gorilla/sessions v1.2.1 |
||||
github.com/guregu/null v4.0.0+incompatible |
||||
github.com/hashicorp/go-multierror v1.1.1 |
||||
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 |
||||
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.8.0 |
||||
github.com/mattn/go-sqlite3 v1.14.6 |
||||
github.com/microcosm-cc/bluemonday v1.0.5 |
||||
github.com/mailgun/mailgun-go v2.0.0+incompatible |
||||
github.com/manifoldco/promptui v0.9.0 |
||||
github.com/mattn/go-sqlite3 v1.14.17 |
||||
github.com/microcosm-cc/bluemonday v1.0.25 |
||||
github.com/mitchellh/go-wordwrap v1.0.1 |
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d |
||||
github.com/onsi/ginkgo v1.16.4 // indirect |
||||
github.com/onsi/gomega v1.13.0 // indirect |
||||
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.7.0 |
||||
github.com/urfave/cli/v2 v2.3.0 |
||||
github.com/stretchr/testify v1.8.4 |
||||
github.com/urfave/cli/v2 v2.25.7 |
||||
github.com/writeas/activity v0.1.2 |
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 |
||||
github.com/writeas/activityserve v0.0.0-20230428180247-dc13a4f4d835 |
||||
github.com/writeas/go-strip-markdown/v2 v2.1.1 |
||||
github.com/writeas/go-webfinger v1.1.0 |
||||
github.com/writeas/httpsig v1.0.0 |
||||
github.com/writeas/impart v1.1.1 |
||||
github.com/writeas/import v0.2.1 |
||||
github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 |
||||
github.com/writeas/monday v1.3.0 |
||||
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320 |
||||
github.com/writeas/slug v1.2.0 |
||||
github.com/writeas/web-core v1.3.1-0.20210330164422-95a3a717ed8f |
||||
github.com/writeas/web-core v1.6.0 |
||||
github.com/writefreely/go-gopher v0.0.0-20220429181814-40127126f83b |
||||
github.com/writefreely/go-nodeinfo v1.2.0 |
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 |
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 |
||||
gopkg.in/ini.v1 v1.62.0 |
||||
golang.org/x/crypto v0.13.0 |
||||
golang.org/x/net v0.15.0 |
||||
) |
||||
|
||||
require ( |
||||
code.as/core/socks v1.0.0 // indirect |
||||
github.com/andybalholm/cascadia v1.1.0 // indirect |
||||
github.com/beevik/etree v1.1.0 // indirect |
||||
github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1 // indirect |
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect |
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect |
||||
github.com/davecgh/go-spew v1.1.1 // indirect |
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 // indirect |
||||
github.com/fatih/structs v1.1.0 // indirect |
||||
github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe // indirect |
||||
github.com/gofrs/uuid v3.3.0+incompatible // indirect |
||||
github.com/gologme/log v1.2.0 // indirect |
||||
github.com/gorilla/css v1.0.0 // indirect |
||||
github.com/gorilla/securecookie v1.1.1 // indirect |
||||
github.com/hashicorp/errwrap v1.0.0 // indirect |
||||
github.com/joho/godotenv v1.3.0 // indirect |
||||
github.com/jtolds/gls v4.2.1+incompatible // indirect |
||||
github.com/mattn/go-colorable v0.1.13 // indirect |
||||
github.com/mattn/go-isatty v0.0.17 // indirect |
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect |
||||
github.com/pkg/errors v0.9.1 // indirect |
||||
github.com/pmezard/go-difflib v1.0.0 // indirect |
||||
github.com/rogpeppe/go-internal v1.3.2 // indirect |
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect |
||||
github.com/sasha-s/go-deadlock v0.3.1 // indirect |
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect |
||||
github.com/writeas/go-writeas/v2 v2.0.2 // indirect |
||||
github.com/writeas/openssl-go v1.0.0 // indirect |
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect |
||||
golang.org/x/sys v0.12.0 // indirect |
||||
golang.org/x/text v0.13.0 // indirect |
||||
gopkg.in/ini.v1 v1.62.0 // indirect |
||||
gopkg.in/yaml.v3 v3.0.1 // indirect |
||||
) |
||||
|
||||
go 1.15 |
||||
go 1.19 |
||||
|
@ -0,0 +1,72 @@ |
||||
package writefreely |
||||
|
||||
import ( |
||||
"github.com/writeas/web-core/log" |
||||
"time" |
||||
) |
||||
|
||||
type PostJob struct { |
||||
ID int64 |
||||
PostID string |
||||
Action string |
||||
Delay int64 |
||||
} |
||||
|
||||
func addJob(app *App, p *PublicPost, action string, delay int64) error { |
||||
j := &PostJob{ |
||||
PostID: p.ID, |
||||
Action: action, |
||||
Delay: delay, |
||||
} |
||||
return app.db.InsertJob(j) |
||||
} |
||||
|
||||
func startPublishJobsQueue(app *App) { |
||||
t := time.NewTicker(62 * time.Second) |
||||
for { |
||||
log.Info("[jobs] Done.") |
||||
<-t.C |
||||
log.Info("[jobs] Fetching email publish jobs...") |
||||
jobs, err := app.db.GetJobsToRun("email") |
||||
if err != nil { |
||||
log.Error("[jobs] %s - Skipping.", err) |
||||
continue |
||||
} |
||||
log.Info("[jobs] Running %d email publish jobs...", len(jobs)) |
||||
err = runJobs(app, jobs, true) |
||||
if err != nil { |
||||
log.Error("[jobs] Failed: %s", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func runJobs(app *App, jobs []*PostJob, reqColl bool) error { |
||||
for _, j := range jobs { |
||||
p, err := app.db.GetPost(j.PostID, 0) |
||||
if err != nil { |
||||
log.Info("[job #%d] Unable to get post: %s", j.ID, err) |
||||
continue |
||||
} |
||||
if !p.CollectionID.Valid && reqColl { |
||||
log.Info("[job #%d] Post %s not part of a collection", j.ID, p.ID) |
||||
app.db.DeleteJob(j.ID) |
||||
continue |
||||
} |
||||
coll, err := app.db.GetCollectionByID(p.CollectionID.Int64) |
||||
if err != nil { |
||||
log.Info("[job #%d] Unable to get collection: %s", j.ID, err) |
||||
continue |
||||
} |
||||
coll.hostName = app.cfg.App.Host |
||||
coll.ForPublic() |
||||
p.Collection = &CollectionObj{Collection: *coll} |
||||
err = emailPost(app, p, p.Collection.ID) |
||||
if err != nil { |
||||
log.Error("[job #%d] Failed to email post %s", j.ID, p.ID) |
||||
continue |
||||
} |
||||
log.Info("[job #%d] Success for post %s.", j.ID, p.ID) |
||||
app.db.DeleteJob(j.ID) |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,38 @@ |
||||
/* |
||||
* Copyright © 2020 Musing Studio 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 |
||||
|
||||
/** |
||||
* Widen `oauth_users.access_token`, necessary only for mysql |
||||
*/ |
||||
func widenOauthAcceesToken(db *datastore) error { |
||||
if db.driverName == driverMySQL { |
||||
t, err := db.Begin() |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
|
||||
_, err = t.Exec(`ALTER TABLE oauth_users MODIFY COLUMN access_token ` + db.typeText() + db.collateMultiByte() + ` NULL`) |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
|
||||
err = t.Commit() |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,33 @@ |
||||
/* |
||||
* Copyright © 2023 Musing Studio 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 fediverseVerifyProfile(db *datastore) error { |
||||
t, err := db.Begin() |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
|
||||
_, err = t.Exec(`ALTER TABLE remoteusers ADD COLUMN url ` + db.typeVarChar(255) + ` NULL` + db.after("shared_inbox")) |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
|
||||
err = t.Commit() |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,58 @@ |
||||
/* |
||||
* Copyright © 2021 Musing Studio 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 supportLetters(db *datastore) error { |
||||
t, err := db.Begin() |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
|
||||
_, err = t.Exec(`CREATE TABLE publishjobs ( |
||||
id ` + db.typeIntPrimaryKey() + `, |
||||
post_id ` + db.typeVarChar(16) + ` not null, |
||||
action ` + db.typeVarChar(16) + ` not null, |
||||
delay ` + db.typeTinyInt() + ` not null |
||||
)`) |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
|
||||
_, err = t.Exec(`CREATE TABLE emailsubscribers ( |
||||
id ` + db.typeChar(8) + ` not null, |
||||
collection_id ` + db.typeInt() + ` not null, |
||||
user_id ` + db.typeInt() + ` null, |
||||
email ` + db.typeVarChar(255) + ` null, |
||||
subscribed ` + db.typeDateTime() + ` not null, |
||||
token ` + db.typeChar(16) + ` not null, |
||||
confirmed ` + db.typeBool() + ` default 0 not null, |
||||
allow_export ` + db.typeBool() + ` default 0 not null, |
||||
constraint eu_coll_email |
||||
unique (collection_id, email), |
||||
constraint eu_coll_user |
||||
unique (collection_id, user_id), |
||||
PRIMARY KEY (id) |
||||
)`) |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
|
||||
err = t.Commit() |
||||
if err != nil { |
||||
t.Rollback() |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,10 @@ |
||||
|
||||
[provider_sect] |
||||
default = default_sect |
||||
legacy = legacy_sect |
||||
|
||||
[default_sect] |
||||
activate = 1 |
||||
|
||||
[legacy_sect] |
||||
activate = 1 |
@ -1,7 +1,6 @@ |
||||
{{define "head"}}<title>Page not found — {{.SiteName}}</title>{{end}} |
||||
{{define "content"}} |
||||
<div class="error-page"> |
||||
<p class="msg">This page is missing.</p> |
||||
<p>Are you sure it was ever here?</p> |
||||
<p class="msg">Page not found.</p> |
||||
</div> |
||||
{{end}} |
||||
|
@ -0,0 +1,8 @@ |
||||
{{define "head"}}<title>{{.ContentTitle}} — {{.SiteName}}</title> |
||||
<meta name="description" content="{{.PlainContent}}"> |
||||
{{end}} |
||||
{{define "content"}}<div class="content-container snug"> |
||||
<h1>{{.ContentTitle}}</h1> |
||||
{{.Content}} |
||||
</div> |
||||
{{end}} |
@ -0,0 +1,43 @@ |
||||
/* |
||||
* Copyright © 2020-2021 Musing Studio 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 spam |
||||
|
||||
import ( |
||||
"github.com/writeas/web-core/id" |
||||
"strings" |
||||
) |
||||
|
||||
var honeypotField string |
||||
|
||||
func HoneypotFieldName() string { |
||||
if honeypotField == "" { |
||||
honeypotField = id.Generate62RandomString(39) |
||||
} |
||||
return honeypotField |
||||
} |
||||
|
||||
// CleanEmail takes an email address and strips it down to a unique address that can be blocked.
|
||||
func CleanEmail(email string) string { |
||||
emailParts := strings.Split(strings.ToLower(email), "@") |
||||
if len(emailParts) < 2 { |
||||
return "" |
||||
} |
||||
u := emailParts[0] |
||||
d := emailParts[1] |
||||
// Ignore anything after '+'
|
||||
plusIdx := strings.IndexRune(u, '+') |
||||
if plusIdx > -1 { |
||||
u = u[:plusIdx] |
||||
} |
||||
// Strip dots in email address
|
||||
u = strings.ReplaceAll(u, ".", "") |
||||
return u + "@" + d |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue