|
| 1 | +import Capacitor |
| 2 | +import IONFileTransferLib |
| 3 | + |
| 4 | +// Basically a reimplementation of certain parts of CapacitorUrlRequest |
| 5 | +// that does not need to be used with the latter. |
| 6 | + |
| 7 | +public enum RequestBodyError: Error { |
| 8 | + case serializationError(String?) |
| 9 | +} |
| 10 | + |
| 11 | +public struct RequestData { |
| 12 | + let body: Data |
| 13 | + let additionalHeaders: [String: String] |
| 14 | + |
| 15 | + init(body: Data) { |
| 16 | + self.body = body |
| 17 | + self.additionalHeaders = [:] |
| 18 | + } |
| 19 | + init(body: Data, additionalHeaders: [String: String]) { |
| 20 | + self.body = body |
| 21 | + self.additionalHeaders = additionalHeaders |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +public func getRequestData( |
| 26 | + call: CAPPluginCall, |
| 27 | + options: inout IONFLTRHttpOptions |
| 28 | +) throws -> Data? { |
| 29 | + guard let body = call.options["data"] as? JSValue else { |
| 30 | + return nil |
| 31 | + } |
| 32 | + let bodyType = call.getString("dataType") |
| 33 | + guard let contentType = options.headers["Content-Type"] else { |
| 34 | + return nil |
| 35 | + } |
| 36 | + guard let reqData = try getRequestData(body, contentType, bodyType) else { |
| 37 | + return nil |
| 38 | + } |
| 39 | + |
| 40 | + options.headers.merge(reqData.additionalHeaders, uniquingKeysWith: { _, new in new }) |
| 41 | + |
| 42 | + return reqData.body |
| 43 | +} |
| 44 | + |
| 45 | +public func getRequestData( |
| 46 | + _ body: JSValue, |
| 47 | + _ contentType: String, |
| 48 | + _ dataType: String? = nil |
| 49 | +) throws -> RequestData? { |
| 50 | + if dataType == "file" { |
| 51 | + guard let stringData = body as? String else { |
| 52 | + throw RequestBodyError.serializationError( |
| 53 | + "[ data ] argument could not be parsed as string" |
| 54 | + ) |
| 55 | + } |
| 56 | + guard let data = Data(base64Encoded: stringData) else {return nil} |
| 57 | + return RequestData(body: data) |
| 58 | + } else if dataType == "formData" { |
| 59 | + return try getRequestDataFromFormData(body, contentType) |
| 60 | + } |
| 61 | + |
| 62 | + // If data can be parsed directly as a string, return that without processing. |
| 63 | + if let strVal = try? getRequestDataAsString(body) { |
| 64 | + return strVal |
| 65 | + } else if contentType.contains("application/json") { |
| 66 | + return try getRequestDataAsJson(body) |
| 67 | + } else if contentType.contains("application/x-www-form-urlencoded") { |
| 68 | + return try getRequestDataAsFormUrlEncoded(body) |
| 69 | + } else if contentType.contains("multipart/form-data") { |
| 70 | + return try getRequestDataAsMultipartFormData(body, contentType) |
| 71 | + } else { |
| 72 | + throw RequestBodyError.serializationError( |
| 73 | + "[ data ] argument could not be parsed for content type [ \(contentType) ]" |
| 74 | + ) |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +public func getRequestDataAsString(_ data: JSValue) throws -> RequestData { |
| 79 | + guard let stringData = data as? String else { |
| 80 | + throw RequestBodyError.serializationError( |
| 81 | + "[ data ] argument could not be parsed as string" |
| 82 | + ) |
| 83 | + } |
| 84 | + return RequestData(body: Data(stringData.utf8)) |
| 85 | +} |
| 86 | + |
| 87 | +public func getRequestDataFromFormData(_ data: JSValue, _ contentType: String) |
| 88 | + throws -> RequestData? |
| 89 | +{ |
| 90 | + guard let list = data as? JSArray else { |
| 91 | + // Throw, other data types explicitly not supported. |
| 92 | + throw RequestBodyError.serializationError( |
| 93 | + "Data must be an array for FormData" |
| 94 | + ) |
| 95 | + } |
| 96 | + var requestHeaders: [String: String] = [:] |
| 97 | + var data = Data() |
| 98 | + var boundary = UUID().uuidString |
| 99 | + if contentType.contains("="), |
| 100 | + let contentBoundary = contentType.components(separatedBy: "=").last |
| 101 | + { |
| 102 | + boundary = contentBoundary |
| 103 | + } else { |
| 104 | + let contentType = "multipart/form-data; boundary=\(boundary)" |
| 105 | + requestHeaders["Content-Type"] = contentType |
| 106 | + } |
| 107 | + for entry in list { |
| 108 | + guard let item = entry as? [String: String] else { |
| 109 | + throw RequestBodyError.serializationError( |
| 110 | + "Data must be an array for FormData" |
| 111 | + ) |
| 112 | + } |
| 113 | + |
| 114 | + let type = item["type"] |
| 115 | + let key = item["key"] |
| 116 | + let value = item["value"]! |
| 117 | + |
| 118 | + if type == "base64File" { |
| 119 | + let fileName = item["fileName"] |
| 120 | + let fileContentType = item["contentType"] |
| 121 | + |
| 122 | + data.append("--\(boundary)\r\n".data(using: .utf8)!) |
| 123 | + data.append( |
| 124 | + "Content-Disposition: form-data; name=\"\(key!)\"; filename=\"\(fileName!)\"\r\n" |
| 125 | + .data(using: .utf8)! |
| 126 | + ) |
| 127 | + data.append( |
| 128 | + "Content-Type: \(fileContentType!)\r\n".data(using: .utf8)! |
| 129 | + ) |
| 130 | + data.append( |
| 131 | + "Content-Transfer-Encoding: binary\r\n".data(using: .utf8)! |
| 132 | + ) |
| 133 | + data.append("\r\n".data(using: .utf8)!) |
| 134 | + |
| 135 | + data.append(Data(base64Encoded: value)!) |
| 136 | + |
| 137 | + data.append("\r\n".data(using: .utf8)!) |
| 138 | + } else if type == "string" { |
| 139 | + data.append("--\(boundary)\r\n".data(using: .utf8)!) |
| 140 | + data.append( |
| 141 | + "Content-Disposition: form-data; name=\"\(key!)\"\r\n".data( |
| 142 | + using: .utf8 |
| 143 | + )! |
| 144 | + ) |
| 145 | + data.append("\r\n".data(using: .utf8)!) |
| 146 | + data.append(value.data(using: .utf8)!) |
| 147 | + data.append("\r\n".data(using: .utf8)!) |
| 148 | + } |
| 149 | + } |
| 150 | + data.append("--\(boundary)--\r\n".data(using: .utf8)!) |
| 151 | + |
| 152 | + return RequestData.init(body: data, additionalHeaders: requestHeaders) |
| 153 | +} |
| 154 | + |
| 155 | +public func getRequestDataAsJson(_ data: JSValue) throws -> RequestData? { |
| 156 | + // We need to check if the JSON is valid before attempting to serialize, as JSONSerialization.data will not throw an exception that can be caught, and will cause the application to crash if it fails. |
| 157 | + if JSONSerialization.isValidJSONObject(data) { |
| 158 | + return RequestData(body: try JSONSerialization.data(withJSONObject: data)) |
| 159 | + } else { |
| 160 | + throw RequestBodyError.serializationError("[ data ] argument for request of content-type [ application/json ] must be serializable to JSON") |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +public func getRequestDataAsFormUrlEncoded(_ data: JSValue) throws -> RequestData? { |
| 165 | + var components = URLComponents() |
| 166 | + components.queryItems = [] |
| 167 | + |
| 168 | + guard let obj = data as? JSObject else { |
| 169 | + // Throw, other data types explicitly not supported |
| 170 | + throw RequestBodyError.serializationError("[ data ] argument for request with content-type [ multipart/form-data ] may only be a plain javascript object") |
| 171 | + } |
| 172 | + |
| 173 | + let allowed = CharacterSet(charactersIn: "-._*").union(.alphanumerics) |
| 174 | + |
| 175 | + obj.keys.forEach { (key: String) in |
| 176 | + let value = obj[key] as? String ?? "" |
| 177 | + components.queryItems?.append(URLQueryItem(name: key.addingPercentEncoding(withAllowedCharacters: allowed)?.replacingOccurrences(of: "%20", with: "+") ?? key, value: value.addingPercentEncoding(withAllowedCharacters: allowed)?.replacingOccurrences(of: "%20", with: "+"))) |
| 178 | + } |
| 179 | + |
| 180 | + if components.query != nil { |
| 181 | + return RequestData(body: Data(components.query!.utf8)) |
| 182 | + } |
| 183 | + |
| 184 | + return nil |
| 185 | + } |
| 186 | + |
| 187 | + public func getRequestDataAsMultipartFormData(_ data: JSValue, _ contentType: String) throws -> RequestData { |
| 188 | + guard let obj = data as? JSObject else { |
| 189 | + // Throw, other data types explicitly not supported. |
| 190 | + throw RequestBodyError.serializationError("[ data ] argument for request with content-type [ application/x-www-form-urlencoded ] may only be a plain javascript object") |
| 191 | + } |
| 192 | + |
| 193 | + var additionalHeaders: [String: String] = [:] |
| 194 | + |
| 195 | + let strings: [String: String] = obj.compactMapValues { any in |
| 196 | + any as? String |
| 197 | + } |
| 198 | + |
| 199 | + var data = Data() |
| 200 | + var boundary = UUID().uuidString |
| 201 | + if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last { |
| 202 | + boundary = contentBoundary |
| 203 | + } else { |
| 204 | + let contentType = "multipart/form-data; boundary=\(boundary)" |
| 205 | + additionalHeaders["Content-Type"] = contentType |
| 206 | + } |
| 207 | + strings.forEach { key, value in |
| 208 | + data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!) |
| 209 | + data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) |
| 210 | + data.append(value.data(using: .utf8)!) |
| 211 | + } |
| 212 | + data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) |
| 213 | + |
| 214 | + return RequestData(body: data, additionalHeaders: additionalHeaders) |
| 215 | + } |
0 commit comments