Last active
April 4, 2023 11:08
-
-
Save sventschui/342fa43610b3dca52f424055ee2e4ccf to your computer and use it in GitHub Desktop.
JS based download in WKWebView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { | |
if (openInDocumentPreview(navigationAction.request.url!)) { | |
decisionHandler(.cancel) | |
// TODO: Add more supported mime-types for missing content-disposition headers | |
webView.evaluateJavaScript(""" | |
(async function download() { | |
const url = '\(navigationAction.request.url!.absoluteString)'; | |
try { | |
// we use a second try block here to have more detailed error information | |
// because of the nature of JS the outer try-catch doesn't know anything where the error happended | |
let res; | |
try { | |
res = await fetch(url, { | |
credentials: 'include' | |
}); | |
} catch (err) { | |
window.webkit.messageHandlers.jsError.postMessage( | |
`fetch threw, error: ${err}, url: ${url}` | |
); | |
return; | |
} | |
if (!res.ok) { | |
window.webkit.messageHandlers.jsError.postMessage( | |
`Response status was not ok, status: ${res.status}, url: ${url}` | |
); | |
return; | |
} | |
const contentDisp = res.headers.get('content-disposition'); | |
if (contentDisp) { | |
const match = contentDisp.match(/(^;|)\\s*filename=\\s*(\"([^\"]*)\"|([^;\\s]*))\\s*(;|$)/i); | |
if (match) { | |
filename = match[3] || match[4]; | |
} else { | |
// TODO: we could here guess the filename from the mime-type (e.g. unnamed.pdf for pdfs, or unnamed.tiff for tiffs) | |
window.webkit.messageHandlers.jsError.postMessage( | |
`content-disposition header could not be matched against regex, content-disposition: ${contentDisp} url: ${url}` | |
); | |
} | |
} else { | |
window.webkit.messageHandlers.jsError.postMessage( | |
`content-disposition header missing, url: ${url}` | |
); | |
} | |
if (!filename) { | |
const contentType = res.headers.get('content-type'); | |
if (contentType) { | |
if (contentType.indexOf('application/json') === 0) { | |
filename = 'unnamed.pdf'; | |
} else if (contentType.indexOf('image/tiff') === 0) { | |
filename = 'unnamed.tiff'; | |
} | |
} | |
} | |
if (!filename) { | |
window.webkit.messageHandlers.jsError.postMessage( | |
`Could not determine filename from content-disposition nor content-type, content-dispositon: ${contentDispositon}, content-type: ${contentType}, url: ${url}` | |
); | |
} | |
let data; | |
try { | |
data = await res.blob(); | |
} catch (err) { | |
window.webkit.messageHandlers.jsError.postMessage( | |
`res.blob() threw, error: ${err}, url: ${url}` | |
); | |
return; | |
} | |
const fr = new FileReader(); | |
fr.onload = () => { | |
console.log('1', new Date()); | |
window.webkit.messageHandlers.openDocument.postMessage( | |
`${filename};${fr.result}` | |
) | |
console.log('2', new Date()); | |
}; | |
fr.addEventListener('error', (err) => { | |
window.webkit.messageHandlers.jsError.postMessage( | |
`FileReader threw, error: ${err}` | |
) | |
}) | |
fr.readAsDataURL(data); | |
} catch (err) { | |
// TODO: better log the error, currently only TypeError: Type error | |
window.webkit.messageHandlers.jsError.postMessage( | |
`JSError while downloading document, url: ${url}, err: ${err}` | |
) | |
debugger; | |
} | |
})(); | |
// null is needed here as this eval returns the last statement and we can't return a promise | |
null; | |
""") { (result, err) in | |
if (err != nil) { | |
debugPrint("JS ERR: \(String(describing: err))") | |
} | |
} | |
} else { | |
decisionHandler(.allow) | |
} | |
} | |
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { | |
debugPrint("did receive message \(message.name)") | |
if (message.name == "openDocument") { | |
previewDocument(messageBody: message.body as! String) | |
} else if (message.name == "jsError") { | |
debugPrint(message.body as! String) | |
} | |
} | |
private func previewDocument(messageBody: String) { | |
// messageBody is in the format <filename>;data:<mime-type>;base64,<base64-encoded-data> | |
// split on the first ";", to reveal the filename | |
let filenameSplits = messageBody.split(separator: ";", maxSplits: 1, omittingEmptySubsequences: false) | |
let filename = String(filenameSplits[0]) | |
// split the remaining part on the first ",", to reveal the base64 data | |
let dataSplits = filenameSplits[1].split(separator: ",", maxSplits: 1, omittingEmptySubsequences: false) | |
let data = Data(base64Encoded: String(dataSplits[1])) | |
if (data == nil) { | |
debugPrint("Could not construct data from base64") | |
return | |
} | |
// store the file on disk | |
let localFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename) | |
do { | |
try data!.write(to: localFileURL); | |
} catch { | |
debugPrint(error) | |
return | |
} | |
// and display it in QL | |
DispatchQueue.main.async { | |
self.documentUrl = localFileURL | |
self.documentPreviewController.refreshCurrentPreviewItem() | |
self.present(self.documentPreviewController, animated: true, completion: nil) | |
} | |
} |
Habe Zeile 15-17 angepasst. fetch scheint unter iOS 11.1 cookies by-default nicht mitzusenden, siehe: https://github.com/github/fetch#sending-cookies Kannst du mal prüfen ob das den Fehler behebt?
Das Problem mit dem window.webkit.messageHandlers.jsError.postMessage
ist, dass ich dir vergessen habe zu sagen, dass du noch einen zweiten message handler im init()
des ViewController registrieren musst:
von:
webView.configuration.userContentController.add(self, name: "jsError")
zu:
webView.configuration.userContentController.add(self, name: "openDocument")
webView.configuration.userContentController.add(self, name: "jsError")
Damit werden jsErrors in deinem VC mittels debugPrint geloggt.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Es sieht so aus, als würde das JS versuchen eine Response zu verarbeiten, die wir nicht erwarten.
"Response" Objekt im Debugger (iOS 11):
Dagegen ist das "Response" Objekt im Debugger (iOS 12) ok:
Es hängt wahrscheinlich mit der F5 zusammen:
https://devcentral.f5.com/articles/http-event-order-access-policy-manager
Der Fehlercode 19 bedeutet:
https://devcentral.f5.com/questions/apm-gives-error-code-19-always-57311