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)
}
}
Copy link

ghost commented Sep 26, 2018

Folgende Exception tritt auf dem Simulator iPhone 8 (iOS 11.4) auf:

[Error] Unhandled Promise Rejection: TypeError: undefined is not an object (evaluating 'window.webkit.messageHandlers.jsError.postMessage')
	(anonyme Funktion) (Skriptelement 43:77)
	asyncFunctionResume
	(anonyme Funktion)
	promiseReactionJob

Der Fehler passiert beim error Handling:

} 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}`
    )

Copy link

ghost commented Sep 26, 2018

Wenn ich das JS anpasse, kommt folgende Exception:
JSError while downloading document, url: https://my.sympany.ch/web/APP/connector/0/401/dl, err: TypeError: undefined is not an object (evaluating 'window.webkit.messageHandlers.jsError.postMessage')

JS:

} catch (err) {
    // TODO: better log the error, currently only TypeError: Type error
    console.log(
    `JSError while downloading document, url: ${url}, err: ${err}`
    )
    debugger;

Copy link

ghost commented Sep 26, 2018

Ursache für das PRoblem beim Exception Handling ist offensichtlich
window.webkit.messageHandlers.jsError.postMessage - das gibt den Type error

Wenn ich alles im Skript mit console.log ersetze, wird die Exception gelogged.

Der eigentliche Fehler ist dann:

[Log] content-disposition header missing, url: https://my.sympany.ch/web/APP/connector/0/367/dl (clientportal, line 33)
[Log] JSError while downloading document, url: https://my.sympany.ch/web/APP/connector/0/367/dl, err: ReferenceError: Can't find variable: filename (clientportal, line 77)

Copy link

ghost commented Sep 26, 2018

Es sieht so aus, als würde das JS versuchen eine Response zu verarbeiten, die wir nicht erwarten.

"Response" Objekt im Debugger (iOS 11):

body: ReadableStream {locked: false}
bodyUsed: false
headers: Headers {append: function, delete: function, get: function, has: function, set: function, …}
ok: true
redirected: true
status: 200
statusText: "OK"
type: "basic"
url: "https://my.sympany.ch/my.logout.php3?errorcode=19"

Dagegen ist das "Response" Objekt im Debugger (iOS 12) ok:

body: ReadableStream {locked: false}
bodyUsed: false
headers: Headers {append: function, delete: function, get: function, has: function, set: function, …}
ok: true
redirected: false
status: 200
statusText: "OK"
type: "basic"
url: "https://my.sympany.ch/web/APP/connector/0/367/dl"

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

"BIG-IP can not find session information in the request. This can happen because your browser restarted after an add-on was installed. If this occurred, click the link below to continue. This can also happen because cookies are disabled in your browser. If so, enable cookies in your browser and start a new session."

@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