diff options
author | Steve Manuel <nilslice@gmail.com> | 2017-03-15 11:01:37 -0700 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2017-03-15 11:01:37 -0700 |
commit | 2f225325d7674a9a7594fc8cd787514e52ce0d77 (patch) | |
tree | be5fdfcc42876b78a9ff3b88adf1aa2632d72432 | |
parent | 07fe1b15899fa6439e587984d6183371f9a6877c (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.go | 230 | ||||
-rw-r--r-- | system/api/delete.go | 140 |
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 + } + +} |