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