2
0

ParameterEncoding.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. //
  2. // ParameterEncoding.swift
  3. //
  4. // Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/)
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. import Foundation
  25. /**
  26. HTTP method definitions.
  27. See https://tools.ietf.org/html/rfc7231#section-4.3
  28. */
  29. public enum Method: String {
  30. case OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE, TRACE, CONNECT
  31. }
  32. // MARK: ParameterEncoding
  33. /**
  34. Used to specify the way in which a set of parameters are applied to a URL request.
  35. - `URL`: Creates a query string to be set as or appended to any existing URL query for `GET`, `HEAD`,
  36. and `DELETE` requests, or set as the body for requests with any other HTTP method. The
  37. `Content-Type` HTTP header field of an encoded request with HTTP body is set to
  38. `application/x-www-form-urlencoded; charset=utf-8`. Since there is no published specification
  39. for how to encode collection types, the convention of appending `[]` to the key for array
  40. values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for nested
  41. dictionary values (`foo[bar]=baz`).
  42. - `URLEncodedInURL`: Creates query string to be set as or appended to any existing URL query. Uses the same
  43. implementation as the `.URL` case, but always applies the encoded result to the URL.
  44. - `JSON`: Uses `NSJSONSerialization` to create a JSON representation of the parameters object, which is
  45. set as the body of the request. The `Content-Type` HTTP header field of an encoded request is
  46. set to `application/json`.
  47. - `PropertyList`: Uses `NSPropertyListSerialization` to create a plist representation of the parameters object,
  48. according to the associated format and write options values, which is set as the body of the
  49. request. The `Content-Type` HTTP header field of an encoded request is set to
  50. `application/x-plist`.
  51. - `Custom`: Uses the associated closure value to construct a new request given an existing request and
  52. parameters.
  53. */
  54. public enum ParameterEncoding {
  55. case url
  56. case urlEncodedInURL
  57. case json
  58. case propertyList(PropertyListSerialization.PropertyListFormat, PropertyListSerialization.WriteOptions)
  59. case custom((URLRequestConvertible, [String: AnyObject]?) -> (URLRequest, NSError?))
  60. /**
  61. Creates a URL request by encoding parameters and applying them onto an existing request.
  62. - parameter URLRequest: The request to have parameters applied.
  63. - parameter parameters: The parameters to apply.
  64. - returns: A tuple containing the constructed request and the error that occurred during parameter encoding,
  65. if any.
  66. */
  67. public func encode(
  68. _ URLRequest: URLRequestConvertible,
  69. parameters: [String: AnyObject]?)
  70. -> (Foundation.URLRequest, NSError?)
  71. {
  72. var urlRequest = URLRequest.urlRequest
  73. guard let parameters = parameters else { return (urlRequest, nil) }
  74. var encodingError: NSError? = nil
  75. switch self {
  76. case .url, .urlEncodedInURL:
  77. func query(_ parameters: [String: AnyObject]) -> String {
  78. var components: [(String, String)] = []
  79. for key in parameters.keys.sorted(isOrderedBefore: <) {
  80. let value = parameters[key]!
  81. components += queryComponents(key, value)
  82. }
  83. return (components.map { "\($0)=\($1)" } as [String]).joined(separator: "&")
  84. }
  85. func encodesParametersInURL(_ method: Method) -> Bool {
  86. switch self {
  87. case .urlEncodedInURL:
  88. return true
  89. default:
  90. break
  91. }
  92. switch method {
  93. case .GET, .HEAD, .DELETE:
  94. return true
  95. default:
  96. return false
  97. }
  98. }
  99. if let method = Method(rawValue: urlRequest.httpMethod!), encodesParametersInURL(method) {
  100. if var
  101. URLComponents = URLComponents(url: urlRequest.url!, resolvingAgainstBaseURL: false),
  102. !parameters.isEmpty {
  103. let percentEncodedQuery = (URLComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
  104. URLComponents.percentEncodedQuery = percentEncodedQuery
  105. urlRequest.url = URLComponents.url
  106. }
  107. } else {
  108. if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
  109. urlRequest.setValue(
  110. "application/x-www-form-urlencoded; charset=utf-8",
  111. forHTTPHeaderField: "Content-Type"
  112. )
  113. }
  114. urlRequest.httpBody = query(parameters).data(
  115. using: String.Encoding.utf8,
  116. allowLossyConversion: false
  117. )
  118. }
  119. case .json:
  120. do {
  121. let options = JSONSerialization.WritingOptions()
  122. let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
  123. if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
  124. urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
  125. }
  126. urlRequest.httpBody = data
  127. } catch {
  128. encodingError = error as NSError
  129. }
  130. case .propertyList(let format, let options):
  131. do {
  132. let data = try PropertyListSerialization.data(
  133. fromPropertyList: parameters,
  134. format: format,
  135. options: options
  136. )
  137. if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
  138. urlRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type")
  139. }
  140. urlRequest.httpBody = data
  141. } catch {
  142. encodingError = error as NSError
  143. }
  144. case .custom(let closure):
  145. (urlRequest, encodingError) = closure(urlRequest, parameters)
  146. }
  147. return (urlRequest, encodingError)
  148. }
  149. /**
  150. Creates percent-escaped, URL encoded query string components from the given key-value pair using recursion.
  151. - parameter key: The key of the query component.
  152. - parameter value: The value of the query component.
  153. - returns: The percent-escaped, URL encoded query string components.
  154. */
  155. public func queryComponents(_ key: String, _ value: AnyObject) -> [(String, String)] {
  156. var components: [(String, String)] = []
  157. if let dictionary = value as? [String: AnyObject] {
  158. for (nestedKey, value) in dictionary {
  159. components += queryComponents("\(key)[\(nestedKey)]", value)
  160. }
  161. } else if let array = value as? [AnyObject] {
  162. for value in array {
  163. components += queryComponents("\(key)[]", value)
  164. }
  165. } else {
  166. components.append((escape(key), escape("\(value)")))
  167. }
  168. return components
  169. }
  170. /**
  171. Returns a percent-escaped string following RFC 3986 for a query string key or value.
  172. RFC 3986 states that the following characters are "reserved" characters.
  173. - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
  174. - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
  175. In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
  176. query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
  177. should be percent-escaped in the query string.
  178. - parameter string: The string to be percent-escaped.
  179. - returns: The percent-escaped string.
  180. */
  181. public func escape(_ string: String) -> String {
  182. let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
  183. let subDelimitersToEncode = "!$&'()*+,;="
  184. // rdar://26850776
  185. // Crash in Xcode 8 Seed 1 when trying to mutate a CharacterSet with remove
  186. var allowedCharacterSet = NSMutableCharacterSet.urlQueryAllowed
  187. allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
  188. var escaped = ""
  189. //==========================================================================================================
  190. //
  191. // Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
  192. // hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
  193. // longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
  194. // info, please refer to:
  195. //
  196. // - https://github.com/Alamofire/Alamofire/issues/206
  197. //
  198. //==========================================================================================================
  199. if #available(iOS 8.3, OSX 10.10, *) {
  200. escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
  201. } else {
  202. let batchSize = 50
  203. var index = string.startIndex
  204. while index != string.endIndex {
  205. let startIndex = index
  206. let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex)
  207. let range = startIndex..<(endIndex ?? string.endIndex)
  208. let substring = string.substring(with: range)
  209. escaped += substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? substring
  210. index = endIndex ?? string.endIndex
  211. }
  212. }
  213. return escaped
  214. }
  215. }