params.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. package webapi
  2. import (
  3. "context"
  4. "mime"
  5. "net/http"
  6. "strings"
  7. )
  8. // The Web API is method-agnostic: parameters may arrive on the query string or in a
  9. // form-encoded body, varying by client and by endpoint. The helpers here read either
  10. // location so a handler never has to care.
  11. const formContentType = "application/x-www-form-urlencoded"
  12. // binaryBodyKey marks a request whose body is a payload rather than form fields.
  13. type binaryBodyKey struct{}
  14. // WithBinaryBody marks a request body as a payload the parameter helpers must not
  15. // consume. expressions/upload POSTs a raw untyped image, and without the marker a
  16. // lookup that missed on the query string would hand it to ParseForm, which reads it
  17. // to EOF.
  18. func WithBinaryBody(next http.Handler) http.Handler {
  19. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  20. next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), binaryBodyKey{}, true)))
  21. })
  22. }
  23. func hasBinaryBody(r *http.Request) bool {
  24. marked, _ := r.Context().Value(binaryBodyKey{}).(bool)
  25. return marked
  26. }
  27. // parseBodyForm parses the request body as form fields, reporting whether the form
  28. // is available to read afterwards. It is safe to call repeatedly: ParseForm caches
  29. // its result on the request.
  30. func parseBodyForm(r *http.Request) bool {
  31. if r.Method != http.MethodPost && r.Method != http.MethodPut {
  32. return false
  33. }
  34. if hasBinaryBody(r) {
  35. return false
  36. }
  37. if ct := r.Header.Get("Content-Type"); ct == "" {
  38. // ParseForm ignores a body it cannot type, and form bodies often arrive
  39. // unannounced. A genuinely non-form body is marked by WithBinaryBody.
  40. r.Header.Set("Content-Type", formContentType)
  41. } else if mediaType, _, err := mime.ParseMediaType(ct); err != nil || mediaType != formContentType {
  42. return false
  43. }
  44. return r.ParseForm() == nil
  45. }
  46. // param returns a request parameter from the query string, falling back to the
  47. // form-encoded body.
  48. func param(r *http.Request, key string) string {
  49. if v := r.URL.Query().Get(key); v != "" {
  50. return v
  51. }
  52. if !parseBodyForm(r) {
  53. return ""
  54. }
  55. return r.PostFormValue(key)
  56. }
  57. // paramValues returns every value sent for a repeated parameter, from the query
  58. // string and the form-encoded body both.
  59. func paramValues(r *http.Request, key string) []string {
  60. return append(r.URL.Query()[key], bodyValues(r, key)...)
  61. }
  62. // bodyValues returns every value sent for a repeated parameter in the form-encoded
  63. // body, ignoring the query string. Most callers want paramValues instead.
  64. func bodyValues(r *http.Request, key string) []string {
  65. if !parseBodyForm(r) {
  66. return nil
  67. }
  68. return r.PostForm[key]
  69. }
  70. // targetNames returns the screen names a request's "t" parameter asks about.
  71. // Both spellings of the list are accepted and combined: one t carrying comma-
  72. // separated names, and t repeated once per name.
  73. func targetNames(r *http.Request) []string {
  74. var targets []string
  75. for _, value := range paramValues(r, "t") {
  76. for _, name := range strings.Split(value, ",") {
  77. if name = strings.TrimSpace(name); name != "" {
  78. targets = append(targets, name)
  79. }
  80. }
  81. }
  82. return targets
  83. }
  84. // isTrueParam reports whether a boolean-ish parameter is set. Clients spell these
  85. // inconsistently, so both "1" and "true" are accepted.
  86. func isTrueParam(v string) bool {
  87. switch strings.ToLower(strings.TrimSpace(v)) {
  88. case "1", "true", "yes":
  89. return true
  90. }
  91. return false
  92. }