Skip to content

Commit

Permalink
Add support for the download attribute on links for Cocoa/MacOS platf…
Browse files Browse the repository at this point in the history
…orm (#1552)

The download attribute wasn't forcing a download on mac, this makes it do that, and respects the suggested filename!

Also add a test but skip it since it needs to be run manually and have user confirmation.
Test fails before change and passes after.
  • Loading branch information
maddyaby authored Dec 27, 2024
1 parent 1ef7feb commit 0424777
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 0 deletions.
70 changes: 70 additions & 0 deletions tests/test_download.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import pytest

import webview

from .util import run_test, assert_js

import time

html = """
<html>
<body>
<button id="start_download" onclick="click_handler()">Click me!</button>
</body>
<script>
function click_handler(){
const a = document.createElement('a'); // Create "a" element
const blob = new Blob(["Hello, World!"], { type: "text/plain" });
const url = URL.createObjectURL(blob); // Create an object URL from blob
a.setAttribute('href', url); // Set "a" element link
a.setAttribute('download', 'test_download.txt');
a.click(); // Start downloading
a.remove();
}
function verify_result(){
return confirm("Did the system prompt you for a download with name test_download.txt or download a file by that name? OK for yes, Cancel for no");
}
function checkFocus() {
return document.hasFocus();
}
</script>
</html>
"""

@pytest.fixture
def window():
return webview.create_window('Download test', html=html)

# skip this test by default since it's a manual test that requires user interaction
@pytest.mark.skip
def test_download_attribute(window):
webview.settings['ALLOW_DOWNLOADS'] = True
run_test(webview, window, download_test)

def get_result():
res = input("Did the system prompt you for a download with name test_download.txt or download a file by that name? Y/N").upper()
return (res == 'Y')

def download_test(window):
# this should not cause the browser to navigate away but instead should trigger a download
# since the type is text the browser should support it and if it navigates to it it will display it instead
window.evaluate_js("document.getElementById('start_download').click();")
time.sleep(0.5) # make sure it has executed

# if it loaded on the page and navigated away then it will fail this
elements = window.dom.get_elements('#start_download')
assert len(elements) == 1

# wait for download prompt to be dismissed and focus to come back to window
while not window.evaluate_js("checkFocus()"):
time.sleep(0.5)

# ask user if the test passed
assert window.evaluate_js("verify_result();")





25 changes: 25 additions & 0 deletions webview/platforms/cocoa.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,23 @@ def userContentController_didReceiveScriptMessage_(self, controller, message):
body['params'] = None
js_bridge_call(self.window, body['funcName'], body['params'], body['id'])

class DownloadDelegate(AppKit.NSObject):
# Download delegate to handle links with download attribute set
def download_decideDestinationUsingResponse_suggestedFilename_completionHandler_(self, download, decideDestinationUsingResponse, suggestedFilename, completionHandler):
save_dlg = AppKit.NSSavePanel.savePanel()
directory = Foundation.NSSearchPathForDirectoriesInDomains(
Foundation.NSDownloadsDirectory,
Foundation.NSUserDomainMask,
True)[0]
save_dlg.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(directory))
save_dlg.setNameFieldStringValue_(suggestedFilename)
if save_dlg.runModal() == AppKit.NSFileHandlingPanelOKButton:
filename = save_dlg.filename()
url = Foundation.NSURL.fileURLWithPath_(filename)
completionHandler(url)
else:
completionHandler(None)

class BrowserDelegate(AppKit.NSObject):

# Display a JavaScript alert panel containing the specified message
Expand Down Expand Up @@ -229,6 +246,11 @@ def webView_decidePolicyForNavigationAction_decisionHandler_(
if not handler.__block_signature__:
handler.__block_signature__ = BrowserView.pyobjc_method_signature(b'v@i')

# Handle links with the download attribute set to recommend a file name
if action.shouldPerformDownload() and webview_settings['ALLOW_DOWNLOADS']:
handler(getattr(WebKit, 'WKNavigationActionPolicyDownload', 2))
return

""" Disable back navigation on pressing the Delete key: """
# Check if the requested navigation action is Back/Forward
if action.navigationType() == getattr(WebKit, 'WKNavigationTypeBackForward', 2):
Expand All @@ -241,6 +263,9 @@ def webView_decidePolicyForNavigationAction_decisionHandler_(
# Normal navigation, allow
handler(getattr(WebKit, 'WKNavigationActionPolicyAllow', 1))

def webView_navigationAction_didBecomeDownload_(self, webview, navigationAction, download):
download.setDelegate_(BrowserView.DownloadDelegate.alloc().init().retain())

def webView_decidePolicyForNavigationResponse_decisionHandler_(self, webview, navigationResponse, decisionHandler):
if navigationResponse.canShowMIMEType():
decisionHandler(WebKit.WKNavigationResponsePolicyAllow)
Expand Down

0 comments on commit 0424777

Please sign in to comment.