Skip to content
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

Cookie retrieval #3068

Merged
merged 18 commits into from
Jan 8, 2025
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
13 changes: 12 additions & 1 deletion android/src/toga_android/widgets/webview.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import json
from http.cookiejar import CookieJar

from android.webkit import ValueCallback, WebView as A_WebView, WebViewClient
from java import dynamic_proxy

from toga.widgets.webview import JavaScriptResult
from toga.widgets.webview import CookiesResult, JavaScriptResult

from .base import Widget

Expand Down Expand Up @@ -70,6 +71,16 @@ def set_user_agent(self, value):
self.default_user_agent if value is None else value
)

def get_cookies(self):
# Create the result object
result = CookiesResult()
result.set_result(CookieJar())

# Signal that this feature is not implemented on the current platform
self.interface.factory.not_implemented("webview.cookies")

return result

def evaluate_javascript(self, javascript, on_result=None):
result = JavaScriptResult(on_result)

Expand Down
7 changes: 7 additions & 0 deletions android/tests_backend/widgets/webview.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from http.cookiejar import CookieJar

from android.webkit import WebView
from pytest import skip

from .base import SimpleProbe

Expand All @@ -8,3 +11,7 @@ class WebViewProbe(SimpleProbe):
content_supports_url = False
javascript_supports_exception = False
supports_on_load = False

def extract_cookie(self, cookie_jar, name):
assert isinstance(cookie_jar, CookieJar)
skip("Cookie retrieval not implemented on Android")
1 change: 1 addition & 0 deletions changes/3068.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Webviews now support the retrieval of cookies.
57 changes: 56 additions & 1 deletion cocoa/src/toga_cocoa/widgets/webview.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from http.cookiejar import Cookie, CookieJar

from rubicon.objc import objc_id, objc_method, objc_property, py_from_ns
from travertino.size import at_least

from toga.widgets.webview import JavaScriptResult
from toga.widgets.webview import CookiesResult, JavaScriptResult

from ..libs import NSURL, NSURLRequest, WKWebView
from .base import Widget
Expand All @@ -19,6 +21,42 @@ def _completion_handler(res: objc_id, error: objc_id) -> None:
return _completion_handler


def cookies_completion_handler(result):
def _completion_handler(cookies: objc_id) -> None:
# Convert cookies from Objective-C to Python objects
cookies_array = py_from_ns(cookies)

# Initialize a CookieJar
cookie_jar = CookieJar()

# Add each cookie from the array into the CookieJar
for cookie in cookies_array:
cookie_obj = Cookie(
version=0,
name=str(cookie.name),
value=str(cookie.value),
port=None,
port_specified=False,
domain=str(cookie.domain),
domain_specified=True,
domain_initial_dot=False,
path=str(cookie.path),
path_specified=True,
secure=bool(cookie.Secure),
expires=None,
discard=bool(cookie.isSessionOnly()),
comment=None,
comment_url=None,
rest={},
)
cookie_jar.set_cookie(cookie_obj)

# Set the result in the AsyncResult
result.set_result(cookie_jar)

return _completion_handler


class TogaWebView(WKWebView):
interface = objc_property(object, weak=True)
impl = objc_property(object, weak=True)
Expand Down Expand Up @@ -78,6 +116,23 @@ def get_user_agent(self):
def set_user_agent(self, value):
self.native.customUserAgent = value

def get_cookies(self):
"""
Retrieve all cookies asynchronously from the WebView.

:returns: An AsyncResult object that can be awaited.
"""
# Create an AsyncResult to manage the cookies
result = CookiesResult()

# Retrieve the cookie store from the WebView
cookie_store = self.native.configuration.websiteDataStore.httpCookieStore

# Call the method to retrieve all cookies and pass the completion handler
cookie_store.getAllCookies(cookies_completion_handler(result))

return result

