Fixed ajax and fetch interceptor when the data/body sent is not a string, fix #724, updated nodejs test server

This commit is contained in:
Lorenzo Pichilli 2021-03-18 17:24:13 +01:00
parent bcc5fc68ba
commit 6f356be623
14 changed files with 1373 additions and 404 deletions

View File

@ -9,6 +9,12 @@
- Fixed wrong mapping of `NavigationAction` class on Android for `androidHasGesture` and `androidIsRedirect` properties
- Fixed "Pull to refresh creating problem in some webpages on Android" [#719](https://github.com/pichillilorenzo/flutter_inappwebview/issues/719)
- Fixed iOS sometimes `scrollView.contentSize` doesn't fit all the `frame.size` available
- Fixed ajax and fetch interceptor when the data/body sent is not a string
- Fixed "InAppLocalhostServer - Error: type 'List<dynamic>' is not a subtype of type 'List<int>' in type cast" [#724](https://github.com/pichillilorenzo/flutter_inappwebview/issues/724)
### BREAKING CHANGES
- `FetchRequest.body` is a dynamic type now
## 5.1.0+4

View File

@ -274,4 +274,17 @@ public class Util {
public static String replaceAll(String s, String oldString, String newString) {
return TextUtils.join(newString, s.split(Pattern.quote(oldString)));
}
public static void log(String tag, String message) {
// Split by line, then ensure each line can fit into Log's maximum length.
for (int i = 0, length = message.length(); i < length; i++) {
int newline = message.indexOf('\n', i);
newline = newline != -1 ? newline : length;
do {
int end = Math.min(newline, i + 4000);
Log.d(tag, message.substring(i, end));
i = end;
} while (i < newline);
}
}
}

View File

@ -183,16 +183,17 @@ public class InterceptAjaxRequestJS {
" this.addEventListener('error', handleEvent);" +
" this.addEventListener('abort', handleEvent);" +
" this.addEventListener('timeout', handleEvent);" +
" " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertBodyRequest(data).then(function(data) {" +
" var ajaxRequest = {" +
" data: data," +
" method: this._flutter_inappwebview_method," +
" url: this._flutter_inappwebview_url," +
" isAsync: this._flutter_inappwebview_isAsync," +
" user: this._flutter_inappwebview_user," +
" password: this._flutter_inappwebview_password," +
" withCredentials: this.withCredentials," +
" headers: this._flutter_inappwebview_request_headers," +
" responseType: this.responseType" +
" method: self._flutter_inappwebview_method," +
" url: self._flutter_inappwebview_url," +
" isAsync: self._flutter_inappwebview_isAsync," +
" user: self._flutter_inappwebview_user," +
" password: self._flutter_inappwebview_password," +
" withCredentials: self.withCredentials," +
" headers: self._flutter_inappwebview_request_headers," +
" responseType: self.responseType" +
" };" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) {" +
" if (result != null) {" +
@ -201,7 +202,22 @@ public class InterceptAjaxRequestJS {
" self.abort();" +
" return;" +
" };" +
" if (result.data != null && !" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.data) && result.data.length > 0) {" +
" var bodyString = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".arrayBufferToString(result.data);" +
" if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isBodyFormData(bodyString)) {" +
" var formDataContentType = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".getFormDataContentType(bodyString);" +
" if (result.headers != null) {" +
" result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type'];" +
" } else {" +
" result.headers = { 'Content-Type': formDataContentType };" +
" }" +
" }" +
" }" +
" if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.data) || result.data == null) {" +
" data = result.data;" +
" } else if (result.data.length > 0) {" +
" data = new Uint8Array(result.data);" +
" }" +
" self.withCredentials = result.withCredentials;" +
" if (result.responseType != null) {" +
" self.responseType = result.responseType;" +
@ -224,6 +240,7 @@ public class InterceptAjaxRequestJS {
" }" +
" send.call(self, data);" +
" });" +
" });" +
" } else {" +
" send.call(this, data);" +
" }" +

View File

@ -21,68 +21,6 @@ public class InterceptFetchRequestJS {
" if (fetch == null) {" +
" return;" +
" }" +
" function convertHeadersToJson(headers) {" +
" var headersObj = {};" +
" for (var header of headers.keys()) {" +
" var value = headers.get(header);" +
" headersObj[header] = value;" +
" }" +
" return headersObj;" +
" }" +
" function convertJsonToHeaders(headersJson) {" +
" return new Headers(headersJson);" +
" }" +
" function convertBodyToArray(body) {" +
" return new Response(body).arrayBuffer().then(function(arrayBuffer) {" +
" var arr = Array.from(new Uint8Array(arrayBuffer));" +
" return arr;" +
" })" +
" }" +
" function convertArrayIntBodyToUint8Array(arrayIntBody) {" +
" return new Uint8Array(arrayIntBody);" +
" }" +
" function convertCredentialsToJson(credentials) {" +
" var credentialsObj = {};" +
" if (window.FederatedCredential != null && credentials instanceof FederatedCredential) {" +
" credentialsObj.type = credentials.type;" +
" credentialsObj.id = credentials.id;" +
" credentialsObj.name = credentials.name;" +
" credentialsObj.protocol = credentials.protocol;" +
" credentialsObj.provider = credentials.provider;" +
" credentialsObj.iconURL = credentials.iconURL;" +
" } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) {" +
" credentialsObj.type = credentials.type;" +
" credentialsObj.id = credentials.id;" +
" credentialsObj.name = credentials.name;" +
" credentialsObj.password = credentials.password;" +
" credentialsObj.iconURL = credentials.iconURL;" +
" } else {" +
" credentialsObj.type = 'default';" +
" credentialsObj.value = credentials;" +
" }" +
" }" +
" function convertJsonToCredential(credentialsJson) {" +
" var credentials;" +
" if (window.FederatedCredential != null && credentialsJson.type === 'federated') {" +
" credentials = new FederatedCredential({" +
" id: credentialsJson.id," +
" name: credentialsJson.name," +
" protocol: credentialsJson.protocol," +
" provider: credentialsJson.provider," +
" iconURL: credentialsJson.iconURL" +
" });" +
" } else if (window.PasswordCredential != null && credentialsJson.type === 'password') {" +
" credentials = new PasswordCredential({" +
" id: credentialsJson.id," +
" name: credentialsJson.name," +
" password: credentialsJson.password," +
" iconURL: credentialsJson.iconURL" +
" });" +
" } else {" +
" credentials = credentialsJson;" +
" }" +
" return credentials;" +
" }" +
" window.fetch = async function(resource, init) {" +
" var w = (window.top == null || window.top === window) ? window : window.top;" +
" if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " == true) {" +
@ -114,7 +52,7 @@ public class InterceptFetchRequestJS {
" fetchRequest.integrity = resource.integrity;" +
" fetchRequest.keepalive = resource.keepalive;" +
" } else {" +
" fetchRequest.url = resource;" +
" fetchRequest.url = resource != null ? resource.toString() : null;" +
" if (init != null) {" +
" fetchRequest.method = init.method;" +
" fetchRequest.headers = init.headers;" +
@ -130,10 +68,10 @@ public class InterceptFetchRequestJS {
" }" +
" }" +
" if (fetchRequest.headers instanceof Headers) {" +
" fetchRequest.headers = convertHeadersToJson(fetchRequest.headers);" +
" fetchRequest.headers = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertHeadersToJson(fetchRequest.headers);" +
" }" +
" fetchRequest.credentials = convertCredentialsToJson(fetchRequest.credentials);" +
" return convertBodyToArray(fetchRequest.body).then(function(body) {" +
" fetchRequest.credentials = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertCredentialsToJson(fetchRequest.credentials);" +
" return " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertBodyRequest(fetchRequest.body).then(function(body) {" +
" fetchRequest.body = body;" +
" return window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) {" +
" if (result != null) {" +
@ -150,7 +88,18 @@ public class InterceptFetchRequestJS {
" controller.abort();" +
" break;" +
" }" +
" resource = (result.url != null) ? result.url : resource;" +
" if (result.body != null && !" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.body) && result.body.length > 0) {" +
" var bodyString = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".arrayBufferToString(result.body);" +
" if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isBodyFormData(bodyString)) {" +
" var formDataContentType = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".getFormDataContentType(bodyString);" +
" if (result.headers != null) {" +
" result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type'];" +
" } else {" +
" result.headers = { 'Content-Type': formDataContentType };" +
" }" +
" }" +
" }" +
" resource = result.url;" +
" if (init == null) {" +
" init = {};" +
" }" +
@ -158,16 +107,18 @@ public class InterceptFetchRequestJS {
" init.method = result.method;" +
" }" +
" if (result.headers != null && Object.keys(result.headers).length > 0) {" +
" init.headers = convertJsonToHeaders(result.headers);" +
" init.headers = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertJsonToHeaders(result.headers);" +
" }" +
" if (result.body != null && result.body.length > 0) {" +
" init.body = convertArrayIntBodyToUint8Array(result.body);" +
" if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.body) || result.body == null) {" +
" init.body = result.body;" +
" } else if (result.body.length > 0) {" +
" init.body = new Uint8Array(result.body);" +
" }" +
" if (result.mode != null && result.mode.length > 0) {" +
" init.mode = result.mode;" +
" }" +
" if (result.credentials != null) {" +
" init.credentials = convertJsonToCredential(result.credentials);" +
" init.credentials = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertJsonToCredential(result.credentials);" +
" }" +
" if (result.cache != null && result.cache.length > 0) {" +
" init.cache = result.cache;" +

View File

@ -14,6 +14,203 @@ public class JavaScriptBridgeJS {
true
);
public static final String JAVASCRIPT_UTIL_VAR_NAME = "window." + JAVASCRIPT_BRIDGE_NAME + "._Util";
public static final String UTIL_JS_SOURCE = JAVASCRIPT_UTIL_VAR_NAME + " = {" +
" support: {" +
" searchParams: 'URLSearchParams' in window," +
" iterable: 'Symbol' in window && 'iterator' in Symbol," +
" blob:" +
" 'FileReader' in window &&" +
" 'Blob' in window &&" +
" (function() {" +
" try {" +
" new Blob();" +
" return true;" +
" } catch (e) {" +
" return false;" +
" }" +
" })()," +
" formData: 'FormData' in window," +
" arrayBuffer: 'ArrayBuffer' in window" +
" }," +
" isDataView: function(obj) {" +
" return obj && DataView.prototype.isPrototypeOf(obj);" +
" }," +
" fileReaderReady: function(reader) {" +
" return new Promise(function(resolve, reject) {" +
" reader.onload = function() {" +
" resolve(reader.result);" +
" };" +
" reader.onerror = function() {" +
" reject(reader.error);" +
" };" +
" });" +
" }," +
" readBlobAsArrayBuffer: function(blob) {" +
" var reader = new FileReader();" +
" var promise = " + JAVASCRIPT_UTIL_VAR_NAME + ".fileReaderReady(reader);" +
" reader.readAsArrayBuffer(blob);" +
" return promise;" +
" }," +
" convertBodyToArrayBuffer: function(body) {" +
" var viewClasses = [" +
" '[object Int8Array]'," +
" '[object Uint8Array]'," +
" '[object Uint8ClampedArray]'," +
" '[object Int16Array]'," +
" '[object Uint16Array]'," +
" '[object Int32Array]'," +
" '[object Uint32Array]'," +
" '[object Float32Array]'," +
" '[object Float64Array]'" +
" ];" +
" var isArrayBufferView = null;" +
" if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.arrayBuffer) {" +
" isArrayBufferView =" +
" ArrayBuffer.isView ||" +
" function(obj) {" +
" return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1;" +
" };" +
" }" +
" var bodyUsed = false;" +
" this._bodyInit = body;" +
" if (!body) {" +
" this._bodyText = '';" +
" } else if (typeof body === 'string') {" +
" this._bodyText = body;" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.blob && Blob.prototype.isPrototypeOf(body)) {" +
" this._bodyBlob = body;" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.formData && FormData.prototype.isPrototypeOf(body)) {" +
" this._bodyFormData = body;" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {" +
" this._bodyText = body.toString();" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.arrayBuffer && " + JAVASCRIPT_UTIL_VAR_NAME + ".support.blob && " + JAVASCRIPT_UTIL_VAR_NAME + ".isDataView(body)) {" +
" this._bodyArrayBuffer = bufferClone(body.buffer);" +
" this._bodyInit = new Blob([this._bodyArrayBuffer]);" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {" +
" this._bodyArrayBuffer = bufferClone(body);" +
" } else {" +
" this._bodyText = body = Object.prototype.toString.call(body);" +
" }" +
" this.blob = function () {" +
" if (bodyUsed) {" +
" return Promise.reject(new TypeError('Already read'));" +
" }" +
" bodyUsed = true;" +
" if (this._bodyBlob) {" +
" return Promise.resolve(this._bodyBlob);" +
" } else if (this._bodyArrayBuffer) {" +
" return Promise.resolve(new Blob([this._bodyArrayBuffer]));" +
" } else if (this._bodyFormData) {" +
" throw new Error('could not read FormData body as blob');" +
" } else {" +
" return Promise.resolve(new Blob([this._bodyText]));" +
" }" +
" };" +
" if (this._bodyArrayBuffer) {" +
" if (bodyUsed) {" +
" return Promise.reject(new TypeError('Already read'));" +
" }" +
" bodyUsed = true;" +
" if (ArrayBuffer.isView(this._bodyArrayBuffer)) {" +
" return Promise.resolve(" +
" this._bodyArrayBuffer.buffer.slice(" +
" this._bodyArrayBuffer.byteOffset," +
" this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength" +
" )" +
" );" +
" } else {" +
" return Promise.resolve(this._bodyArrayBuffer);" +
" }" +
" }" +
" return this.blob().then(" + JAVASCRIPT_UTIL_VAR_NAME + ".readBlobAsArrayBuffer);" +
" }," +
" isString: function(variable) {" +
" return typeof variable === 'string' || variable instanceof String;" +
" }," +
" convertBodyRequest: function(body) {" +
" if (body == null) {" +
" return new Promise((resolve, reject) => resolve(null));" +
" }" +
" if (" + JAVASCRIPT_UTIL_VAR_NAME + ".isString(body) || (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.searchParams && body instanceof URLSearchParams)) {" +
" return new Promise((resolve, reject) => resolve(body.toString()));" +
" }" +
" if (window.Response != null) {" +
" return new Response(body).arrayBuffer().then(function(arrayBuffer) {" +
" return Array.from(new Uint8Array(arrayBuffer));" +
" });" +
" }" +
" return " + JAVASCRIPT_UTIL_VAR_NAME + ".convertBodyToArrayBuffer(body).then(function(arrayBuffer) {" +
" return Array.from(new Uint8Array(arrayBuffer));" +
" });" +
" }," +
" arrayBufferToString: function(arrayBuffer) {" +
" return String.fromCharCode.apply(String, arrayBuffer);" +
" }," +
" isBodyFormData: function(bodyString) {" +
" return bodyString.indexOf('------WebKitFormBoundary') >= 0;" +
" }," +
" getFormDataContentType: function(bodyString) {" +
" var boundary = bodyString.substr(2, 40);" +
" return 'multipart/form-data; boundary=' + boundary;" +
" }," +
" convertHeadersToJson: function(headers) {" +
" var headersObj = {};" +
" for (var header of headers.keys()) {" +
" var value = headers.get(header);" +
" headersObj[header] = value;" +
" }" +
" return headersObj;" +
" }," +
" convertJsonToHeaders: function(headersJson) {" +
" return new Headers(headersJson);" +
" }," +
" convertCredentialsToJson: function(credentials) {" +
" var credentialsObj = {};" +
" if (window.FederatedCredential != null && credentials instanceof FederatedCredential) {" +
" credentialsObj.type = credentials.type;" +
" credentialsObj.id = credentials.id;" +
" credentialsObj.name = credentials.name;" +
" credentialsObj.protocol = credentials.protocol;" +
" credentialsObj.provider = credentials.provider;" +
" credentialsObj.iconURL = credentials.iconURL;" +
" } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) {" +
" credentialsObj.type = credentials.type;" +
" credentialsObj.id = credentials.id;" +
" credentialsObj.name = credentials.name;" +
" credentialsObj.password = credentials.password;" +
" credentialsObj.iconURL = credentials.iconURL;" +
" } else {" +
" credentialsObj.type = 'default';" +
" credentialsObj.value = credentials;" +
" }" +
" return credentialsObj;" +
" }," +
" convertJsonToCredential: function(credentialsJson) {" +
" var credentials;" +
" if (window.FederatedCredential != null && credentialsJson.type === 'federated') {" +
" credentials = new FederatedCredential({" +
" id: credentialsJson.id," +
" name: credentialsJson.name," +
" protocol: credentialsJson.protocol," +
" provider: credentialsJson.provider," +
" iconURL: credentialsJson.iconURL" +
" });" +
" } else if (window.PasswordCredential != null && credentialsJson.type === 'password') {" +
" credentials = new PasswordCredential({" +
" id: credentialsJson.id," +
" name: credentialsJson.name," +
" password: credentialsJson.password," +
" iconURL: credentialsJson.iconURL" +
" });" +
" } else {" +
" credentials = credentialsJson.value == null ? undefined : credentialsJson.value;" +
" }" +
" return credentials;" +
" }" +
"};";
public static final String JAVASCRIPT_BRIDGE_JS_SOURCE = "if (window.top == null || window.top === window) {" +
" window." + JAVASCRIPT_BRIDGE_NAME + ".callHandler = function() {" +
" var _callHandlerID = setTimeout(function(){});" +
@ -30,8 +227,9 @@ public class JavaScriptBridgeJS {
" return new Promise(function(resolve, reject) {" +
" window.top." + JAVASCRIPT_BRIDGE_NAME + "[_callHandlerID] = resolve;" +
" });" +
" };"+
"}";
" };" +
"}" +
UTIL_JS_SOURCE;
public static final String PLATFORM_READY_JS_SOURCE = "(function() {" +
" if ((window.top == null || window.top === window) && window." + JAVASCRIPT_BRIDGE_NAME + "._platformReady == null) {" +

View File

@ -1706,7 +1706,8 @@ void main() {
skip: !Platform.isAndroid,
);
testWidgets('intercept ajax request', (WidgetTester tester) async {
group('intercept ajax request', () {
testWidgets('send string data', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer shouldInterceptAjaxPostRequestCompleter =
Completer<void>();
@ -1736,10 +1737,6 @@ void main() {
xhttp.open("POST", "http://${environment["NODE_SERVER_IP"]}:8082/test-ajax-post");
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send("firstname=Foo&lastname=Bar");
var xhttp2 = new XMLHttpRequest();
xhttp2.open("GET", "http://${environment["NODE_SERVER_IP"]}:8082/test-download-file");
xhttp2.send();
});
</script>
</body>
@ -1754,25 +1751,23 @@ void main() {
controllerCompleter.complete(controller);
},
shouldInterceptAjaxRequest: (controller, ajaxRequest) async {
if (ajaxRequest.url!.toString().endsWith("/test-ajax-post")) {
expect(ajaxRequest.data, "firstname=Foo&lastname=Bar");
ajaxRequest.responseType = 'json';
ajaxRequest.data = "firstname=Foo2&lastname=Bar2";
shouldInterceptAjaxPostRequestCompleter.complete(controller);
}
return ajaxRequest;
},
onAjaxReadyStateChange: (controller, ajaxRequest) async {
if (ajaxRequest.readyState == AjaxRequestReadyState.DONE &&
ajaxRequest.status == 200 &&
ajaxRequest.url!.toString().endsWith("/test-ajax-post")) {
ajaxRequest.status == 200) {
Map<String, dynamic> res = ajaxRequest.response;
onAjaxReadyStateChangeCompleter.complete(res);
}
return AjaxRequestAction.PROCEED;
},
onAjaxProgress: (controller, ajaxRequest) async {
if (ajaxRequest.event!.type == AjaxRequestEventType.LOAD &&
ajaxRequest.url!.toString().endsWith("/test-ajax-post")) {
if (ajaxRequest.event!.type == AjaxRequestEventType.LOAD) {
Map<String, dynamic> res = ajaxRequest.response;
onAjaxProgressCompleter.complete(res);
}
@ -1798,6 +1793,621 @@ void main() {
true);
});
testWidgets('send json data', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer shouldInterceptAjaxPostRequestCompleter =
Completer<void>();
final Completer<Map<String, dynamic>> onAjaxReadyStateChangeCompleter =
Completer<Map<String, dynamic>>();
final Completer<Map<String, dynamic>> onAjaxProgressCompleter =
Completer<Map<String, dynamic>>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialData: InAppWebViewInitialData(data: """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>InAppWebViewAjaxTest</title>
</head>
<body>
<h1>InAppWebViewAjaxTest</h1>
<script>
window.addEventListener('flutterInAppWebViewPlatformReady', function(event) {
var jsonData = {
firstname: 'Foo',
lastname: 'Bar'
};
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://${environment["NODE_SERVER_IP"]}:8082/test-ajax-post");
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(JSON.stringify(jsonData));
});
</script>
</body>
</html>
"""),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
clearCache: true,
useShouldInterceptAjaxRequest: true,
)),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
},
shouldInterceptAjaxRequest: (controller, ajaxRequest) async {
expect(ajaxRequest.data, '{"firstname":"Foo","lastname":"Bar"}');
ajaxRequest.responseType = 'json';
ajaxRequest.data = "{'firstname': 'Foo2', 'lastname': 'Bar2'}";
shouldInterceptAjaxPostRequestCompleter.complete(controller);
return ajaxRequest;
},
onAjaxReadyStateChange: (controller, ajaxRequest) async {
if (ajaxRequest.readyState == AjaxRequestReadyState.DONE &&
ajaxRequest.status == 200) {
Map<String, dynamic> res = ajaxRequest.response;
onAjaxReadyStateChangeCompleter.complete(res);
}
return AjaxRequestAction.PROCEED;
},
onAjaxProgress: (controller, ajaxRequest) async {
if (ajaxRequest.event!.type == AjaxRequestEventType.LOAD) {
Map<String, dynamic> res = ajaxRequest.response;
onAjaxProgressCompleter.complete(res);
}
return AjaxRequestAction.PROCEED;
},
),
),
);
await shouldInterceptAjaxPostRequestCompleter.future;
final Map<String, dynamic> onAjaxReadyStateChangeValue =
await onAjaxReadyStateChangeCompleter.future;
final Map<String, dynamic> onAjaxProgressValue =
await onAjaxProgressCompleter.future;
expect(
mapEquals(onAjaxReadyStateChangeValue,
{'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
expect(
mapEquals(
onAjaxProgressValue, {'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
});
testWidgets('send URLSearchParams data', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer shouldInterceptAjaxPostRequestCompleter =
Completer<void>();
final Completer<Map<String, dynamic>> onAjaxReadyStateChangeCompleter =
Completer<Map<String, dynamic>>();
final Completer<Map<String, dynamic>> onAjaxProgressCompleter =
Completer<Map<String, dynamic>>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialData: InAppWebViewInitialData(data: """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>InAppWebViewAjaxTest</title>
</head>
<body>
<h1>InAppWebViewAjaxTest</h1>
<script>
window.addEventListener('flutterInAppWebViewPlatformReady', function(event) {
var paramsString = "firstname=Foo&lastname=Bar";
var searchParams = new URLSearchParams(paramsString);
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://${environment["NODE_SERVER_IP"]}:8082/test-ajax-post");
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send(searchParams);
});
</script>
</body>
</html>
"""),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
clearCache: true,
useShouldInterceptAjaxRequest: true,
)),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
},
shouldInterceptAjaxRequest: (controller, ajaxRequest) async {
expect(ajaxRequest.data, "firstname=Foo&lastname=Bar");
ajaxRequest.responseType = 'json';
ajaxRequest.data = "firstname=Foo2&lastname=Bar2";
shouldInterceptAjaxPostRequestCompleter.complete(controller);
return ajaxRequest;
},
onAjaxReadyStateChange: (controller, ajaxRequest) async {
if (ajaxRequest.readyState == AjaxRequestReadyState.DONE &&
ajaxRequest.status == 200) {
Map<String, dynamic> res = ajaxRequest.response;
onAjaxReadyStateChangeCompleter.complete(res);
}
return AjaxRequestAction.PROCEED;
},
onAjaxProgress: (controller, ajaxRequest) async {
if (ajaxRequest.event!.type == AjaxRequestEventType.LOAD) {
Map<String, dynamic> res = ajaxRequest.response;
onAjaxProgressCompleter.complete(res);
}
return AjaxRequestAction.PROCEED;
},
),
),
);
await shouldInterceptAjaxPostRequestCompleter.future;
final Map<String, dynamic> onAjaxReadyStateChangeValue =
await onAjaxReadyStateChangeCompleter.future;
final Map<String, dynamic> onAjaxProgressValue =
await onAjaxProgressCompleter.future;
expect(
mapEquals(onAjaxReadyStateChangeValue,
{'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
expect(
mapEquals(
onAjaxProgressValue, {'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
});
testWidgets('send FormData', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer shouldInterceptAjaxPostRequestCompleter =
Completer<void>();
final Completer<Map<String, dynamic>> onAjaxReadyStateChangeCompleter =
Completer<Map<String, dynamic>>();
final Completer<Map<String, dynamic>> onAjaxProgressCompleter =
Completer<Map<String, dynamic>>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialData: InAppWebViewInitialData(data: """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>InAppWebViewAjaxTest</title>
</head>
<body>
<h1>InAppWebViewAjaxTest</h1>
<script>
window.addEventListener('flutterInAppWebViewPlatformReady', function(event) {
var formData = new FormData();
formData.append('firstname', 'Foo');
formData.append('lastname', 'Bar');
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://${environment["NODE_SERVER_IP"]}:8082/test-ajax-post");
xhttp.send(formData);
});
</script>
</body>
</html>
"""),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
clearCache: true,
useShouldInterceptAjaxRequest: true,
)),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
},
shouldInterceptAjaxRequest: (controller, ajaxRequest) async {
expect(ajaxRequest.data, isNotNull);
var body = ajaxRequest.data.cast<int>();
var bodyString = String.fromCharCodes(body);
expect(bodyString.indexOf("WebKitFormBoundary") >= 0, true);
ajaxRequest.data = utf8.encode(bodyString.replaceFirst("Foo", "Foo2").replaceFirst("Bar", "Bar2"));
ajaxRequest.responseType = 'json';
shouldInterceptAjaxPostRequestCompleter.complete(controller);
return ajaxRequest;
},
onAjaxReadyStateChange: (controller, ajaxRequest) async {
if (ajaxRequest.readyState == AjaxRequestReadyState.DONE &&
ajaxRequest.status == 200) {
Map<String, dynamic> res = ajaxRequest.response;
onAjaxReadyStateChangeCompleter.complete(res);
}
return AjaxRequestAction.PROCEED;
},
onAjaxProgress: (controller, ajaxRequest) async {
if (ajaxRequest.event!.type == AjaxRequestEventType.LOAD) {
Map<String, dynamic> res = ajaxRequest.response;
onAjaxProgressCompleter.complete(res);
}
return AjaxRequestAction.PROCEED;
},
),
),
);
await shouldInterceptAjaxPostRequestCompleter.future;
final Map<String, dynamic> onAjaxReadyStateChangeValue =
await onAjaxReadyStateChangeCompleter.future;
final Map<String, dynamic> onAjaxProgressValue =
await onAjaxProgressCompleter.future;
expect(
mapEquals(onAjaxReadyStateChangeValue,
{'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
expect(
mapEquals(
onAjaxProgressValue, {'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
});
});
group('intercept fetch request', () {
testWidgets('send string data', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer<String> fetchGetCompleter = Completer<String>();
final Completer<Map<String, dynamic>> fetchPostCompleter =
Completer<Map<String, dynamic>>();
final Completer<void> shouldInterceptFetchPostRequestCompleter =
Completer<void>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialData: InAppWebViewInitialData(data: """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>InAppWebViewFetchTest</title>
</head>
<body>
<h1>InAppWebViewFetchTest</h1>
<script>
window.addEventListener('flutterInAppWebViewPlatformReady', function(event) {
fetch("http://${environment["NODE_SERVER_IP"]}:8082/test-ajax-post", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: "firstname=Foo&lastname=Bar"
}).then(function(response) {
response.json().then(function(value) {
window.flutter_inappwebview.callHandler('fetchPost', value);
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
});
</script>
</body>
</html>
"""),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
clearCache: true,
useShouldInterceptFetchRequest: true,
)),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
controller.addJavaScriptHandler(
handlerName: "fetchPost",
callback: (args) {
fetchPostCompleter
.complete(args[0] as Map<String, dynamic>);
});
},
shouldInterceptFetchRequest: (controller, fetchRequest) async {
expect(fetchRequest.body, "firstname=Foo&lastname=Bar");
fetchRequest.body = "firstname=Foo2&lastname=Bar2";
shouldInterceptFetchPostRequestCompleter.complete();
return fetchRequest;
},
),
),
);
var fetchGetCompleterValue = await fetchGetCompleter.future;
expect(fetchGetCompleterValue, '200');
await shouldInterceptFetchPostRequestCompleter.future;
var fetchPostCompleterValue = await fetchPostCompleter.future;
expect(
mapEquals(fetchPostCompleterValue,
{'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
});
testWidgets('send json data', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer<String> fetchGetCompleter = Completer<String>();
final Completer<Map<String, dynamic>> fetchPostCompleter =
Completer<Map<String, dynamic>>();
final Completer<void> shouldInterceptFetchPostRequestCompleter =
Completer<void>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialData: InAppWebViewInitialData(data: """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>InAppWebViewFetchTest</title>
</head>
<body>
<h1>InAppWebViewFetchTest</h1>
<script>
window.addEventListener('flutterInAppWebViewPlatformReady', function(event) {
var jsonData = {
firstname: 'Foo',
lastname: 'Bar'
};
fetch("http://${environment["NODE_SERVER_IP"]}:8082/test-ajax-post", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
}).then(function(response) {
response.json().then(function(value) {
window.flutter_inappwebview.callHandler('fetchPost', value);
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
});
</script>
</body>
</html>
"""),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
clearCache: true,
useShouldInterceptFetchRequest: true,
)),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
controller.addJavaScriptHandler(
handlerName: "fetchPost",
callback: (args) {
fetchPostCompleter
.complete(args[0] as Map<String, dynamic>);
});
},
shouldInterceptFetchRequest: (controller, fetchRequest) async {
expect(fetchRequest.body, '{"firstname":"Foo","lastname":"Bar"}');
fetchRequest.body = "{'firstname': 'Foo2', 'lastname': 'Bar2'}";
shouldInterceptFetchPostRequestCompleter.complete();
return fetchRequest;
},
),
),
);
var fetchGetCompleterValue = await fetchGetCompleter.future;
expect(fetchGetCompleterValue, '200');
await shouldInterceptFetchPostRequestCompleter.future;
var fetchPostCompleterValue = await fetchPostCompleter.future;
expect(
mapEquals(fetchPostCompleterValue,
{'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
});
testWidgets('send URLSearchParams data', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer<String> fetchGetCompleter = Completer<String>();
final Completer<Map<String, dynamic>> fetchPostCompleter =
Completer<Map<String, dynamic>>();
final Completer<void> shouldInterceptFetchPostRequestCompleter =
Completer<void>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialData: InAppWebViewInitialData(data: """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>InAppWebViewFetchTest</title>
</head>
<body>
<h1>InAppWebViewFetchTest</h1>
<script>
window.addEventListener('flutterInAppWebViewPlatformReady', function(event) {
var paramsString = "firstname=Foo&lastname=Bar";
var searchParams = new URLSearchParams(paramsString);
fetch("http://${environment["NODE_SERVER_IP"]}:8082/test-ajax-post", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: searchParams
}).then(function(response) {
response.json().then(function(value) {
window.flutter_inappwebview.callHandler('fetchPost', value);
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
});
</script>
</body>
</html>
"""),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
clearCache: true,
useShouldInterceptFetchRequest: true,
)),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
controller.addJavaScriptHandler(
handlerName: "fetchPost",
callback: (args) {
fetchPostCompleter
.complete(args[0] as Map<String, dynamic>);
});
},
shouldInterceptFetchRequest: (controller, fetchRequest) async {
expect(fetchRequest.body, "firstname=Foo&lastname=Bar");
fetchRequest.body = "firstname=Foo2&lastname=Bar2";
shouldInterceptFetchPostRequestCompleter.complete();
return fetchRequest;
},
),
),
);
var fetchGetCompleterValue = await fetchGetCompleter.future;
expect(fetchGetCompleterValue, '200');
await shouldInterceptFetchPostRequestCompleter.future;
var fetchPostCompleterValue = await fetchPostCompleter.future;
expect(
mapEquals(fetchPostCompleterValue,
{'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
});
testWidgets('send FormData', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer<String> fetchGetCompleter = Completer<String>();
final Completer<Map<String, dynamic>> fetchPostCompleter =
Completer<Map<String, dynamic>>();
final Completer<void> shouldInterceptFetchPostRequestCompleter =
Completer<void>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialData: InAppWebViewInitialData(data: """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>InAppWebViewFetchTest</title>
</head>
<body>
<h1>InAppWebViewFetchTest</h1>
<script>
window.addEventListener('flutterInAppWebViewPlatformReady', function(event) {
var formData = new FormData();
formData.append('firstname', 'Foo');
formData.append('lastname', 'Bar');
fetch("http://${environment["NODE_SERVER_IP"]}:8082/test-ajax-post", {
method: 'POST',
body: formData
}).then(function(response) {
response.json().then(function(value) {
window.flutter_inappwebview.callHandler('fetchPost', value);
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
});
</script>
</body>
</html>
"""),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
clearCache: true,
useShouldInterceptFetchRequest: true,
)),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
controller.addJavaScriptHandler(
handlerName: "fetchPost",
callback: (args) {
fetchPostCompleter
.complete(args[0] as Map<String, dynamic>);
});
},
shouldInterceptFetchRequest: (controller, fetchRequest) async {
expect(fetchRequest.body, isNotNull);
var body = fetchRequest.body.cast<int>();
var bodyString = String.fromCharCodes(body);
expect(bodyString.indexOf("WebKitFormBoundary") >= 0, true);
fetchRequest.body = utf8.encode(bodyString.replaceFirst("Foo", "Foo2").replaceFirst("Bar", "Bar2"));
shouldInterceptFetchPostRequestCompleter.complete();
return fetchRequest;
},
),
),
);
var fetchGetCompleterValue = await fetchGetCompleter.future;
expect(fetchGetCompleterValue, '200');
await shouldInterceptFetchPostRequestCompleter.future;
var fetchPostCompleterValue = await fetchPostCompleter.future;
expect(
mapEquals(fetchPostCompleterValue,
{'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
});
});
testWidgets('Content Blocker', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer<void> pageLoaded = Completer<void>();
@ -1834,108 +2444,6 @@ void main() {
await expectLater(pageLoaded.future, completes);
});
testWidgets('intercept fetch request', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer<String> fetchGetCompleter = Completer<String>();
final Completer<Map<String, dynamic>> fetchPostCompleter =
Completer<Map<String, dynamic>>();
final Completer<void> shouldInterceptFetchPostRequestCompleter =
Completer<void>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialData: InAppWebViewInitialData(data: """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>InAppWebViewFetchTest</title>
</head>
<body>
<h1>InAppWebViewFetchTest</h1>
<script>
window.addEventListener('flutterInAppWebViewPlatformReady', function(event) {
fetch(new Request("http://${environment["NODE_SERVER_IP"]}:8082/test-download-file")).then(function(response) {
window.flutter_inappwebview.callHandler('fetchGet', response.status);
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchGet', "ERROR: " + error);
});
fetch("http://${environment["NODE_SERVER_IP"]}:8082/test-ajax-post", {
method: 'POST',
body: JSON.stringify({
firstname: 'Foo',
lastname: 'Bar'
}),
headers: {
'Content-Type': 'application/json'
}
}).then(function(response) {
response.json().then(function(value) {
window.flutter_inappwebview.callHandler('fetchPost', value);
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
}).catch(function(error) {
window.flutter_inappwebview.callHandler('fetchPost', "ERROR: " + error);
});
});
</script>
</body>
</html>
"""),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
clearCache: true,
useShouldInterceptFetchRequest: true,
)),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
controller.addJavaScriptHandler(
handlerName: "fetchGet",
callback: (args) {
fetchGetCompleter.complete(args[0].toString());
});
controller.addJavaScriptHandler(
handlerName: "fetchPost",
callback: (args) {
fetchPostCompleter
.complete(args[0] as Map<String, dynamic>);
});
},
shouldInterceptFetchRequest: (controller, fetchRequest) async {
if (fetchRequest.url!.toString().endsWith("/test-ajax-post")) {
fetchRequest.body = utf8.encode("""{
"firstname": "Foo2",
"lastname": "Bar2"
}
""") as Uint8List;
shouldInterceptFetchPostRequestCompleter.complete();
}
return fetchRequest;
},
),
),
);
var fetchGetCompleterValue = await fetchGetCompleter.future;
expect(fetchGetCompleterValue, '200');
await shouldInterceptFetchPostRequestCompleter.future;
var fetchPostCompleterValue = await fetchPostCompleter.future;
expect(
mapEquals(fetchPostCompleterValue,
{'firstname': 'Foo2', 'lastname': 'Bar2'}),
true);
});
testWidgets('Http Auth Credential Database', (WidgetTester tester) async {
HttpAuthCredentialDatabase httpAuthCredentialDatabase =
HttpAuthCredentialDatabase.instance();
@ -5001,4 +5509,37 @@ setTimeout(function() {
expect(chromeSafariBrowser.isOpened(), false);
});
});
group('InAppLocalhostServer', () {
final InAppLocalhostServer localhostServer = InAppLocalhostServer();
setUpAll(() async {
localhostServer.start();
});
testWidgets('load asset file', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest:
URLRequest(url: Uri.parse('http://localhost:8080/test_assets/index.html')),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
},
),
),
);
final InAppWebViewController controller =
await controllerCompleter.future;
final String? currentUrl = (await controller.getUrl())?.toString();
expect(currentUrl, 'http://localhost:8080/test_assets/index.html');
});
tearDownAll(() async {
localhostServer.close();
});
});
}

