ParameterEncoding.swift 11 KB

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