mirror of https://github.com/jlelse/GoBlog
Add comment functionality
This commit is contained in:
parent
6a0eda2184
commit
72f676dda2
|
@ -22,13 +22,7 @@ func jwtKey() []byte {
|
|||
|
||||
func authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. Check basic auth
|
||||
username, password, basicauth := r.BasicAuth()
|
||||
if basicauth && checkCredentials(username, password) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// 2. Check JWT
|
||||
// 1. Check JWT
|
||||
if tokenCookie, err := r.Cookie("token"); err == nil {
|
||||
if tkn, err := jwt.Parse(tokenCookie.Value, func(t *jwt.Token) (interface{}, error) {
|
||||
return jwtKey(), nil
|
||||
|
@ -37,7 +31,7 @@ func authMiddleware(next http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
}
|
||||
// 3. Show login form
|
||||
// 2. Show login form
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
h, _ := json.Marshal(r.Header.Clone())
|
||||
b, _ := ioutil.ReadAll(io.LimitReader(r.Body, 2000000)) // Only allow 20 Megabyte
|
||||
|
@ -68,8 +62,7 @@ func checkIsLogin(next http.Handler) http.Handler {
|
|||
func checkLogin(w http.ResponseWriter, r *http.Request) bool {
|
||||
if r.Method == http.MethodPost &&
|
||||
r.Header.Get(contentType) == contentTypeWWWForm &&
|
||||
r.FormValue("loginaction") == "login" &&
|
||||
checkCredentials(r.FormValue("username"), r.FormValue("password")) {
|
||||
r.FormValue("loginaction") == "login" {
|
||||
// Do original request
|
||||
loginbody, _ := base64.StdEncoding.DecodeString(r.FormValue("loginbody"))
|
||||
req, _ := http.NewRequest(r.FormValue("loginmethod"), r.RequestURI, bytes.NewReader(loginbody))
|
||||
|
@ -80,10 +73,18 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
|
|||
for k, v := range headers {
|
||||
req.Header[k] = v
|
||||
}
|
||||
// Set basic auth
|
||||
req.SetBasicAuth(r.FormValue("username"), r.FormValue("password"))
|
||||
// Send cookie
|
||||
sendTokenCookie(w, r)
|
||||
// Check credential
|
||||
if checkCredentials(r.FormValue("username"), r.FormValue("password")) {
|
||||
tokenCookie, err := createTokenCookie()
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
// Add cookie to original request
|
||||
req.AddCookie(tokenCookie)
|
||||
// Send cookie
|
||||
http.SetCookie(w, tokenCookie)
|
||||
}
|
||||
// Serve original request
|
||||
d.ServeHTTP(w, req)
|
||||
return true
|
||||
|
@ -91,20 +92,18 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func sendTokenCookie(w http.ResponseWriter, r *http.Request) {
|
||||
func createTokenCookie() (*http.Cookie, error) {
|
||||
expiration := time.Now().Add(7 * 24 * time.Hour)
|
||||
tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwt.StandardClaims{ExpiresAt: expiration.Unix()}).SignedString(jwtKey())
|
||||
if err != nil {
|
||||
serveError(w, r, "Failed to sign JWT", http.StatusInternalServerError)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
return &http.Cookie{
|
||||
Name: "token",
|
||||
Value: tokenString,
|
||||
Expires: expiration,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
return
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dchest/captcha"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
func initCaptcha() {
|
||||
}
|
||||
|
||||
func captchaMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. Check JWT
|
||||
if captchaCookie, err := r.Cookie("captcha"); err == nil {
|
||||
if tkn, err := jwt.Parse(captchaCookie.Value, func(t *jwt.Token) (interface{}, error) {
|
||||
return jwtKey(), nil
|
||||
}); err == nil && tkn.Valid {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 2. Show Captcha
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
h, _ := json.Marshal(r.Header.Clone())
|
||||
b, _ := ioutil.ReadAll(io.LimitReader(r.Body, 2000000)) // Only allow 20 Megabyte
|
||||
_ = r.Body.Close()
|
||||
if len(b) == 0 {
|
||||
// Maybe it's a form
|
||||
_ = r.ParseForm()
|
||||
b = []byte(r.PostForm.Encode())
|
||||
}
|
||||
render(w, templateCaptcha, &renderData{
|
||||
Data: map[string]string{
|
||||
"captchamethod": r.Method,
|
||||
"captchaheaders": base64.StdEncoding.EncodeToString(h),
|
||||
"captchabody": base64.StdEncoding.EncodeToString(b),
|
||||
"captchaid": captcha.New(),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func checkIsCaptcha(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if !checkCaptcha(rw, r) {
|
||||
next.ServeHTTP(rw, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func checkCaptcha(w http.ResponseWriter, r *http.Request) bool {
|
||||
if r.Method == http.MethodPost &&
|
||||
r.Header.Get(contentType) == contentTypeWWWForm &&
|
||||
r.FormValue("captchaaction") == "captcha" {
|
||||
// Do original request
|
||||
captchabody, _ := base64.StdEncoding.DecodeString(r.FormValue("captchabody"))
|
||||
req, _ := http.NewRequest(r.FormValue("captchamethod"), r.RequestURI, bytes.NewReader(captchabody))
|
||||
// Copy original headers
|
||||
captchaheaders, _ := base64.StdEncoding.DecodeString(r.FormValue("captchaheaders"))
|
||||
var headers http.Header
|
||||
json.Unmarshal(captchaheaders, &headers)
|
||||
for k, v := range headers {
|
||||
req.Header[k] = v
|
||||
}
|
||||
// Check captcha
|
||||
if captcha.VerifyString(r.FormValue("captchaid"), r.FormValue("digits")) {
|
||||
// Create cookie
|
||||
captchaCookie, err := createCaptchaCookie()
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
// Add cookie to original request
|
||||
req.AddCookie(captchaCookie)
|
||||
// Send cookie
|
||||
http.SetCookie(w, captchaCookie)
|
||||
}
|
||||
// Serve original request
|
||||
d.ServeHTTP(w, req)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func createCaptchaCookie() (*http.Cookie, error) {
|
||||
expiration := time.Now().Add(24 * time.Hour)
|
||||
tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwt.StandardClaims{ExpiresAt: expiration.Unix()}).SignedString(jwtKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Cookie{
|
||||
Name: "captcha",
|
||||
Value: tokenString,
|
||||
Expires: expiration,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
type comment struct {
|
||||
ID int
|
||||
Target string
|
||||
Name string
|
||||
Website string
|
||||
Comment string
|
||||
}
|
||||
|
||||
func serveComment(blog string) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
row, err := appDbQueryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id))
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
comment := &comment{}
|
||||
if err = row.Scan(&comment.ID, &comment.Target, &comment.Name, &comment.Website, &comment.Comment); err == sql.ErrNoRows {
|
||||
serve404(w, r)
|
||||
return
|
||||
} else if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
render(w, templateComment, &renderData{
|
||||
BlogString: blog,
|
||||
Data: comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createComment(blog, commentsPath string) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check target
|
||||
target := checkCommentTarget(w, r)
|
||||
if target == "" {
|
||||
return
|
||||
}
|
||||
// Check comment
|
||||
comment := r.FormValue("comment")
|
||||
if comment == "" {
|
||||
serveError(w, r, "Comment is empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name := r.FormValue("name")
|
||||
if name == "" {
|
||||
name = "Anonymous"
|
||||
}
|
||||
website := r.FormValue("website")
|
||||
// Clean
|
||||
strict := bluemonday.StrictPolicy()
|
||||
name = strict.Sanitize(name)
|
||||
website = strict.Sanitize(website)
|
||||
comment = strict.Sanitize(comment)
|
||||
// Insert
|
||||
result, err := appDbExec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website))
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
commentID, err := result.LastInsertId()
|
||||
commentAddress := fmt.Sprintf("%s/%d", commentsPath, commentID)
|
||||
// Send webmention
|
||||
createWebmention(appConfig.Server.PublicAddress+commentAddress, appConfig.Server.PublicAddress+target)
|
||||
// Redirect to comment
|
||||
http.Redirect(w, r, commentAddress, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func checkCommentTarget(w http.ResponseWriter, r *http.Request) string {
|
||||
target := r.FormValue("target")
|
||||
if target == "" {
|
||||
serveError(w, r, "No target specified", http.StatusBadRequest)
|
||||
return ""
|
||||
}
|
||||
postExists := 0
|
||||
row, err := appDbQueryRow("select exists(select 1 from posts where path = @path)", sql.Named("path", target))
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return ""
|
||||
}
|
||||
if err = row.Scan(&postExists); err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return ""
|
||||
}
|
||||
if postExists != 1 {
|
||||
serveError(w, r, "Post does not exist", http.StatusBadRequest)
|
||||
return ""
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func commentsAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
comments, err := getComments()
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
render(w, templateCommentsAdmin, &renderData{
|
||||
Data: comments,
|
||||
})
|
||||
}
|
||||
|
||||
func getComments() ([]*comment, error) {
|
||||
comments := []*comment{}
|
||||
rows, err := appDbQuery("select id, target, name, website, comment from comments")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
c := &comment{}
|
||||
err = rows.Scan(&c.ID, &c.Target, &c.Name, &c.Website, &c.Comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
comments = append(comments, c)
|
||||
}
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
func commentsAdminDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(r.FormValue("commentid"))
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = deleteComment(id)
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
purgeCache()
|
||||
http.Redirect(w, r, ".", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
func deleteComment(id int) error {
|
||||
_, err := appDbExec("delete from comments where id = @id", sql.Named("id", id))
|
||||
return err
|
||||
}
|
|
@ -64,6 +64,7 @@ type configBlog struct {
|
|||
Telegram *configTelegram `mapstructure:"telegram"`
|
||||
PostAsHome bool `mapstructure:"postAsHome"`
|
||||
RandomPost *randomPost `mapstructure:"randomPost"`
|
||||
Comments *comments `mapstructure:"comments"`
|
||||
}
|
||||
|
||||
type section struct {
|
||||
|
@ -124,6 +125,10 @@ type randomPost struct {
|
|||
Path string `mapstructure:"path"`
|
||||
}
|
||||
|
||||
type comments struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
type configUser struct {
|
||||
Nick string `mapstructure:"nick"`
|
||||
Name string `mapstructure:"name"`
|
||||
|
|
|
@ -121,6 +121,15 @@ func migrateDb() error {
|
|||
return err
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "00010",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
create table comments (id integer primary key autoincrement, target text not null, name text not null, website text not null, comment text not null);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
5
go.mod
5
go.mod
|
@ -8,6 +8,7 @@ require (
|
|||
github.com/andybalholm/cascadia v1.2.0 // indirect
|
||||
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4
|
||||
github.com/caddyserver/certmagic v0.12.0
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/go-chi/chi v1.5.1
|
||||
github.com/go-fed/httpsig v1.1.0
|
||||
|
@ -31,7 +32,8 @@ require (
|
|||
github.com/lopezator/migrator v0.3.0
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/mholt/acmez v0.1.2 // indirect
|
||||
github.com/mholt/acmez v0.1.3 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.4
|
||||
github.com/miekg/dns v1.1.35 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
|
@ -54,6 +56,7 @@ require (
|
|||
golang.org/x/mod v0.4.1 // indirect
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
|
||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619 // indirect
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
|
|
16
go.sum
16
go.sum
|
@ -32,6 +32,8 @@ github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4/go.mod h1:hMAUZF
|
|||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
|
@ -41,6 +43,8 @@ github.com/caddyserver/certmagic v0.12.0/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog2
|
|||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
|
||||
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
|
||||
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
|
@ -51,6 +55,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
|
@ -110,6 +116,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
|
|||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
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.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
|
@ -209,8 +217,10 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
|
|||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mholt/acmez v0.1.1 h1:KQODCqk+hBn3O7qfCRPj6L96uG65T5BSS95FKNEqtdA=
|
||||
github.com/mholt/acmez v0.1.1/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
|
||||
github.com/mholt/acmez v0.1.2 h1:26ncYNBt59D+59cMUHuGa/Fzjmu6FFrBm6kk/8hdXt0=
|
||||
github.com/mholt/acmez v0.1.2/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
|
||||
github.com/mholt/acmez v0.1.3 h1:J7MmNIk4Qf9b8mAGqAh4XkNeowv3f1zW816yf4zt7Qk=
|
||||
github.com/mholt/acmez v0.1.3/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
|
@ -435,6 +445,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619 h1:yLLDsUUPDliIQpKl7BjVb1igwngIMH2GBjo1VpwLTE0=
|
||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
|
17
http.go
17
http.go
|
@ -8,6 +8,7 @@ import (
|
|||
"sync/atomic"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/dchest/captcha"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
)
|
||||
|
@ -85,6 +86,7 @@ func buildHandler() (http.Handler, error) {
|
|||
r.Use(middleware.NoCache)
|
||||
}
|
||||
r.Use(checkIsLogin)
|
||||
r.Use(checkIsCaptcha)
|
||||
|
||||
// Profiler
|
||||
if appConfig.Server.Debug {
|
||||
|
@ -186,6 +188,9 @@ func buildHandler() (http.Handler, error) {
|
|||
// Media files
|
||||
r.Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, serveMediaFile)
|
||||
|
||||
// Captcha
|
||||
r.Handle("/captcha/*", captcha.Server(500, 250))
|
||||
|
||||
// Short paths
|
||||
r.With(cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", redirectToLongPath)
|
||||
|
||||
|
@ -333,6 +338,18 @@ func buildHandler() (http.Handler, error) {
|
|||
mpRouter.Get("/", serveEditor(blog))
|
||||
mpRouter.Post("/", serveEditorPost(blog))
|
||||
})
|
||||
|
||||
// Comments
|
||||
if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled {
|
||||
commentsPath := blogPath + "/comment"
|
||||
r.Route(commentsPath, func(cr chi.Router) {
|
||||
cr.With(cacheMiddleware, minifier.Middleware).Get("/{id:[0-9]+}", serveComment(blog))
|
||||
cr.With(captchaMiddleware).Post("/", createComment(blog, commentsPath))
|
||||
// Admin
|
||||
cr.With(minifier.Middleware, authMiddleware).Get("/", commentsAdmin)
|
||||
cr.With(authMiddleware).Post("/delete", commentsAdminDelete)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sitemap
|
||||
|
|
|
@ -35,6 +35,9 @@ const templateEditor = "editor"
|
|||
const templateLogin = "login"
|
||||
const templateStaticHome = "statichome"
|
||||
const templateBlogStats = "blogstats"
|
||||
const templateComment = "comment"
|
||||
const templateCaptcha = "captcha"
|
||||
const templateCommentsAdmin = "commentsadmin"
|
||||
|
||||
var templates map[string]*template.Template
|
||||
var templateFunctions template.FuncMap
|
||||
|
@ -199,6 +202,9 @@ func initRendering() error {
|
|||
}
|
||||
return parsed
|
||||
},
|
||||
"commentsenabled": func(blog *configBlog) bool {
|
||||
return blog.Comments != nil && blog.Comments.Enabled
|
||||
},
|
||||
}
|
||||
|
||||
templates = map[string]*template.Template{}
|
||||
|
|
|
@ -154,6 +154,10 @@ footer * {
|
|||
height: 400px;
|
||||
}
|
||||
|
||||
.captchaimg {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Print */
|
||||
@media print {
|
||||
html {
|
||||
|
|
|
@ -181,6 +181,10 @@ footer {
|
|||
height: 400px;
|
||||
}
|
||||
|
||||
.captchaimg {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Print */
|
||||
@media print {
|
||||
html {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{{ define "title" }}
|
||||
<title>{{ string .Blog.Lang "captcha" }} - {{ .Blog.Title }}</title>
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main>
|
||||
<h1>{{ string .Blog.Lang "captcha" }}</h1>
|
||||
<img src="/captcha/{{ .Data.captchaid }}.png" class="captchaimg">
|
||||
<form class="fw-form p" method="post">
|
||||
<input type="hidden" name="captchaaction" value="captcha">
|
||||
<input type="hidden" name="captchamethod" value="{{ .Data.captchamethod }}">
|
||||
<input type="hidden" name="captchaheaders" value="{{ .Data.captchaheaders }}">
|
||||
<input type="hidden" name="captchabody" value="{{ .Data.captchabody }}">
|
||||
<input type="hidden" name="captchaid" value="{{ .Data.captchaid }}">
|
||||
<input type="text" name="digits" placeholder="{{ string .Blog.Lang "captchainstructions" }}">
|
||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "submit" }}">
|
||||
</form>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "captcha" }}
|
||||
{{ template "base" . }}
|
||||
{{ end }}
|
|
@ -0,0 +1,20 @@
|
|||
{{ define "title" }}
|
||||
<title>{{ string .Blog.Lang "acommentby" }} {{ .Data.Name }} - {{ .Blog.Title }}</title>
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main class=h-entry>
|
||||
<p><a class="u-in-reply-to" href="{{ absolute .Data.Target }}">{{ absolute .Data.Target }}</a></p>
|
||||
<div class="p-author h-card p">
|
||||
{{ string .Blog.Lang "acommentby" }}
|
||||
{{ if .Data.Website }}<a href="{{ .Data.Website }}" class="p-name u-url" target="_blank" rel="nofollow noopener noreferrer ugc">{{ .Data.Name }}</a>{{ else }}<span class="p-name">{{ .Data.Name }}</span>{{ end }}:
|
||||
</div>
|
||||
<div class="e-content p">
|
||||
{{ .Data.Comment }}
|
||||
</div>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "comment" }}
|
||||
{{ template "base" . }}
|
||||
{{ end }}
|
|
@ -0,0 +1,28 @@
|
|||
{{ define "title" }}
|
||||
<title>{{ string .Blog.Lang "comments" }} - {{ .Blog.Title }}</title>
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main>
|
||||
<h1>{{ string .Blog.Lang "comments" }}</h1>
|
||||
{{ $blog := .Blog }}
|
||||
{{ range $i, $comment := .Data }}
|
||||
<div class="p">
|
||||
<p>
|
||||
Target: <a href="{{ $comment.Target }}" target="_blank">{{ $comment.Target }}</a><br/>
|
||||
Name: {{ $comment.Name }}<br/>
|
||||
Website: {{ $comment.Website }}<br/>
|
||||
Comment: {{ $comment.Comment }}
|
||||
</p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="commentid" value="{{ $comment.ID }}">
|
||||
<input type="submit" formaction="comment/delete" value="{{ string $blog.Lang "delete" }}">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "commentsadmin" }}
|
||||
{{ template "base" . }}
|
||||
{{ end }}
|
|
@ -6,7 +6,7 @@
|
|||
{{ if $mentions }}
|
||||
<ul>
|
||||
{{ range $i, $mention := $mentions }}
|
||||
<li><a href="{{$mention.Source}}" target="_blank" rel="nofollow noopener noreferrer">
|
||||
<li><a href="{{$mention.Source}}" target="_blank" rel="nofollow noopener noreferrer ugc">
|
||||
{{ if $mention.Author }}
|
||||
{{ $mention.Author }}
|
||||
{{ else }}
|
||||
|
@ -16,12 +16,18 @@
|
|||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
<form class="fw-form" method="post" action="/webmention">
|
||||
<form class="fw-form p" method="post" action="/webmention">
|
||||
<label for="wm-source" class="p">{{ string .Blog.Lang "interactionslabel" }}</label>
|
||||
<input id="wm-source" type="url" name="source" placeholder="URL">
|
||||
<input id="wm-source" type="url" name="source" placeholder="URL" required>
|
||||
<input type="hidden" name="target" value="{{ $postperma }}">
|
||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "send" }}">
|
||||
</form>
|
||||
<a class="p button fw ct" href="https://quill.p3k.io/?dontask=1&me=https://commentpara.de&reply={{ $postperma }}">{{ string .Blog.Lang "anoncomment" }}</a>
|
||||
<form class="fw-form p" method="post" action="{{ blogrelative .Blog "/comment" }}">
|
||||
<input type="hidden" name="target" value="{{ .Data.Path }}">
|
||||
<input type="text" name="name" placeholder="{{ string .Blog.Lang "nameopt" }}">
|
||||
<input type="url" name="website" placeholder="{{ string .Blog.Lang "websiteopt" }}">
|
||||
<textarea name="comment" required placeholder="{{ string .Blog.Lang "comment" }}"></textarea>
|
||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "docomment" }}">
|
||||
</form>
|
||||
</details>
|
||||
{{ end }}
|
|
@ -27,7 +27,9 @@
|
|||
</article>
|
||||
{{ include "author" . }}
|
||||
</main>
|
||||
{{ include "interactions" . }}
|
||||
{{ if commentsenabled .Blog }}
|
||||
{{ include "interactions" . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "post" }}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
anoncomment: "Du kannst auch einen anonymen Kommentar verfassen."
|
||||
acommentby: "Ein Kommentar von"
|
||||
comment: "Kommentar"
|
||||
comments: "Kommentare"
|
||||
count: "Anzahl"
|
||||
create: "Erstellen"
|
||||
delete: "Löschen"
|
||||
docomment: "Kommentieren"
|
||||
drafts: "Entwürfe"
|
||||
editor: "Editor"
|
||||
interactions: "Interaktionen & Kommentare"
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
anoncomment: "You can also create an anonymous comment."
|
||||
acommentby: "A comment by"
|
||||
approve: "Approve"
|
||||
approved: "Approved"
|
||||
authenticate: "Authenticate"
|
||||
captcha: "Captcha"
|
||||
captchainstructions: "Please enter the digits from the image above"
|
||||
comment: "Comment"
|
||||
comments: "Comments"
|
||||
count: "Count"
|
||||
create: "Create"
|
||||
delete: "Delete"
|
||||
docomment: "Comment"
|
||||
drafts: "Drafts"
|
||||
editor: "Editor"
|
||||
indieauth: "IndieAuth"
|
||||
|
@ -12,6 +17,7 @@ interactions: "Interactions & Comments"
|
|||
interactionslabel: "Have you published a response to this? Paste the URL here."
|
||||
likeof: "Like of"
|
||||
login: "Login"
|
||||
nameopt: "Name (optional)"
|
||||
next: "Next"
|
||||
oldcontent: "⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed."
|
||||
password: "Password"
|
||||
|
@ -25,6 +31,7 @@ share: "Share"
|
|||
shorturl: "Short link:"
|
||||
speak: "Read to me, please."
|
||||
stopspeak: "Stop speaking!"
|
||||
submit: "Submit"
|
||||
total: "Total"
|
||||
translations: "Translations"
|
||||
update: "Update"
|
||||
|
@ -34,4 +41,5 @@ username: "Username"
|
|||
verified: "Verified"
|
||||
view: "View"
|
||||
webmentions: "Webmentions"
|
||||
websiteopt: "Website (optional)"
|
||||
year: "Year"
|
|
@ -117,7 +117,7 @@ func webmentionAdminDelete(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
purgeCache()
|
||||
http.Redirect(w, r, "/webmention", http.StatusFound)
|
||||
http.Redirect(w, r, ".", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -133,7 +133,7 @@ func webmentionAdminApprove(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
purgeCache()
|
||||
http.Redirect(w, r, "/webmention", http.StatusFound)
|
||||
http.Redirect(w, r, ".", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/joncrlsn/dque"
|
||||
|
@ -87,10 +86,6 @@ func (m *mention) verifyMention() error {
|
|||
m.Title = m.Title[0:57] + "…"
|
||||
}
|
||||
newStatus := webmentionStatusVerified
|
||||
if strings.HasPrefix(m.Source, appConfig.Server.PublicAddress) {
|
||||
// Approve if it's server-intern
|
||||
newStatus = webmentionStatusApproved
|
||||
}
|
||||
if webmentionExists(m.Source, m.Target) {
|
||||
_, err = appDbExec("update webmentions set status = @status, title = @title, content = @content, author = @author where source = @source and target = @target",
|
||||
sql.Named("status", newStatus), sql.Named("title", m.Title), sql.Named("content", m.Content), sql.Named("author", m.Author), sql.Named("source", m.Source), sql.Named("target", m.Target))
|
||||
|
|
Loading…
Reference in New Issue