Skip to content

Commit 57c14da

Browse files
committed
Include headers in response from iOS and include request body
1 parent ea20459 commit 57c14da

2 files changed

Lines changed: 220 additions & 2 deletions

File tree

packages/capacitor-plugin/ios/Sources/FileTransferPlugin/FileTransferPlugin.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
3232
@objc func downloadFile(_ call: CAPPluginCall) {
3333
do {
3434
let prepData = try validateAndPrepare(call: call, action: .download)
35+
var httpOptions = prepData.httpOptions
36+
let reqData = try getRequestData(call: call, options: &httpOptions)
3537

3638
try manager.downloadFile(
3739
fromServerURL: prepData.serverURL,
3840
toFileURL: prepData.fileURL,
39-
withHttpOptions: prepData.httpOptions
41+
withHttpOptions: httpOptions,
42+
body: reqData
4043
).sink(
4144
receiveCompletion: handleCompletion(call: call, source: prepData.serverURL.absoluteString, target: prepData.fileURL.absoluteString),
4245
receiveValue: handleReceiveValue(
@@ -225,7 +228,7 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
225228
let result: JSObject = {
226229
switch type {
227230
case .download:
228-
return ["path": path]
231+
return ["path": path, "headers": data.headers as JSObject]
229232
case .upload:
230233
return [
231234
"bytesSent": data.totalBytes,
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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

Comments
 (0)