package api import ( "encoding/json" "errors" "io" "mime" "net/http" "net/url" "strconv" "strings" "git.golem15.com/golem15/summercms/modules/lagoon" ) // errBodyTooLarge is a body over the router's cap. var errBodyTooLarge = errors.New("feedback: request body too large") // multipartMemory is how much of a multipart body is kept in memory; the // rest spills to temporary files. The router's body cap bounds the total. const multipartMemory = 32 << 20 // decodeInput is Laravel's $request->all(): the query string overlaid by // the body. A JSON body decodes with numbers as json.Number; a form or // multipart body gives strings (the last value of a repeated key) and // lagoon.UploadedFile values for files. An unreadable body is an empty bag. func decodeInput(r *http.Request) (map[string]any, error) { out := map[string]any{} mergeValues(out, r.URL.Query()) if r.Body == nil || r.Method == http.MethodGet || r.Method == http.MethodHead { return out, nil } mediaType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) switch mediaType { case "multipart/form-data": if err := r.ParseMultipartForm(multipartMemory); err != nil { if isBodyTooLarge(err) { return nil, errBodyTooLarge } return out, nil } if r.MultipartForm != nil { mergeValues(out, r.MultipartForm.Value) for key, headers := range r.MultipartForm.File { if len(headers) > 0 { out[key] = lagoon.UploadedFileFromHeader(headers[len(headers)-1]) } } } return out, nil case "application/x-www-form-urlencoded": if err := r.ParseForm(); err != nil { if isBodyTooLarge(err) { return nil, errBodyTooLarge } return out, nil } mergeValues(out, r.PostForm) return out, nil default: raw, err := io.ReadAll(r.Body) if err != nil { if isBodyTooLarge(err) { return nil, errBodyTooLarge } return out, nil } if strings.TrimSpace(string(raw)) == "" { return out, nil } dec := json.NewDecoder(strings.NewReader(string(raw))) dec.UseNumber() var body map[string]any if err := dec.Decode(&body); err != nil { // Laravel's json() of an invalid body is an empty bag. return out, nil } for k, v := range body { out[k] = v } return out, nil } } func mergeValues(out map[string]any, values url.Values) { for key, vals := range values { if len(vals) > 0 { out[key] = vals[len(vals)-1] } } } func isBodyTooLarge(err error) bool { var mbe *http.MaxBytesError if errors.As(err, &mbe) { return true } // mime/multipart reports the reader error as text, not wrapped. return err != nil && strings.Contains(err.Error(), "request body too large") } // readInput decodes the request; a body over the router's cap is a 413. func readInput(w http.ResponseWriter, r *http.Request) (map[string]any, bool) { input, err := decodeInput(r) if errors.Is(err, errBodyTooLarge) { writeJSON(w, http.StatusRequestEntityTooLarge, map[string]any{"error": "Payload too large"}) return nil, false } if err != nil { input = map[string]any{} } return input, true } // inputString is $request->input($key) for a validated string field: nil // when absent or null, else the string as sent. func inputString(input map[string]any, key string) *string { switch v := input[key].(type) { case string: return &v case json.Number: s := v.String() return &s } return nil } // laravelTrue is (bool) of a value the boolean rule accepted (true, false, // 1, 0, "1", "0"): true for true, 1 and "1". func laravelTrue(v any) bool { switch t := v.(type) { case bool: return t case json.Number: f, err := t.Float64() return err == nil && f == 1 case float64: return t == 1 case int: return t == 1 case string: n, err := strconv.ParseFloat(strings.TrimSpace(t), 64) return err == nil && n == 1 } return false }