You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

483 lines
19 KiB

6 years ago
5 years ago
6 years ago
  1. //
  2. // ParameterEncoding.swift
  3. //
  4. // Copyright (c) 2014 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. /// HTTP method definitions.
  26. ///
  27. /// See https://tools.ietf.org/html/rfc7231#section-4.3
  28. public enum HTTPMethod: String {
  29. case options = "OPTIONS"
  30. case get = "GET"
  31. case head = "HEAD"
  32. case post = "POST"
  33. case put = "PUT"
  34. case patch = "PATCH"
  35. case delete = "DELETE"
  36. case trace = "TRACE"
  37. case connect = "CONNECT"
  38. }
  39. // MARK: -
  40. /// A dictionary of parameters to apply to a `URLRequest`.
  41. public typealias Parameters = [String: Any]
  42. /// A type used to define how a set of parameters are applied to a `URLRequest`.
  43. public protocol ParameterEncoding {
  44. /// Creates a URL request by encoding parameters and applying them onto an existing request.
  45. ///
  46. /// - parameter urlRequest: The request to have parameters applied.
  47. /// - parameter parameters: The parameters to apply.
  48. ///
  49. /// - throws: An `AFError.parameterEncodingFailed` error if encoding fails.
  50. ///
  51. /// - returns: The encoded request.
  52. func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
  53. }
  54. // MARK: -
  55. /// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP
  56. /// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as
  57. /// the HTTP body depends on the destination of the encoding.
  58. ///
  59. /// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
  60. /// `application/x-www-form-urlencoded; charset=utf-8`.
  61. ///
  62. /// There is no published specification for how to encode collection types. By default the convention of appending
  63. /// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for
  64. /// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the
  65. /// square brackets appended to array keys.
  66. ///
  67. /// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode
  68. /// `true` as 1 and `false` as 0.
  69. public struct URLEncoding: ParameterEncoding {
  70. // MARK: Helper Types
  71. /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the
  72. /// resulting URL request.
  73. ///
  74. /// - methodDependent: Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE`
  75. /// requests and sets as the HTTP body for requests with any other HTTP method.
  76. /// - queryString: Sets or appends encoded query string result to existing query string.
  77. /// - httpBody: Sets encoded query string result as the HTTP body of the URL request.
  78. public enum Destination {
  79. case methodDependent, queryString, httpBody
  80. }
  81. /// Configures how `Array` parameters are encoded.
  82. ///
  83. /// - brackets: An empty set of square brackets is appended to the key for every value.
  84. /// This is the default behavior.
  85. /// - noBrackets: No brackets are appended. The key is encoded as is.
  86. public enum ArrayEncoding {
  87. case brackets, noBrackets
  88. func encode(key: String) -> String {
  89. switch self {
  90. case .brackets:
  91. return "\(key)[]"
  92. case .noBrackets:
  93. return key
  94. }
  95. }
  96. }
  97. /// Configures how `Bool` parameters are encoded.
  98. ///
  99. /// - numeric: Encode `true` as `1` and `false` as `0`. This is the default behavior.
  100. /// - literal: Encode `true` and `false` as string literals.
  101. public enum BoolEncoding {
  102. case numeric, literal
  103. func encode(value: Bool) -> String {
  104. switch self {
  105. case .numeric:
  106. return value ? "1" : "0"
  107. case .literal:
  108. return value ? "true" : "false"
  109. }
  110. }
  111. }
  112. // MARK: Properties
  113. /// Returns a default `URLEncoding` instance.
  114. public static var `default`: URLEncoding { return URLEncoding() }
  115. /// Returns a `URLEncoding` instance with a `.methodDependent` destination.
  116. public static var methodDependent: URLEncoding { return URLEncoding() }
  117. /// Returns a `URLEncoding` instance with a `.queryString` destination.
  118. public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }
  119. /// Returns a `URLEncoding` instance with an `.httpBody` destination.
  120. public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }
  121. /// The destination defining where the encoded query string is to be applied to the URL request.
  122. public let destination: Destination
  123. /// The encoding to use for `Array` parameters.
  124. public let arrayEncoding: ArrayEncoding
  125. /// The encoding to use for `Bool` parameters.
  126. public let boolEncoding: BoolEncoding
  127. // MARK: Initialization
  128. /// Creates a `URLEncoding` instance using the specified destination.
  129. ///
  130. /// - parameter destination: The destination defining where the encoded query string is to be applied.
  131. /// - parameter arrayEncoding: The encoding to use for `Array` parameters.
  132. /// - parameter boolEncoding: The encoding to use for `Bool` parameters.
  133. ///
  134. /// - returns: The new `URLEncoding` instance.
  135. public init(destination: Destination = .methodDependent, arrayEncoding: ArrayEncoding = .brackets, boolEncoding: BoolEncoding = .numeric) {
  136. self.destination = destination
  137. self.arrayEncoding = arrayEncoding
  138. self.boolEncoding = boolEncoding
  139. }
  140. // MARK: Encoding
  141. /// Creates a URL request by encoding parameters and applying them onto an existing request.
  142. ///
  143. /// - parameter urlRequest: The request to have parameters applied.
  144. /// - parameter parameters: The parameters to apply.
  145. ///
  146. /// - throws: An `Error` if the encoding process encounters an error.
  147. ///
  148. /// - returns: The encoded request.
  149. public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
  150. var urlRequest = try urlRequest.asURLRequest()
  151. guard let parameters = parameters else { return urlRequest }
  152. if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodesParametersInURL(with: method) {
  153. guard let url = urlRequest.url else {
  154. throw AFError.parameterEncodingFailed(reason: .missingURL)
  155. }
  156. if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
  157. let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
  158. urlComponents.percentEncodedQuery = percentEncodedQuery
  159. urlRequest.url = urlComponents.url
  160. }
  161. } else {
  162. if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
  163. urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
  164. }
  165. urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false)
  166. }
  167. return urlRequest
  168. }
  169. /// Creates percent-escaped, URL encoded query string components from the given key-value pair using recursion.
  170. ///
  171. /// - parameter key: The key of the query component.
  172. /// - parameter value: The value of the query component.
  173. ///
  174. /// - returns: The percent-escaped, URL encoded query string components.
  175. public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
  176. var components: [(String, String)] = []
  177. if let dictionary = value as? [String: Any] {
  178. for (nestedKey, value) in dictionary {
  179. components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
  180. }
  181. } else if let array = value as? [Any] {
  182. for value in array {
  183. components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
  184. }
  185. } else if let value = value as? NSNumber {
  186. if value.isBool {
  187. components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue))))
  188. } else {
  189. components.append((escape(key), escape("\(value)")))
  190. }
  191. } else if let bool = value as? Bool {
  192. components.append((escape(key), escape(boolEncoding.encode(value: bool))))
  193. } else {
  194. components.append((escape(key), escape("\(value)")))
  195. }
  196. return components
  197. }
  198. /// Returns a percent-escaped string following RFC 3986 for a query string key or value.
  199. ///
  200. /// RFC 3986 states that the following characters are "reserved" characters.
  201. ///
  202. /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
  203. /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
  204. ///
  205. /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
  206. /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
  207. /// should be percent-escaped in the query string.
  208. ///
  209. /// - parameter string: The string to be percent-escaped.
  210. ///
  211. /// - returns: The percent-escaped string.
  212. public func escape(_ string: String) -> String {
  213. let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
  214. let subDelimitersToEncode = "!$&'()*+,;="
  215. var allowedCharacterSet = CharacterSet.urlQueryAllowed
  216. allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
  217. var escaped = ""
  218. //==========================================================================================================
  219. //
  220. // Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
  221. // hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
  222. // longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
  223. // info, please refer to:
  224. //
  225. // - https://github.com/Alamofire/Alamofire/issues/206
  226. //
  227. //==========================================================================================================
  228. if #available(iOS 8.3, *) {
  229. escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
  230. } else {
  231. let batchSize = 50
  232. var index = string.startIndex
  233. while index != string.endIndex {
  234. let startIndex = index
  235. let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
  236. let range = startIndex..<endIndex
  237. let substring = string[range]
  238. escaped += substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? String(substring)
  239. index = endIndex
  240. }
  241. }
  242. return escaped
  243. }
  244. private func query(_ parameters: [String: Any]) -> String {
  245. var components: [(String, String)] = []
  246. for key in parameters.keys.sorted(by: <) {
  247. let value = parameters[key]!
  248. components += queryComponents(fromKey: key, value: value)
  249. }
  250. return components.map { "\($0)=\($1)" }.joined(separator: "&")
  251. }
  252. private func encodesParametersInURL(with method: HTTPMethod) -> Bool {
  253. switch destination {
  254. case .queryString:
  255. return true
  256. case .httpBody:
  257. return false
  258. default:
  259. break
  260. }
  261. switch method {
  262. case .get, .head, .delete:
  263. return true
  264. default:
  265. return false
  266. }
  267. }
  268. }
  269. // MARK: -
  270. /// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the
  271. /// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
  272. public struct JSONEncoding: ParameterEncoding {
  273. // MARK: Properties
  274. /// Returns a `JSONEncoding` instance with default writing options.
  275. public static var `default`: JSONEncoding { return JSONEncoding() }
  276. /// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options.
  277. public static var prettyPrinted: JSONEncoding { return JSONEncoding(options: .prettyPrinted) }
  278. /// The options for writing the parameters as JSON data.
  279. public let options: JSONSerialization.WritingOptions
  280. // MARK: Initialization
  281. /// Creates a `JSONEncoding` instance using the specified options.
  282. ///
  283. /// - parameter options: The options for writing the parameters as JSON data.
  284. ///
  285. /// - returns: The new `JSONEncoding` instance.
  286. public init(options: JSONSerialization.WritingOptions = []) {
  287. self.options = options
  288. }
  289. // MARK: Encoding
  290. /// Creates a URL request by encoding parameters and applying them onto an existing request.
  291. ///
  292. /// - parameter urlRequest: The request to have parameters applied.
  293. /// - parameter parameters: The parameters to apply.
  294. ///
  295. /// - throws: An `Error` if the encoding process encounters an error.
  296. ///
  297. /// - returns: The encoded request.
  298. public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
  299. var urlRequest = try urlRequest.asURLRequest()
  300. guard let parameters = parameters else { return urlRequest }
  301. do {
  302. let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
  303. if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
  304. urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
  305. }
  306. urlRequest.httpBody = data
  307. } catch {
  308. throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
  309. }
  310. return urlRequest
  311. }
  312. /// Creates a URL request by encoding the JSON object and setting the resulting data on the HTTP body.
  313. ///
  314. /// - parameter urlRequest: The request to apply the JSON object to.
  315. /// - parameter jsonObject: The JSON object to apply to the request.
  316. ///
  317. /// - throws: An `Error` if the encoding process encounters an error.
  318. ///
  319. /// - returns: The encoded request.
  320. public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest {
  321. var urlRequest = try urlRequest.asURLRequest()
  322. guard let jsonObject = jsonObject else { return urlRequest }
  323. do {
  324. let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options)
  325. if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
  326. urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
  327. }
  328. urlRequest.httpBody = data
  329. } catch {
  330. throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
  331. }
  332. return urlRequest
  333. }
  334. }
  335. // MARK: -
  336. /// Uses `PropertyListSerialization` to create a plist representation of the parameters object, according to the
  337. /// associated format and write options values, which is set as the body of the request. The `Content-Type` HTTP header
  338. /// field of an encoded request is set to `application/x-plist`.
  339. public struct PropertyListEncoding: ParameterEncoding {
  340. // MARK: Properties
  341. /// Returns a default `PropertyListEncoding` instance.
  342. public static var `default`: PropertyListEncoding { return PropertyListEncoding() }
  343. /// Returns a `PropertyListEncoding` instance with xml formatting and default writing options.
  344. public static var xml: PropertyListEncoding { return PropertyListEncoding(format: .xml) }
  345. /// Returns a `PropertyListEncoding` instance with binary formatting and default writing options.
  346. public static var binary: PropertyListEncoding { return PropertyListEncoding(format: .binary) }
  347. /// The property list serialization format.
  348. public let format: PropertyListSerialization.PropertyListFormat
  349. /// The options for writing the parameters as plist data.
  350. public let options: PropertyListSerialization.WriteOptions
  351. // MARK: Initialization
  352. /// Creates a `PropertyListEncoding` instance using the specified format and options.
  353. ///
  354. /// - parameter format: The property list serialization format.
  355. /// - parameter options: The options for writing the parameters as plist data.
  356. ///
  357. /// - returns: The new `PropertyListEncoding` instance.
  358. public init(
  359. format: PropertyListSerialization.PropertyListFormat = .xml,
  360. options: PropertyListSerialization.WriteOptions = 0)
  361. {
  362. self.format = format
  363. self.options = options
  364. }
  365. // MARK: Encoding
  366. /// Creates a URL request by encoding parameters and applying them onto an existing request.
  367. ///
  368. /// - parameter urlRequest: The request to have parameters applied.
  369. /// - parameter parameters: The parameters to apply.
  370. ///
  371. /// - throws: An `Error` if the encoding process encounters an error.
  372. ///
  373. /// - returns: The encoded request.
  374. public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
  375. var urlRequest = try urlRequest.asURLRequest()
  376. guard let parameters = parameters else { return urlRequest }
  377. do {
  378. let data = try PropertyListSerialization.data(
  379. fromPropertyList: parameters,
  380. format: format,
  381. options: options
  382. )
  383. if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
  384. urlRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type")
  385. }
  386. urlRequest.httpBody = data
  387. } catch {
  388. throw AFError.parameterEncodingFailed(reason: .propertyListEncodingFailed(error: error))
  389. }
  390. return urlRequest
  391. }
  392. }
  393. // MARK: -
  394. extension NSNumber {
  395. fileprivate var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) }
  396. }