Skip to content

Commit d4047cf

Browse files
authored
feat: Capacitor Cookies & Capacitor Http core plugins
* 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
1 parent 4706f83 commit d4047cf

24 files changed

+3119
-4
lines changed

android/capacitor/src/main/assets/native-bridge.js

Lines changed: 269 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/*! Capacitor: https://capacitorjs.com/ - MIT License */
33
/* Generated File. Do not edit. */
44

5-
var nativeBridge = (function (exports) {
5+
const nativeBridge = (function (exports) {
66
'use strict';
77

88
var ExceptionCode;
@@ -268,6 +268,274 @@ var nativeBridge = (function (exports) {
268268
}
269269
return String(msg);
270270
};
271+
/**
272+
* Safely web decode a string value (inspired by js-cookie)
273+
* @param str The string value to decode
274+
*/
275+
const decode = (str) => str.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent);
276+
const platform = getPlatformId(win);
277+
if (platform == 'android' || platform == 'ios') {
278+
// patch document.cookie on Android/iOS
279+
win.CapacitorCookiesDescriptor =
280+
Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') ||
281+
Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie');
282+
let doPatchCookies = false;
283+
// check if capacitor cookies is disabled before patching
284+
if (platform === 'ios') {
285+
// Use prompt to synchronously get capacitor cookies config.
286+
// https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
287+
const payload = {
288+
type: 'CapacitorCookies.isEnabled',
289+
};
290+
const isCookiesEnabled = prompt(JSON.stringify(payload));
291+
if (isCookiesEnabled === 'true') {
292+
doPatchCookies = true;
293+
}
294+
}
295+
else if (typeof win.CapacitorCookiesAndroidInterface !== 'undefined') {
296+
const isCookiesEnabled = win.CapacitorCookiesAndroidInterface.isEnabled();
297+
if (isCookiesEnabled === true) {
298+
doPatchCookies = true;
299+
}
300+
}
301+
if (doPatchCookies) {
302+
Object.defineProperty(document, 'cookie', {
303+
get: function () {
304+
if (platform === 'ios') {
305+
// Use prompt to synchronously get cookies.
306+
// https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
307+
const payload = {
308+
type: 'CapacitorCookies',
309+
};
310+
const res = prompt(JSON.stringify(payload));
311+
return res;
312+
}
313+
else if (typeof win.CapacitorCookiesAndroidInterface !== 'undefined') {
314+
return win.CapacitorCookiesAndroidInterface.getCookies();
315+
}
316+
},
317+
set: function (val) {
318+
const cookiePairs = val.split(';');
319+
for (const cookiePair of cookiePairs) {
320+
const cookieKey = cookiePair.split('=')[0];
321+
const cookieValue = cookiePair.split('=')[1];
322+
if (null == cookieValue) {
323+
continue;
324+
}
325+
cap.toNative('CapacitorCookies', 'setCookie', {
326+
key: cookieKey,
327+
value: decode(cookieValue),
328+
});
329+
}
330+
},
331+
});
332+
}
333+
// patch fetch / XHR on Android/iOS
334+
// store original fetch & XHR functions
335+
win.CapacitorWebFetch = window.fetch;
336+
win.CapacitorWebXMLHttpRequest = {
337+
abort: window.XMLHttpRequest.prototype.abort,
338+
open: window.XMLHttpRequest.prototype.open,
339+
send: window.XMLHttpRequest.prototype.send,
340+
setRequestHeader: window.XMLHttpRequest.prototype.setRequestHeader,
341+
};
342+
let doPatchHttp = false;
343+
// check if capacitor http is disabled before patching
344+
if (platform === 'ios') {
345+
// Use prompt to synchronously get capacitor http config.
346+
// https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
347+
const payload = {
348+
type: 'CapacitorHttp',
349+
};
350+
const isHttpEnabled = prompt(JSON.stringify(payload));
351+
if (isHttpEnabled === 'true') {
352+
doPatchHttp = true;
353+
}
354+
}
355+
else if (typeof win.CapacitorHttpAndroidInterface !== 'undefined') {
356+
const isHttpEnabled = win.CapacitorHttpAndroidInterface.isEnabled();
357+
if (isHttpEnabled === true) {
358+
doPatchHttp = true;
359+
}
360+
}
361+
if (doPatchHttp) {
362+
// fetch patch
363+
window.fetch = async (resource, options) => {
364+
if (resource.toString().startsWith('data:') ||
365+
resource.toString().startsWith('blob:')) {
366+
return win.CapacitorWebFetch(resource, options);
367+
}
368+
try {
369+
// intercept request & pass to the bridge
370+
const nativeResponse = await cap.nativePromise('CapacitorHttp', 'request', {
371+
url: resource,
372+
method: (options === null || options === void 0 ? void 0 : options.method) ? options.method : undefined,
373+
data: (options === null || options === void 0 ? void 0 : options.body) ? options.body : undefined,
374+
headers: (options === null || options === void 0 ? void 0 : options.headers) ? options.headers : undefined,
375+
});
376+
const data = typeof nativeResponse.data === 'string'
377+
? nativeResponse.data
378+
: JSON.stringify(nativeResponse.data);
379+
// intercept & parse response before returning
380+
const response = new Response(data, {
381+
headers: nativeResponse.headers,
382+
status: nativeResponse.status,
383+
});
384+
return response;
385+
}
386+
catch (error) {
387+
return Promise.reject(error);
388+
}
389+
};
390+
// XHR event listeners
391+
const addEventListeners = function () {
392+
this.addEventListener('abort', function () {
393+
if (typeof this.onabort === 'function')
394+
this.onabort();
395+
});
396+
this.addEventListener('error', function () {
397+
if (typeof this.onerror === 'function')
398+
this.onerror();
399+
});
400+
this.addEventListener('load', function () {
401+
if (typeof this.onload === 'function')
402+
this.onload();
403+
});
404+
this.addEventListener('loadend', function () {
405+
if (typeof this.onloadend === 'function')
406+
this.onloadend();
407+
});
408+
this.addEventListener('loadstart', function () {
409+
if (typeof this.onloadstart === 'function')
410+
this.onloadstart();
411+
});
412+
this.addEventListener('readystatechange', function () {
413+
if (typeof this.onreadystatechange === 'function')
414+
this.onreadystatechange();
415+
});
416+
this.addEventListener('timeout', function () {
417+
if (typeof this.ontimeout === 'function')
418+
this.ontimeout();
419+
});
420+
};
421+
// XHR patch abort
422+
window.XMLHttpRequest.prototype.abort = function () {
423+
this.readyState = 0;
424+
this.dispatchEvent(new Event('abort'));
425+
this.dispatchEvent(new Event('loadend'));
426+
};
427+
// XHR patch open
428+
window.XMLHttpRequest.prototype.open = function (method, url) {
429+
Object.defineProperties(this, {
430+
_headers: {
431+
value: {},
432+
writable: true,
433+
},
434+
readyState: {
435+
get: function () {
436+
var _a;
437+
return (_a = this._readyState) !== null && _a !== void 0 ? _a : 0;
438+
},
439+
set: function (val) {
440+
this._readyState = val;
441+
this.dispatchEvent(new Event('readystatechange'));
442+
},
443+
},
444+
response: {
445+
value: '',
446+
writable: true,
447+
},
448+
responseText: {
449+
value: '',
450+
writable: true,
451+
},
452+
responseURL: {
453+
value: '',
454+
writable: true,
455+
},
456+
status: {
457+
value: 0,
458+
writable: true,
459+
},
460+
});
461+
addEventListeners.call(this);
462+
this._method = method;
463+
this._url = url;
464+
this.readyState = 1;
465+
};
466+
// XHR patch set request header
467+
window.XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
468+
this._headers[header] = value;
469+
};
470+
// XHR patch send
471+
window.XMLHttpRequest.prototype.send = function (body) {
472+
try {
473+
this.readyState = 2;
474+
// intercept request & pass to the bridge
475+
cap
476+
.nativePromise('CapacitorHttp', 'request', {
477+
url: this._url,
478+
method: this._method,
479+
data: body !== null ? body : undefined,
480+
headers: this._headers,
481+
})
482+
.then((nativeResponse) => {
483+
// intercept & parse response before returning
484+
if (this.readyState == 2) {
485+
this.dispatchEvent(new Event('loadstart'));
486+
this._headers = nativeResponse.headers;
487+
this.status = nativeResponse.status;
488+
this.response = nativeResponse.data;
489+
this.responseText =
490+
typeof nativeResponse.data === 'string'
491+
? nativeResponse.data
492+
: JSON.stringify(nativeResponse.data);
493+
this.responseURL = nativeResponse.url;
494+
this.readyState = 4;
495+
this.dispatchEvent(new Event('load'));
496+
this.dispatchEvent(new Event('loadend'));
497+
}
498+
})
499+
.catch((error) => {
500+
this.dispatchEvent(new Event('loadstart'));
501+
this.status = error.status;
502+
this._headers = error.headers;
503+
this.response = error.data;
504+
this.responseText = JSON.stringify(error.data);
505+
this.responseURL = error.url;
506+
this.readyState = 4;
507+
this.dispatchEvent(new Event('error'));
508+
this.dispatchEvent(new Event('loadend'));
509+
});
510+
}
511+
catch (error) {
512+
this.dispatchEvent(new Event('loadstart'));
513+
this.status = 500;
514+
this._headers = {};
515+
this.response = error;
516+
this.responseText = error.toString();
517+
this.responseURL = this._url;
518+
this.readyState = 4;
519+
this.dispatchEvent(new Event('error'));
520+
this.dispatchEvent(new Event('loadend'));
521+
}
522+
};
523+
// XHR patch getAllResponseHeaders
524+
window.XMLHttpRequest.prototype.getAllResponseHeaders = function () {
525+
let returnString = '';
526+
for (const key in this._headers) {
527+
if (key != 'Set-Cookie') {
528+
returnString += key + ': ' + this._headers[key] + '\r\n';
529+
}
530+
}
531+
return returnString;
532+
};
533+
// XHR patch getResponseHeader
534+
window.XMLHttpRequest.prototype.getResponseHeader = function (name) {
535+
return this._headers[name];
536+
};
537+
}
538+
}
271539
// patch window.console on iOS and store original console fns
272540
const isIos = getPlatformId(win) === 'ios';
273541
if (win.console && isIos) {

android/capacitor/src/main/java/com/getcapacitor/Bridge.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,9 @@ private void initWebView() {
556556
* Register our core Plugin APIs
557557
*/
558558
private void registerAllPlugins() {
559+
this.registerPlugin(com.getcapacitor.plugin.CapacitorCookies.class);
559560
this.registerPlugin(com.getcapacitor.plugin.WebView.class);
561+
this.registerPlugin(com.getcapacitor.plugin.CapacitorHttp.class);
560562

561563
for (Class<? extends Plugin> pluginClass : this.initialPlugins) {
562564
this.registerPlugin(pluginClass);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.getcapacitor;
2+
3+
import org.json.JSONException;
4+
5+
/**
6+
* Represents a single user-data value of any type on the capacitor PluginCall object.
7+
*/
8+
public class JSValue {
9+
10+
private final Object value;
11+
12+
/**
13+
* @param call The capacitor plugin call, used for accessing the value safely.
14+
* @param name The name of the property to access.
15+
*/
16+
public JSValue(PluginCall call, String name) {
17+
this.value = this.toValue(call, name);
18+
}
19+
20+
/**
21+
* Returns the coerced but uncasted underlying value.
22+
*/
23+
public Object getValue() {
24+
return this.value;
25+
}
26+
27+
@Override
28+
public String toString() {
29+
return this.getValue().toString();
30+
}
31+
32+
/**
33+
* Returns the underlying value as a JSObject, or throwing if it cannot.
34+
*
35+
* @throws JSONException If the underlying value is not a JSObject.
36+
*/
37+
public JSObject toJSObject() throws JSONException {
38+
if (this.value instanceof JSObject) return (JSObject) this.value;
39+
throw new JSONException("JSValue could not be coerced to JSObject.");
40+
}
41+
42+
/**
43+
* Returns the underlying value as a JSArray, or throwing if it cannot.
44+
*
45+
* @throws JSONException If the underlying value is not a JSArray.
46+
*/
47+
public JSArray toJSArray() throws JSONException {
48+
if (this.value instanceof JSArray) return (JSArray) this.value;
49+
throw new JSONException("JSValue could not be coerced to JSArray.");
50+
}
51+
52+
/**
53+
* Returns the underlying value this object represents, coercing it into a capacitor-friendly object if supported.
54+
*/
55+
private Object toValue(PluginCall call, String name) {
56+
Object value = null;
57+
value = call.getArray(name, null);
58+
if (value != null) return value;
59+
value = call.getObject(name, null);
60+
if (value != null) return value;
61+
value = call.getString(name, null);
62+
if (value != null) return value;
63+
return call.getData().opt(name);
64+
}
65+
}

0 commit comments

Comments
 (0)