ParameterEncoding.swift 11 KB

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