View File

@ -183,16 +183,17 @@ let INTERCEPT_AJAX_REQUEST_JS_SOURCE = """
this.addEventListener('error', handleEvent);
this.addEventListener('abort', handleEvent);
this.addEventListener('timeout', handleEvent);
\(JAVASCRIPT_UTIL_VAR_NAME).convertBodyRequest(data).then(function(data) {
var ajaxRequest = {
data: data,
method: this._flutter_inappwebview_method,
url: this._flutter_inappwebview_url,
isAsync: this._flutter_inappwebview_isAsync,
user: this._flutter_inappwebview_user,
password: this._flutter_inappwebview_password,
withCredentials: this.withCredentials,
headers: this._flutter_inappwebview_request_headers,
responseType: this.responseType
method: self._flutter_inappwebview_method,
url: self._flutter_inappwebview_url,
isAsync: self._flutter_inappwebview_isAsync,
user: self._flutter_inappwebview_user,
password: self._flutter_inappwebview_password,
withCredentials: self.withCredentials,
headers: self._flutter_inappwebview_request_headers,
responseType: self.responseType
};
window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) {
if (result != null) {
@ -201,11 +202,27 @@ let INTERCEPT_AJAX_REQUEST_JS_SOURCE = """
self.abort();
return;
};
if (result.data != null && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.data) && result.data.length > 0) {
var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.data);
if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) {
var formDataContentType = \(JAVASCRIPT_UTIL_VAR_NAME).getFormDataContentType(bodyString);
if (result.headers != null) {
result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type'];
} else {
result.headers = { 'Content-Type': formDataContentType };
}
}
}
if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.data) || result.data == null) {
data = result.data;
} else if (result.data.length > 0) {
data = new Uint8Array(result.data);
}
self.withCredentials = result.withCredentials;
if (result.responseType != null) {
self.responseType = result.responseType;
};
if (result.headers != null) {
for (var header in result.headers) {
var value = result.headers[header];
var flutter_inappwebview_value = self._flutter_inappwebview_request_headers[header];
@ -216,6 +233,7 @@ let INTERCEPT_AJAX_REQUEST_JS_SOURCE = """
}
setRequestHeader.call(self, header, value);
};
}
if ((self._flutter_inappwebview_method != result.method && result.method != null) || (self._flutter_inappwebview_url != result.url && result.url != null)) {
self.abort();
self.open(result.method, result.url, result.isAsync, result.user, result.password);
@ -224,6 +242,7 @@ let INTERCEPT_AJAX_REQUEST_JS_SOURCE = """
}
send.call(self, data);
});
});
} else {
send.call(this, data);
}

