262 lines
11 KiB
Swift
262 lines
11 KiB
Swift
//
|
|
// ParameterEncoding.swift
|
|
//
|
|
// Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/)
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/**
|
|
HTTP method definitions.
|
|
|
|
See https://tools.ietf.org/html/rfc7231#section-4.3
|
|
*/
|
|
public enum Method: String {
|
|
case OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE, TRACE, CONNECT
|
|
}
|
|
|
|
// MARK: ParameterEncoding
|
|
|
|
/**
|
|
Used to specify the way in which a set of parameters are applied to a URL request.
|
|
|
|
- `URL`: Creates a query string to be set as or appended to any existing URL query for `GET`, `HEAD`,
|
|
and `DELETE` requests, or set as the body for requests with any other HTTP method. The
|
|
`Content-Type` HTTP header field of an encoded request with HTTP body is set to
|
|
`application/x-www-form-urlencoded; charset=utf-8`. Since there is no published specification
|
|
for how to encode collection types, the convention of appending `[]` to the key for array
|
|
values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for nested
|
|
dictionary values (`foo[bar]=baz`).
|
|
|
|
- `URLEncodedInURL`: Creates query string to be set as or appended to any existing URL query. Uses the same
|
|
implementation as the `.URL` case, but always applies the encoded result to the URL.
|
|
|
|
- `JSON`: Uses `NSJSONSerialization` to create a JSON representation of the parameters object, which is
|
|
set as the body of the request. The `Content-Type` HTTP header field of an encoded request is
|
|
set to `application/json`.
|
|
|
|
- `PropertyList`: Uses `NSPropertyListSerialization` to create a plist representation of the parameters object,
|
|
according to the associated format and write options values, which is set as the body of the
|
|
request. The `Content-Type` HTTP header field of an encoded request is set to
|
|
`application/x-plist`.
|
|
|
|
- `Custom`: Uses the associated closure value to construct a new request given an existing request and
|
|
parameters.
|
|
*/
|
|
public enum ParameterEncoding {
|
|
case URL
|
|
case URLEncodedInURL
|
|
case JSON
|
|
case PropertyList(NSPropertyListFormat, NSPropertyListWriteOptions)
|
|
case Custom((URLRequestConvertible, [String: AnyObject]?) -> (NSMutableURLRequest, NSError?))
|
|
|
|
/**
|
|
Creates a URL request by encoding parameters and applying them onto an existing request.
|
|
|
|
- parameter URLRequest: The request to have parameters applied.
|
|
- parameter parameters: The parameters to apply.
|
|
|
|
- returns: A tuple containing the constructed request and the error that occurred during parameter encoding,
|
|
if any.
|
|
*/
|
|
public func encode(
|
|
URLRequest: URLRequestConvertible,
|
|
parameters: [String: AnyObject]?)
|
|
-> (NSMutableURLRequest, NSError?)
|
|
{
|
|
var mutableURLRequest = URLRequest.URLRequest
|
|
|
|
guard let parameters = parameters else { return (mutableURLRequest, nil) }
|
|
|
|
var encodingError: NSError? = nil
|
|
|
|
switch self {
|
|
case .URL, .URLEncodedInURL:
|
|
func query(parameters: [String: AnyObject]) -> String {
|
|
var components: [(String, String)] = []
|
|
|
|
for key in parameters.keys.sort(<) {
|
|
let value = parameters[key]!
|
|
components += queryComponents(key, value)
|
|
}
|
|
|
|
return (components.map { "\($0)=\($1)" } as [String]).joinWithSeparator("&")
|
|
}
|
|
|
|
func encodesParametersInURL(method: Method) -> Bool {
|
|
switch self {
|
|
case .URLEncodedInURL:
|
|
return true
|
|
default:
|
|
break
|
|
}
|
|
|
|
switch method {
|
|
case .GET, .HEAD, .DELETE:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
if let method = Method(rawValue: mutableURLRequest.HTTPMethod) where encodesParametersInURL(method) {
|
|
if let
|
|
URLComponents = NSURLComponents(URL: mutableURLRequest.URL!, resolvingAgainstBaseURL: false)
|
|
where !parameters.isEmpty
|
|
{
|
|
let percentEncodedQuery = (URLComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
|
|
URLComponents.percentEncodedQuery = percentEncodedQuery
|
|
mutableURLRequest.URL = URLComponents.URL
|
|
}
|
|
} else {
|
|
if mutableURLRequest.valueForHTTPHeaderField("Content-Type") == nil {
|
|
mutableURLRequest.setValue(
|
|
"application/x-www-form-urlencoded; charset=utf-8",
|
|
forHTTPHeaderField: "Content-Type"
|
|
)
|
|
}
|
|
|
|
mutableURLRequest.HTTPBody = query(parameters).dataUsingEncoding(
|
|
NSUTF8StringEncoding,
|
|
allowLossyConversion: false
|
|
)
|
|
}
|
|
case .JSON:
|
|
do {
|
|
let options = NSJSONWritingOptions()
|
|
let data = try NSJSONSerialization.dataWithJSONObject(parameters, options: options)
|
|
|
|
if mutableURLRequest.valueForHTTPHeaderField("Content-Type") == nil {
|
|
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
}
|
|
|
|
mutableURLRequest.HTTPBody = data
|
|
} catch {
|
|
encodingError = error as NSError
|
|
}
|
|
case .PropertyList(let format, let options):
|
|
do {
|
|
let data = try NSPropertyListSerialization.dataWithPropertyList(
|
|
parameters,
|
|
format: format,
|
|
options: options
|
|
)
|
|
|
|
if mutableURLRequest.valueForHTTPHeaderField("Content-Type") == nil {
|
|
mutableURLRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type")
|
|
}
|
|
|
|
mutableURLRequest.HTTPBody = data
|
|
} catch {
|
|
encodingError = error as NSError
|
|
}
|
|
case .Custom(let closure):
|
|
(mutableURLRequest, encodingError) = closure(mutableURLRequest, parameters)
|
|
}
|
|
|
|
return (mutableURLRequest, encodingError)
|
|
}
|
|
|
|
/**
|
|
Creates percent-escaped, URL encoded query string components from the given key-value pair using recursion.
|
|
|
|
- parameter key: The key of the query component.
|
|
- parameter value: The value of the query component.
|
|
|
|
- returns: The percent-escaped, URL encoded query string components.
|
|
*/
|
|
public func queryComponents(key: String, _ value: AnyObject) -> [(String, String)] {
|
|
var components: [(String, String)] = []
|
|
|
|
if let dictionary = value as? [String: AnyObject] {
|
|
for (nestedKey, value) in dictionary {
|
|
components += queryComponents("\(key)[\(nestedKey)]", value)
|
|
}
|
|
} else if let array = value as? [AnyObject] {
|
|
for value in array {
|
|
components += queryComponents("\(key)[]", value)
|
|
}
|
|
} else {
|
|
components.append((escape(key), escape("\(value)")))
|
|
}
|
|
|
|
return components
|
|
}
|
|
|
|
/**
|
|
Returns a percent-escaped string following RFC 3986 for a query string key or value.
|
|
|
|
RFC 3986 states that the following characters are "reserved" characters.
|
|
|
|
- General Delimiters: ":", "#", "[", "]", "@", "?", "/"
|
|
- Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
|
|
|
|
In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
|
|
query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
|
|
should be percent-escaped in the query string.
|
|
|
|
- parameter string: The string to be percent-escaped.
|
|
|
|
- returns: The percent-escaped string.
|
|
*/
|
|
public func escape(string: String) -> String {
|
|
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
|
|
let subDelimitersToEncode = "!$&'()*+,;="
|
|
|
|
let allowedCharacterSet = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy() as! NSMutableCharacterSet
|
|
allowedCharacterSet.removeCharactersInString(generalDelimitersToEncode + subDelimitersToEncode)
|
|
|
|
var escaped = ""
|
|
|
|
//==========================================================================================================
|
|
//
|
|
// Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
|
|
// hundred Chinense characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
|
|
// longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
|
|
// info, please refer to:
|
|
//
|
|
// - https://github.com/Alamofire/Alamofire/issues/206
|
|
//
|
|
//==========================================================================================================
|
|
|
|
if #available(iOS 8.3, OSX 10.10, *) {
|
|
escaped = string.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? string
|
|
} else {
|
|
let batchSize = 50
|
|
var index = string.startIndex
|
|
|
|
while index != string.endIndex {
|
|
let startIndex = index
|
|
let endIndex = index.advancedBy(batchSize, limit: string.endIndex)
|
|
let range = startIndex..<endIndex
|
|
|
|
let substring = string.substringWithRange(range)
|
|
|
|
escaped += substring.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? substring
|
|
|
|
index = endIndex
|
|
}
|
|
}
|
|
|
|
return escaped
|
|
}
|
|
}
|