Skip to content

WebView URL filtering #3442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open

WebView URL filtering #3442

wants to merge 5 commits into from

Conversation

t-arn
Copy link
Contributor

@t-arn t-arn commented May 13, 2025

This PR aims to implement URL filtering for WebView on Windows and Android.
Possible use cases are:

  • preventing users from navigating off the site intended to be handled by your application's WebView
  • preventing users from navigating to known malicious web sites

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

t-arn added 4 commits May 13, 2025 07:34
* implented on_navigation_started for android
* extended the webview example
* applied black
Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

Thanks for the PR; I've flagged a few issues inline, and the test suite is obviously failing on most platforms (although it looks like the reason for the failure is something that won't be an issue once you've addressed one of the comments inline).

@Override(jboolean, [A_WebView, WebResourceRequest])
def shouldOverrideUrlLoading(self, webview, webresourcerequest):
if self.webview_impl.interface.on_navigation_starting:
event = TogaNavigationEvent(webresourcerequest)
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little confused about what is going on here. TogaNavigationEvent is a Python class, wrapping a request and a cancel attribute. It's never assigned to anything, or passed as an argument to anything - then it's cleared. Why is it required at all?

@@ -86,3 +117,7 @@ def evaluate_javascript(self, javascript, on_result=None):

self.native.evaluateJavascript(javascript, ReceiveString(result))
return result

def set_on_navigation_starting(self, handler):
Copy link
Member

Choose a reason for hiding this comment

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

At this point, I'm pretty sure we've removed all the "set_on_..." methods; unless there's a specific use case (and it doesn't look like there is), this can be removed.

@@ -33,6 +33,7 @@ def __init__(
url: str | None = None,
content: str | None = None,
user_agent: str | None = None,
on_navigation_starting=None,
Copy link
Member

Choose a reason for hiding this comment

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

This needs a type declaration (if only for documentation purposes)

@@ -53,6 +53,10 @@ build_gradle_dependencies = [
"com.google.android.material:material:1.12.0",
]

build_gradle_extra_content="""
chaquopy.defaultConfig.staticProxy("toga_android.widgets.webview")
"""
Copy link
Member

Choose a reason for hiding this comment

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

Hrm... Requiring users to manually inject an "extra content" block to make a feature work is a problematic requirement. I understand why it's needed - but we possibly need to solve the bigger problem of allowing toga to declare a list of "classes that need a static proxy".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. But this probably means that this PR cannot be merged any time soon, right?

Copy link
Member

Choose a reason for hiding this comment

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

It depends how essential the change is.

If a WebView will continue to work on Android, but this feature won't work without the extra content declaration, we can probably merge it. We will need to add a set of platform notes in the docs, and we'll need to prioritise finding a better fix - but I don't think it needs to be a complete blocker.

However, if Webview (or the whole Android backend) won't work at all if this addition isn't in place, then we'll need to solve that problem first.

allow = False
message = f"Navigation not allowed to: {url}"
dialog = toga.InfoDialog("on_navigation_starting()", message)
asyncio.create_task(self.dialog(dialog))
Copy link
Member

Choose a reason for hiding this comment

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

It might be easier to make on_navigation_starting an async callback, and then await the response.

event = TogaNavigationEvent(webresourcerequest)
allow = self.webview_impl.interface.on_navigation_starting(
webresourcerequest.getUrl().toString()
)
Copy link
Member

Choose a reason for hiding this comment

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

I haven't checked, but this could be a problem if on_navigation_starting is an async callback, because the returned value will be a future.

@@ -92,6 +106,9 @@ def startup(self):
on_webview_load=self.on_webview_load,
style=Pack(flex=1),
)
# activate web navigation filtering on supported platforms
if getattr(self.webview._impl, "SUPPORTS_ON_NAVIGATION_STARTING", True):
Copy link
Member

Choose a reason for hiding this comment

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

Good instinct, but it's fine for the example to raise a warning if a feature isn't available.

@@ -136,6 +135,10 @@ def winforms_initialization_completed(self, sender, args):
settings.IsSwipeNavigationEnabled = False
settings.IsZoomControlEnabled = True

self.native.CoreWebView2.NavigationStarting += (
self.winforms_navigation_starting
Copy link
Member

Choose a reason for hiding this comment

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

This almost certainly needs a WeakrefCallable wrapper.

Copy link
Contributor Author

@t-arn t-arn May 13, 2025

Choose a reason for hiding this comment

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

You mean like the winforms_FormClosing in the winforms dialogs module?

Copy link
Member

Choose a reason for hiding this comment

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

Yes - you can see the same handling in NavigationCompleted and CoreWebView2InitializationCompleted in the WebView widget.

def winforms_navigation_starting(self, sender, event):
# print(f"winforms_navigation_starting: {event.Uri}")
if self.interface.on_navigation_starting:
allow = self.interface.on_navigation_starting(event.Uri)
Copy link
Member

Choose a reason for hiding this comment

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

As with the Android version, this needs to handle a Future being returned.

@t-arn
Copy link
Contributor Author

t-arn commented May 13, 2025

@freakboy3742 I wonder if it is possible to make the on_navigation_starting handler async when it is being called by a native event. Aren't those native events always synchronous?

@freakboy3742
Copy link
Member

@freakboy3742 I wonder if it is possible to make the on_navigation_starting handler async when it is being called by a native event. Aren't those native events always synchronous?

Yes, native event handlers are always synchronous - but Toga events must be able to be defined asynchronously. That's why they return a future; we need to make sure that if a future is returned, we wait for that future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants