From 21b9f301c5e1305c3be4e260161d2495bca059de Mon Sep 17 00:00:00 2001 From: Steve Date: Thu, 12 Jan 2017 16:19:00 -0800 Subject: [addons] Expanding basic addon framework (#29) --- system/addon/addon.go | 234 +++++++++++ system/addon/manager.go | 117 ++++++ system/admin/admin.go | 3 +- system/admin/config/config.go | 12 +- system/admin/handlers.go | 584 +++++++++++++++++++++++++++- system/admin/server.go | 3 + system/admin/static/dashboard/css/admin.css | 17 + system/admin/upload/upload.go | 2 +- system/api/external.go | 7 +- system/api/handlers.go | 2 +- system/db/addon.go | 151 +++++++ system/db/content.go | 29 +- system/db/init.go | 7 +- system/item/types.go | 24 +- 14 files changed, 1145 insertions(+), 47 deletions(-) create mode 100644 system/addon/addon.go create mode 100644 system/addon/manager.go create mode 100644 system/db/addon.go (limited to 'system') diff --git a/system/addon/addon.go b/system/addon/addon.go new file mode 100644 index 0000000..51be9dc --- /dev/null +++ b/system/addon/addon.go @@ -0,0 +1,234 @@ +package addon + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/ponzu-cms/ponzu/system/db" + "github.com/ponzu-cms/ponzu/system/item" + + "github.com/tidwall/sjson" +) + +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 constructs a new addon and registers it with the system. Meta is a +// addon.Meta and fn is a closure returning a pointer to your own addon type +func Register(m Meta, fn func() interface{}) Addon { + // get or create the reverse DNS identifier + if m.PonzuAddonReverseDNS == "" { + revDNS, err := reverseDNS(m) + if err != nil { + panic(err) + } + + m.PonzuAddonReverseDNS = revDNS + } + + Types[m.PonzuAddonReverseDNS] = fn + + a := Addon{Meta: m} + + err := register(a) + if err != nil { + panic(err) + } + + return a +} + +// register sets up the system to use the Addon by: +// 1. Validating the Addon struct +// 2. Saving it to the __addons bucket in DB with id/key = addon_reverse_dns +func register(a Addon) error { + if a.PonzuAddonName == "" { + return fmt.Errorf(`Addon must have valid Meta struct embedded: missing %s field.`, "PonzuAddonName") + } + if a.PonzuAddonAuthor == "" { + return fmt.Errorf(`Addon must have valid Meta struct embedded: missing %s field.`, "PonzuAddonAuthor") + } + if a.PonzuAddonAuthorURL == "" { + return fmt.Errorf(`Addon must have valid Meta struct embedded: missing %s field.`, "PonzuAddonAuthorURL") + } + if a.PonzuAddonVersion == "" { + return fmt.Errorf(`Addon must have valid Meta struct embedded: missing %s field.`, "PonzuAddonVersion") + } + + if _, ok := Types[a.PonzuAddonReverseDNS]; !ok { + return fmt.Errorf(`Addon "%s" has no record in the addons.Types map`, a.PonzuAddonName) + } + + // 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, fmt.Sprintf("%v", v)) + } + + 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 + kind, ok := Types[a.PonzuAddonReverseDNS] + if !ok { + return fmt.Errorf("Error: no addon to set with id: %s", a.PonzuAddonReverseDNS) + } + + err = db.SetAddon(vals, kind()) + 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, err = sjson.SetBytes(a, "addon_status", status) + if err != nil { + return err + } + + kind, ok := Types[key] + if !ok { + return fmt.Errorf("Error: no addon to set with id: %s", key) + } + + // convert json => map[string]interface{} => url.Values + var kv map[string]interface{} + err = json.Unmarshal(a, &kv) + if err != nil { + return err + } + + vals := make(url.Values) + for k, v := range kv { + switch v.(type) { + case []string: + s := v.([]string) + for i := range s { + if i == 0 { + vals.Set(k, s[i]) + } + + vals.Add(k, s[i]) + } + default: + vals.Set(k, fmt.Sprintf("%v", v)) + } + } + + err = db.SetAddon(vals, kind()) + 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, 0, 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..d3c9673 --- /dev/null +++ b/system/addon/manager.go @@ -0,0 +1,117 @@ +package addon + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "net/url" + + "github.com/ponzu-cms/ponzu/management/editor" + + "github.com/gorilla/schema" + "github.com/tidwall/gjson" +) + +const defaultInput = `` + +const managerHTML = ` +
+
+
+
{{ .AddonName }}
+
+ {{ .DefaultInputs }} + {{ .Editor }} +
+
+` + +type manager struct { + DefaultInputs template.HTML + Editor template.HTML + AddonName string +} + +// Manage ... +func Manage(data []byte, reverseDNS string) ([]byte, error) { + a, ok := Types[reverseDNS] + if !ok { + return nil, fmt.Errorf("Addon has not been added to addon.Types map") + } + + // convert json => map[string]interface{} => url.Values + var kv map[string]interface{} + err := json.Unmarshal(data, &kv) + if err != nil { + return nil, err + } + + vals := make(url.Values) + for k, v := range kv { + switch v.(type) { + case []string: + s := v.([]string) + for i := range s { + if i == 0 { + vals.Set(k, s[i]) + } + + vals.Add(k, s[i]) + } + default: + vals.Set(k, fmt.Sprintf("%v", v)) + } + } + + at := a() + + dec := schema.NewDecoder() + dec.IgnoreUnknownKeys(true) + dec.SetAliasTag("json") + err = dec.Decode(at, vals) + 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, gjson.GetBytes(data, f).String()) + _, 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), + AddonName: gjson.GetBytes(data, "addon_name").String(), + } + + // 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..9c4cfdd 100644 --- a/system/admin/admin.go +++ b/system/admin/admin.go @@ -64,7 +64,8 @@ var mainAdminHTML = `
System
  • settingsConfiguration
  • -
  • supervisor_accountUsers
  • +
  • supervisor_accountAdmin Users
  • +
  • settings_input_svideoAddons
  • diff --git a/system/admin/config/config.go b/system/admin/config/config.go index 2bc80c6..7b57dc0 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -8,7 +8,6 @@ import ( // Config represents the confirgurable options of the system type Config struct { item.Item - editor editor.Editor Name string `json:"name"` Domain string `json:"domain"` @@ -23,9 +22,6 @@ type Config struct { // String partially implements item.Identifiable and overrides Item's String() func (c *Config) String() string { return c.Name } -// Editor partially implements editor.Editable -func (c *Config) Editor() *editor.Editor { return &c.editor } - // MarshalEditor writes a buffer of html to edit a Post and partially implements editor.Editable func (c *Config) MarshalEditor() ([]byte, error) { view, err := editor.Form(c, @@ -90,7 +86,13 @@ func (c *Config) MarshalEditor() ([]byte, error) { return nil, err } - open := []byte(`
    `) + open := []byte(` +
    +
    +
    System Configuration
    +
    + + `) close := []byte(`
    `) script := []byte(`