def evaluate_javascript(self, javascript: str, on_result=None) -> str:
result = JavaScriptResult(on_result=on_result)
self.native.evaluateJavaScript(
Expand Down
3 changes: 3 additions & 0 deletions cocoa/tests_backend/widgets/webview.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ class WebViewProbe(SimpleProbe):
content_supports_url = True
javascript_supports_exception = True
supports_on_load = True

def extract_cookie(self, cookie_jar, name):
return next((c for c in cookie_jar if c.name == name), None)
17 changes: 17 additions & 0 deletions core/src/toga/widgets/webview.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class JavaScriptResult(AsyncResult):
RESULT_TYPE = "JavaScript"


class CookiesResult(AsyncResult):
RESULT_TYPE = "Cookies"


class OnWebViewLoadHandler(Protocol):
def __call__(self, widget: WebView, **kwargs: Any) -> object:
"""A handler to invoke when the WebView is loaded.
Expand Down Expand Up @@ -135,6 +139,19 @@ def set_content(self, root_url: str, content: str) -> None:
"""
self._impl.set_content(root_url, content)

@property
def cookies(self) -> CookiesResult:
"""Retrieve cookies from the WebView.

**This is an asynchronous property**. The value returned by this method must be
awaited to obtain the cookies that are currently set.

**Note:** This property is not currently supported on Android or Linux.

:returns: An object that returns a CookieJar when awaited.
"""
return self._impl.get_cookies()

def evaluate_javascript(
self,
javascript: str,
Expand Down
49 changes: 49 additions & 0 deletions core/tests/widgets/test_webview.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
from http.cookiejar import Cookie, CookieJar
from unittest.mock import Mock

import pytest
Expand Down Expand Up @@ -248,3 +249,51 @@ async def delayed_page_load():

# The async handler was invoked
on_result_handler.assert_called_once_with(42)


async def test_retrieve_cookies(widget):
"""Cookies can be retrieved."""

# Simulate backend cookie retrieval
cookies = [
Cookie(
version=0,
name="test",
value="test_value",
port=None,
port_specified=False,
domain="example.com",
domain_specified=True,
domain_initial_dot=False,
path="/",
path_specified=True,
secure=True,
expires=None, # Simulating a session cookie
discard=True,
comment=None,
comment_url=None,
rest={},
rfc2109=False,
)
]

async def delayed_cookie_retrieval():
await asyncio.sleep(0.1)
widget._impl.simulate_cookie_retrieval(cookies)

asyncio.create_task(delayed_cookie_retrieval())

# Get the cookie jar from the future
cookie_jar = await widget.cookies

# The result returned is a cookiejar
assert isinstance(cookie_jar, CookieJar)

# Validate the cookies in the CookieJar
Copy link
Member

Choose a reason for hiding this comment

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

This should also validate that the result is a cookiejar, not just that it is iterable.

cookie = next(iter(cookie_jar)) # Get the first (and only) cookie
assert cookie.name == "test"
assert cookie.value == "test_value"
assert cookie.domain == "example.com"
assert cookie.path == "/"
assert cookie.secure is True
assert cookie.expires is None
16 changes: 15 additions & 1 deletion dummy/src/toga_dummy/widgets/webview.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from toga.widgets.webview import JavaScriptResult
from http.cookiejar import CookieJar

from toga.widgets.webview import CookiesResult, JavaScriptResult

from .base import Widget

Expand All @@ -24,6 +26,11 @@ def set_url(self, value, future=None):
self._set_value("url", value)
self._set_value("loaded_future", future)

def get_cookies(self):
self._action("cookies")
self._cookie_result = CookiesResult()
return self._cookie_result

def evaluate_javascript(self, javascript, on_result=None):
self._action("evaluate_javascript", javascript=javascript)
self._js_result = JavaScriptResult(on_result)
Expand All @@ -39,3 +46,10 @@ def simulate_page_loaded(self):

def simulate_javascript_result(self, value):
self._js_result.set_result(42)

def simulate_cookie_retrieval(self, cookies):
"""Simulate completion of cookie retrieval."""
kloknibor marked this conversation as resolved.
Show resolved Hide resolved
cookie_jar = CookieJar()
for cookie in cookies:
cookie_jar.set_cookie(cookie)
self._cookie_result.set_result(cookie_jar)
14 changes: 13 additions & 1 deletion gtk/src/toga_gtk/widgets/webview.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from http.cookiejar import CookieJar

from travertino.size import at_least

from toga.widgets.webview import JavaScriptResult
from toga.widgets.webview import CookiesResult, JavaScriptResult

from ..libs import GLib, WebKit2
from .base import Widget
Expand Down Expand Up @@ -75,6 +77,16 @@ def set_user_agent(self, value):
def set_content(self, root_url, content):
self.native.load_html(content, root_url)

def get_cookies(self):
# Create the result object
result = CookiesResult()
result.set_result(CookieJar())

# Signal that this feature is not implemented on the current platform
self.interface.factory.not_implemented("webview.cookies")

return result

def evaluate_javascript(self, javascript, on_result=None):
# Construct a future on the event loop
result = JavaScriptResult(on_result)
Expand Down
8 changes: 8 additions & 0 deletions gtk/tests_backend/widgets/webview.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from http.cookiejar import CookieJar

from pytest import skip

from toga_gtk.libs import WebKit2

from .base import SimpleProbe
Expand All @@ -8,3 +12,7 @@ class WebViewProbe(SimpleProbe):
content_supports_url = True
javascript_supports_exception = True
supports_on_load = True

def extract_cookie(self, cookie_jar, name):
assert isinstance(cookie_jar, CookieJar)
skip("Cookie retrieval not implemented on GTK")
58 changes: 57 additions & 1 deletion iOS/src/toga_iOS/widgets/webview.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from http.cookiejar import Cookie, CookieJar

from rubicon.objc import objc_id, objc_method, objc_property, py_from_ns
from travertino.size import at_least

from toga.widgets.webview import JavaScriptResult
from toga.widgets.webview import CookiesResult, JavaScriptResult
from toga_iOS.libs import NSURL, NSURLRequest, WKWebView
from toga_iOS.widgets.base import Widget

Expand All @@ -18,6 +20,43 @@ def _completion_handler(res: objc_id, error: objc_id) -> None:
return _completion_handler


def cookies_completion_handler(result):
def _completion_handler(cookies: objc_id) -> None:

# Convert cookies from Objective-C to Python objects
cookies_array = py_from_ns(cookies)

# Initialize a CookieJar
cookie_jar = CookieJar()

# Add each cookie from the array into the CookieJar
for cookie in cookies_array:
cookie_obj = Cookie(
version=0,
name=str(cookie.name),
value=str(cookie.value),
port=None,
port_specified=False,
domain=str(cookie.domain),
domain_specified=True,
domain_initial_dot=False,
path=str(cookie.path),
path_specified=True,
secure=bool(cookie.Secure),
expires=None,
discard=bool(cookie.isSessionOnly()),
comment=None,
comment_url=None,
rest={},
)
cookie_jar.set_cookie(cookie_obj)

# Set the result in the AsyncResult
result.set_result(cookie_jar)

return _completion_handler


class TogaWebView(WKWebView):
interface = objc_property(object, weak=True)
impl = objc_property(object, weak=True)
Expand Down Expand Up @@ -69,6 +108,23 @@ def get_user_agent(self):
def set_user_agent(self, value):
self.native.customUserAgent = value

def get_cookies(self):
"""
Retrieve all cookies asynchronously from the WebView.

:returns: An AsyncResult object that can be awaited.
"""
# Create an AsyncResult to manage the cookies
result = CookiesResult()

# Retrieve the cookie store from the WebView
cookie_store = self.native.configuration.websiteDataStore.httpCookieStore

# Call the method to retrieve all cookies and pass the completion handler
cookie_store.getAllCookies(cookies_completion_handler(result))

return result

def evaluate_javascript(self, javascript, on_result=None):
result = JavaScriptResult(on_result)
self.native.evaluateJavaScript(
Expand Down
3 changes: 3 additions & 0 deletions iOS/tests_backend/widgets/webview.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ def has_focus(self):
# put 2 WKContentViews on the page at once.
current = self.widget.window._impl.native.firstResponder()
return current.objc_class.name == "WKContentView"

def extract_cookie(self, cookie_jar, name):
return next((c for c in cookie_jar if c.name == name), None)
Loading
Loading