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) --- addons/github.com/bosssauce/reference/LICENSE | 29 + addons/github.com/bosssauce/reference/README.md | 3 + addons/github.com/bosssauce/reference/reference.go | 150 +++++ .../github.com/ponzu-cms/addons/reference/LICENSE | 29 - .../ponzu-cms/addons/reference/README.md | 3 - .../ponzu-cms/addons/reference/reference.go | 150 ----- cmd/ponzu/contentType.tmpl | 12 +- cmd/ponzu/main.go | 1 - .../vendor/github.com/tidwall/sjson/.travis.yml | 1 + cmd/ponzu/vendor/github.com/tidwall/sjson/LICENSE | 21 + .../vendor/github.com/tidwall/sjson/README.md | 278 +++++++++ cmd/ponzu/vendor/github.com/tidwall/sjson/logo.png | Bin 0 -> 16874 bytes cmd/ponzu/vendor/github.com/tidwall/sjson/sjson.go | 653 +++++++++++++++++++++ content/doc.go | 4 +- management/editor/editor.go | 20 +- 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 +- 29 files changed, 2300 insertions(+), 246 deletions(-) create mode 100644 addons/github.com/bosssauce/reference/LICENSE create mode 100644 addons/github.com/bosssauce/reference/README.md create mode 100644 addons/github.com/bosssauce/reference/reference.go delete mode 100644 addons/github.com/ponzu-cms/addons/reference/LICENSE delete mode 100644 addons/github.com/ponzu-cms/addons/reference/README.md delete mode 100644 addons/github.com/ponzu-cms/addons/reference/reference.go create mode 100644 cmd/ponzu/vendor/github.com/tidwall/sjson/.travis.yml create mode 100644 cmd/ponzu/vendor/github.com/tidwall/sjson/LICENSE create mode 100644 cmd/ponzu/vendor/github.com/tidwall/sjson/README.md create mode 100644 cmd/ponzu/vendor/github.com/tidwall/sjson/logo.png create mode 100644 cmd/ponzu/vendor/github.com/tidwall/sjson/sjson.go create mode 100644 system/addon/addon.go create mode 100644 system/addon/manager.go create mode 100644 system/db/addon.go diff --git a/addons/github.com/bosssauce/reference/LICENSE b/addons/github.com/bosssauce/reference/LICENSE new file mode 100644 index 0000000..720d6cd --- /dev/null +++ b/addons/github.com/bosssauce/reference/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2016 Boss Sauce Creative, LLC. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/addons/github.com/bosssauce/reference/README.md b/addons/github.com/bosssauce/reference/README.md new file mode 100644 index 0000000..57f008c --- /dev/null +++ b/addons/github.com/bosssauce/reference/README.md @@ -0,0 +1,3 @@ +# Reference + +A Ponzu addon to embed a reference to a content type from within another content type in the CMS. diff --git a/addons/github.com/bosssauce/reference/reference.go b/addons/github.com/bosssauce/reference/reference.go new file mode 100644 index 0000000..9918f36 --- /dev/null +++ b/addons/github.com/bosssauce/reference/reference.go @@ -0,0 +1,150 @@ +// Package reference is a Ponzu addon to enable content editors to create +// references to other content types which are stored as query strings within +// the referencer's content DB +package reference + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "log" + "strings" + + "github.com/ponzu-cms/ponzu/management/editor" + "github.com/ponzu-cms/ponzu/system/addon" +) + +// Select returns the []byte of a HTML element plus internal with a label. +// It also includes repeat controllers (+ / -) so the element can be +// dynamically multiplied or reduced. +// IMPORTANT: +// The `fieldName` argument will cause a panic if it is not exactly the string +// form of the struct field that this editor input is representing +func SelectRepeater(fieldName string, p interface{}, attrs map[string]string, contentType, tmplString string) []byte { + scope := editor.TagNameFromStructField(fieldName, p) + html := bytes.Buffer{} + _, err := html.WriteString(``) + if err != nil { + log.Println("Error writing HTML string to SelectRepeater buffer") + return nil + } + + if _, ok := attrs["class"]; ok { + attrs["class"] += " browser-default" + } else { + attrs["class"] = "browser-default" + } + + // find the field values in p to determine if an option is pre-selected + fieldVals := editor.ValueFromStructField(fieldName, p) + vals := strings.Split(fieldVals, "__ponzu") + + options, err := encodeDataToOptions(contentType, tmplString) + if err != nil { + log.Println("Error encoding data to options for", contentType, err) + return nil + } + + for _, val := range vals { + sel := editor.NewElement("select", attrs["label"], fieldName, p, attrs) + var opts []*editor.Element + + // provide a call to action for the select element + cta := &editor.Element{ + TagName: "option", + Attrs: map[string]string{"disabled": "true", "selected": "true"}, + Data: "Select an option...", + ViewBuf: &bytes.Buffer{}, + } + + // provide a selection reset (will store empty string in db) + reset := &editor.Element{ + TagName: "option", + Attrs: map[string]string{"value": ""}, + Data: "None", + ViewBuf: &bytes.Buffer{}, + } + + opts = append(opts, cta, reset) + + for k, v := range options { + optAttrs := map[string]string{"value": k} + if k == val { + optAttrs["selected"] = "true" + } + opt := &editor.Element{ + TagName: "option", + Attrs: optAttrs, + Data: v, + ViewBuf: &bytes.Buffer{}, + } + + opts = append(opts, opt) + } + + _, err := html.Write(editor.DOMElementWithChildrenSelect(sel, opts)) + if err != nil { + log.Println("Error writing DOMElementWithChildrenSelect to SelectRepeater buffer") + return nil + } + } + + _, err = html.WriteString("") + if err != nil { + log.Println("Error writing HTML string to SelectRepeater buffer") + return nil + } + + return append(html.Bytes(), editor.RepeatController(fieldName, p, "select", ".input-field")...) +} + +func encodeDataToOptions(contentType, tmplString string) (map[string]string, error) { + // encode all content type from db into options map + // options in form of map["?type=&id="]t.String() + options := make(map[string]string) + + var all map[string]interface{} + j := addon.ContentAll(contentType) + + err := json.Unmarshal(j, &all) + if err != nil { + return nil, err + } + + // make template for option html display + tmpl := template.Must(template.New(contentType).Parse(tmplString)) + + // make data something usable to iterate over and assign options + data := all["data"].([]interface{}) + + for i := range data { + item := data[i].(map[string]interface{}) + k := fmt.Sprintf("?type=%s&id=%.0f", contentType, item["id"].(float64)) + v := &bytes.Buffer{} + err := tmpl.Execute(v, item) + if err != nil { + return nil, fmt.Errorf( + "Error executing template for reference of %s: %s", + contentType, err.Error()) + } + + options[k] = v.String() + } + + return options, nil +} diff --git a/addons/github.com/ponzu-cms/addons/reference/LICENSE b/addons/github.com/ponzu-cms/addons/reference/LICENSE deleted file mode 100644 index 720d6cd..0000000 --- a/addons/github.com/ponzu-cms/addons/reference/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2016 Boss Sauce Creative, LLC. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/addons/github.com/ponzu-cms/addons/reference/README.md b/addons/github.com/ponzu-cms/addons/reference/README.md deleted file mode 100644 index 57f008c..0000000 --- a/addons/github.com/ponzu-cms/addons/reference/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Reference - -A Ponzu addon to embed a reference to a content type from within another content type in the CMS. diff --git a/addons/github.com/ponzu-cms/addons/reference/reference.go b/addons/github.com/ponzu-cms/addons/reference/reference.go deleted file mode 100644 index 9918f36..0000000 --- a/addons/github.com/ponzu-cms/addons/reference/reference.go +++ /dev/null @@ -1,150 +0,0 @@ -// Package reference is a Ponzu addon to enable content editors to create -// references to other content types which are stored as query strings within -// the referencer's content DB -package reference - -import ( - "bytes" - "encoding/json" - "fmt" - "html/template" - "log" - "strings" - - "github.com/ponzu-cms/ponzu/management/editor" - "github.com/ponzu-cms/ponzu/system/addon" -) - -// Select returns the []byte of a HTML element plus internal with a label. -// It also includes repeat controllers (+ / -) so the element can be -// dynamically multiplied or reduced. -// IMPORTANT: -// The `fieldName` argument will cause a panic if it is not exactly the string -// form of the struct field that this editor input is representing -func SelectRepeater(fieldName string, p interface{}, attrs map[string]string, contentType, tmplString string) []byte { - scope := editor.TagNameFromStructField(fieldName, p) - html := bytes.Buffer{} - _, err := html.WriteString(``) - if err != nil { - log.Println("Error writing HTML string to SelectRepeater buffer") - return nil - } - - if _, ok := attrs["class"]; ok { - attrs["class"] += " browser-default" - } else { - attrs["class"] = "browser-default" - } - - // find the field values in p to determine if an option is pre-selected - fieldVals := editor.ValueFromStructField(fieldName, p) - vals := strings.Split(fieldVals, "__ponzu") - - options, err := encodeDataToOptions(contentType, tmplString) - if err != nil { - log.Println("Error encoding data to options for", contentType, err) - return nil - } - - for _, val := range vals { - sel := editor.NewElement("select", attrs["label"], fieldName, p, attrs) - var opts []*editor.Element - - // provide a call to action for the select element - cta := &editor.Element{ - TagName: "option", - Attrs: map[string]string{"disabled": "true", "selected": "true"}, - Data: "Select an option...", - ViewBuf: &bytes.Buffer{}, - } - - // provide a selection reset (will store empty string in db) - reset := &editor.Element{ - TagName: "option", - Attrs: map[string]string{"value": ""}, - Data: "None", - ViewBuf: &bytes.Buffer{}, - } - - opts = append(opts, cta, reset) - - for k, v := range options { - optAttrs := map[string]string{"value": k} - if k == val { - optAttrs["selected"] = "true" - } - opt := &editor.Element{ - TagName: "option", - Attrs: optAttrs, - Data: v, - ViewBuf: &bytes.Buffer{}, - } - - opts = append(opts, opt) - } - - _, err := html.Write(editor.DOMElementWithChildrenSelect(sel, opts)) - if err != nil { - log.Println("Error writing DOMElementWithChildrenSelect to SelectRepeater buffer") - return nil - } - } - - _, err = html.WriteString("") - if err != nil { - log.Println("Error writing HTML string to SelectRepeater buffer") - return nil - } - - return append(html.Bytes(), editor.RepeatController(fieldName, p, "select", ".input-field")...) -} - -func encodeDataToOptions(contentType, tmplString string) (map[string]string, error) { - // encode all content type from db into options map - // options in form of map["?type=&id="]t.String() - options := make(map[string]string) - - var all map[string]interface{} - j := addon.ContentAll(contentType) - - err := json.Unmarshal(j, &all) - if err != nil { - return nil, err - } - - // make template for option html display - tmpl := template.Must(template.New(contentType).Parse(tmplString)) - - // make data something usable to iterate over and assign options - data := all["data"].([]interface{}) - - for i := range data { - item := data[i].(map[string]interface{}) - k := fmt.Sprintf("?type=%s&id=%.0f", contentType, item["id"].(float64)) - v := &bytes.Buffer{} - err := tmpl.Execute(v, item) - if err != nil { - return nil, fmt.Errorf( - "Error executing template for reference of %s: %s", - contentType, err.Error()) - } - - options[k] = v.String() - } - - return options, nil -} diff --git a/cmd/ponzu/contentType.tmpl b/cmd/ponzu/contentType.tmpl index 7bf6f06..c16cfd6 100644 --- a/cmd/ponzu/contentType.tmpl +++ b/cmd/ponzu/contentType.tmpl @@ -9,14 +9,13 @@ import ( type {{ .Name }} struct { item.Item - editor editor.Editor {{ range .Fields }}{{ .Name }} {{ .TypeName }} `json:"{{ .JSONName }}"` {{ end }} } -// MarshalEditor writes a buffer of html to edit a {{ .Name }} -// partially implements editor.Editable +// MarshalEditor writes a buffer of html to edit a {{ .Name }} within the CMS +// and implements editor.Editable func ({{ .Initial }} *{{ .Name }}) MarshalEditor() ([]byte, error) { view, err := editor.Form({{ .Initial }}, // Take note that the first argument to these Input-like functions @@ -41,9 +40,4 @@ func ({{ .Initial }} *{{ .Name }}) MarshalEditor() ([]byte, error) { func init() { item.Types["{{ .Name }}"] = func() interface{} { return new({{ .Name }}) } -} - -// Editor is a buffer of bytes for the Form function to write input views -// partially implements editor.Editable -func ({{ .Initial }} *{{ .Name }}) Editor() *editor.Editor { return &{{ .Initial }}.editor } - +} \ No newline at end of file diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index 6ac6dad..90ad613 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -15,7 +15,6 @@ import ( "github.com/ponzu-cms/ponzu/system/db" "github.com/ponzu-cms/ponzu/system/tls" - // import registers content types _ "github.com/ponzu-cms/ponzu/content" ) diff --git a/cmd/ponzu/vendor/github.com/tidwall/sjson/.travis.yml b/cmd/ponzu/vendor/github.com/tidwall/sjson/.travis.yml new file mode 100644 index 0000000..4f2ee4d --- /dev/null +++ b/cmd/ponzu/vendor/github.com/tidwall/sjson/.travis.yml @@ -0,0 +1 @@ +language: go diff --git a/cmd/ponzu/vendor/github.com/tidwall/sjson/LICENSE b/cmd/ponzu/vendor/github.com/tidwall/sjson/LICENSE new file mode 100644 index 0000000..89593c7 --- /dev/null +++ b/cmd/ponzu/vendor/github.com/tidwall/sjson/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/cmd/ponzu/vendor/github.com/tidwall/sjson/README.md b/cmd/ponzu/vendor/github.com/tidwall/sjson/README.md new file mode 100644 index 0000000..1a7c5c4 --- /dev/null +++ b/cmd/ponzu/vendor/github.com/tidwall/sjson/README.md @@ -0,0 +1,278 @@ +

