-
-
Notifications
You must be signed in to change notification settings - Fork 772
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
base: main
Are you sure you want to change the base?
WebView URL filtering #3442
Conversation
* implented on_navigation_started for android * extended the webview example
* applied black
There was a problem hiding this 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the TogaNavigationEvent was a left-over of some old code - I removed it.
@@ -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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed it.
core/src/toga/widgets/webview.py
Outdated
@@ -33,6 +33,7 @@ def __init__( | |||
url: str | None = None, | |||
content: str | None = None, | |||
user_agent: str | None = None, | |||
on_navigation_starting=None, |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, added type declaration
@@ -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") | |||
""" |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example app now has both, a synchronous and an async handler
event = TogaNavigationEvent(webresourcerequest) | ||
allow = self.webview_impl.interface.on_navigation_starting( | ||
webresourcerequest.getUrl().toString() | ||
) |
There was a problem hiding this comment.
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.
examples/webview/webview/app.py
Outdated
@@ -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): |
There was a problem hiding this comment.
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.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not quite sure, how to do this. Is there a similar case in the code base where I could get some inspiration?
@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. |
* added type declaration for handler * added WeakrefCallable wrapper
…_navigation_starting handlers
@freakboy3742 I implemented support for synchronous and asynchronous on_navigation_starting handlers for Windows. The code still contains a lot of print statements for easier debugging. If you approve of this approach, I'll implement something similar for Android and clean up the code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the approach you've taken here.
Firstly, I'm not sure why the cached "allowed" and "not allowed" URL lists is required. It seems to undermine the ability to make per-request based decisions on whether a navigation is required, which is the point of having a callback.
Secondly, the general approach of "if async, cancel the request, and force a URL load if the async result succeeds" makes sense, but the way it gets to the actual resolution seems... convoluted.
wrapped_handler
takes a function or co-routine, and guarantees that it will be invoked on the event loop. It already has a an existing cleanup
method that can be used to attach "on completion" logic. There's no need to have a whole standalone "attach completion callback to result" handler - it can be baked into the cleanup
method when the handler is installed.
@freakboy3742 The "allowed" and "not allowed" lists are meant to cache the user's decision whether to allow the URL or not. I think, the user will not want to be asked for the same URL again and again. As for setting the callback method: I didn't realize that wrapped_handler already has the "on completion" functionality. I'll rewrite the code and use the cleanup method. |
We could make it configurable whether the user's decisions should be saved, for example by setting WebView.save_user_url_permissions=True before setting the on_navigation_starting handler. What do you think? Should the default be True or False? |
I see no practical use for that API. A method that returns If the intention is to allow user interaction on specific URLs, and cache answers - that's a decision the developer can make if that feature makes sense to have in an app. If nothing else, any caching scheme like that would need to have stored user preferences, a user-clearable cache, and more - so having it baked into Toga seems like massive overreach. |
@freakboy3742 I refactored and simplified the Winforms code by using the cleanup method of wrapped_handler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks a lot better. There's obviously still a lot of debug to clean up, and I've flagged one way to clean up the cleanup
handler even more - but the bones of this are lot closer to something I can see being merged.
The one big outstanding question is the need for the staticProxy on WebView, and whether there's any way to make this opt-in if you want navigation callbacks.
core/src/toga/widgets/webview.py
Outdated
|
||
def on_navigation_starting_callback(self, widget, result): | ||
try: | ||
url = widget._requested_url |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure this wouldn't be needed if this callback method was an inner method inside on_navigation_starting
. That way, it could; be a closure over the url
argument. See how Window.on_close
is implemented - that inner cleanup
method is a closure over handler
.
@freakboy3742 I defined on_navigation_starting cleanup method as an inner method, but the url parameter is always None inside cleanup. Is there something wrong with how I changed the code? The webview._requested_url is still there and set, but not used anymore. I will remove it when the url parameter is working, but currently I don't see how to make it work without webview._requested_url |
Ah - on closer inspection - the So - I suspect what is needed here is a modification to |
This PR aims to implement URL filtering for WebView on Windows and Android.
Possible use cases are:
PR Checklist: