diff options
author | Steve Manuel <nilslice@gmail.com> | 2017-01-10 09:45:17 -0800 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2017-01-10 09:45:17 -0800 |
commit | 5e120ac4d5f6e8c53e5828df6640b20dc5862faa (patch) | |
tree | dd22241be556cc51eeb7aab2abdd60a1156034c4 | |
parent | d5b31987a05df02cf4129e8603f2304b191e0834 (diff) |
adding initial support for third-party addons and the basic framework for how they are registered by the system
-rw-r--r-- | system/addon/addon.go | 170 | ||||
-rw-r--r-- | system/addon/manager.go | 89 | ||||
-rw-r--r-- | system/admin/admin.go | 1 | ||||
-rw-r--r-- | system/admin/handlers.go | 474 | ||||
-rw-r--r-- | system/admin/server.go | 3 | ||||
-rw-r--r-- | system/admin/upload/upload.go | 2 | ||||
-rw-r--r-- | system/api/external.go | 7 | ||||
-rw-r--r-- | system/api/handlers.go | 2 | ||||
-rw-r--r-- | system/db/addon.go | 155 | ||||
-rw-r--r-- | system/db/init.go | 2 |
10 files changed, 886 insertions, 19 deletions
diff --git a/system/addon/addon.go b/system/addon/addon.go new file mode 100644 index 0000000..bf1a717 --- /dev/null +++ b/system/addon/addon.go @@ -0,0 +1,170 @@ +package addon + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/ponzu-cms/ponzu/system/db" + "github.com/ponzu-cms/ponzu/system/item" +) + +var ( + // Types is a record of addons, like content types, of addon_reverse_dns:interface{} + Types = make(map[string]func() interface{}) +) + +const ( + // StatusEnabled defines string status for Addon enabled state + StatusEnabled = "enabled" + // StatusDisabled defines string status for Addon disabled state + StatusDisabled = "disabled" +) + +// Meta contains the basic information about the addon +type Meta struct { + PonzuAddonName string `json:"addon_name"` + PonzuAddonAuthor string `json:"addon_author"` + PonzuAddonAuthorURL string `json:"addon_author_url"` + PonzuAddonVersion string `json:"addon_version"` + PonzuAddonReverseDNS string `json:"addon_reverse_dns"` + PonzuAddonStatus string `json:"addon_status"` +} + +// Addon contains information about a provided addon to the system +type Addon struct { + item.Item + Meta +} + +// Register sets up the system to use the Addon by: +// 1. Adding Meta to the Addon struct +// 2. Saving it to the __addons bucket in DB with id/key = addon_reverse_dns +// 3. Checking that the Addon parent type was added to Types (likely via its init()) +func Register(meta Meta, addon Addon) error { + a := Addon{Meta: meta} + + // get or create the reverse DNS identifier + if a.PonzuAddonReverseDNS == "" { + revDNS, err := reverseDNS(meta) + if err != nil { + return err + } + + a.PonzuAddonReverseDNS = revDNS + } + + if _, ok := Types[a.PonzuAddonReverseDNS]; !ok { + panic(`Addon "` + a.PonzuAddonName + `" has no record in the addons.Types map`) + } + + // check if addon is already registered in db as addon_reverse_dns + if db.AddonExists(a.PonzuAddonReverseDNS) { + return nil + } + + // convert a.Item into usable data, Item{} => []byte(json) => map[string]interface{} + kv := make(map[string]interface{}) + + data, err := json.Marshal(a.Item) + if err != nil { + return err + } + + err = json.Unmarshal(data, &kv) + if err != nil { + return err + } + + // save new addon to db + vals := make(url.Values) + for k, v := range kv { + vals.Set(k, v.(string)) + } + + vals.Set("addon_name", a.PonzuAddonName) + vals.Set("addon_author", a.PonzuAddonAuthor) + vals.Set("addon_author_url", a.PonzuAddonAuthorURL) + vals.Set("addon_version", a.PonzuAddonVersion) + vals.Set("addon_reverse_dns", a.PonzuAddonReverseDNS) + vals.Set("addon_status", StatusDisabled) + + // db.SetAddon is like SetContent, but rather than the key being an int64 ID, + // we need it to be a string based on the addon_reverse_dns + err = db.SetAddon(vals) + if err != nil { + return err + } + + return nil +} + +// Deregister removes an addon from the system. `key` is the addon_reverse_dns +func Deregister(key string) error { + err := db.DeleteAddon(key) + if err != nil { + return err + } + + delete(Types, key) + return nil +} + +// Enable sets the addon status to `enabled`. `key` is the addon_reverse_dns +func Enable(key string) error { + err := setStatus(key, StatusEnabled) + if err != nil { + return err + } + + return nil +} + +// Disable sets the addon status to `disabled`. `key` is the addon_reverse_dns +func Disable(key string) error { + err := setStatus(key, StatusDisabled) + if err != nil { + return err + } + + return nil +} + +func setStatus(key, status string) error { + a, err := db.Addon(key) + if err != nil { + return err + } + + a.Set("addon_status", status) + + err = db.SetAddon(a) + if err != nil { + return err + } + + return nil +} + +func reverseDNS(meta Meta) (string, error) { + u, err := url.Parse(meta.PonzuAddonAuthorURL) + if err != nil { + return "", nil + } + + if u.Host == "" { + return "", fmt.Errorf(`Error parsing Addon Author URL: %s. Ensure URL is formatted as "scheme://hostname/path?query" (path & query optional)`, meta.PonzuAddonAuthorURL) + } + + name := strings.Replace(meta.PonzuAddonName, " ", "", -1) + + // reverse the host name parts, split on '.', ex. bosssauce.it => it.bosssauce + parts := strings.Split(u.Host, ".") + strap := make([]string, len(parts), len(parts)) + for i := len(parts) - 1; i >= 0; i-- { + strap = append(strap, parts[i]) + } + + return strings.Join(append(strap, name), "."), nil +} diff --git a/system/addon/manager.go b/system/addon/manager.go new file mode 100644 index 0000000..1be09c0 --- /dev/null +++ b/system/addon/manager.go @@ -0,0 +1,89 @@ +package addon + +import ( + "bytes" + "fmt" + "html/template" + "net/url" + + "encoding/json" + + "github.com/ponzu-cms/ponzu/management/editor" +) + +const defaultInput = `<input type="hidden" name="%s" value="%s"/>` + +const managerHTML = ` +<div class="card editor"> + <form method="post" action="/admin/addon" enctype="multipart/form-data"> + {{ .DefaultInputs }} + {{ .Editor }} + </form> +</div> +` + +type manager struct { + DefaultInputs template.HTML + Editor template.HTML +} + +// Manage ... +func Manage(data url.Values, reverseDNS string) ([]byte, error) { + a, ok := Types[reverseDNS] + if !ok { + return nil, fmt.Errorf("Addon has not been added to addon.Types map") + } + + at := a() + + // convert data => json => at{} + j, err := json.Marshal(data) + if err != nil { + return nil, err + } + + err = json.Unmarshal(j, &at) + if err != nil { + return nil, err + } + + e, ok := at.(editor.Editable) + if !ok { + return nil, fmt.Errorf("Addon is not editable - must implement editor.Editable: %T", at) + } + + v, err := e.MarshalEditor() + if err != nil { + return nil, fmt.Errorf("Couldn't marshal editor for addon: %s", err.Error()) + } + + inputs := &bytes.Buffer{} + fields := []string{ + "addon_name", + "addon_author", + "addon_author_url", + "addon_version", + "addon_reverse_dns", + "addon_status", + } + + for _, f := range fields { + input := fmt.Sprintf(defaultInput, f, data.Get(f)) + _, err := inputs.WriteString(input) + if err != nil { + return nil, fmt.Errorf("Failed to write input for addon view: %s", f) + } + } + + m := manager{ + DefaultInputs: template.HTML(inputs.Bytes()), + Editor: template.HTML(v), + } + + // execute html template into buffer for func return val + buf := &bytes.Buffer{} + tmpl := template.Must(template.New("manager").Parse(managerHTML)) + tmpl.Execute(buf, m) + + return buf.Bytes(), nil +} diff --git a/system/admin/admin.go b/system/admin/admin.go index e3ae2d6..db1f2ce 100644 --- a/system/admin/admin.go +++ b/system/admin/admin.go @@ -65,6 +65,7 @@ var mainAdminHTML = ` <div class="row collection-item"> <li><a class="col s12" href="/admin/configure"><i class="tiny left material-icons">settings</i>Configuration</a></li> <li><a class="col s12" href="/admin/configure/users"><i class="tiny left material-icons">supervisor_account</i>Users</a></li> + <li><a class="col s12" href="/admin/addons"><i class="tiny left material-icons">supervisor_account</i>Addons</a></li> </div> </ul> </div> diff --git a/system/admin/handlers.go b/system/admin/handlers.go index c5c4a4c..cccfa54 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -2,17 +2,20 @@ package admin import ( "bytes" + "context" "encoding/base64" "encoding/json" "fmt" "log" "net/http" + "net/url" "strconv" "strings" "time" "github.com/ponzu-cms/ponzu/management/editor" "github.com/ponzu-cms/ponzu/management/manager" + "github.com/ponzu-cms/ponzu/system/addon" "github.com/ponzu-cms/ponzu/system/admin/config" "github.com/ponzu-cms/ponzu/system/admin/upload" "github.com/ponzu-cms/ponzu/system/admin/user" @@ -263,6 +266,17 @@ func configUsersEditHandler(res http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodPost: err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB + if err != nil { + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } // check if user to be edited is current user j, err := db.CurrentUser(req) @@ -385,6 +399,17 @@ func configUsersDeleteHandler(res http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodPost: err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB + if err != nil { + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } // do not allow current user to delete themselves j, err := db.CurrentUser(req) @@ -567,8 +592,9 @@ func forgotPasswordHandler(res http.ResponseWriter, req *http.Request) { case http.MethodPost: err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB if err != nil { - res.WriteHeader(http.StatusBadRequest) - errView, err := Error400() + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() if err != nil { return } @@ -939,12 +965,37 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { log.Println("Error unmarshal json into", t, err, string(posts[i])) post := `<li class="col s12">Error decoding data. Possible file corruption.</li>` - b.Write([]byte(post)) + _, err := b.Write([]byte(post)) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } + continue } post := adminPostListItem(p, t, status) - b.Write(post) + _, err = b.Write(post) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } } case "pending": @@ -964,12 +1015,36 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { log.Println("Error unmarshal json into", t, err, string(posts[i])) post := `<li class="col s12">Error decoding data. Possible file corruption.</li>` - b.Write([]byte(post)) + _, err := b.Write([]byte(post)) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } continue } post := adminPostListItem(p, t, status) - b.Write(post) + _, err = b.Write(post) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } } } @@ -982,18 +1057,54 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { log.Println("Error unmarshal json into", t, err, string(posts[i])) post := `<li class="col s12">Error decoding data. Possible file corruption.</li>` - b.Write([]byte(post)) + _, err := b.Write([]byte(post)) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } continue } post := adminPostListItem(p, t, status) - b.Write(post) + _, err = b.Write(post) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } } } html += `<ul class="posts row">` - b.Write([]byte(`</ul>`)) + _, err = b.Write([]byte(`</ul>`)) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } statusDisabled := "disabled" prevStatus := "" @@ -1042,7 +1153,19 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { ` } - b.Write([]byte(pagination + `</div></div>`)) + _, err = b.Write([]byte(pagination + `</div></div>`)) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } script := ` <script> @@ -1269,6 +1392,10 @@ func approveContentHandler(res http.ResponseWriter, req *http.Request) { return } + // set the target in the context so user can get saved value from db in hook + ctx := context.WithValue(req.Context(), "target", fmt.Sprintf("%s:%d", t, id)) + req = req.WithContext(ctx) + err = hook.AfterSave(req) if err != nil { log.Println("Error running AfterSave hook in approveContentHandler for:", t, err) @@ -1358,6 +1485,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) { log.Println("Content type", t, "doesn't implement item.Identifiable") return } + item.SetItemID(-1) } @@ -1388,8 +1516,8 @@ func editHandler(res http.ResponseWriter, req *http.Request) { err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB if err != nil { log.Println(err) - res.WriteHeader(http.StatusBadRequest) - errView, err := Error405() + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() if err != nil { return } @@ -1509,6 +1637,10 @@ func editHandler(res http.ResponseWriter, req *http.Request) { return } + // set the target in the context so user can get saved value from db in hook + ctx := context.WithValue(req.Context(), "target", fmt.Sprintf("%s:%d", t, id)) + req = req.WithContext(ctx) + err = hook.AfterSave(req) if err != nil { log.Println(err) @@ -1549,6 +1681,12 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) { if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) return } @@ -1729,15 +1867,51 @@ func searchHandler(res http.ResponseWriter, req *http.Request) { log.Println("Error unmarshal search result json into", t, err, posts[i]) post := `<li class="col s12">Error decoding data. Possible file corruption.</li>` - b.Write([]byte(post)) + _, err = b.Write([]byte(post)) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } continue } post := adminPostListItem(p, t, status) - b.Write([]byte(post)) + _, err = b.Write([]byte(post)) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } } - b.Write([]byte(`</ul></div></div>`)) + _, err := b.WriteString(`</ul></div></div>`) + if err != nil { + log.Println(err) + + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + } + + res.Write(errView) + return + } btn := `<div class="col s3"><a href="/admin/edit?type=` + t + `" class="btn new-post waves-effect waves-light">New ` + t + `</a></div></div>` html = html + b.String() + btn @@ -1752,3 +1926,273 @@ func searchHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "text/html") res.Write(adminView) } + +func addonsHandler(res http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + all := db.AddonAll() + list := &bytes.Buffer{} + + for i := range all { + v := adminAddonListItem(all[i]) + _, err := list.Write(v) + if err != nil { + log.Println("Error writing bytes to addon list view:", err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + return + } + + res.Write(errView) + } + } + + html := &bytes.Buffer{} + open := `<div class="col s9 card"> + <div class="card-content"> + <div class="row"> + <div class="card-title col s7">Addons</div> + </div> + <ul class="posts row">` + + _, err := html.WriteString(open) + if err != nil { + log.Println("Error writing open html to addon html view:", err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + return + } + + res.Write(errView) + } + + _, err = html.Write(list.Bytes()) + if err != nil { + log.Println("Error writing list bytes to addon html view:", err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + return + } + + res.Write(errView) + } + + _, err = html.WriteString(`</ul></div></div>`) + if err != nil { + log.Println("Error writing close html to addon html view:", err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + return + } + + res.Write(errView) + } + + view, err := Admin(html.Bytes()) + if err != nil { + log.Println("Error writing addon html to admin view:", err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + log.Println(err) + return + } + + res.Write(errView) + } + + res.Write(view) + + case http.MethodPost: + err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB + if err != nil { + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + + id := req.PostFormValue("id") + action := req.PostFormValue("action") + + _, err = db.Addon(id) + if err == db.ErrNoAddonExists { + log.Println(err) + res.WriteHeader(http.StatusNotFound) + errView, err := Error404() + if err != nil { + return + } + + res.Write(errView) + return + } + if err != nil { + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + + switch action { + case "enable": + err := addon.Enable(id) + if err != nil { + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + case "disable": + err := addon.Disable(id) + if err != nil { + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + default: + res.WriteHeader(http.StatusBadRequest) + errView, err := Error405() + if err != nil { + return + } + + res.Write(errView) + return + } + + default: + res.WriteHeader(http.StatusBadRequest) + errView, err := Error405() + if err != nil { + log.Println(err) + return + } + + res.Write(errView) + } +} + +func addonHandler(res http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + id := req.FormValue("id") + + data, err := db.Addon(id) + if err != nil { + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + + _, ok := addon.Types[id] + if !ok { + log.Println("Addon: ", id, "is not found in addon.Types map") + res.WriteHeader(http.StatusNotFound) + errView, err := Error404() + if err != nil { + return + } + + res.Write(errView) + return + } + + m, err := addon.Manage(data, id) + if err != nil { + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + + addonView, err := Admin(m) + if err != nil { + log.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + res.Header().Set("Content-Type", "text/html") + res.Write(addonView) + + case http.MethodPost: + default: + res.WriteHeader(http.StatusBadRequest) + errView, err := Error405() + if err != nil { + log.Println(err) + return + } + + res.Write(errView) + } +} + +func adminAddonListItem(data url.Values) []byte { + var action string + var buttonClass string + status := data.Get("addon_status") + if status != addon.StatusEnabled { + action = "Enable" + buttonClass = "green" + } else { + action = "Disable" + buttonClass = "red" + } + + id := data.Get("addon_reverse_dns") + name := data.Get("addon_name") + a := ` + <li class="col s12"> + <a href="/admin/addon?id=` + id + `" alt="Configure '` + name + `'">` + name + `</a> + + <form enctype="multipart/form-data" class="quick-` + strings.ToLower(action) + `-addon __ponzu right" action="/admin/addons" method="post"> + <button class="btn waves-effect waves-effect-light ` + buttonClass + `">` + action + `</button> + <input type="hidden" name="id" value="` + id + `" /> + <input type="hidden" name="action" value="` + action + `" /> + </form> + </li>` + + return []byte(a) +} diff --git a/system/admin/server.go b/system/admin/server.go index 155892a..f2bf244 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -23,6 +23,9 @@ func Run() { http.HandleFunc("/admin/recover", forgotPasswordHandler) http.HandleFunc("/admin/recover/key", recoveryKeyHandler) + http.HandleFunc("/admin/addons", user.Auth(addonsHandler)) + http.HandleFunc("/admin/addon", user.Auth(addonHandler)) + http.HandleFunc("/admin/configure", user.Auth(configHandler)) http.HandleFunc("/admin/configure/users", user.Auth(configUsersHandler)) http.HandleFunc("/admin/configure/users/edit", user.Auth(configUsersEditHandler)) diff --git a/system/admin/upload/upload.go b/system/admin/upload/upload.go index 323f371..486f55c 100644 --- a/system/admin/upload/upload.go +++ b/system/admin/upload/upload.go @@ -14,7 +14,7 @@ import ( func StoreFiles(req *http.Request) (map[string]string, error) { err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB if err != nil { - return nil, fmt.Errorf("%s", err) + return nil, err } ts := req.FormValue("timestamp") // timestamp in milliseconds since unix epoch diff --git a/system/api/external.go b/system/api/external.go index 302b7c9..662fc07 100644 --- a/system/api/external.go +++ b/system/api/external.go @@ -1,6 +1,7 @@ package api import ( + "context" "fmt" "log" "net/http" @@ -136,13 +137,17 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) { spec = "__pending" } - _, err = db.SetContent(t+spec+":-1", req.PostForm) + id, err := db.SetContent(t+spec+":-1", req.PostForm) if err != nil { log.Println("[External] error:", err) res.WriteHeader(http.StatusInternalServerError) return } + // set the target in the context so user can get saved value from db in hook + ctx := context.WithValue(req.Context(), "target", fmt.Sprintf("%s:%d", t, id)) + req = req.WithContext(ctx) + err = hook.AfterSave(req) if err != nil { log.Println("[External] error:", err) diff --git a/system/api/handlers.go b/system/api/handlers.go index 869640f..1bc4fbb 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -178,7 +178,7 @@ func hide(it interface{}, res http.ResponseWriter, req *http.Request) bool { // check if should be hidden if h, ok := it.(item.Hideable); ok { err := h.Hide(req) - if err != nil && err == item.ErrAllowHiddenItem { + if err == item.ErrAllowHiddenItem { return false } diff --git a/system/db/addon.go b/system/db/addon.go new file mode 100644 index 0000000..da83b7f --- /dev/null +++ b/system/db/addon.go @@ -0,0 +1,155 @@ +package db + +import ( + "bytes" + "errors" + "fmt" + "log" + "net/url" + + "github.com/boltdb/bolt" +) + +var ( + // ErrNoAddonExists indicates that there was not addon found in the db + ErrNoAddonExists = errors.New("No addon exists.") +) + +// Addon looks for an addon by its addon_reverse_dns as the key and returns +// the url.Values representation of an addon +func Addon(key string) (url.Values, error) { + buf := &bytes.Buffer{} + + err := store.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("__addons")) + + val := b.Get([]byte(key)) + + if val == nil { + return ErrNoAddonExists + } + + _, err := buf.Write(val) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + data, err := url.ParseQuery(buf.String()) + if err != nil { + return nil, err + } + + return data, nil +} + +// SetAddon stores the values of an addon into the __addons bucket with a the +// addon_reverse_dns field used as the key +func SetAddon(data url.Values) error { + // we don't know the structure of the addon type from a addon developer, so + // encoding to json before it's stored in the db is difficult. Instead, we + // can just encode the url.Values to a query string using the Encode() method. + // The downside is that we will have to parse the values out of the query + // string when loading it from the db + v := data.Encode() + + err := store.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("__addons")) + k := data.Get("addon_reverse_dns") + if k == "" { + name := data.Get("addon_name") + return fmt.Errorf(`Addon "%s" has no identifier to use as key.`, name) + } + + err := b.Put([]byte(k), []byte(v)) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + return nil +} + +// AddonAll returns all registered addons as a [][]byte +func AddonAll() []url.Values { + var all []url.Values + buf := &bytes.Buffer{} + + err := store.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("__addons")) + err := b.ForEach(func(k, v []byte) error { + _, err := buf.Write(v) + if err != nil { + return err + } + + data, err := url.ParseQuery(buf.String()) + if err != nil { + return err + } + + all = append(all, data) + return nil + }) + if err != nil { + return err + } + + return nil + }) + if err != nil { + log.Println("Error finding addons in db with db.AddonAll:", err) + return nil + } + + return all +} + +// DeleteAddon removes an addon from the db by its key, the addon_reverse_dns +func DeleteAddon(key string) error { + err := store.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("__addons")) + + if err := b.Delete([]byte(key)); err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + return nil +} + +// AddonExists checks if there is an existing addon stored. The key is an the +// value at addon_reverse_dns +func AddonExists(key string) bool { + var exists bool + err := store.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("__addons")) + if b.Get([]byte(key)) == nil { + return nil + } + + exists = true + return nil + }) + if err != nil { + log.Println("Error checking existence of addon with key:", key, "-", err) + return false + } + + return exists +} diff --git a/system/db/init.go b/system/db/init.go index bf93bc6..6bc4753 100644 --- a/system/db/init.go +++ b/system/db/init.go @@ -45,7 +45,7 @@ func Init() { } // init db with other buckets as needed - buckets := []string{"__config", "__users", "__contentIndex"} + buckets := []string{"__config", "__users", "__contentIndex", "__addons"} for _, name := range buckets { _, err := tx.CreateBucketIfNotExists([]byte(name)) if err != nil { |