+SJSON +
+Build Status +GoDoc +

+ +

set a json value quickly

+ +SJSON is a Go package that provides a [very fast](#performance) and simple way to set a value in a json document. The purpose for this library is to provide efficient json updating for the [SummitDB](https://github.com/tidwall/summitdb) project. +For quickly retrieving json values check out [GJSON](https://github.com/tidwall/gjson). + +For a command line interface check out [JSONed](https://github.com/tidwall/jsoned). + +Getting Started +=============== + +Installing +---------- + +To start using SJSON, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/sjson +``` + +This will retrieve the library. + +Set a value +----------- +Set sets the value for the specified path. +A path is in dot syntax, such as "name.last" or "age". +This function expects that the json is well-formed and validated. +Invalid json will not panic, but it may return back unexpected results. +Invalid paths may return an error. + +```go +package main + +import "github.com/tidwall/sjson" + +const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` + +func main() { + value, _ := sjson.Set(json, "name.last", "Anderson") + println(value) +} +``` + +This will print: + +```json +{"name":{"first":"Janet","last":"Anderson"},"age":47} +``` + +Path syntax +----------- + +A path is a series of keys separated by a dot. +The dot and colon characters can be escaped with '\'. + +```json +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"first": "James", "last": "Murphy"}, + {"first": "Roger", "last": "Craig"} + ] +} +``` +``` +"name.last" >> "Anderson" +"age" >> 37 +"children.1" >> "Alex" +"friends.1.last" >> "Craig" +``` + +The `-1` key can be used to append a value to an existing array: + +``` +"children.-1" >> appends a new value to the end of the children array +``` + +Normally number keys are used to modify arrays, but it's possible to force a numeric object key by using the colon character: + +```json +{ + "users":{ + "2313":{"name":"Sara"}, + "7839":{"name":"Andy"} + } +} +``` + +A colon path would look like: + +``` +"users.:2313.name" >> "Sara" +``` + +Supported types +--------------- + +Pretty much any type is supported: + +```go +sjson.Set(`{"key":true}`, "key", nil) +sjson.Set(`{"key":true}`, "key", false) +sjson.Set(`{"key":true}`, "key", 1) +sjson.Set(`{"key":true}`, "key", 10.5) +sjson.Set(`{"key":true}`, "key", "hello") +sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"}) +``` + +When a type is not recognized, SJSON will fallback to the `encoding/json` Marshaller. + + +Examples +-------- + +Set a value from empty document: +```go +value, _ := sjson.Set("", "name", "Tom") +println(value) + +// Output: +// {"name":"Tom"} +``` + +Set a nested value from empty document: +```go +value, _ := sjson.Set("", "name.last", "Anderson") +println(value) + +// Output: +// {"name":{"last":"Anderson"}} +``` + +Set a new value: +```go +value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara") +println(value) + +// Output: +// {"name":{"first":"Sara","last":"Anderson"}} +``` + +Update an existing value: +```go +value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith") +println(value) + +// Output: +// {"name":{"last":"Smith"}} +``` + +Set a new array value: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol","Sara"] +``` + +Append an array value by using the `-1` key in a path: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol","Sara"] +``` + +Append an array value that is past the end: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol",null,null,"Sara"] +``` + +Delete a value: +```go +value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first") +println(value) + +// Output: +// {"name":{"last":"Anderson"}} +``` + +Delete an array value: +```go +value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1") +println(value) + +// Output: +// {"friends":["Andy"]} +``` + +Delete the last array value: +```go +value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1") +println(value) + +// Output: +// {"friends":["Andy"]} +``` + +## Performance + +Benchmarks of SJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), +[ffjson](https://github.com/pquerna/ffjson), +[EasyJSON](https://github.com/mailru/easyjson), +and [Gabs](https://github.com/Jeffail/gabs) + +``` +Benchmark_SJSON-8 3000000 805 ns/op 1077 B/op 3 allocs/op +Benchmark_SJSON_ReplaceInPlace-8 3000000 449 ns/op 0 B/op 0 allocs/op +Benchmark_JSON_Map-8 300000 21236 ns/op 6392 B/op 150 allocs/op +Benchmark_JSON_Struct-8 300000 14691 ns/op 1789 B/op 24 allocs/op +Benchmark_Gabs-8 300000 21311 ns/op 6752 B/op 150 allocs/op +Benchmark_FFJSON-8 300000 17673 ns/op 3589 B/op 47 allocs/op +Benchmark_EasyJSON-8 1500000 3119 ns/op 1061 B/op 13 allocs/op +``` + +JSON document used: + +```json +{ + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } +} +``` + +Each operation was rotated though one of the following search paths: + +``` +widget.window.name +widget.image.hOffset +widget.text.onMouseUp +``` + +*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7.* + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +SJSON source code is available under the MIT [License](/LICENSE). diff --git a/cmd/ponzu/vendor/github.com/tidwall/sjson/logo.png b/cmd/ponzu/vendor/github.com/tidwall/sjson/logo.png new file mode 100644 index 0000000..b5aa257 Binary files /dev/null and b/cmd/ponzu/vendor/github.com/tidwall/sjson/logo.png differ diff --git a/cmd/ponzu/vendor/github.com/tidwall/sjson/sjson.go b/cmd/ponzu/vendor/github.com/tidwall/sjson/sjson.go new file mode 100644 index 0000000..7f1d358 --- /dev/null +++ b/cmd/ponzu/vendor/github.com/tidwall/sjson/sjson.go @@ -0,0 +1,653 @@ +// Package sjson provides setting json values. +package sjson + +import ( + jsongo "encoding/json" + "reflect" + "strconv" + "unsafe" + + "github.com/tidwall/gjson" +) + +type errorType struct { + msg string +} + +func (err *errorType) Error() string { + return err.msg +} + +// Options represents additional options for the Set and Delete functions. +type Options struct { + // Optimistic is a hint that the value likely exists which + // allows for the sjson to perform a fast-track search and replace. + Optimistic bool + // ReplaceInPlace is a hint to replace the input json rather than + // allocate a new json byte slice. When this field is specified + // the input json will not longer be valid and it should not be used + // In the case when the destination slice doesn't have enough free + // bytes to replace the data in place, a new bytes slice will be + // created under the hood. + // The Optimistic flag must be set to true and the input must be a + // byte slice in order to use this field. + ReplaceInPlace bool +} + +type pathResult struct { + part string // current key part + path string // remaining path + force bool // force a string key + more bool // there is more path to parse +} + +func parsePath(path string) (pathResult, error) { + var r pathResult + if len(path) > 0 && path[0] == ':' { + r.force = true + path = path[1:] + } + for i := 0; i < len(path); i++ { + if path[i] == '.' { + r.part = path[:i] + r.path = path[i+1:] + r.more = true + return r, nil + } + if path[i] == '*' || path[i] == '?' { + return r, &errorType{"wildcard characters not allowed in path"} + } else if path[i] == '#' { + return r, &errorType{"array access character not allowed in path"} + } + if path[i] == '\\' { + // go into escape mode. this is a slower path that + // strips off the escape character from the part. + epart := []byte(path[:i]) + i++ + if i < len(path) { + epart = append(epart, path[i]) + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + if i < len(path) { + epart = append(epart, path[i]) + } + continue + } else if path[i] == '.' { + r.part = string(epart) + r.path = path[i+1:] + r.more = true + return r, nil + } else if path[i] == '*' || path[i] == '?' { + return r, &errorType{ + "wildcard characters not allowed in path"} + } else if path[i] == '#' { + return r, &errorType{ + "array access character not allowed in path"} + } + epart = append(epart, path[i]) + } + } + // append the last part + r.part = string(epart) + return r, nil + } + } + r.part = path + return r, nil +} + +func mustMarshalString(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' { + return true + } + } + return false +} + +// appendStringify makes a json string and appends to buf. +func appendStringify(buf []byte, s string) []byte { + if mustMarshalString(s) { + b, _ := jsongo.Marshal(s) + return append(buf, b...) + } + buf = append(buf, '"') + buf = append(buf, s...) + buf = append(buf, '"') + return buf +} + +// appendBuild builds a json block from a json path. +func appendBuild(buf []byte, array bool, paths []pathResult, raw string, + stringify bool) []byte { + if !array { + buf = appendStringify(buf, paths[0].part) + buf = append(buf, ':') + } + if len(paths) > 1 { + n, numeric := atoui(paths[1]) + if numeric || (!paths[1].force && paths[1].part == "-1") { + buf = append(buf, '[') + buf = appendRepeat(buf, "null,", n) + buf = appendBuild(buf, true, paths[1:], raw, stringify) + buf = append(buf, ']') + } else { + buf = append(buf, '{') + buf = appendBuild(buf, false, paths[1:], raw, stringify) + buf = append(buf, '}') + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + return buf +} + +// atoui does a rip conversion of string -> unigned int. +func atoui(r pathResult) (n int, ok bool) { + if r.force { + return 0, false + } + for i := 0; i < len(r.part); i++ { + if r.part[i] < '0' || r.part[i] > '9' { + return 0, false + } + n = n*10 + int(r.part[i]-'0') + } + return n, true +} + +// appendRepeat repeats string "n" times and appends to buf. +func appendRepeat(buf []byte, s string, n int) []byte { + for i := 0; i < n; i++ { + buf = append(buf, s...) + } + return buf +} + +// trim does a rip trim +func trim(s string) string { + for len(s) > 0 { + if s[0] <= ' ' { + s = s[1:] + continue + } + break + } + for len(s) > 0 { + if s[len(s)-1] <= ' ' { + s = s[:len(s)-1] + continue + } + break + } + return s +} + +// deleteTailItem deletes the previous key or comma. +func deleteTailItem(buf []byte) ([]byte, bool) { +loop: + for i := len(buf) - 1; i >= 0; i-- { + // look for either a ',',':','[' + switch buf[i] { + case '[': + return buf, true + case ',': + return buf[:i], false + case ':': + // delete tail string + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + if i >= 0 && i == '\\' { + i-- + continue + } + for ; i >= 0; i-- { + // look for either a ',','{' + switch buf[i] { + case '{': + return buf[:i+1], true + case ',': + return buf[:i], false + } + } + } + } + break + } + } + break loop + } + } + return buf, false +} + +var errNoChange = &errorType{"no change"} + +func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, + stringify, del bool) ([]byte, error) { + var err error + var res gjson.Result + var found bool + if del { + if paths[0].part == "-1" && !paths[0].force { + res = gjson.Get(jstr, "#") + if res.Int() > 0 { + res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10)) + found = true + } + } + } + if !found { + res = gjson.Get(jstr, paths[0].part) + } + if res.Index > 0 { + if len(paths) > 1 { + buf = append(buf, jstr[:res.Index]...) + buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, + stringify, del) + if err != nil { + return nil, err + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + buf = append(buf, jstr[:res.Index]...) + var exidx int // additional forward stripping + if del { + var delNextComma bool + buf, delNextComma = deleteTailItem(buf) + if delNextComma { + i, j := res.Index+len(res.Raw), 0 + for ; i < len(jstr); i, j = i+1, j+1 { + if jstr[i] <= ' ' { + continue + } + if jstr[i] == ',' { + exidx = j + 1 + } + break + } + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...) + return buf, nil + } + if del { + return nil, errNoChange + } + n, numeric := atoui(paths[0]) + isempty := true + for i := 0; i < len(jstr); i++ { + if jstr[i] > ' ' { + isempty = false + break + } + } + if isempty { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + } + jsres := gjson.Parse(jstr) + if jsres.Type != gjson.JSON { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + jsres = gjson.Parse(jstr) + } + var comma bool + for i := 1; i < len(jsres.Raw); i++ { + if jsres.Raw[i] <= ' ' { + continue + } + if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' { + break + } + comma = true + break + } + switch jsres.Raw[0] { + default: + return nil, &errorType{"json must be an object or array"} + case '{': + buf = append(buf, '{') + buf = appendBuild(buf, false, paths, raw, stringify) + if comma { + buf = append(buf, ',') + } + buf = append(buf, jsres.Raw[1:]...) + return buf, nil + case '[': + var appendit bool + if !numeric { + if paths[0].part == "-1" && !paths[0].force { + appendit = true + } else { + return nil, &errorType{ + "cannot set array element for non-numeric key '" + + paths[0].part + "'"} + } + } + if appendit { + njson := trim(jsres.Raw) + if njson[len(njson)-1] == ']' { + njson = njson[:len(njson)-1] + } + buf = append(buf, njson...) + if comma { + buf = append(buf, ',') + } + + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } + buf = append(buf, '[') + ress := jsres.Array() + for i := 0; i < len(ress); i++ { + if i > 0 { + buf = append(buf, ',') + } + buf = append(buf, ress[i].Raw...) + } + if len(ress) == 0 { + buf = appendRepeat(buf, "null,", n-len(ress)) + } else { + buf = appendRepeat(buf, ",null", n-len(ress)) + if comma { + buf = append(buf, ',') + } + } + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } +} + +func isOptimisticPath(path string) bool { + for i := 0; i < len(path); i++ { + if path[i] < '.' || path[i] > 'z' { + return false + } + if path[i] > '9' && path[i] < 'A' { + return false + } + if path[i] > 'z' { + return false + } + } + return true +} + +func set(jstr, path, raw string, + stringify, del, optimistic, inplace bool) ([]byte, error) { + if path == "" { + return nil, &errorType{"path cannot be empty"} + } + if !del && optimistic && isOptimisticPath(path) { + res := gjson.Get(jstr, path) + if res.Exists() && res.Index > 0 { + sz := len(jstr) - len(res.Raw) + len(raw) + if stringify { + sz += 2 + } + if inplace && sz <= len(jstr) { + if !stringify || !mustMarshalString(raw) { + jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&jstr)) + jsonbh := reflect.SliceHeader{ + Data: jsonh.Data, Len: jsonh.Len, Cap: jsonh.Len} + jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh)) + if stringify { + jbytes[res.Index] = '"' + copy(jbytes[res.Index+1:], []byte(raw)) + jbytes[res.Index+1+len(raw)] = '"' + copy(jbytes[res.Index+1+len(raw)+1:], + jbytes[res.Index+len(res.Raw):]) + } else { + copy(jbytes[res.Index:], []byte(raw)) + copy(jbytes[res.Index+len(raw):], + jbytes[res.Index+len(res.Raw):]) + } + return jbytes[:sz], nil + } + return nil, nil + } + buf := make([]byte, 0, sz) + buf = append(buf, jstr[:res.Index]...) + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + } + // parse the path, make sure that it does not contain invalid characters + // such as '#', '?', '*' + paths := make([]pathResult, 0, 4) + r, err := parsePath(path) + if err != nil { + return nil, err + } + paths = append(paths, r) + for r.more { + if r, err = parsePath(r.path); err != nil { + return nil, err + } + paths = append(paths, r) + } + + njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del) + if err != nil { + return nil, err + } + return njson, nil +} + +// Set sets a json value for the specified path. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +// +// A path is a series of keys separated by a dot. +// +// { +// "name": {"first": "Tom", "last": "Anderson"}, +// "age":37, +// "children": ["Sara","Alex","Jack"], +// "friends": [ +// {"first": "James", "last": "Murphy"}, +// {"first": "Roger", "last": "Craig"} +// ] +// } +// "name.last" >> "Anderson" +// "age" >> 37 +// "children.1" >> "Alex" +// +func Set(json, path string, value interface{}) (string, error) { + return SetOptions(json, path, value, nil) +} + +// SetOptions sets a json value for the specified path with options. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +func SetOptions(json, path string, value interface{}, + opts *Options) (string, error) { + if opts != nil { + if opts.ReplaceInPlace { + // it's not safe to replace bytes in-place for strings + // copy the Options and set options.ReplaceInPlace to false. + nopts := *opts + opts = &nopts + opts.ReplaceInPlace = false + } + } + jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&json)) + jsonbh := reflect.SliceHeader{Data: jsonh.Data, Len: jsonh.Len} + jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) + res, err := SetBytesOptions(jsonb, path, value, opts) + return string(res), err +} + +// SetBytes sets a json value for the specified path. +// If working with bytes, this method preferred over +// Set(string(data), path, value) +func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { + return SetBytesOptions(json, path, value, nil) +} + +// SetBytesOptions sets a json value for the specified path with options. +// If working with bytes, this method preferred over +// SetOptions(string(data), path, value) +func SetBytesOptions(json []byte, path string, value interface{}, + opts *Options) ([]byte, error) { + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + jstr := *(*string)(unsafe.Pointer(&json)) + var res []byte + var err error + switch v := value.(type) { + default: + b, err := jsongo.Marshal(value) + if err != nil { + return nil, err + } + raw := *(*string)(unsafe.Pointer(&b)) + res, err = set(jstr, path, raw, false, false, optimistic, inplace) + case dtype: + res, err = set(jstr, path, "", false, true, optimistic, inplace) + case string: + res, err = set(jstr, path, v, true, false, optimistic, inplace) + case []byte: + raw := *(*string)(unsafe.Pointer(&v)) + res, err = set(jstr, path, raw, true, false, optimistic, inplace) + case bool: + if v { + res, err = set(jstr, path, "true", false, false, optimistic, inplace) + } else { + res, err = set(jstr, path, "false", false, false, optimistic, inplace) + } + case int8: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int16: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int32: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int64: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case uint8: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint16: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint32: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint64: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case float32: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + case float64: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + } + if err == errNoChange { + return json, nil + } + return res, err +} + +// SetRaw sets a raw json value for the specified path. +// This function works the same as Set except that the value is set as a +// raw block of json. This allows for setting premarshalled json objects. +func SetRaw(json, path, value string) (string, error) { + return SetRawOptions(json, path, value, nil) +} + +// SetRawOptions sets a raw json value for the specified path with options. +// This furnction works the same as SetOptions except that the value is set +// as a raw block of json. This allows for setting premarshalled json objects. +func SetRawOptions(json, path, value string, opts *Options) (string, error) { + var optimistic bool + if opts != nil { + optimistic = opts.Optimistic + } + res, err := set(json, path, value, false, false, optimistic, false) + if err == errNoChange { + return json, nil + } + return string(res), err +} + +// SetRawBytes sets a raw json value for the specified path. +// If working with bytes, this method preferred over +// SetRaw(string(data), path, value) +func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { + return SetRawBytesOptions(json, path, value, nil) +} + +// SetRawBytesOptions sets a raw json value for the specified path with options. +// If working with bytes, this method preferred over +// SetRawOptions(string(data), path, value, opts) +func SetRawBytesOptions(json []byte, path string, value []byte, + opts *Options) ([]byte, error) { + jstr := *(*string)(unsafe.Pointer(&json)) + vstr := *(*string)(unsafe.Pointer(&value)) + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + res, err := set(jstr, path, vstr, false, false, optimistic, inplace) + if err == errNoChange { + return json, nil + } + return res, err +} + +type dtype struct{} + +// Delete deletes a value from json for the specified path. +func Delete(json, path string) (string, error) { + return Set(json, path, dtype{}) +} + +// DeleteBytes deletes a value from json for the specified path. +func DeleteBytes(json []byte, path string) ([]byte, error) { + return SetBytes(json, path, dtype{}) +} diff --git a/content/doc.go b/content/doc.go index 3e15b11..8ae4c06 100644 --- a/content/doc.go +++ b/content/doc.go @@ -1,6 +1,6 @@ // Package content contains all user-supplied content which the system is to // manage. Generate content types by using the Ponzu command line tool 'ponzu' // by running `$ ponzu generate ` -// Note: doc.go file is required to build the Ponzu command since main.go -// imports content package to a blank identifier. +// Note: doc.go file is required to build the Ponzu command since some packages +// import content package to a blank identifier. package content diff --git a/management/editor/editor.go b/management/editor/editor.go index 511edb2..d1b169e 100644 --- a/management/editor/editor.go +++ b/management/editor/editor.go @@ -10,7 +10,6 @@ import ( // Editable ensures data is editable type Editable interface { - Editor() *Editor MarshalEditor() ([]byte, error) } @@ -36,10 +35,10 @@ type Field struct { // Form takes editable content and any number of Field funcs to describe the edit // page for any content struct added by a user func Form(post Editable, fields ...Field) ([]byte, error) { - editor := post.Editor() + editor := &Editor{} editor.ViewBuf = &bytes.Buffer{} - _, err := editor.ViewBuf.WriteString(`
`) + _, err := editor.ViewBuf.WriteString(`
`) if err != nil { log.Println("Error writing HTML string to editor Form buffer") return nil, err @@ -56,7 +55,7 @@ func Form(post Editable, fields ...Field) ([]byte, error) { } // content items with Item embedded have some default fields we need to render - _, err = editor.ViewBuf.WriteString(`
`) + _, err = editor.ViewBuf.WriteString(`
`) if err != nil { log.Println("Error writing HTML string to editor Form buffer") return nil, err @@ -149,12 +148,16 @@ func Form(post Editable, fields ...Field) ([]byte, error) { save = form.find('button.save-post'), del = form.find('button.delete-post'), external = form.find('.post-controls.external'), - id = form.find('input[name=id]'); + id = form.find('input[name=id]'), + timestamp = $('.__ponzu.content-only'), + slug = $('input[name=slug]'), + hiddenInput = $('input[type=hidden]'); // hide if this is a new post, or a non-post editor page if (id.val() === '-1' || form.attr('action') !== '/admin/edit') { del.hide(); external.hide(); + hiddenInput.parent().filter('.input-field').hide() } // hide approval if not on a pending content item @@ -162,6 +165,13 @@ func Form(post Editable, fields ...Field) ([]byte, error) { external.hide(); } + // no timestamp, slug or hidden input parents visible on addons + if (form.attr('action') === '/admin/addon') { + timestamp.hide(); + slug.parent().hide(); + hiddenInput.parent().filter('.input-field').hide() + } + save.on('click', function(e) { e.preventDefault(); 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
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(`