Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ Twin Sun, LLC <[email protected]>
Amir Panahandeh <[email protected]>
Daniele Cambi <[email protected]>
Michele Benedetti <[email protected]>
Taskulu LDA <[email protected]>
Taskulu LDA <[email protected]>
LinXunFeng <[email protected]>
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ Maurits van Beusekom <[email protected]>
Antonino Di Natale <[email protected]>
Nick Bradshaw <[email protected]>
The Vinh Luong <[email protected]>
LinXunFeng <[email protected]>
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.13.1

* Fixes `JSON.stringify()` cannot serialize cyclic structures.

## 3.13.0

* Adds `decidePolicyForNavigationResponse` to internal WKNavigationDelegate to support the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,64 @@ Future<void> main() async {
await expectLater(
debugMessageReceived.future, completion('debug:Debug message'));
});

testWidgets('can receive console log messages with cyclic object value',
(WidgetTester tester) async {
const String testPage = '''
<!DOCTYPE html>
<html>
<head>
<title>WebResourceError test</title>
<script type="text/javascript">
function onLoad() {
const obj1 = {
name: "obj1",
};
const obj2 = {
name: "obj2",
obj1: obj1,
};
const obj = {
obj1: obj1,
obj2: obj2,
};
obj.self = obj;
console.log(obj);
}
</script>
</head>
<body onload="onLoad();">
</html>
''';

final Completer<String> debugMessageReceived = Completer<String>();
final PlatformWebViewController controller = PlatformWebViewController(
const PlatformWebViewControllerCreationParams(),
);
unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));

await controller
.setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) {
debugMessageReceived
.complete('${consoleMessage.level.name}:${consoleMessage.message}');
});

await controller.loadHtmlString(testPage);

await tester.pumpWidget(Builder(
builder: (BuildContext context) {
return PlatformWebViewWidget(
PlatformWebViewWidgetCreationParams(controller: controller),
).build(context);
},
));

await expectLater(
debugMessageReceived.future,
completion(
'log:{"obj1":{"name":"obj1"},"obj2":{"name":"obj2","obj1":{"name":"obj1"}}}'),
);
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,33 +630,65 @@ class WebKitWebViewController extends PlatformWebViewController {
}

Future<void> _injectConsoleOverride() {
// Within overrideScript, a series of console output methods such as
// console.log will be rewritten to pass the output content to the Flutter
// end.
//
// These output contents will first be serialized through JSON.stringify(),
// but if the output content contains cyclic objects, it will encounter the
// following error.
// TypeError: JSON.stringify cannot serialize cyclic structures.
// See https://github.com/flutter/flutter/issues/144535.
//
// Considering this is just looking at the logs printed via console.log,
// the cyclic object is not important, so remove it.
// Therefore, the replacer parameter of JSON.stringify() is used and the
// removeCyclicObject method is passed in to solve the error.
const WKUserScript overrideScript = WKUserScript(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment briefly explaining the approach at a high level so readers don't need to reverse-engineer what is being done here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

'''
function log(type, args) {
var message = Object.values(args)
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString())
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
.join(", ");

var log = {
level: type,
message: message
};

window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
}
var _flutter_webview_plugin_overrides = _flutter_webview_plugin_overrides || {
removeCyclicObject: function() {
const traversalStack = [];
return function (k, v) {
if (typeof v !== "object" || v === null) { return v; }
const currentParentObj = this;
while (
traversalStack.length > 0 &&
traversalStack[traversalStack.length - 1] !== currentParentObj
) {
traversalStack.pop();
}
if (traversalStack.includes(v)) { return; }
traversalStack.push(v);
return v;
};
},
log: function (type, args) {
var message = Object.values(args)
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, _flutter_webview_plugin_overrides.removeCyclicObject()) : v.toString())
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
.join(", ");

var log = {
level: type,
message: message
};

window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
}
};

let originalLog = console.log;
let originalInfo = console.info;
let originalWarn = console.warn;
let originalError = console.error;
let originalDebug = console.debug;

console.log = function() { log("log", arguments); originalLog.apply(null, arguments) };
console.info = function() { log("info", arguments); originalInfo.apply(null, arguments) };
console.warn = function() { log("warning", arguments); originalWarn.apply(null, arguments) };
console.error = function() { log("error", arguments); originalError.apply(null, arguments) };
console.debug = function() { log("debug", arguments); originalDebug.apply(null, arguments) };
console.log = function() { _flutter_webview_plugin_overrides.log("log", arguments); originalLog.apply(null, arguments) };
console.info = function() { _flutter_webview_plugin_overrides.log("info", arguments); originalInfo.apply(null, arguments) };
console.warn = function() { _flutter_webview_plugin_overrides.log("warning", arguments); originalWarn.apply(null, arguments) };
console.error = function() { _flutter_webview_plugin_overrides.log("error", arguments); originalError.apply(null, arguments) };
console.debug = function() { _flutter_webview_plugin_overrides.log("debug", arguments); originalDebug.apply(null, arguments) };

window.addEventListener("error", function(e) {
log("error", e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview
description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control.
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
version: 3.13.0
version: 3.13.1

environment:
sdk: ^3.2.3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1378,31 +1378,49 @@ void main() {
expect(overrideConsoleScript.injectionTime,
WKUserScriptInjectionTime.atDocumentStart);
expect(overrideConsoleScript.source, '''
function log(type, args) {
var message = Object.values(args)
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString())
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
.join(", ");

var log = {
level: type,
message: message
};

window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
}
var _flutter_webview_plugin_overrides = _flutter_webview_plugin_overrides || {
removeCyclicObject: function() {
const traversalStack = [];
return function (k, v) {
if (typeof v !== "object" || v === null) { return v; }
const currentParentObj = this;
while (
traversalStack.length > 0 &&
traversalStack[traversalStack.length - 1] !== currentParentObj
) {
traversalStack.pop();
}
if (traversalStack.includes(v)) { return; }
traversalStack.push(v);
return v;
};
},
log: function (type, args) {
var message = Object.values(args)
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, _flutter_webview_plugin_overrides.removeCyclicObject()) : v.toString())
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
.join(", ");

var log = {
level: type,
message: message
};

window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
}
};

let originalLog = console.log;
let originalInfo = console.info;
let originalWarn = console.warn;
let originalError = console.error;
let originalDebug = console.debug;

console.log = function() { log("log", arguments); originalLog.apply(null, arguments) };
console.info = function() { log("info", arguments); originalInfo.apply(null, arguments) };
console.warn = function() { log("warning", arguments); originalWarn.apply(null, arguments) };
console.error = function() { log("error", arguments); originalError.apply(null, arguments) };
console.debug = function() { log("debug", arguments); originalDebug.apply(null, arguments) };
console.log = function() { _flutter_webview_plugin_overrides.log("log", arguments); originalLog.apply(null, arguments) };
console.info = function() { _flutter_webview_plugin_overrides.log("info", arguments); originalInfo.apply(null, arguments) };
console.warn = function() { _flutter_webview_plugin_overrides.log("warning", arguments); originalWarn.apply(null, arguments) };
console.error = function() { _flutter_webview_plugin_overrides.log("error", arguments); originalError.apply(null, arguments) };
console.debug = function() { _flutter_webview_plugin_overrides.log("debug", arguments); originalDebug.apply(null, arguments) };

window.addEventListener("error", function(e) {
log("error", e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno);
Expand Down