Skip to content

Commit

Permalink
feat: Capacitor Cookies & Capacitor Http core plugins
Browse files Browse the repository at this point in the history
* feat: capacitor core http initial implementation

* chore: run build and run fmt

* feat: merge cookies and http

* chore: allow branch for ci dev release

* chore: allow dev release

* feat: add support for disabling http via config

* fix: angular zone.js race condition

* fix: default http method to GET

* fix: default cap http to opt-in

* chore: run fmt

* fix: get response headers for XHR

* chore(ios): swiftlint fixes

* feat: add opt-in config for cookies

* fix(ci): verify tests

* Update declarations.ts
  • Loading branch information
ItsChaceD committed Sep 21, 2022
1 parent 4706f83 commit d4047cf
Show file tree
Hide file tree
Showing 24 changed files with 3,119 additions and 4 deletions.
270 changes: 269 additions & 1 deletion android/capacitor/src/main/assets/native-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/*! Capacitor: https://capacitorjs.com/ - MIT License */
/* Generated File. Do not edit. */

var nativeBridge = (function (exports) {
const nativeBridge = (function (exports) {
'use strict';

var ExceptionCode;
Expand Down Expand Up @@ -268,6 +268,274 @@ var nativeBridge = (function (exports) {
}
return String(msg);
};
/**
* Safely web decode a string value (inspired by js-cookie)
* @param str The string value to decode
*/
const decode = (str) => str.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent);
const platform = getPlatformId(win);
if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =
Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') ||
Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie');
let doPatchCookies = false;
// check if capacitor cookies is disabled before patching
if (platform === 'ios') {
// Use prompt to synchronously get capacitor cookies config.
// https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
const payload = {
type: 'CapacitorCookies.isEnabled',
};
const isCookiesEnabled = prompt(JSON.stringify(payload));
if (isCookiesEnabled === 'true') {
doPatchCookies = true;
}
}
else if (typeof win.CapacitorCookiesAndroidInterface !== 'undefined') {
const isCookiesEnabled = win.CapacitorCookiesAndroidInterface.isEnabled();
if (isCookiesEnabled === true) {
doPatchCookies = true;
}
}
if (doPatchCookies) {
Object.defineProperty(document, 'cookie', {
get: function () {
if (platform === 'ios') {
// Use prompt to synchronously get cookies.
// https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
const payload = {
type: 'CapacitorCookies',
};
const res = prompt(JSON.stringify(payload));
return res;
}
else if (typeof win.CapacitorCookiesAndroidInterface !== 'undefined') {
return win.CapacitorCookiesAndroidInterface.getCookies();
}
},
set: function (val) {
const cookiePairs = val.split(';');
for (const cookiePair of cookiePairs) {
const cookieKey = cookiePair.split('=')[0];
const cookieValue = cookiePair.split('=')[1];
if (null == cookieValue) {
continue;
}
cap.toNative('CapacitorCookies', 'setCookie', {
key: cookieKey,
value: decode(cookieValue),
});
}
},
});
}
// patch fetch / XHR on Android/iOS
// store original fetch & XHR functions
win.CapacitorWebFetch = window.fetch;
win.CapacitorWebXMLHttpRequest = {
abort: window.XMLHttpRequest.prototype.abort,
open: window.XMLHttpRequest.prototype.open,
send: window.XMLHttpRequest.prototype.send,
setRequestHeader: window.XMLHttpRequest.prototype.setRequestHeader,
};
let doPatchHttp = false;
// check if capacitor http is disabled before patching
if (platform === 'ios') {
// Use prompt to synchronously get capacitor http config.
// https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
const payload = {
type: 'CapacitorHttp',
};
const isHttpEnabled = prompt(JSON.stringify(payload));
if (isHttpEnabled === 'true') {
doPatchHttp = true;
}
}
else if (typeof win.CapacitorHttpAndroidInterface !== 'undefined') {
const isHttpEnabled = win.CapacitorHttpAndroidInterface.isEnabled();
if (isHttpEnabled === true) {
doPatchHttp = true;
}
}
if (doPatchHttp) {
// fetch patch
window.fetch = async (resource, options) => {
if (resource.toString().startsWith('data:') ||
resource.toString().startsWith('blob:')) {
return win.CapacitorWebFetch(resource, options);
}
try {
// intercept request & pass to the bridge
const nativeResponse = await cap.nativePromise('CapacitorHttp', 'request', {
url: resource,
method: (options === null || options === void 0 ? void 0 : options.method) ? options.method : undefined,
data: (options === null || options === void 0 ? void 0 : options.body) ? options.body : undefined,
headers: (options === null || options === void 0 ? void 0 : options.headers) ? options.headers : undefined,
});
const data = typeof nativeResponse.data === 'string'
? nativeResponse.data
: JSON.stringify(nativeResponse.data);
// intercept & parse response before returning
const response = new Response(data, {
headers: nativeResponse.headers,
status: nativeResponse.status,
});
return response;
}
catch (error) {
return Promise.reject(error);
}
};
// XHR event listeners
const addEventListeners = function () {
this.addEventListener('abort', function () {
if (typeof this.onabort === 'function')
this.onabort();
});
this.addEventListener('error', function () {
if (typeof this.onerror === 'function')
this.onerror();
});
this.addEventListener('load', function () {
if (typeof this.onload === 'function')
this.onload();
});
this.addEventListener('loadend', function () {
if (typeof this.onloadend === 'function')
this.onloadend();
});
this.addEventListener('loadstart', function () {
if (typeof this.onloadstart === 'function')
this.onloadstart();
});
this.addEventListener('readystatechange', function () {
if (typeof this.onreadystatechange === 'function')
this.onreadystatechange();
});
this.addEventListener('timeout', function () {
if (typeof this.ontimeout === 'function')
this.ontimeout();
});
};
// XHR patch abort
window.XMLHttpRequest.prototype.abort = function () {
this.readyState = 0;
this.dispatchEvent(new Event('abort'));
this.dispatchEvent(new Event('loadend'));
};
// XHR patch open
window.XMLHttpRequest.prototype.open = function (method, url) {
Object.defineProperties(this, {
_headers: {
value: {},
writable: true,
},
readyState: {
get: function () {
var _a;
return (_a = this._readyState) !== null && _a !== void 0 ? _a : 0;
},
set: function (val) {
this._readyState = val;
this.dispatchEvent(new Event('readystatechange'));
},
},
response: {
value: '',
writable: true,
},
responseText: {
value: '',
writable: true,
},
responseURL: {
value: '',
writable: true,
},
status: {
value: 0,
writable: true,
},
});
addEventListeners.call(this);
this._method = method;
this._url = url;
this.readyState = 1;
};
// XHR patch set request header
window.XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
this._headers[header] = value;
};
// XHR patch send
window.XMLHttpRequest.prototype.send = function (body) {
try {
this.readyState = 2;
// intercept request & pass to the bridge
cap
.nativePromise('CapacitorHttp', 'request', {
url: this._url,
method: this._method,
data: body !== null ? body : undefined,
headers: this._headers,
})
.then((nativeResponse) => {
// intercept & parse response before returning
if (this.readyState == 2) {
this.dispatchEvent(new Event('loadstart'));
this._headers = nativeResponse.headers;
this.status = nativeResponse.status;
this.response = nativeResponse.data;
this.responseText =
typeof nativeResponse.data === 'string'
? nativeResponse.data
: JSON.stringify(nativeResponse.data);
this.responseURL = nativeResponse.url;
this.readyState = 4;
this.dispatchEvent(new Event('load'));
this.dispatchEvent(new Event('loadend'));
}
})
.catch((error) => {
this.dispatchEvent(new Event('loadstart'));
this.status = error.status;
this._headers = error.headers;
this.response = error.data;
this.responseText = JSON.stringify(error.data);
this.responseURL = error.url;
this.readyState = 4;
this.dispatchEvent(new Event('error'));
this.dispatchEvent(new Event('loadend'));
});
}
catch (error) {
this.dispatchEvent(new Event('loadstart'));
this.status = 500;
this._headers = {};
this.response = error;
this.responseText = error.toString();
this.responseURL = this._url;
this.readyState = 4;
this.dispatchEvent(new Event('error'));
this.dispatchEvent(new Event('loadend'));
}
};
// XHR patch getAllResponseHeaders
window.XMLHttpRequest.prototype.getAllResponseHeaders = function () {
let returnString = '';
for (const key in this._headers) {
if (key != 'Set-Cookie') {
returnString += key + ': ' + this._headers[key] + '\r\n';
}
}
return returnString;
};
// XHR patch getResponseHeader
window.XMLHttpRequest.prototype.getResponseHeader = function (name) {
return this._headers[name];
};
}
}
// patch window.console on iOS and store original console fns
const isIos = getPlatformId(win) === 'ios';
if (win.console && isIos) {
Expand Down
2 changes: 2 additions & 0 deletions android/capacitor/src/main/java/com/getcapacitor/Bridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,9 @@ private void initWebView() {
* Register our core Plugin APIs
*/
private void registerAllPlugins() {
this.registerPlugin(com.getcapacitor.plugin.CapacitorCookies.class);
this.registerPlugin(com.getcapacitor.plugin.WebView.class);
this.registerPlugin(com.getcapacitor.plugin.CapacitorHttp.class);

for (Class<? extends Plugin> pluginClass : this.initialPlugins) {
this.registerPlugin(pluginClass);
Expand Down
65 changes: 65 additions & 0 deletions android/capacitor/src/main/java/com/getcapacitor/JSValue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.getcapacitor;

import org.json.JSONException;

/**
* Represents a single user-data value of any type on the capacitor PluginCall object.
*/
public class JSValue {

private final Object value;

/**
* @param call The capacitor plugin call, used for accessing the value safely.
* @param name The name of the property to access.
*/
public JSValue(PluginCall call, String name) {
this.value = this.toValue(call, name);
}

/**
* Returns the coerced but uncasted underlying value.
*/
public Object getValue() {
return this.value;
}

@Override
public String toString() {
return this.getValue().toString();
}

/**
* Returns the underlying value as a JSObject, or throwing if it cannot.
*
* @throws JSONException If the underlying value is not a JSObject.
*/
public JSObject toJSObject() throws JSONException {
if (this.value instanceof JSObject) return (JSObject) this.value;
throw new JSONException("JSValue could not be coerced to JSObject.");
}

/**
* Returns the underlying value as a JSArray, or throwing if it cannot.
*
* @throws JSONException If the underlying value is not a JSArray.
*/
public JSArray toJSArray() throws JSONException {
if (this.value instanceof JSArray) return (JSArray) this.value;
throw new JSONException("JSValue could not be coerced to JSArray.");
}

/**
* Returns the underlying value this object represents, coercing it into a capacitor-friendly object if supported.
*/
private Object toValue(PluginCall call, String name) {
Object value = null;
value = call.getArray(name, null);
if (value != null) return value;
value = call.getObject(name, null);
if (value != null) return value;
value = call.getString(name, null);
if (value != null) return value;
return call.getData().opt(name);
}
}
Loading

0 comments on commit d4047cf

Please sign in to comment.