Skip to content

Instantly share code, notes, and snippets.

@sventschui
Last active April 4, 2023 11:08
Show Gist options
  • Save sventschui/342fa43610b3dca52f424055ee2e4ccf to your computer and use it in GitHub Desktop.
Save sventschui/342fa43610b3dca52f424055ee2e4ccf to your computer and use it in GitHub Desktop.
JS based download in WKWebView
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)
}
}
@sventschui
Copy link
Author

sventschui commented Sep 26, 2018

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