ParameterEncoding.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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(NSPropertyListFormat, NSPropertyListWriteOptions)
  59. case Custom((URLRequestConvertible, [String: AnyObject]?) -> (NSMutableURLRequest, 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. -> (NSMutableURLRequest, NSError?)
  71. {
  72. var mutableURLRequest = URLRequest.URLRequest
  73. guard let parameters = parameters else { return (mutableURLRequest, 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.sort(<) {
  80. let value = parameters[key]!
  81. components += queryComponents(key, value)
  82. }
  83. return (components.map { "\($0)=\($1)" } as [String]).joinWithSeparator("&")
  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: mutableURLRequest.HTTPMethod) where encodesParametersInURL(method) {
  100. if let
  101. URLComponents = NSURLComponents(URL: mutableURLRequest.URL!, resolvingAgainstBaseURL: false)
  102. where !parameters.isEmpty
  103. {
  104. let percentEncodedQuery = (URLComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
  105. URLComponents.percentEncodedQuery = percentEncodedQuery
  106. mutableURLRequest.URL = URLComponents.URL
  107. }
  108. } else {
  109. if mutableURLRequest.valueForHTTPHeaderField("Content-Type") == nil {
  110. mutableURLRequest.setValue(
  111. "application/x-www-form-urlencoded; charset=utf-8",
  112. forHTTPHeaderField: "Content-Type"
  113. )
  114. }
  115. mutableURLRequest.HTTPBody = query(parameters).dataUsingEncoding(
  116. NSUTF8StringEncoding,
  117. allowLossyConversion: false
  118. )
  119. }
  120. case .JSON:
  121. do {
  122. let options = NSJSONWritingOptions()
  123. let data = try NSJSONSerialization.dataWithJSONObject(parameters, options: options)
  124. if mutableURLRequest.valueForHTTPHeaderField("Content-Type") == nil {
  125. mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
  126. }
  127. mutableURLRequest.HTTPBody = data
  128. } catch {
  129. encodingError = error as NSError
  130. }
  131. case .PropertyList(let format, let options):
  132. do {
  133. let data = try NSPropertyListSerialization.dataWithPropertyList(
  134. parameters,
  135. format: format,
  136. options: options
  137. )
  138. if mutableURLRequest.valueForHTTPHeaderField("Content-Type") == nil {
  139. mutableURLRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type")
  140. }
  141. mutableURLRequest.HTTPBody = data
  142. } catch {
  143. encodingError = error as NSError
  144. }
  145. case .Custom(let closure):
  146. (mutableURLRequest, encodingError) = closure(mutableURLRequest, parameters)
  147. }
  148. return (mutableURLRequest, encodingError)
  149. }
  150. /**
  151. Creates percent-escaped, URL encoded query string components from the given key-value pair using recursion.
  152. - parameter key: The key of the query component.
  153. - parameter value: The value of the query component.
  154. - returns: The percent-escaped, URL encoded query string components.
  155. */
  156. public func queryComponents(key: String, _ value: AnyObject) -> [(String, String)] {
  157. var components: [(String, String)] = []
  158. if let dictionary = value as? [String: AnyObject] {
  159. for (nestedKey, value) in dictionary {
  160. components += queryComponents("\(key)[\(nestedKey)]", value)
  161. }
  162. } else if let array = value as? [AnyObject] {
  163. for value in array {
  164. components += queryComponents("\(key)[]", value)
  165. }
  166. } else {
  167. components.append((escape(key), escape("\(value)")))
  168. }
  169. return components
  170. }
  171. /**
  172. Returns a percent-escaped string following RFC 3986 for a query string key or value.
  173. RFC 3986 states that the following characters are "reserved" characters.
  174. - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
  175. - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
  176. In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
  177. query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
  178. should be percent-escaped in the query string.
  179. - parameter string: The string to be percent-escaped.
  180. - returns: The percent-escaped string.
  181. */
  182. public func escape(string: String) -> String {
  183. let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
  184. let subDelimitersToEncode = "!$&'()*+,;="
  185. let allowedCharacterSet = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy() as! NSMutableCharacterSet
  186. allowedCharacterSet.removeCharactersInString(generalDelimitersToEncode + subDelimitersToEncode)
  187. var escaped = ""
  188. //==========================================================================================================
  189. //
  190. // Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
  191. // hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
  192. // longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
  193. // info, please refer to:
  194. //
  195. // - https://github.com/Alamofire/Alamofire/issues/206
  196. //
  197. //==========================================================================================================
  198. if #available(iOS 8.3, OSX 10.10, *) {
  199. escaped = string.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? string
  200. } else {
  201. let batchSize = 50
  202. var index = string.startIndex
  203. while index != string.endIndex {
  204. let startIndex = index
  205. let endIndex = index.advancedBy(batchSize, limit: string.endIndex)
  206. let range = startIndex..<endIndex
  207. let substring = string.substringWithRange(range)
  208. escaped += substring.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? substring
  209. index = endIndex
  210. }
  211. }
  212. return escaped
  213. }
  214. }