Bump loofah from 2.2.3 to 2.3.1 in /apps/workbench
[arvados.git] / lib / controller / router / request.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package router
6
7 import (
8         "encoding/json"
9         "fmt"
10         "io"
11         "mime"
12         "net/http"
13         "strconv"
14         "strings"
15
16         "github.com/julienschmidt/httprouter"
17 )
18
19 // Parse req as an Arvados V1 API request and return the request
20 // parameters.
21 //
22 // If the request has a parameter whose name is attrsKey (e.g.,
23 // "collection"), it is renamed to "attrs".
24 func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[string]interface{}, error) {
25         err := req.ParseForm()
26         if err != nil {
27                 return nil, httpError(http.StatusBadRequest, err)
28         }
29         params := map[string]interface{}{}
30
31         // Load parameters from req.Form, which (after
32         // req.ParseForm()) includes the query string and -- when
33         // Content-Type is application/x-www-form-urlencoded -- the
34         // request body.
35         for k, values := range req.Form {
36                 // All of these form values arrive as strings, so we
37                 // need some type-guessing to accept non-string
38                 // inputs:
39                 //
40                 // Values for parameters that take ints (limit=1) or
41                 // bools (include_trash=1) are parsed accordingly.
42                 //
43                 // "null" and "" are nil.
44                 //
45                 // Values that look like JSON objects, arrays, or
46                 // strings are parsed as JSON.
47                 //
48                 // The rest are left as strings.
49                 for _, v := range values {
50                         switch {
51                         case intParams[k]:
52                                 params[k], err = strconv.ParseInt(v, 10, 64)
53                                 if err != nil {
54                                         return nil, err
55                                 }
56                         case boolParams[k]:
57                                 params[k] = stringToBool(v)
58                         case v == "null" || v == "":
59                                 params[k] = nil
60                         case strings.HasPrefix(v, "["):
61                                 var j []interface{}
62                                 err := json.Unmarshal([]byte(v), &j)
63                                 if err != nil {
64                                         return nil, err
65                                 }
66                                 params[k] = j
67                         case strings.HasPrefix(v, "{"):
68                                 var j map[string]interface{}
69                                 err := json.Unmarshal([]byte(v), &j)
70                                 if err != nil {
71                                         return nil, err
72                                 }
73                                 params[k] = j
74                         case strings.HasPrefix(v, "\""):
75                                 var j string
76                                 err := json.Unmarshal([]byte(v), &j)
77                                 if err != nil {
78                                         return nil, err
79                                 }
80                                 params[k] = j
81                         default:
82                                 params[k] = v
83                         }
84                         // TODO: Need to accept "?foo[]=bar&foo[]=baz"
85                         // as foo=["bar","baz"]?
86                 }
87         }
88
89         // Decode body as JSON if Content-Type request header is
90         // missing or application/json.
91         mt := req.Header.Get("Content-Type")
92         if ct, _, err := mime.ParseMediaType(mt); err != nil && mt != "" {
93                 return nil, fmt.Errorf("error parsing media type %q: %s", mt, err)
94         } else if (ct == "application/json" || mt == "") && req.ContentLength != 0 {
95                 jsonParams := map[string]interface{}{}
96                 err := json.NewDecoder(req.Body).Decode(&jsonParams)
97                 if err != nil {
98                         return nil, httpError(http.StatusBadRequest, err)
99                 }
100                 for k, v := range jsonParams {
101                         params[k] = v
102                 }
103                 if attrsKey != "" && params[attrsKey] == nil {
104                         // Copy top-level parameters from JSON request
105                         // body into params[attrsKey]. Some SDKs rely
106                         // on this Rails API feature; see
107                         // https://api.rubyonrails.org/v5.2.1/classes/ActionController/ParamsWrapper.html
108                         params[attrsKey] = jsonParams
109                 }
110         }
111
112         routeParams, _ := req.Context().Value(httprouter.ParamsKey).(httprouter.Params)
113         for _, p := range routeParams {
114                 params[p.Key] = p.Value
115         }
116
117         if v, ok := params[attrsKey]; ok && attrsKey != "" {
118                 params["attrs"] = v
119                 delete(params, attrsKey)
120         }
121
122         if order, ok := params["order"].(string); ok {
123                 // We must accept strings ("foo, bar desc") and arrays
124                 // (["foo", "bar desc"]) because RailsAPI does.
125                 // Convert to an array here before trying to unmarshal
126                 // into options structs.
127                 if order == "" {
128                         delete(params, "order")
129                 } else {
130                         params["order"] = strings.Split(order, ",")
131                 }
132         }
133
134         return params, nil
135 }
136
137 // Copy src to dst, using json as an intermediate format in order to
138 // invoke src's json-marshaling and dst's json-unmarshaling behaviors.
139 func (rtr *router) transcode(src interface{}, dst interface{}) error {
140         var errw error
141         pr, pw := io.Pipe()
142         go func() {
143                 defer pw.Close()
144                 errw = json.NewEncoder(pw).Encode(src)
145         }()
146         defer pr.Close()
147         err := json.NewDecoder(pr).Decode(dst)
148         if errw != nil {
149                 return errw
150         }
151         return err
152 }
153
154 var intParams = map[string]bool{
155         "limit":  true,
156         "offset": true,
157 }
158
159 var boolParams = map[string]bool{
160         "distinct":             true,
161         "ensure_unique_name":   true,
162         "include_trash":        true,
163         "include_old_versions": true,
164 }
165
166 func stringToBool(s string) bool {
167         switch s {
168         case "", "false", "0":
169                 return false
170         default:
171                 return true
172         }
173 }