diff options
-rw-r--r-- | addons/reference/reference.go | 54 | ||||
-rw-r--r-- | cmd/ponzu/contentType.tmpl | 5 | ||||
-rw-r--r-- | cmd/ponzu/main.go | 12 | ||||
-rw-r--r-- | cmd/ponzu/options.go | 114 | ||||
-rw-r--r-- | content/doc.go | 6 | ||||
-rw-r--r-- | management/editor/editor.go | 6 | ||||
-rw-r--r-- | management/editor/elements.go | 2 | ||||
-rw-r--r-- | management/manager/manager.go | 10 | ||||
-rw-r--r-- | management/manager/process.go | 72 | ||||
-rw-r--r-- | system/addon/api.go | 47 | ||||
-rw-r--r-- | system/admin/admin.go | 12 | ||||
-rw-r--r-- | system/admin/config/config.go | 14 | ||||
-rw-r--r-- | system/admin/handlers.go | 56 | ||||
-rw-r--r-- | system/api/external.go | 8 | ||||
-rw-r--r-- | system/api/handlers.go | 10 | ||||
-rw-r--r-- | system/api/server.go | 4 | ||||
-rw-r--r-- | system/db/cache.go | 35 | ||||
-rw-r--r-- | system/db/config.go | 44 | ||||
-rw-r--r-- | system/db/content.go | 20 | ||||
-rw-r--r-- | system/db/init.go | 6 | ||||
-rw-r--r-- | system/item/item.go (renamed from content/item.go) | 73 | ||||
-rw-r--r-- | system/item/types.go (renamed from content/types.go) | 4 |
22 files changed, 388 insertions, 226 deletions
diff --git a/addons/reference/reference.go b/addons/reference/reference.go new file mode 100644 index 0000000..78e46eb --- /dev/null +++ b/addons/reference/reference.go @@ -0,0 +1,54 @@ +// 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" + + "github.com/bosssauce/ponzu/management/editor" + "github.com/bosssauce/ponzu/system/addon" +) + +// Select returns the []byte of a <select> HTML element plus internal <options> with a label. +// 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 Select(fieldName string, p interface{}, attrs map[string]string, contentType, tmplString string) []byte { + // decode all content type from db into options map + // map["?type=<contentType>&id=<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 + } + + // 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 { + log.Println("Error executing template for reference of:", contentType) + return nil + } + + options[k] = v.String() + } + + return editor.Select(fieldName, p, attrs, options) +} diff --git a/cmd/ponzu/contentType.tmpl b/cmd/ponzu/contentType.tmpl index af60e57..1804555 100644 --- a/cmd/ponzu/contentType.tmpl +++ b/cmd/ponzu/contentType.tmpl @@ -4,10 +4,11 @@ import ( "fmt" "github.com/bosssauce/ponzu/management/editor" + "github.com/bosssauce/ponzu/system/item" ) type {{ .Name }} struct { - Item + item.Item editor editor.Editor {{ range .Fields }}{{ .Name }} {{ .TypeName }} `json:"{{ .JSONName }}"` @@ -38,7 +39,7 @@ func ({{ .Initial }} *{{ .Name }}) MarshalEditor() ([]byte, error) { } func init() { - Types["{{ .Name }}"] = func() interface{} { return new({{ .Name }}) } + item.Types["{{ .Name }}"] = func() interface{} { return new({{ .Name }}) } } // Editor is a buffer of bytes for the Form function to write input views diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index ebcbbad..b2b5e75 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -15,6 +15,9 @@ import ( "github.com/bosssauce/ponzu/system/api/analytics" "github.com/bosssauce/ponzu/system/db" "github.com/bosssauce/ponzu/system/tls" + + // import registers content types + _ "github.com/bosssauce/ponzu/content" ) var year = fmt.Sprintf("%d", time.Now().Year()) @@ -291,7 +294,14 @@ func main() { tls.Enable() } - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) + // save the port the system is listening on so internal system can make + // HTTP api calls while in dev or production w/o adding more cli flags + err := db.PutConfig("http_port", fmt.Sprintf("%d", port)) + if err != nil { + log.Fatalln("System failed to save config. Please try to run again.") + } + + log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) case "": fmt.Println(usage) diff --git a/cmd/ponzu/options.go b/cmd/ponzu/options.go index 7c93130..9c2767f 100644 --- a/cmd/ponzu/options.go +++ b/cmd/ponzu/options.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -127,8 +126,8 @@ func createProjInDir(path string) error { } } - // create a 'vendor' directory in $path/cmd/ponzu and move 'content', - // 'management' and 'system' packages into it + // create an internal vendor directory in ./cmd/ponzu and move content, + // management and system packages into it err = vendorCorePackages(path) if err != nil { return err @@ -148,84 +147,121 @@ func vendorCorePackages(path string) error { vendorPath := filepath.Join(path, "cmd", "ponzu", "vendor", "github.com", "bosssauce", "ponzu") err := os.MkdirAll(vendorPath, os.ModeDir|os.ModePerm) if err != nil { - // TODO: rollback, remove ponzu project from path return err } + // // create a user content directory to be vendored + // contentPath := filepath.Join(path, "content") + // err = os.Mkdir(contentPath, os.ModeDir|os.ModePerm) + // if err != nil { + // return err + // } + dirs := []string{"content", "management", "system"} for _, dir := range dirs { err = os.Rename(filepath.Join(path, dir), filepath.Join(vendorPath, dir)) if err != nil { - // TODO: rollback, remove ponzu project from path return err } } - // create a user 'content' package + // create a user content directory at project root contentPath := filepath.Join(path, "content") err = os.Mkdir(contentPath, os.ModeDir|os.ModePerm) if err != nil { - // TODO: rollback, remove ponzu project from path return err } return nil } -func buildPonzuServer(args []string) error { - // copy all ./content .go files to $vendor/content - // check to see if any file exists, move on to next file if so, - // and report this conflict to user for them to fix & re-run build - pwd, err := os.Getwd() +func copyFile(src, dst string) error { + noRoot := strings.Split(src, string(filepath.Separator))[1:] + path := filepath.Join(noRoot...) + dstFile, err := os.Create(filepath.Join(dst, path)) + defer dstFile.Close() if err != nil { return err } - contentSrcPath := filepath.Join(pwd, "content") - contentDstPath := filepath.Join(pwd, "cmd", "ponzu", "vendor", "github.com", "bosssauce", "ponzu", "content") + srcFile, err := os.Open(src) + defer srcFile.Close() + if err != nil { + return err + } - srcFiles, err := ioutil.ReadDir(contentSrcPath) + _, err = io.Copy(dstFile, srcFile) if err != nil { return err } - var conflictFiles = []string{"item.go", "types.go"} - var mustRenameFiles = []string{} - for _, srcFileInfo := range srcFiles { - // check srcFile exists in contentDstPath - for _, conflict := range conflictFiles { - if srcFileInfo.Name() == conflict { - mustRenameFiles = append(mustRenameFiles, conflict) - continue - } - } + return nil +} - dstFile, err := os.Create(filepath.Join(contentDstPath, srcFileInfo.Name())) +func copyFilesWarnConflicts(srcDir, dstDir string, conflicts []string) error { + err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - srcFile, err := os.Open(filepath.Join(contentSrcPath, srcFileInfo.Name())) - if err != nil { - return err + if info.IsDir() { + if len(path) > len(srcDir) { + path = path[len(srcDir)+1:] + } + dir := filepath.Join(dstDir, path) + err := os.MkdirAll(dir, os.ModeDir|os.ModePerm) + if err != nil { + return err + } + + return nil + } + + for _, conflict := range conflicts { + if info.Name() == conflict { + fmt.Println("Ponzu couldn't fully build your project:") + fmt.Println("You must rename the following file, as it conflicts with Ponzu core:") + fmt.Println(path) + fmt.Println("") + fmt.Println("Once the files above have been renamed, run '$ ponzu build' to retry.") + return errors.New("Ponzu has very few internal conflicts, sorry for the inconvenience.") + } } - _, err = io.Copy(dstFile, srcFile) + err = copyFile(path, dstDir) if err != nil { return err } + + return nil + }) + if err != nil { + return err } - if len(mustRenameFiles) > 1 { - fmt.Println("Ponzu couldn't fully build your project:") - fmt.Println("Some of your files in the content directory exist in the vendored directory.") - fmt.Println("You must rename the following files, as they conflict with Ponzu core:") - for _, file := range mustRenameFiles { - fmt.Println(file) - } + return nil +} - fmt.Println("Once the files above have been renamed, run '$ ponzu build' to retry.") - return errors.New("Ponzu has very few internal conflicts, sorry for the inconvenience.") +func buildPonzuServer(args []string) error { + pwd, err := os.Getwd() + if err != nil { + return err + } + + // copy all ./content files to internal vendor directory + src := "content" + dst := filepath.Join("cmd", "ponzu", "vendor", "github.com", "bosssauce", "ponzu", "content") + err = copyFilesWarnConflicts(src, dst, []string{"doc.go"}) + if err != nil { + return err + } + + // copy all ./addons files & dirs to internal vendor directory + src = "addons" + dst = filepath.Join("cmd", "ponzu", "vendor") + err = copyFilesWarnConflicts(src, dst, nil) + if err != nil { + return err } // execute go build -o ponzu-cms cmd/ponzu/*.go diff --git a/content/doc.go b/content/doc.go new file mode 100644 index 0000000..3e15b11 --- /dev/null +++ b/content/doc.go @@ -0,0 +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. +package content diff --git a/management/editor/editor.go b/management/editor/editor.go index 6b55a38..7194c27 100644 --- a/management/editor/editor.go +++ b/management/editor/editor.go @@ -13,12 +13,6 @@ type Editable interface { MarshalEditor() ([]byte, error) } -// Sortable ensures data is sortable by time -type Sortable interface { - Time() int64 - Touch() int64 -} - // Mergeable allows external post content to be approved and published through // the public-facing API type Mergeable interface { diff --git a/management/editor/elements.go b/management/editor/elements.go index 4a8ae55..bb2cb3f 100644 --- a/management/editor/elements.go +++ b/management/editor/elements.go @@ -242,8 +242,6 @@ func Select(fieldName string, p interface{}, attrs, options map[string]string) [ // find the field value in p to determine if an option is pre-selected fieldVal := valueFromStructField(fieldName, p) - // may need to alloc a buffer, as we will probably loop through options - // and append the []byte from domElement() called for each option attrs["class"] = "browser-default" sel := newElement("select", attrs["label"], fieldName, p, attrs) var opts []*element diff --git a/management/manager/manager.go b/management/manager/manager.go index 989eb98..5eafb9a 100644 --- a/management/manager/manager.go +++ b/management/manager/manager.go @@ -5,8 +5,8 @@ import ( "fmt" "html/template" - "github.com/bosssauce/ponzu/content" "github.com/bosssauce/ponzu/management/editor" + "github.com/bosssauce/ponzu/system/item" uuid "github.com/satori/go.uuid" ) @@ -121,14 +121,14 @@ func Manage(e editor.Editable, typeName string) ([]byte, error) { return nil, fmt.Errorf("Couldn't marshal editor for content %s. %s", typeName, err.Error()) } - i, ok := e.(content.Identifiable) + i, ok := e.(item.Identifiable) if !ok { - return nil, fmt.Errorf("Content type %s does not implement content.Identifiable.", typeName) + return nil, fmt.Errorf("Content type %s does not implement item.Identifiable.", typeName) } - s, ok := e.(content.Sluggable) + s, ok := e.(item.Sluggable) if !ok { - return nil, fmt.Errorf("Content type %s does not implement content.Sluggable.", typeName) + return nil, fmt.Errorf("Content type %s does not implement item.Sluggable.", typeName) } m := manager{ diff --git a/management/manager/process.go b/management/manager/process.go deleted file mode 100644 index ad6da94..0000000 --- a/management/manager/process.go +++ /dev/null @@ -1,72 +0,0 @@ -package manager - -import ( - "regexp" - "strings" - "unicode" - - "github.com/bosssauce/ponzu/content" - - "golang.org/x/text/transform" - "golang.org/x/text/unicode/norm" -) - -// Slug returns a URL friendly string from the title of a post item -func Slug(i content.Identifiable) (string, error) { - // get the name of the post item - name := strings.TrimSpace(i.String()) - - // filter out non-alphanumeric character or non-whitespace - slug, err := stringToSlug(name) - if err != nil { - return "", err - } - - return slug, nil -} - -func isMn(r rune) bool { - return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks -} - -// modified version of: https://www.socketloop.com/tutorials/golang-format-strings-to-seo-friendly-url-example -func stringToSlug(s string) (string, error) { - src := []byte(strings.ToLower(s)) - - // convert all spaces to dash - rx := regexp.MustCompile("[[:space:]]") - src = rx.ReplaceAll(src, []byte("-")) - - // remove all blanks such as tab - rx = regexp.MustCompile("[[:blank:]]") - src = rx.ReplaceAll(src, []byte("")) - - rx = regexp.MustCompile("[!/:-@[-`{-~]") - src = rx.ReplaceAll(src, []byte("")) - - rx = regexp.MustCompile("/[^\x20-\x7F]/") - src = rx.ReplaceAll(src, []byte("")) - - rx = regexp.MustCompile("`&(amp;)?#?[a-z0-9]+;`i") - src = rx.ReplaceAll(src, []byte("-")) - - rx = regexp.MustCompile("`&([a-z])(acute|uml|circ|grave|ring|cedil|slash|tilde|caron|lig|quot|rsquo);`i") - src = rx.ReplaceAll(src, []byte("\\1")) - - rx = regexp.MustCompile("`[^a-z0-9]`i") - src = rx.ReplaceAll(src, []byte("-")) - - rx = regexp.MustCompile("`[-]+`") - src = rx.ReplaceAll(src, []byte("-")) - - str := strings.Replace(string(src), "'", "", -1) - str = strings.Replace(str, `"`, "", -1) - - t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) - slug, _, err := transform.String(t, str) - if err != nil { - return "", err - } - - return strings.TrimSpace(slug), nil -} diff --git a/system/addon/api.go b/system/addon/api.go new file mode 100644 index 0000000..4c3152b --- /dev/null +++ b/system/addon/api.go @@ -0,0 +1,47 @@ +// Package addon provides an API for Ponzu addons to interface with the system +package addon + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "net/http" + "time" + + "github.com/bosssauce/ponzu/system/db" +) + +// ContentAll retrives all items from the HTTP API within the provided namespace +func ContentAll(namespace string) []byte { + host := db.ConfigCache("domain") + port := db.ConfigCache("http_port") + endpoint := "http://%s:%s/api/contents?type=%s" + buf := []byte{} + r := bytes.NewReader(buf) + url := fmt.Sprintf(endpoint, host, port, namespace) + + req, err := http.NewRequest(http.MethodGet, url, r) + if err != nil { + log.Println("Error creating request for reference of:", namespace) + return nil + } + + c := http.Client{ + Timeout: time.Duration(time.Second * 5), + } + res, err := c.Do(req) + if err != nil { + log.Println("Error making HTTP request for reference of:", namespace) + return nil + } + defer res.Body.Close() + + j, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Println("Error reading request body for reference of:", namespace) + return nil + } + + return j +} diff --git a/system/admin/admin.go b/system/admin/admin.go index 9ddff84..78f607d 100644 --- a/system/admin/admin.go +++ b/system/admin/admin.go @@ -8,10 +8,10 @@ import ( "html/template" "net/http" - "github.com/bosssauce/ponzu/content" "github.com/bosssauce/ponzu/system/admin/user" "github.com/bosssauce/ponzu/system/api/analytics" "github.com/bosssauce/ponzu/system/db" + "github.com/bosssauce/ponzu/system/item" ) var startAdminHTML = `<!doctype html> @@ -104,7 +104,7 @@ func Admin(view []byte) ([]byte, error) { a := admin{ Logo: string(cfg), - Types: content.Types, + Types: item.Types, Subview: template.HTML(view), } @@ -169,17 +169,17 @@ var initAdminHTML = ` func Init() ([]byte, error) { html := startAdminHTML + initAdminHTML + endAdminHTML - cfg, err := db.Config("name") + name, err := db.Config("name") if err != nil { return nil, err } - if cfg == nil { - cfg = []byte("") + if name == nil { + name = []byte("") } a := admin{ - Logo: string(cfg), + Logo: string(name), } buf := &bytes.Buffer{} diff --git a/system/admin/config/config.go b/system/admin/config/config.go index b898b49..ab17425 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -1,24 +1,25 @@ package config import ( - "github.com/bosssauce/ponzu/content" "github.com/bosssauce/ponzu/management/editor" + "github.com/bosssauce/ponzu/system/item" ) -//Config represents the confirgurable options of the system +// Config represents the confirgurable options of the system type Config struct { - content.Item + item.Item editor editor.Editor Name string `json:"name"` Domain string `json:"domain"` + HTTPPort string `json:"http_port"` AdminEmail string `json:"admin_email"` ClientSecret string `json:"client_secret"` Etag string `json:"etag"` CacheInvalidate []string `json:"cache"` } -// String partially implements content.Identifiable and overrides Item's String() +// String partially implements item.Identifiable and overrides Item's String() func (c *Config) String() string { return c.Name } // Editor partially implements editor.Editable @@ -40,6 +41,11 @@ func (c *Config) MarshalEditor() ([]byte, error) { }), }, editor.Field{ + View: editor.Input("HTTPPort", c, map[string]string{ + "type": "hidden", + }), + }, + editor.Field{ View: editor.Input("AdminEmail", c, map[string]string{ "label": "Adminstrator Email (will be notified of internal system information)", }), diff --git a/system/admin/handlers.go b/system/admin/handlers.go index 1e6a26c..266535a 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -11,7 +11,6 @@ import ( "strings" "time" - "github.com/bosssauce/ponzu/content" "github.com/bosssauce/ponzu/management/editor" "github.com/bosssauce/ponzu/management/manager" "github.com/bosssauce/ponzu/system/admin/config" @@ -19,6 +18,7 @@ import ( "github.com/bosssauce/ponzu/system/admin/user" "github.com/bosssauce/ponzu/system/api" "github.com/bosssauce/ponzu/system/db" + "github.com/bosssauce/ponzu/system/item" "github.com/gorilla/schema" emailer "github.com/nilslice/email" @@ -71,6 +71,7 @@ func initHandler(res http.ResponseWriter, req *http.Request) { etag := db.NewEtag() req.Form.Set("etag", etag) + // create and save admin user email := strings.ToLower(req.FormValue("email")) password := req.FormValue("password") usr := user.NewUser(email, password) @@ -82,6 +83,10 @@ func initHandler(res http.ResponseWriter, req *http.Request) { return } + // set HTTP port which should be previously added to config cache + port := db.ConfigCache("http_port") + req.Form.Set("http_port", port) + // set initial user email as admin_email and make config req.Form.Set("admin_email", email) err = db.SetConfig(req.Form) @@ -754,7 +759,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { status := q.Get("status") - if _, ok := content.Types[t]; !ok { + if _, ok := item.Types[t]; !ok { res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { @@ -765,7 +770,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { return } - pt := content.Types[t]() + pt := item.Types[t]() p, ok := pt.(editor.Editable) if !ok { @@ -1048,17 +1053,17 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { // p is the asserted post as an Editable, t is the Type of the post. // specifier is passed to append a name to a namespace like __pending func adminPostListItem(e editor.Editable, typeName, status string) []byte { - s, ok := e.(editor.Sortable) + s, ok := e.(item.Sortable) if !ok { - log.Println("Content type", typeName, "doesn't implement editor.Sortable") - post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces. (editor.Sortable)</li>` + log.Println("Content type", typeName, "doesn't implement item.Sortable") + post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces. (item.Sortable)</li>` return []byte(post) } - i, ok := e.(content.Identifiable) + i, ok := e.(item.Identifiable) if !ok { - log.Println("Content type", typeName, "doesn't implement content.Identifiable") - post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces. (content.Identifiable)</li>` + log.Println("Content type", typeName, "doesn't implement item.Identifiable") + post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces. (item.Identifiable)</li>` return []byte(post) } @@ -1122,12 +1127,12 @@ func approveContentHandler(res http.ResponseWriter, req *http.Request) { t = strings.Split(t, "__")[0] } - post := content.Types[t]() + post := item.Types[t]() // run hooks - hook, ok := post.(content.Hookable) + hook, ok := post.(item.Hookable) if !ok { - log.Println("Type", t, "does not implement content.Hookable or embed content.Item.") + log.Println("Type", t, "does not implement item.Hookable or embed item.Item.") res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { @@ -1262,9 +1267,9 @@ func editHandler(res http.ResponseWriter, req *http.Request) { t := q.Get("type") status := q.Get("status") - contentType, ok := content.Types[t] + contentType, ok := item.Types[t] if !ok { - fmt.Fprintf(res, content.ErrTypeNotRegistered, t) + fmt.Fprintf(res, item.ErrTypeNotRegistered, t) return } post := contentType() @@ -1288,7 +1293,6 @@ func editHandler(res http.ResponseWriter, req *http.Request) { } if len(data) < 1 || data == nil { - fmt.Println(string(data)) res.WriteHeader(http.StatusNotFound) errView, err := Error404() if err != nil { @@ -1312,12 +1316,12 @@ func editHandler(res http.ResponseWriter, req *http.Request) { return } } else { - s, ok := post.(content.Identifiable) + item, ok := post.(item.Identifiable) if !ok { - log.Println("Content type", t, "doesn't implement editor.Identifiable") + log.Println("Content type", t, "doesn't implement item.Identifiable") return } - s.SetItemID(-1) + item.SetItemID(-1) } m, err := manager.Manage(post.(editor.Editable), t) @@ -1414,7 +1418,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) { t = strings.Split(t, "__")[0] } - p, ok := content.Types[t] + p, ok := item.Types[t] if !ok { log.Println("Type", t, "is not a content type. Cannot edit or save.") res.WriteHeader(http.StatusBadRequest) @@ -1428,9 +1432,9 @@ func editHandler(res http.ResponseWriter, req *http.Request) { } post := p() - hook, ok := post.(content.Hookable) + hook, ok := post.(item.Hookable) if !ok { - log.Println("Type", t, "does not implement content.Hookable or embed content.Item.") + log.Println("Type", t, "does not implement item.Hookable or embed item.Item.") res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { @@ -1525,9 +1529,9 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) { ct = spec[0] } - p, ok := content.Types[ct] + p, ok := item.Types[ct] if !ok { - log.Println("Type", t, "does not implement content.Hookable or embed content.Item.") + log.Println("Type", t, "does not implement item.Hookable or embed item.Item.") res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { @@ -1539,9 +1543,9 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) { } post := p() - hook, ok := post.(content.Hookable) + hook, ok := post.(item.Hookable) if !ok { - log.Println("Type", t, "does not implement content.Hookable or embed content.Item.") + log.Println("Type", t, "does not implement item.Hookable or embed item.Item.") res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { @@ -1656,7 +1660,7 @@ func searchHandler(res http.ResponseWriter, req *http.Request) { posts := db.ContentAll(t + specifier) b := &bytes.Buffer{} - p := content.Types[t]().(editor.Editable) + p := item.Types[t]().(editor.Editable) html := `<div class="col s9 card"> <div class="card-content"> diff --git a/system/api/external.go b/system/api/external.go index 0d1ea03..8112e29 100644 --- a/system/api/external.go +++ b/system/api/external.go @@ -7,9 +7,9 @@ import ( "strings" "time" - "github.com/bosssauce/ponzu/content" "github.com/bosssauce/ponzu/system/admin/upload" "github.com/bosssauce/ponzu/system/db" + "github.com/bosssauce/ponzu/system/item" ) // Externalable accepts or rejects external POST requests to endpoints such as: @@ -44,7 +44,7 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) { return } - p, found := content.Types[t] + p, found := item.Types[t] if !found { log.Println("[External] attempt to submit unknown type:", t, "from:", req.RemoteAddr) res.WriteHeader(http.StatusNotFound) @@ -105,9 +105,9 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) { return } - hook, ok := post.(content.Hookable) + hook, ok := post.(item.Hookable) if !ok { - log.Println("[External] error: Type", t, "does not implement content.Hookable or embed content.Item.") + log.Println("[External] error: Type", t, "does not implement item.Hookable or embed item.Item.") res.WriteHeader(http.StatusBadRequest) return } diff --git a/system/api/handlers.go b/system/api/handlers.go index 7a2073d..1e2f1e2 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -8,14 +8,14 @@ import ( "strconv" "strings" - "github.com/bosssauce/ponzu/content" "github.com/bosssauce/ponzu/system/api/analytics" "github.com/bosssauce/ponzu/system/db" + "github.com/bosssauce/ponzu/system/item" ) func typesHandler(res http.ResponseWriter, req *http.Request) { var types = []string{} - for t := range content.Types { + for t := range item.Types { types = append(types, string(t)) } @@ -36,7 +36,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { return } - if _, ok := content.Types[t]; !ok { + if _, ok := item.Types[t]; !ok { res.WriteHeader(http.StatusNotFound) return } @@ -98,7 +98,7 @@ func contentHandler(res http.ResponseWriter, req *http.Request) { return } - if _, ok := content.Types[t]; !ok { + if _, ok := item.Types[t]; !ok { res.WriteHeader(http.StatusNotFound) return } @@ -224,7 +224,7 @@ func SendJSON(res http.ResponseWriter, j map[string]interface{}) { sendData(res, data, 200) } -// CORS wraps a HandleFunc to response to OPTIONS requests properly +// CORS wraps a HandleFunc to respond to OPTIONS requests properly func CORS(next http.HandlerFunc) http.HandlerFunc { return db.CacheControl(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { if req.Method == http.MethodOptions { diff --git a/system/api/server.go b/system/api/server.go index 823ec16..f31a748 100644 --- a/system/api/server.go +++ b/system/api/server.go @@ -1,8 +1,6 @@ package api -import ( - "net/http" -) +import "net/http" // Run adds Handlers to default http listener for API func Run() { diff --git a/system/db/cache.go b/system/db/cache.go index 5f2dd03..30ecf5a 100644 --- a/system/db/cache.go +++ b/system/db/cache.go @@ -2,10 +2,8 @@ package db import ( "encoding/base64" - "encoding/json" "fmt" "net/http" - "net/url" "strings" "time" ) @@ -39,41 +37,10 @@ func NewEtag() string { // InvalidateCache sets a new Etag for http responses func InvalidateCache() error { - kv := make(map[string]interface{}) - - c, err := ConfigAll() + err := PutConfig("etag", NewEtag()) if err != nil { return err } - err = json.Unmarshal(c, &kv) - if err != nil { - return err - } - - kv["etag"] = NewEtag() - - data := make(url.Values) - for k, v := range kv { - switch v.(type) { - case string: - data.Set(k, v.(string)) - case []string: - vv := v.([]string) - for i := range vv { - if i == 0 { - data.Set(k, vv[i]) - } else { - data.Add(k, vv[i]) - } - } - } - } - - err = SetConfig(data) - if err != nil { - - } - return nil } diff --git a/system/db/config.go b/system/db/config.go index b5a07e4..4bbf29b 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -3,6 +3,7 @@ package db import ( "bytes" "encoding/json" + "fmt" "net/url" "strings" @@ -117,7 +118,50 @@ func ConfigAll() ([]byte, error) { return val.Bytes(), nil } +// PutConfig updates a single k/v in the config +func PutConfig(key string, value interface{}) error { + kv := make(map[string]interface{}) + + c, err := ConfigAll() + if err != nil { + return err + } + + err = json.Unmarshal(c, &kv) + if err != nil { + return err + } + + // set k/v from params to decoded map + kv[key] = value + + data := make(url.Values) + for k, v := range kv { + switch v.(type) { + case string: + data.Set(k, v.(string)) + + case []string: + vv := v.([]string) + for i := range vv { + data.Add(k, vv[i]) + } + + default: + data.Set(k, fmt.Sprintf("%v", v)) + } + } + + err = SetConfig(data) + if err != nil { + return err + } + + return nil +} + // ConfigCache is a in-memory cache of the Configs for quicker lookups +// 'key' is the JSON tag associated with the config field func ConfigCache(key string) string { return configCache.Get(key) } diff --git a/system/db/content.go b/system/db/content.go index 87b3e69..535b601 100644 --- a/system/db/content.go +++ b/system/db/content.go @@ -10,9 +10,7 @@ import ( "strconv" "strings" - "github.com/bosssauce/ponzu/content" - "github.com/bosssauce/ponzu/management/editor" - "github.com/bosssauce/ponzu/management/manager" + "github.com/bosssauce/ponzu/system/item" "github.com/boltdb/bolt" "github.com/gorilla/schema" @@ -305,7 +303,7 @@ func Query(namespace string, opts QueryOptions) (int, [][]byte) { // correct bad input rather than return nil or error // similar to default case for opts.Order switch below if opts.Count < 0 { - opts.Count = 0 + opts.Count = -1 } if opts.Offset < 0 { @@ -420,7 +418,7 @@ func SortContent(namespace string) { // decode each (json) into type to then sort for i := range all { j := all[i] - post := content.Types[namespace]() + post := item.Types[namespace]() err := json.Unmarshal(j, &post) if err != nil { @@ -428,7 +426,7 @@ func SortContent(namespace string) { return } - posts = append(posts, post.(editor.Sortable)) + posts = append(posts, post.(item.Sortable)) } // sort posts @@ -469,7 +467,7 @@ func SortContent(namespace string) { } -type sortableContent []editor.Sortable +type sortableContent []item.Sortable func (s sortableContent) Len() int { return len(s) @@ -485,9 +483,9 @@ func (s sortableContent) Swap(i, j int) { func postToJSON(ns string, data url.Values) ([]byte, error) { // find the content type and decode values into it - t, ok := content.Types[ns] + t, ok := item.Types[ns] if !ok { - return nil, fmt.Errorf(content.ErrTypeNotRegistered, ns) + return nil, fmt.Errorf(item.ErrTypeNotRegistered, ns) } post := t() @@ -502,7 +500,7 @@ func postToJSON(ns string, data url.Values) ([]byte, error) { // if the content has no slug, and has no specifier, create a slug, check it // for duplicates, and add it to our values if data.Get("slug") == "" && data.Get("__specifier") == "" { - slug, err := manager.Slug(post.(content.Identifiable)) + slug, err := item.Slug(post.(item.Identifiable)) if err != nil { return nil, err } @@ -512,7 +510,7 @@ func postToJSON(ns string, data url.Values) ([]byte, error) { return nil, err } - post.(content.Sluggable).SetSlug(slug) + post.(item.Sluggable).SetSlug(slug) data.Set("slug", slug) } diff --git a/system/db/init.go b/system/db/init.go index fa2c42e..9a0c3e5 100644 --- a/system/db/init.go +++ b/system/db/init.go @@ -4,8 +4,8 @@ import ( "encoding/json" "log" - "github.com/bosssauce/ponzu/content" "github.com/bosssauce/ponzu/system/admin/config" + "github.com/bosssauce/ponzu/system/item" "github.com/boltdb/bolt" "github.com/nilslice/jwt" @@ -32,7 +32,7 @@ func Init() { err = store.Update(func(tx *bolt.Tx) error { // initialize db with all content type buckets & sorted bucket for type - for t := range content.Types { + for t := range item.Types { _, err := tx.CreateBucketIfNotExists([]byte(t)) if err != nil { return err @@ -86,7 +86,7 @@ func Init() { } go func() { - for t := range content.Types { + for t := range item.Types { SortContent(t) } }() diff --git a/content/item.go b/system/item/item.go index eb79aa0..a813669 100644 --- a/content/item.go +++ b/system/item/item.go @@ -1,10 +1,15 @@ -package content +package item import ( "fmt" "net/http" + "regexp" + "strings" + "unicode" uuid "github.com/satori/go.uuid" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" ) // Sluggable makes a struct locatable by URL with it's own path @@ -27,6 +32,12 @@ type Identifiable interface { String() string } +// Sortable ensures data is sortable by time +type Sortable interface { + Time() int64 + Touch() int64 +} + // Hookable provides our user with an easy way to intercept or add functionality // to the different lifecycles/events a struct may encounter. Item implements // Hookable with no-ops so our user can override only whichever ones necessary. @@ -136,3 +147,63 @@ func (i Item) BeforeReject(req *http.Request) error { func (i Item) AfterReject(req *http.Request) error { return nil } + +// Slug returns a URL friendly string from the title of a post item +func Slug(i Identifiable) (string, error) { + // get the name of the post item + name := strings.TrimSpace(i.String()) + + // filter out non-alphanumeric character or non-whitespace + slug, err := stringToSlug(name) + if err != nil { + return "", err + } + + return slug, nil +} + +func isMn(r rune) bool { + return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks +} + +// modified version of: https://www.socketloop.com/tutorials/golang-format-strings-to-seo-friendly-url-example +func stringToSlug(s string) (string, error) { + src := []byte(strings.ToLower(s)) + + // convert all spaces to dash + rx := regexp.MustCompile("[[:space:]]") + src = rx.ReplaceAll(src, []byte("-")) + + // remove all blanks such as tab + rx = regexp.MustCompile("[[:blank:]]") + src = rx.ReplaceAll(src, []byte("")) + + rx = regexp.MustCompile("[!/:-@[-`{-~]") + src = rx.ReplaceAll(src, []byte("")) + + rx = regexp.MustCompile("/[^\x20-\x7F]/") + src = rx.ReplaceAll(src, []byte("")) + + rx = regexp.MustCompile("`&(amp;)?#?[a-z0-9]+;`i") + src = rx.ReplaceAll(src, []byte("-")) + + rx = regexp.MustCompile("`&([a-z])(acute|uml|circ|grave|ring|cedil|slash|tilde|caron|lig|quot|rsquo);`i") + src = rx.ReplaceAll(src, []byte("\\1")) + + rx = regexp.MustCompile("`[^a-z0-9]`i") + src = rx.ReplaceAll(src, []byte("-")) + + rx = regexp.MustCompile("`[-]+`") + src = rx.ReplaceAll(src, []byte("-")) + + str := strings.Replace(string(src), "'", "", -1) + str = strings.Replace(str, `"`, "", -1) + + t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) + slug, _, err := transform.String(t, str) + if err != nil { + return "", err + } + + return strings.TrimSpace(slug), nil +} diff --git a/content/types.go b/system/item/types.go index 696d589..33e9ced 100644 --- a/content/types.go +++ b/system/item/types.go @@ -1,4 +1,4 @@ -package content +package item const ( // ErrTypeNotRegistered means content type isn't registered (not found in Types map) @@ -9,7 +9,7 @@ Add this to the file which defines %[1]s{} in the 'content' package: func init() { - Types["%[1]s"] = func() interface{} { return new(%[1]s) } + item.Types["%[1]s"] = func() interface{} { return new(%[1]s) } } |