diff --git a/.gitignore b/.gitignore
index 7fb25a4..bd54b44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
gowiki
+tmp
+run.sh
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index ab38ad3..02c4088 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -1,12 +1,12 @@
{
"ImportPath": "git.giftfish.de/ston1th/gowiki",
- "GoVersion": "go1.7",
- "GodepVersion": "v74",
+ "GoVersion": "go1.8",
+ "GodepVersion": "v79",
"Deps": [
{
"ImportPath": "git.giftfish.de/ston1th/godrop",
- "Comment": "v1.0",
- "Rev": "8666e4461c9612e1432f05af6e0abfd716adca36"
+ "Comment": "v1.1",
+ "Rev": "108583814ef55f34ac8e939cc47699dc7e9befb8"
},
{
"ImportPath": "github.com/blevesearch/bleve",
@@ -183,8 +183,8 @@
},
{
"ImportPath": "github.com/gorilla/mux",
- "Comment": "v1.1",
- "Rev": "0eeaf8392f5b04950925b8a69fe70f110fa7cbfc"
+ "Comment": "v1.3.0",
+ "Rev": "392c28fe23e1c45ddba891b0320b3b5df220beea"
},
{
"ImportPath": "github.com/gorilla/securecookie",
@@ -193,12 +193,13 @@
},
{
"ImportPath": "github.com/gorilla/sessions",
- "Rev": "f7261893ca3ea922c30eabe742c036d2c1de6e0a"
+ "Comment": "v1.1",
+ "Rev": "ca9ada44574153444b00d3fd9c8559e4cc95f896"
},
{
"ImportPath": "github.com/russross/blackfriday",
- "Comment": "v1.4-31-g2004188",
- "Rev": "2004188462c3946efefbdcd91aa83e49b4175bfb"
+ "Comment": "v1.4",
+ "Rev": "0b647d0506a698cca42caca173e55559b12a69f2"
},
{
"ImportPath": "github.com/shurcooL/sanitized_anchor_name",
@@ -208,6 +209,11 @@
"ImportPath": "github.com/steveyen/gtreap",
"Rev": "0abe01ef9be25c4aedc174758ec2d917314d6d70"
},
+ {
+ "ImportPath": "github.com/urfave/cli",
+ "Comment": "v1.19.1",
+ "Rev": "0bdeddeeb0f650497d603c4ad7b20cfe685682f6"
+ },
{
"ImportPath": "golang.org/x/crypto/bcrypt",
"Rev": "9477e0b78b9ac3d0b03822fd95422e2fe07627cd"
diff --git a/Makefile b/Makefile
index d21289b..3131911 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,8 @@
CC=go
BUILD=build -v
-LDFLAGS=--ldflags "-s -w"
+VERSION=$(shell cat VERSION)
+GCFLAGS=-gcflags '-e'
+LDFLAGS=-ldflags '-X main.version=$(VERSION) -s -w'
PROGRAM=gowiki
all: $(PROGRAM)
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..c0ec837
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+v2.0
diff --git a/cache.go b/cache.go
new file mode 100644
index 0000000..27723ba
--- /dev/null
+++ b/cache.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+ "sync"
+ "time"
+)
+
+type CachedArticle struct {
+ Article *Article
+ CacheTime time.Time
+}
+
+type IndexCache struct {
+ sync.RWMutex
+ Cache map[string]*CachedArticle
+}
+
+func NewIndexCache() *IndexCache {
+ return &IndexCache{Cache: make(map[string]*CachedArticle)}
+}
+
+func (ic IndexCache) Add(title string, a *Article) {
+ now := time.Now()
+ ic.Lock()
+ defer ic.Unlock()
+ c, ok := ic.Cache[title]
+ if ok && now.After(c.CacheTime) {
+ c.Article = a
+ c.CacheTime = now
+ ic.Cache[title] = c
+ return
+ }
+ ic.Cache[title] = &CachedArticle{a, now}
+}
+
+func (ic IndexCache) Get(title string) *Article {
+ ic.RLock()
+ defer ic.RUnlock()
+ if a, ok := ic.Cache[title]; ok {
+ return a.Article
+ }
+ return nil
+}
+
+func (ic *IndexCache) Delete(title string) {
+ ic.Lock()
+ delete(ic.Cache, title)
+ ic.Unlock()
+}
diff --git a/cmd.go b/cmd.go
new file mode 100644
index 0000000..f50f4d5
--- /dev/null
+++ b/cmd.go
@@ -0,0 +1,193 @@
+// Copyright (C) 2017 Marius Schellenberger
+
+package main
+
+import (
+ "errors"
+ "git.giftfish.de/ston1th/godrop"
+ "github.com/urfave/cli"
+ logger "log"
+ "net"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+)
+
+const (
+ blevePath = "bleve"
+
+ defDataDir = "/var/lib/gowiki"
+ defListen = "127.0.0.1:8090"
+ defLogFile = "gowiki.log"
+
+ defRunUser = "wiki"
+ defRunGroup = "wiki"
+)
+
+func server(conf serverConfig) (err error) {
+ dropCfg := godrop.Config{
+ User: conf.User,
+ Group: conf.Group,
+ Foreground: conf.Foreground,
+ }
+ err = godrop.Drop(dropCfg, func() (net.Listener, error) {
+ return net.Listen("tcp", conf.ListenAddr)
+ })
+ if err != nil {
+ return
+ }
+ if err = os.Chdir(conf.DataDir); err != nil {
+ return
+ }
+
+ newLogger(conf.LogFile)
+
+ l, err := godrop.GetListener()
+ if err != nil {
+ return
+ }
+
+ srv := NewHTTPServer(l, conf.Version, conf.SecureCookie)
+ err = srv.Start()
+ if err != nil {
+ return
+ }
+ log.Println("gowiki started")
+ sigs := make(chan os.Signal)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+ sig := <-sigs
+ log.Println("caught " + sig.String())
+ go func() {
+ time.Sleep(time.Second * 20)
+ log.Fatal(errors.New("stop timed out: killing"))
+ }()
+ err = srv.Stop()
+ if err != nil {
+ log.Println(err)
+ }
+ log.Println("gowiki stopped")
+ return
+}
+
+var (
+ verbose bool
+ conf serverConfig
+)
+
+func run(app *cli.App) {
+ app.Commands = []cli.Command{
+ {
+ Name: "server",
+ Usage: "start gowiki server",
+ Flags: defaultFlags(serverFlags()),
+ Action: func(c *cli.Context) error {
+ conf.Version = app.Version
+ if err := server(conf); err != nil {
+ logger.Fatal("server: ", err)
+ }
+ return nil
+ },
+ },
+ {
+ Name: "dump",
+ Usage: "create a json dump of all articles in the data directory (dump is written to stdout) gowiki needs to be stopped",
+ Flags: defaultFlags(dumpRestoreFlags()),
+ Action: func(c *cli.Context) error {
+ if err := os.Chdir(conf.DataDir); err != nil {
+ logger.Fatal("dump: ", err)
+ }
+ if err := dump(blevePath, verbose); err != nil {
+ logger.Fatal("dump: ", err)
+ }
+ return nil
+ },
+ },
+ {
+ Name: "restore",
+ Usage: "restore a json dump to the data directory (dump is read from stdin) gowiki needs to be stopped",
+ Flags: defaultFlags(dumpRestoreFlags()),
+ Action: func(c *cli.Context) error {
+ if err := os.Chdir(conf.DataDir); err != nil {
+ logger.Fatal("restore: ", err)
+ }
+ if err := restore(blevePath, verbose); err != nil {
+ logger.Fatal("restore: ", err)
+ }
+ return nil
+ },
+ },
+ }
+ app.Run(os.Args)
+}
+
+type serverConfig struct {
+ DataDir string
+ User string
+ Group string
+ ListenAddr string
+ LogFile string
+ Version string
+ Foreground bool
+ SecureCookie bool
+}
+
+func serverFlags() []cli.Flag {
+ return []cli.Flag{
+ cli.StringFlag{
+ Name: "user, u",
+ Value: defRunUser,
+ Usage: "drop privileges to user",
+ Destination: &conf.User,
+ },
+ cli.StringFlag{
+ Name: "group, g",
+ Value: defRunGroup,
+ Usage: "drop privileges to group",
+ Destination: &conf.Group,
+ },
+ cli.StringFlag{
+ Name: "listen, l",
+ Value: defListen,
+ Usage: "listening
:",
+ Destination: &conf.ListenAddr,
+ },
+ cli.StringFlag{
+ Name: "log",
+ Value: defLogFile,
+ Usage: "log file (leave empty for stdout, this only works in combination with -f)",
+ Destination: &conf.LogFile,
+ },
+ cli.BoolFlag{
+ Name: "foreground, f",
+ Usage: "do not fork into the background",
+ Destination: &conf.Foreground,
+ },
+ cli.BoolFlag{
+ Name: "secure, s",
+ Usage: "enable secure cookie",
+ Destination: &conf.SecureCookie,
+ },
+ }
+}
+
+func dumpRestoreFlags() []cli.Flag {
+ return []cli.Flag{
+ cli.BoolFlag{
+ Name: "verbose, v",
+ Usage: "enable verbose mode",
+ Destination: &verbose,
+ },
+ }
+}
+
+func defaultFlags(f []cli.Flag) []cli.Flag {
+ return append([]cli.Flag{
+ cli.StringFlag{
+ Name: "data, d",
+ Value: defDataDir,
+ Usage: "data directory",
+ Destination: &conf.DataDir,
+ },
+ }, f...)
+}
diff --git a/context.go b/context.go
index 0513111..44f9c79 100644
--- a/context.go
+++ b/context.go
@@ -3,63 +3,16 @@
package main
import (
- "crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"github.com/gorilla/mux"
- "github.com/gorilla/sessions"
"html/template"
- "io"
"net/http"
"time"
)
-const (
- keyLen = 32
- authLen = keyLen * 2
- cookieName = "gosession"
-)
-
-var store *sessions.CookieStore
-
-func cookieStore() {
- options := &sessions.Options{
- Path: "/",
- MaxAge: 3600 * 12,
- Secure: secureCookie,
- HttpOnly: true,
- }
- authKey := genKey(authLen)
- encKey := genKey(keyLen)
- store = sessions.NewCookieStore(authKey, encKey)
- store.Options = options
-
- go func() {
- for {
- time.Sleep(time.Hour * 12)
- newAuthKey := genKey(authLen)
- newEncKey := genKey(keyLen)
- newStore := sessions.NewCookieStore(newAuthKey, newEncKey, authKey, encKey)
- newStore.Options = options
-
- store = newStore
- authKey = newAuthKey
- encKey = newEncKey
- }
- }()
-}
-
-func genKey(length int) (bytes []byte) {
- bytes = make([]byte, length)
- if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
- log.Println("genKey:", err)
- }
- return
-}
-
-func newContext(w http.ResponseWriter, r *http.Request) *Context {
- log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL.Path)
+func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context {
h := w.Header()
h.Set("X-Frame-Options", "DENY")
h.Set("X-Content-Type-Options", "nosniff")
@@ -68,18 +21,27 @@ func newContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Request: r,
Response: w,
+ Srv: s,
+ Time: time.Now(),
}
}
type Context struct {
Request *http.Request
Response http.ResponseWriter
+ Srv *HTTPServer
T *template.Template
+ Status int
+ Err error
+ Time time.Time
Data webData
}
type webData struct {
+ Version string
+ Time int64
+
Title string
BodyTitle string
@@ -91,25 +53,53 @@ type webData struct {
Msg string
Search string
+ Editor *Editor
+
Data interface{}
}
func (c *Context) Exec() {
+ defer c.log()
if c.T == nil {
- log.Println("Exec:", errors.New("template is nil"))
+ c.Status = http.StatusInternalServerError
+ c.Err = errors.New("template is nil")
return
}
+ c.newToken()
+ c.Data.Version = c.Srv.Version
+ if c.Data.BodyTitle == "" {
+ c.Data.BodyTitle = c.Data.Title
+ }
c.Data.Admin = c.GetAdmin()
c.Data.User = c.GetUser()
c.Data.Login = c.LoggedOn()
- err := c.T.Execute(c.Response, c.Data)
- if err != nil {
- log.Println("Exec:", err)
+ c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e6
+ c.Err = c.T.Execute(c.Response, c.Data)
+ if c.Err != nil {
+ c.Status = http.StatusInternalServerError
+ }
+ if c.Status == 0 {
+ c.Status = http.StatusOK
}
}
+func (c *Context) log() {
+ if c.Err != nil {
+ log.Printf("%s %s %s %d error: %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status, c.Err)
+ return
+ }
+ log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status)
+}
+
+func (c *Context) NotFound() {
+ c.Template("notFoundHandler")
+ c.Data = webData{Title: "404"}
+ c.Status = http.StatusNotFound
+ c.Response.WriteHeader(http.StatusNotFound)
+ c.Exec()
+}
func (c *Context) Template(name string) {
- c.T = templ[name]
+ c.T = c.Srv.templ[name]
}
func (c *Context) Write(buf []byte) (err error) {
@@ -122,6 +112,7 @@ func (c *Context) SetHeader(name, value string) {
}
func (c *Context) Redirect(uri string, code int) {
+ log.Printf("%s %s %s %d %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, code, uri)
http.Redirect(c.Response, c.Request, uri, code)
}
@@ -149,12 +140,12 @@ func (c *Context) Var(name string) (ret string) {
return
}
-func (c *Context) InitToken() {
+func (c *Context) initToken() {
token := c.getToken()
if token == nil || len(token) != keyLen {
- s, _ := store.Get(c.Request, cookieName)
+ s, _ := c.Srv.Store.Get(c.Request, cookieName)
s.Values["token"] = genKey(keyLen)
- s.Options = store.Options
+ s.Options = c.Srv.Store.Options
err := s.Save(c.Request, c.Response)
if err != nil {
log.Println("InitToken: ", err)
@@ -162,14 +153,14 @@ func (c *Context) InitToken() {
}
}
-// NewToken sets and returns the xsrf token
-func (c *Context) NewToken() {
+func (c *Context) newToken() {
+ c.initToken()
rnd := genKey(keyLen)
c.Data.Token = base64.StdEncoding.EncodeToString(append(rnd, xor(rnd, c.getToken())...))
}
// CheckToken validates the xsrf token
-func (c *Context) CheckToken() bool {
+func (c *Context) CheckToken(f func()) bool {
token, err := base64.StdEncoding.DecodeString(c.Form("token"))
if err != nil {
log.Println("CheckToken: ", err)
@@ -178,36 +169,45 @@ func (c *Context) CheckToken() bool {
if len(token) != authLen {
return false
}
- return subtle.ConstantTimeCompare(c.getToken(), xor(token[keyLen:], token[:keyLen])) == 1
+ ok := subtle.ConstantTimeCompare(c.getToken(), xor(token[keyLen:], token[:keyLen])) == 1
+ if !ok {
+ if f != nil {
+ f()
+ return ok
+ }
+ c.Data.Msg = "wrong csrf token"
+ c.Exec()
+ }
+ return ok
}
func (c *Context) LoggedOn() (ok bool) {
- s, _ := store.Get(c.Request, cookieName)
+ s, _ := c.Srv.Store.Get(c.Request, cookieName)
_, ok = s.Values["user"]
return
}
func (c *Context) SetUdata(user string, admin bool) {
- s, _ := store.Get(c.Request, cookieName)
+ s, _ := c.Srv.Store.Get(c.Request, cookieName)
s.Values["user"] = user
s.Values["admin"] = admin
- s.Options = store.Options
+ s.Options = c.Srv.Store.Options
if err := s.Save(c.Request, c.Response); err != nil {
log.Println(err)
}
}
func (c *Context) UnsetUdata() {
- s, _ := store.Get(c.Request, cookieName)
+ s, _ := c.Srv.Store.Get(c.Request, cookieName)
s.Values = make(map[interface{}]interface{})
- s.Options = store.Options
+ s.Options = c.Srv.Store.Options
if err := s.Save(c.Request, c.Response); err != nil {
log.Println(err)
}
}
func (c *Context) GetUser() string {
- s, _ := store.Get(c.Request, cookieName)
+ s, _ := c.Srv.Store.Get(c.Request, cookieName)
if ret, ok := s.Values["user"].(string); ok {
return ret
}
@@ -215,7 +215,7 @@ func (c *Context) GetUser() string {
}
func (c *Context) GetAdmin() bool {
- s, _ := store.Get(c.Request, cookieName)
+ s, _ := c.Srv.Store.Get(c.Request, cookieName)
if ret, ok := s.Values["admin"].(bool); ok {
return ret
}
@@ -223,7 +223,7 @@ func (c *Context) GetAdmin() bool {
}
func (c *Context) getToken() []byte {
- s, _ := store.Get(c.Request, cookieName)
+ s, _ := c.Srv.Store.Get(c.Request, cookieName)
t, ok := s.Values["token"]
if !ok {
return nil
diff --git a/dump.go b/dump.go
index 4cea771..5712078 100644
--- a/dump.go
+++ b/dump.go
@@ -6,10 +6,14 @@ import (
"encoding/json"
"fmt"
"os"
-
- "github.com/blevesearch/bleve"
)
+type Dump struct {
+ Articles []DumpArticle `json:"articles"`
+ Snippets []Snippet `json:"snippets"`
+ Users []User `json:"users"`
+}
+
type DumpArticle struct {
Title string `json:"title"`
LinkTitle string `json:"link_title"`
@@ -19,35 +23,38 @@ type DumpArticle struct {
Public bool `json:"public"`
}
-func verboseLog(prefix, title string) {
- if verbose {
+func verboseLog(prefix, title string, v bool) {
+ if v {
fmt.Fprintln(os.Stderr, prefix, title)
}
}
-func dumpWiki() error {
- index, err := bleve.OpenUsing(bleveStore, map[string]interface{}{"read_only": true})
+func dump(path string, v bool) (err error) {
+ i, err := NewDumpIndex(path)
if err != nil {
- return err
+ return
}
- engine = NewEngine(index)
- art, err := engine.Get(false, indexName)
+ defer i.Close()
+ bs, err := NewBoltStore()
if err != nil {
- return err
+ return
}
- defer engine.Close()
- var arts []DumpArticle
- titles, err := engine.Dump()
+ art, err := i.Get(false, indexName)
if err != nil {
- return err
+ return
+ }
+ var dump Dump
+ titles, err := i.Dump()
+ if err != nil {
+ return
}
for _, title := range titles {
- art, err = engine.Get(false, title)
+ art, err = i.Get(false, title)
if err != nil {
- return err
+ return
}
- verboseLog("dump:", art.Title)
- arts = append(arts, DumpArticle{
+ verboseLog("dump:", art.Title, v)
+ dump.Articles = append(dump.Articles, DumpArticle{
Title: art.Title,
LinkTitle: art.LinkTitle,
MD: art.MD,
@@ -56,31 +63,41 @@ func dumpWiki() error {
Public: art.Public,
})
}
- return json.NewEncoder(os.Stdout).Encode(arts)
-}
-
-func restoreWiki() (err error) {
- index, err := bleve.Open(bleveStore)
- if err != nil {
- mapping := bleve.NewIndexMapping()
- index, err = bleve.New(bleveStore, mapping)
- if err != nil {
- return
- }
- }
- engine = NewEngine(index)
- defer engine.Close()
- var arts []DumpArticle
- err = json.NewDecoder(os.Stdin).Decode(&arts)
+ //TODO snippets
+ //verboseLog("dump:", "snippets", v)
+ verboseLog("dump:", "users", v)
+ dump.Users, err = bs.DumpUsers()
if err != nil {
return
}
- for _, art := range arts {
- verboseLog("restore:", art.Title)
- err = engine.Restore(art)
+ return json.NewEncoder(os.Stdout).Encode(dump)
+}
+
+func restore(path string, v bool) (err error) {
+ i, err := NewIndex(path)
+ if err != nil {
+ return
+ }
+ defer i.Close()
+ bs, err := NewBoltStore()
+ if err != nil {
+ return
+ }
+ var dump Dump
+ err = json.NewDecoder(os.Stdin).Decode(&dump)
+ if err != nil {
+ return
+ }
+ for _, art := range dump.Articles {
+ verboseLog("restore:", art.Title, v)
+ err = i.Restore(&art)
if err != nil {
return
}
}
+ //TODO snippets
+ //verboseLog("restore:", "snippets", v)
+ verboseLog("restore:", "users", v)
+ err = bs.RestoreUsers(dump.Users)
return
}
diff --git a/dump.test b/dump.test
new file mode 100644
index 0000000..e52948d
--- /dev/null
+++ b/dump.test
@@ -0,0 +1 @@
+{"articles":[{"title":"Index","link_title":"Index","md":"# Welcome to GoWiki\nThis is the [Index](/wiki/Index) page. \nYou can customize it how you like.","created":"admin 2017-04-20 15:25:38","updated":"","public":true},{"title":"dcsdcsd","link_title":"dcsdcsd","md":"# OpenBSD httpd\r\n\r\nEnable and start service:\r\n\r\n```\r\nrcctl enable httpd\r\nrcctl start httpd\r\n```\r\n\r\n## pf Rules\r\n\r\n```\r\ncat \u003c\u003cEOF\u003e\u003e /etc/pf.conf\r\npass in on egress proto tcp to any port http\r\npass in on egress proto tcp to any port https\r\nEOF\r\npfctl -f /etc/pf.conf\r\n```\r\n\r\n## vHosts\r\n\r\nhttpd config file: `/etc/httpd.conf`\r\n\r\n### TLS vHost\r\n\r\nCertificate using acme. More info at [OpenBSD Lets Encrypt](/wiki/OpenBSD+Lets+Encrypt)\r\n\r\n```\r\ndomain=\"example.com\"\r\n\r\nserver $domain {\r\n listen on * port 80\r\n location \"/.well-known/acme-challenge/*\" {\r\n root { \"/acme\", strip 2 }\r\n }\r\n location \"/*\" {\r\n block return 301 \"https://$SERVER_NAME$DOCUMENT_URI\"\r\n }\r\n}\r\n\r\nserver $domain {\r\n listen on * tls port 443\r\n tls {\r\n certificate \"/etc/ssl/acme/example.com/fullchain.pem\"\r\n key \"/etc/ssl/acme/private/example.com/privkey.pem\"\r\n ecdhe secp384r1\r\n ciphers ECDHE-RSA-CHACHA20-POLY1305:CHACHA20:ECDHE+AESGCM:ECDHE+AES\r\n }\r\n hsts { max-age 31536000, subdomains }\r\n location \"/\" {\r\n directory auto index\r\n }\r\n}\r\n```\r\n## robots.txt\r\n\r\nCreate a new directory: `mkdir /var/www/robots`\r\n\r\nCreate the robots.txt file:\r\n\r\n```\r\ncat \u003c\u003cEOF\u003e /var/www/robots/robots.txt\r\nUser-agent: *\r\nDisallow: /\r\nEOF\r\n```\r\n\r\nAdd this to your httpd.conf:\r\n\r\n```\r\nlocation \"/robots.txt\" {\r\n root \"/robots\"\r\n}\r\n```","created":"admin 2017-04-22 13:14:30","updated":"admin 2017-04-22 13:23:49","public":true},{"title":"mail","link_title":"mail","md":"# OpenBSD Mail Server\r\n\r\n## Intro\r\n\r\nThis is how to setup an OpenBSD Mail Server with:\r\n\r\n* OpenSMTPD (mta)\r\n* Spamd\r\n* ClamAV (in and out scanning)\r\n* Zarafa (webapp, z-push) not included are IMAP, POP3, CalDav. Maybe they will be added later.\r\n\r\n\r\nNote: This is mostly a copy/past guide. It is still recommended to read the man-pages to 'understand' why and how things happen.\r\n\r\n## Mail-Flow\r\n\r\n### Incoming mail\r\n\r\n```\r\npf -\u003e relay to spamd -\u003e send to opensmtpd on lo0 -\u003e relay untagged mail to clamsmtpd on port 10025 -\u003e relay to clamd on port 3310 -\u003e\r\nreturn to clamsmtpd -\u003e return to opensmtpd on lo0 port 10026 and tag it CLAM_IN -\u003e deliver to mda (zarafa-dagent)\r\n```\r\n\r\n### Outgoing mail\r\n\r\n```\r\nzarafa (zarafa-spooler) -\u003e opensmtpd on lo0 -\u003e relay untagged mail to clamsmtpd on port 10027 -\u003e relay to clamd on port 3310 -\u003e\r\nreturn to clamsmtpd -\u003e return to opensmtpd on lo0 port 10028 and tag it CLAM_OUT -\u003e relay out\r\n```\r\n\r\n## smtpd\r\n\r\nBoot startup: \r\n\r\n```\r\nrcctl enable smtpd\r\nrcctl set smtpd flags -v\r\n```\r\n\r\n### Virtual Domains\r\n\r\nThis file holds all virtual domains:\r\n\r\n```\r\ncat /etc/mail/vdomains\r\nexample.com\r\nexample.org\r\n```\r\n\r\n### Virtual Users\r\n\r\nThis file holds all virtual users:\r\n\r\n```\r\ncat /etc/mail/vusers\r\njohn@example.com john\r\n@example.com john@example.com\r\ndoe@example.org doe\r\nhostmaster@example.org doe@example.org\r\n```\r\n\r\nThis file holds all user mappings:\r\n\r\n```\r\ncat /etc/mail/umap\r\njohn 1000:100:/var/virtual\r\ndoe 1000:100:/var/virtual\r\n```\r\n\r\n### smtpd.conf\r\n\r\nReplace `example.com` with your domain.\r\n\r\n```\r\nmax-message-size 20MB\r\npki mail.example.com certificate \"/etc/ssl/acme/example.com/fullchain.pem\"\r\npki mail.example.com key \"/etc/ssl/acme/private/example.com/privkey.pem\"\r\nciphers ECDHE-RSA-CHACHA20-POLY1305:CHACHA20:ECDHE+AESGCM:ECDHE+AES\r\n\r\nlisten on lo0\r\nlisten on lo0 port 10026 tag CLAM_IN \r\nlisten on lo0 port 10028 tag CLAM_OUT \r\nlisten on egress tls pki mail.example.com auth-optional\r\n\r\ntable aliases file:/etc/mail/aliases\r\ntable vdomains file:/etc/mail/vdomains\r\ntable vusers file:/etc/mail/vusers\r\ntable umap file:/etc/mail/umap\r\n\r\naccept for local alias \u003caliases\u003e deliver to mbox\r\n\r\naccept tagged CLAM_IN for domain \u003cvdomains\u003e virtual \u003cvusers\u003e userbase \u003cumap\u003e \\\r\n deliver to mda \"/usr/local/sbin/zarafa-dagent %{dest}\" as \"_zarafa\"\r\naccept tagged CLAM_OUT for any relay\r\n\r\naccept from any for domain \u003cvdomains\u003e relay via smtp://127.0.0.1:10025 \r\naccept from local for any relay via smtp://127.0.0.1:10027\r\n```\r\n\r\nRestart smtpd: `rcctl restart smtpd`\r\n\r\n## spamd\r\n\r\nCreate custom spam files:\r\n\r\n```\r\ntouch /etc/mail/nospamd\r\ntouch /etc/mail/spamd.black\r\n```\r\n\r\nConfig:\r\n\r\n```\r\nall:\\\r\n :nixspam:custom_black:\r\n\r\nnixspam:\\\r\n :black:\\\r\n :msg=\"Your address %A is in the nixspam list\\n\\\r\n See http://www.heise.de/ix/nixspam/dnsbl_en/ for details\":\\\r\n :method=http:\\\r\n :file=www.openbsd.org/spamd/nixspam.gz\r\n\r\ncustom_black:\\\r\n :black:\\\r\n :msg=\"Your address %A has been manually blacklisted\":\\\r\n :method=file:\\\r\n :file=/etc/mail/custom.black:\r\n```\r\n\r\nBoot startup: \r\n\r\nReplace `example.com` with your domain.\r\n\r\n```\r\nrcctl enable spamd\r\nrcctl set spamd flags \"-v -G 10:4:864 -K /etc/ssl/acme/private/example.com/privkey.pem -C /etc/ssl/acme/example.com/fullchain.pem\"\r\nrcctl enable spamlogd\r\n```\r\n\r\nSince we use 10 minutes greylisting `-G 10:` reload the blacklist every 5 minutes.\r\n\r\nCreate `spamd-setup` cronjob: `echo \"5 * * * * /usr/libexec/spamd-setup -b \u003e/dev/null 2\u003e\u00261\" \u003e\u003e /var/cron/tabs/root`\r\n\r\n### pf Rules\r\n\r\n```\r\ncat \u003c\u003cEOF\u003e\u003e /etc/pf.conf\r\nset limit table-entries 400000\r\n\r\ntable \u003cspamd\u003e persist\r\ntable \u003cnospamd\u003e persist file \"/etc/mail/nospamd\"\r\ntable \u003cspamd-white\u003e persist\r\n\r\npass in quick log on egress proto tcp \\\r\n from { \u003cnospamd\u003e, \u003cspamd-white\u003e } \\\r\n to any port smtp \\\r\n flags S/SA modulate state\r\n\r\npass in quick proto tcp from \u003cspamd\u003e \\\r\n to egress port { smtp, submission, smtps } \\\r\n rdr-to 127.0.0.1 port spamd\r\n\r\npass in quick proto tcp from any \\\r\n to egress port smtp \\\r\n rdr-to 127.0.0.1 port spamd\r\n\r\npass out log on egress proto tcp to any port smtp\r\nEOF\r\npfctl -f /etc/pf.conf\r\n```\r\n\r\nStart spamd and spamlogd:\r\n\r\n```\r\nrcctl start spamd\r\nrcctl start spamlogd\r\n```\r\n\r\n## ClamAV\r\n\r\nInstall: `pkg_add clamav clamsmtp`\r\n\r\nBoot startup:\r\n\r\n```\r\nrcctl enable clamd\r\nrcctl enable clamsmtpd_in\r\nrcctl enable clamsmtpd_out\r\n```\r\n\r\n### Config\r\n\r\n#### clamd\r\n\r\n```\r\n/etc/clamd.conf\r\n\r\nComment out the 'Example' string:\r\n\r\n#Example\r\n\r\nUncomment these options:\r\n\r\nTCPSocket 3310\r\nTCPAddr 127.0.0.1\r\n```\r\n\r\n#### freshclam\r\n\r\n```\r\n/etc/freshclam.conf\r\n\r\nComment out the 'Example' string:\r\n\r\n#Example\r\n\r\nChange\r\n#DatabaseMirror db.XY.clamav.net\r\nto\r\nDatabaseMirror db.de.clamav.net\r\n```\r\n\r\nCreate the update cronjob:\r\n\r\n`echo \"20 * * * * /usr/local/bin/freshclam \u003e/dev/null 2\u003e\u00261\" \u003e\u003e /var/cron/tabs/root`\r\n\r\n#### clamsmtpd\r\n\r\nCreate two clamsmtpd configs:\r\n\r\n```\r\ncp /etc/clamsmtpd.conf /etc/clamsmtpd-in.conf\r\ncp /etc/clamsmtpd.conf /etc/clamsmtpd-out.conf\r\n```\r\n\r\n```\r\n/etc/clamsmtpd-in.conf\r\nChange to match these values:\r\nOutAddress: 10026\r\nListen: 127.0.0.1:10025\r\nUser: _clamav\r\n\r\nAdd this option:\r\nClamAddress: 127.0.0.1:3310\r\n```\r\n\r\n```\r\n/etc/clamsmtpd-out.conf\r\nChange to match these values:\r\nOutAddress: 10028\r\nListen: 127.0.0.1:10027\r\nUser: _clamav\r\n\r\nAdd this option:\r\nClamAddress: 127.0.0.1:3310\r\n```\r\n\r\nCreate the two init scripts:\r\n\r\n```\r\ncat \u003c\u003cEOF\u003e /etc/rc.d/clamsmtpd_in\r\n#!/bin/sh\r\n\r\ndaemon=\"/usr/local/sbin/clamsmtpd -f /etc/clamsmtpd-in.conf\"\r\n\r\n. /etc/rc.d/rc.subr\r\n\r\nrc_cmd $1\r\nEOF\r\n```\r\n\r\n```\r\ncat \u003c\u003cEOF\u003e /etc/rc.d/clamsmtpd_out\r\n#!/bin/sh\r\n\r\ndaemon=\"/usr/local/sbin/clamsmtpd -f /etc/clamsmtpd-out.conf\"\r\n\r\n. /etc/rc.d/rc.subr\r\n\r\nrc_cmd $1\r\nEOF\r\n```\r\n\r\n`chmod 0755 /etc/rc.d/clamsmtpd_{in,out}`\r\n\r\nRun the initial freshclam: `/usr/local/bin/freshclam`\r\n\r\nStart the daemons:\r\n\r\n```\r\nrcctl start clamd\r\nrcctl start clamsmtpd_in\r\nrcctl start clamsmtpd_out\r\n```\r\n\r\n## MariaDB\r\n\r\nInstall: `pkg_add mariadb-server`\r\n\r\nBoot startup: `rcctl enable mysqld`\r\n\r\n### Init mysql\r\n\r\n```\r\nmysql_install_db\r\nrcctl start mysqld\r\nmysql_secure_installation\r\n```\r\n\r\n### Config\r\n\r\n```\r\n/etc/my.cnf\r\n\r\nComment out these options:\r\n[client]\r\nport = 3306\r\n\r\n[mysqld]\r\nport = 3306\r\nbind-address = 127.0.0.1\r\n```\r\n\r\nRestart mysqld: `rcctl restart mysqld`\r\n\r\n### Database\r\n\r\n```\r\nmysql -u root -p\r\n\r\nCREATE DATABASE zarafa;\r\nGRANT ALL ON zarafa.* TO 'zarafa'@'localhost' IDENTIFIED BY '\u003cpassword\u003e';\r\nFLUSH PRIVILEGES;\r\n```\r\n\r\n## Zarafa\r\n\r\nInstall: `pkg_add zarafa`\r\n\r\nBoot startup: `rcctl enable zarafa`\r\n\r\n### Config\r\n\r\n```\r\n/etc/rc.d/zarafa\r\n\r\nChange\r\n_pkg_scripts=\"zarafa_server zarafa_spooler zarafa_monitor zarafa_gateway zarafa_ical\"\r\nto\r\n_pkg_scripts=\"zarafa_server zarafa_spooler zarafa_monitor\"\r\n```\r\n\r\nAdd this after the `rc_pre()` function in `/etc/rc.d/zarafa_server`:\r\n\r\n```\r\nsock=\"/var/run/zarafad/server.sock\"\r\nnew=\"/var/www/run/server.sock\"\r\n\r\nrc_start() {\r\n ${rcexec} \"${daemon} ${daemon_flags} ${_bg}\"\r\n exc=$?\r\n sleep 2\r\n ln $sock $new\r\n chgrp www $new\r\n return $exc\r\n}\r\n\r\nrc_post() {\r\n rm -f $new\r\n}\r\n```\r\n\r\n```\r\n/etc/zarafa/server.cfg\r\n\r\nChange\r\nserver_tcp_enabled = yes\r\nto\r\nserver_tcp_enabled = no\r\n\r\nSet mysql credentials\r\nmysql_user = zarafa\r\nmysql_password = \u003cpassword\u003e\r\nmysql_socket = /var/run/mysql/mysql.sock\r\n```\r\n\r\nAdd _zarafa user to the user mapping:\r\n\r\n```\r\nawk -F':' '/_zarafa/{print $1\" \"$3\":\"$4\":\"$6}' /etc/passwd \u003e\u003e /etc/mail/umap\r\n```\r\n\r\nRestart smtpd: `rcctl restart smtpd`\r\n\r\nStart zarafa: `rcctl start zarafa`\r\n\r\nCreate a user: `zarafa-admin -c \"john@example.com\" -f \"John Doe\" -e \"john@example.com\" -P`\r\n\r\n## Zarafa Webapp\r\n\r\nInstall: `pkg_add zarafa-webapp`\r\n\r\nBoot startup: `rcctl enable php56_fpm`\r\n\r\n### Config\r\n\r\n`echo \"extension=mapi.so\" \u003e /etc/php-5.6/zarafa.ini`\r\n\r\n```\r\n/etc/php-5.6.ini\r\n\r\nChange\r\nexpose_php = On\r\nto\r\nexpose_php = off\r\n```\r\n\r\n```\r\n/var/www/zarafa-webapp/config.php\r\n\r\nComment out this if/else block and insert the single DEFAULT_SERVER define.\r\nIt should look like this:\r\n\r\n /*if(version_compare(phpversion('mapi'), '7.2.2', '\u003e=')) {\r\n define(\"DEFAULT_SERVER\", \"default:\");\r\n } else {\r\n #define(\"DEFAULT_SERVER\",\"http://localhost:236/zarafa\");\r\n define(\"DEFAULT_SERVER\",\"file:///var/run/zarafa\");\r\n }*/\r\n define(\"DEFAULT_SERVER\", \"file:///run/server.sock\");\r\n```\r\n\r\nStart php-fpm: `rcctl start php56_fpm`\r\n\r\n## Z-Push\r\n\r\nInstall: `pkg_add z-push`\r\n\r\n### Config\r\n\r\nCreate directories:\r\n\r\n```\r\nmkdir -p /var/www/var/{db/z-push,log/z-push}\r\nchown www /var/www/var/{db/z-push,log/z-push}\r\n```\r\n\r\n```\r\n/var/www/z-push/config.php\r\n\r\nChange if needed\r\ndefine('USE_FULLEMAIL_FOR_LOGIN', false);\r\nto\r\ndefine('USE_FULLEMAIL_FOR_LOGIN', true);\r\n```\r\n\r\n```\r\n/var/www/z-push/backend/zarafa/config.php\r\n\r\nChange\r\ndefine('MAPI_SERVER', 'http://127.0.0.1:236/zarafa');\r\nto\r\ndefine('MAPI_SERVER', 'file:///run/server.sock');\r\n```\r\n\r\n## httpd\r\n\r\nBasic config at [OpenBSD httpd](/wiki/OpenBSD+httpd)\r\n\r\nZarafa config:\r\n\r\n```\r\n root \"/zarafa-webapp\"\r\n directory index \"index.php\"\r\n location \"*.php\" {\r\n fastcgi socket \"/run/php-fpm.sock\"\r\n }\r\n location \"/Microsoft-Server-ActiveSync\" {\r\n root \"/z-push/index.php\"\r\n fastcgi socket \"/run/php-fpm.sock\"\r\n }\r\n```\r\n\r\n# Optionals\r\n\r\n## OpenBGPD\r\n\r\nProject: [bgp-spamd](http://bgp-spamd.net/)\r\n\r\nBoot startup: `rcctl enable bgpd`\r\n\r\n### Config\r\n\r\nOpenBGPD config:\r\n\r\n```\r\ncat \u003c\u003cEOF\u003e /etc/bgpd.conf\r\nspamdAS=\"65066\"\r\n\r\nAS 65001\r\n# Mandatory, to not update the local routing table\r\nfib-update no\r\n\r\ngroup \"spamd-bgp\" {\r\n remote-as $spamdAS\r\n multihop 64\r\n\r\n # Do not send Route Server any information\r\n announce none\r\n\r\n # us.bgp-spamd.net\r\n neighbor 64.142.121.62\r\n\r\n # eu.bgp-spamd.net\r\n neighbor 217.31.80.170\r\n\r\n # IPv6 eu.bgp-spamd.net\r\n #neighbor 2a00:15a8:0:100:0:d91f:50aa:1\r\n}\r\n \r\n# 'match' is required, to remove entries when routes are withdrawn\r\nmatch from group \"spamd-bgp\" community $spamdAS:42 set pftable \"bgp_spamd_bypass\"\r\nmatch from group \"spamd-bgp\" community $spamdAS:666 set pftable \"bgp_spamd\"\r\nEOF\r\nchmod 0600 /etc/bgpd.conf\r\n```\r\n\r\nStart OpenBGPD: `rctl start pbgd`\r\n\r\nAdd these options to your `/etc/pf.conf`:\r\n\r\n```\r\nset limit table-entries 400000\r\n\r\ntable \u003cbgp_spamd_bypass\u003e persist\r\ntable \u003cbgp_spamd\u003e persist\r\n\r\nbgp_addrs = \"{ 64.142.121.62, 217.31.80.170, 2a00:15a8:0:100:0:d91f:50aa:1 }\"\r\npass in on egress proto tcp from $bgp_addrs to any port bgp\r\n```\r\n\r\nChange your smtp rules to match these:\r\n\r\n```\r\npass in quick log on egress proto tcp \\\r\n from { \u003cbgp_spamd_bypass\u003e, \u003cnospamd\u003e, \u003cspamd-white\u003e } \\\r\n to any port smtp \\\r\n flags S/SA modulate state\r\n\r\npass in quick proto tcp from { \u003cbgp_spamd\u003e, \u003cspamd\u003e } \\\r\n to egress port { smtp, submission, smtps } \\\r\n rdr-to 127.0.0.1 port spamd\r\n```\r\n\r\nReload pf: `pfctl -f /etc/pf.conf`\r\n\r\nEdit the `/etc/mail/spamd.conf`:\r\n\r\n```\r\n# Add bgp-spamd to the all list:\r\nall:\\\r\n :nixspam:custom_black:bgp-spamd:\r\n\r\n# Add the bgp-spamd list:\r\nbgp-spamd:\\\r\n :black:\\\r\n :msg=\"Your address %A has sent mail to a spamtrap\\n\\\r\n within the last 24 hours\":\\\r\n :method=file:\\\r\n :file=/etc/mail/spamd.black:\r\n```\r\n\r\nCreate the spamd-reload script:\r\n\r\n```\r\ncat \u003c\u003cEOF\u003e /usr/local/sbin/bgp-spamd.black.sh\r\n#!/bin/sh\r\n\r\nblack=\"/etc/mail/spamd.black\"\r\n\r\nbgpctl show rib community 65066:666 | tail -n +5 |\r\n awk '{sub(/\\/.*/, \"\", $1);print $1}' |\r\n uniq \u003e ${black}\r\n#sed -e '1,4d' -e 's/\\/.*$//' -e 's/[ \\*\\\u003e]*//' \u003e ${black}\r\n\r\n/usr/libexec/spamd-setup -b\r\nEOF\r\nchmod 0744 /usr/local/sbin/bgp-spamd.black.sh\r\n```\r\n\r\nReplace the `spamd-setup` cronjob from above with the new script and run it once manually: `/usr/local/sbin/bgp-spamd.black.sh`\r\n\r\n## SpamAssassin\r\n\r\nInstall: `pkg_add p5-Mail-SpamAssassin spampd`\r\n\r\nBoot startup:\r\n\r\n```\r\nrcctl enable spampd\r\nrcctl enable spamassassin\r\nrcctl set spampd flags \"--port=10029 --relayhost=127.0.0.1:10030 --tagall --aw\"\r\n```\r\n\r\n### Config\r\n\r\n```\r\n/etc/mail/spamassassin/local.cf\r\n\r\nUncomment this option\r\nrewrite_header\r\n```\r\n\r\nChange your `smtpd.conf` to match this:\r\n\r\n```\r\n/etc/mail/smtpd.conf\r\n\r\nlisten on lo0 port 10030 tag SPAM_IN\r\n\r\naccept tagged SPAM_IN for domain \u003cvdomains\u003e virtual \u003cvusers\u003e userbase \u003cumap\u003e \\\r\n deliver to mda \"/usr/local/sbin/zarafa-dagent %{dest}\" as \"_zarafa\"\r\naccept tagged CLAM_IN for any relay via smtp://127.0.0.1:10029\r\naccept tagged CLAM_OUT for any relay\r\n```\r\n\r\nStart the daemons:\r\n\r\n```\r\nrcctl start spampd\r\nrcctl start spamassassin\r\n```\r\n\r\nRestart OpenSMTPD: `rcctl restart smtpd`\r\n\r\n## Backup\r\n\r\n```\r\n#!/bin/sh\r\n\r\ndb=\"zarafa\"\r\ndb_user=\"zarafa\"\r\ndb_pw=\"\u003cpassword\u003e\"\r\n\r\nset -A paths \\\r\n/var/db/zarafa \\\r\n/etc/zarafa \\\r\n/etc/mail \\\r\n/etc/pf.conf \\\r\n/etc/httpd.conf \\\r\n/etc/clamsmtpd-in.conf \\\r\n/etc/clamsmtpd-out.conf \\\r\n/etc/clamd.conf \\\r\n/etc/freshclam.conf \\\r\n/etc/ssl/acme \\\r\n/etc/acme \\\r\n/etc/bgpd.conf \\\r\n/etc/rc.conf.local \\\r\n/etc/rc.d/clamsmtpd_in \\\r\n/etc/rc.d/clamsmtpd_out \\\r\n/etc/my.cnf \\\r\n/etc/rc.d/zarafa \\\r\n/etc/rc.d/zarafa_server \\\r\n/etc/php-5.6/zarafa.ini \\\r\n/etc/php-5.6.ini \\\r\n/var/www/zarafa-webapp/config.php \\\r\n/var/www/z-push/config.php \\\r\n/var/www/z-push/backend/zarafa/config.php \\\r\n/usr/local/sbin/bgp-spamd.black.sh \\\r\n/usr/local/sbin/cron-acme-update \\\r\n/etc/mail/spamassassin/local.cf \\\r\n/var/cron/tabs/root \\\r\n/tmp/${db}.sql\r\n\r\nmysqldump -u ${db_user} -p${db_pw} ${db} \u003e/tmp/${db}.sql\r\n\r\ntar cfvz /root/mailserver_backup_$(date +%Y%m%d_%H%M%S).tgz ${paths[@]}\r\n\r\nrm /tmp/${db}.sql\r\n```\r\n\r\n# END","created":"admin 2017-04-22 13:55:37","updated":"","public":true},{"title":"sdcsdcs","link_title":"sdcsdcs","md":"# Bla\r\n\r\n## HTTP\r\n\r\n### Config\r\n\r\nBla\r\n\r\n## HTTPS\r\n\r\n### Config\r\n\r\nBlubb","created":"admin 2017-04-22 13:45:27","updated":"admin 2017-04-22 13:54:42","public":true}],"snippets":null,"users":[{"Username":"admin","Password":"$2a$13$M5UIpGUG4WmEItvd7c6Vi.x1aOQ4U3wz1SOScrXT0C3dUy8eNiu7m","Admin":true,"Locked":0}]}
diff --git a/editor.go b/editor.go
new file mode 100644
index 0000000..24ba7da
--- /dev/null
+++ b/editor.go
@@ -0,0 +1,43 @@
+package main
+
+import "sync"
+
+type Editor struct {
+ Username string
+ Time string
+}
+
+type EditorStore struct {
+ sync.RWMutex
+ m map[string]*Editor
+}
+
+func NewEditorStore() *EditorStore {
+ return &EditorStore{m: make(map[string]*Editor)}
+}
+
+func (e *EditorStore) Set(page, username string) *Editor {
+ e.Lock()
+ defer e.Unlock()
+ editor := e.m[page]
+ if editor != nil {
+ return editor
+ }
+ e.m[page] = &Editor{username, now()}
+ return nil
+}
+
+func (e *EditorStore) RemoveEditor(page, username string) {
+ e.Lock()
+ defer e.Unlock()
+ if editor, ok := e.m[page]; ok {
+ if editor.Username == username {
+ delete(e.m, page)
+ }
+ }
+}
+func (e *EditorStore) Remove(page string) {
+ e.Lock()
+ defer e.Unlock()
+ delete(e.m, page)
+}
diff --git a/engine.go b/engine.go
deleted file mode 100644
index 0e50af9..0000000
--- a/engine.go
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright (C) 2017 Marius Schellenberger
-
-package main
-
-import (
- "errors"
- "html/template"
- "regexp"
- "time"
-
- "github.com/blevesearch/bleve"
- "github.com/blevesearch/bleve/document"
-)
-
-const (
- timeFmt = "2006-01-02 15:04:05"
- maxAllResult = 1000000
- maxSearchResult = 101
-)
-
-var titleRe = regexp.MustCompile("[^a-zA-Z0-9+ ]+")
-
-func now() string {
- return time.Now().Format(timeFmt)
-}
-
-type Article struct {
- Title string `json:"title"`
- LinkTitle string
- Search string `json:"search"`
- Index template.HTML `json:"index"`
- Text template.HTML `json:"text"`
- MD string `json:"md"`
- Created string `json:"created"`
- Updated string `json:"updated"`
- Public bool `json:"public"`
-}
-
-type Engine struct {
- index bleve.Index
-}
-
-func NewEngine(index bleve.Index) *Engine {
- return &Engine{index}
-}
-
-func (e *Engine) Close() error {
- return e.index.Close()
-}
-
-func (e *Engine) Restore(dumpArt DumpArticle) error {
- art := render(dumpArt.MD)
- art.Title = dumpArt.Title
- art.Created = dumpArt.Created
- art.Updated = dumpArt.Updated
- art.Public = dumpArt.Public
- return e.index.Index(dumpArt.LinkTitle, art)
-}
-
-func (e *Engine) Index(title, text, user string, public bool) (string, error) {
- if user == "" {
- return "", errors.New("engine: user cannot be empty")
- }
- title = titleRe.ReplaceAllString(title, "")
- newTitle := makeLinkTitle(title)
- if err := e.Exists(false, newTitle); err == nil {
- return "", errors.New("engine: article '" + title + "' already exists")
- }
- art := render(text)
- art.Title = title
- art.Created = user + " " + now()
- art.Public = public
- err := e.index.Index(newTitle, art)
- return newTitle, err
-}
-
-func (e *Engine) Exists(public bool, title string) error {
- _, err := e.Get(public, title)
- if err != nil {
- return err
- }
- return nil
-}
-
-func (e *Engine) Delete(title string) error {
- if title == indexName {
- return errors.New("engine: page '" + indexName + "' cannot be deleted")
- }
- if err := e.Exists(false, title); err != nil {
- return err
- }
- return e.index.Delete(title)
-}
-
-func (e *Engine) Update(title, text, user string, public bool) error {
- if user == "" {
- return errors.New("engine: user cannot be empty")
- }
- if title == indexName && !public {
- return errors.New("engine: page '" + indexName + "' must stay public")
- }
- if err := e.Exists(false, title); err != nil {
- return err
- }
- art, err := e.Get(false, title)
- if err != nil {
- return err
- }
- rend := render(text)
- art.Updated = user + " " + now()
- art.Search = rend.Search
- art.Index = rend.Index
- art.Text = rend.Text
- art.MD = rend.MD
- art.Public = public
- return e.index.Index(title, art)
-}
-
-func (e *Engine) Get(public bool, title string) (Article, error) {
- var art Article
- doc, err := e.index.Document(title)
- if err != nil {
- return art, err
- }
- if doc == nil || doc.Fields == nil {
- return art, errors.New("engine: article '" + title + "' does not esist")
- }
- for _, field := range doc.Fields {
- switch f := field.(type) {
- case *document.TextField:
- switch field.Name() {
- case "title":
- art.Title = string(f.Value())
- case "index":
- art.Index = template.HTML(string(f.Value()))
- case "text":
- art.Text = template.HTML(string(f.Value()))
- case "md":
- art.MD = string(f.Value())
- case "created":
- art.Created = string(f.Value())
- case "updated":
- art.Updated = string(f.Value())
- }
- case *document.BooleanField:
- if field.Name() == "public" {
- p, err := f.Boolean()
- if err != nil {
- return art, err
- }
- art.Public = p
- }
- }
- }
- art.LinkTitle = title
- if public && !art.Public {
- err = errors.New("engine: article '" + title + "' does not esist")
- }
- return art, err
-}
-
-func (e *Engine) Search(public bool, search string) (string, error) {
- var (
- res *bleve.SearchResult
- req *bleve.SearchRequest
- err error
- )
- if public {
- req = bleve.NewSearchRequest(bleve.NewConjunctionQuery([]bleve.Query{
- bleve.NewBoolFieldQuery(true).SetField("public"),
- bleve.NewQueryStringQuery("title:" + search + " search:" + search),
- }))
- } else {
- req = bleve.NewSearchRequest(bleve.NewQueryStringQuery("title:" + search + " search:" + search))
- }
- req.Highlight = bleve.NewHighlightWithStyle("html")
- req.Size = maxSearchResult
- res, err = e.index.Search(req)
- if err != nil {
- return "", err
- }
- if res.Total == 0 {
- return "No search results", nil
- } else if res.Total == maxSearchResult {
- return "", errors.New("engine: too many search results")
- }
- html := ""
- for _, hit := range res.Hits {
- if hit == nil {
- continue
- }
- art, err := e.Get(false, hit.ID)
- if err != nil {
- continue
- }
- html += `` + art.Title + ""
- for fragField, frags := range hit.Fragments {
- if fragField == "title" || fragField == "search" {
- html += ``
- for _, f := range frags {
- html += f
- }
- html += ""
- break
- }
- }
- html += "
"
- }
- return html, nil
-}
-
-type Title struct {
- Title string
- LinkTitle string
-}
-
-func (e *Engine) GetAll(public bool) (t []Title, err error) {
- var (
- res *bleve.SearchResult
- req *bleve.SearchRequest
- )
- if public {
- req = bleve.NewSearchRequest(bleve.NewBoolFieldQuery(true).SetField("public"))
- } else {
- req = bleve.NewSearchRequest(bleve.NewMatchAllQuery())
- }
- req.Size = maxAllResult
- res, err = e.index.Search(req)
- if err != nil {
- return
- }
- for _, v := range res.Hits {
- if v == nil {
- continue
- }
- if v.ID == indexName {
- continue
- }
- doc, err := e.index.Document(v.ID)
- if err != nil || doc == nil || doc.Fields == nil {
- continue
- }
- for _, field := range doc.Fields {
- if field.Name() == "title" {
- f, ok := field.(*document.TextField)
- if ok {
- t = append(t, Title{string(f.Value()), v.ID})
- break
- }
- }
- }
- }
- return
-}
-
-func (e *Engine) Dump() (titles []string, err error) {
- req := bleve.NewSearchRequest(bleve.NewMatchAllQuery())
- req.Size = maxAllResult
- res, err := e.index.Search(req)
- if err != nil {
- return
- }
- for _, v := range res.Hits {
- if v == nil {
- continue
- }
- titles = append(titles, v.ID)
- }
- return
-}
diff --git a/handler.go b/handler.go
index 5165955..d7e6f6a 100644
--- a/handler.go
+++ b/handler.go
@@ -4,39 +4,16 @@ package main
import (
"html/template"
- "net"
"net/http"
- "regexp"
"time"
)
-func webserver(l net.Listener) {
- cookieStore()
- loadTemplates()
- srv := &http.Server{
- Handler: buildRoutes(),
- }
- log.Fatal("webserver: ", srv.Serve(l))
+type notFoundHandler struct {
+ s *HTTPServer
}
-type notFoundHandler struct{}
-
-func (notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- f := func(ctx *Context) {
- ctx.Template("notFoundHandler")
- ctx.Data = webData{
- Title: "404",
- }
- ctx.Exec()
- }
- w.WriteHeader(http.StatusNotFound)
- f(newContext(w, r))
-}
-
-func contextWrapper(h ctxHandler) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- h(newContext(w, r))
- }
+func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ newContext(w, r, nf.s).NotFound()
}
func adminAuthHandler(h ctxHandler) ctxHandler {
@@ -45,7 +22,7 @@ func adminAuthHandler(h ctxHandler) ctxHandler {
h(ctx)
return
}
- ctx.Redirect("/404", 302)
+ ctx.NotFound()
}
}
@@ -59,7 +36,17 @@ func userAuthHandler(h ctxHandler) ctxHandler {
h(ctx)
return
}
- ctx.Redirect("/404", 302)
+ ctx.NotFound()
+ }
+}
+
+func authHandler(h ctxHandler) ctxHandler {
+ return func(ctx *Context) {
+ if ctx.LoggedOn() {
+ h(ctx)
+ return
+ }
+ ctx.NotFound()
}
}
@@ -69,9 +56,9 @@ func staticHandler(ctx *Context) {
ctx.SetHeader("Expires", time.Now().Add(time.Hour*12).Format(http.TimeFormat))
switch ctx.Path() {
case "/bootstrap.css":
- err = ctx.Write(res["bootstrap.css"])
+ err = ctx.Write(ctx.Srv.res["bootstrap.css"])
case "/custom.css":
- err = ctx.Write(res["custom.css"])
+ err = ctx.Write(ctx.Srv.res["custom.css"])
}
if err != nil {
log.Println("static:", err)
@@ -79,7 +66,6 @@ func staticHandler(ctx *Context) {
}
func indexHandler(ctx *Context) {
- ctx.InitToken()
if !ctx.LoggedOn() {
ctx.UnsetUdata()
}
@@ -87,7 +73,6 @@ func indexHandler(ctx *Context) {
}
func loginHandler(ctx *Context) {
- ctx.InitToken()
if ctx.LoggedOn() {
ctx.Redirect("/wiki/Index", 302)
return
@@ -99,38 +84,31 @@ func loginHandler(ctx *Context) {
}
switch ctx.Method() {
case "GET":
- ctx.NewToken()
ctx.Exec()
case "POST":
user := ctx.Form("user")
password := ctx.Form("password")
submit := ctx.Form("submit")
- if !ctx.CheckToken() {
- ctx.NewToken()
- ctx.Data.Msg = "wrong csrf token"
- ctx.Exec()
+ if !ctx.CheckToken(nil) {
return
}
if user == "" || password == "" || submit != "Login" {
- ctx.NewToken()
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
}
- err := db.Login(user, password)
+ err := ctx.Srv.BS.Login(user, password)
if err != nil {
- ctx.NewToken()
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
- ctx.SetUdata(user, db.UserIsAdmin(user))
+ ctx.SetUdata(user, ctx.Srv.BS.UserIsAdmin(user))
ctx.Redirect("/wiki/Index", 302)
}
}
func searchHandler(ctx *Context) {
- ctx.InitToken()
ctx.Template("searchHandler")
ctx.Data = webData{
Title: "Search",
@@ -138,34 +116,27 @@ func searchHandler(ctx *Context) {
}
switch ctx.Method() {
case "GET":
- ctx.NewToken()
ctx.Exec()
case "POST":
search := ctx.Form("search")
submit := ctx.Form("submit")
- if !ctx.CheckToken() {
- ctx.NewToken()
- ctx.Data.Msg = "wrong csrf token"
- ctx.Exec()
+ if !ctx.CheckToken(nil) {
return
}
- if search == "" || submit != "Search" {
- ctx.NewToken()
+ if submit != "Search" {
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
}
- res, err := engine.Search(!ctx.LoggedOn(), search)
+ res, err := ctx.Srv.Index.Search(!ctx.LoggedOn(), search)
if err != nil {
- ctx.NewToken()
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
ctx.Data.Search = search
ctx.Data.Data = template.HTML(res)
- ctx.NewToken()
ctx.Exec()
}
}
@@ -177,7 +148,7 @@ func allHandler(ctx *Context) {
BodyTitle: "All Articles",
Admin: ctx.GetAdmin(),
}
- all, err := engine.GetAll(!ctx.LoggedOn())
+ all, err := ctx.Srv.Index.GetAll(!ctx.LoggedOn())
if err != nil {
ctx.Data.Msg = err.Error()
}
@@ -187,14 +158,9 @@ func allHandler(ctx *Context) {
func wikiNewHandler(ctx *Context) {
ctx.Template("wikiNewHandler")
- ctx.Data = webData{
- Title: "New Article",
- BodyTitle: "New Article",
- Admin: ctx.GetAdmin(),
- }
+ ctx.Data = webData{Title: "New Article"}
switch ctx.Method() {
case "GET":
- ctx.NewToken()
ctx.Exec()
case "POST":
title := ctx.Form("title")
@@ -210,28 +176,7 @@ func wikiNewHandler(ctx *Context) {
MD: text,
}
ctx.Data.Data = art
- if !ctx.CheckToken() {
- ctx.NewToken()
- ctx.Data.Msg = "wrong csrf token"
- ctx.Exec()
- return
- }
- ctx.NewToken()
- m, err := regexp.MatchString("[^a-zA-Z0-9 _-]", title)
- if err != nil {
- ctx.Data.Msg = err.Error()
- ctx.Exec()
- return
- }
- if m {
- ctx.Data.Msg = "title contains forbidden characters, allowed are [a-zA-Z0-9 _-]"
- ctx.Exec()
- return
- }
- err = engine.Exists(!ctx.LoggedOn(), title)
- if err == nil {
- ctx.Data.Msg = "article alerady exsist"
- ctx.Exec()
+ if !ctx.CheckToken(nil) {
return
}
if submit != "Submit" {
@@ -239,7 +184,7 @@ func wikiNewHandler(ctx *Context) {
ctx.Exec()
return
}
- newTitle, err := engine.Index(title, text, ctx.GetUser(), pub)
+ newTitle, err := ctx.Srv.Index.Index(title, text, ctx.GetUser(), pub)
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
@@ -256,11 +201,10 @@ func wikiHandler(ctx *Context) {
ctx.Data = webData{
Admin: ctx.GetAdmin(),
}
- ctx.NewToken()
title := ctx.Var("title")
- art, err := engine.Get(!ctx.LoggedOn(), title)
+ art, err := ctx.Srv.Index.Get(!ctx.LoggedOn(), title)
if err != nil {
- ctx.Redirect("/404", 302)
+ ctx.NotFound()
return
}
art.LinkTitle = title
@@ -283,16 +227,16 @@ func wikiEditHandler(ctx *Context) {
title := ctx.Var("title")
switch ctx.Method() {
case "GET":
- ctx.NewToken()
- art, err := engine.Get(!ctx.LoggedOn(), title)
+ art, err := ctx.Srv.Index.Get(!ctx.LoggedOn(), title)
if err != nil {
- ctx.Redirect("/404", 302)
+ ctx.NotFound()
return
}
art.LinkTitle = title
ctx.Data.Data = art
ctx.Data.Title = art.Title
ctx.Data.BodyTitle = art.Title
+ ctx.Data.Editor = ctx.Srv.Editors.Set(title, ctx.GetUser())
ctx.Exec()
case "POST":
ftitle := ctx.Form("title")
@@ -311,24 +255,26 @@ func wikiEditHandler(ctx *Context) {
}
ctx.Data.Title = ftitle
ctx.Data.BodyTitle = ftitle
- if !ctx.CheckToken() {
- ctx.NewToken()
- ctx.Data.Msg = "wrong csrf token"
- ctx.Exec()
+ if !ctx.CheckToken(nil) {
+ return
+ }
+ if submit == "Back" {
+ ctx.Srv.Editors.RemoveEditor(title, ctx.GetUser())
+ ctx.Redirect("/wiki/"+title, 302)
return
}
- ctx.NewToken()
if submit != "Submit" {
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
}
- err := engine.Update(title, text, ctx.GetUser(), pub)
+ err := ctx.Srv.Index.Update(title, text, ctx.GetUser(), pub)
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
+ ctx.Srv.Editors.Remove(title)
ctx.Redirect("/wiki/"+title, 302)
}
}
@@ -336,24 +282,26 @@ func wikiEditHandler(ctx *Context) {
func wikiDelHandler(ctx *Context) {
title := ctx.Var("title")
submit := ctx.Form("submit")
- if !ctx.CheckToken() {
+ f := func() {
ctx.Redirect("/wiki/"+title, 302)
+ }
+ if !ctx.CheckToken(f) {
return
}
if submit != "Delete" {
- ctx.Redirect("/wiki/"+title, 302)
+ f()
return
}
- if err := engine.Delete(title); err != nil {
+ if err := ctx.Srv.Index.Delete(title); err != nil {
log.Println("delete:", err)
- ctx.Redirect("/wiki/"+title, 302)
+ f()
}
ctx.Redirect("/wiki/Index", 302)
}
func usersHandler(ctx *Context) {
if !ctx.LoggedOn() {
- ctx.Redirect("/404", 302)
+ ctx.NotFound()
return
}
ctx.Template("usersHandler")
@@ -362,8 +310,7 @@ func usersHandler(ctx *Context) {
BodyTitle: "Users",
User: ctx.GetUser(),
}
- ctx.NewToken()
- us, err := db.GetUsers()
+ us, err := ctx.Srv.BS.GetUsers()
if err != nil {
ctx.Data.Msg = err.Error()
}
@@ -379,7 +326,6 @@ func userNewHandler(ctx *Context) {
}
switch ctx.Method() {
case "GET":
- ctx.NewToken()
ctx.Exec()
case "POST":
user := ctx.Form("user")
@@ -391,21 +337,15 @@ func userNewHandler(ctx *Context) {
adm = true
}
submit := ctx.Form("submit")
- if !ctx.CheckToken() {
- ctx.NewToken()
- ctx.Data.Msg = "wrong csrf token"
- ctx.Exec()
+ if !ctx.CheckToken(nil) {
return
}
if user == "" || password == "" || password != repeat || submit != "Submit" {
- ctx.NewToken()
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
}
- log.Println(user, password, adm)
- if err := db.CreateUser(user, password, adm); err != nil {
- ctx.NewToken()
+ if err := ctx.Srv.BS.CreateUser(user, password, adm); err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
@@ -422,14 +362,13 @@ func userEditHandler(ctx *Context) {
}
switch ctx.Method() {
case "GET":
- u, err := db.GetUserWithoutPassword(ctx.Var("user"))
+ u, err := ctx.Srv.BS.GetUserWithoutPassword(ctx.Var("user"))
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
ctx.Data.Data = u
- ctx.NewToken()
ctx.Exec()
case "POST":
user := ctx.Var("user")
@@ -441,29 +380,23 @@ func userEditHandler(ctx *Context) {
adm = true
}
submit := ctx.Form("submit")
- if !ctx.CheckToken() {
- ctx.NewToken()
- ctx.Data.Msg = "wrong csrf token"
- ctx.Exec()
+ if !ctx.CheckToken(nil) {
return
}
if user == "" || password != repeat || submit != "Submit" {
- ctx.NewToken()
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
}
if ctx.GetAdmin() {
- if err := db.AdminUpdateUser(user, password, adm); err != nil {
- ctx.NewToken()
+ if err := ctx.Srv.BS.AdminUpdateUser(user, password, adm); err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
} else {
if user == ctx.GetUser() {
- if err := db.UpdateUserPassword(user, password); err != nil {
- ctx.NewToken()
+ if err := ctx.Srv.BS.UpdateUserPassword(user, password); err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
@@ -478,7 +411,7 @@ func userEditHandler(ctx *Context) {
func userUnlockHandler(ctx *Context) {
user := ctx.Var("user")
- if err := db.UnlockUser(user); err != nil {
+ if err := ctx.Srv.BS.UnlockUser(user); err != nil {
log.Println("unlock:", err)
}
ctx.Redirect("/users", 302)
@@ -492,17 +425,16 @@ func userDelHandler(ctx *Context) {
}
switch ctx.Method() {
case "GET":
- user, err := db.GetUserWithoutPassword(ctx.Var("user"))
+ user, err := ctx.Srv.BS.GetUserWithoutPassword(ctx.Var("user"))
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
ctx.Data.Data = user
- ctx.NewToken()
ctx.Exec()
case "POST":
- if err := db.DeleteUser(ctx.Var("user")); err != nil {
+ if err := ctx.Srv.BS.DeleteUser(ctx.Var("user")); err != nil {
log.Println("delete:", err)
}
ctx.Redirect("/users", 302)
diff --git a/helper.go b/helper.go
new file mode 100644
index 0000000..c197ce8
--- /dev/null
+++ b/helper.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+ "fmt"
+ "strconv"
+ "time"
+)
+
+const timeFmt = "2006-01-02 15:04:05"
+
+func now() string {
+ return time.Now().Format(timeFmt)
+}
+
+func since(t time.Time) string {
+ s := time.Since(t)
+ u := uint64(s)
+ if u <= 0 {
+ return "0s"
+ }
+ str := s.String()
+ switch {
+ case u >= uint64(time.Second):
+ f, _ := strconv.ParseFloat(str[:len(str)-1], 64)
+ return fmt.Sprintf("%.fs", f)
+ case u < uint64(time.Microsecond):
+ f, _ := strconv.ParseFloat(str[:len(str)-2], 64)
+ return fmt.Sprintf("%.fns", f)
+ case u < uint64(time.Millisecond):
+ f, _ := strconv.ParseFloat(str[:len(str)-3], 64)
+ return fmt.Sprintf("%.fµs", f)
+ default:
+ f, _ := strconv.ParseFloat(str[:len(str)-2], 64)
+ return fmt.Sprintf("%.fms", f)
+ }
+}
diff --git a/index.go b/index.go
new file mode 100644
index 0000000..5eda052
--- /dev/null
+++ b/index.go
@@ -0,0 +1,328 @@
+// Copyright (C) 2017 Marius Schellenberger
+
+package main
+
+import (
+ "fmt"
+ "html/template"
+ "os"
+
+ "github.com/blevesearch/bleve"
+ "github.com/blevesearch/bleve/document"
+)
+
+const (
+ maxResult = 1e6
+ maxSearchResult = 101
+ noResults = "No search results"
+ indexName = "Index"
+ welcome = `# Welcome to GoWiki
+This is the [Index](/wiki/Index) page.
+You can customize it how you like.`
+)
+
+type Article struct {
+ Title string `json:"title"`
+ LinkTitle string `json:"-"`
+ Search string `json:"search"`
+ Index template.HTML `json:"index"`
+ Text template.HTML `json:"text"`
+ MD string `json:"md"`
+ Created string `json:"created"`
+ Updated string `json:"updated"`
+ Public bool `json:"public"`
+}
+
+func notExists(title string) error {
+ return indexErr("article '" + title + "' does not esist")
+}
+
+func indexErr(i interface{}) error {
+ return fmt.Errorf("index: %s", i)
+}
+
+type Index struct {
+ index bleve.Index
+ cache *IndexCache
+}
+
+func NewIndex(path string) (i *Index, err error) {
+ var bi bleve.Index
+ if _, err = os.Stat(path); os.IsNotExist(err) {
+ mapping := bleve.NewIndexMapping()
+ bi, err = bleve.New(path, mapping)
+ if err != nil {
+ err = indexErr(err)
+ return
+ }
+ i = &Index{index: bi}
+ _, err = i.Index(indexName, welcome, defUser, true)
+ if err != nil {
+ err = indexErr(err)
+ return
+ }
+ } else {
+ bi, err = bleve.Open(path)
+ if err != nil {
+ err = indexErr(err)
+ return
+ }
+ i = &Index{index: bi}
+ }
+ i.cache = NewIndexCache()
+ return
+}
+
+func NewDumpIndex(path string) (i *Index, err error) {
+ bi, err := bleve.OpenUsing(path, map[string]interface{}{"read_only": true})
+ if err != nil {
+ err = indexErr(err)
+ return
+ }
+ i = &Index{index: bi}
+ return
+}
+
+func (i *Index) Close() error {
+ return i.index.Close()
+}
+
+func (i *Index) Restore(dumpArt *DumpArticle) error {
+ art := render(dumpArt.MD).Article()
+ art.Title = dumpArt.Title
+ art.Created = dumpArt.Created
+ art.Updated = dumpArt.Updated
+ art.Public = dumpArt.Public
+ return i.index.Index(dumpArt.LinkTitle, art)
+}
+
+func (i *Index) Index(title, text, user string, public bool) (string, error) {
+ if user == "" {
+ return "", indexErr("user cannot be empty")
+ }
+ newTitle, title := makeLinkTitle(title)
+ if i.exists(false, newTitle) {
+ return "", indexErr("article '" + title + "' already exists")
+ }
+ art := render(text).Article()
+ art.Title = title
+ art.Created = user + " " + now()
+ art.Public = public
+ err := i.index.Index(newTitle, art)
+ i.cache.Add(newTitle, art)
+ return newTitle, err
+}
+
+func (i *Index) exists(public bool, title string) bool {
+ _, err := i.Get(public, title)
+ return err == nil
+}
+
+func (i *Index) Delete(title string) error {
+ if title == indexName {
+ return indexErr("page '" + indexName + "' cannot be deleted")
+ }
+ if !i.exists(false, title) {
+ return notExists(title)
+ }
+ err := i.index.Delete(title)
+ if err != nil {
+ return err
+ }
+ i.cache.Delete(title)
+ return err
+}
+
+func (i *Index) Update(title, text, user string, public bool) error {
+ if user == "" {
+ return indexErr("user cannot be empty")
+ }
+ if title == indexName && !public {
+ return indexErr("page '" + indexName + "' must stay public")
+ }
+ if !i.exists(false, title) {
+ return notExists(title)
+ }
+ art, err := i.Get(false, title)
+ if err != nil {
+ return err
+ }
+ rend := render(text).Article()
+ art.Updated = user + " " + now()
+ art.Search = rend.Search
+ art.Index = rend.Index
+ art.Text = rend.Text
+ art.MD = rend.MD
+ art.Public = public
+ err = i.index.Index(title, art)
+ if err != nil {
+ return err
+ }
+ i.cache.Add(title, &art)
+ return err
+}
+
+func (i *Index) Get(public bool, title string) (art Article, err error) {
+ a := i.cache.Get(title)
+ switch {
+ case a != nil:
+ art = *a
+ default:
+ var doc *document.Document
+ doc, err = i.index.Document(title)
+ if err != nil {
+ return
+ }
+ if doc == nil || doc.Fields == nil {
+ err = notExists(title)
+ return
+ }
+ for _, field := range doc.Fields {
+ switch f := field.(type) {
+ case *document.TextField:
+ switch field.Name() {
+ case "title":
+ art.Title = string(f.Value())
+ case "index":
+ art.Index = template.HTML(string(f.Value()))
+ case "text":
+ art.Text = template.HTML(string(f.Value()))
+ case "md":
+ art.MD = string(f.Value())
+ case "created":
+ art.Created = string(f.Value())
+ case "updated":
+ art.Updated = string(f.Value())
+ }
+ case *document.BooleanField:
+ if field.Name() == "public" {
+ p, e := f.Boolean()
+ if e != nil {
+ err = e
+ return
+ }
+ art.Public = p
+ }
+ }
+ }
+ }
+ art.LinkTitle = title
+ if public && !art.Public {
+ err = notExists(title)
+ }
+ return
+}
+
+func (i *Index) Search(public bool, search string) (html string, err error) {
+ if search == "" {
+ html = noResults
+ return
+ }
+ var (
+ res *bleve.SearchResult
+ req *bleve.SearchRequest
+ )
+ if public {
+ req = bleve.NewSearchRequest(bleve.NewConjunctionQuery([]bleve.Query{
+ bleve.NewBoolFieldQuery(true).SetField("public"),
+ bleve.NewQueryStringQuery("title:" + search + " search:" + search),
+ }))
+ } else {
+ req = bleve.NewSearchRequest(bleve.NewQueryStringQuery("title:" + search + " search:" + search))
+ }
+ req.Highlight = bleve.NewHighlightWithStyle("html")
+ req.Size = maxSearchResult
+ res, err = i.index.Search(req)
+ if err != nil {
+ return
+ }
+ if res.Total == 0 {
+ html = noResults
+ return
+ } else if res.Total == maxSearchResult {
+ err = indexErr("too many search results")
+ return
+ }
+ for _, hit := range res.Hits {
+ if hit == nil {
+ continue
+ }
+ art, err := i.Get(false, hit.ID)
+ if err != nil {
+ continue
+ }
+ html += `` + art.Title + ""
+ for fragField, frags := range hit.Fragments {
+ if fragField == "title" || fragField == "search" {
+ html += ``
+ for _, f := range frags {
+ html += f
+ }
+ html += ""
+ break
+ }
+ }
+ html += "
"
+ }
+ return
+}
+
+type Title struct {
+ Title string
+ LinkTitle string
+}
+
+func (i *Index) GetAll(public bool) (t []Title, err error) {
+ var (
+ res *bleve.SearchResult
+ req *bleve.SearchRequest
+ )
+ if public {
+ req = bleve.NewSearchRequest(bleve.NewBoolFieldQuery(true).SetField("public"))
+ } else {
+ req = bleve.NewSearchRequest(bleve.NewMatchAllQuery())
+ }
+ req.Size = maxResult
+ res, err = i.index.Search(req)
+ if err != nil {
+ return
+ }
+ for _, v := range res.Hits {
+ if v == nil {
+ continue
+ }
+ if v.ID == indexName {
+ continue
+ }
+ doc, err := i.index.Document(v.ID)
+ if err != nil || doc == nil || doc.Fields == nil {
+ continue
+ }
+ for _, field := range doc.Fields {
+ if field.Name() == "title" {
+ f, ok := field.(*document.TextField)
+ if ok {
+ t = append(t, Title{string(f.Value()), v.ID})
+ break
+ }
+ }
+ }
+ }
+ return
+}
+
+func (i *Index) Dump() (titles []string, err error) {
+ req := bleve.NewSearchRequest(bleve.NewMatchAllQuery())
+ req.Size = maxResult
+ res, err := i.index.Search(req)
+ if err != nil {
+ return
+ }
+ for _, v := range res.Hits {
+ if v == nil {
+ continue
+ }
+ titles = append(titles, v.ID)
+ }
+ return
+}
diff --git a/main.go b/main.go
index 7b9d41c..3f7c614 100644
--- a/main.go
+++ b/main.go
@@ -4,154 +4,14 @@
package main
-import (
- "errors"
- "flag"
- logger "log"
- "net"
- "os"
- "os/signal"
- "syscall"
- "time"
+import "github.com/urfave/cli"
- "git.giftfish.de/ston1th/godrop"
- "github.com/blevesearch/bleve"
-)
-
-const (
- bleveStore = "."
-
- defDataDir = "/var/lib/gowiki"
- defListen = "127.0.0.1:8090"
- defLog = "gowiki.log"
-
- defUser = "admin"
- defPassword = "gowiki"
-
- defRunUser = "wiki"
- defRunGroup = "wiki"
-
- welcome = `# Welcome to GoWiki
-This is the [Index](/wiki/Index) page.
-You can customize it how you like.`
- indexName = "Index"
-)
-
-var (
- datadir string
- listen string
- secureCookie bool
- user string
- group string
- logfile string
-
- engine *Engine
- db *BoltStore
-
- dump bool
- restore bool
- verbose bool
-)
+var version string
func main() {
- flag.StringVar(&datadir, "d", defDataDir, "data directory")
- flag.StringVar(&user, "u", defRunUser, "drop privileges to user")
- flag.StringVar(&group, "g", defRunGroup, "drop privileges to group")
- flag.StringVar(&listen, "l", defListen, "listening :")
- flag.StringVar(&logfile, "log", defLog, "log file")
- flag.BoolVar(&secureCookie, "s", false, "enable secure cookie")
- flag.BoolVar(&dump, "dump", false, "create a json dump of all articles in the data directory (dump is written to stdout)")
- flag.BoolVar(&restore, "restore", false, "restore a json dump to the data directory (dump is read from stdin)")
- flag.BoolVar(&verbose, "v", false, "enable verbose dump/restore mode")
- flag.Parse()
-
- err := os.Chdir(datadir)
- if err != nil {
- logger.Fatal("main: ", err)
- }
-
- if dump {
- err = dumpWiki()
- if err != nil {
- logger.Fatal("dump: ", err)
- }
- return
- }
-
- if restore {
- err = restoreWiki()
- if err != nil {
- logger.Fatal("restore: ", err)
- }
- return
- }
-
- cfg := godrop.Config{
- User: user,
- Group: group,
- }
- err = godrop.Drop(cfg, func() (net.Listener, error) {
- return net.Listen("tcp", listen)
- })
- if err != nil {
- logger.Fatal("main: ", err)
- }
- newLogger(logfile)
- l, err := godrop.GetListener()
- if err != nil {
- log.Fatal("main: ", err)
- }
-
- db, err = NewBoltStore("users")
- if err != nil {
- log.Fatal("main: ", err)
- }
- if !db.UserExists(defUser) {
- err = db.CreateUser(defUser, defPassword, true)
- if err != nil {
- log.Fatal("main: ", err)
- }
- }
-
- if _, err := os.Stat(bleveStore); os.IsNotExist(err) {
- mapping := bleve.NewIndexMapping()
- index, err := bleve.New(bleveStore, mapping)
- if err != nil {
- log.Fatal("main: ", err)
- }
- engine = NewEngine(index)
- _, err = engine.Index(indexName, welcome, defUser, true)
- if err != nil {
- log.Fatal("main: ", err)
- }
- } else {
- index, err := bleve.Open(bleveStore)
- if err != nil {
- log.Fatal("main: ", err)
- }
- engine = NewEngine(index)
- }
-
- go webserver(l)
- log.Println("gowiki started")
- sigs := make(chan os.Signal)
- signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
- sig := <-sigs
- log.Println("caught " + sig.String())
- go func() {
- time.Sleep(time.Second * 20)
- log.Fatal(errors.New("stop timed out: killing"))
- }()
- err = l.Close()
- if err != nil {
- log.Println(err)
- }
- err = db.Close()
- if err != nil {
- log.Println(err)
- }
- err = engine.Close()
- if err != nil {
- log.Println(err)
- }
+ app := cli.NewApp()
+ app.Name = "gowiki"
+ app.Usage = "a simple wiki engine"
+ app.Version = version
+ run(app)
}
diff --git a/render.go b/render.go
index 1636cc5..be2b360 100644
--- a/render.go
+++ b/render.go
@@ -25,38 +25,74 @@ var (
reLink = regexp.MustCompile(`id="(.+)">`)
whiteSpace = regexp.MustCompile(`\s+`)
reIndex = regexp.MustCompile("(.+)")
+ titleRe = regexp.MustCompile("[^a-zA-Z0-9 -]+")
)
-func render(text string) Article {
+type rendered struct {
+ Index template.HTML
+ Text template.HTML
+ MD string
+ Raw []byte
+}
+
+func (r *rendered) Article() *Article {
+ return &Article{
+ Index: r.Index,
+ Text: r.Text,
+ MD: r.MD,
+ Search: whiteSpace.ReplaceAllString(string(filter.Filter(r.Raw)), " "),
+ }
+}
+
+func (r *rendered) Snippet() *Snippet {
+ return &Snippet{
+ Index: r.Index,
+ Text: r.Text,
+ MD: r.MD,
+ }
+}
+
+func render(text string) *rendered {
rend := blackfriday.MarkdownCommon([]byte(text))
- html := string(rend)
- index := buildIndex(html)
+ index, html := buildIndex(string(rend))
html = strings.Replace(html, "", ``, -1)
- html = reHeader.ReplaceAllString(html, `${1} id="${2}">${2}${3}`)
- html = reLink.ReplaceAllStringFunc(html, makeLinkTitle)
html = strings.Replace(html, "", `
`, -1)
html = strings.Replace(html, "${2}${3}`)
+ html = reLink.ReplaceAllStringFunc(html, spaceReplace)
for k, v := range hn {
- link := makeLinkTitle(v[2])
+ link, _ := makeLinkTitle(v[2])
+ i := m[link]
+ m[link] = i + 1
+ newLink := link + strconv.Itoa(i)
+ html = strings.Replace(html, link+`"`, newLink+`"`, 1)
ret += before(hn, k)
- ret += `` + v[2] + ""
+ ret += `` + v[2] + ""
ret += after(hn, k)
}
- return
+ return ret, html
}
func before(arr [][]string, i int) string {
diff --git a/routes.go b/routes.go
index d6426fe..b9f09c7 100644
--- a/routes.go
+++ b/routes.go
@@ -2,134 +2,126 @@
package main
-import (
- "net/http"
-
- "github.com/gorilla/mux"
-)
-
type route struct {
Path string
- Handler http.HandlerFunc
+ Handler ctxHandler
Methods []string
}
-func buildRoutes() http.Handler {
- m := mux.NewRouter()
- m.NotFoundHandler = notFoundHandler{}
- for _, v := range routes {
- m.HandleFunc(v.Path, v.Handler).Methods(v.Methods...)
- }
- return m
-}
-
var routes = []route{
{
"/",
- contextWrapper(
- indexHandler),
+ indexHandler,
[]string{"GET"},
},
{
"/bootstrap.css",
- contextWrapper(
- staticHandler),
+ staticHandler,
[]string{"GET"},
},
{
"/custom.css",
- contextWrapper(
- staticHandler),
+ staticHandler,
[]string{"GET"},
},
{
"/login",
- contextWrapper(
- loginHandler),
+ loginHandler,
[]string{"GET", "POST"},
},
{
"/search",
- contextWrapper(
- searchHandler),
+ searchHandler,
[]string{"GET", "POST"},
},
{
"/new",
- contextWrapper(
+ authHandler(
wikiNewHandler),
[]string{"GET", "POST"},
},
{
"/all",
- contextWrapper(
- allHandler),
+ allHandler,
[]string{"GET"},
},
{
- "/wiki/{title:[a-zA-Z0-9+]+$}",
- contextWrapper(
+ "/wiki/{title:[a-zA-Z0-9+-]+$}",
+ wikiHandler,
+ []string{"GET"},
+ },
+ {
+ "/wiki/{title:[a-zA-Z0-9+-]+}/md",
+ wikiMDHandler(
wikiHandler),
[]string{"GET"},
},
{
- "/wiki/{title:[a-zA-Z0-9+]+}/md",
- contextWrapper(
- wikiMDHandler(
- wikiHandler)),
- []string{"GET"},
- },
- {
- "/wiki/{title:[a-zA-Z0-9+]+}/edit",
- contextWrapper(
+ "/wiki/{title:[a-zA-Z0-9+-]+}/edit",
+ authHandler(
wikiEditHandler),
[]string{"GET", "POST"},
},
{
- "/wiki/{title:[a-zA-Z0-9+]+}/del",
- contextWrapper(
+ "/wiki/{title:[a-zA-Z0-9+-]+}/del",
+ authHandler(
wikiDelHandler),
[]string{"POST"},
},
{
"/users",
- contextWrapper(
- adminAuthHandler(
- usersHandler)),
+ adminAuthHandler(
+ usersHandler),
[]string{"GET"},
},
{
- "/user/new",
- contextWrapper(
- adminAuthHandler(
- userNewHandler)),
+ "/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+$}",
+ wikiHandler,
+ []string{"GET"},
+ },
+ {
+ "/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/md",
+ wikiMDHandler(
+ wikiHandler),
+ []string{"GET"},
+ },
+ {
+ "/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/edit",
+ wikiEditHandler,
[]string{"GET", "POST"},
},
{
- "/user/del/{user:[a-z]+$}",
- contextWrapper(
- adminAuthHandler(
- userDelHandler)),
- []string{"GET", "POST"},
- },
- {
- "/user/unlock/{user:[a-z]+$}",
- contextWrapper(
- adminAuthHandler(
- userUnlockHandler)),
+ "/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/del",
+ wikiDelHandler,
[]string{"POST"},
},
{
- "/user/edit/{user:[a-z]+$}",
- contextWrapper(
- userAuthHandler(
- userEditHandler)),
+ "/user/new",
+ adminAuthHandler(
+ userNewHandler),
+ []string{"GET", "POST"},
+ },
+ {
+ "/user/del/{user:[a-zA-Z0-9]+$}",
+ adminAuthHandler(
+ userDelHandler),
+ []string{"GET", "POST"},
+ },
+ {
+ "/user/unlock/{user:[a-zA-Z0-9]+$}",
+ adminAuthHandler(
+ userUnlockHandler),
+ []string{"POST"},
+ },
+ {
+ "/user/edit/{user:[a-zA-Z0-9]+$}",
+ userAuthHandler(
+ userEditHandler),
[]string{"GET", "POST"},
},
{
"/logout",
- contextWrapper(
- logoutHandler),
+ logoutHandler,
[]string{"GET", "POST"},
},
}
diff --git a/scripts/gowiki_init b/scripts/gowiki_init
old mode 100755
new mode 100644
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..9d54f92
--- /dev/null
+++ b/server.go
@@ -0,0 +1,118 @@
+package main
+
+import (
+ "context"
+ "crypto/rand"
+ "github.com/gorilla/mux"
+ "github.com/gorilla/sessions"
+ "html/template"
+ "io"
+ "net"
+ "net/http"
+ "time"
+)
+
+const (
+ keyLen = 32
+ authLen = keyLen * 2
+ cookieName = "gosession"
+)
+
+type HTTPServer struct {
+ Version string
+ SecCookie bool
+ Store *sessions.CookieStore
+ Editors *EditorStore
+ BS *BoltStore
+ Index *Index
+ listener net.Listener
+ srv *http.Server
+ templ map[string]*template.Template
+ res map[string][]byte
+}
+
+func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServer) {
+ srv = &HTTPServer{
+ Version: version,
+ SecCookie: secCookie,
+ listener: l,
+ }
+ srv.Editors = NewEditorStore()
+ srv.cookieStore()
+ srv.loadTemplates()
+ srv.srv = &http.Server{
+ Handler: srv.buildRoutes(),
+ }
+ return
+}
+
+func (s *HTTPServer) Start() (err error) {
+ s.BS, err = NewBoltStore()
+ if err != nil {
+ return
+ }
+ s.Index, err = NewIndex(blevePath)
+ if err != nil {
+ return
+ }
+ go func() {
+ log.Println(s.srv.Serve(s.listener))
+ }()
+ return
+}
+
+func (s *HTTPServer) Stop() error {
+ s.BS.Close()
+ s.Index.Close()
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ defer cancel()
+ return s.srv.Shutdown(ctx)
+}
+
+func (s *HTTPServer) buildRoutes() http.Handler {
+ m := mux.NewRouter()
+ m.NotFoundHandler = ¬FoundHandler{s}
+ for _, v := range routes {
+ m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
+ }
+ return m
+}
+func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h(newContext(w, r, s))
+ }
+}
+func (s *HTTPServer) cookieStore() {
+ options := &sessions.Options{
+ Path: "/",
+ MaxAge: 3600 * 12,
+ Secure: s.SecCookie,
+ HttpOnly: true,
+ }
+ authKey := genKey(authLen)
+ encKey := genKey(keyLen)
+ s.Store = sessions.NewCookieStore(authKey, encKey)
+ s.Store.Options = options
+
+ go func() {
+ for {
+ time.Sleep(time.Hour * 12)
+ newAuthKey := genKey(authLen)
+ newEncKey := genKey(keyLen)
+ newStore := sessions.NewCookieStore(newAuthKey, newEncKey, authKey, encKey)
+ newStore.Options = options
+
+ s.Store = newStore
+ authKey = newAuthKey
+ encKey = newEncKey
+ }
+ }()
+}
+
+func genKey(length int) (bytes []byte) {
+ bytes = make([]byte, length)
+ if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
+ log.Println("genKey:", err)
+ }
+ return
+}
diff --git a/snippet.go b/snippet.go
new file mode 100644
index 0000000..e267c3e
--- /dev/null
+++ b/snippet.go
@@ -0,0 +1,148 @@
+package main
+
+import (
+ "bytes"
+ "github.com/boltdb/bolt"
+ "html/template"
+ "strings"
+)
+
+type Snippet struct {
+ Title string `json:"title"`
+ LinkTitle string `json:"-"`
+ Index template.HTML `json:"index"`
+ Text template.HTML `json:"text"`
+ MD string `json:"md"`
+ Created string `json:"created"`
+ Updated string `json:"updated"`
+ Permission int `json:"permission"`
+}
+
+func copyBuf(buf []byte) (b []byte) {
+ b = make([]byte, len(buf))
+ copy(b, buf)
+ return
+}
+
+var (
+ errSnippetNotExists = dbErr("snippet does not exist")
+ errSnippetExists = dbErr("snippet already exist")
+ errInvalidPermission = dbErr("invalid permission")
+)
+
+func (bs *BoltStore) GetSnippets(username string) (snip []Snippet, err error) {
+ user := []byte(username)
+ var buf [][]byte
+ err = bs.db.View(func(tx *bolt.Tx) error {
+ return tx.Bucket([]byte(snippetTable)).ForEach(func(k, v []byte) error {
+ if !bytes.HasPrefix(k, user) {
+ return nil
+ }
+ buf = append(buf, copyBuf(v))
+ return nil
+ })
+ })
+ if err != nil {
+ return
+ }
+ for _, b := range buf {
+ s := new(Snippet)
+ if err = bs.Unmarshal(b, s); err != nil {
+ return
+ }
+ snip = append(snip, *s)
+ }
+ return
+}
+
+func (bs *BoltStore) GetSnippet(username, title string, perm int) (*Snippet, error) {
+ s := new(Snippet)
+ err := bs.db.View(func(tx *bolt.Tx) error {
+ v := tx.Bucket([]byte(snippetTable)).Get([]byte(title))
+ if v == nil {
+ return errSnippetNotExists
+ }
+ return bs.Unmarshal(v, s)
+ })
+ if err != nil {
+ return nil, err
+ }
+ switch s.Permission {
+ case 0:
+ return s, nil
+ case 1:
+ if username != "" {
+ return s, nil
+ }
+ case 2:
+ if strings.HasPrefix(title, username) {
+ return s, nil
+ }
+ }
+ return nil, errSnippetNotExists
+}
+
+func (bs *BoltStore) snippetExists(username, title string, perm int) (err error) {
+ _, err = bs.GetSnippet(username, title, perm)
+ return
+}
+
+func makeKey(username, title string) string {
+ return username + title
+}
+
+func checkPerm(p int) error {
+ switch p {
+ case 0, 1, 2:
+ return nil
+ }
+ return errInvalidPermission
+}
+
+func (bs *BoltStore) CreateSnippet(username, title, text string, perm int) (newTitle string, err error) {
+ newTitle, title = makeLinkTitle(title)
+ err = checkPerm(perm)
+ if err != nil {
+ return
+ }
+ err = bs.snippetExists(username, title, perm)
+ if err == nil {
+ err = errSnippetExists
+ return
+ }
+ err = bs.db.Update(func(tx *bolt.Tx) error {
+ snip := render(text).Snippet()
+ snip.Title = title
+ snip.Created = username + " " + now()
+ snip.Permission = perm
+ v, err := bs.Marshal(snip)
+ if err != nil {
+ return err
+ }
+ return tx.Bucket([]byte(snippetTable)).Put([]byte(makeKey(username, title)), v)
+ })
+ return
+}
+
+func (bs *BoltStore) EditSnippet(username, title, text string, perm int) (err error) {
+ err = checkPerm(perm)
+ if err != nil {
+ return
+ }
+ err = bs.snippetExists(username, title, perm)
+ if err != nil {
+ return
+ }
+ err = bs.db.Update(func(tx *bolt.Tx) error {
+ snip := render(text).Snippet()
+ snip.Title = title
+ snip.Created = username + " " + now()
+ snip.Permission = perm
+ v, err := bs.Marshal(snip)
+ if err != nil {
+ return err
+ }
+ return tx.Bucket([]byte(snippetTable)).Put([]byte(makeKey(username, title)), v)
+ })
+ return
+}
diff --git a/snippet_handler.go b/snippet_handler.go
new file mode 100644
index 0000000..f2c4fd7
--- /dev/null
+++ b/snippet_handler.go
@@ -0,0 +1,155 @@
+package main
+
+func allSnippetHandler(ctx *Context) {
+ ctx.Template("allSnippetHandler")
+ ctx.Data = webData{Title: "All Snippets"}
+ all, err := ctx.Srv.BS.GetSnippets(ctx.GetUser())
+ if err != nil {
+ ctx.Data.Msg = err.Error()
+ }
+ ctx.Data.Data = all
+ ctx.Exec()
+}
+
+func snippetNewHandler(ctx *Context) {
+ ctx.Template("snippetNewHandler")
+ ctx.Data = webData{Title: "New Snippet"}
+ switch ctx.Method() {
+ case "GET":
+ ctx.Exec()
+ case "POST":
+ user := ctx.GetUser()
+ title := ctx.Form("title")
+ text := ctx.Form("text")
+ perm := ctx.Form("permission")
+ submit := ctx.Form("submit")
+ snip := Snippet{
+ Title: title,
+ MD: text,
+ }
+ ctx.Data.Data = art
+ if !ctx.CheckToken() {
+ ctx.Data.Msg = "wrong csrf token"
+ ctx.Exec()
+ return
+ }
+ iperm, err := strconv.Atoi(perm)
+ if err != nil || submit != "Submit" {
+ ctx.Data.Msg = "wrong inputs"
+ ctx.Exec()
+ return
+ }
+ newTitle, err := ctx.Srv.BS.CreateSnippet(user, title, text, iperm)
+ if err != nil {
+ ctx.Data.Msg = err.Error()
+ ctx.Exec()
+ return
+ }
+ ctx.Redirect("/users/"+user+"/"+newTitle, 302)
+ }
+}
+
+func wikiHandler(ctx *Context) {
+ if ctx.T == nil {
+ ctx.Template("wikiHandler")
+ }
+ ctx.Data = webData{
+ Admin: ctx.GetAdmin(),
+ }
+ title := ctx.Var("title")
+ art, err := ctx.Srv.Index.Get(!ctx.LoggedOn(), title)
+ if err != nil {
+ ctx.NotFound()
+ return
+ }
+ art.LinkTitle = title
+ ctx.Data.Data = art
+ ctx.Data.Title = art.Title
+ ctx.Data.BodyTitle = art.Title
+ ctx.Exec()
+}
+
+func wikiMDHandler(f ctxHandler) ctxHandler {
+ return func(ctx *Context) {
+ ctx.Template("wikiMDHandler")
+ f(ctx)
+ }
+}
+
+func wikiEditHandler(ctx *Context) {
+ ctx.Template("wikiEditHandler")
+ ctx.Data = webData{}
+ title := ctx.Var("title")
+ switch ctx.Method() {
+ case "GET":
+ art, err := ctx.Srv.Index.Get(!ctx.LoggedOn(), title)
+ if err != nil {
+ ctx.NotFound()
+ return
+ }
+ art.LinkTitle = title
+ ctx.Data.Data = art
+ ctx.Data.Title = art.Title
+ ctx.Data.BodyTitle = art.Title
+ ctx.Data.Editor = ctx.Srv.Editors.Set(title, ctx.GetUser())
+ ctx.Exec()
+ case "POST":
+ ftitle := ctx.Form("title")
+ text := ctx.Form("text")
+ public := ctx.Form("public")
+ pub := false
+ if public == "0" {
+ pub = true
+ }
+ submit := ctx.Form("submit")
+ ctx.Data.Data = Article{
+ Title: ftitle,
+ LinkTitle: title,
+ MD: text,
+ Public: pub,
+ }
+ ctx.Data.Title = ftitle
+ ctx.Data.BodyTitle = ftitle
+ if !ctx.CheckToken() {
+ ctx.Data.Msg = "wrong csrf token"
+ ctx.Exec()
+ return
+ }
+ if submit == "Back" {
+ ctx.Srv.Editors.RemoveEditor(title, ctx.GetUser())
+ ctx.Redirect("/wiki/"+title, 302)
+ return
+ }
+ if submit != "Submit" {
+ ctx.Data.Msg = "wrong inputs"
+ ctx.Exec()
+ return
+ }
+ err := ctx.Srv.Index.Update(title, text, ctx.GetUser(), pub)
+ if err != nil {
+ ctx.Data.Msg = err.Error()
+ ctx.Exec()
+ return
+ }
+ ctx.Srv.Editors.Remove(title)
+ ctx.Redirect("/wiki/"+title, 302)
+ }
+}
+
+func wikiDelHandler(ctx *Context) {
+ title := ctx.Var("title")
+ submit := ctx.Form("submit")
+ if !ctx.CheckToken() {
+ ctx.Redirect("/wiki/"+title, 302)
+ return
+ }
+ if submit != "Delete" {
+ ctx.Redirect("/wiki/"+title, 302)
+ return
+ }
+ if err := ctx.Srv.Index.Delete(title); err != nil {
+ log.Println("delete:", err)
+ ctx.Redirect("/wiki/"+title, 302)
+ }
+ ctx.Redirect("/wiki/Index", 302)
+}
diff --git a/templates.go b/templates.go
index 73efdc1..dcc6edc 100644
--- a/templates.go
+++ b/templates.go
@@ -31,10 +31,10 @@ const (
GoWiki | {{.Title}}
-
-
+
+
-
+
- {{if .Msg}}
-
- {{end}}
- {{template "body" .}}
+
+
+
+ {{if .Msg}}
+
+ {{end}}
+ {{template "body" .}}
+
+
+
+
+
`
@@ -94,22 +113,35 @@ const (
{{end}}`
menu = `{{define "menu"}}
-{{if .Login}}
+
{{else}}
-
-
{{end}}
+
{{end}}`
notFound = `{{define "body"}}