14287: Fix accepting boolean params via query string.
[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         "io"
10         "mime"
11         "net/http"
12         "strconv"
13         "strings"
14
15         "github.com/julienschmidt/httprouter"
16 )
17
18 // Parse req as an Arvados V1 API request and return the request
19 // parameters.
20 //
21 // If the request has a parameter whose name is attrsKey (e.g.,
22 // "collection"), it is renamed to "attrs".
23 func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[string]interface{}, error) {
24         err := req.ParseForm()
25         if err != nil {
26                 return nil, httpError(http.StatusBadRequest, err)
27         }
28         params := map[string]interface{}{}
29         for k, values := range req.Form {
30                 for _, v := range values {
31                         switch {
32                         case boolParams[k]:
33                                 params[k] = stringToBool(v)
34                         case v == "null" || v == "":
35                                 params[k] = nil
36                         case strings.HasPrefix(v, "["):
37                                 var j []interface{}
38                                 err := json.Unmarshal([]byte(v), &j)
39                                 if err != nil {
40                                         return nil, err
41                                 }
42                                 params[k] = j
43                         case strings.HasPrefix(v, "{"):
44                                 var j map[string]interface{}
45                                 err := json.Unmarshal([]byte(v), &j)
46                                 if err != nil {
47                                         return nil, err
48                                 }
49                                 params[k] = j
50                         case strings.HasPrefix(v, "\""):
51                                 var j string
52                                 err := json.Unmarshal([]byte(v), &j)
53                                 if err != nil {
54                                         return nil, err
55                                 }
56                                 params[k] = j
57                         case k == "limit" || k == "offset":
58                                 params[k], err = strconv.ParseInt(v, 10, 64)
59                                 if err != nil {
60                                         return nil, err
61                                 }
62                         default:
63                                 params[k] = v
64                         }
65                         // TODO: Need to accept "?foo[]=bar&foo[]=baz"
66                         // as foo=["bar","baz"]?
67                 }
68         }
69         if ct, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")); err != nil && ct == "application/json" {
70                 jsonParams := map[string]interface{}{}
71                 err := json.NewDecoder(req.Body).Decode(jsonParams)
72                 if err != nil {
73                         return nil, httpError(http.StatusBadRequest, err)
74                 }
75                 for k, v := range jsonParams {
76                         params[k] = v
77                 }
78                 if attrsKey != "" && params[attrsKey] == nil {
79                         // Copy top-level parameters from JSON request
80                         // body into params[attrsKey]. Some SDKs rely
81                         // on this Rails API feature; see
82                         // https://api.rubyonrails.org/v5.2.1/classes/ActionController/ParamsWrapper.html
83                         params[attrsKey] = jsonParams
84                 }
85         }
86
87         routeParams, _ := req.Context().Value(httprouter.ParamsKey).(httprouter.Params)
88         for _, p := range routeParams {
89                 params[p.Key] = p.Value
90         }
91
92         if v, ok := params[attrsKey]; ok && attrsKey != "" {
93                 params["attrs"] = v
94                 delete(params, attrsKey)
95         }
96         return params, nil
97 }
98
99 // Copy src to dst, using json as an intermediate format in order to
100 // invoke src's json-marshaling and dst's json-unmarshaling behaviors.
101 func (rtr *router) transcode(src interface{}, dst interface{}) error {
102         var errw error
103         pr, pw := io.Pipe()
104         go func() {
105                 defer pw.Close()
106                 errw = json.NewEncoder(pw).Encode(src)
107         }()
108         defer pr.Close()
109         err := json.NewDecoder(pr).Decode(dst)
110         if errw != nil {
111                 return errw
112         }
113         return err
114 }
115
116 var boolParams = map[string]bool{
117         "ensure_unique_name":   true,
118         "include_trash":        true,
119         "include_old_versions": true,
120 }
121
122 func stringToBool(s string) bool {
123         switch s {
124         case "", "false", "0":
125                 return false
126         default:
127                 return true
128         }
129 }