From 2b568328434b9b58ccc9dbeacbc06e49b99f46d7 Mon Sep 17 00:00:00 2001 From: ston1th Date: Wed, 7 Feb 2018 21:41:54 +0100 Subject: [PATCH] major changes --- .gitignore | 2 + Godeps/Godeps.json | 24 +- Makefile | 4 +- VERSION | 1 + cache.go | 49 + cmd.go | 193 +++ context.go | 136 +- dump.go | 91 +- dump.test | 1 + editor.go | 43 + engine.go | 270 ---- handler.go | 184 +-- helper.go | 36 + index.go | 328 ++++ main.go | 154 +- render.go | 68 +- routes.go | 124 +- scripts/gowiki_init | 0 server.go | 118 ++ snippet.go | 148 ++ snippet_handler.go | 155 ++ templates.go | 170 +- templates.sh | 39 +- templates/custom.css | 32 +- templates/index.html | 39 +- templates/menu.html | 40 +- templates/search.html | 12 +- templates/wikiEdit.html | 8 +- userdb.go | 156 +- .../git.giftfish.de/ston1th/godrop/.gitignore | 3 +- vendor/git.giftfish.de/ston1th/godrop/LICENSE | 2 +- .../git.giftfish.de/ston1th/godrop/README.md | 2 + .../git.giftfish.de/ston1th/godrop/godrop.go | 162 +- .../git.giftfish.de/ston1th/godrop/lookup.go | 29 + .../ston1th/godrop/lookup_cgo.go | 23 + .../ston1th/godrop/lookup_nocgo.go | 43 + vendor/github.com/gorilla/mux/.travis.yml | 3 +- vendor/github.com/gorilla/mux/README.md | 113 +- .../github.com/gorilla/mux/context_gorilla.go | 26 + .../github.com/gorilla/mux/context_native.go | 24 + vendor/github.com/gorilla/mux/doc.go | 34 + vendor/github.com/gorilla/mux/mux.go | 117 +- vendor/github.com/gorilla/mux/regexp.go | 57 +- vendor/github.com/gorilla/mux/route.go | 13 +- .../github.com/gorilla/sessions/.travis.yml | 24 +- vendor/github.com/gorilla/sessions/README.md | 47 +- vendor/github.com/gorilla/sessions/doc.go | 16 +- vendor/github.com/gorilla/sessions/lex.go | 102 ++ .../github.com/gorilla/sessions/sessions.go | 8 +- vendor/github.com/gorilla/sessions/store.go | 27 +- .../russross/blackfriday/.travis.yml | 40 +- .../github.com/russross/blackfriday/README.md | 6 +- .../github.com/russross/blackfriday/block.go | 76 +- .../github.com/russross/blackfriday/inline.go | 25 +- .../russross/blackfriday/markdown.go | 84 +- vendor/github.com/urfave/cli/.gitignore | 2 + vendor/github.com/urfave/cli/.travis.yml | 39 + vendor/github.com/urfave/cli/CHANGELOG.md | 392 +++++ vendor/github.com/urfave/cli/LICENSE | 21 + vendor/github.com/urfave/cli/README.md | 1364 +++++++++++++++++ vendor/github.com/urfave/cli/app.go | 492 ++++++ vendor/github.com/urfave/cli/appveyor.yml | 24 + vendor/github.com/urfave/cli/category.go | 44 + vendor/github.com/urfave/cli/cli.go | 21 + vendor/github.com/urfave/cli/command.go | 286 ++++ vendor/github.com/urfave/cli/context.go | 276 ++++ vendor/github.com/urfave/cli/errors.go | 110 ++ vendor/github.com/urfave/cli/flag-types.json | 93 ++ vendor/github.com/urfave/cli/flag.go | 799 ++++++++++ .../github.com/urfave/cli/flag_generated.go | 627 ++++++++ vendor/github.com/urfave/cli/funcs.go | 28 + .../github.com/urfave/cli/generate-flag-types | 255 +++ vendor/github.com/urfave/cli/help.go | 294 ++++ vendor/github.com/urfave/cli/runtests | 122 ++ vendor/golang.org/x/sys/unix/mksyscall.pl | 0 .../x/sys/unix/mksyscall_solaris.pl | 0 .../golang.org/x/sys/unix/mksysctl_openbsd.pl | 0 .../golang.org/x/sys/unix/mksysnum_darwin.pl | 0 .../x/sys/unix/mksysnum_dragonfly.pl | 0 .../golang.org/x/sys/unix/mksysnum_freebsd.pl | 0 .../golang.org/x/sys/unix/mksysnum_linux.pl | 0 .../golang.org/x/sys/unix/mksysnum_netbsd.pl | 0 .../golang.org/x/sys/unix/mksysnum_openbsd.pl | 0 83 files changed, 7768 insertions(+), 1252 deletions(-) create mode 100644 VERSION create mode 100644 cache.go create mode 100644 cmd.go create mode 100644 dump.test create mode 100644 editor.go delete mode 100644 engine.go create mode 100644 helper.go create mode 100644 index.go mode change 100755 => 100644 scripts/gowiki_init create mode 100644 server.go create mode 100644 snippet.go create mode 100644 snippet_handler.go create mode 100644 vendor/git.giftfish.de/ston1th/godrop/lookup.go create mode 100644 vendor/git.giftfish.de/ston1th/godrop/lookup_cgo.go create mode 100644 vendor/git.giftfish.de/ston1th/godrop/lookup_nocgo.go create mode 100644 vendor/github.com/gorilla/mux/context_gorilla.go create mode 100644 vendor/github.com/gorilla/mux/context_native.go create mode 100644 vendor/github.com/gorilla/sessions/lex.go create mode 100644 vendor/github.com/urfave/cli/.gitignore create mode 100644 vendor/github.com/urfave/cli/.travis.yml create mode 100644 vendor/github.com/urfave/cli/CHANGELOG.md create mode 100644 vendor/github.com/urfave/cli/LICENSE create mode 100644 vendor/github.com/urfave/cli/README.md create mode 100644 vendor/github.com/urfave/cli/app.go create mode 100644 vendor/github.com/urfave/cli/appveyor.yml create mode 100644 vendor/github.com/urfave/cli/category.go create mode 100644 vendor/github.com/urfave/cli/cli.go create mode 100644 vendor/github.com/urfave/cli/command.go create mode 100644 vendor/github.com/urfave/cli/context.go create mode 100644 vendor/github.com/urfave/cli/errors.go create mode 100644 vendor/github.com/urfave/cli/flag-types.json create mode 100644 vendor/github.com/urfave/cli/flag.go create mode 100644 vendor/github.com/urfave/cli/flag_generated.go create mode 100644 vendor/github.com/urfave/cli/funcs.go create mode 100644 vendor/github.com/urfave/cli/generate-flag-types create mode 100644 vendor/github.com/urfave/cli/help.go create mode 100644 vendor/github.com/urfave/cli/runtests mode change 100755 => 100644 vendor/golang.org/x/sys/unix/mksyscall.pl mode change 100755 => 100644 vendor/golang.org/x/sys/unix/mksyscall_solaris.pl mode change 100755 => 100644 vendor/golang.org/x/sys/unix/mksysctl_openbsd.pl mode change 100755 => 100644 vendor/golang.org/x/sys/unix/mksysnum_darwin.pl mode change 100755 => 100644 vendor/golang.org/x/sys/unix/mksysnum_dragonfly.pl mode change 100755 => 100644 vendor/golang.org/x/sys/unix/mksysnum_freebsd.pl mode change 100755 => 100644 vendor/golang.org/x/sys/unix/mksysnum_linux.pl mode change 100755 => 100644 vendor/golang.org/x/sys/unix/mksysnum_netbsd.pl mode change 100755 => 100644 vendor/golang.org/x/sys/unix/mksysnum_openbsd.pl 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}} - - + + - + {{end}}` menu = `{{define "menu"}} -{{if .Login}} + + +
    +
    + + + + +
    +
    + {{else}} - - {{end}} + {{end}}` notFound = `{{define "body"}} {{end}}` wikiEdit = `{{define "body"}} +{{if .Editor}} +
    +

    Warning!

    +

    {{.Editor.Username}} is editing this page since {{.Editor.Time}}

    +
    +{{end}}