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  }