View File

@ -24,68 +24,6 @@ let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """
if (fetch == null) {
return;
}
function convertHeadersToJson(headers) {
var headersObj = {};
for (var header of headers.keys()) {
var value = headers.get(header);
headersObj[header] = value;
}
return headersObj;
}
function convertJsonToHeaders(headersJson) {
return new Headers(headersJson);
}
function convertBodyToArray(body) {
return new Response(body).arrayBuffer().then(function(arrayBuffer) {
var arr = Array.from(new Uint8Array(arrayBuffer));
return arr;
})
}
function convertArrayIntBodyToUint8Array(arrayIntBody) {
return new Uint8Array(arrayIntBody);
}
function convertCredentialsToJson(credentials) {
var credentialsObj = {};
if (window.FederatedCredential != null && credentials instanceof FederatedCredential) {
credentialsObj.type = credentials.type;
credentialsObj.id = credentials.id;
credentialsObj.name = credentials.name;
credentialsObj.protocol = credentials.protocol;
credentialsObj.provider = credentials.provider;
credentialsObj.iconURL = credentials.iconURL;
} else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) {
credentialsObj.type = credentials.type;
credentialsObj.id = credentials.id;
credentialsObj.name = credentials.name;
credentialsObj.password = credentials.password;
credentialsObj.iconURL = credentials.iconURL;
} else {
credentialsObj.type = 'default';
credentialsObj.value = credentials;
}
}
function convertJsonToCredential(credentialsJson) {
var credentials;
if (window.FederatedCredential != null && credentialsJson.type === 'federated') {
credentials = new FederatedCredential({
id: credentialsJson.id,
name: credentialsJson.name,
protocol: credentialsJson.protocol,
provider: credentialsJson.provider,
iconURL: credentialsJson.iconURL
});
} else if (window.PasswordCredential != null && credentialsJson.type === 'password') {
credentials = new PasswordCredential({
id: credentialsJson.id,
name: credentialsJson.name,
password: credentialsJson.password,
iconURL: credentialsJson.iconURL
});
} else {
credentials = credentialsJson;
}
return credentials;
}
window.fetch = async function(resource, init) {
if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == true) {
var fetchRequest = {
@ -116,7 +54,7 @@ let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """
fetchRequest.integrity = resource.integrity;
fetchRequest.keepalive = resource.keepalive;
} else {
fetchRequest.url = resource;
fetchRequest.url = resource != null ? resource.toString() : null;
if (init != null) {
fetchRequest.method = init.method;
fetchRequest.headers = init.headers;
@ -132,10 +70,10 @@ let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """
}
}
if (fetchRequest.headers instanceof Headers) {
fetchRequest.headers = convertHeadersToJson(fetchRequest.headers);
fetchRequest.headers = \(JAVASCRIPT_UTIL_VAR_NAME).convertHeadersToJson(fetchRequest.headers);
}
fetchRequest.credentials = convertCredentialsToJson(fetchRequest.credentials);
return convertBodyToArray(fetchRequest.body).then(function(body) {
fetchRequest.credentials = \(JAVASCRIPT_UTIL_VAR_NAME).convertCredentialsToJson(fetchRequest.credentials);
return \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyRequest(fetchRequest.body).then(function(body) {
fetchRequest.body = body;
return window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) {
if (result != null) {
@ -152,7 +90,18 @@ let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """
controller.abort();
break;
}
resource = (result.url != null) ? result.url : resource;
if (result.body != null && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.body) && result.body.length > 0) {
var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.body);
if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) {
var formDataContentType = \(JAVASCRIPT_UTIL_VAR_NAME).getFormDataContentType(bodyString);
if (result.headers != null) {
result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type'];
} else {
result.headers = { 'Content-Type': formDataContentType };
}
}
}
resource = result.url;
if (init == null) {
init = {};
}
@ -160,16 +109,18 @@ let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """
init.method = result.method;
}
if (result.headers != null && Object.keys(result.headers).length > 0) {
init.headers = convertJsonToHeaders(result.headers);
init.headers = \(JAVASCRIPT_UTIL_VAR_NAME).convertJsonToHeaders(result.headers);
}
if (result.body != null && result.body.length > 0) {
init.body = convertArrayIntBodyToUint8Array(result.body);
if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.body) || result.body == null) {
init.body = result.body;
} else if (result.body.length > 0) {
init.body = new Uint8Array(result.body);
}
if (result.mode != null && result.mode.length > 0) {
init.mode = result.mode;
}
if (result.credentials != null) {
init.credentials = convertJsonToCredential(result.credentials);
init.credentials = \(JAVASCRIPT_UTIL_VAR_NAME).convertJsonToCredential(result.credentials);
}
if (result.cache != null && result.cache.length > 0) {
init.cache = result.cache;

View File

@ -30,6 +30,213 @@ window.\(JAVASCRIPT_BRIDGE_NAME).callHandler = function() {
});
};
\(WEB_MESSAGE_LISTENER_JS_SOURCE)
\(UTIL_JS_SOURCE)
"""
let PLATFORM_READY_JS_SOURCE = "window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));";
let JAVASCRIPT_UTIL_VAR_NAME = "window.\(JAVASCRIPT_BRIDGE_NAME)._Util"
/*
https://github.com/github/fetch/blob/master/fetch.js
*/
let UTIL_JS_SOURCE = """
\(JAVASCRIPT_UTIL_VAR_NAME) = {
support: {
searchParams: 'URLSearchParams' in window,
iterable: 'Symbol' in window && 'iterator' in Symbol,
blob:
'FileReader' in window &&
'Blob' in window &&
(function() {
try {
new Blob();
return true;
} catch (e) {
return false;
}
})(),
formData: 'FormData' in window,
arrayBuffer: 'ArrayBuffer' in window
},
isDataView: function(obj) {
return obj && DataView.prototype.isPrototypeOf(obj);
},
fileReaderReady: function(reader) {
return new Promise(function(resolve, reject) {
reader.onload = function() {
resolve(reader.result);
};
reader.onerror = function() {
reject(reader.error);
};
});
},
readBlobAsArrayBuffer: function(blob) {
var reader = new FileReader();
var promise = \(JAVASCRIPT_UTIL_VAR_NAME).fileReaderReady(reader);
reader.readAsArrayBuffer(blob);
return promise;
},
convertBodyToArrayBuffer: function(body) {
var viewClasses = [
'[object Int8Array]',
'[object Uint8Array]',
'[object Uint8ClampedArray]',
'[object Int16Array]',
'[object Uint16Array]',
'[object Int32Array]',
'[object Uint32Array]',
'[object Float32Array]',
'[object Float64Array]'
];
var isArrayBufferView = null;
if (\(JAVASCRIPT_UTIL_VAR_NAME).support.arrayBuffer) {
isArrayBufferView =
ArrayBuffer.isView ||
function(obj) {
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1;
};
}
var bodyUsed = false;
this._bodyInit = body;
if (!body) {
this._bodyText = '';
} else if (typeof body === 'string') {
this._bodyText = body;
} else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body;
} else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body;
} else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this._bodyText = body.toString();
} else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.arrayBuffer && \(JAVASCRIPT_UTIL_VAR_NAME).support.blob && \(JAVASCRIPT_UTIL_VAR_NAME).isDataView(body)) {
this._bodyArrayBuffer = bufferClone(body.buffer);
this._bodyInit = new Blob([this._bodyArrayBuffer]);
} else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
this._bodyArrayBuffer = bufferClone(body);
} else {
this._bodyText = body = Object.prototype.toString.call(body);
}
this.blob = function () {
if (bodyUsed) {
return Promise.reject(new TypeError('Already read'));
}
bodyUsed = true;
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob);
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]));
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob');
} else {
return Promise.resolve(new Blob([this._bodyText]));
}
};
if (this._bodyArrayBuffer) {
if (bodyUsed) {
return Promise.reject(new TypeError('Already read'));
}
bodyUsed = true;
if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
return Promise.resolve(
this._bodyArrayBuffer.buffer.slice(
this._bodyArrayBuffer.byteOffset,
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
)
);
} else {
return Promise.resolve(this._bodyArrayBuffer);
}
}
return this.blob().then(\(JAVASCRIPT_UTIL_VAR_NAME).readBlobAsArrayBuffer);
},
isString: function(variable) {
return typeof variable === 'string' || variable instanceof String;
},
convertBodyRequest: function(body) {
if (body == null) {
return new Promise((resolve, reject) => resolve(null));
}
if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(body) || (\(JAVASCRIPT_UTIL_VAR_NAME).support.searchParams && body instanceof URLSearchParams)) {
return new Promise((resolve, reject) => resolve(body.toString()));
}
if (window.Response != null) {
return new Response(body).arrayBuffer().then(function(arrayBuffer) {
return Array.from(new Uint8Array(arrayBuffer));
});
}
return \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyToArrayBuffer(body).then(function(arrayBuffer) {
return Array.from(new Uint8Array(arrayBuffer));
});
},
arrayBufferToString: function(arrayBuffer) {
return String.fromCharCode.apply(String, arrayBuffer);
},
isBodyFormData: function(bodyString) {
return bodyString.indexOf('------WebKitFormBoundary') >= 0;
},
getFormDataContentType: function(bodyString) {
var boundary = bodyString.substr(2, 40);
return 'multipart/form-data; boundary=' + boundary;
},
convertHeadersToJson: function(headers) {
var headersObj = {};
for (var header of headers.keys()) {
var value = headers.get(header);
headersObj[header] = value;
}
return headersObj;
},
convertJsonToHeaders: function(headersJson) {
return new Headers(headersJson);
},
convertCredentialsToJson: function(credentials) {
var credentialsObj = {};
if (window.FederatedCredential != null && credentials instanceof FederatedCredential) {
credentialsObj.type = credentials.type;
credentialsObj.id = credentials.id;
credentialsObj.name = credentials.name;
credentialsObj.protocol = credentials.protocol;
credentialsObj.provider = credentials.provider;
credentialsObj.iconURL = credentials.iconURL;
} else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) {
credentialsObj.type = credentials.type;
credentialsObj.id = credentials.id;
credentialsObj.name = credentials.name;
credentialsObj.password = credentials.password;
credentialsObj.iconURL = credentials.iconURL;
} else {
credentialsObj.type = 'default';
credentialsObj.value = credentials;
}
return credentialsObj;
},
convertJsonToCredential: function(credentialsJson) {
var credentials;
if (window.FederatedCredential != null && credentialsJson.type === 'federated') {
credentials = new FederatedCredential({
id: credentialsJson.id,
name: credentialsJson.name,
protocol: credentialsJson.protocol,
provider: credentialsJson.provider,
iconURL: credentialsJson.iconURL
});
} else if (window.PasswordCredential != null && credentialsJson.type === 'password') {
credentials = new PasswordCredential({
id: credentialsJson.id,
name: credentialsJson.name,
password: credentialsJson.password,
iconURL: credentialsJson.iconURL
});
} else {
credentials = credentialsJson.value == null ? undefined : credentialsJson.value;
}
return credentials;
}
};
"""

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart' show rootBundle;
@ -39,7 +40,8 @@ class InAppLocalhostServer {
this._server = server;
server.listen((HttpRequest request) async {
var body = [] as List<int>;
Uint8List body = Uint8List(0);
var path = request.requestedUri.path;
path = (path.startsWith('/')) ? path.substring(1) : path;
path += (path.endsWith('/')) ? 'index.html' : '';

View File

@ -2873,7 +2873,7 @@ class AjaxRequestHeaders {
///Class that represents a JavaScript [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) object.
class AjaxRequest {
///Data passed as a parameter to the `XMLHttpRequest.send()` method.
dynamic data;
dynamic? data;
///The HTTP request method of the `XMLHttpRequest` request.
String? method;
@ -3225,7 +3225,7 @@ class FetchRequest {
Map<String, dynamic>? headers;
///Body of the request.
Uint8List? body;
dynamic? body;
///The mode used by the request.
String? mode;
@ -3291,9 +3291,7 @@ class FetchRequest {
url: map["url"] != null ? Uri.parse(map["url"]) : null,
method: map["method"],
headers: map["headers"]?.cast<String, dynamic>(),
body: map["body"] != null
? Uint8List.fromList(map["body"].cast<int>())
: null,
body: map["body"],
mode: map["mode"],
credentials: credentials,
cache: map["cache"],

View File

@ -9,6 +9,8 @@ const appHttps = express()
const appAuthBasic = express()
const fs = require('fs')
const path = require('path')
const bodyParser = require('body-parser');
const multiparty = require('multiparty');
var options = {
key: fs.readFileSync('server-key.pem'),
@ -117,14 +119,11 @@ appAuthBasic.get('/test-index', (req, res) => {
appAuthBasic.listen(8081);
// Parse URL-encoded bodies (as sent by HTML forms)
app.use(express.urlencoded());
app.use(cors());
app.use(bodyParser.urlencoded({extended: false}));
// Parse JSON bodies (as sent by API clients)
app.use(express.json());
app.use(bodyParser.json());
app.use(express.static(__dirname + '/public'));
@ -162,14 +161,27 @@ app.post("/test-post", (req, res) => {
})
app.post("/test-ajax-post", (req, res) => {
console.log(JSON.stringify(req.headers))
console.log(JSON.stringify(req.body))
console.log(JSON.stringify(req.headers));
if (req.headers["content-type"].indexOf("multipart/form-data;") === 0) {
const form = new multiparty.Form();
form.parse(req, function(err, fields, files) {
console.log(fields);
res.set("Content-Type", "application/json")
res.send(JSON.stringify({
"firstname": fields.firstname[0],
"lastname": fields.lastname[0],
}));
res.end();
});
} else {
console.log(req.body);
res.set("Content-Type", "application/json")
res.send(JSON.stringify({
"firstname": req.body.firstname,
"lastname": req.body.lastname,
}))
res.end()
}));
res.end();
}
})
app.get("/test-download-file", (req, res) => {

View File

@ -252,6 +252,45 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multiparty": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
"requires": {
"http-errors": "~1.8.0",
"safe-buffer": "5.2.1",
"uid-safe": "2.1.5"
},
"dependencies": {
"http-errors": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
}
}
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
@ -294,6 +333,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -382,6 +426,14 @@
"mime-types": "~2.1.24"
}
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View File

@ -10,8 +10,10 @@
"license": "ISC",
"dependencies": {
"basic-auth": "latest",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"express": "latest",
"https": "latest"
"https": "latest",
"multiparty": "^4.2.2"
}
}