summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve Manuel <nilslice@gmail.com>2017-03-15 11:01:37 -0700
committerSteve Manuel <nilslice@gmail.com>2017-03-15 11:01:37 -0700
commit2f225325d7674a9a7594fc8cd787514e52ce0d77 (patch)
treebe5fdfcc42876b78a9ff3b88adf1aa2632d72432
parent07fe1b15899fa6439e587984d6183371f9a6877c (diff)
changing API for external client interaction. Externalable -> Createable, +Deleteable, changing Hookable interface methods to conform to pattern: BeforeAPI$ACTION, etc.
-rw-r--r--system/api/create.go230
-rw-r--r--system/api/delete.go140
2 files changed, 370 insertions, 0 deletions
diff --git a/system/api/create.go b/system/api/create.go
new file mode 100644
index 0000000..fbd00dc
--- /dev/null
+++ b/system/api/create.go
@@ -0,0 +1,230 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/ponzu-cms/ponzu/system/admin/upload"
+ "github.com/ponzu-cms/ponzu/system/db"
+ "github.com/ponzu-cms/ponzu/system/item"
+)
+
+// Createable accepts or rejects external POST requests to endpoints such as:
+// /api/content/create?type=Review
+type Createable interface {
+ // Create enables external clients to submit content of a specific type
+ Create(http.ResponseWriter, *http.Request) error
+}
+
+// Trustable allows external content to be auto-approved, meaning content sent
+// as an Createable will be stored in the public content bucket
+type Trustable interface {
+ AutoApprove(http.ResponseWriter, *http.Request) error
+}
+
+func externalContentHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
+ if err != nil {
+ log.Println("[Create] error:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ t := req.URL.Query().Get("type")
+ if t == "" {
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ p, found := item.Types[t]
+ if !found {
+ log.Println("[Create] attempt to submit unknown type:", t, "from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ post := p()
+
+ ext, ok := post.(Createable)
+ if !ok {
+ log.Println("[Create] rejected non-createable type:", t, "from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ ts := fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond))
+ req.PostForm.Set("timestamp", ts)
+ req.PostForm.Set("updated", ts)
+
+ urlPaths, err := upload.StoreFiles(req)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ for name, urlPath := range urlPaths {
+ req.PostForm.Set(name, urlPath)
+ }
+
+ // check for any multi-value fields (ex. checkbox fields)
+ // and correctly format for db storage. Essentially, we need
+ // fieldX.0: value1, fieldX.1: value2 => fieldX: []string{value1, value2}
+ fieldOrderValue := make(map[string]map[string][]string)
+ ordVal := make(map[string][]string)
+ for k, v := range req.PostForm {
+ if strings.Contains(k, ".") {
+ fo := strings.Split(k, ".")
+
+ // put the order and the field value into map
+ field := string(fo[0])
+ order := string(fo[1])
+ fieldOrderValue[field] = ordVal
+
+ // orderValue is 0:[?type=Thing&id=1]
+ orderValue := fieldOrderValue[field]
+ orderValue[order] = v
+ fieldOrderValue[field] = orderValue
+
+ // discard the post form value with name.N
+ req.PostForm.Del(k)
+ }
+
+ }
+
+ // add/set the key & value to the post form in order
+ for f, ov := range fieldOrderValue {
+ for i := 0; i < len(ov); i++ {
+ position := fmt.Sprintf("%d", i)
+ fieldValue := ov[position]
+
+ if req.PostForm.Get(f) == "" {
+ for i, fv := range fieldValue {
+ if i == 0 {
+ req.PostForm.Set(f, fv)
+ } else {
+ req.PostForm.Add(f, fv)
+ }
+ }
+ } else {
+ for _, fv := range fieldValue {
+ req.PostForm.Add(f, fv)
+ }
+ }
+ }
+ }
+
+ hook, ok := post.(item.Hookable)
+ if !ok {
+ log.Println("[Create] error: Type", t, "does not implement item.Hookable or embed item.Item.")
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ err = hook.BeforeAPICreate(res, req)
+ if err != nil {
+ log.Println("[Create] error calling BeforeAccept:", err)
+ return
+ }
+
+ err = ext.Create(res, req)
+ if err != nil {
+ log.Println("[Create] error calling Accept:", err)
+ return
+ }
+
+ err = hook.BeforeSave(res, req)
+ if err != nil {
+ log.Println("[Create] error calling BeforeSave:", err)
+ return
+ }
+
+ // set specifier for db bucket in case content is/isn't Trustable
+ var spec string
+
+ // check if the content is Trustable should be auto-approved, if so the
+ // content is immediately added to the public content API. If not, then it
+ // is added to a "pending" list, only visible to Admins in the CMS and only
+ // if the type implements editor.Mergable
+ trusted, ok := post.(Trustable)
+ if ok {
+ err := trusted.AutoApprove(res, req)
+ if err != nil {
+ log.Println("[Create] error calling AutoApprove:", err)
+ return
+ }
+ } else {
+ spec = "__pending"
+ }
+
+ id, err := db.SetContent(t+spec+":-1", req.PostForm)
+ if err != nil {
+ log.Println("[Create] error calling SetContent:", 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(res, req)
+ if err != nil {
+ log.Println("[Create] error calling AfterSave:", err)
+ return
+ }
+
+ err = hook.AfterAPICreate(res, req)
+ if err != nil {
+ log.Println("[Create] error calling AfterAccept:", err)
+ return
+ }
+
+ // create JSON response to send data back to client
+ var data map[string]interface{}
+ if spec != "" {
+ spec = strings.TrimPrefix(spec, "__")
+ data = map[string]interface{}{
+ "status": spec,
+ "type": t,
+ }
+ } else {
+ spec = "public"
+ data = map[string]interface{}{
+ "id": id,
+ "status": spec,
+ "type": t,
+ }
+ }
+
+ resp := map[string]interface{}{
+ "data": []map[string]interface{}{
+ data,
+ },
+ }
+
+ j, err := json.Marshal(resp)
+ if err != nil {
+ log.Println("[Create] error marshalling response to JSON:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "application/json")
+ _, err = res.Write(j)
+ if err != nil {
+ log.Println("[Create] error writing response:", err)
+ return
+ }
+
+}
diff --git a/system/api/delete.go b/system/api/delete.go
new file mode 100644
index 0000000..68b5f35
--- /dev/null
+++ b/system/api/delete.go
@@ -0,0 +1,140 @@
+package api
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+
+ "github.com/ponzu-cms/ponzu/system/db"
+ "github.com/ponzu-cms/ponzu/system/item"
+)
+
+// Deleteable accepts or rejects update POST requests to endpoints such as:
+// /api/content/delete?type=Review&id=1
+type Deleteable interface {
+ // Delete enables external clients to delete content of a specific type
+ Delete(http.ResponseWriter, *http.Request) error
+}
+
+func deleteContentHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
+ if err != nil {
+ log.Println("[Delete] error:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ t := req.URL.Query().Get("type")
+ if t == "" {
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ p, found := item.Types[t]
+ if !found {
+ log.Println("[Delete] attempt to delete content of unknown type:", t, "from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ id := req.URL.Query().Get("id")
+ if !db.IsValidID(id) {
+ log.Println("[Delete] attempt to delete content with missing or invalid id from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ post := p()
+
+ ext, ok := post.(Deleteable)
+ if !ok {
+ log.Println("[Delete] rejected non-deleteable type:", t, "from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ hook, ok := post.(item.Hookable)
+ if !ok {
+ log.Println("[Delete] error: Type", t, "does not implement item.Hookable or embed item.Item.")
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ err = hook.BeforeAPIDelete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling BeforeAPIDelete:", err)
+ if err == ErrNoAuth {
+ // BeforeAPIDelete can check user.IsValid(req) for auth
+ res.WriteHeader(http.StatusUnauthorized)
+ }
+ return
+ }
+
+ err = ext.Delete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling Delete:", err)
+ if err == ErrNoAuth {
+ // Delete can check user.IsValid(req) or other forms of validation for auth
+ res.WriteHeader(http.StatusUnauthorized)
+ }
+ return
+ }
+
+ err = hook.BeforeDelete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling BeforeSave:", err)
+ return
+ }
+
+ err = db.DeleteContent(t+":"+id, req.PostForm)
+ if err != nil {
+ log.Println("[Delete] error calling DeleteContent:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ err = hook.AfterDelete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling AfterDelete:", err)
+ return
+ }
+
+ err = hook.AfterAPIDelete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling AfterAPIDelete:", err)
+ return
+ }
+
+ // create JSON response to send data back to client
+ var data = map[string]interface{}{
+ "id": id,
+ "status": "deleted",
+ "type": t,
+ }
+
+ resp := map[string]interface{}{
+ "data": []map[string]interface{}{
+ data,
+ },
+ }
+
+ j, err := json.Marshal(resp)
+ if err != nil {
+ log.Println("[Delete] error marshalling response to JSON:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "application/json")
+ _, err = res.Write(j)
+ if err != nil {
+ log.Println("[Delete] error writing response:", err)
+ return
+ }
+
+}