github.com/bosssauce/ponzu@v0.11.1-0.20200102001432-9bc41b703131/system/api/create.go (about) 1 package api 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log" 8 "net/http" 9 "strings" 10 "time" 11 12 "github.com/ponzu-cms/ponzu/system/admin/upload" 13 "github.com/ponzu-cms/ponzu/system/db" 14 "github.com/ponzu-cms/ponzu/system/item" 15 16 "github.com/gorilla/schema" 17 ) 18 19 // Createable accepts or rejects external POST requests to endpoints such as: 20 // /api/content/create?type=Review 21 type Createable interface { 22 // Create enables external clients to submit content of a specific type 23 Create(http.ResponseWriter, *http.Request) error 24 } 25 26 // Trustable allows external content to be auto-approved, meaning content sent 27 // as an Createable will be stored in the public content bucket 28 type Trustable interface { 29 AutoApprove(http.ResponseWriter, *http.Request) error 30 } 31 32 func createContentHandler(res http.ResponseWriter, req *http.Request) { 33 if req.Method != http.MethodPost { 34 res.WriteHeader(http.StatusMethodNotAllowed) 35 return 36 } 37 38 err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB 39 if err != nil { 40 log.Println("[Create] error:", err) 41 res.WriteHeader(http.StatusInternalServerError) 42 return 43 } 44 45 t := req.URL.Query().Get("type") 46 if t == "" { 47 res.WriteHeader(http.StatusBadRequest) 48 return 49 } 50 51 p, found := item.Types[t] 52 if !found { 53 log.Println("[Create] attempt to submit unknown type:", t, "from:", req.RemoteAddr) 54 res.WriteHeader(http.StatusNotFound) 55 return 56 } 57 58 post := p() 59 60 ext, ok := post.(Createable) 61 if !ok { 62 log.Println("[Create] rejected non-createable type:", t, "from:", req.RemoteAddr) 63 res.WriteHeader(http.StatusBadRequest) 64 return 65 } 66 67 ts := fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond)) 68 req.PostForm.Set("timestamp", ts) 69 req.PostForm.Set("updated", ts) 70 71 urlPaths, err := upload.StoreFiles(req) 72 if err != nil { 73 log.Println(err) 74 res.WriteHeader(http.StatusInternalServerError) 75 return 76 } 77 78 for name, urlPath := range urlPaths { 79 req.PostForm.Set(name, urlPath) 80 } 81 82 // check for any multi-value fields (ex. checkbox fields) 83 // and correctly format for db storage. Essentially, we need 84 // fieldX.0: value1, fieldX.1: value2 => fieldX: []string{value1, value2} 85 fieldOrderValue := make(map[string]map[string][]string) 86 for k, v := range req.PostForm { 87 if strings.Contains(k, ".") { 88 fo := strings.Split(k, ".") 89 90 // put the order and the field value into map 91 field := string(fo[0]) 92 order := string(fo[1]) 93 if len(fieldOrderValue[field]) == 0 { 94 fieldOrderValue[field] = make(map[string][]string) 95 } 96 97 // orderValue is 0:[?type=Thing&id=1] 98 orderValue := fieldOrderValue[field] 99 orderValue[order] = v 100 fieldOrderValue[field] = orderValue 101 102 // discard the post form value with name.N 103 req.PostForm.Del(k) 104 } 105 106 } 107 108 // add/set the key & value to the post form in order 109 for f, ov := range fieldOrderValue { 110 for i := 0; i < len(ov); i++ { 111 position := fmt.Sprintf("%d", i) 112 fieldValue := ov[position] 113 114 if req.PostForm.Get(f) == "" { 115 for i, fv := range fieldValue { 116 if i == 0 { 117 req.PostForm.Set(f, fv) 118 } else { 119 req.PostForm.Add(f, fv) 120 } 121 } 122 } else { 123 for _, fv := range fieldValue { 124 req.PostForm.Add(f, fv) 125 } 126 } 127 } 128 } 129 130 hook, ok := post.(item.Hookable) 131 if !ok { 132 log.Println("[Create] error: Type", t, "does not implement item.Hookable or embed item.Item.") 133 res.WriteHeader(http.StatusBadRequest) 134 return 135 } 136 137 // Let's be nice and make a proper item for the Hookable methods 138 dec := schema.NewDecoder() 139 dec.IgnoreUnknownKeys(true) 140 dec.SetAliasTag("json") 141 err = dec.Decode(post, req.PostForm) 142 if err != nil { 143 log.Println("Error decoding post form for edit handler:", t, err) 144 res.WriteHeader(http.StatusBadRequest) 145 return 146 } 147 148 err = hook.BeforeAPICreate(res, req) 149 if err != nil { 150 log.Println("[Create] error calling BeforeCreate:", err) 151 return 152 } 153 154 err = ext.Create(res, req) 155 if err != nil { 156 log.Println("[Create] error calling Accept:", err) 157 return 158 } 159 160 err = hook.BeforeSave(res, req) 161 if err != nil { 162 log.Println("[Create] error calling BeforeSave:", err) 163 return 164 } 165 166 // set specifier for db bucket in case content is/isn't Trustable 167 var spec string 168 169 // check if the content is Trustable should be auto-approved, if so the 170 // content is immediately added to the public content API. If not, then it 171 // is added to a "pending" list, only visible to Admins in the CMS and only 172 // if the type implements editor.Mergable 173 trusted, ok := post.(Trustable) 174 if ok { 175 err := trusted.AutoApprove(res, req) 176 if err != nil { 177 log.Println("[Create] error calling AutoApprove:", err) 178 return 179 } 180 } else { 181 spec = "__pending" 182 } 183 184 id, err := db.SetContent(t+spec+":-1", req.PostForm) 185 if err != nil { 186 log.Println("[Create] error calling SetContent:", err) 187 res.WriteHeader(http.StatusInternalServerError) 188 return 189 } 190 191 // set the target in the context so user can get saved value from db in hook 192 ctx := context.WithValue(req.Context(), "target", fmt.Sprintf("%s:%d", t, id)) 193 req = req.WithContext(ctx) 194 195 err = hook.AfterSave(res, req) 196 if err != nil { 197 log.Println("[Create] error calling AfterSave:", err) 198 return 199 } 200 201 err = hook.AfterAPICreate(res, req) 202 if err != nil { 203 log.Println("[Create] error calling AfterAccept:", err) 204 return 205 } 206 207 // create JSON response to send data back to client 208 var data map[string]interface{} 209 if spec != "" { 210 spec = strings.TrimPrefix(spec, "__") 211 data = map[string]interface{}{ 212 "status": spec, 213 "type": t, 214 } 215 } else { 216 spec = "public" 217 data = map[string]interface{}{ 218 "id": id, 219 "status": spec, 220 "type": t, 221 } 222 } 223 224 resp := map[string]interface{}{ 225 "data": []map[string]interface{}{ 226 data, 227 }, 228 } 229 230 j, err := json.Marshal(resp) 231 if err != nil { 232 log.Println("[Create] error marshalling response to JSON:", err) 233 res.WriteHeader(http.StatusInternalServerError) 234 return 235 } 236 237 res.Header().Set("Content-Type", "application/json") 238 _, err = res.Write(j) 239 if err != nil { 240 log.Println("[Create] error writing response:", err) 241 return 242 } 243 244 }