1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
16 "github.com/gorilla/mux"
19 func guessAndParse(k, v string) (interface{}, error) {
20 // All of these form values arrive as strings, so we need some
21 // type-guessing to accept non-string inputs:
23 // Values for parameters that take ints (limit=1) or bools
24 // (include_trash=1) are parsed accordingly.
26 // "null" and "" are nil.
28 // Values that look like JSON objects, arrays, or strings are
31 // The rest are left as strings.
34 return strconv.ParseInt(v, 10, 64)
36 return stringToBool(v), nil
37 case v == "null" || v == "":
39 case strings.HasPrefix(v, "["):
41 err := json.Unmarshal([]byte(v), &j)
43 case strings.HasPrefix(v, "{"):
44 var j map[string]interface{}
45 err := json.Unmarshal([]byte(v), &j)
47 case strings.HasPrefix(v, "\""):
49 err := json.Unmarshal([]byte(v), &j)
54 // TODO: Need to accept "?foo[]=bar&foo[]=baz" as
58 // Return a map of incoming HTTP request parameters. Also load
59 // parameters into opts, unless opts is nil.
61 // If the request has a parameter whose name is attrsKey (e.g.,
62 // "collection"), it is renamed to "attrs".
63 func (rtr *router) loadRequestParams(req *http.Request, attrsKey string, opts interface{}) (map[string]interface{}, error) {
64 // Here we call ParseForm and ParseMultipartForm explicitly
65 // (even though ParseMultipartForm calls ParseForm if
66 // necessary) to ensure we catch errors encountered in
67 // ParseForm. In the non-multipart-form case,
68 // ParseMultipartForm returns ErrNotMultipart and hides the
70 err := req.ParseForm()
72 err = req.ParseMultipartForm(int64(rtr.config.MaxRequestSize))
73 if err == http.ErrNotMultipart {
78 if err.Error() == "http: request body too large" {
79 return nil, httpError(http.StatusRequestEntityTooLarge, err)
81 return nil, httpError(http.StatusBadRequest, err)
84 params := map[string]interface{}{}
86 // Load parameters from req.Form, which (after
87 // req.ParseForm()) includes the query string and -- when
88 // Content-Type is application/x-www-form-urlencoded -- the
90 for k, values := range req.Form {
91 for _, v := range values {
92 params[k], err = guessAndParse(k, v)
99 // Decode body as JSON if Content-Type request header is
100 // missing or application/json.
101 mt := req.Header.Get("Content-Type")
102 if ct, _, err := mime.ParseMediaType(mt); err != nil && mt != "" {
103 return nil, fmt.Errorf("error parsing media type %q: %s", mt, err)
104 } else if (ct == "application/json" || mt == "") && req.ContentLength != 0 {
105 jsonParams := map[string]interface{}{}
106 err := json.NewDecoder(req.Body).Decode(&jsonParams)
108 return nil, httpError(http.StatusBadRequest, err)
110 for k, v := range jsonParams {
111 switch v := v.(type) {
113 // The Ruby "arv" cli tool sends a
114 // JSON-encode params map with
115 // JSON-encoded values.
116 dec, err := guessAndParse(k, v)
126 if attrsKey != "" && params[attrsKey] == nil {
127 // Copy top-level parameters from JSON request
128 // body into params[attrsKey]. Some SDKs rely
129 // on this Rails API feature; see
130 // https://api.rubyonrails.org/v5.2.1/classes/ActionController/ParamsWrapper.html
131 params[attrsKey] = jsonParams
135 for k, v := range mux.Vars(req) {
139 if v, ok := params[attrsKey]; ok && attrsKey != "" {
141 delete(params, attrsKey)
144 for _, paramname := range []string{"include", "order"} {
145 // We must accept strings ("foo, bar desc") and arrays
146 // (["foo", "bar desc"]) because RailsAPI does.
147 // Convert to an array here before trying to unmarshal
148 // into options structs.
149 if val, ok := params[paramname].(string); ok {
151 delete(params, paramname)
153 params[paramname] = strings.Split(val, ",")
159 // Load all path, query, and form params into opts.
160 err = rtr.transcode(params, opts)
162 return nil, fmt.Errorf("transcode: %w", err)
165 // Special case: if opts has Method or Header fields, load the
166 // request method/header.
167 err = rtr.transcode(struct {
170 }{req.Method, req.Header}, opts)
172 return nil, fmt.Errorf("transcode: %w", err)
179 // Copy src to dst, using json as an intermediate format in order to
180 // invoke src's json-marshaling and dst's json-unmarshaling behaviors.
181 func (rtr *router) transcode(src interface{}, dst interface{}) error {
186 errw = json.NewEncoder(pw).Encode(src)
189 err := json.NewDecoder(pr).Decode(dst)
196 var intParams = map[string]bool{
201 var boolParams = map[string]bool{
203 "ensure_unique_name": true,
204 "include_trash": true,
205 "include_old_versions": true,
206 "redirect_to_new_user": true,
207 "send_notification_email": true,
208 "bypass_federation": true,
210 "exclude_home_project": true,
214 func stringToBool(s string) bool {
216 case "", "false", "0":