summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/github.com/bosssauce/reference/LICENSE (renamed from addons/github.com/ponzu-cms/addons/reference/LICENSE)0
-rw-r--r--addons/github.com/bosssauce/reference/README.md (renamed from addons/github.com/ponzu-cms/addons/reference/README.md)0
-rw-r--r--addons/github.com/bosssauce/reference/reference.go (renamed from addons/github.com/ponzu-cms/addons/reference/reference.go)0
-rw-r--r--cmd/ponzu/contentType.tmpl12
-rw-r--r--cmd/ponzu/main.go1
-rw-r--r--cmd/ponzu/vendor/github.com/tidwall/sjson/.travis.yml1
-rw-r--r--cmd/ponzu/vendor/github.com/tidwall/sjson/LICENSE21
-rw-r--r--cmd/ponzu/vendor/github.com/tidwall/sjson/README.md278
-rw-r--r--cmd/ponzu/vendor/github.com/tidwall/sjson/logo.pngbin0 -> 16874 bytes
-rw-r--r--cmd/ponzu/vendor/github.com/tidwall/sjson/sjson.go653
-rw-r--r--content/doc.go4
-rw-r--r--management/editor/editor.go20
-rw-r--r--system/addon/addon.go234
-rw-r--r--system/addon/manager.go117
-rw-r--r--system/admin/admin.go3
-rw-r--r--system/admin/config/config.go12
-rw-r--r--system/admin/handlers.go584
-rw-r--r--system/admin/server.go3
-rw-r--r--system/admin/static/dashboard/css/admin.css17
-rw-r--r--system/admin/upload/upload.go2
-rw-r--r--system/api/external.go7
-rw-r--r--system/api/handlers.go2
-rw-r--r--system/db/addon.go151
-rw-r--r--system/db/content.go29
-rw-r--r--system/db/init.go7
-rw-r--r--system/item/types.go24
26 files changed, 2118 insertions, 64 deletions
diff --git a/addons/github.com/ponzu-cms/addons/reference/LICENSE b/addons/github.com/bosssauce/reference/LICENSE
index 720d6cd..720d6cd 100644
--- a/addons/github.com/ponzu-cms/addons/reference/LICENSE
+++ b/addons/github.com/bosssauce/reference/LICENSE
diff --git a/addons/github.com/ponzu-cms/addons/reference/README.md b/addons/github.com/bosssauce/reference/README.md
index 57f008c..57f008c 100644
--- a/addons/github.com/ponzu-cms/addons/reference/README.md
+++ b/addons/github.com/bosssauce/reference/README.md
diff --git a/addons/github.com/ponzu-cms/addons/reference/reference.go b/addons/github.com/bosssauce/reference/reference.go
index 9918f36..9918f36 100644
--- a/addons/github.com/ponzu-cms/addons/reference/reference.go
+++ b/addons/github.com/bosssauce/reference/reference.go
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 @@
+<p align="center">
+<img
+ src="logo.png"
+ width="240" height="78" border="0" alt="SJSON">
+<br>
+<a href="https://travis-ci.org/tidwall/sjson"><img src="https://img.shields.io/travis/tidwall/sjson.svg?style=flat-square" alt="Build Status"></a>
+<a href="https://godoc.org/github.com/tidwall/sjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
+</p>
+
+<p align="center">set a json value quickly</a></p>
+
+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
--- /dev/null
+++ b/cmd/ponzu/vendor/github.com/tidwall/sjson/logo.png
Binary files 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 <contentName> <fieldName:type...>`
-// 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(`<table><tbody class="row"><tr class="col s8"><td>`)
+ _, err := editor.ViewBuf.WriteString(`<table><tbody class="row"><tr class="col s8 editor-fields"><td class="col s12">`)
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(`<tr class="col s4 default-fields"><td>`)
+ _, err = editor.ViewBuf.WriteString(`<tr class="col s4 default-fields"><td class="col s12">`)
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 = `<input type="hidden" name="%s" value="%s"/>`
+
+const managerHTML = `
+<div class="card editor">
+ <form method="post" action="/admin/addon" enctype="multipart/form-data">
+ <div class="card-content">
+ <div class="card-title">{{ .AddonName }}</div>
+ </div>
+ {{ .DefaultInputs }}
+ {{ .Editor }}
+ </form>
+</div>
+`
+
+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 = `
<div class="card-title">System</div>
<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/configure/users"><i class="tiny left material-icons">supervisor_account</i>Admin Users</a></li>
+ <li><a class="col s12" href="/admin/addons"><i class="tiny left material-icons">settings_input_svideo</i>Addons</a></li>
</div>
</ul>
</div>
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(`<div class="card"><form action="/admin/configure" method="post">`)
+ open := []byte(`
+ <div class="card">
+ <div class="card-content">
+ <div class="card-title">System Configuration</div>
+ </div>
+ <form action="/admin/configure" method="post">
+ `)
close := []byte(`</form></div>`)
script := []byte(`
<script>
diff --git a/system/admin/handlers.go b/system/admin/handlers.go
index e7dabfe..c39fee4 100644
--- a/system/admin/handlers.go
+++ b/system/admin/handlers.go
@@ -2,6 +2,7 @@ package admin
import (
"bytes"
+ "context"
"encoding/base64"
"encoding/json"
"fmt"
@@ -13,12 +14,14 @@ import (
"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"
"github.com/ponzu-cms/ponzu/system/api"
"github.com/ponzu-cms/ponzu/system/db"
"github.com/ponzu-cms/ponzu/system/item"
+ "github.com/tidwall/gjson"
"github.com/gorilla/schema"
emailer "github.com/nilslice/email"
@@ -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)
@@ -1306,7 +1433,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
contentType, ok := item.Types[t]
if !ok {
- fmt.Fprintf(res, item.ErrTypeNotRegistered, t)
+ fmt.Fprintf(res, item.ErrTypeNotRegistered.Error(), t)
return
}
post := contentType()
@@ -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
}
@@ -1452,11 +1580,12 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
req.PostForm.Del(discardKey)
}
+ pt := t
if strings.Contains(t, "__") {
- t = strings.Split(t, "__")[0]
+ pt = strings.Split(t, "__")[0]
}
- p, ok := item.Types[t]
+ p, ok := item.Types[pt]
if !ok {
log.Println("Type", t, "is not a content type. Cannot edit or save.")
res.WriteHeader(http.StatusBadRequest)
@@ -1472,7 +1601,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
post := p()
hook, ok := post.(item.Hookable)
if !ok {
- log.Println("Type", t, "does not implement item.Hookable or embed item.Item.")
+ log.Println("Type", pt, "does not implement item.Hookable or embed item.Item.")
res.WriteHeader(http.StatusBadRequest)
errView, err := Error400()
if err != nil {
@@ -1509,6 +1638,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)
@@ -1526,7 +1659,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
host := req.URL.Host
path := req.URL.Path
sid := fmt.Sprintf("%d", id)
- redir := scheme + host + path + "?type=" + t + "&id=" + sid
+ redir := scheme + host + path + "?type=" + pt + "&id=" + sid
if req.URL.Query().Get("status") == "pending" {
redir += "&status=pending"
@@ -1549,6 +1682,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 +1868,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 +1927,372 @@ 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)
+ return
+ }
+ }
+
+ 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)
+ return
+ }
+
+ _, 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)
+ return
+ }
+
+ _, 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)
+ return
+ }
+
+ if html.Len() == 0 {
+ _, err := html.WriteString(`<p>No addons available.</p>`)
+ if err != nil {
+ log.Println("Error writing default addon html to admin view:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+ }
+
+ 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)
+ return
+ }
+
+ 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 := strings.ToLower(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 := Error400()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ http.Redirect(res, req, req.URL.String(), http.StatusFound)
+
+ default:
+ res.WriteHeader(http.StatusBadRequest)
+ errView, err := Error400()
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+}
+
+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:
+ // save req.Form
+ 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
+ }
+
+ name := req.FormValue("addon_name")
+ id := req.FormValue("addon_reverse_dns")
+
+ at, ok := addon.Types[id]
+ if !ok {
+ log.Println("Error: addon", name, "has no record in addon.Types map at", id)
+ res.WriteHeader(http.StatusBadRequest)
+ errView, err := Error400()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ // if Hookable, call BeforeSave prior to saving
+ h, ok := at().(item.Hookable)
+ if ok {
+ err := h.BeforeSave(req)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+ }
+
+ err = db.SetAddon(req.Form, at())
+ if err != nil {
+ log.Println("Error saving addon:", name, err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ http.Redirect(res, req, "/admin/addon?id="+id, http.StatusFound)
+
+ default:
+ res.WriteHeader(http.StatusBadRequest)
+ errView, err := Error405()
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+}
+
+func adminAddonListItem(data []byte) []byte {
+ id := gjson.GetBytes(data, "addon_reverse_dns").String()
+ status := gjson.GetBytes(data, "addon_status").String()
+ name := gjson.GetBytes(data, "addon_name").String()
+ author := gjson.GetBytes(data, "addon_author").String()
+ authorURL := gjson.GetBytes(data, "addon_author_url").String()
+ version := gjson.GetBytes(data, "addon_version").String()
+
+ var action string
+ var buttonClass string
+ if status != addon.StatusEnabled {
+ action = "Enable"
+ buttonClass = "green"
+ } else {
+ action = "Disable"
+ buttonClass = "red"
+ }
+
+ a := `
+ <li class="col s12">
+ <div class="row">
+ <div class="col s9">
+ <a class="addon-name" href="/admin/addon?id=` + id + `" alt="Configure '` + name + `'">` + name + `</a>
+ <span class="addon-meta addon-author">by: <a href="` + authorURL + `">` + author + `</a></span>
+ <span class="addon-meta addon-version">version: ` + version + `</span>
+ </div>
+
+ <div class="col s3">
+ <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>
+ </div>
+ </div>
+ </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/static/dashboard/css/admin.css b/system/admin/static/dashboard/css/admin.css
index ce533a4..a977afb 100644
--- a/system/admin/static/dashboard/css/admin.css
+++ b/system/admin/static/dashboard/css/admin.css
@@ -191,6 +191,23 @@ li:hover .quick-delete-post, li:hover .delete-user {
top: -10px;
}
+tr.default-fields, tr.editor-fields {
+ margin-top: 20px;
+ margin-bottom: 20px;
+}
+
+.addon-meta a {
+ color: #7e7e7e;
+ text-decoration: underline;
+ font-style: italic;
+}
+
+.addon-meta {
+ display: block;
+ color: #9e9e9e;
+ font-size: 12px;
+}
+
/* OVERRIDE Bootstrap + Materialize conflicts */
.iso-texteditor.input-field label {
color: #9e9e9e;
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 8b4a387..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.Error() == item.AllowHiddenItem {
+ if err == item.ErrAllowHiddenItem {
return false
}
diff --git a/system/db/addon.go b/system/db/addon.go
new file mode 100644
index 0000000..f4621fa
--- /dev/null
+++ b/system/db/addon.go
@@ -0,0 +1,151 @@
+package db
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net/url"
+
+ "github.com/boltdb/bolt"
+ "github.com/gorilla/schema"
+)
+
+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 []byte as json representation of an addon
+func Addon(key string) ([]byte, 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
+ }
+
+ return buf.Bytes(), nil
+}
+
+// SetAddon stores the values of an addon into the __addons bucket with a the
+// `addon_reverse_dns` field used as the key. `kind` is the interface{} type for
+// the provided addon (as in the result of calling addon.Types[id])
+func SetAddon(data url.Values, kind interface{}) error {
+ dec := schema.NewDecoder()
+ dec.IgnoreUnknownKeys(true)
+ dec.SetAliasTag("json")
+ err := dec.Decode(kind, data)
+
+ v, err := json.Marshal(kind)
+
+ 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), 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() [][]byte {
+ var all [][]byte
+
+ err := store.View(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte("__addons"))
+ err := b.ForEach(func(k, v []byte) error {
+ all = append(all, v)
+
+ 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
+
+ if store == nil {
+ Init()
+ }
+
+ err := store.Update(func(tx *bolt.Tx) error {
+ b, err := tx.CreateBucketIfNotExists([]byte("__addons"))
+ if err != nil {
+ return err
+ }
+ 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/content.go b/system/db/content.go
index dc4477f..b8d9cb8 100644
--- a/system/db/content.go
+++ b/system/db/content.go
@@ -432,29 +432,40 @@ func SortContent(namespace string) {
// sort posts
sort.Sort(posts)
+ // marshal posts to json
+ var bb [][]byte
+ for i := range posts {
+ j, err := json.Marshal(posts[i])
+ if err != nil {
+ // log error and kill sort so __sorted is not in invalid state
+ log.Println("Error marshal post to json in SortContent:", err)
+ return
+ }
+
+ bb = append(bb, j)
+ }
+
// store in <namespace>_sorted bucket, first delete existing
err := store.Update(func(tx *bolt.Tx) error {
bname := []byte(namespace + "__sorted")
err := tx.DeleteBucket(bname)
- if err != nil {
+ if err != nil && err != bolt.ErrBucketNotFound {
+ fmt.Println("Error in DeleteBucket")
return err
}
b, err := tx.CreateBucketIfNotExists(bname)
if err != nil {
+ fmt.Println("Error in CreateBucketIfNotExists")
return err
}
// encode to json and store as 'i:post.Time()':post
- for i := range posts {
- j, err := json.Marshal(posts[i])
- if err != nil {
- return err
- }
-
+ for i := range bb {
cid := fmt.Sprintf("%d:%d", i, posts[i].Time())
- err = b.Put([]byte(cid), j)
+ err = b.Put([]byte(cid), bb[i])
if err != nil {
+ fmt.Println("Error in Put")
return err
}
}
@@ -485,7 +496,7 @@ func postToJSON(ns string, data url.Values) ([]byte, error) {
// find the content type and decode values into it
t, ok := item.Types[ns]
if !ok {
- return nil, fmt.Errorf(item.ErrTypeNotRegistered, ns)
+ return nil, fmt.Errorf(item.ErrTypeNotRegistered.Error(), ns)
}
post := t()
diff --git a/system/db/init.go b/system/db/init.go
index bf93bc6..eaf6d76 100644
--- a/system/db/init.go
+++ b/system/db/init.go
@@ -24,6 +24,10 @@ func Close() {
// Init creates a db connection, initializes db with required info, sets secrets
func Init() {
+ if store != nil {
+ return
+ }
+
var err error
store, err = bolt.Open("system.db", 0666, nil)
if err != nil {
@@ -45,7 +49,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 {
@@ -90,7 +94,6 @@ func Init() {
SortContent(t)
}
}()
-
}
// SystemInitComplete checks if there is at least 1 admin user in the db which
diff --git a/system/item/types.go b/system/item/types.go
index b4b361b..bcae58a 100644
--- a/system/item/types.go
+++ b/system/item/types.go
@@ -1,8 +1,9 @@
package item
+import "errors"
+
const (
- // ErrTypeNotRegistered means content type isn't registered (not found in Types map)
- ErrTypeNotRegistered = `Error:
+ typeNotRegistered = `Error:
There is no type registered for %[1]s
Add this to the file which defines %[1]s{} in the 'content' package:
@@ -14,13 +15,22 @@ Add this to the file which defines %[1]s{} in the 'content' package:
`
+)
- // AllowHiddenItem should be used as an error to tell a caller of Hideable#Hide
+var (
+ // ErrTypeNotRegistered means content type isn't registered (not found in Types map)
+ ErrTypeNotRegistered = errors.New(typeNotRegistered)
+
+ // ErrAllowHiddenItem should be used as an error to tell a caller of Hideable#Hide
// that this type is hidden, but should be shown in a particular case, i.e.
// if requested by a valid admin or user
- AllowHiddenItem = `Allow hidden item`
+ ErrAllowHiddenItem = errors.New(`Allow hidden item`)
+
+ // Types is a map used to reference a type name to its actual Editable type
+ // mainly for lookups in /admin route based utilities
+ Types map[string]func() interface{}
)
-// Types is a map used to reference a type name to its actual Editable type
-// mainly for lookups in /admin route based utilities
-var Types = make(map[string]func() interface{})
+func init() {
+ Types = make(map[string]func() interface{})
+}