diff --git a/pkg/core/const.go b/pkg/core/const.go index 0e98f4d..135d9f0 100644 --- a/pkg/core/const.go +++ b/pkg/core/const.go @@ -8,7 +8,8 @@ const ( IndexURI = WikiSection + "/" + IndexPage - LoginURI = "/login" - LogoutURI = "/logout" - TotpURI = "/totp" + LoginURI = "/login" + LogoutURI = "/logout" + TotpURI = "/totp" + SectionsURI = "/sections" ) diff --git a/pkg/db/db.go b/pkg/db/db.go index 6bae139..445c1c7 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -68,7 +68,8 @@ func New(cfg core.Config) (db *DB, err error) { if err != nil { return } - return db.RunMigrations() + err = db.RunMigrations() + return } func (db *DB) Dump(w io.Writer) error { diff --git a/pkg/db/helper.go b/pkg/db/helper.go index 3772ca0..820c792 100644 --- a/pkg/db/helper.go +++ b/pkg/db/helper.go @@ -4,7 +4,6 @@ package db import ( "crypto/sha256" - "git.giftfish.de/ston1th/gowiki/pkg/core" "time" ) @@ -14,10 +13,11 @@ var reservedNames = []string{ "logout", "new", "search", + "section", "sections", "user", + "users", "view", - "wiki", } // Parts taken from strings.TrimPrefix @@ -28,20 +28,6 @@ func hasTrimPrefix(s, prefix string) (string, bool) { return s, false } -func addOrRemove(s string, a []string) []string { - i, _ := core.Contains(s, a) - switch i { - case -1: - return append(a, s) - case 0: - return a[1:] - case len(a): - return a[:i] - default: - return append(a[:i], a[i+1:]...) - } -} - func validatePassword(pw string) (b []byte) { b = []byte(pw) if len(pw) <= 56 { diff --git a/pkg/db/migration.go b/pkg/db/migration.go index 1408bdd..441ada2 100644 --- a/pkg/db/migration.go +++ b/pkg/db/migration.go @@ -3,11 +3,12 @@ package db import ( "git.giftfish.de/ston1th/gowiki/pkg/core" "git.giftfish.de/ston1th/gowiki/pkg/log" + "git.giftfish.de/ston1th/gowiki/pkg/store" ) const ( versionKey = "version/version" - currentVersion = 2 + currentVersion = 1 ) type migrator func(*DB) (int, error) @@ -18,10 +19,6 @@ var migrators = []migrator{ }, func(db *DB) (version int, err error) { version = 1 - return - }, - func(db *DB) (version int, err error) { - version = 2 // recreate all pages with new format var pages []string db.store.ForEach(func(k string, _ []byte) error { @@ -38,12 +35,25 @@ var migrators = []migrator{ log.Printf("db: migrator[%d]: pageGet %s: %s", version, k, err) continue } + if p.Perm == 3 { + p.Perm = core.Private + } err = db.store.Set(k, &p) if err != nil { log.Printf("db: migrator[%d]: pageSet %s: %s", version, k, err) continue } } + users, err := db.GetUsers() + if err != nil { + return + } + for _, u := range users { + err = db.CreateSection(u.Username, []string{u.Username}, true) + if err != nil { + log.Printf("db: migrator[%d]: userSection %s: %s", version, u.Username, err) + } + } return }, } @@ -51,16 +61,21 @@ var migrators = []migrator{ func (db *DB) RunMigrations() (err error) { var version int err = db.store.Get(versionKey, &version) - if err == ErrKeyNotFound { - err = db.store.Set(versionKey, 0) + if err == store.ErrKeyNotFound { + err = db.store.Set(versionKey, currentVersion) + if err != nil { + return + } + err = db.store.Get(versionKey, &version) if err != nil { return } } - log.Printf("db: detected database version %d", version) + log.Printf("db: database version %d", version) for version < currentVersion { - log.Printf("db: running migration %d", version) - version, err = migrators[version](db) + v := version + 1 + log.Printf("db: running migration %d", v) + version, err = migrators[v](db) if err != nil { return } @@ -69,4 +84,5 @@ func (db *DB) RunMigrations() (err error) { return } } + return } diff --git a/pkg/db/page.go b/pkg/db/page.go index 1f0f163..93991fb 100644 --- a/pkg/db/page.go +++ b/pkg/db/page.go @@ -21,7 +21,7 @@ var ( errDeleteIndexPage = errors.New("db: index page can not be deleted") ) -func (db *DB) GetAllSections(username string) (sections core.SectionPages) { +func (db *DB) GetAllSections(username string, admin bool) (sections core.SectionPages) { m := make(map[string]core.UpdatedPages) db.store.ForEach(func(k string, _ []byte) error { if trim, ok := hasTrimPrefix(k, pagePrefix); ok { @@ -39,6 +39,16 @@ func (db *DB) GetAllSections(username string) (sections core.SectionPages) { } return nil }) + if admin { + secs, err := db.GetSections() + if err == nil { + for _, s := range secs { + if _, ok := m[s.Name]; !ok { + m[s.Name] = core.UpdatedPages{} + } + } + } + } for s, p := range m { sort.Sort(p) l := len(p) @@ -51,7 +61,11 @@ func (db *DB) GetAllSections(username string) (sections core.SectionPages) { return } -func (db *DB) GetAllSectionPages(section, username string) (pages core.Pages, err error) { +func (db *DB) GetAllSectionPages(section, username string, admin bool) (pages core.Pages, err error) { + err = db.SectionExists(section) + if err != nil { + return + } db.store.ForEach(func(k string, _ []byte) error { if trim, ok := hasTrimPrefix(k, pagePrefix); ok { if !strings.HasPrefix(trim, section+"/") { @@ -67,7 +81,7 @@ func (db *DB) GetAllSectionPages(section, username string) (pages core.Pages, er } return nil }) - if len(pages) == 0 { + if len(pages) == 0 && !admin { err = errPageNotFound return } @@ -128,6 +142,14 @@ func (db *DB) CreatePage(title, section, markdown, username string, p core.Permi if err != store.ErrKeyNotFound { return } + s, err := db.GetSection(section) + if err != nil { + return + } + if _, ok := core.Contains(username, s.Members); !ok { + err = errPermissionDenied + return + } page = &core.Page{ Title: title, diff --git a/pkg/db/permission.go b/pkg/db/permission.go index 4d806f3..f920db8 100644 --- a/pkg/db/permission.go +++ b/pkg/db/permission.go @@ -4,6 +4,7 @@ package db import ( "git.giftfish.de/ston1th/gowiki/pkg/core" + "git.giftfish.de/ston1th/gowiki/pkg/log" "git.giftfish.de/ston1th/gowiki/pkg/render" ) @@ -11,6 +12,7 @@ func (db *DB) ReadPerm(st, username string, p core.Permission) bool { section, _ := render.SplitStoreTitle(st) s, err := db.GetSection(section) if err != nil { + log.Debugf("db: read: %s", err) return false } return core.ReadPerm(username, p, s) @@ -20,6 +22,7 @@ func (db *DB) WritePerm(st, username string, p core.Permission) bool { section, _ := render.SplitStoreTitle(st) s, err := db.GetSection(section) if err != nil { + log.Debugf("db: read: %s", err) return false } return core.WritePerm(username, p, s) diff --git a/pkg/db/section.go b/pkg/db/section.go index 0f88086..39a108e 100644 --- a/pkg/db/section.go +++ b/pkg/db/section.go @@ -30,6 +30,20 @@ func (db *DB) GetSections() (secs core.Sections, err error) { return } +func (db *DB) GetUserSections(username string) (sections map[string]bool, err error) { + sections = make(map[string]bool) + secs, err := db.GetSections() + if err != nil { + return + } + for _, s := range secs { + if _, ok := core.Contains(username, s.Members); ok { + sections[s.Name] = false + } + } + return +} + func (db *DB) GetSection(section string) (s core.Section, err error) { err = db.store.Get(sectionPrefix+section, &s) return @@ -68,11 +82,11 @@ func (db *DB) UpdateSection(section string, members []string, user bool) error { return db.store.Set(sectionPrefix+section, &core.Section{section, user, m}) } -func (db *DB) DeleteSection(section string, user bool) error { +func (db *DB) DeleteSection(section string) error { if section == core.WikiSection { return errors.New("section '" + core.WikiSection + "' cannot be deleted") } - s, err := db.GetSection(section) + err := db.SectionExists(section) if err != nil { return err } @@ -80,9 +94,6 @@ func (db *DB) DeleteSection(section string, user bool) error { if err != nil { return err } - if !s.User && !user { - return nil - } section += "/" var pages []string db.store.ForEach(func(k string, _ []byte) error { diff --git a/pkg/db/user.go b/pkg/db/user.go index 05a922b..af8cb60 100644 --- a/pkg/db/user.go +++ b/pkg/db/user.go @@ -220,5 +220,5 @@ func (db *DB) DeleteUser(username string) error { if err != nil { return err } - return db.DeleteSection(username, true) + return db.DeleteSection(username) } diff --git a/pkg/render/render.go b/pkg/render/render.go index 4366eba..d958b22 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -48,7 +48,11 @@ func StoreTitle(section, title string) string { } func SplitStoreTitle(st string) (section, title string) { - if a := strings.Split(st, "/"); len(a) == 2 { + a := strings.Split(st, "/") + switch len(a) { + case 1: + return a[0], "" + case 2: return a[0], a[1] } return diff --git a/pkg/render/render_test.go b/pkg/render/render_test.go index 7fb5971..8e01008 100644 --- a/pkg/render/render_test.go +++ b/pkg/render/render_test.go @@ -21,6 +21,12 @@ func TestTitleFunctions(t *testing.T) { t.Fail() } }) + t.Run("SplitStoreTitleSingle", func(t *testing.T) { + se, ti := SplitStoreTitle(section) + if se != section || ti != "" { + t.Fail() + } + }) t.Run("Title", func(t *testing.T) { if Title("9ßw$%4h4t-8v74 287(G /SV&3[45v}=94#+?)") != "9w4h4t-8v74+287G+SV345v94" { t.Fail() diff --git a/pkg/server/context.go b/pkg/server/context.go index 96a1597..57be0d0 100644 --- a/pkg/server/context.go +++ b/pkg/server/context.go @@ -115,6 +115,16 @@ type loginData struct { Referer string } +type sectionData struct { + Name string + Members map[string]bool +} + +type pageData struct { + Page core.Page + Sections map[string]bool +} + func (c *Context) Exec() { defer c.log() if c.T == nil { diff --git a/pkg/server/handler.go b/pkg/server/handler.go index f89f7bf..65cc6c6 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -215,7 +215,7 @@ func sectionsHandler(ctx *Context) { BodyTitle: "All Sections", Admin: ctx.Admin(), } - ctx.Data.Data = ctx.Srv.DB.GetAllSections(ctx.User()) + ctx.Data.Data = ctx.Srv.DB.GetAllSections(ctx.User(), ctx.Admin()) ctx.Exec() } @@ -228,7 +228,7 @@ func sectionHandler(ctx *Context) { Admin: ctx.Admin(), } var err error - ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User()) + ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User(), ctx.Admin()) if err != nil { ctx.NotFound() return @@ -238,61 +238,140 @@ func sectionHandler(ctx *Context) { func sectionNewHandler(ctx *Context) { ctx.Template("sectionNewHandler") - section := ctx.Var("section") ctx.Data = webData{ - Title: section, - BodyTitle: section, - Admin: ctx.Admin(), + Title: "New Section", + BodyTitle: "New Section", } - var err error - ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User()) + users, err := ctx.Srv.DB.GetUsers() if err != nil { - ctx.NotFound() + ctx.Error(err) return } - ctx.Exec() -} - -func sectionDelHandler(ctx *Context) { - ctx.Template("sectionDelHandler") - section := ctx.Var("section") - ctx.Data = webData{ - Title: section, - BodyTitle: section, - Admin: ctx.Admin(), + sd := sectionData{Members: make(map[string]bool)} + for _, u := range users { + sd.Members[u.Username] = false } - var err error - ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User()) - if err != nil { - ctx.NotFound() - return + switch ctx.Method() { + case "GET": + ctx.Data.Data = sd + ctx.Exec() + case "POST": + if !ctx.CheckXsrf() { + return + } + section := ctx.Form("section") + members := ctx.FormSlice("members") + for _, m := range members { + sd.Members[m] = true + } + ctx.Data.Data = sd + if section == "" { + ctx.Error("empty section") + return + } + err = ctx.Srv.DB.CreateSection(section, members, false) + if err != nil { + ctx.Error(err) + return + } + log.Printf("create: section %s created by %s\n", section, ctx.User()) + ctx.Redirect(core.SectionsURI, http.StatusFound) } - ctx.Exec() } func sectionEditHandler(ctx *Context) { ctx.Template("sectionEditHandler") - section := ctx.Var("section") ctx.Data = webData{ - Title: section, - BodyTitle: section, - Admin: ctx.Admin(), + Title: "Edit Section", + BodyTitle: "Edit Section", } - var err error - ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User()) + users, err := ctx.Srv.DB.GetUsers() if err != nil { - ctx.NotFound() + ctx.Error(err) return } - ctx.Exec() + sd := sectionData{Members: make(map[string]bool)} + for _, u := range users { + sd.Members[u.Username] = false + } + s, err := ctx.Srv.DB.GetSection(ctx.Var("section")) + if err != nil { + ctx.Error(err) + return + } + sd.Name = s.Name + switch ctx.Method() { + case "GET": + for _, m := range s.Members { + sd.Members[m] = true + } + ctx.Data.Data = sd + ctx.Exec() + case "POST": + if !ctx.CheckXsrf() { + return + } + members := ctx.FormSlice("members") + for _, m := range members { + sd.Members[m] = true + } + ctx.Data.Data = sd + err = ctx.Srv.DB.UpdateSection(s.Name, members, s.User) + if err != nil { + ctx.Error(err) + return + } + log.Printf("update: section %s updated by %s\n", s.Name, ctx.User()) + ctx.Redirect(core.SectionsURI, http.StatusFound) + } +} + +func sectionDelHandler(ctx *Context) { + ctx.Template("sectionDelHandler") + ctx.Data = webData{ + Title: "Delete Section", + BodyTitle: "Delete Section", + } + s, err := ctx.Srv.DB.GetSection(ctx.Var("section")) + if err != nil { + ctx.Error(err) + return + } + if s.User { + ctx.Error("user section can not be deleted") + return + } + switch ctx.Method() { + case "GET": + ctx.Data.Data = s.Name + ctx.Exec() + case "POST": + if !ctx.CheckXsrf() { + return + } + err := ctx.Srv.DB.DeleteSection(s.Name) + if err != nil { + ctx.Error(err) + return + } + log.Printf("delete: section %s deleted by %s\n", s.Name, ctx.User()) + ctx.Redirect(core.SectionsURI, http.StatusFound) + } } func pageNewHandler(ctx *Context) { ctx.Template("pageNewHandler") ctx.Data = webData{Title: "New Page"} + user := ctx.User() + secs, err := ctx.Srv.DB.GetUserSections(user) + if err != nil { + ctx.Error(err) + return + } switch ctx.Method() { case "GET": - ctx.Data.Data = core.Page{Perm: core.Public} + secs[user] = true + ctx.Data.Data = pageData{core.Page{Perm: core.Public}, secs} ctx.Exec() case "POST": if !ctx.CheckXsrf() { @@ -307,7 +386,8 @@ func pageNewHandler(ctx *Context) { Markdown: markdown, Perm: perm, } - ctx.Data.Data = p + secs[section] = true + ctx.Data.Data = pageData{p, secs} if title == "" { ctx.Error("empty title") return @@ -317,10 +397,7 @@ func pageNewHandler(ctx *Context) { return } - if section != core.WikiSection { - section = ctx.User() - } - page, err := ctx.Srv.DB.CreatePage(title, section, markdown, ctx.User(), perm) + page, err := ctx.Srv.DB.CreatePage(title, section, markdown, user, perm) if err != nil { ctx.Error(err) return @@ -540,12 +617,12 @@ func pageDelHandler(ctx *Context) { } } -func userHandler(ctx *Context) { +func usersHandler(ctx *Context) { if !ctx.LoggedOn() { ctx.NotFound() return } - ctx.Template("userHandler") + ctx.Template("usersHandler") ctx.Data = webData{ Title: "Users", BodyTitle: "Users", @@ -593,8 +670,8 @@ func userNewHandler(ctx *Context) { ctx.Error(err) return } - log.Printf("create: %s created by %s\n", user, ctx.User()) - ctx.Redirect("/user", http.StatusFound) + log.Printf("create: user %s created by %s\n", user, ctx.User()) + ctx.Redirect("/users", http.StatusFound) } } @@ -635,8 +712,8 @@ func userEditHandler(ctx *Context) { ctx.Error(err) return } - log.Printf("update: %s updated by %s\n", user, ctx.User()) - ctx.Redirect("/user", http.StatusFound) + log.Printf("update: user %s updated by %s\n", user, ctx.User()) + ctx.Redirect("/users", http.StatusFound) return } if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil { @@ -724,7 +801,7 @@ func userUnlockHandler(ctx *Context) { log.Println("unlock:", err) } log.Printf("unlock: %s unlocked by %s\n", user, ctx.User()) - ctx.Redirect("/user", http.StatusFound) + ctx.Redirect("/users", http.StatusFound) } func userDelHandler(ctx *Context) { @@ -751,12 +828,12 @@ func userDelHandler(ctx *Context) { if err := ctx.Srv.DB.DeleteUser(user); err != nil { log.Println("delete:", err) } - log.Printf("delete: %s deleted by %s\n", user, self) + log.Printf("delete: user %s deleted by %s\n", user, self) if user == self { ctx.Redirect("/logout", http.StatusFound) return } - ctx.Redirect("/user", http.StatusFound) + ctx.Redirect("/users", http.StatusFound) } } diff --git a/pkg/server/routes.go b/pkg/server/routes.go index ac753ef..26c4094 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -35,23 +35,23 @@ var routes = []route{ []string{"GET"}, }, { - "/sections/new", + "/section/new", adminAuthHandler( sectionNewHandler), []string{"GET", "POST"}, }, - { - "/section/del/{section:[a-zA-Z0-9]+$}", - adminAuthHandler( - sectionDelHandler), - []string{"GET", "POST"}, - }, { "/section/edit/{section:[a-zA-Z0-9]+$}", adminAuthHandler( sectionEditHandler), []string{"GET", "POST"}, }, + { + "/section/del/{section:[a-zA-Z0-9]+$}", + adminAuthHandler( + sectionDelHandler), + []string{"GET", "POST"}, + }, { "/search", searchHandler, @@ -81,9 +81,9 @@ var routes = []route{ []string{"GET", "POST"}, }, { - "/user", + "/users", adminAuthHandler( - userHandler), + usersHandler), []string{"GET"}, }, { @@ -92,18 +92,18 @@ var routes = []route{ userNewHandler), []string{"GET", "POST"}, }, - { - "/user/del/{user:[a-zA-Z0-9]+$}", - userAuthHandler( - userDelHandler), - []string{"GET", "POST"}, - }, { "/user/edit/{user:[a-zA-Z0-9]+$}", userAuthHandler( userEditHandler), []string{"GET", "POST"}, }, + { + "/user/del/{user:[a-zA-Z0-9]+$}", + userAuthHandler( + userDelHandler), + []string{"GET", "POST"}, + }, { "/user/totp/{user:[a-zA-Z0-9]+$}", userAuthHandler( diff --git a/pkg/server/templates.go b/pkg/server/templates.go index 45fcf56..ce21ebf 100644 --- a/pkg/server/templates.go +++ b/pkg/server/templates.go @@ -36,7 +36,7 @@ const ( GoWiki | {{.Title}} - + @@ -150,7 +150,7 @@ const (