From 53090161ad304ac8d287d6e100c7939c14b9dcb4 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Wed, 26 Feb 2020 11:29:25 -0600 Subject: [PATCH 01/29] iOS GET and POST works for json --- core/src/core-plugin-definitions.ts | 31 ++++ .../app/src/main/assets/capacitor.config.json | 3 + example/src/app/app.component.ts | 3 +- example/src/pages/http/http.html | 22 +++ example/src/pages/http/http.module.ts | 13 ++ example/src/pages/http/http.scss | 4 + example/src/pages/http/http.ts | 60 ++++++++ .../Capacitor/Plugins/DefaultPlugins.m | 4 + ios/Capacitor/Capacitor/Plugins/Http.swift | 140 ++++++++++++++++++ 9 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 example/src/pages/http/http.html create mode 100644 example/src/pages/http/http.module.ts create mode 100644 example/src/pages/http/http.scss create mode 100644 example/src/pages/http/http.ts create mode 100644 ios/Capacitor/Capacitor/Plugins/Http.swift diff --git a/core/src/core-plugin-definitions.ts b/core/src/core-plugin-definitions.ts index 6ae95be5e9..801e1cf04d 100644 --- a/core/src/core-plugin-definitions.ts +++ b/core/src/core-plugin-definitions.ts @@ -11,6 +11,7 @@ export interface PluginRegistry { Filesystem: FilesystemPlugin; Geolocation: GeolocationPlugin; Haptics: HapticsPlugin; + Http: HttpPlugin; Keyboard: KeyboardPlugin; LocalNotifications: LocalNotificationsPlugin; Modals: ModalsPlugin; @@ -904,6 +905,36 @@ export enum HapticsNotificationType { ERROR = 'ERROR' } +// Http + +export interface HttpPlugin { + request(options: HttpOptions): Promise; +} + +export interface HttpOptions { + url: string; + method: string; + params?: HttpParams; + data?: any; + headers?: HttpHeaders; +} + +export interface HttpParams { + [key:string]: string; +} + +export interface HttpHeaders { + [key:string]: string; +} + +export interface HttpResponse { + data: any; + status: number; + headers: HttpHeaders; +} + +// Vibrate + export interface VibrateOptions { duration?: number; } diff --git a/example/android/app/src/main/assets/capacitor.config.json b/example/android/app/src/main/assets/capacitor.config.json index 5bd0006f90..7d42eec986 100644 --- a/example/android/app/src/main/assets/capacitor.config.json +++ b/example/android/app/src/main/assets/capacitor.config.json @@ -6,6 +6,9 @@ "plugins": { "SplashScreen": { "launchShowDuration": 12345 + }, + "LocalNotifications": { + "smallIcon": "ic_stat_icon_config_sample" } } } \ No newline at end of file diff --git a/example/src/app/app.component.ts b/example/src/app/app.component.ts index 0195524525..a40f823bef 100644 --- a/example/src/app/app.component.ts +++ b/example/src/app/app.component.ts @@ -9,7 +9,7 @@ import { Plugins } from '@capacitor/core'; export class MyApp { @ViewChild(Nav) nav: Nav; - rootPage = 'AppPage'; + rootPage = 'HttpPage'; PLUGINS = [ { name: 'App', page: 'AppPage' }, @@ -22,6 +22,7 @@ export class MyApp { { name: 'Filesystem', page: 'FilesystemPage' }, { name: 'Geolocation', page: 'GeolocationPage' }, { name: 'Haptics', page: 'HapticsPage' }, + { name: 'Http', page: 'HttpPage' }, { name: 'Keyboard', page: 'KeyboardPage' }, { name: 'LocalNotifications', page: 'LocalNotificationsPage' }, { name: 'Modals', page: 'ModalsPage' }, diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html new file mode 100644 index 0000000000..57ac4c24c2 --- /dev/null +++ b/example/src/pages/http/http.html @@ -0,0 +1,22 @@ + + + + + Http + + + + + + + + + +

Output

+ +
\ No newline at end of file diff --git a/example/src/pages/http/http.module.ts b/example/src/pages/http/http.module.ts new file mode 100644 index 0000000000..24d45cfbb9 --- /dev/null +++ b/example/src/pages/http/http.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { HttpPage } from './http'; + +@NgModule({ + declarations: [ + HttpPage, + ], + imports: [ + IonicPageModule.forChild(HttpPage), + ], +}) +export class HttpPageModule { } diff --git a/example/src/pages/http/http.scss b/example/src/pages/http/http.scss new file mode 100644 index 0000000000..c07c459aa2 --- /dev/null +++ b/example/src/pages/http/http.scss @@ -0,0 +1,4 @@ +ion-textarea { + border: 1px solid #eee; + height: 500px; +} \ No newline at end of file diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts new file mode 100644 index 0000000000..465b292eb6 --- /dev/null +++ b/example/src/pages/http/http.ts @@ -0,0 +1,60 @@ +import { Component } from '@angular/core'; +import { IonicPage, NavController, NavParams } from 'ionic-angular'; + +import { Plugins } from '@capacitor/core'; + +/** + * Generated class for the KeyboardPage page. + * + * See https://ionicframework.com/docs/components/#navigation for more info on + * Ionic pages and navigation. + */ + +@IonicPage() +@Component({ + selector: 'page-http', + templateUrl: 'http.html', +}) +export class HttpPage { + url: string = 'https://jsonplaceholder.typicode.com'; + output: string = ''; + + constructor(public navCtrl: NavController, public navParams: NavParams) { + } + + ionViewDidLoad() { + console.log('ionViewDidLoad KeyboardPage'); + } + + async get() { + this.output = ''; + + const ret = await Plugins.Http.request({ + method: 'GET', + url: `${this.url}/posts/1` + }); + console.log('Got ret', ret); + + this.output = JSON.stringify(ret.data); + } + + async post() { + this.output = ''; + + const ret = await Plugins.Http.request({ + url: `${this.url}/posts`, + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + data: { + title: 'foo', + body: 'bar', + userId: 1 + } + }); + console.log('Got ret', ret); + this.output = JSON.stringify(ret.data); + } + +} \ No newline at end of file diff --git a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m index 8d8b4e895a..dc8821bf9a 100644 --- a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m +++ b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m @@ -73,6 +73,10 @@ CAP_PLUGIN_METHOD(vibrate, CAPPluginReturnNone); ) +CAP_PLUGIN(CAPHttpPlugin, "Http", + CAP_PLUGIN_METHOD(request, CAPPluginReturnPromise); +) + CAP_PLUGIN(CAPKeyboard, "Keyboard", CAP_PLUGIN_METHOD(show, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(hide, CAPPluginReturnPromise); diff --git a/ios/Capacitor/Capacitor/Plugins/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http.swift new file mode 100644 index 0000000000..f413a73f59 --- /dev/null +++ b/ios/Capacitor/Capacitor/Plugins/Http.swift @@ -0,0 +1,140 @@ +import Foundation +import AudioToolbox + +@objc(CAPHttpPlugin) +public class CAPHttpPlugin: CAPPlugin { + + @objc public func request(_ call: CAPPluginCall) { + guard let urlValue = call.getString("url") else { + return call.reject("Must provide a URL") + } + guard let method = call.getString("method") else { + return call.reject("Must provide a method. One of GET, DELETE, HEAD PATCH, POST, or PUT") + } + + guard let url = URL(string: urlValue) else { + return call.reject("Invalid URL") + } + + switch method { + case "GET", "HEAD": + get(call, url, method) + case "DELETE", "PATCH", "POST", "PUT": + mutate(call, url, method) + default: + call.reject("Unknown method") + } + } + + // Handle GET operations + func get(_ call: CAPPluginCall, _ url: URL, _ method: String) { + var request = URLRequest(url: url) + request.httpMethod = method + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + if error != nil { + print("Error on GET", data, response, error) + call.reject("Error", error, [:]) + return + } + + let res = response as! HTTPURLResponse + + call.resolve(self.buildResponse(data, res)) + } + + task.resume() + } + + // Handle mutation operations: DELETE, PATCH, POST, and PUT + func mutate(_ call: CAPPluginCall, _ url: URL, _ method: String) { + let data = call.getObject("data") + + var request = URLRequest(url: url) + request.httpMethod = method + + let headers = (call.getObject("headers") ?? [:]) as [String:Any] + + let contentType = getRequestHeader(headers, "Content-Type") as? String + + if data != nil && contentType != nil { + do { + try setRequestData(request, data!, contentType!) + } catch let e { + call.reject("Unable to set request data", e) + return + } + } + + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + if error != nil { + print("Error on mutate ", data, response, error) + call.reject("Error", error, [:]) + return + } + + let res = response as! HTTPURLResponse + + call.resolve(self.buildResponse(data, res)) + } + + task.resume() + } + + func buildResponse(_ data: Data?, _ response: HTTPURLResponse) -> [String:Any] { + + var ret = [:] as [String:Any] + + ret["status"] = response.statusCode + ret["headers"] = response.allHeaderFields + + let contentType = response.allHeaderFields["Content-Type"] as? String + + if data != nil && contentType != nil && contentType!.contains("application/json") { + if let json = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any] { + print("Got json") + print(json) + // handle json... + ret["data"] = json + } + } + // TODO: Handle other response content types, including binary + /* + else { + ret["data"] = + }*/ + + return ret + } + + func getRequestHeader(_ headers: [String:Any], _ header: String) -> Any? { + var normalizedHeaders = [:] as [String:Any] + headers.keys.forEach { (key) in + normalizedHeaders[key.lowercased()] = headers[key] + } + return normalizedHeaders[header.lowercased()] + } + + func setRequestData(_ request: URLRequest, _ data: [String:Any], _ contentType: String) throws { + if contentType.contains("application/json") { + try setRequestDataJson(request, data) + } else if contentType.contains("application/x-www-form-urlencoded") { + setRequestDataFormUrlEncoded(request, data) + } else if contentType.contains("multipart/form-data") { + setRequestDataMultipartFormData(request, data) + } + } + + func setRequestDataJson(_ request: URLRequest, _ data: [String:Any]) throws { + var req = request + let jsonData = try JSONSerialization.data(withJSONObject: data) + req.httpBody = jsonData + } + + func setRequestDataFormUrlEncoded(_ request: URLRequest, _ data: [String:Any]) { + + } + + func setRequestDataMultipartFormData(_ request: URLRequest, _ data: [String:Any]) { + + } +} From 4bcc2b98c8d51baf363d84b8ca30bb19584ddb0d Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Wed, 26 Feb 2020 12:19:42 -0600 Subject: [PATCH 02/29] HTTP --- example/src/pages/http/http.html | 4 ++++ example/src/pages/http/http.scss | 4 +++- example/src/pages/http/http.ts | 38 +++++++++++++++++++++----------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html index 57ac4c24c2..c0764fee8b 100644 --- a/example/src/pages/http/http.html +++ b/example/src/pages/http/http.html @@ -15,7 +15,11 @@ + + + +

Output

diff --git a/example/src/pages/http/http.scss b/example/src/pages/http/http.scss index c07c459aa2..e28a8a1a9c 100644 --- a/example/src/pages/http/http.scss +++ b/example/src/pages/http/http.scss @@ -1,4 +1,6 @@ ion-textarea { border: 1px solid #eee; - height: 500px; + textarea { + height: 500px; + } } \ No newline at end of file diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index 465b292eb6..cdba47c7c9 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { IonicPage, NavController, NavParams, LoadingController, Loading } from 'ionic-angular'; import { Plugins } from '@capacitor/core'; @@ -19,7 +19,9 @@ export class HttpPage { url: string = 'https://jsonplaceholder.typicode.com'; output: string = ''; - constructor(public navCtrl: NavController, public navParams: NavParams) { + loading: Loading; + + constructor(public navCtrl: NavController, public navParams: NavParams, public loadingCtrl: LoadingController) { } ionViewDidLoad() { @@ -29,32 +31,42 @@ export class HttpPage { async get() { this.output = ''; + this.loading = this.loadingCtrl.create({ + content: 'Requesting...' + }); + this.loading.present(); const ret = await Plugins.Http.request({ method: 'GET', url: `${this.url}/posts/1` }); console.log('Got ret', ret); + this.loading.dismiss(); - this.output = JSON.stringify(ret.data); + this.output = JSON.stringify(ret, null, 2); } - async post() { - this.output = ''; + delete = () => this.mutate('/posts/1', 'DELETE', { title: 'foo', body: 'bar', userId: 1 }); + patch = () => this.mutate('/posts/1', 'PATCH', { title: 'foo', body: 'bar', userId: 1 }); + post = () => this.mutate('/posts', 'POST', { title: 'foo', body: 'bar', userId: 1 }); + put = () => this.mutate('/posts/1', 'PUT', { title: 'foo', body: 'bar', userId: 1 }); + async mutate(path, method, data = {}) { + this.output = ''; + this.loading = this.loadingCtrl.create({ + content: 'Requesting...' + }); + this.loading.present(); const ret = await Plugins.Http.request({ - url: `${this.url}/posts`, - method: 'POST', + url: `${this.url}${path}`, + method: method, headers: { 'content-type': 'application/json' }, - data: { - title: 'foo', - body: 'bar', - userId: 1 - } + data }); console.log('Got ret', ret); - this.output = JSON.stringify(ret.data); + this.loading.dismiss(); + this.output = JSON.stringify(ret, null, 2); } } \ No newline at end of file From eaef1c6f17cb8ea5a68c230713068f5564cc45e0 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Fri, 28 Feb 2020 21:39:53 -0600 Subject: [PATCH 03/29] Fix head --- example/src/pages/http/http.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index cdba47c7c9..15279c57dc 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -28,7 +28,7 @@ export class HttpPage { console.log('ionViewDidLoad KeyboardPage'); } - async get() { + async get(path = '/posts/1', method = 'GET') { this.output = ''; this.loading = this.loadingCtrl.create({ @@ -36,8 +36,8 @@ export class HttpPage { }); this.loading.present(); const ret = await Plugins.Http.request({ - method: 'GET', - url: `${this.url}/posts/1` + method: method, + url: `${this.url}${path}` }); console.log('Got ret', ret); this.loading.dismiss(); @@ -45,6 +45,7 @@ export class HttpPage { this.output = JSON.stringify(ret, null, 2); } + head = () => this.get('/posts/1', 'HEAD'); delete = () => this.mutate('/posts/1', 'DELETE', { title: 'foo', body: 'bar', userId: 1 }); patch = () => this.mutate('/posts/1', 'PATCH', { title: 'foo', body: 'bar', userId: 1 }); post = () => this.mutate('/posts', 'POST', { title: 'foo', body: 'bar', userId: 1 }); From 4c4d36165bd04c282338b03de4e2e4ab7fffade1 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sat, 29 Feb 2020 09:34:19 -0600 Subject: [PATCH 04/29] application/x-www-form-urlencoded support --- example/package-lock.json | 125 +++++++-------------- example/package.json | 2 + example/src/pages/http/http.html | 2 + example/src/pages/http/http.ts | 16 +++ ios/Capacitor/Capacitor/Plugins/Http.swift | 42 ++++--- 5 files changed, 88 insertions(+), 99 deletions(-) diff --git a/example/package-lock.json b/example/package-lock.json index 6b06984a1c..bf536f7040 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -557,7 +557,6 @@ "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, "requires": { "mime-types": "~2.1.24", "negotiator": "0.6.2" @@ -691,8 +690,7 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-unique": { "version": "0.2.1", @@ -967,7 +965,6 @@ "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dev": true, "requires": { "bytes": "3.1.0", "content-type": "~1.0.4", @@ -1137,8 +1134,7 @@ "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, "cache-base": { "version": "1.0.1", @@ -1370,7 +1366,6 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, "requires": { "safe-buffer": "5.1.2" } @@ -1378,8 +1373,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "continuable-cache": { "version": "0.3.1", @@ -1390,14 +1384,12 @@ "cookie": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "copy-descriptor": { "version": "0.1.1", @@ -1594,8 +1586,7 @@ "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "des.js": { "version": "1.0.0", @@ -1610,8 +1601,7 @@ "destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "diff": { "version": "4.0.1", @@ -1697,8 +1687,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { "version": "1.3.306", @@ -1730,8 +1719,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "enhanced-resolve": { "version": "3.4.1", @@ -1887,8 +1875,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", @@ -1938,8 +1925,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "event-emitter": { "version": "0.3.5", @@ -2002,7 +1988,6 @@ "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "dev": true, "requires": { "accepts": "~1.3.7", "array-flatten": "1.1.1", @@ -2039,8 +2024,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" } } }, @@ -2142,7 +2126,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -2196,8 +2179,7 @@ "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" }, "fragment-cache": { "version": "0.2.1", @@ -2210,8 +2192,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fs-extra": { "version": "4.0.3", @@ -2980,7 +2961,6 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dev": true, "requires": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -2992,8 +2972,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, @@ -3024,7 +3003,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -3088,10 +3066,9 @@ "integrity": "sha1-QLja9P16MRUL0AIWD2ZJbiKpjDw=" }, "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", - "dev": true + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, "is-accessor-descriptor": { "version": "0.1.6", @@ -3563,8 +3540,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "mem": { "version": "1.1.0", @@ -3606,14 +3582,12 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "micromatch": { "version": "2.3.11", @@ -3648,20 +3622,17 @@ "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { "version": "1.42.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", - "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==", - "dev": true + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==" }, "mime-types": { "version": "2.1.25", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", - "dev": true, "requires": { "mime-db": "1.42.0" } @@ -3780,8 +3751,7 @@ "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "neo-async": { "version": "2.6.1", @@ -4089,7 +4059,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, "requires": { "ee-first": "1.1.1" } @@ -4223,8 +4192,7 @@ "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "pascalcase": { "version": "0.1.1", @@ -4382,13 +4350,12 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", - "dev": true, + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" + "ipaddr.js": "1.9.1" } }, "proxy-middleware": { @@ -4438,8 +4405,7 @@ "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "querystring": { "version": "0.2.0", @@ -4497,14 +4463,12 @@ "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dev": true, "requires": { "bytes": "3.1.0", "http-errors": "1.7.2", @@ -5044,8 +5008,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass-graph": { "version": "2.2.4", @@ -5096,7 +5059,6 @@ "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dev": true, "requires": { "debug": "2.6.9", "depd": "~1.1.2", @@ -5116,8 +5078,7 @@ "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" } } }, @@ -5125,7 +5086,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dev": true, "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -5174,8 +5134,7 @@ "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, "sha.js": { "version": "2.4.11", @@ -5426,8 +5385,7 @@ "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, "stdout-stream": { "version": "1.4.1", @@ -5666,8 +5624,7 @@ "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "tough-cookie": { "version": "2.4.3", @@ -5798,7 +5755,6 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, "requires": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -5920,8 +5876,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "unset-value": { "version": "1.0.0", @@ -6042,8 +5997,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { "version": "3.3.3", @@ -6064,8 +6018,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "verror": { "version": "1.10.0", diff --git a/example/package.json b/example/package.json index f2df3b3fb9..d3354a0dfa 100644 --- a/example/package.json +++ b/example/package.json @@ -25,6 +25,8 @@ "@angular/http": "5.0.1", "@angular/platform-browser": "5.0.1", "@angular/platform-browser-dynamic": "5.0.1", + "body-parser": "^1.19.0", + "express": "^4.17.1", "ionic-angular": "^3.9.6", "ionicons": "3.0.0", "rxjs": "5.5.2", diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html index c0764fee8b..f5e9382233 100644 --- a/example/src/pages/http/http.html +++ b/example/src/pages/http/http.html @@ -21,6 +21,8 @@ + +

Output

\ No newline at end of file diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index 15279c57dc..cfded7b000 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -70,4 +70,20 @@ export class HttpPage { this.output = JSON.stringify(ret, null, 2); } + + formPost = async () => { + const server = 'http://localhost:3455/form-data'; + + const ret = await Plugins.Http.request({ + url: server, + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + data: { + name: 'Max', + age: 5 + } + }); + } } \ No newline at end of file diff --git a/ios/Capacitor/Capacitor/Plugins/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http.swift index f413a73f59..2cee22fa58 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http.swift @@ -32,7 +32,7 @@ public class CAPHttpPlugin: CAPPlugin { request.httpMethod = method let task = URLSession.shared.dataTask(with: request) { (data, response, error) in if error != nil { - print("Error on GET", data, response, error) + CAPLog.print("Error on GET", data, response, error) call.reject("Error", error, [:]) return } @@ -56,9 +56,13 @@ public class CAPHttpPlugin: CAPPlugin { let contentType = getRequestHeader(headers, "Content-Type") as? String + if contentType != nil { + request.setValue(contentType, forHTTPHeaderField: "Content-Type") + } + if data != nil && contentType != nil { do { - try setRequestData(request, data!, contentType!) + request.httpBody = try getRequestData(request, data!, contentType!) } catch let e { call.reject("Unable to set request data", e) return @@ -67,7 +71,7 @@ public class CAPHttpPlugin: CAPPlugin { let task = URLSession.shared.dataTask(with: request) { (data, response, error) in if error != nil { - print("Error on mutate ", data, response, error) + CAPLog.print("Error on mutate ", data, response, error) call.reject("Error", error, [:]) return } @@ -114,27 +118,39 @@ public class CAPHttpPlugin: CAPPlugin { return normalizedHeaders[header.lowercased()] } - func setRequestData(_ request: URLRequest, _ data: [String:Any], _ contentType: String) throws { + func getRequestData(_ request: URLRequest, _ data: [String:Any], _ contentType: String) throws -> Data? { if contentType.contains("application/json") { - try setRequestDataJson(request, data) + return try setRequestDataJson(request, data) } else if contentType.contains("application/x-www-form-urlencoded") { - setRequestDataFormUrlEncoded(request, data) + return try setRequestDataFormUrlEncoded(request, data) } else if contentType.contains("multipart/form-data") { - setRequestDataMultipartFormData(request, data) + return try setRequestDataMultipartFormData(request, data) } + return nil } - func setRequestDataJson(_ request: URLRequest, _ data: [String:Any]) throws { - var req = request + func setRequestDataJson(_ request: URLRequest, _ data: [String:Any]) throws -> Data? { let jsonData = try JSONSerialization.data(withJSONObject: data) - req.httpBody = jsonData + return jsonData } - func setRequestDataFormUrlEncoded(_ request: URLRequest, _ data: [String:Any]) { + func setRequestDataFormUrlEncoded(_ request: URLRequest, _ data: [String:Any]) -> Data? { + guard var components = URLComponents(url: request.url!, resolvingAgainstBaseURL: false) else { + return nil + } + components.queryItems = [] + data.keys.forEach { (key) in + components.queryItems?.append(URLQueryItem(name: key, value: "\(data[key] ?? "")")) + } + if components.query != nil { + return Data(components.query!.utf8) + } + + return nil } - func setRequestDataMultipartFormData(_ request: URLRequest, _ data: [String:Any]) { - + func setRequestDataMultipartFormData(_ request: URLRequest, _ data: [String:Any]) -> Data? { + return nil } } From a5d534f825f9add3ddf2584dc75ed1818ef681a3 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sat, 29 Feb 2020 11:38:54 -0600 Subject: [PATCH 05/29] Support setting and getting cookies for URLs --- core/src/core-plugin-definitions.ts | 21 ++++++++ example/package-lock.json | 16 ++++++ example/package.json | 1 + example/src/pages/http/http.html | 3 ++ example/src/pages/http/http.ts | 37 +++++++++++-- .../Capacitor/Plugins/DefaultPlugins.m | 2 + ios/Capacitor/Capacitor/Plugins/Http.swift | 54 ++++++++++++++++++- 7 files changed, 130 insertions(+), 4 deletions(-) diff --git a/core/src/core-plugin-definitions.ts b/core/src/core-plugin-definitions.ts index 801e1cf04d..f9f8b2af41 100644 --- a/core/src/core-plugin-definitions.ts +++ b/core/src/core-plugin-definitions.ts @@ -909,6 +909,8 @@ export enum HapticsNotificationType { export interface HttpPlugin { request(options: HttpOptions): Promise; + setCookie(options: HttpSetCookieOptions): Promise; + getCookies(options: HttpGetCookiesOptions): Promise; } export interface HttpOptions { @@ -933,6 +935,25 @@ export interface HttpResponse { headers: HttpHeaders; } +export interface HttpCookie { + key: string; + value: string; +} + +export interface HttpSetCookieOptions { + url: string; + key: string; + value: string; +} + +export interface HttpGetCookiesOptions { + url: string; +} + +export interface HttpGetCookiesResult { + value: HttpCookie[]; +} + // Vibrate export interface VibrateOptions { diff --git a/example/package-lock.json b/example/package-lock.json index bf536f7040..08899fa335 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -1386,6 +1386,22 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, + "cookie-parser": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", + "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/example/package.json b/example/package.json index d3354a0dfa..a6687baf79 100644 --- a/example/package.json +++ b/example/package.json @@ -26,6 +26,7 @@ "@angular/platform-browser": "5.0.1", "@angular/platform-browser-dynamic": "5.0.1", "body-parser": "^1.19.0", + "cookie-parser": "^1.4.4", "express": "^4.17.1", "ionic-angular": "^3.9.6", "ionicons": "3.0.0", diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html index f5e9382233..36f6457baa 100644 --- a/example/src/pages/http/http.html +++ b/example/src/pages/http/http.html @@ -22,6 +22,9 @@ + + +

Output

diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index cfded7b000..d962608916 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams, LoadingController, Loading } from 'ionic-angular'; import { Plugins } from '@capacitor/core'; +import { SERVER_TRANSITION_PROVIDERS } from '@angular/platform-browser/src/browser/server-transition'; /** * Generated class for the KeyboardPage page. @@ -16,7 +17,9 @@ import { Plugins } from '@capacitor/core'; templateUrl: 'http.html', }) export class HttpPage { + serverUrl = 'http://localhost:3455'; url: string = 'https://jsonplaceholder.typicode.com'; + output: string = ''; loading: Loading; @@ -70,12 +73,11 @@ export class HttpPage { this.output = JSON.stringify(ret, null, 2); } + apiUrl = (path: string) => `${this.serverUrl}${path}`; formPost = async () => { - const server = 'http://localhost:3455/form-data'; - const ret = await Plugins.Http.request({ - url: server, + url: this.apiUrl('/form-data'), method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' @@ -86,4 +88,33 @@ export class HttpPage { } }); } + + setCookie = async () => { + const ret = await Plugins.Http.setCookie({ + url: this.apiUrl('/cookie'), + key: 'language', + value: 'en' + }); + } + + getCookies = async () => { + const ret = await Plugins.Http.getCookies({ + url: this.apiUrl('/cookie') + }); + console.log('Got cookies', ret); + this.output = JSON.stringify(ret.value); + } + + testCookies = async () => { + this.loading = this.loadingCtrl.create({ + content: 'Requesting...' + }); + this.loading.present(); + const ret = await Plugins.Http.request({ + method: 'GET', + url: this.apiUrl('/cookie') + }); + console.log('Got ret', ret); + this.loading.dismiss(); + } } \ No newline at end of file diff --git a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m index dc8821bf9a..a8a8e73abf 100644 --- a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m +++ b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m @@ -75,6 +75,8 @@ CAP_PLUGIN(CAPHttpPlugin, "Http", CAP_PLUGIN_METHOD(request, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(setCookie, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(getCookies, CAPPluginReturnPromise); ) CAP_PLUGIN(CAPKeyboard, "Keyboard", diff --git a/ios/Capacitor/Capacitor/Plugins/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http.swift index 2cee22fa58..88b9e2213f 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http.swift @@ -3,7 +3,58 @@ import AudioToolbox @objc(CAPHttpPlugin) public class CAPHttpPlugin: CAPPlugin { - + @objc public func setCookie(_ call: CAPPluginCall) { + + guard let key = call.getString("key") else { + return call.reject("Must provide key") + } + guard let value = call.getString("value") else { + return call.reject("Must provide value") + } + guard let urlString = call.getString("url") else { + return call.reject("Must provide URL") + } + + guard let url = URL(string: urlString) else { + return call.reject("Invalid URL") + } + + let jar = HTTPCookieStorage.shared + let field = ["Set-Cookie": "\(key)=\(value)"] + let cookies = HTTPCookie.cookies(withResponseHeaderFields: field, for: url) + jar.setCookies(cookies, for: url, mainDocumentURL: url) + + call.resolve() + } + + @objc public func getCookies(_ call: CAPPluginCall) { + guard let urlString = call.getString("url") else { + return call.reject("Must provide URL") + } + + guard let url = URL(string: urlString) else { + return call.reject("Invalid URL") + } + + let jar = HTTPCookieStorage.shared + guard let cookies = jar.cookies(for: url) else { + return call.resolve([ + "value": [] + ]) + } + + let c = cookies.map { (cookie: HTTPCookie) -> [String:String] in + return [ + "key": cookie.name, + "value": cookie.value + ] + } + + call.resolve([ + "value": c + ]) + } + @objc public func request(_ call: CAPPluginCall) { guard let urlValue = call.getString("url") else { return call.reject("Must provide a URL") @@ -29,6 +80,7 @@ public class CAPHttpPlugin: CAPPlugin { // Handle GET operations func get(_ call: CAPPluginCall, _ url: URL, _ method: String) { var request = URLRequest(url: url) + request.httpMethod = method let task = URLSession.shared.dataTask(with: request) { (data, response, error) in if error != nil { From ba67fdf3d9230bb140338af2a6624ae198b4bbc7 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sat, 29 Feb 2020 13:41:44 -0600 Subject: [PATCH 06/29] Working on download --- core/src/core-plugin-definitions.ts | 366 +----------------- core/src/plugins/fs.ts | 309 +++++++++++++++ core/src/plugins/http.ts | 66 ++++ example/server/document.pdf | Bin 0 -> 13619 bytes example/src/pages/http/http.html | 3 + example/src/pages/http/http.ts | 10 + .../Capacitor/Plugins/DefaultPlugins.m | 2 + ios/Capacitor/Capacitor/Plugins/Http.swift | 37 ++ 8 files changed, 433 insertions(+), 360 deletions(-) create mode 100644 core/src/plugins/fs.ts create mode 100644 core/src/plugins/http.ts create mode 100644 example/server/document.pdf diff --git a/core/src/core-plugin-definitions.ts b/core/src/core-plugin-definitions.ts index f9f8b2af41..c79f627b10 100644 --- a/core/src/core-plugin-definitions.ts +++ b/core/src/core-plugin-definitions.ts @@ -1,5 +1,11 @@ import { Plugin, PluginListenerHandle } from './definitions'; +import { HttpPlugin } from './plugins/http'; +import { FilesystemPlugin } from './plugins/fs'; + +export * from './plugins/http'; +export * from './plugins/fs'; + export interface PluginRegistry { Accessibility: AccessibilityPlugin; App: AppPlugin; @@ -476,317 +482,6 @@ export interface DeviceBatteryInfo { export interface DeviceLanguageCodeResult { value: string; } -// - -export interface FilesystemPlugin extends Plugin { - /** - * Read a file from disk - * @param options options for the file read - * @return a promise that resolves with the read file data result - */ - readFile(options: FileReadOptions): Promise; - - /** - * Write a file to disk in the specified location on device - * @param options options for the file write - * @return a promise that resolves with the file write result - */ - writeFile(options: FileWriteOptions): Promise; - - /** - * Append to a file on disk in the specified location on device - * @param options options for the file append - * @return a promise that resolves with the file write result - */ - appendFile(options: FileAppendOptions): Promise; - - /** - * Delete a file from disk - * @param options options for the file delete - * @return a promise that resolves with the deleted file data result - */ - deleteFile(options: FileDeleteOptions): Promise; - - /** - * Create a directory. - * @param options options for the mkdir - * @return a promise that resolves with the mkdir result - */ - mkdir(options: MkdirOptions): Promise; - - /** - * Remove a directory - * @param options the options for the directory remove - */ - rmdir(options: RmdirOptions): Promise; - - /** - * Return a list of files from the directory (not recursive) - * @param options the options for the readdir operation - * @return a promise that resolves with the readdir directory listing result - */ - readdir(options: ReaddirOptions): Promise; - - /** - * Return full File URI for a path and directory - * @param options the options for the stat operation - * @return a promise that resolves with the file stat result - */ - getUri(options: GetUriOptions): Promise; - - /** - * Return data about a file - * @param options the options for the stat operation - * @return a promise that resolves with the file stat result - */ - stat(options: StatOptions): Promise; - - /** - * Rename a file or directory - * @param options the options for the rename operation - * @return a promise that resolves with the rename result - */ - rename(options: RenameOptions): Promise; - - /** - * Copy a file or directory - * @param options the options for the copy operation - * @return a promise that resolves with the copy result - */ - copy(options: CopyOptions): Promise; -} - -export enum FilesystemDirectory { - /** - * The Application directory - */ - Application = 'APPLICATION', - /** - * The Documents directory - */ - Documents = 'DOCUMENTS', - /** - * The Data directory - */ - Data = 'DATA', - /** - * The Cache directory - */ - Cache = 'CACHE', - /** - * The external directory (Android only) - */ - External = 'EXTERNAL', - /** - * The external storage directory (Android only) - */ - ExternalStorage = 'EXTERNAL_STORAGE' -} - -export enum FilesystemEncoding { - UTF8 = 'utf8', - ASCII = 'ascii', - UTF16 = 'utf16' -} - -export interface FileWriteOptions { - /** - * The filename to write - */ - path: string; - /** - * The data to write - */ - data: string; - /** - * The FilesystemDirectory to store the file in - */ - directory?: FilesystemDirectory; - /** - * The encoding to write the file in. If not provided, data - * is written as base64 encoded data. - * - * Pass FilesystemEncoding.UTF8 to write data as string - */ - encoding?: FilesystemEncoding; - /** - * Whether to create any missing parent directories. - * Defaults to false - */ - recursive?: boolean; -} - -export interface FileAppendOptions { - /** - * The filename to write - */ - path: string; - /** - * The data to write - */ - data: string; - /** - * The FilesystemDirectory to store the file in - */ - directory?: FilesystemDirectory; - /** - * The encoding to write the file in. If not provided, data - * is written as base64 encoded data. - * - * Pass FilesystemEncoding.UTF8 to write data as string - */ - encoding?: FilesystemEncoding; -} - -export interface FileReadOptions { - /** - * The filename to read - */ - path: string; - /** - * The FilesystemDirectory to read the file from - */ - directory?: FilesystemDirectory; - /** - * The encoding to read the file in, if not provided, data - * is read as binary and returned as base64 encoded data. - * - * Pass FilesystemEncoding.UTF8 to read data as string - */ - encoding?: FilesystemEncoding; -} - -export interface FileDeleteOptions { - /** - * The filename to delete - */ - path: string; - /** - * The FilesystemDirectory to delete the file from - */ - directory?: FilesystemDirectory; -} - -export interface MkdirOptions { - /** - * The path of the new directory - */ - path: string; - /** - * The FilesystemDirectory to make the new directory in - */ - directory?: FilesystemDirectory; - /** - * Whether to create any missing parent directories as well. - * Defaults to false - */ - recursive?: boolean; -} - -export interface RmdirOptions { - /** - * The path of the directory to remove - */ - path: string; - /** - * The FilesystemDirectory to remove the directory from - */ - directory?: FilesystemDirectory; - /** - * Whether to recursively remove the contents of the directory - * Defaults to false - */ - recursive?: boolean; -} - -export interface ReaddirOptions { - /** - * The path of the directory to read - */ - path: string; - /** - * The FilesystemDirectory to list files from - */ - directory?: FilesystemDirectory; -} - -export interface GetUriOptions { - /** - * The path of the file to get the URI for - */ - path: string; - /** - * The FilesystemDirectory to get the file under - */ - directory: FilesystemDirectory; -} - -export interface StatOptions { - /** - * The path of the file to get data about - */ - path: string; - /** - * The FilesystemDirectory to get the file under - */ - directory?: FilesystemDirectory; -} - -export interface CopyOptions { - /** - * The existing file or directory - */ - from: string; - /** - * The destination file or directory - */ - to: string; - /** - * The FilesystemDirectory containing the existing file or directory - */ - directory?: FilesystemDirectory; - /** - * The FilesystemDirectory containing the destination file or directory. If not supplied will use the 'directory' - * parameter as the destination - */ - toDirectory?: FilesystemDirectory; -} - -export interface RenameOptions extends CopyOptions {} - -export interface FileReadResult { - data: string; -} -export interface FileDeleteResult { -} -export interface FileWriteResult { - uri: string; -} -export interface FileAppendResult { -} -export interface MkdirResult { -} -export interface RmdirResult { -} -export interface RenameResult { -} -export interface CopyResult { -} -export interface ReaddirResult { - files: string[]; -} -export interface GetUriResult { - uri: string; -} -export interface StatResult { - type: string; - size: number; - ctime: number; - mtime: number; - uri: string; -} - -// export interface GeolocationPlugin extends Plugin { /** @@ -905,55 +600,6 @@ export enum HapticsNotificationType { ERROR = 'ERROR' } -// Http - -export interface HttpPlugin { - request(options: HttpOptions): Promise; - setCookie(options: HttpSetCookieOptions): Promise; - getCookies(options: HttpGetCookiesOptions): Promise; -} - -export interface HttpOptions { - url: string; - method: string; - params?: HttpParams; - data?: any; - headers?: HttpHeaders; -} - -export interface HttpParams { - [key:string]: string; -} - -export interface HttpHeaders { - [key:string]: string; -} - -export interface HttpResponse { - data: any; - status: number; - headers: HttpHeaders; -} - -export interface HttpCookie { - key: string; - value: string; -} - -export interface HttpSetCookieOptions { - url: string; - key: string; - value: string; -} - -export interface HttpGetCookiesOptions { - url: string; -} - -export interface HttpGetCookiesResult { - value: HttpCookie[]; -} - // Vibrate export interface VibrateOptions { diff --git a/core/src/plugins/fs.ts b/core/src/plugins/fs.ts new file mode 100644 index 0000000000..d4bef27789 --- /dev/null +++ b/core/src/plugins/fs.ts @@ -0,0 +1,309 @@ +import { Plugin } from '../definitions'; + +export interface FilesystemPlugin extends Plugin { + /** + * Read a file from disk + * @param options options for the file read + * @return a promise that resolves with the read file data result + */ + readFile(options: FileReadOptions): Promise; + + /** + * Write a file to disk in the specified location on device + * @param options options for the file write + * @return a promise that resolves with the file write result + */ + writeFile(options: FileWriteOptions): Promise; + + /** + * Append to a file on disk in the specified location on device + * @param options options for the file append + * @return a promise that resolves with the file write result + */ + appendFile(options: FileAppendOptions): Promise; + + /** + * Delete a file from disk + * @param options options for the file delete + * @return a promise that resolves with the deleted file data result + */ + deleteFile(options: FileDeleteOptions): Promise; + + /** + * Create a directory. + * @param options options for the mkdir + * @return a promise that resolves with the mkdir result + */ + mkdir(options: MkdirOptions): Promise; + + /** + * Remove a directory + * @param options the options for the directory remove + */ + rmdir(options: RmdirOptions): Promise; + + /** + * Return a list of files from the directory (not recursive) + * @param options the options for the readdir operation + * @return a promise that resolves with the readdir directory listing result + */ + readdir(options: ReaddirOptions): Promise; + + /** + * Return full File URI for a path and directory + * @param options the options for the stat operation + * @return a promise that resolves with the file stat result + */ + getUri(options: GetUriOptions): Promise; + + /** + * Return data about a file + * @param options the options for the stat operation + * @return a promise that resolves with the file stat result + */ + stat(options: StatOptions): Promise; + + /** + * Rename a file or directory + * @param options the options for the rename operation + * @return a promise that resolves with the rename result + */ + rename(options: RenameOptions): Promise; + + /** + * Copy a file or directory + * @param options the options for the copy operation + * @return a promise that resolves with the copy result + */ + copy(options: CopyOptions): Promise; +} + +export enum FilesystemDirectory { + /** + * The Application directory + */ + Application = 'APPLICATION', + /** + * The Documents directory + */ + Documents = 'DOCUMENTS', + /** + * The Data directory + */ + Data = 'DATA', + /** + * The Cache directory + */ + Cache = 'CACHE', + /** + * The external directory (Android only) + */ + External = 'EXTERNAL', + /** + * The external storage directory (Android only) + */ + ExternalStorage = 'EXTERNAL_STORAGE' +} + +export enum FilesystemEncoding { + UTF8 = 'utf8', + ASCII = 'ascii', + UTF16 = 'utf16' +} + +export interface FileWriteOptions { + /** + * The filename to write + */ + path: string; + /** + * The data to write + */ + data: string; + /** + * The FilesystemDirectory to store the file in + */ + directory?: FilesystemDirectory; + /** + * The encoding to write the file in. If not provided, data + * is written as base64 encoded data. + * + * Pass FilesystemEncoding.UTF8 to write data as string + */ + encoding?: FilesystemEncoding; + /** + * Whether to create any missing parent directories. + * Defaults to false + */ + recursive?: boolean; +} + +export interface FileAppendOptions { + /** + * The filename to write + */ + path: string; + /** + * The data to write + */ + data: string; + /** + * The FilesystemDirectory to store the file in + */ + directory?: FilesystemDirectory; + /** + * The encoding to write the file in. If not provided, data + * is written as base64 encoded data. + * + * Pass FilesystemEncoding.UTF8 to write data as string + */ + encoding?: FilesystemEncoding; +} + +export interface FileReadOptions { + /** + * The filename to read + */ + path: string; + /** + * The FilesystemDirectory to read the file from + */ + directory?: FilesystemDirectory; + /** + * The encoding to read the file in, if not provided, data + * is read as binary and returned as base64 encoded data. + * + * Pass FilesystemEncoding.UTF8 to read data as string + */ + encoding?: FilesystemEncoding; +} + +export interface FileDeleteOptions { + /** + * The filename to delete + */ + path: string; + /** + * The FilesystemDirectory to delete the file from + */ + directory?: FilesystemDirectory; +} + +export interface MkdirOptions { + /** + * The path of the new directory + */ + path: string; + /** + * The FilesystemDirectory to make the new directory in + */ + directory?: FilesystemDirectory; + /** + * Whether to create any missing parent directories as well. + * Defaults to false + */ + recursive?: boolean; +} + +export interface RmdirOptions { + /** + * The path of the directory to remove + */ + path: string; + /** + * The FilesystemDirectory to remove the directory from + */ + directory?: FilesystemDirectory; + /** + * Whether to recursively remove the contents of the directory + * Defaults to false + */ + recursive?: boolean; +} + +export interface ReaddirOptions { + /** + * The path of the directory to read + */ + path: string; + /** + * The FilesystemDirectory to list files from + */ + directory?: FilesystemDirectory; +} + +export interface GetUriOptions { + /** + * The path of the file to get the URI for + */ + path: string; + /** + * The FilesystemDirectory to get the file under + */ + directory: FilesystemDirectory; +} + +export interface StatOptions { + /** + * The path of the file to get data about + */ + path: string; + /** + * The FilesystemDirectory to get the file under + */ + directory?: FilesystemDirectory; +} + +export interface CopyOptions { + /** + * The existing file or directory + */ + from: string; + /** + * The destination file or directory + */ + to: string; + /** + * The FilesystemDirectory containing the existing file or directory + */ + directory?: FilesystemDirectory; + /** + * The FilesystemDirectory containing the destination file or directory. If not supplied will use the 'directory' + * parameter as the destination + */ + toDirectory?: FilesystemDirectory; +} + +export interface RenameOptions extends CopyOptions {} + +export interface FileReadResult { + data: string; +} +export interface FileDeleteResult { +} +export interface FileWriteResult { + uri: string; +} +export interface FileAppendResult { +} +export interface MkdirResult { +} +export interface RmdirResult { +} +export interface RenameResult { +} +export interface CopyResult { +} +export interface ReaddirResult { + files: string[]; +} +export interface GetUriResult { + uri: string; +} +export interface StatResult { + type: string; + size: number; + ctime: number; + mtime: number; + uri: string; +} diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts new file mode 100644 index 0000000000..443cf56a5a --- /dev/null +++ b/core/src/plugins/http.ts @@ -0,0 +1,66 @@ +import { FilesystemDirectory } from './fs'; +import { Plugin } from '../definitions'; + +export interface HttpPlugin extends Plugin { + request(options: HttpOptions): Promise; + setCookie(options: HttpSetCookieOptions): Promise; + getCookies(options: HttpGetCookiesOptions): Promise; + uploadFile(options: HttpUploadFileOptions): Promise; + downloadFile(options: HttpDownloadFileOptions): Promise; +} + +export interface HttpOptions { + url: string; + method?: string; + params?: HttpParams; + data?: any; + headers?: HttpHeaders; +} + +export interface HttpParams { + [key:string]: string; +} + +export interface HttpHeaders { + [key:string]: string; +} + +export interface HttpResponse { + data: any; + status: number; + headers: HttpHeaders; +} + +export interface HttpUploadFileOptions extends HttpOptions { + filePath: string; + fileDirectory?: FilesystemDirectory; +} + +export interface HttpDownloadFileOptions extends HttpOptions { + filePath: string; + fileDirectory?: FilesystemDirectory; +} + +export interface HttpUploadFileOptions { + url: string; + filePath: string; +} + +export interface HttpCookie { + key: string; + value: string; +} + +export interface HttpSetCookieOptions { + url: string; + key: string; + value: string; +} + +export interface HttpGetCookiesOptions { + url: string; +} + +export interface HttpGetCookiesResult { + value: HttpCookie[]; +} diff --git a/example/server/document.pdf b/example/server/document.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f0d0e16d47b326a677df4adb23f6a6943caf945e GIT binary patch literal 13619 zcmch81yEegwl2Zl9fG?KFt`s+u;A|QGB^Z+2bT~aIKhGhm*DR1?(XjL$bbHO>zsS; zt9R>Fy*IO`x>tAaUcJ8cb=T~fwWyWEC7Ib+I1#B^Z*yJ{*~tK84#w7q0s>^La#r?c zM$T4VW~O9pZxR_B2NxSJ8LKpz4jC5@HyIl{7nvRzt0EZ}8LPxwCkGHn#;WjVsOn!5 z2bqu%qM5zvpXtc{f4>kp{>czkPe(H{R!wCy^EWfh>@C2SWSjspR!J*cu$l8)X=?;F z6E`z)Fnt^Kk5b9V+0KRR4+vrowyt*eE@W@EDLI>&TA6?yoZn1;vyF^Z;|&QmGWNI6 z8xAt|re+>*4R0dnKV)+UGB&oiWhj%eDmyrU-=_GhuJVQ`85{Rs^SS+v+kb_JRYsgl zM*v{N#ttw70E~@z*o@c#yryh#g*lHIkFk*%4<|2B=>I%I?~fB)z|Lkyc8DHnCdQ^~ z7KX-#JP1_D-vTrxMFJ{N#h2hPm_ZCxHKSh+!;F^Jwl5U9Zd{6AOH3;koRBBH#9Uf zGc?p5g@A_5Bv^wOFo!k94Wg4~#^(SmS*)2Du7xw$B@gzrK|uWOfVus5>$(0(yO@JL z*vua6LdN|Mx5~ef!wBT?hXR1NH`g0SxOv(CFyW0%Zv+3+G-Wdv2Ulm4H)H?sOTq&z zr2>8<;9rsy#~*I}p~)Y9NxrH0t638G#*_bSR&sVQQ85F*DPn!wyl>mh%me(!Kf5;o zL4TKGf0r_E`v1(N0(N#a0smtmE^iaD{sZ4XnEw;Fe=PYAcl@{D{tYX~8+-qmR@17( z)|~+4mPdLYlBpWg!FZ#ivGS`Xj5#Pj;K*o( z(m9K5mA9vuBU-z!d>|HwFTYK5en#klntT1ct#zqhs$_5HWrri8OXzPt7my!3$TBh) zeUXs@q3mI#XM*j zcs4}yRXE^+63-~aH)S8ct*{Z0UTZlYKD~Q9B>`9t_SLSacGA~d4n#uDX@VL=V=C5J zrb5V1g^=7e($>sd}9(`PSIC!Bmn<+V=icn(025$ z^{`bCDzd~-ux*zBKJGoNq$4pxC&nc?UIoMj#VblMafBiC23>SF0MiM$MH&4WvI~BNA2ek25qQNrrlIW@ibH`wZ-en~W8El^6W-#WhE&@0C{ zMBKAZh3DTiB?GSTh+~{;9+Hi}G zX*&*!>3ARJ7qm(#)N`)rh_``y2W55i~jssRbV8wCGP>- z5jNT}(r2*F8}0WQwQJW%V++OOof{=dfM6JFhkjSmnp0O&Hr6@LT&PF#CdORM9Rsn5 z4@yR46KWY{3#>z%ANdOoDn$!|UYy1{bF^Pz0s3l40mkZQ3-s0PI}(A2J0XEk59}9A zUdUb^l2AN~&~+%DAP-1=@u%2L84rYWN)Li_6AzekEFMfx7@_Suf~N30cMs+ZcAqx| zpV}*kRz#jKS0bCB3)YysCW%`ETFF15I8eTDHwE6I^g66J_eD6uu1GzhMpL{%oum7p z^`iUvZ-xnXz=a7r9eqZlWEEje6OU|@3VS`@f4%j6(0JVudcIseniPJ-$$Xh-^?4|0 z-Ix@<4_17=^Gft^RrgAIf5rW>Ir;i_9s4}AJ_h zt*Bv)UoZM*i^~2AO{2|g-L<5KWHeRvDqUq4&XPOK^`tyZ2g&{x4P}zXp;fRvj&n+X?H|?D-{Y_inRTTOgj>h1SvbtWL14#WA(Q z3ZncA2D|6rFq#hsb0c6sdJFo1mP|_ls5MJ?2vQlM^sHXJ67?3h2^gl3@^M9!369L^ zb3N8mUT`NF&3E7p5|7a2+{w7daRcwOC0@ARDk6PyJ&?m_!wO~^8-j%uAcK5&t^Nh+ z_yztvL3mQKpK4p0d>^yg6fQ%0*TjLe<2z}>>E8C8F1Y}0bWCLTq`Tk)QJUS7AZaXg zynKGF78qcc>yE_i@yWtyd3`UF$g#yi>R3u9_9&0WOS4|lNA>Ar!j!jwaXSt|*6uyj zQ%bT^E}|86E2Tp`+CC3o=u1Whx;<`PM48p0;}l-@Yzs+qpL>@AVjs|%TuXc=9%W0? z8O}3>m^^brnJh-p;eD=?~J1eB-3o5G+>oeFu(R_4Vf3G?Jc4j4dWqEIY(+LI35!c6~L$TAP z996MqdT*Rmgx@bc+3^HZnub7Zq%$Q&3^&2NlF;IU!I{?6|MppN3u0tKXAApgUaq-m zRBRuf@0&vVE`_#Y>b`kH@JYVI1Sxc>A$AF}rIt}LKDK_4dyt7Z;|kPiIP3<+@)fxegLZqu2L4Y><)FqjMG(htPh|ZXX zD9hf2e#$*O^LXM5G;&DD^houutmPcO2+u@bvk~V{Em1>wcpC)iA7*kCKcTyK8(RIQ zQNdEw&q6WgkD_e+9KPrT4yKBNkB9n2!P$}iUFFKI$$=Ob#ZmQ+^5zBboB)s!V3wzi zaD>whsSDcaX^zGiZNhny$nk*Fns!J>qGJ zxBDZs9xHQkTrtd26s5lR?NGcyJenEGO)pofM8D#L*obJJJh#6l%UD1YPKLcp3deg6@#yjm2Q}4=b?i*S8n8Mo?I7|{;&3rzLs7hB@^1yt4ulRfp zko1CF=&n19*2J6-v#=LfNm8k>a5?|wi79nYT?_cQ7qGO_54JFE8VlMK47u$Lr4TZ1 zBbrxkawYMHtHzwN3e29EQ0h+}@BR^vh;$*fwY`H(`b`iltqyQG%Ze27`Yy~VaZXL^ ziG?L#pTx0(VNutw9x-nU+;i2AqC)&CqW;^@rxX))Dx}-rZ=;Kx>A0*jvLQKgp*%oq z6K)_VQJWR+C&&0ZL&mxk|U z(pHB&IWFkwHAU&&Ss7G1a@Fb4Vhm~uo11Epr;mYzJ@>uSL?Nc_3V|3}l+H#;tyZ*7 zQRHdazbLfRJgPLd^ATQ75r?V-m#BH-L^z>?Cl^v!_0(%FYfQNHgy?#`-dgakJ*%!Dw99e)efOG*Q0 z%I7#g)(@amc6=`k@Z-t9+;cJ4pCDpggTtKIv^vO_X2x@^T@#Mv5;(Wu$4#P8$dhPLkapc{akLBd2_Ec!xd(t40{p09bH zFtu*{URe?2n875juWNocx-S+~P8-7a#SB8I1{p=9!pp zO{oSjo1G0JufoMgk&Fha*fb-r+4cI&Wy$Mh$y|-f^bTcFBX-?G@91lqvRau2f;W5A z5WfHi#>GD;<$P4XMXD0dqi4$B6h`4bxKG!9uT4ElK5$pA%m1;PK}>eQp=64Hci8%T z&$h;ryHle5^ZrMu38)4OYrOJK4|w&VwB;3he5H{5d@c_KASfa$CIf<1{YCv*0;|+R4O72I zp$BZ=qUN7XwWE%Fl#gW}kYiRggI#LEuu;}e-|;mG%+G1mKBXIOz1~$nMU3`(XsF#) zS+`y0JSgpJWX4QO)kh2FQ0cd29&fnOYw#8RhRE!#G1$w$6?`~f651~|5V(6-93Zwi zuWJy0FMNFR44_Qt_Lg)PclgSyr&88f?-y@onq5$+vMJGhFict9V5xwV~%9)k9vB5!U21D@ni zm>x8%NRD98j~(ahiWYN4LanjU82parl+7?O#+t8xO35F-`zt0CJuDOs;N5pfyK(F$ zcg_uFVE)botRihc*{7G8RHyump#vEMo;`!rnM>t{y4DV;ap6WaCk#oc=wWO#`x ztV^$NC#9Al$z4q8g)#bYM>}z~lxdM8{6d7mCM77iq@h*;**RC@fPeYt&^|yL#77nf zKbOdms11i`cJ{dD=cl5T2gNuneIJu1|5!Y^N0nZP#Yy5GkZK(Xo6v5H4m?)~nS_Y9 z>8T1%_=SZdW%#ty!$3P`b4pBtPKA>C5S&^qUJN5@Y66pf!xGFODT@`EBgCf+MMVG0 z1VyK>pJz^nHaiGqT0dD&g{g|%#9~v0N06!Mby5nr#G$tEs_8IpB8a5aDQfHSzLvgi z*jtlGwd44Au_2fw#^rL#qYuDI0@RFp-nUNsTGDd@O%hr;?3_Vi_svZ@AS=txzT+A# zFgc2~SmnHX4=C*h!@+IhavScTel7trm>q`=no{Mg!@iw@{_(CfH^^0J*6obm>J@EM zjUU~+yv)2QR3Dx2ql3;y7LIf9Cg>g%Y6+y)Va9d*J^i~94vZiR+3ts6?*gtAuoyP3 zT+($hS9X8Zb=oB3%wn-)?+%uisUTl%Edrl?llf4m>O21kI_2D&fO4Nr<9;eyvB`VJ z^?qJ4Q@pyldiqF4R94u+z7i>q(wTA6n_+Dv+s8C++C&1`N^`((b;u$?WlT#RV}lK< za_S{(4LxJ2R9;@;pVF^XOY~Nl@nO0YRGFv~l`5~vb!wI|_IAiI{d818`jYDV;$*+2 zGR_#?&hIEBgSech6(R0-LyOF@; zNh8d@C-@VrwX_Qgu)qfRGSwC)0Oo!ohzn&*3xBad9v*huXo-$-=h1ud&l=Yr!%X{a z>%Dn+`{WzpPMkc(=hPMNjvK#pVe?Bf0o#pL*!h?*Csh+UsvT0~{TI@G_O(9dTT2Day8Rvd!h+w)_xGTkgLx9lVdk#x83TJsft7{9jb5KP$GR4|j#`>*rZXAb%5>SUAB?U1MrU=bPg;vDP@63} z^EmA5OLh^eGi)5!x|b4c)+?2Uv7gPKxqLNO^Mvz-OJ@UC3LGZt_Uk08q7Ny(0u>eg zEhQNCI6JnXf$@?+%j$j1Fr98YW6dMXn27Hz=hf1ps$G=HnBDyiEf_gj?{r z;Ne$)p81YGe8+Crr^uP+{!8cOX7c2FUx!U^+`}*}+T%hoVGj;Z?NG zwr4Fk%xHY;Xv=>ZSbqBY(QemdDOkaS9-9{7JI_P;61^Xt52m{|4^{!j`OJGld*E(q zT>tR}#;uck7c&E2X!m;c4-AZ`Nn3!9+&3U)9TjD}Ur)dN3a7cu$)x?l*2)fQW8+ee zPS)@9kwUh4KO?zcLShg3n%B;jvQJEOEcE+AHXjtCKcsyzbZptz!_<1|rj3gs&f zWjPkqIHK7;nNBr3bsB#Q4skslxV1S=VIXp|U`d&Kh{dnBlk%L*O&j#m-k%;!O0@U< zxeGC3Lg1B;oQB2Wu{N4b*f_JsCO;-OH9VoZk84c`ZV>q0r@uwK&XUAy4U>*q@Ol&PbqMCq^%kZ+>e zmSJOJ;xfbv!P2ZWK*9X3LOj0jLU0I$+(9M~J^U^Unx9+X^KY`WB17(m%#ULqxu;M{t6c^HiV_C?2%B(ST z1nqIy=f05G?3pohbQDHuU6$dR0w^#>Wr{7Ok@NR3A!Sg0R@K!V%~m7A?M%HlZNH!g z`Dwcern?w-9bKcBv@t!fmo4z)`?$bLD2`5=F1hKD$B0^{hMm5R#9o&4W~;*EcC}(s zUrUL@krmke{u`@FE=l{=vfJLn5ypf2kM*zH7Wz+z5uq!fN&OhS#9hZ0TSqfXRX5@| zi4DBVE~gD-r_c4EsXaTsW(9}ns{LWIy|6emY~%;gR9Eq&qOLfe@79lMt2UWua;>L= z^s_#b9VZ>OntSd@zhfZpgHgT>W*7z98qIpR2C*;;zc_K1w7ScQ^#|ic5%9ZBNkp5n zIVxUu<%F`+e4VPjYHEMdkl9i(qP_9M_v8ovOg@lip2#d_{g9fdo!Ox77x+?C4Yx@$ z>?I30)I+eJEe|i@)>Ebs%v9zL#b26ku=cIVUeL5F;$od)o?+!;;$q=q<~l_^8Ywr2 zo?xGVjgd>WD`X!*8zQalXr(A+KBno^%V!R>A8at2dk#6ye`G%)t!6y4ZA*E3R3NOR z#T%J1%S}Z#4!{h(v76=wiD@u zaxr63liuQz^i!oE@Rq1UIH1<>>hV@n6)=bH^aaHNl_pAl_WFNq!(L*^Soci|-jVTe zIL?U(*YClgrt8s}NLLy-o5I*u>P08O$QNnJ zKtGe&QN)FiQSk#(jL&I7MXR36;*X9 z@AWyI?iHp7I3Nkc=vOc=r}g~RJM`YZx7?p=K!1v2+Ar7l-d~ueoN(|uiKfqkN#P!; zKkA>aE&o|#bZlI}!GB&9ST}6#aZ=gokTAYN&3tY42 z^745lmq$$7et+R$UWfILvZ}heZx*Q10Hr%ti-vrZ$QWB-QKcI$mkKv(moP5^?Ktp| zHSIql0?^)8m8LfLZulZz-Qeu&Sz0Tzn7>?t^R9W1%T9AKPcUcN@^-R&jfT>lMTr~9 zkG{8YpxCA{aV(RckO`5!D-$l7v}NA$|3j75|Ezpa};%+`}eave1GUlqonB%B|~1Px45Xz zLzO20cygCM+0ub5BTsgyQW*#L@5?vHCL#2l0u#Uqfez=;VV*uAI{Tce0eu_|oK-`I zFq<%gA6jPmCXHQAGEZJEjdnkLO_pk+7g5lN(E1eTZunp9@4C)Qe@nfrV4Tw|mZ~S% zRxP5Cc@R4}?Yjj+VW}rfo6B+&5=;IdAF=07i6m@4tO(G!nXo+-WxaNrh!>oxj}_e7 z%jFR3!SwD^*quXL_}QEgxokuIQz^sJP{OU)7&#bw5-^FK0GP&R`-mlbG*G)?Xw`or zZpN(O79e$EB{`|+=uWwK{H>LDKTa{{QQmQIISq7u3N0F{7vWI-G&NgxxKnWt<1#1MVwd z@^3LH+GB+VyH#ZiIa8!L1cU?PKthx-LeXLF{p(iwRuY{(#;NlgSm1~ZmAi&sSJMQ}UwV5h} za$P)zvNQ76>w+e#9V;-1t{o>GV$hdv6v`2rr{$#xVR106yoA34{ibJR7WMZ zJ5^Q+!m1bJT;ZnkjZqa`*6qiHRzXEYDEq~##n&uRISrtwr}MFNACp)jK^;-h)H_1^ z2A>PhI;ixITrb*j0S3HWb?He)9VSns$4?_`&ut!#4XY)t3%r|)Jg@)gqS}CexbnX9 zFb)vQ^Y|q!Bt#N6}_=b*&^@*b<*eS(QW6?-R%;aMun?o z-!%-vwoSwkCkdxdBi^jRRFz$Oz{13AJ76z9-hqaMvh(!U>kR_tPfyH#TaffSE860$ zVDWGoup?nP_#77qSG==`XM8^w+g`2xxS+haht|(Poicxy5TDSV(I>J#is~kQO%yaOMI7Aa97qtP`a=UM-sYor^x?uRB zvckXB$eC*Y2;PkC>B7Y=R9!P|`2kTNKph%^YH_PK7QLU2ylHx8B<1i+ck0zn&!BL; z%XA!cmjpQ=d{H)iHydz;cO!^IVnbSa`r~0y;I82A;C@`$pcf?fY97 z{oeaj-m;kJkK5C1MNNE?i5k6y7sip1f7SXt#N_-Un@Wp$9K!qS96xdHn$6%c?f4L1 zxD{?SQA!GB9H#VnTFAA8&N6TKP$czl`6x$5vW6&bUg(clIIncvlc!m5D1zCZXS=J- z!uz?eqw>DKN*Yo#h2kjnep*%R*do!r*e*w>F&SaVy-&98Ql*q-2}Cob&p+%ob*k4S zS3sAd#Jx$HIiKJJ1Rm-{yUv$!=m4S<3rVw%Z4nr4&`+@45ET*O;%*QggN0GYrZJD@ zGxH^rk!->UI9Yn0MvZmo?W_uQ_^ftH9=^?l^lZzHj0F(i6kIUQ0H^WJ+0DG3I}$$} zYKI$3d=*U~+^GT9a0K0oi#gK1@sjFLmfVOf^qlOLu z&∈Xw@FCo(ogxbT)dBIW54kumCAH+^PBbSenBKW7ltm7AA(gR$87oW&FBkHK<|7 zh!mS{^WOC}^n2x_cYYQIr}4_brxf)e{?O-fBnJl`lE&bp(dZmq)KTVc!h?#3UuJRkx!2RPL8Sw@;)YaZe-QTKeu5`U1-N@dHW9}jN2xXDB;au@} zIWZa|OsK!DFf>kq+-lFsCwkrGD~ZIvKgj8pvr^*2;cxl!1BdM&jKGmDhy^zB7sS#5B^&Dl-I@3o z_UV}x(hVCP_fYA*m1$9M_Lf>hFt8H%jS??W&8H0iu8HKqF<-~i3);h#X#|SZ7ke`e z^^~c^{21Q!{ss%FxMudY`8M~I!+}=fM&%T%rS7Xj9Qf`foHyp=x_P~M<`(2ufa-z@ z&4(cQh{Mt_s|0uvdDdK(heA+-(*^E>`8Mhi#fTcjU4ZV(HTdAW4aOZKKD^FuEKag2_q zkQm)DoyBl=LiRb+q0QCZ;^7CiGy6V8x1hmSl(q`RbqCHE7^uP6?-pUYfANhhb@Z7R z{=7g3I|_RXAw=(E(;Yc*Mk%AVPTV6NU&PSCU9nnxN@TIv_u<8ZjhvC zao>?#SyU9Rz#cL98Po#etxhhhPh%m#rhyA*(MoJoN&L|JK$p}*@pbZ$Jbf_l@LE@a zO3vQGM%#0Kq#DSwshpKKSl#edGevBlE+J#YEX~d_Y>?QwmM!HBpJn4ZN4hd8A{P40 z8G}`3jiPEK+*C^9*D>DjZ*^3$RqXm|Jw8o}JwC){0j-=F77Z4jCiVz3q!(BjilFwb-P}#Gx+&xd1SQ8y&a|xnb;at8y{aP)0Omu zs+=uc3tLJ|R4rA1xBe=zo(=lxc@MSFP}^GC^z&7GUNxU7riPRJH4}TcY6GF9)MrLr z$i@a_3F&}TAEj8j^fRzRZL?_F1CNt%Oy34erKpE*JD+XILn^PHRdFc>FFv##zXUP$ z$)wYqN*$OBmZoRy5bO?TIzr>ikvJKY%A-1=MU#p!9jd+W1vz#P%p|U{IMAuQ))=>y z;4sby7314J*Z7-luPf550#pUA>v(c6GUyN$FXp_(3#^_ z$cgUZnR<3JDVD_`zBoT?NSeH$XcPGw;~`LEHPxyktZ#*<@|ayQcYS@k=0jvK|#o zV-1%sm@&5%8vo+Iw_VT^q5FPviGio&Mci||d zvJC$Xd$vE(=Km43{g1G2MPq9fu-$*g26F=5BI5tscy5J1xf_yJ&Msgw4xWFB9`;op zv|j|EH{CMcSdtELOJKr7&N(gP2V+sXrHZXXnN$=hX<aGY}hQ>d1^DEusl0YHEc zx1qe`TzRvzg+A6HM#rp5``Zsp{OCAl||BhP!*LgI5 zrN^+p;pefrWUBSJ(dc43Gbki1U^@W8@6>a5ghXWM|_-q^6coltlbr DCg>u% literal 0 HcmV?d00001 diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html index 36f6457baa..0bdf297c31 100644 --- a/example/src/pages/http/http.html +++ b/example/src/pages/http/http.html @@ -26,6 +26,9 @@ + + +

Output

\ No newline at end of file diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index d962608916..c06718ff1b 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -117,4 +117,14 @@ export class HttpPage { console.log('Got ret', ret); this.loading.dismiss(); } + + downloadFile = async () => { + const ret = await Plugins.Http.downloadFile({ + url: this.apiUrl('/download-pdf'), + filePath: 'document.pdf' + }); + } + + uploadFile = async () => { + } } \ No newline at end of file diff --git a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m index a8a8e73abf..053bc3c182 100644 --- a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m +++ b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m @@ -77,6 +77,8 @@ CAP_PLUGIN_METHOD(request, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setCookie, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getCookies, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(downloadFile, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(uploadFile, CAPPluginReturnPromise); ) CAP_PLUGIN(CAPKeyboard, "Keyboard", diff --git a/ios/Capacitor/Capacitor/Plugins/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http.swift index 88b9e2213f..8ff9934848 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http.swift @@ -76,6 +76,43 @@ public class CAPHttpPlugin: CAPPlugin { call.reject("Unknown method") } } + + + @objc public func downloadFile(_ call: CAPPluginCall) { + guard let urlValue = call.getString("url") else { + return call.reject("Must provide a URL") + } + guard let filePath = call.getString("filePath") else { + return call.reject("Must provide a file path to download the file to") + } + //let fileDirectory = call.getString("filePath") ?? "DOCUMENTS" + + guard let url = URL(string: urlValue) else { + return call.reject("Invalid URL") + } + + var request = URLRequest(url: url) + + request.httpMethod = "GET" + + let task = URLSession.shared.downloadTask(with: request) { (data, response, error) in + if error != nil { + CAPLog.print("Error on download file", data, response, error) + call.reject("Error", error, [:]) + return + } + + let res = response as! HTTPURLResponse + + CAPLog.print("Downloaded file") + call.resolve() + } + + task.resume() + } + + + /* PRIVATE */ // Handle GET operations func get(_ call: CAPPluginCall, _ url: URL, _ method: String) { From 8250dd93812055b20a74f6a3130730c59926298a Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sat, 29 Feb 2020 17:31:28 -0600 Subject: [PATCH 07/29] Downloads working --- core/src/plugins/http.ts | 11 ++++++-- example/src/pages/http/http.ts | 26 ++++++++++++------ ios/Capacitor/Capacitor/Plugins/Http.swift | 32 ++++++++++++++++++---- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts index 443cf56a5a..04c83100fd 100644 --- a/core/src/plugins/http.ts +++ b/core/src/plugins/http.ts @@ -5,8 +5,8 @@ export interface HttpPlugin extends Plugin { request(options: HttpOptions): Promise; setCookie(options: HttpSetCookieOptions): Promise; getCookies(options: HttpGetCookiesOptions): Promise; - uploadFile(options: HttpUploadFileOptions): Promise; - downloadFile(options: HttpDownloadFileOptions): Promise; + uploadFile(options: HttpUploadFileOptions): Promise; + downloadFile(options: HttpDownloadFileOptions): Promise; } export interface HttpOptions { @@ -64,3 +64,10 @@ export interface HttpGetCookiesOptions { export interface HttpGetCookiesResult { value: HttpCookie[]; } + +export interface HttpDownloadFileResult { + path: string; +} + +export interface HttpUploadFileResult { +} \ No newline at end of file diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index c06718ff1b..75650ade0e 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -1,9 +1,11 @@ import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams, LoadingController, Loading } from 'ionic-angular'; -import { Plugins } from '@capacitor/core'; +import { FilesystemDirectory, Plugins } from '@capacitor/core'; import { SERVER_TRANSITION_PROVIDERS } from '@angular/platform-browser/src/browser/server-transition'; +const { Filesystem, Http } = Plugins; + /** * Generated class for the KeyboardPage page. * @@ -38,7 +40,7 @@ export class HttpPage { content: 'Requesting...' }); this.loading.present(); - const ret = await Plugins.Http.request({ + const ret = await Http.request({ method: method, url: `${this.url}${path}` }); @@ -60,7 +62,7 @@ export class HttpPage { content: 'Requesting...' }); this.loading.present(); - const ret = await Plugins.Http.request({ + const ret = await Http.request({ url: `${this.url}${path}`, method: method, headers: { @@ -76,7 +78,7 @@ export class HttpPage { apiUrl = (path: string) => `${this.serverUrl}${path}`; formPost = async () => { - const ret = await Plugins.Http.request({ + const ret = await Http.request({ url: this.apiUrl('/form-data'), method: 'POST', headers: { @@ -90,7 +92,7 @@ export class HttpPage { } setCookie = async () => { - const ret = await Plugins.Http.setCookie({ + const ret = await Http.setCookie({ url: this.apiUrl('/cookie'), key: 'language', value: 'en' @@ -98,7 +100,7 @@ export class HttpPage { } getCookies = async () => { - const ret = await Plugins.Http.getCookies({ + const ret = await Http.getCookies({ url: this.apiUrl('/cookie') }); console.log('Got cookies', ret); @@ -110,7 +112,7 @@ export class HttpPage { content: 'Requesting...' }); this.loading.present(); - const ret = await Plugins.Http.request({ + const ret = await Http.request({ method: 'GET', url: this.apiUrl('/cookie') }); @@ -119,10 +121,18 @@ export class HttpPage { } downloadFile = async () => { - const ret = await Plugins.Http.downloadFile({ + const ret = await Http.downloadFile({ url: this.apiUrl('/download-pdf'), filePath: 'document.pdf' }); + + console.log('Got download ret', ret); + + const read = await Filesystem.readFile({ + path: ret.path, + directory: FilesystemDirectory.Documents + }) + console.log('Read file', read); } uploadFile = async () => { diff --git a/ios/Capacitor/Capacitor/Plugins/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http.swift index 8ff9934848..f6e07c0794 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http.swift @@ -91,20 +91,40 @@ public class CAPHttpPlugin: CAPPlugin { return call.reject("Invalid URL") } - var request = URLRequest(url: url) - - request.httpMethod = "GET" - let task = URLSession.shared.downloadTask(with: request) { (data, response, error) in + let task = URLSession.shared.downloadTask(with: url) { (downloadLocation, response, error) in if error != nil { - CAPLog.print("Error on download file", data, response, error) + CAPLog.print("Error on download file", downloadLocation, response, error) call.reject("Error", error, [:]) return } + guard let location = downloadLocation else { + call.reject("Unable to get file after downloading") + return + } + let res = response as! HTTPURLResponse - CAPLog.print("Downloaded file") + let basename = location.lastPathComponent + + // TODO: Move to abstracted FS operations + let fileManager = FileManager.default + let dir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first + + do { + let dest = dir!.appendingPathComponent(basename) + print("File Dest", dest.absoluteString) + try fileManager.moveItem(at: location, to: dest) + call.resolve([ + "path": dest.absoluteString + ]) + } catch let e { + CAPLog.print("Unable to download file", e) + } + + + CAPLog.print("Downloaded file", location) call.resolve() } From aafe6e9ba1c3de48b95f94edf754825ca411f07c Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sat, 29 Feb 2020 22:38:19 -0600 Subject: [PATCH 08/29] Working on upload --- core/src/plugins/http.ts | 1 + example/package-lock.json | 116 +++++++++++++++++- example/package.json | 1 + example/src/pages/http/http.ts | 6 + .../Plugins/{ => Filesystem}/Filesystem.swift | 110 +++-------------- .../Plugins/Filesystem/FilesystemUtils.swift | 110 +++++++++++++++++ .../Capacitor/Plugins/{ => Http}/Http.swift | 64 ++++++++++ 7 files changed, 312 insertions(+), 96 deletions(-) rename ios/Capacitor/Capacitor/Plugins/{ => Filesystem}/Filesystem.swift (74%) create mode 100644 ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift rename ios/Capacitor/Capacitor/Plugins/{ => Http}/Http.swift (78%) diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts index 04c83100fd..94c732a5cd 100644 --- a/core/src/plugins/http.ts +++ b/core/src/plugins/http.ts @@ -44,6 +44,7 @@ export interface HttpDownloadFileOptions extends HttpOptions { export interface HttpUploadFileOptions { url: string; filePath: string; + fileDirectory?: FilesystemDirectory; } export interface HttpCookie { diff --git a/example/package-lock.json b/example/package-lock.json index 08899fa335..9a4322db97 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -638,6 +638,11 @@ "normalize-path": "^2.0.0" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -1113,6 +1118,11 @@ "isarray": "^1.0.0" } }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", @@ -1131,6 +1141,38 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1344,6 +1386,17 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -1619,6 +1672,38 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "diff": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", @@ -3724,6 +3809,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", @@ -3987,8 +4087,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", @@ -5435,6 +5534,11 @@ "xtend": "^4.0.0" } }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-template": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", @@ -5776,6 +5880,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, "typescript": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", @@ -6756,8 +6865,7 @@ "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "3.2.1", diff --git a/example/package.json b/example/package.json index a6687baf79..49ec5d3118 100644 --- a/example/package.json +++ b/example/package.json @@ -30,6 +30,7 @@ "express": "^4.17.1", "ionic-angular": "^3.9.6", "ionicons": "3.0.0", + "multer": "^1.4.2", "rxjs": "5.5.2", "sw-toolbox": "3.6.0", "zone.js": "0.8.18" diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index 75650ade0e..c61f410a5f 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -136,5 +136,11 @@ export class HttpPage { } uploadFile = async () => { + const ret = await Http.uploadFile({ + url: this.apiUrl('/upload-pdf'), + filePath: 'document.pdf', + }); + + console.log('Got upload ret', ret); } } \ No newline at end of file diff --git a/ios/Capacitor/Capacitor/Plugins/Filesystem.swift b/ios/Capacitor/Capacitor/Plugins/Filesystem/Filesystem.swift similarity index 74% rename from ios/Capacitor/Capacitor/Plugins/Filesystem.swift rename to ios/Capacitor/Capacitor/Plugins/Filesystem/Filesystem.swift index 43105055d0..9b6a575168 100644 --- a/ios/Capacitor/Capacitor/Plugins/Filesystem.swift +++ b/ios/Capacitor/Capacitor/Plugins/Filesystem/Filesystem.swift @@ -5,39 +5,7 @@ import Foundation public class CAPFilesystemPlugin : CAPPlugin { let DEFAULT_DIRECTORY = "DOCUMENTS" - /** - * Get the SearchPathDirectory corresponding to the JS string - */ - func getDirectory(directory: String) -> FileManager.SearchPathDirectory { - switch directory { - case "DOCUMENTS": - return .documentDirectory - case "APPLICATION": - return .applicationDirectory - case "CACHE": - return .cachesDirectory - default: - return .documentDirectory - } - } - - /** - * Get the URL for this file, supporting file:// paths and - * files with directory mappings. - */ - func getFileUrl(_ path: String, _ directoryOption: String) -> URL? { - if path.starts(with: "file://") { - return URL(string: path) - } - - let directory = getDirectory(directory: directoryOption) - - guard let dir = FileManager.default.urls(for: directory, in: .userDomainMask).first else { - return nil - } - - return dir.appendingPathComponent(path) - } + /** * Helper for handling errors @@ -58,23 +26,11 @@ public class CAPFilesystemPlugin : CAPPlugin { } let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)! - guard let fileUrl = getFileUrl(file, directoryOption) else { - handleError(call, "Invalid path") - return - } - do { - if encoding != nil { - let data = try String(contentsOf: fileUrl, encoding: .utf8) - call.success([ - "data": data - ]) - } else { - let data = try Data(contentsOf: fileUrl) - call.success([ - "data": data.base64EncodedString() - ]) - } + let data = try FilesystemUtils.readFileString(file, directoryOption, encoding) + call.resolve([ + "data": data + ]) } catch let error as NSError { handleError(call, error.localizedDescription, error) } @@ -87,7 +43,7 @@ public class CAPFilesystemPlugin : CAPPlugin { let encoding = call.getString("encoding") let recursive = call.get("recursive", Bool.self, false)! // TODO: Allow them to switch encoding - guard let file = call.get("path", String.self) else { + guard let path = call.get("path", String.self) else { handleError(call, "path must be provided and must be a string.") return } @@ -99,31 +55,9 @@ public class CAPFilesystemPlugin : CAPPlugin { let directoryOption = call.get("directory", String.self) ?? DEFAULT_DIRECTORY - guard let fileUrl = getFileUrl(file, directoryOption) else { - handleError(call, "Invalid path") - return - } - do { - if !FileManager.default.fileExists(atPath: fileUrl.deletingLastPathComponent().absoluteString) { - if recursive { - try FileManager.default.createDirectory(at: fileUrl.deletingLastPathComponent(), withIntermediateDirectories: recursive, attributes: nil) - } else { - handleError(call, "Parent folder doesn't exist"); - return - } - } - if encoding != nil { - try data.write(to: fileUrl, atomically: false, encoding: .utf8) - } else { - let cleanData = getCleanData(data) - if let base64Data = Data(base64Encoded: cleanData) { - try base64Data.write(to: fileUrl) - } else { - handleError(call, "Unable to save file") - return - } - } + let fileUrl = try FilesystemUtils.writeFileString(path, directoryOption, encoding, data, recursive) + call.success([ "uri": fileUrl.absoluteString ]) @@ -149,7 +83,7 @@ public class CAPFilesystemPlugin : CAPPlugin { } let directoryOption = call.get("directory", String.self) ?? DEFAULT_DIRECTORY - guard let fileUrl = getFileUrl(file, directoryOption) else { + guard let fileUrl = FilesystemUtils.getFileUrl(file, directoryOption) else { handleError(call, "Invalid path") return } @@ -165,7 +99,7 @@ public class CAPFilesystemPlugin : CAPPlugin { } writeData = userData } else { - let cleanData = getCleanData(data) + let cleanData = FilesystemUtils.getCleanBase64Data(data) if let base64Data = Data(base64Encoded: cleanData) { writeData = base64Data } else { @@ -187,14 +121,6 @@ public class CAPFilesystemPlugin : CAPPlugin { } } - func getCleanData(_ data: String) -> String { - let dataParts = data.split(separator: ",") - var cleanData = data - if dataParts.count > 0 { - cleanData = String(dataParts.last!) - } - return cleanData - } /** * Delete a file. @@ -208,7 +134,7 @@ public class CAPFilesystemPlugin : CAPPlugin { } let directoryOption = call.get("directory", String.self) ?? DEFAULT_DIRECTORY - guard let fileUrl = getFileUrl(file, directoryOption) else { + guard let fileUrl = FilesystemUtils.getFileUrl(file, directoryOption) else { handleError(call, "Invalid path") return } @@ -235,7 +161,7 @@ public class CAPFilesystemPlugin : CAPPlugin { let recursive = call.get("recursive", Bool.self, false)! let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)! - guard let fileUrl = getFileUrl(path, directoryOption) else { + guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else { handleError(call, "Invalid path") return } @@ -259,7 +185,7 @@ public class CAPFilesystemPlugin : CAPPlugin { } let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)! - guard let fileUrl = getFileUrl(path, directoryOption) else { + guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else { handleError(call, "Invalid path") return } @@ -295,7 +221,7 @@ public class CAPFilesystemPlugin : CAPPlugin { } let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)! - guard let fileUrl = getFileUrl(path, directoryOption) else { + guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else { handleError(call, "Invalid path") return } @@ -323,7 +249,7 @@ public class CAPFilesystemPlugin : CAPPlugin { } let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)! - guard let fileUrl = getFileUrl(path, directoryOption) else { + guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else { handleError(call, "Invalid path") return } @@ -349,7 +275,7 @@ public class CAPFilesystemPlugin : CAPPlugin { } let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)! - guard let fileUrl = getFileUrl(path, directoryOption) else { + guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else { handleError(call, "Invalid path") return } @@ -390,12 +316,12 @@ public class CAPFilesystemPlugin : CAPPlugin { toDirectoryOption = directoryOption; } - guard let fromUrl = getFileUrl(from, directoryOption) else { + guard let fromUrl = FilesystemUtils.getFileUrl(from, directoryOption) else { handleError(call, "Invalid from path") return } - guard let toUrl = getFileUrl(to, toDirectoryOption) else { + guard let toUrl = FilesystemUtils.getFileUrl(to, toDirectoryOption) else { handleError(call, "Invalid to path") return } diff --git a/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift b/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift new file mode 100644 index 0000000000..564589d017 --- /dev/null +++ b/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift @@ -0,0 +1,110 @@ +import Foundation +import MobileCoreServices + +enum FilesystemError: Error { + case fileNotFound(String) + case invalidPath(String) + case parentFolderNotExists(String) + case saveError(String) +} + +class FilesystemUtils { + /** + * Get the SearchPathDirectory corresponding to the JS string + */ + static func getDirectory(directory: String) -> FileManager.SearchPathDirectory { + switch directory { + case "DOCUMENTS": + return .documentDirectory + case "APPLICATION": + return .applicationDirectory + case "CACHE": + return .cachesDirectory + default: + return .documentDirectory + } + } + + /** + * Get the URL for this file, supporting file:// paths and + * files with directory mappings. + */ + static func getFileUrl(_ path: String, _ directoryOption: String) -> URL? { + if path.starts(with: "file://") { + return URL(string: path) + } + + let directory = FilesystemUtils.getDirectory(directory: directoryOption) + + guard let dir = FileManager.default.urls(for: directory, in: .userDomainMask).first else { + return nil + } + + return dir.appendingPathComponent(path) + } + + /** + * Read a file as a string at the given directory and with the given encoding + */ + static func readFileString(_ path: String, _ directory: String, _ encoding: String?) throws -> String { + guard let fileUrl = FilesystemUtils.getFileUrl(path, directory) else { + throw FilesystemError.fileNotFound("No such file exists") + } + if encoding != nil { + let data = try String(contentsOf: fileUrl, encoding: .utf8) + return data + } else { + let data = try Data(contentsOf: fileUrl) + return data.base64EncodedString() + } + } + + static func writeFileString(_ path: String, _ directory: String, _ encoding: String?, _ data: String, _ recursive: Bool = false) throws -> URL { + + guard let fileUrl = FilesystemUtils.getFileUrl(path, directory) else { + throw FilesystemError.invalidPath("Invlid path") + } + + if !FileManager.default.fileExists(atPath: fileUrl.deletingLastPathComponent().absoluteString) { + if recursive { + try FileManager.default.createDirectory(at: fileUrl.deletingLastPathComponent(), withIntermediateDirectories: recursive, attributes: nil) + } else { + throw FilesystemError.parentFolderNotExists("Parent folder doesn't exist") + } + } + + if encoding != nil { + try data.write(to: fileUrl, atomically: false, encoding: .utf8) + } else { + let cleanData = getCleanBase64Data(data) + if let base64Data = Data(base64Encoded: cleanData) { + try base64Data.write(to: fileUrl) + } else { + throw FilesystemError.saveError("Unable to save file") + } + } + + return fileUrl + } + + + static func getCleanBase64Data(_ data: String) -> String { + let dataParts = data.split(separator: ",") + var cleanData = data + if dataParts.count > 0 { + cleanData = String(dataParts.last!) + } + return cleanData + } + + static func mimeTypeForPath(path: String) -> String { + let url = NSURL(fileURLWithPath: path) + let pathExtension = url.pathExtension + if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension! as NSString, nil)?.takeRetainedValue() { + if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { + return mimetype as String + } + } + return "application/octet-stream" + } +} diff --git a/ios/Capacitor/Capacitor/Plugins/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift similarity index 78% rename from ios/Capacitor/Capacitor/Plugins/Http.swift rename to ios/Capacitor/Capacitor/Plugins/Http/Http.swift index f6e07c0794..95b9398a5e 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift @@ -131,6 +131,70 @@ public class CAPHttpPlugin: CAPPlugin { task.resume() } + @objc public func uploadFile(_ call: CAPPluginCall) { + guard let urlValue = call.getString("url") else { + return call.reject("Must provide a URL") + } + guard let filePath = call.getString("filePath") else { + return call.reject("Must provide a file path to download the file to") + } + let fileDirectory = call.getString("filePath") ?? "DOCUMENTS" + + guard let url = URL(string: urlValue) else { + return call.reject("Invalid URL") + } + + guard let fileUrl = FilesystemUtils.getFileUrl(filePath, fileDirectory) else { + return call.reject("Unable to get file URL") + } + + var request = URLRequest.init(url: url) + request.httpMethod = "POST" + + let boundary = UUID().uuidString + + var fullFormData: Data? + do { + fullFormData = try generateFullMultipartRequestBody(fileUrl, boundary) + } catch let e { + return call.reject("Unable to read file to upload", e) + } + + + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + let task = URLSession.shared.uploadTask(with: request, from: fullFormData) { (data, response, error) in + if error != nil { + CAPLog.print("Error on upload file", data, response, error) + call.reject("Error", error, [:]) + return + } + + let res = response as! HTTPURLResponse + + //CAPLog.print("Uploaded file", location) + call.resolve() + } + + task.resume() + } + + func generateFullMultipartRequestBody(_ url: URL, _ boundary: String) throws -> Data { + var data = Data() + + let fileData = try Data(contentsOf: url) + + + let fname = url.lastPathComponent + let mimeType = FilesystemUtils.mimeTypeForPath(path: fname) + data.append("--\(boundary)--\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fname)\"\r\n".data(using: .utf8)!) + data.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) + data.append(fileData) + data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) + + return data + } /* PRIVATE */ From f023d602ac1c200029fac86763e1bf06a9a4390f Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sun, 1 Mar 2020 14:18:49 -0600 Subject: [PATCH 09/29] Uploads working --- core/src/plugins/http.ts | 30 +++++++++++++++---- example/src/pages/http/http.ts | 12 ++++++-- .../Capacitor/Plugins/Http/Http.swift | 10 ++++--- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts index 94c732a5cd..efc798a651 100644 --- a/core/src/plugins/http.ts +++ b/core/src/plugins/http.ts @@ -31,19 +31,37 @@ export interface HttpResponse { headers: HttpHeaders; } -export interface HttpUploadFileOptions extends HttpOptions { - filePath: string; - fileDirectory?: FilesystemDirectory; -} - export interface HttpDownloadFileOptions extends HttpOptions { + /** + * The path the downloaded file should be moved to + */ filePath: string; + /** + * Optionally, the directory to put the file in + * + * If this option is used, filePath can be a relative path rather than absolute + */ fileDirectory?: FilesystemDirectory; } -export interface HttpUploadFileOptions { +export interface HttpUploadFileOptions extends HttpOptions { + /** + * The URL to upload the file to + */ url: string; + /** + * The field name to upload the file with + */ + name: string; + /** + * The path to the file on disk to upload + */ filePath: string; + /** + * Optionally, the directory to look for the file in. + * + * If this option is used, filePath can be a relative path rather than absolute + */ fileDirectory?: FilesystemDirectory; } diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index c61f410a5f..b9d4e0789f 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -128,16 +128,24 @@ export class HttpPage { console.log('Got download ret', ret); + const renameRet = await Filesystem.rename({ + from: ret.path, + to: 'document.pdf', + toDirectory: FilesystemDirectory.Documents + }); + + console.log('Did rename', renameRet); + const read = await Filesystem.readFile({ - path: ret.path, + path: 'document.pdf', directory: FilesystemDirectory.Documents }) - console.log('Read file', read); } uploadFile = async () => { const ret = await Http.uploadFile({ url: this.apiUrl('/upload-pdf'), + name: 'myFile', filePath: 'document.pdf', }); diff --git a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift index 95b9398a5e..95f7020d74 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift @@ -138,6 +138,8 @@ public class CAPHttpPlugin: CAPPlugin { guard let filePath = call.getString("filePath") else { return call.reject("Must provide a file path to download the file to") } + let name = call.getString("name") ?? "file" + let fileDirectory = call.getString("filePath") ?? "DOCUMENTS" guard let url = URL(string: urlValue) else { @@ -155,7 +157,7 @@ public class CAPHttpPlugin: CAPPlugin { var fullFormData: Data? do { - fullFormData = try generateFullMultipartRequestBody(fileUrl, boundary) + fullFormData = try generateFullMultipartRequestBody(fileUrl, name, boundary) } catch let e { return call.reject("Unable to read file to upload", e) } @@ -179,7 +181,7 @@ public class CAPHttpPlugin: CAPPlugin { task.resume() } - func generateFullMultipartRequestBody(_ url: URL, _ boundary: String) throws -> Data { + func generateFullMultipartRequestBody(_ url: URL, _ name: String, _ boundary: String) throws -> Data { var data = Data() let fileData = try Data(contentsOf: url) @@ -187,8 +189,8 @@ public class CAPHttpPlugin: CAPPlugin { let fname = url.lastPathComponent let mimeType = FilesystemUtils.mimeTypeForPath(path: fname) - data.append("--\(boundary)--\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fname)\"\r\n".data(using: .utf8)!) + data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fname)\"\r\n".data(using: .utf8)!) data.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) data.append(fileData) data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) From d05dc999e03e9453e19fa609ca4b2880f3f8c8b5 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sun, 1 Mar 2020 20:20:32 -0600 Subject: [PATCH 10/29] Starting Android and mroe cookie methods and headers --- .../java/com/getcapacitor/plugin/Http.java | 85 +++++++ core/src/plugins/http.ts | 11 + example/src/pages/http/http.html | 3 + example/src/pages/http/http.ts | 38 ++- .../Capacitor/Plugins/DefaultPlugins.m | 2 + .../Capacitor/Plugins/Http/Http.swift | 219 +++++++++++------- 6 files changed, 269 insertions(+), 89 deletions(-) create mode 100644 android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java new file mode 100644 index 0000000000..6776028226 --- /dev/null +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java @@ -0,0 +1,85 @@ +package com.getcapacitor.plugin; + +import android.Manifest; +import android.content.Context; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.view.HapticFeedbackConstants; + +import com.getcapacitor.NativePlugin; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Haptic engine plugin, also handles vibration. + * + * Requires the android.permission.VIBRATE permission. + */ +@NativePlugin() +public class Http extends Plugin { + @PluginMethod() + public void request(PluginCall call) { + String url = call.getString("url"); + String method = call.getString("method"); + + switch (method) { + case "GET": + case "HEAD": + get(call, url, method); + return; + case "DELETE": + case "PATCH": + case "POST": + case "PUT": + mutate(call, url, method); + return; + } + } + + private void get(PluginCall call, String urlString, String method) { + try { + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + + } catch (IOException ex) { + call.error("Error", ex); + } catch (MalformedURLException ex) { + call.error("Invalid URL", ex); + } + } + + private void mutate(PluginCall call, String url, String method) { + } + + @PluginMethod() + public void downloadFile(PluginCall call) { + } + + @PluginMethod() + public void uploadFile(PluginCall call) { + } + + @PluginMethod() + public void setCookie(PluginCall call) { + } + + @PluginMethod() + public void getCookie(PluginCall call) { + } + + @PluginMethod() + public void deleteCookie(PluginCall call) { + } + + @PluginMethod() + public void clearCookies(PluginCall call) { + } +} \ No newline at end of file diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts index efc798a651..89932dd7ef 100644 --- a/core/src/plugins/http.ts +++ b/core/src/plugins/http.ts @@ -5,6 +5,8 @@ export interface HttpPlugin extends Plugin { request(options: HttpOptions): Promise; setCookie(options: HttpSetCookieOptions): Promise; getCookies(options: HttpGetCookiesOptions): Promise; + deleteCookie(options: HttpDeleteCookieOptions): Promise; + clearCookies(options: HttpClearCookiesOptions): Promise; uploadFile(options: HttpUploadFileOptions): Promise; downloadFile(options: HttpDownloadFileOptions): Promise; } @@ -80,6 +82,15 @@ export interface HttpGetCookiesOptions { url: string; } +export interface HttpDeleteCookieOptions { + url: string; + key: string; +} + +export interface HttpClearCookiesOptions { + url: string; +} + export interface HttpGetCookiesResult { value: HttpCookie[]; } diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html index 0bdf297c31..abbebb96e0 100644 --- a/example/src/pages/http/http.html +++ b/example/src/pages/http/http.html @@ -22,8 +22,11 @@ + + + diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index b9d4e0789f..b3f145077e 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -20,7 +20,6 @@ const { Filesystem, Http } = Plugins; }) export class HttpPage { serverUrl = 'http://localhost:3455'; - url: string = 'https://jsonplaceholder.typicode.com'; output: string = ''; @@ -33,7 +32,7 @@ export class HttpPage { console.log('ionViewDidLoad KeyboardPage'); } - async get(path = '/posts/1', method = 'GET') { + async get(path = '/get', method = 'GET') { this.output = ''; this.loading = this.loadingCtrl.create({ @@ -42,7 +41,13 @@ export class HttpPage { this.loading.present(); const ret = await Http.request({ method: method, - url: `${this.url}${path}` + url: this.apiUrl(path), + headers: { + 'X-Fake-Header': 'Max was here' + }, + params: { + 'size': 'XL' + } }); console.log('Got ret', ret); this.loading.dismiss(); @@ -50,11 +55,11 @@ export class HttpPage { this.output = JSON.stringify(ret, null, 2); } - head = () => this.get('/posts/1', 'HEAD'); - delete = () => this.mutate('/posts/1', 'DELETE', { title: 'foo', body: 'bar', userId: 1 }); - patch = () => this.mutate('/posts/1', 'PATCH', { title: 'foo', body: 'bar', userId: 1 }); - post = () => this.mutate('/posts', 'POST', { title: 'foo', body: 'bar', userId: 1 }); - put = () => this.mutate('/posts/1', 'PUT', { title: 'foo', body: 'bar', userId: 1 }); + head = () => this.get('/head', 'HEAD'); + delete = () => this.mutate('/delete', 'DELETE', { title: 'foo', body: 'bar', userId: 1 }); + patch = () => this.mutate('/patch', 'PATCH', { title: 'foo', body: 'bar', userId: 1 }); + post = () => this.mutate('/post', 'POST', { title: 'foo', body: 'bar', userId: 1 }); + put = () => this.mutate('/put', 'PUT', { title: 'foo', body: 'bar', userId: 1 }); async mutate(path, method, data = {}) { this.output = ''; @@ -63,10 +68,10 @@ export class HttpPage { }); this.loading.present(); const ret = await Http.request({ - url: `${this.url}${path}`, + url: this.apiUrl(path), method: method, headers: { - 'content-type': 'application/json' + 'content-type': 'application/json', }, data }); @@ -99,6 +104,19 @@ export class HttpPage { }); } + deleteCookie = async () => { + const ret = await Http.deleteCookie({ + url: this.apiUrl('/cookie'), + key: 'language', + }); + } + + clearCookies = async () => { + const ret = await Http.clearCookies({ + url: this.apiUrl('/cookie'), + }); + } + getCookies = async () => { const ret = await Http.getCookies({ url: this.apiUrl('/cookie') diff --git a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m index 053bc3c182..75cfe6db03 100644 --- a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m +++ b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m @@ -77,6 +77,8 @@ CAP_PLUGIN_METHOD(request, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setCookie, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getCookies, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(deleteCookie, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(clearCookies, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(downloadFile, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(uploadFile, CAPPluginReturnPromise); ) diff --git a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift index 95f7020d74..5caaa96c4c 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift @@ -3,58 +3,7 @@ import AudioToolbox @objc(CAPHttpPlugin) public class CAPHttpPlugin: CAPPlugin { - @objc public func setCookie(_ call: CAPPluginCall) { - - guard let key = call.getString("key") else { - return call.reject("Must provide key") - } - guard let value = call.getString("value") else { - return call.reject("Must provide value") - } - guard let urlString = call.getString("url") else { - return call.reject("Must provide URL") - } - - guard let url = URL(string: urlString) else { - return call.reject("Invalid URL") - } - - let jar = HTTPCookieStorage.shared - let field = ["Set-Cookie": "\(key)=\(value)"] - let cookies = HTTPCookie.cookies(withResponseHeaderFields: field, for: url) - jar.setCookies(cookies, for: url, mainDocumentURL: url) - - call.resolve() - } - - @objc public func getCookies(_ call: CAPPluginCall) { - guard let urlString = call.getString("url") else { - return call.reject("Must provide URL") - } - - guard let url = URL(string: urlString) else { - return call.reject("Invalid URL") - } - - let jar = HTTPCookieStorage.shared - guard let cookies = jar.cookies(for: url) else { - return call.resolve([ - "value": [] - ]) - } - - let c = cookies.map { (cookie: HTTPCookie) -> [String:String] in - return [ - "key": cookie.name, - "value": cookie.value - ] - } - - call.resolve([ - "value": c - ]) - } - + @objc public func request(_ call: CAPPluginCall) { guard let urlValue = call.getString("url") else { return call.reject("Must provide a URL") @@ -63,15 +12,20 @@ public class CAPHttpPlugin: CAPPlugin { return call.reject("Must provide a method. One of GET, DELETE, HEAD PATCH, POST, or PUT") } - guard let url = URL(string: urlValue) else { + let headers = (call.getObject("headers") ?? [:]) as [String:String] + + let params = (call.getObject("params") ?? [:]) as [String:String] + + guard var url = URL(string: urlValue) else { return call.reject("Invalid URL") } + switch method { case "GET", "HEAD": - get(call, url, method) + get(call, &url, method, headers, params) case "DELETE", "PATCH", "POST", "PUT": - mutate(call, url, method) + mutate(call, url, method, headers) default: call.reject("Unknown method") } @@ -181,33 +135,110 @@ public class CAPHttpPlugin: CAPPlugin { task.resume() } - func generateFullMultipartRequestBody(_ url: URL, _ name: String, _ boundary: String) throws -> Data { - var data = Data() + @objc public func setCookie(_ call: CAPPluginCall) { + + guard let key = call.getString("key") else { + return call.reject("Must provide key") + } + guard let value = call.getString("value") else { + return call.reject("Must provide value") + } + guard let urlString = call.getString("url") else { + return call.reject("Must provide URL") + } - let fileData = try Data(contentsOf: url) - + guard let url = URL(string: urlString) else { + return call.reject("Invalid URL") + } - let fname = url.lastPathComponent - let mimeType = FilesystemUtils.mimeTypeForPath(path: fname) - data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fname)\"\r\n".data(using: .utf8)!) - data.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) - data.append(fileData) - data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) + let jar = HTTPCookieStorage.shared + let field = ["Set-Cookie": "\(key)=\(value)"] + let cookies = HTTPCookie.cookies(withResponseHeaderFields: field, for: url) + jar.setCookies(cookies, for: url, mainDocumentURL: url) - return data + call.resolve() } + @objc public func getCookies(_ call: CAPPluginCall) { + guard let urlString = call.getString("url") else { + return call.reject("Must provide URL") + } + + guard let url = URL(string: urlString) else { + return call.reject("Invalid URL") + } + + let jar = HTTPCookieStorage.shared + guard let cookies = jar.cookies(for: url) else { + return call.resolve([ + "value": [] + ]) + } + + let c = cookies.map { (cookie: HTTPCookie) -> [String:String] in + return [ + "key": cookie.name, + "value": cookie.value + ] + } + + call.resolve([ + "value": c + ]) + } + + @objc public func deleteCookie(_ call: CAPPluginCall) { + guard let urlString = call.getString("url") else { + return call.reject("Must provide URL") + } + guard let key = call.getString("key") else { + return call.reject("Must provide key") + } + guard let url = URL(string: urlString) else { + return call.reject("Invalid URL") + } + + let jar = HTTPCookieStorage.shared + + let cookie = jar.cookies(for: url)?.first(where: { (cookie) -> Bool in + return cookie.name == key + }) + if cookie != nil { + jar.deleteCookie(cookie!) + } + + call.resolve() + } + + @objc public func clearCookies(_ call: CAPPluginCall) { + guard let urlString = call.getString("url") else { + return call.reject("Must provide URL") + } + guard let url = URL(string: urlString) else { + return call.reject("Invalid URL") + } + let jar = HTTPCookieStorage.shared + jar.cookies(for: url)?.forEach({ (cookie) in + jar.deleteCookie(cookie) + }) + call.resolve() + } + + /* PRIVATE */ // Handle GET operations - func get(_ call: CAPPluginCall, _ url: URL, _ method: String) { + func get(_ call: CAPPluginCall, _ url: inout URL, _ method: String, _ headers: [String:String], _ params: [String:String]) { + setUrlQuery(&url, params) + var request = URLRequest(url: url) request.httpMethod = method + + setRequestHeaders(&request, headers) + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in if error != nil { - CAPLog.print("Error on GET", data, response, error) call.reject("Error", error, [:]) return } @@ -220,21 +251,34 @@ public class CAPHttpPlugin: CAPPlugin { task.resume() } + func setUrlQuery(_ url: inout URL, _ params: [String:String]) { + var cmps = URLComponents(url: url, resolvingAgainstBaseURL: true) + cmps!.queryItems = params.map({ (key, value) -> URLQueryItem in + return URLQueryItem(name: key, value: value) + }) + url = cmps!.url! + } + + func setRequestHeaders(_ request: inout URLRequest, _ headers: [String:String]) { + headers.keys.forEach { (key) in + guard let value = headers[key] else { + return + } + request.addValue(value, forHTTPHeaderField: key) + } + } + // Handle mutation operations: DELETE, PATCH, POST, and PUT - func mutate(_ call: CAPPluginCall, _ url: URL, _ method: String) { + func mutate(_ call: CAPPluginCall, _ url: URL, _ method: String, _ headers: [String:String]) { let data = call.getObject("data") var request = URLRequest(url: url) request.httpMethod = method - - let headers = (call.getObject("headers") ?? [:]) as [String:Any] + + setRequestHeaders(&request, headers) let contentType = getRequestHeader(headers, "Content-Type") as? String - if contentType != nil { - request.setValue(contentType, forHTTPHeaderField: "Content-Type") - } - if data != nil && contentType != nil { do { request.httpBody = try getRequestData(request, data!, contentType!) @@ -246,7 +290,6 @@ public class CAPHttpPlugin: CAPPlugin { let task = URLSession.shared.dataTask(with: request) { (data, response, error) in if error != nil { - CAPLog.print("Error on mutate ", data, response, error) call.reject("Error", error, [:]) return } @@ -297,9 +340,9 @@ public class CAPHttpPlugin: CAPPlugin { if contentType.contains("application/json") { return try setRequestDataJson(request, data) } else if contentType.contains("application/x-www-form-urlencoded") { - return try setRequestDataFormUrlEncoded(request, data) + return setRequestDataFormUrlEncoded(request, data) } else if contentType.contains("multipart/form-data") { - return try setRequestDataMultipartFormData(request, data) + return setRequestDataMultipartFormData(request, data) } return nil } @@ -328,4 +371,22 @@ public class CAPHttpPlugin: CAPPlugin { func setRequestDataMultipartFormData(_ request: URLRequest, _ data: [String:Any]) -> Data? { return nil } + + + func generateFullMultipartRequestBody(_ url: URL, _ name: String, _ boundary: String) throws -> Data { + var data = Data() + + let fileData = try Data(contentsOf: url) + + + let fname = url.lastPathComponent + let mimeType = FilesystemUtils.mimeTypeForPath(path: fname) + data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fname)\"\r\n".data(using: .utf8)!) + data.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) + data.append(fileData) + data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) + + return data + } } From 13cb9fd9be801950fdc9dfc30f09cd63317509c4 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sun, 1 Mar 2020 22:02:42 -0600 Subject: [PATCH 11/29] Android --- .../java/com/getcapacitor/plugin/Http.java | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java index 6776028226..a94d3b6e68 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java @@ -7,15 +7,20 @@ import android.os.Vibrator; import android.view.HapticFeedbackConstants; +import com.getcapacitor.JSObject; import com.getcapacitor.NativePlugin; import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; +import java.io.DataOutputStream; import java.io.IOException; +import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; +import java.util.Iterator; +import java.util.Map; /** * Haptic engine plugin, also handles vibration. @@ -28,11 +33,12 @@ public class Http extends Plugin { public void request(PluginCall call) { String url = call.getString("url"); String method = call.getString("method"); + JSObject headers = call.getObject("headers"); switch (method) { case "GET": case "HEAD": - get(call, url, method); + get(call, url, method, headers); return; case "DELETE": case "PATCH": @@ -43,16 +49,33 @@ public void request(PluginCall call) { } } - private void get(PluginCall call, String urlString, String method) { + private void get(PluginCall call, String urlString, String method, JSObject headers) { try { URL url = new URL(urlString); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(method); + + setRequestHeaders(conn, headers); + + conn.setDoInput(true); + + conn.connect(); + + InputStream stream = conn.getInputStream(); - } catch (IOException ex) { - call.error("Error", ex); } catch (MalformedURLException ex) { call.error("Invalid URL", ex); + } catch (IOException ex) { + call.error("Error", ex); + } + } + + private void setRequestHeaders(HttpURLConnection conn, JSObject headers) { + Iterator keys = headers.keys(); + while (keys.hasNext()) { + String key = keys.next(); + String value = headers.getString(key); + conn.setRequestProperty(key, value); } } From ea768ac04bea70fd7cbf3da4e459876c3014ea63 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sat, 14 Mar 2020 10:25:16 -0500 Subject: [PATCH 12/29] Android cookie routines --- .../main/java/com/getcapacitor/Bridge.java | 2 + .../java/com/getcapacitor/plugin/Http.java | 97 ++++++++++++++++++- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java index 72ecfcbc6c..2b108d062e 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java @@ -27,6 +27,7 @@ import com.getcapacitor.plugin.Filesystem; import com.getcapacitor.plugin.Geolocation; import com.getcapacitor.plugin.Haptics; +import com.getcapacitor.plugin.Http; import com.getcapacitor.plugin.Keyboard; import com.getcapacitor.plugin.LocalNotifications; import com.getcapacitor.plugin.Modals; @@ -401,6 +402,7 @@ private void registerAllPlugins() { this.registerPlugin(Filesystem.class); this.registerPlugin(Geolocation.class); this.registerPlugin(Haptics.class); + this.registerPlugin(Http.class); this.registerPlugin(Keyboard.class); this.registerPlugin(Modals.class); this.registerPlugin(Network.class); diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java index a94d3b6e68..3cf34849af 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java @@ -2,11 +2,14 @@ import android.Manifest; import android.content.Context; +import android.net.Uri; import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; +import android.util.Log; import android.view.HapticFeedbackConstants; +import com.getcapacitor.JSArray; import com.getcapacitor.JSObject; import com.getcapacitor.NativePlugin; import com.getcapacitor.Plugin; @@ -16,10 +19,16 @@ import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.Map; /** @@ -29,16 +38,24 @@ */ @NativePlugin() public class Http extends Plugin { + CookieManager cookieManager = new CookieManager(); + + @Override + public void load() { + CookieHandler.setDefault(cookieManager); + } + @PluginMethod() public void request(PluginCall call) { String url = call.getString("url"); String method = call.getString("method"); JSObject headers = call.getObject("headers"); + JSObject params = call.getObject("params"); switch (method) { case "GET": case "HEAD": - get(call, url, method, headers); + get(call, url, method, headers, params); return; case "DELETE": case "PATCH": @@ -49,9 +66,16 @@ public void request(PluginCall call) { } } - private void get(PluginCall call, String urlString, String method, JSObject headers) { + private void get(PluginCall call, String urlString, String method, JSObject headers, JSObject params) { try { + /* + Uri.Builder builder = Uri.parse(urlString) + .buildUpon(); + + */ URL url = new URL(urlString); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(method); @@ -62,7 +86,8 @@ private void get(PluginCall call, String urlString, String method, JSObject head conn.connect(); InputStream stream = conn.getInputStream(); - + + Log.d(getLogTag(), "GET request completed, got data"); } catch (MalformedURLException ex) { call.error("Invalid URL", ex); } catch (IOException ex) { @@ -92,17 +117,81 @@ public void uploadFile(PluginCall call) { @PluginMethod() public void setCookie(PluginCall call) { + String url = call.getString("url"); + String key = call.getString("key"); + String value = call.getString("value"); + + URI uri = getUri(url); + if (uri == null) { + call.error("Invalid URL"); + return; + } + + cookieManager.getCookieStore().add(uri, new HttpCookie(key, value)); + + call.resolve(); } @PluginMethod() - public void getCookie(PluginCall call) { + public void getCookies(PluginCall call) { + String url = call.getString("url"); + + URI uri = getUri(url); + if (uri == null) { + call.error("Invalid URL"); + return; + } + + List cookies = cookieManager.getCookieStore().get(uri); + + JSArray cookiesArray = new JSArray(); + + for (HttpCookie cookie : cookies) { + JSObject ret = new JSObject(); + ret.put("key", cookie.getName()); + ret.put("value", cookie.getValue()); + cookiesArray.put(ret); + } + + JSObject ret = new JSObject(); + ret.put("value", cookiesArray); + call.resolve(ret); } @PluginMethod() public void deleteCookie(PluginCall call) { + String url = call.getString("url"); + String key = call.getString("key"); + + URI uri = getUri(url); + if (uri == null) { + call.error("Invalid URL"); + return; + } + + + List cookies = cookieManager.getCookieStore().get(uri); + + for (HttpCookie cookie : cookies) { + if (cookie.getName().equals(key)) { + cookieManager.getCookieStore().remove(uri, cookie); + } + } + + call.resolve(); } @PluginMethod() public void clearCookies(PluginCall call) { + cookieManager.getCookieStore().removeAll(); + call.resolve(); + } + + private URI getUri(String url) { + try { + return new URI(url); + } catch (Exception ex) { + return null; + } } } \ No newline at end of file From 2cd077f62e3949ff21b84fd11f49b99341a500b7 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sat, 14 Mar 2020 11:35:45 -0500 Subject: [PATCH 13/29] Http cookies --- .../java/com/getcapacitor/plugin/Http.java | 14 +++-- .../android/app/src/main/AndroidManifest.xml | 1 + example/src/pages/filesystem/filesystem.ts | 2 +- example/src/pages/http/http.ts | 51 ++++++++++++------- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java index 3cf34849af..489782f9aa 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java @@ -88,10 +88,14 @@ private void get(PluginCall call, String urlString, String method, JSObject head InputStream stream = conn.getInputStream(); Log.d(getLogTag(), "GET request completed, got data"); + + call.resolve(); } catch (MalformedURLException ex) { - call.error("Invalid URL", ex); + call.reject("Invalid URL", ex); } catch (IOException ex) { - call.error("Error", ex); + call.reject("Error", ex); + } catch (Exception ex) { + call.reject("Error", ex); } } @@ -123,7 +127,7 @@ public void setCookie(PluginCall call) { URI uri = getUri(url); if (uri == null) { - call.error("Invalid URL"); + call.reject("Invalid URL"); return; } @@ -138,7 +142,7 @@ public void getCookies(PluginCall call) { URI uri = getUri(url); if (uri == null) { - call.error("Invalid URL"); + call.reject("Invalid URL"); return; } @@ -165,7 +169,7 @@ public void deleteCookie(PluginCall call) { URI uri = getUri(url); if (uri == null) { - call.error("Invalid URL"); + call.reject("Invalid URL"); return; } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 75c05281f7..ef723c5d50 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ package="com.getcapacitor.myapp"> android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:usesCleartextTraffic="true" android:theme="@style/AppTheme"> this.get('/head', 'HEAD'); @@ -130,12 +136,19 @@ export class HttpPage { content: 'Requesting...' }); this.loading.present(); - const ret = await Http.request({ - method: 'GET', - url: this.apiUrl('/cookie') - }); - console.log('Got ret', ret); - this.loading.dismiss(); + try { + const ret = await Http.request({ + method: 'GET', + url: this.apiUrl('/cookie') + }); + console.log('Got ret', ret); + this.loading.dismiss(); + } catch (e) { + this.output = `Error: ${e.message}`; + console.error(e); + } finally { + this.loading.dismiss(); + } } downloadFile = async () => { From ffdfe3588b0deb1af6f84b04884e2ca121a96ecb Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sat, 14 Mar 2020 16:08:19 -0500 Subject: [PATCH 14/29] Response and headers for Android --- .../java/com/getcapacitor/plugin/Http.java | 79 ++++++++++++++++--- core/src/plugins/http.ts | 9 +++ example/src/pages/http/http.html | 2 + example/src/pages/http/http.ts | 3 + .../Capacitor/Plugins/Http/Http.swift | 11 +-- 5 files changed, 87 insertions(+), 17 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java index 489782f9aa..2419462376 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java @@ -16,9 +16,11 @@ import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; +import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.CookieHandler; import java.net.CookieManager; import java.net.HttpCookie; @@ -68,28 +70,28 @@ public void request(PluginCall call) { private void get(PluginCall call, String urlString, String method, JSObject headers, JSObject params) { try { - /* - Uri.Builder builder = Uri.parse(urlString) - .buildUpon(); + Integer connectTimeout = call.getInt("connectTimeout"); + Integer readTimeout = call.getInt("readTimeout"); - */ URL url = new URL(urlString); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setAllowUserInteraction(false); conn.setRequestMethod(method); - setRequestHeaders(conn, headers); - - conn.setDoInput(true); + if (connectTimeout != null) { + conn.setConnectTimeout(connectTimeout); + } - conn.connect(); + if (readTimeout != null) { + conn.setReadTimeout(readTimeout); + } - InputStream stream = conn.getInputStream(); + setRequestHeaders(conn, headers); - Log.d(getLogTag(), "GET request completed, got data"); + conn.connect(); - call.resolve(); + buildResponse(call, conn); } catch (MalformedURLException ex) { call.reject("Invalid URL", ex); } catch (IOException ex) { @@ -99,6 +101,59 @@ private void get(PluginCall call, String urlString, String method, JSObject head } } + private void buildResponse(PluginCall call, HttpURLConnection conn) throws Exception { + int statusCode = conn.getResponseCode(); + + JSObject ret = new JSObject(); + ret.put("status", statusCode); + ret.put("headers", makeResponseHeaders(conn)); + + InputStream stream = conn.getInputStream(); + + BufferedReader in = new BufferedReader(new InputStreamReader(stream)); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = in.readLine()) != null) { + builder.append(line); + } + in.close(); + + Log.d(getLogTag(), "GET request completed, got data"); + + String contentType = conn.getHeaderField("Content-Type"); + + if (contentType != null) { + if (contentType.contains("application/json")) { + JSObject jsonValue = new JSObject(builder.toString()); + ret.put("data", jsonValue); + } else { + ret.put("data", builder.toString()); + } + } else { + ret.put("data", builder.toString()); + } + + call.resolve(ret); + } + + private JSArray makeResponseHeaders(HttpURLConnection conn) { + JSArray ret = new JSArray(); + + for (Map.Entry> entries : conn.getHeaderFields().entrySet()) { + JSObject header = new JSObject(); + + String val = ""; + for (String headerVal : entries.getValue()) { + val += headerVal + ", "; + } + + header.put(entries.getKey(), val); + ret.put(header); + } + + return ret; + } + private void setRequestHeaders(HttpURLConnection conn, JSObject headers) { Iterator keys = headers.keys(); while (keys.hasNext()) { diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts index 89932dd7ef..88d82acba5 100644 --- a/core/src/plugins/http.ts +++ b/core/src/plugins/http.ts @@ -17,6 +17,15 @@ export interface HttpOptions { params?: HttpParams; data?: any; headers?: HttpHeaders; + /** + * How long to wait to read additional data. Resets each time new + * data is received + */ + readTimeout?: number; + /** + * How long to wait for the initial connection. + */ + connectTimeout?: number; } export interface HttpParams { diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html index abbebb96e0..a0e120f9a8 100644 --- a/example/src/pages/http/http.html +++ b/example/src/pages/http/http.html @@ -15,6 +15,8 @@ + + diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index 01728ac344..385494b7db 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -61,6 +61,9 @@ export class HttpPage { } } + getJson = () => this.get('/get-json'); + getHtml = () => this.get('/get-html'); + head = () => this.get('/head', 'HEAD'); delete = () => this.mutate('/delete', 'DELETE', { title: 'foo', body: 'bar', userId: 1 }); patch = () => this.mutate('/patch', 'PATCH', { title: 'foo', body: 'bar', userId: 1 }); diff --git a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift index 5caaa96c4c..66746bfa7f 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift @@ -318,12 +318,13 @@ public class CAPHttpPlugin: CAPPlugin { // handle json... ret["data"] = json } + } else { + if (data != nil) { + ret["data"] = String(data: data!, encoding: .utf8); + } else { + ret["data"] = "" + } } - // TODO: Handle other response content types, including binary - /* - else { - ret["data"] = - }*/ return ret } From b32b9790542137b9adfc767bac24ee40d754e6dd Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Thu, 19 Mar 2020 12:51:26 -0500 Subject: [PATCH 15/29] Post JSON on Android --- .../java/com/getcapacitor/PluginCall.java | 15 ++++-- .../java/com/getcapacitor/plugin/Http.java | 53 ++++++++++++++++++- example/src/pages/http/http.html | 2 +- example/src/pages/http/http.scss | 13 +++++ example/src/pages/http/http.ts | 31 ++++++----- 5 files changed, 96 insertions(+), 18 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java b/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java index d3bbac9393..1601fd3234 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java +++ b/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java @@ -83,7 +83,7 @@ public void errorCallback(String msg) { this.msgHandler.sendResponseMessage(this, null, errorResult); } - public void error(String msg, Exception ex) { + public void error(String msg, Exception ex, JSObject data) { PluginResult errorResult = new PluginResult(); if(ex != null) { @@ -92,6 +92,7 @@ public void error(String msg, Exception ex) { try { errorResult.put("message", msg); + errorResult.put("platformMessage", ex.getMessage()); } catch (Exception jsonEx) { Log.e(LogUtils.getPluginTag(), jsonEx.getMessage()); } @@ -99,12 +100,20 @@ public void error(String msg, Exception ex) { this.msgHandler.sendResponseMessage(this, null, errorResult); } + public void error(String msg, Exception ex) { + error(msg, ex, null); + } + public void error(String msg) { - error(msg, null); + error(msg, null, null); + } + + public void reject(String msg, Exception ex, JSObject data) { + error(msg, ex, data); } public void reject(String msg, Exception ex) { - error(msg, ex); + error(msg, ex, null); } public void reject(String msg) { diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java index 2419462376..2e5bea9f71 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java @@ -28,6 +28,7 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -63,7 +64,7 @@ public void request(PluginCall call) { case "PATCH": case "POST": case "PUT": - mutate(call, url, method); + mutate(call, url, method, headers); return; } } @@ -163,7 +164,55 @@ private void setRequestHeaders(HttpURLConnection conn, JSObject headers) { } } - private void mutate(PluginCall call, String url, String method) { + private void setRequestBody(HttpURLConnection conn, JSObject data, JSObject headers) throws IOException { + String contentType = conn.getHeaderField("Content-Type"); + + if (contentType != null) { + if (contentType.contains("application/json")) { + DataOutputStream os = new DataOutputStream(conn.getOutputStream()); + os.writeBytes(URLEncoder.encode(data.toString(), "UTF-8")); + os.flush(); + os.close(); + } else if (contentType.contains("application/x-www-form-urlencoded")) { + } else if (contentType.contains("multipart/form-data")) { + } + } + } + + private void mutate(PluginCall call, String urlString, String method, JSObject headers) { + try { + Integer connectTimeout = call.getInt("connectTimeout"); + Integer readTimeout = call.getInt("readTimeout"); + JSObject data = call.getObject("data"); + + URL url = new URL(urlString); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setAllowUserInteraction(false); + conn.setRequestMethod(method); + + if (connectTimeout != null) { + conn.setConnectTimeout(connectTimeout); + } + + if (readTimeout != null) { + conn.setReadTimeout(readTimeout); + } + + setRequestHeaders(conn, headers); + + setRequestBody(conn, data, headers); + + conn.connect(); + + buildResponse(call, conn); + } catch (MalformedURLException ex) { + call.reject("Invalid URL", ex); + } catch (IOException ex) { + call.reject("Error", ex); + } catch (Exception ex) { + call.reject("Error", ex); + } } @PluginMethod() diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html index a0e120f9a8..b07721a43b 100644 --- a/example/src/pages/http/http.html +++ b/example/src/pages/http/http.html @@ -35,5 +35,5 @@

Output

- +
{{output}}
\ No newline at end of file diff --git a/example/src/pages/http/http.scss b/example/src/pages/http/http.scss index e28a8a1a9c..377b84b2e6 100644 --- a/example/src/pages/http/http.scss +++ b/example/src/pages/http/http.scss @@ -3,4 +3,17 @@ ion-textarea { textarea { height: 500px; } +} + +#output { + height: 400px; + overflow: auto; + + unicode-bidi: embed; + font-family: monospace; + white-space: pre; + word-wrap: break-word; + max-width: 100%; + word-break: break-word; + white-space: pre; } \ No newline at end of file diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index 385494b7db..fae0a49134 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -54,7 +54,7 @@ export class HttpPage { console.log('Got ret', ret); this.output = JSON.stringify(ret, null, 2); } catch (e) { - this.output = `Error: ${e.message}`; + this.output = `Error: ${e.message}, ${e.platformMessage}`; console.error(e); } finally { this.loading.dismiss(); @@ -76,17 +76,24 @@ export class HttpPage { content: 'Requesting...' }); this.loading.present(); - const ret = await Http.request({ - url: this.apiUrl(path), - method: method, - headers: { - 'content-type': 'application/json', - }, - data - }); - console.log('Got ret', ret); - this.loading.dismiss(); - this.output = JSON.stringify(ret, null, 2); + try { + const ret = await Http.request({ + url: this.apiUrl(path), + method: method, + headers: { + 'content-type': 'application/json', + }, + data + }); + console.log('Got ret', ret); + this.loading.dismiss(); + this.output = JSON.stringify(ret, null, 2); + } catch (e) { + this.output = `Error: ${e.message}, ${e.platformMessage}`; + console.error(e); + } finally { + this.loading.dismiss(); + } } apiUrl = (path: string) => `${this.serverUrl}${path}`; From 178df14f918ab5d2bceb07fa634522acc4278c5c Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Thu, 19 Mar 2020 14:06:55 -0500 Subject: [PATCH 16/29] Android post urlencoded form data --- .../java/com/getcapacitor/plugin/Http.java | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java index 2e5bea9f71..2b803633de 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java @@ -16,6 +16,8 @@ import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; +import org.json.JSONException; + import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; @@ -155,30 +157,6 @@ private JSArray makeResponseHeaders(HttpURLConnection conn) { return ret; } - private void setRequestHeaders(HttpURLConnection conn, JSObject headers) { - Iterator keys = headers.keys(); - while (keys.hasNext()) { - String key = keys.next(); - String value = headers.getString(key); - conn.setRequestProperty(key, value); - } - } - - private void setRequestBody(HttpURLConnection conn, JSObject data, JSObject headers) throws IOException { - String contentType = conn.getHeaderField("Content-Type"); - - if (contentType != null) { - if (contentType.contains("application/json")) { - DataOutputStream os = new DataOutputStream(conn.getOutputStream()); - os.writeBytes(URLEncoder.encode(data.toString(), "UTF-8")); - os.flush(); - os.close(); - } else if (contentType.contains("application/x-www-form-urlencoded")) { - } else if (contentType.contains("multipart/form-data")) { - } - } - } - private void mutate(PluginCall call, String urlString, String method, JSObject headers) { try { Integer connectTimeout = call.getInt("connectTimeout"); @@ -295,6 +273,49 @@ public void clearCookies(PluginCall call) { call.resolve(); } + + private void setRequestHeaders(HttpURLConnection conn, JSObject headers) { + Iterator keys = headers.keys(); + while (keys.hasNext()) { + String key = keys.next(); + String value = headers.getString(key); + conn.setRequestProperty(key, value); + } + } + + private void setRequestBody(HttpURLConnection conn, JSObject data, JSObject headers) throws IOException, JSONException { + String contentType = conn.getRequestProperty("Content-Type"); + + if (contentType != null) { + if (contentType.contains("application/json")) { + DataOutputStream os = new DataOutputStream(conn.getOutputStream()); + os.writeBytes(data.toString()); + os.flush(); + os.close(); + } else if (contentType.contains("application/x-www-form-urlencoded")) { + + StringBuilder builder = new StringBuilder(); + + Iterator keys = data.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object d = data.get(key); + if (d != null) { + builder.append(key + "=" + URLEncoder.encode(d.toString(), "UTF-8")); + if (keys.hasNext()) { + builder.append("&"); + } + } + } + + DataOutputStream os = new DataOutputStream(conn.getOutputStream()); + os.writeBytes(builder.toString()); + os.flush(); + os.close(); + } else if (contentType.contains("multipart/form-data")) { + } + } + } private URI getUri(String url) { try { return new URI(url); From 9e06d7b3880d0fa274155f1eada74a24b889a591 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Thu, 19 Mar 2020 17:02:37 -0500 Subject: [PATCH 17/29] Download sort of working on android --- .../main/java/com/getcapacitor/Bridge.java | 4 +- .../plugin/{ => filesystem}/Filesystem.java | 90 +++-------- .../plugin/filesystem/FilesystemUtils.java | 68 ++++++++ .../getcapacitor/plugin/http/FormBuilder.java | 145 ++++++++++++++++++ .../getcapacitor/plugin/{ => http}/Http.java | 95 +++++++++++- core/src/plugins/fs.ts | 4 + example/src/pages/http/http.html | 1 + example/src/pages/http/http.ts | 80 ++++++++-- .../Capacitor/Plugins/Http/Http.swift | 2 +- 9 files changed, 403 insertions(+), 86 deletions(-) rename android/capacitor/src/main/java/com/getcapacitor/plugin/{ => filesystem}/Filesystem.java (89%) create mode 100644 android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/FilesystemUtils.java create mode 100644 android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormBuilder.java rename android/capacitor/src/main/java/com/getcapacitor/plugin/{ => http}/Http.java (73%) diff --git a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java index 2b108d062e..4b219ae032 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java @@ -24,10 +24,10 @@ import com.getcapacitor.plugin.Camera; import com.getcapacitor.plugin.Clipboard; import com.getcapacitor.plugin.Device; -import com.getcapacitor.plugin.Filesystem; +import com.getcapacitor.plugin.filesystem.Filesystem; import com.getcapacitor.plugin.Geolocation; import com.getcapacitor.plugin.Haptics; -import com.getcapacitor.plugin.Http; +import com.getcapacitor.plugin.http.Http; import com.getcapacitor.plugin.Keyboard; import com.getcapacitor.plugin.LocalNotifications; import com.getcapacitor.plugin.Modals; diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Filesystem.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/Filesystem.java similarity index 89% rename from android/capacitor/src/main/java/com/getcapacitor/plugin/Filesystem.java rename to android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/Filesystem.java index 5b99f5cb2f..fda96bf488 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Filesystem.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/Filesystem.java @@ -1,4 +1,4 @@ -package com.getcapacitor.plugin; +package com.getcapacitor.plugin.filesystem; import android.Manifest; import android.content.Context; @@ -62,46 +62,6 @@ private Charset getEncoding(String encoding) { return null; } - private File getDirectory(String directory) { - Context c = bridge.getContext(); - switch(directory) { - case "APPLICATION": - return c.getFilesDir(); - case "DOCUMENTS": - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); - case "DATA": - return c.getFilesDir(); - case "CACHE": - return c.getCacheDir(); - case "EXTERNAL": - return c.getExternalFilesDir(null); - case "EXTERNAL_STORAGE": - return Environment.getExternalStorageDirectory(); - } - return null; - } - - private File getFileObject(String path, String directory) { - if (directory == null) { - Uri u = Uri.parse(path); - if (u.getScheme() == null || u.getScheme().equals("file")) { - return new File(u.getPath()); - } - } - - File androidDirectory = this.getDirectory(directory); - - if (androidDirectory == null) { - return null; - } else { - if(!androidDirectory.exists()) { - androidDirectory.mkdir(); - } - } - - return new File(androidDirectory, path); - } - private InputStream getInputStream(String path, String directory) throws IOException { if (directory == null) { Uri u = Uri.parse(path); @@ -112,7 +72,7 @@ private InputStream getInputStream(String path, String directory) throws IOExcep } } - File androidDirectory = this.getDirectory(directory); + File androidDirectory = FilesystemUtils.getDirectory(getContext(), directory); if (androidDirectory == null) { throw new IOException("Directory not found"); @@ -163,7 +123,7 @@ public void readFile(PluginCall call) { return; } - if (!isPublicDirectory(directory) + if (!FilesystemUtils.isPublicDirectory(directory) || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_READ_FILE_PERMISSIONS, Manifest.permission.READ_EXTERNAL_STORAGE)) { try { InputStream is = getInputStream(file, directory); @@ -208,10 +168,10 @@ public void writeFile(PluginCall call) { String directory = getDirectoryParameter(call); if (directory != null) { - if (!isPublicDirectory(directory) + if (!FilesystemUtils.isPublicDirectory(directory) || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_WRITE_FILE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // create directory because it might not exist - File androidDir = getDirectory(directory); + File androidDir = FilesystemUtils.getDirectory(getContext(), directory); if (androidDir != null) { if (androidDir.exists() || androidDir.mkdirs()) { // path might include directories as well @@ -283,7 +243,7 @@ private void saveFile(PluginCall call, File file, String data) { if (success) { // update mediaStore index only if file was written to external storage - if (isPublicDirectory(getDirectoryParameter(call))) { + if (FilesystemUtils.isPublicDirectory(getDirectoryParameter(call))) { MediaScannerConnection.scanFile(getContext(), new String[] {file.getAbsolutePath()}, null, null); } Log.d(getLogTag(), "File '" + file.getAbsolutePath() + "' saved!"); @@ -310,9 +270,9 @@ public void deleteFile(PluginCall call) { String file = call.getString("path"); String directory = getDirectoryParameter(call); - File fileObject = getFileObject(file, directory); + File fileObject = FilesystemUtils.getFileObject(getContext(), file, directory); - if (!isPublicDirectory(directory) + if (!FilesystemUtils.isPublicDirectory(directory) || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_DELETE_FILE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { if (!fileObject.exists()) { call.error("File does not exist"); @@ -335,14 +295,14 @@ public void mkdir(PluginCall call) { String directory = getDirectoryParameter(call); boolean recursive = call.getBoolean("recursive", false).booleanValue(); - File fileObject = getFileObject(path, directory); + File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory); if (fileObject.exists()) { call.error("Directory exists"); return; } - if (!isPublicDirectory(directory) + if (!FilesystemUtils.isPublicDirectory(directory) || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_WRITE_FOLDER_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { boolean created = false; if (recursive) { @@ -365,9 +325,9 @@ public void rmdir(PluginCall call) { String directory = getDirectoryParameter(call); Boolean recursive = call.getBoolean("recursive", false); - File fileObject = getFileObject(path, directory); + File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory); - if (!isPublicDirectory(directory) + if (!FilesystemUtils.isPublicDirectory(directory) || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_DELETE_FOLDER_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { if (!fileObject.exists()) { call.error("Directory does not exist"); @@ -401,9 +361,9 @@ public void readdir(PluginCall call) { String path = call.getString("path"); String directory = getDirectoryParameter(call); - File fileObject = getFileObject(path, directory); + File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory); - if (!isPublicDirectory(directory) + if (!FilesystemUtils.isPublicDirectory(directory) || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_READ_FOLDER_PERMISSIONS, Manifest.permission.READ_EXTERNAL_STORAGE)) { if (fileObject != null && fileObject.exists()) { String[] files = fileObject.list(); @@ -423,9 +383,9 @@ public void getUri(PluginCall call) { String path = call.getString("path"); String directory = getDirectoryParameter(call); - File fileObject = getFileObject(path, directory); + File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory); - if (!isPublicDirectory(directory) + if (!FilesystemUtils.isPublicDirectory(directory) || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_URI_PERMISSIONS, Manifest.permission.READ_EXTERNAL_STORAGE)) { JSObject data = new JSObject(); data.put("uri", Uri.fromFile(fileObject).toString()); @@ -439,9 +399,9 @@ public void stat(PluginCall call) { String path = call.getString("path"); String directory = getDirectoryParameter(call); - File fileObject = getFileObject(path, directory); + File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory); - if (!isPublicDirectory(directory) + if (!FilesystemUtils.isPublicDirectory(directory) || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_STAT_PERMISSIONS, Manifest.permission.READ_EXTERNAL_STORAGE)) { if (!fileObject.exists()) { call.error("File does not exist"); @@ -525,8 +485,8 @@ private void _copy(PluginCall call, boolean doRename) { return; } - File fromObject = getFileObject(from, directory); - File toObject = getFileObject(to, toDirectory); + File fromObject = FilesystemUtils.getFileObject(getContext(), from, directory); + File toObject = FilesystemUtils.getFileObject(getContext(), to, toDirectory); assert fromObject != null; assert toObject != null; @@ -551,7 +511,7 @@ private void _copy(PluginCall call, boolean doRename) { return; } - if (isPublicDirectory(directory) || isPublicDirectory(toDirectory)) { + if (FilesystemUtils.isPublicDirectory(directory) || FilesystemUtils.isPublicDirectory(toDirectory)) { if (doRename) { if (!isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_RENAME_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { return; @@ -626,13 +586,7 @@ private String getDirectoryParameter(PluginCall call) { return call.getString("directory"); } - /** - * True if the given directory string is a public storage directory, which is accessible by the user or other apps. - * @param directory the directory string. - */ - private boolean isPublicDirectory(String directory) { - return "DOCUMENTS".equals(directory) || "EXTERNAL_STORAGE".equals(directory); - } + @Override protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/FilesystemUtils.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/FilesystemUtils.java new file mode 100644 index 0000000000..0978b605c5 --- /dev/null +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/FilesystemUtils.java @@ -0,0 +1,68 @@ +package com.getcapacitor.plugin.filesystem; + +import android.content.Context; +import android.net.Uri; +import android.os.Environment; + +import java.io.File; + +public class FilesystemUtils { + public static final String DIRECTORY_DOCUMENTS = "DOCUMENTS"; + public static final String DIRECTORY_APPLICATION = "APPLICATION"; + public static final String DIRECTORY_DOWNLOADS = "DOWNLOADS"; + public static final String DIRECTORY_DATA = "DATA"; + public static final String DIRECTORY_CACHE = "CACHE"; + public static final String DIRECTORY_EXTERNAL = "EXTERNAL"; + public static final String DIRECTORY_EXTERNAL_STORAGE = "EXTERNAL_STORAGE"; + + public static File getFileObject(Context c, String path, String directory) { + if (directory == null) { + Uri u = Uri.parse(path); + if (u.getScheme() == null || u.getScheme().equals("file")) { + return new File(u.getPath()); + } + } + + File androidDirectory = FilesystemUtils.getDirectory(c, directory); + + if (androidDirectory == null) { + return null; + } else { + if(!androidDirectory.exists()) { + androidDirectory.mkdir(); + } + } + + return new File(androidDirectory, path); + } + + public static File getDirectory(Context c, String directory) { + switch(directory) { + case DIRECTORY_APPLICATION: + return c.getFilesDir(); + case DIRECTORY_DOCUMENTS: + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); + case DIRECTORY_DOWNLOADS: + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + case DIRECTORY_DATA: + return c.getFilesDir(); + case DIRECTORY_CACHE: + return c.getCacheDir(); + case DIRECTORY_EXTERNAL: + return c.getExternalFilesDir(null); + case DIRECTORY_EXTERNAL_STORAGE: + return Environment.getExternalStorageDirectory(); + } + return null; + } + + /** + * True if the given directory string is a public storage directory, which is accessible by the user or other apps. + * @param directory the directory string. + */ + public static boolean isPublicDirectory(String directory) { + return DIRECTORY_DOCUMENTS.equals(directory) || + DIRECTORY_DOWNLOADS.equals(directory) || + "EXTERNAL_STORAGE".equals(directory); + } +} diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormBuilder.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormBuilder.java new file mode 100644 index 0000000000..0f216d29d0 --- /dev/null +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormBuilder.java @@ -0,0 +1,145 @@ +package com.getcapacitor.plugin.http; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class FormBuilder { + private final String boundary; + private static final String LINE_FEED = "\r\n"; + private HttpURLConnection httpConn; + private String charset; + private OutputStream outputStream; + private PrintWriter writer; + + /** + * This constructor initializes a new HTTP POST request with content type + * is set to multipart/form-data + * + * @param requestURL + * @param charset + * @throws java.io.IOException + */ + public FormBuilder(String requestURL, String charset) + throws IOException { + this.charset = charset; + + // creates a unique boundary based on time stamp + UUID uuid = UUID.randomUUID(); + boundary = "===" + uuid.toString() + "==="; + URL url = new URL(requestURL); + httpConn = (HttpURLConnection) url.openConnection(); + httpConn.setUseCaches(false); + httpConn.setDoOutput(true); // indicates POST method + httpConn.setDoInput(true); + httpConn.setRequestProperty("Content-Type", + "multipart/form-data; boundary=" + boundary); + outputStream = httpConn.getOutputStream(); + writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), + true); + } + + /** + * Adds a form field to the request + * + * @param name field name + * @param value field value + */ + public void addFormField(String name, String value) { + writer.append("--" + boundary).append(LINE_FEED); + writer.append("Content-Disposition: form-data; name=\"" + name + "\"") + .append(LINE_FEED); + writer.append("Content-Type: text/plain; charset=" + charset).append( + LINE_FEED); + writer.append(LINE_FEED); + writer.append(value).append(LINE_FEED); + writer.flush(); + } + + /** + * Adds a upload file section to the request + * + * @param fieldName name attribute in + * @param uploadFile a File to be uploaded + * @throws IOException + */ + public void addFilePart(String fieldName, File uploadFile) + throws IOException { + String fileName = uploadFile.getName(); + writer.append("--" + boundary).append(LINE_FEED); + writer.append( + "Content-Disposition: form-data; name=\"" + fieldName + + "\"; filename=\"" + fileName + "\"") + .append(LINE_FEED); + writer.append( + "Content-Type: " + + URLConnection.guessContentTypeFromName(fileName)) + .append(LINE_FEED); + writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED); + writer.append(LINE_FEED); + writer.flush(); + + FileInputStream inputStream = new FileInputStream(uploadFile); + byte[] buffer = new byte[4096]; + int bytesRead = -1; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + inputStream.close(); + writer.append(LINE_FEED); + writer.flush(); + } + + /** + * Adds a header field to the request. + * + * @param name - name of the header field + * @param value - value of the header field + */ + public void addHeaderField(String name, String value) { + writer.append(name + ": " + value).append(LINE_FEED); + writer.flush(); + } + + /** + * Completes the request and receives response from the server. + * + * @return a list of Strings as response in case the server returned + * status OK, otherwise an exception is thrown. + * @throws IOException + */ + public List finish() throws IOException { + List response = new ArrayList<>(); + writer.append(LINE_FEED).flush(); + writer.append("--" + boundary + "--").append(LINE_FEED); + writer.close(); + + // checks server's status code first + int status = httpConn.getResponseCode(); + if (status == HttpURLConnection.HTTP_OK) { + BufferedReader reader = new BufferedReader(new InputStreamReader( + httpConn.getInputStream())); + String line = null; + while ((line = reader.readLine()) != null) { + response.add(line); + } + reader.close(); + httpConn.disconnect(); + } else { + throw new IOException("Server returned non-OK status: " + status); + } + return response; + } +} diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java similarity index 73% rename from android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java rename to android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java index 2b803633de..f0a16e0826 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java @@ -1,4 +1,4 @@ -package com.getcapacitor.plugin; +package com.getcapacitor.plugin.http; import android.Manifest; import android.content.Context; @@ -15,11 +15,15 @@ import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; +import com.getcapacitor.PluginRequestCodes; +import com.getcapacitor.plugin.filesystem.FilesystemUtils; import org.json.JSONException; import java.io.BufferedReader; import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -195,10 +199,98 @@ private void mutate(PluginCall call, String urlString, String method, JSObject h @PluginMethod() public void downloadFile(PluginCall call) { + try { + String urlString = call.getString("url"); + String filePath = call.getString("filePath"); + String fileDirectory = call.getString("fileDirectory", FilesystemUtils.DIRECTORY_DOCUMENTS); + JSObject headers = call.getObject("headers"); + + Integer connectTimeout = call.getInt("connectTimeout"); + Integer readTimeout = call.getInt("readTimeout"); + + URL url = new URL(urlString); + + if (!FilesystemUtils.isPublicDirectory(fileDirectory) + || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_WRITE_FILE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + + } + + + File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); + + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setAllowUserInteraction(false); + conn.setRequestMethod("GET"); + + if (connectTimeout != null) { + conn.setConnectTimeout(connectTimeout); + } + + if (readTimeout != null) { + conn.setReadTimeout(readTimeout); + } + + setRequestHeaders(conn, headers); + + InputStream is = conn.getInputStream(); + + FileOutputStream fos = new FileOutputStream(file, false); + + byte[] buffer = new byte[1024]; + int len; + + while ((len = is.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + + is.close(); + fos.close(); + + call.resolve(); + } catch (MalformedURLException ex) { + call.reject("Invalid URL", ex); + } catch (IOException ex) { + call.reject("Error", ex); + } catch (Exception ex) { + call.reject("Error", ex); + } + } + + private boolean isStoragePermissionGranted(int permissionRequestCode, String permission) { + if (hasPermission(permission)) { + Log.v(getLogTag(),"Permission '" + permission + "' is granted"); + return true; + } else { + Log.v(getLogTag(),"Permission '" + permission + "' denied. Asking user for it."); + pluginRequestPermissions(new String[] {permission}, permissionRequestCode); + return false; + } } + @Override + protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.handleRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @PluginMethod() public void uploadFile(PluginCall call) { + String url = call.getString("url"); + String filePath = call.getString("filePath"); + String fileDirectory = call.getString("fileDirectory", FilesystemUtils.DIRECTORY_DOCUMENTS); + String name = call.getString("name", "file"); + JSObject headers = call.getObject("headers"); + JSObject params = call.getObject("params"); + JSObject data = call.getObject("data"); + + try { + File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); + + call.resolve(); + } catch (Exception ex) { + call.reject("Error", ex); + } } @PluginMethod() @@ -313,6 +405,7 @@ private void setRequestBody(HttpURLConnection conn, JSObject data, JSObject head os.flush(); os.close(); } else if (contentType.contains("multipart/form-data")) { + } } } diff --git a/core/src/plugins/fs.ts b/core/src/plugins/fs.ts index d4bef27789..1d2e486eb8 100644 --- a/core/src/plugins/fs.ts +++ b/core/src/plugins/fs.ts @@ -87,6 +87,10 @@ export enum FilesystemDirectory { * The Documents directory */ Documents = 'DOCUMENTS', + /** + * The Downloads directory + */ + Downloads = 'DOWNLOADS', /** * The Data directory */ diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html index b07721a43b..b4ebec38fb 100644 --- a/example/src/pages/http/http.html +++ b/example/src/pages/http/http.html @@ -24,6 +24,7 @@ + diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index fae0a49134..addc023a91 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -99,17 +99,61 @@ export class HttpPage { apiUrl = (path: string) => `${this.serverUrl}${path}`; formPost = async () => { - const ret = await Http.request({ - url: this.apiUrl('/form-data'), - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - data: { - name: 'Max', - age: 5 - } + this.output = ''; + this.loading = this.loadingCtrl.create({ + content: 'Requesting...' }); + this.loading.present(); + try { + const ret = await Http.request({ + url: this.apiUrl('/form-data'), + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + data: { + name: 'Max', + age: 5 + } + }); + console.log('Got ret', ret); + this.loading.dismiss(); + this.output = JSON.stringify(ret, null, 2); + } catch (e) { + this.output = `Error: ${e.message}, ${e.platformMessage}`; + console.error(e); + } finally { + this.loading.dismiss(); + } + } + + formPostMultipart = async () => { + this.output = ''; + this.loading = this.loadingCtrl.create({ + content: 'Requesting...' + }); + this.loading.present(); + try { + const ret = await Http.request({ + url: this.apiUrl('/form-data-multi'), + method: 'POST', + headers: { + 'content-type': 'multipart/form-data' + }, + data: { + name: 'Max', + age: 5 + } + }); + console.log('Got ret', ret); + this.loading.dismiss(); + this.output = JSON.stringify(ret, null, 2); + } catch (e) { + this.output = `Error: ${e.message}, ${e.platformMessage}`; + console.error(e); + } finally { + this.loading.dismiss(); + } } setCookie = async () => { @@ -162,25 +206,32 @@ export class HttpPage { } downloadFile = async () => { + console.log('Doing download', FilesystemDirectory.Downloads); + const ret = await Http.downloadFile({ url: this.apiUrl('/download-pdf'), - filePath: 'document.pdf' + filePath: 'document.pdf', + fileDirectory: FilesystemDirectory.Downloads }); console.log('Got download ret', ret); + /* const renameRet = await Filesystem.rename({ from: ret.path, to: 'document.pdf', - toDirectory: FilesystemDirectory.Documents + toDirectory: FilesystemDirectory.Downloads }); console.log('Did rename', renameRet); + */ const read = await Filesystem.readFile({ path: 'document.pdf', - directory: FilesystemDirectory.Documents - }) + directory: FilesystemDirectory.Downloads + }); + + console.log('Read', read); } uploadFile = async () => { @@ -188,6 +239,7 @@ export class HttpPage { url: this.apiUrl('/upload-pdf'), name: 'myFile', filePath: 'document.pdf', + fileDirectory: FilesystemDirectory.Downloads }); console.log('Got upload ret', ret); diff --git a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift index 66746bfa7f..0d8626a4bc 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift @@ -94,7 +94,7 @@ public class CAPHttpPlugin: CAPPlugin { } let name = call.getString("name") ?? "file" - let fileDirectory = call.getString("filePath") ?? "DOCUMENTS" + let fileDirectory = call.getString("fileDirectory") ?? "DOCUMENTS" guard let url = URL(string: urlValue) else { return call.reject("Invalid URL") From e879dca8bbd4db45f5861e619db4341c17735d47 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Thu, 19 Mar 2020 17:26:59 -0500 Subject: [PATCH 18/29] Uploads at least working on Android --- .../{FormBuilder.java => FormUploader.java} | 20 +++++++++---------- .../com/getcapacitor/plugin/http/Http.java | 11 ++++------ 2 files changed, 13 insertions(+), 18 deletions(-) rename android/capacitor/src/main/java/com/getcapacitor/plugin/http/{FormBuilder.java => FormUploader.java} (91%) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormBuilder.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java similarity index 91% rename from android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormBuilder.java rename to android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java index 0f216d29d0..1b75990b89 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormBuilder.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java @@ -15,11 +15,11 @@ import java.util.List; import java.util.UUID; -public class FormBuilder { +public class FormUploader { private final String boundary; private static final String LINE_FEED = "\r\n"; private HttpURLConnection httpConn; - private String charset; + private String charset = "UTF-8"; private OutputStream outputStream; private PrintWriter writer; @@ -28,16 +28,12 @@ public class FormBuilder { * is set to multipart/form-data * * @param requestURL - * @param charset * @throws java.io.IOException */ - public FormBuilder(String requestURL, String charset) - throws IOException { - this.charset = charset; - + public FormUploader(String requestURL) throws IOException { // creates a unique boundary based on time stamp UUID uuid = UUID.randomUUID(); - boundary = "===" + uuid.toString() + "==="; + boundary = uuid.toString(); URL url = new URL(requestURL); httpConn = (HttpURLConnection) url.openConnection(); httpConn.setUseCaches(false); @@ -77,6 +73,7 @@ public void addFormField(String name, String value) { public void addFilePart(String fieldName, File uploadFile) throws IOException { String fileName = uploadFile.getName(); + writer.append(LINE_FEED); writer.append("--" + boundary).append(LINE_FEED); writer.append( "Content-Disposition: form-data; name=\"" + fieldName @@ -85,9 +82,10 @@ public void addFilePart(String fieldName, File uploadFile) writer.append( "Content-Type: " + URLConnection.guessContentTypeFromName(fileName)) + .append(LINE_FEED) .append(LINE_FEED); - writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED); - writer.append(LINE_FEED); + //writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED); + // writer.append(LINE_FEED); writer.flush(); FileInputStream inputStream = new FileInputStream(uploadFile); @@ -98,7 +96,7 @@ public void addFilePart(String fieldName, File uploadFile) } outputStream.flush(); inputStream.close(); - writer.append(LINE_FEED); + writer.append(LINE_FEED).append("--" + boundary + "--").append(LINE_FEED); writer.flush(); } diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java index f0a16e0826..87ec56545c 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java @@ -1,13 +1,7 @@ package com.getcapacitor.plugin.http; import android.Manifest; -import android.content.Context; -import android.net.Uri; -import android.os.Build; -import android.os.VibrationEffect; -import android.os.Vibrator; import android.util.Log; -import android.view.HapticFeedbackConstants; import com.getcapacitor.JSArray; import com.getcapacitor.JSObject; @@ -35,7 +29,6 @@ import java.net.URI; import java.net.URL; import java.net.URLEncoder; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -287,6 +280,10 @@ public void uploadFile(PluginCall call) { try { File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); + FormUploader builder = new FormUploader(url); + builder.addFilePart(name, file); + builder.finish(); + call.resolve(); } catch (Exception ex) { call.reject("Error", ex); From 3d323127d754222eee252ff551a3f2b33e9ab84b Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Thu, 19 Mar 2020 23:25:36 -0500 Subject: [PATCH 19/29] Clean up android and multipart post --- .../plugin/http/FormUploader.java | 51 ++--- .../com/getcapacitor/plugin/http/Http.java | 199 +++++++++--------- .../Capacitor/Plugins/Http/Http.swift | 2 +- 3 files changed, 114 insertions(+), 138 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java index 1b75990b89..3fce58f0db 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java @@ -1,18 +1,13 @@ package com.getcapacitor.plugin.http; -import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.HttpURLConnection; -import java.net.URL; import java.net.URLConnection; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; public class FormUploader { @@ -27,23 +22,18 @@ public class FormUploader { * This constructor initializes a new HTTP POST request with content type * is set to multipart/form-data * - * @param requestURL + * @param conn * @throws java.io.IOException */ - public FormUploader(String requestURL) throws IOException { - // creates a unique boundary based on time stamp + public FormUploader(HttpURLConnection conn) throws IOException { UUID uuid = UUID.randomUUID(); boundary = uuid.toString(); - URL url = new URL(requestURL); - httpConn = (HttpURLConnection) url.openConnection(); - httpConn.setUseCaches(false); - httpConn.setDoOutput(true); // indicates POST method - httpConn.setDoInput(true); - httpConn.setRequestProperty("Content-Type", - "multipart/form-data; boundary=" + boundary); + httpConn = conn; + + httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + outputStream = httpConn.getOutputStream(); - writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), - true); + writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), true); } /** @@ -53,13 +43,15 @@ public FormUploader(String requestURL) throws IOException { * @param value field value */ public void addFormField(String name, String value) { + writer.append(LINE_FEED); writer.append("--" + boundary).append(LINE_FEED); writer.append("Content-Disposition: form-data; name=\"" + name + "\"") .append(LINE_FEED); writer.append("Content-Type: text/plain; charset=" + charset).append( LINE_FEED); writer.append(LINE_FEED); - writer.append(value).append(LINE_FEED); + writer.append(value); + writer.append(LINE_FEED).append("--" + boundary + "--").append(LINE_FEED); writer.flush(); } @@ -84,13 +76,11 @@ public void addFilePart(String fieldName, File uploadFile) + URLConnection.guessContentTypeFromName(fileName)) .append(LINE_FEED) .append(LINE_FEED); - //writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED); - // writer.append(LINE_FEED); writer.flush(); FileInputStream inputStream = new FileInputStream(uploadFile); byte[] buffer = new byte[4096]; - int bytesRead = -1; + int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } @@ -118,26 +108,9 @@ public void addHeaderField(String name, String value) { * status OK, otherwise an exception is thrown. * @throws IOException */ - public List finish() throws IOException { - List response = new ArrayList<>(); + public void finish() throws IOException { writer.append(LINE_FEED).flush(); writer.append("--" + boundary + "--").append(LINE_FEED); writer.close(); - - // checks server's status code first - int status = httpConn.getResponseCode(); - if (status == HttpURLConnection.HTTP_OK) { - BufferedReader reader = new BufferedReader(new InputStreamReader( - httpConn.getInputStream())); - String line = null; - while ((line = reader.readLine()) != null) { - response.add(line); - } - reader.close(); - httpConn.disconnect(); - } else { - throw new IOException("Server returned non-OK status: " + status); - } - return response; } } diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java index 87ec56545c..590de4c998 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java @@ -75,21 +75,7 @@ private void get(PluginCall call, String urlString, String method, JSObject head URL url = new URL(urlString); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setAllowUserInteraction(false); - conn.setRequestMethod(method); - - if (connectTimeout != null) { - conn.setConnectTimeout(connectTimeout); - } - - if (readTimeout != null) { - conn.setReadTimeout(readTimeout); - } - - setRequestHeaders(conn, headers); - - conn.connect(); + HttpURLConnection conn = makeUrlConnection(url, method, connectTimeout, readTimeout, headers); buildResponse(call, conn); } catch (MalformedURLException ex) { @@ -101,58 +87,6 @@ private void get(PluginCall call, String urlString, String method, JSObject head } } - private void buildResponse(PluginCall call, HttpURLConnection conn) throws Exception { - int statusCode = conn.getResponseCode(); - - JSObject ret = new JSObject(); - ret.put("status", statusCode); - ret.put("headers", makeResponseHeaders(conn)); - - InputStream stream = conn.getInputStream(); - - BufferedReader in = new BufferedReader(new InputStreamReader(stream)); - StringBuilder builder = new StringBuilder(); - String line; - while ((line = in.readLine()) != null) { - builder.append(line); - } - in.close(); - - Log.d(getLogTag(), "GET request completed, got data"); - - String contentType = conn.getHeaderField("Content-Type"); - - if (contentType != null) { - if (contentType.contains("application/json")) { - JSObject jsonValue = new JSObject(builder.toString()); - ret.put("data", jsonValue); - } else { - ret.put("data", builder.toString()); - } - } else { - ret.put("data", builder.toString()); - } - - call.resolve(ret); - } - - private JSArray makeResponseHeaders(HttpURLConnection conn) { - JSArray ret = new JSArray(); - - for (Map.Entry> entries : conn.getHeaderFields().entrySet()) { - JSObject header = new JSObject(); - - String val = ""; - for (String headerVal : entries.getValue()) { - val += headerVal + ", "; - } - - header.put(entries.getKey(), val); - ret.put(header); - } - - return ret; - } private void mutate(PluginCall call, String urlString, String method, JSObject headers) { try { @@ -162,19 +96,9 @@ private void mutate(PluginCall call, String urlString, String method, JSObject h URL url = new URL(urlString); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setAllowUserInteraction(false); - conn.setRequestMethod(method); - - if (connectTimeout != null) { - conn.setConnectTimeout(connectTimeout); - } - - if (readTimeout != null) { - conn.setReadTimeout(readTimeout); - } + HttpURLConnection conn = makeUrlConnection(url, method, connectTimeout, readTimeout, headers); - setRequestHeaders(conn, headers); + conn.setDoOutput(true); setRequestBody(conn, data, headers); @@ -190,6 +114,26 @@ private void mutate(PluginCall call, String urlString, String method, JSObject h } } + private HttpURLConnection makeUrlConnection(URL url, String method, Integer connectTimeout, Integer readTimeout, JSObject headers) throws Exception { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + conn.setAllowUserInteraction(false); + conn.setRequestMethod(method); + + if (connectTimeout != null) { + conn.setConnectTimeout(connectTimeout); + } + + if (readTimeout != null) { + conn.setReadTimeout(readTimeout); + } + + setRequestHeaders(conn, headers); + + return conn; + } + + @SuppressWarnings("unused") @PluginMethod() public void downloadFile(PluginCall call) { try { @@ -208,23 +152,9 @@ public void downloadFile(PluginCall call) { } + File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); - File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); - - - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setAllowUserInteraction(false); - conn.setRequestMethod("GET"); - - if (connectTimeout != null) { - conn.setConnectTimeout(connectTimeout); - } - - if (readTimeout != null) { - conn.setReadTimeout(readTimeout); - } - - setRequestHeaders(conn, headers); + HttpURLConnection conn = makeUrlConnection(url, "GET", connectTimeout, readTimeout, headers); InputStream is = conn.getInputStream(); @@ -267,29 +197,38 @@ protected void handleRequestPermissionsResult(int requestCode, String[] permissi } + @SuppressWarnings("unused") @PluginMethod() public void uploadFile(PluginCall call) { - String url = call.getString("url"); + String urlString = call.getString("url"); String filePath = call.getString("filePath"); String fileDirectory = call.getString("fileDirectory", FilesystemUtils.DIRECTORY_DOCUMENTS); String name = call.getString("name", "file"); + Integer connectTimeout = call.getInt("connectTimeout"); + Integer readTimeout = call.getInt("readTimeout"); JSObject headers = call.getObject("headers"); JSObject params = call.getObject("params"); JSObject data = call.getObject("data"); try { + URL url = new URL(urlString); + File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); - FormUploader builder = new FormUploader(url); + HttpURLConnection conn = makeUrlConnection(url, "POST", connectTimeout, readTimeout, headers); + conn.setDoOutput(true); + + FormUploader builder = new FormUploader(conn); builder.addFilePart(name, file); builder.finish(); - call.resolve(); + buildResponse(call, conn); } catch (Exception ex) { call.reject("Error", ex); } } + @SuppressWarnings("unused") @PluginMethod() public void setCookie(PluginCall call) { String url = call.getString("url"); @@ -307,6 +246,7 @@ public void setCookie(PluginCall call) { call.resolve(); } + @SuppressWarnings("unused") @PluginMethod() public void getCookies(PluginCall call) { String url = call.getString("url"); @@ -333,6 +273,7 @@ public void getCookies(PluginCall call) { call.resolve(ret); } + @SuppressWarnings("unused") @PluginMethod() public void deleteCookie(PluginCall call) { String url = call.getString("url"); @@ -356,12 +297,65 @@ public void deleteCookie(PluginCall call) { call.resolve(); } + @SuppressWarnings("unused") @PluginMethod() public void clearCookies(PluginCall call) { cookieManager.getCookieStore().removeAll(); call.resolve(); } + private void buildResponse(PluginCall call, HttpURLConnection conn) throws Exception { + int statusCode = conn.getResponseCode(); + + JSObject ret = new JSObject(); + ret.put("status", statusCode); + ret.put("headers", makeResponseHeaders(conn)); + + InputStream stream = conn.getInputStream(); + + BufferedReader in = new BufferedReader(new InputStreamReader(stream)); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = in.readLine()) != null) { + builder.append(line); + } + in.close(); + + Log.d(getLogTag(), "GET request completed, got data"); + + String contentType = conn.getHeaderField("Content-Type"); + + if (contentType != null) { + if (contentType.contains("application/json")) { + JSObject jsonValue = new JSObject(builder.toString()); + ret.put("data", jsonValue); + } else { + ret.put("data", builder.toString()); + } + } else { + ret.put("data", builder.toString()); + } + + call.resolve(ret); + } + + private JSArray makeResponseHeaders(HttpURLConnection conn) { + JSArray ret = new JSArray(); + + for (Map.Entry> entries : conn.getHeaderFields().entrySet()) { + JSObject header = new JSObject(); + + String val = ""; + for (String headerVal : entries.getValue()) { + val += headerVal + ", "; + } + + header.put(entries.getKey(), val); + ret.put(header); + } + + return ret; + } private void setRequestHeaders(HttpURLConnection conn, JSObject headers) { Iterator keys = headers.keys(); @@ -402,7 +396,16 @@ private void setRequestBody(HttpURLConnection conn, JSObject data, JSObject head os.flush(); os.close(); } else if (contentType.contains("multipart/form-data")) { + FormUploader uploader = new FormUploader(conn); + + Iterator keys = data.keys(); + while (keys.hasNext()) { + String key = keys.next(); + String d = data.get(key).toString(); + uploader.addFormField(key, d); + } + uploader.finish(); } } } diff --git a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift index 0d8626a4bc..35d1908edf 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift @@ -236,7 +236,7 @@ public class CAPHttpPlugin: CAPPlugin { request.httpMethod = method setRequestHeaders(&request, headers) - + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in if error != nil { call.reject("Error", error, [:]) From 0eb6dcc78b3fd04c4d73c9cc3934ad0f2d4f76aa Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Thu, 19 Mar 2020 23:47:40 -0500 Subject: [PATCH 20/29] Properly handle android download permissions --- .../com/getcapacitor/PluginRequestCodes.java | 1 + .../com/getcapacitor/plugin/http/Http.java | 66 ++++++++++++++----- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java b/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java index f3ca5ebae3..ff85ce45c3 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java +++ b/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java @@ -23,4 +23,5 @@ public class PluginRequestCodes { public static final int FILESYSTEM_REQUEST_STAT_PERMISSIONS = 9019; public static final int FILESYSTEM_REQUEST_RENAME_PERMISSIONS = 9020; public static final int FILESYSTEM_REQUEST_COPY_PERMISSIONS = 9021; + public static final int HTTP_REQUEST_WRITE_FILE_PERMISSIONS = 9022; } diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java index 590de4c998..eb9845531f 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java @@ -1,6 +1,7 @@ package com.getcapacitor.plugin.http; import android.Manifest; +import android.content.pm.PackageManager; import android.util.Log; import com.getcapacitor.JSArray; @@ -38,7 +39,9 @@ * * Requires the android.permission.VIBRATE permission. */ -@NativePlugin() +@NativePlugin(requestCodes = { + PluginRequestCodes.HTTP_REQUEST_WRITE_FILE_PERMISSIONS, +}) public class Http extends Plugin { CookieManager cookieManager = new CookieManager(); @@ -137,6 +140,7 @@ private HttpURLConnection makeUrlConnection(URL url, String method, Integer conn @PluginMethod() public void downloadFile(PluginCall call) { try { + saveCall(call); String urlString = call.getString("url"); String filePath = call.getString("filePath"); String fileDirectory = call.getString("fileDirectory", FilesystemUtils.DIRECTORY_DOCUMENTS); @@ -148,29 +152,29 @@ public void downloadFile(PluginCall call) { URL url = new URL(urlString); if (!FilesystemUtils.isPublicDirectory(fileDirectory) - || isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_WRITE_FILE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + || isStoragePermissionGranted(PluginRequestCodes.HTTP_REQUEST_WRITE_FILE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + this.freeSavedCall(); - } - - File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); + File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); - HttpURLConnection conn = makeUrlConnection(url, "GET", connectTimeout, readTimeout, headers); + HttpURLConnection conn = makeUrlConnection(url, "GET", connectTimeout, readTimeout, headers); - InputStream is = conn.getInputStream(); + InputStream is = conn.getInputStream(); - FileOutputStream fos = new FileOutputStream(file, false); + FileOutputStream fos = new FileOutputStream(file, false); - byte[] buffer = new byte[1024]; - int len; + byte[] buffer = new byte[1024]; + int len; - while ((len = is.read(buffer)) > 0) { - fos.write(buffer, 0, len); - } + while ((len = is.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } - is.close(); - fos.close(); + is.close(); + fos.close(); - call.resolve(); + call.resolve(); + } } catch (MalformedURLException ex) { call.reject("Invalid URL", ex); } catch (IOException ex) { @@ -194,6 +198,36 @@ private boolean isStoragePermissionGranted(int permissionRequestCode, String per @Override protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.handleRequestPermissionsResult(requestCode, permissions, grantResults); + + if (getSavedCall() == null) { + Log.d(getLogTag(),"No stored plugin call for permissions request result"); + return; + } + + PluginCall savedCall = getSavedCall(); + + for (int i = 0; i < grantResults.length; i++) { + int result = grantResults[i]; + String perm = permissions[i]; + if(result == PackageManager.PERMISSION_DENIED) { + Log.d(getLogTag(), "User denied storage permission: " + perm); + savedCall.error("User denied write permission needed to save files"); + this.freeSavedCall(); + return; + } + } + + this.freeSavedCall(); + + final Http httpPlugin = this; + bridge.execute(new Runnable() { + @Override + public void run() { + if (requestCode == PluginRequestCodes.HTTP_REQUEST_WRITE_FILE_PERMISSIONS) { + httpPlugin.downloadFile(savedCall); + } + } + }); } From 965700e9ee5434169d8bf67c0b0c092a0cf5d81d Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Fri, 20 Mar 2020 00:01:57 -0500 Subject: [PATCH 21/29] Manage perms for uploading files --- .../java/com/getcapacitor/PluginCall.java | 4 ++- .../com/getcapacitor/PluginRequestCodes.java | 3 +- .../com/getcapacitor/plugin/http/Http.java | 29 ++++++++++++------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java b/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java index 1601fd3234..d46a35e0cc 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java +++ b/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java @@ -92,7 +92,9 @@ public void error(String msg, Exception ex, JSObject data) { try { errorResult.put("message", msg); - errorResult.put("platformMessage", ex.getMessage()); + if (ex != null) { + errorResult.put("platformMessage", ex.getMessage()); + } } catch (Exception jsonEx) { Log.e(LogUtils.getPluginTag(), jsonEx.getMessage()); } diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java b/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java index ff85ce45c3..b0999e99c7 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java +++ b/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java @@ -23,5 +23,6 @@ public class PluginRequestCodes { public static final int FILESYSTEM_REQUEST_STAT_PERMISSIONS = 9019; public static final int FILESYSTEM_REQUEST_RENAME_PERMISSIONS = 9020; public static final int FILESYSTEM_REQUEST_COPY_PERMISSIONS = 9021; - public static final int HTTP_REQUEST_WRITE_FILE_PERMISSIONS = 9022; + public static final int HTTP_REQUEST_DOWNLOAD_WRITE_PERMISSIONS = 9022; + public static final int HTTP_REQUEST_UPLOAD_READ_PERMISSIONS = 9023; } diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java index eb9845531f..d0aa19fd92 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java @@ -40,7 +40,8 @@ * Requires the android.permission.VIBRATE permission. */ @NativePlugin(requestCodes = { - PluginRequestCodes.HTTP_REQUEST_WRITE_FILE_PERMISSIONS, + PluginRequestCodes.HTTP_REQUEST_DOWNLOAD_WRITE_PERMISSIONS, + PluginRequestCodes.HTTP_REQUEST_UPLOAD_READ_PERMISSIONS, }) public class Http extends Plugin { CookieManager cookieManager = new CookieManager(); @@ -152,7 +153,7 @@ public void downloadFile(PluginCall call) { URL url = new URL(urlString); if (!FilesystemUtils.isPublicDirectory(fileDirectory) - || isStoragePermissionGranted(PluginRequestCodes.HTTP_REQUEST_WRITE_FILE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + || isStoragePermissionGranted(PluginRequestCodes.HTTP_REQUEST_DOWNLOAD_WRITE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { this.freeSavedCall(); File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); @@ -219,12 +220,15 @@ protected void handleRequestPermissionsResult(int requestCode, String[] permissi this.freeSavedCall(); + // Run on background thread to avoid main-thread network requests final Http httpPlugin = this; bridge.execute(new Runnable() { @Override public void run() { - if (requestCode == PluginRequestCodes.HTTP_REQUEST_WRITE_FILE_PERMISSIONS) { + if (requestCode == PluginRequestCodes.HTTP_REQUEST_DOWNLOAD_WRITE_PERMISSIONS) { httpPlugin.downloadFile(savedCall); + } else if (requestCode == PluginRequestCodes.HTTP_REQUEST_UPLOAD_READ_PERMISSIONS) { + httpPlugin.uploadFile(savedCall); } } }); @@ -245,18 +249,23 @@ public void uploadFile(PluginCall call) { JSObject data = call.getObject("data"); try { + saveCall(call); URL url = new URL(urlString); - File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); + if (!FilesystemUtils.isPublicDirectory(fileDirectory) + || isStoragePermissionGranted(PluginRequestCodes.HTTP_REQUEST_UPLOAD_READ_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + this.freeSavedCall(); + File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory); - HttpURLConnection conn = makeUrlConnection(url, "POST", connectTimeout, readTimeout, headers); - conn.setDoOutput(true); + HttpURLConnection conn = makeUrlConnection(url, "POST", connectTimeout, readTimeout, headers); + conn.setDoOutput(true); - FormUploader builder = new FormUploader(conn); - builder.addFilePart(name, file); - builder.finish(); + FormUploader builder = new FormUploader(conn); + builder.addFilePart(name, file); + builder.finish(); - buildResponse(call, conn); + buildResponse(call, conn); + } } catch (Exception ex) { call.reject("Error", ex); } From 7afc93da4a9d620d2e4d7888c0c4579143c0e0b4 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Fri, 20 Mar 2020 10:33:23 -0500 Subject: [PATCH 22/29] Cookies are set --- example/src/pages/http/http.html | 2 ++ example/src/pages/http/http.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html index b4ebec38fb..4497573887 100644 --- a/example/src/pages/http/http.html +++ b/example/src/pages/http/http.html @@ -23,6 +23,8 @@ + + diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index addc023a91..463552caf5 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -98,6 +98,8 @@ export class HttpPage { apiUrl = (path: string) => `${this.serverUrl}${path}`; + testSetCookies = () => this.get('/set-cookies'); + formPost = async () => { this.output = ''; this.loading = this.loadingCtrl.create({ From 961709a0b67acb3eb2cfad7296f78b924bfbc5c5 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Fri, 20 Mar 2020 13:26:12 -0500 Subject: [PATCH 23/29] Fix directory support for iOS downloads --- .../Plugins/Filesystem/FilesystemUtils.swift | 12 ++++++++++++ ios/Capacitor/Capacitor/Plugins/Http/Http.swift | 16 +++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift b/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift index 564589d017..33fffee71f 100644 --- a/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift +++ b/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift @@ -20,6 +20,8 @@ class FilesystemUtils { return .applicationDirectory case "CACHE": return .cachesDirectory + case "DOWNLOADS": + return .downloadsDirectory default: return .documentDirectory } @@ -43,6 +45,16 @@ class FilesystemUtils { return dir.appendingPathComponent(path) } + static func createDirectoryForFile(_ fileUrl: URL, _ recursive: Bool) throws { + if !FileManager.default.fileExists(atPath: fileUrl.deletingLastPathComponent().absoluteString) { + if recursive { + try FileManager.default.createDirectory(at: fileUrl.deletingLastPathComponent(), withIntermediateDirectories: recursive, attributes: nil) + } else { + throw FilesystemError.parentFolderNotExists("Parent folder doesn't exist") + } + } + } + /** * Read a file as a string at the given directory and with the given encoding */ diff --git a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift index 35d1908edf..4b2c130447 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift @@ -39,7 +39,8 @@ public class CAPHttpPlugin: CAPPlugin { guard let filePath = call.getString("filePath") else { return call.reject("Must provide a file path to download the file to") } - //let fileDirectory = call.getString("filePath") ?? "DOCUMENTS" + + let fileDirectory = call.getString("fileDirectory") ?? "DOCUMENTS" guard let url = URL(string: urlValue) else { return call.reject("Invalid URL") @@ -58,17 +59,18 @@ public class CAPHttpPlugin: CAPPlugin { return } - let res = response as! HTTPURLResponse - - let basename = location.lastPathComponent - // TODO: Move to abstracted FS operations let fileManager = FileManager.default - let dir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first + + let foundDir = FilesystemUtils.getDirectory(directory: fileDirectory) + let dir = fileManager.urls(for: foundDir, in: .userDomainMask).first do { - let dest = dir!.appendingPathComponent(basename) + let dest = dir!.appendingPathComponent(filePath) print("File Dest", dest.absoluteString) + + try FilesystemUtils.createDirectoryForFile(dest, true) + try fileManager.moveItem(at: location, to: dest) call.resolve([ "path": dest.absoluteString From 7de917340e6b349985f8f795ed28a3387858436f Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Fri, 20 Mar 2020 15:17:45 -0500 Subject: [PATCH 24/29] Download location on Android --- .../src/main/java/com/getcapacitor/plugin/http/Http.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java index d0aa19fd92..fb97d2e427 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java @@ -174,7 +174,9 @@ public void downloadFile(PluginCall call) { is.close(); fos.close(); - call.resolve(); + call.resolve(new JSObject() {{ + put("path", file.getAbsolutePath()); + }}); } } catch (MalformedURLException ex) { call.reject("Invalid URL", ex); From 63128f2d4dbf36d24e2bea2ecb344f3734df3813 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Fri, 20 Mar 2020 17:03:10 -0500 Subject: [PATCH 25/29] Starting web implementation --- core/src/plugins/http.ts | 4 ++++ core/src/web-plugins.ts | 1 + core/src/web/http.ts | 0 ios/Capacitor/Capacitor/Plugins/Http/Http.swift | 1 - 4 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 core/src/web/http.ts diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts index 88d82acba5..6b0912c7ab 100644 --- a/core/src/plugins/http.ts +++ b/core/src/plugins/http.ts @@ -26,6 +26,10 @@ export interface HttpOptions { * How long to wait for the initial connection. */ connectTimeout?: number; + /** + * Extra arguments for fetch when running on the web + */ + webFetchExtra?: RequestInit; } export interface HttpParams { diff --git a/core/src/web-plugins.ts b/core/src/web-plugins.ts index 34d7e81d31..314b2b790f 100644 --- a/core/src/web-plugins.ts +++ b/core/src/web-plugins.ts @@ -9,6 +9,7 @@ export * from './web/clipboard'; export * from './web/filesystem'; export * from './web/geolocation'; export * from './web/device'; +export * from './web/http'; export * from './web/local-notifications'; export * from './web/share'; export * from './web/modals'; diff --git a/core/src/web/http.ts b/core/src/web/http.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift index 4b2c130447..6d0d3eda2b 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift @@ -46,7 +46,6 @@ public class CAPHttpPlugin: CAPPlugin { return call.reject("Invalid URL") } - let task = URLSession.shared.downloadTask(with: url) { (downloadLocation, response, error) in if error != nil { CAPLog.print("Error on download file", downloadLocation, response, error) From 84b0ad0e411928d14d3571c9c79473c2618c097c Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Thu, 2 Apr 2020 17:22:13 -0500 Subject: [PATCH 26/29] Add cookies support to web --- core/src/plugins/http.ts | 5 +- core/src/web/http.ts | 138 ++++++++++++++++++++++++++++++++++++++ example/package-lock.json | 9 +++ example/package.json | 1 + 4 files changed, 151 insertions(+), 2 deletions(-) diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts index 6b0912c7ab..258d44e9f2 100644 --- a/core/src/plugins/http.ts +++ b/core/src/plugins/http.ts @@ -33,11 +33,11 @@ export interface HttpOptions { } export interface HttpParams { - [key:string]: string; + [key: string]: string; } export interface HttpHeaders { - [key:string]: string; + [key: string]: string; } export interface HttpResponse { @@ -89,6 +89,7 @@ export interface HttpSetCookieOptions { url: string; key: string; value: string; + ageDays?: number; } export interface HttpGetCookiesOptions { diff --git a/core/src/web/http.ts b/core/src/web/http.ts index e69de29bb2..23fc9969d3 100644 --- a/core/src/web/http.ts +++ b/core/src/web/http.ts @@ -0,0 +1,138 @@ +import { WebPlugin } from './index'; + +import { + HttpPlugin, + HttpOptions, + //HttpCookie, + HttpDeleteCookieOptions, + HttpHeaders, + HttpResponse, + HttpSetCookieOptions, + HttpClearCookiesOptions, + HttpGetCookiesOptions, + HttpGetCookiesResult, + //HttpParams, + HttpDownloadFileOptions, + HttpDownloadFileResult, + HttpUploadFileOptions, + HttpUploadFileResult +} from '../core-plugin-definitions'; + +export class HttpPluginWeb extends WebPlugin implements HttpPlugin { + constructor() { + super({ + name: 'Http', + platforms: ['web', 'electron'] + }); + } + + private getRequestHeader(headers: HttpHeaders, key: string): string { + const originalKeys = Object.keys(headers); + const keys = Object.keys(headers).map(k => k.toLocaleLowerCase()); + const lowered = keys.reduce((newHeaders, key, index) => { + newHeaders[key] = headers[originalKeys[index]]; + return newHeaders; + }, {} as HttpHeaders); + + return lowered[key.toLocaleLowerCase()]; + } + + private nativeHeadersToObject(headers: Headers): HttpHeaders { + const h = {} as HttpHeaders; + + headers.forEach((value: string, key: string) => { + h[key] = value; + }); + + return h; + } + + private makeFetchOptions(options: HttpOptions, fetchExtra: RequestInit): RequestInit { + const req = { + method: options.method || 'GET', + headers: options.headers, + ...(fetchExtra || {}) + } as RequestInit; + + const contentType = this.getRequestHeader(options.headers || {}, 'content-type') || ''; + + if (contentType.indexOf('application/json') === 0) { + req['body'] = JSON.stringify(options.data); + } else if (contentType.indexOf('application/x-www-form-urlencoded') === 0) { + } else if (contentType.indexOf('multipart/form-data') === 0) { + } + + return req; + } + + async request(options: HttpOptions): Promise { + const fetchOptions = this.makeFetchOptions(options, options.webFetchExtra); + + const ret = await fetch(options.url, fetchOptions); + + const contentType = ret.headers.get('content-type'); + + let data; + if (contentType && contentType.indexOf('application/json') === 0) { + data = await ret.json(); + } else { + data = await ret.text(); + } + + return { + status: ret.status, + data, + headers: this.nativeHeadersToObject(ret.headers) + } + } + + async setCookie(options: HttpSetCookieOptions) { + var expires = ""; + if (options.ageDays) { + const date = new Date(); + date.setTime(date.getTime() + (options.ageDays * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = options.key + "=" + (options.value || "") + expires + "; path=/"; + } + + async getCookies(_options: HttpGetCookiesOptions): Promise { + // const url = options.url; + var cookies = document.cookie.split(';'); + console.log('Got cookies', cookies); + return { + value: cookies.map(c => { + const cParts = c.split(';').map(cv => cv.trim()); + const cNameValue = cParts[0]; + const cValueParts = cNameValue.split('='); + const key = cValueParts[0]; + const value = cValueParts[1]; + + return { + key, + value + } + }) + } + } + + deleteCookie(_options: HttpDeleteCookieOptions): Promise { + throw new Error("Method not implemented."); + } + + clearCookies(_options: HttpClearCookiesOptions): Promise { + throw new Error("Method not implemented."); + } + + uploadFile(_options: HttpUploadFileOptions): Promise { + throw new Error("Method not implemented."); + } + + downloadFile(_options: HttpDownloadFileOptions): Promise { + throw new Error("Method not implemented."); + } +} + +const Http = new HttpPluginWeb(); + +export { Http }; diff --git a/example/package-lock.json b/example/package-lock.json index 9a4322db97..5fac7eef69 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -1470,6 +1470,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", diff --git a/example/package.json b/example/package.json index 49ec5d3118..55446a0c41 100644 --- a/example/package.json +++ b/example/package.json @@ -27,6 +27,7 @@ "@angular/platform-browser-dynamic": "5.0.1", "body-parser": "^1.19.0", "cookie-parser": "^1.4.4", + "cors": "^2.8.5", "express": "^4.17.1", "ionic-angular": "^3.9.6", "ionicons": "3.0.0", From 6e7bb495c955cdcd9006d4b1419589329adfc7e2 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Thu, 2 Apr 2020 17:30:16 -0500 Subject: [PATCH 27/29] Set and remove cookies --- core/src/web/http.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/core/src/web/http.ts b/core/src/web/http.ts index 23fc9969d3..e86c41d75e 100644 --- a/core/src/web/http.ts +++ b/core/src/web/http.ts @@ -97,9 +97,11 @@ export class HttpPluginWeb extends WebPlugin implements HttpPlugin { } async getCookies(_options: HttpGetCookiesOptions): Promise { - // const url = options.url; + if (!document.cookie) { + return { value: [] } + } + var cookies = document.cookie.split(';'); - console.log('Got cookies', cookies); return { value: cookies.map(c => { const cParts = c.split(';').map(cv => cv.trim()); @@ -116,12 +118,16 @@ export class HttpPluginWeb extends WebPlugin implements HttpPlugin { } } - deleteCookie(_options: HttpDeleteCookieOptions): Promise { - throw new Error("Method not implemented."); + async deleteCookie(options: HttpDeleteCookieOptions) { + document.cookie = options.key + '=; Max-Age=0' } - clearCookies(_options: HttpClearCookiesOptions): Promise { - throw new Error("Method not implemented."); + async clearCookies(_options: HttpClearCookiesOptions) { + document.cookie + .split(";") + .forEach(c => + document.cookie = c.replace(/^ +/, '') + .replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)); } uploadFile(_options: HttpUploadFileOptions): Promise { From 7afcde6e0ad0d6e962d4fdcb9092081940a8e56b Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Fri, 3 Apr 2020 15:51:05 -0500 Subject: [PATCH 28/29] Web api for downloads and uploads --- core/src/plugins/http.ts | 11 +++++--- core/src/web/http.ts | 27 ++++++++++++++++--- example/src/pages/http/http.ts | 23 +++++++++------- .../Capacitor/Plugins/Http/Http.swift | 3 ++- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts index 258d44e9f2..d179b40771 100644 --- a/core/src/plugins/http.ts +++ b/core/src/plugins/http.ts @@ -69,9 +69,13 @@ export interface HttpUploadFileOptions extends HttpOptions { */ name: string; /** - * The path to the file on disk to upload + * For uploading a file on the web, a JavaScript Blob to upload */ - filePath: string; + blob?: Blob; + /** + * For uploading a file natively, the path to the file on disk to upload + */ + filePath?: string; /** * Optionally, the directory to look for the file in. * @@ -110,7 +114,8 @@ export interface HttpGetCookiesResult { } export interface HttpDownloadFileResult { - path: string; + path?: string; + blob?: Blob; } export interface HttpUploadFileResult { diff --git a/core/src/web/http.ts b/core/src/web/http.ts index e86c41d75e..273c92e98e 100644 --- a/core/src/web/http.ts +++ b/core/src/web/http.ts @@ -130,12 +130,31 @@ export class HttpPluginWeb extends WebPlugin implements HttpPlugin { .replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)); } - uploadFile(_options: HttpUploadFileOptions): Promise { - throw new Error("Method not implemented."); + async uploadFile(options: HttpUploadFileOptions): Promise { + const fetchOptions = this.makeFetchOptions(options, options.webFetchExtra); + + const formData = new FormData(); + formData.append(options.name, options.blob); + + await fetch(options.url, { + ...fetchOptions, + body: formData, + method: 'POST' + }); + + return {}; } - downloadFile(_options: HttpDownloadFileOptions): Promise { - throw new Error("Method not implemented."); + async downloadFile(options: HttpDownloadFileOptions): Promise { + const fetchOptions = this.makeFetchOptions(options, options.webFetchExtra); + + const ret = await fetch(options.url, fetchOptions); + + const blob = await ret.blob(); + + return { + blob + } } } diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts index 463552caf5..46e4ee902b 100644 --- a/example/src/pages/http/http.ts +++ b/example/src/pages/http/http.ts @@ -64,11 +64,11 @@ export class HttpPage { getJson = () => this.get('/get-json'); getHtml = () => this.get('/get-html'); - head = () => this.get('/head', 'HEAD'); - delete = () => this.mutate('/delete', 'DELETE', { title: 'foo', body: 'bar', userId: 1 }); - patch = () => this.mutate('/patch', 'PATCH', { title: 'foo', body: 'bar', userId: 1 }); - post = () => this.mutate('/post', 'POST', { title: 'foo', body: 'bar', userId: 1 }); - put = () => this.mutate('/put', 'PUT', { title: 'foo', body: 'bar', userId: 1 }); + head = () => this.get('/head', 'HEAD'); + delete = () => this.mutate('/delete', 'DELETE', { title: 'foo', body: 'bar', userId: 1 }); + patch = () => this.mutate('/patch', 'PATCH', { title: 'foo', body: 'bar', userId: 1 }); + post = () => this.mutate('/post', 'POST', { title: 'foo', body: 'bar', userId: 1 }); + put = () => this.mutate('/put', 'PUT', { title: 'foo', body: 'bar', userId: 1 }); async mutate(path, method, data = {}) { this.output = ''; @@ -218,6 +218,7 @@ export class HttpPage { console.log('Got download ret', ret); + /* const renameRet = await Filesystem.rename({ from: ret.path, @@ -228,12 +229,14 @@ export class HttpPage { console.log('Did rename', renameRet); */ - const read = await Filesystem.readFile({ - path: 'document.pdf', - directory: FilesystemDirectory.Downloads - }); + if (ret.path) { + const read = await Filesystem.readFile({ + path: 'document.pdf', + directory: FilesystemDirectory.Downloads + }); - console.log('Read', read); + console.log('Read', read); + } } uploadFile = async () => { diff --git a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift index 6d0d3eda2b..e9c0891065 100644 --- a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift +++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift @@ -75,7 +75,8 @@ public class CAPHttpPlugin: CAPPlugin { "path": dest.absoluteString ]) } catch let e { - CAPLog.print("Unable to download file", e) + call.reject("Unable to download file", e) + return } From ae81f32e41e77cd93e7d2a8015fb6478ba512299 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Tue, 14 Apr 2020 23:24:09 -0500 Subject: [PATCH 29/29] Missing server.js --- example/server/server.js | 127 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 example/server/server.js diff --git a/example/server/server.js b/example/server/server.js new file mode 100644 index 0000000000..fef0fd56e2 --- /dev/null +++ b/example/server/server.js @@ -0,0 +1,127 @@ +var path = require('path'), + express = require('express'), + bodyParser = require('body-parser'), + cors = require('cors'), + cookieParser = require('cookie-parser'), + multer = require('multer'), + upload = multer({ dest: 'uploads/' }) + + +var fs = require('fs'); + +var app = express(); + +var staticPath = path.join(__dirname, '/public'); +app.use(express.static(staticPath)); + +app.use(cors({ origin: true })); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(cookieParser()); + +app.listen(3455, function() { + console.log('listening'); + +}); + +app.get('/get', (req, res) => { + const headers = req.headers; + const params = req.query; + console.log('Got headers', headers); + console.log('Got params', params); + console.log(req.url); + res.status(200); + res.send(); +}); + +app.get('/get-json', (req, res) => { + res.status(200); + res.json({ + name: 'Max', + superpower: 'Being Awesome' + }) +}); + +app.get('/get-html', (req, res) => { + res.status(200); + res.header('Content-Type', 'text/html'); + res.send('

Hi

'); +}); + +app.get('/head', (req, res) => { + const headers = req.headers; + console.log('HEAD'); + console.log('Got headers', headers); + res.status(200); + res.send(); +}); + +app.delete('/delete', (req, res) => { + const headers = req.headers; + console.log('DELETE'); + console.log('Got headers', headers); + res.status(200); + res.send(); +}); +app.patch('/patch', (req, res) => { + const headers = req.headers; + console.log('PATCH'); + console.log('Got headers', headers); + res.status(200); + res.send(); +}); +app.post('/post', (req, res) => { + const headers = req.headers; + console.log('POST'); + console.log('Got headers', headers); + res.status(200); + res.send(); +}); +app.put('/put', (req, res) => { + const headers = req.headers; + console.log('PUT'); + console.log('Got headers', headers); + res.status(200); + res.send(); +}); + +app.get('/cookie', (req, res) => { + console.log('COOKIE', req.cookies); + res.status(200); + res.send(); +}); + +app.get('/download-pdf', (req, res) => { + console.log('Sending PDF to request', +new Date); + res.download('document.pdf'); +}); + +app.get('/set-cookies', (req, res) => { + res.cookie('style', 'very cool'); + res.send(); +}); + +app.post('/upload-pdf', upload.single('myFile'), (req, res) => { + console.log('Handling upload'); + const file = req.file; + console.log('Got file', file); + + res.status(200); + res.send(); +}); + +app.post('/form-data', (req, res) => { + console.log('Got form data post', req.body); + + res.status(200); + res.send(); +}) + +app.post('/form-data-multi', upload.any(), (req, res) => { + console.log('Got form data multipart post', req.body); + + console.log(req.files); + + res.status(200); + res.send(); +}) \ No newline at end of file