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  }