Skip to content

fix(ios): support for direct downloads of .kmp packages from within the in-app browser#4568

Merged
jahorton merged 7 commits intobetafrom
fix/ios/2197-app-browser-pkg-downloads
Mar 5, 2021
Merged

fix(ios): support for direct downloads of .kmp packages from within the in-app browser#4568
jahorton merged 7 commits intobetafrom
fix/ios/2197-app-browser-pkg-downloads

Conversation

@jahorton
Copy link
Copy Markdown
Contributor

@jahorton jahorton commented Mar 2, 2021

Fixes Mitigates #2197.

Seems that the root issue was that the UIWebView type is more restrictive in how links are followed. Since it's an in-app WebView, it doesn't want to perform downloads that good ol' Safari's perfectly happy to perform. So... we need to be more proactive and intercept .kmp links.

Then... it turns out that with all the nifty, type-aware KMP installation functionality from earlier in 14.0's development... I accidentally made non-type-aware KMP installation significantly trickier. Ah well.

ResourceDownloadManager.shared.downloadRawKMP(from: request.url!) { file, error in
// do something!
if let error = error {
let alert = ResourceFileManager.shared.buildSimpleAlert(title: "Error", message: error.localizedDescription)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that "Error" should be localized. Sadly, I noted that there are a couple of other matching, non-localized spots yet to handle... which are actually rather related:

public func buildKMPError(_ error: KMPError) -> UIAlertController {
return buildSimpleAlert(title: "Error", message: error.localizedDescription)
}
public func buildSimpleAlert(title: String, message: String, completionHandler: (() -> Void)? = nil ) -> UIAlertController {
let alertController = UIAlertController(title: title, message: message,
preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction(title: "OK",
style: UIAlertAction.Style.default,
handler: { _ in
completionHandler?()
}))
//UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
return alertController
}

(Lines 268 and 274, in particular.)

May as well address all three together, whether that be within this PR or a separate one.

Comment on lines +266 to +269
let alertController = UIAlertController(title: "Cannot Open Page",
message: error.localizedDescription,
preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction(title: "OK",
Copy link
Copy Markdown
Contributor Author

@jahorton jahorton Mar 2, 2021

Choose a reason for hiding this comment

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

And probably these, too, which (despite how Git will show it) are unchanged lines.

@jahorton
Copy link
Copy Markdown
Contributor Author

jahorton commented Mar 3, 2021

The noted i18n spots have been rounded up as part of #4577.

func webView(_ webView: UIWebView,
shouldStartLoadWith request: URLRequest,
navigationType: UIWebView.NavigationType) -> Bool {
if request.url?.lastPathComponent.hasSuffix(".kmp") ?? false {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This may not be 100% correct. We should be looking at the content-disposition header if it exists to determine the filename. We cannot assume that a URL will have a .kmp extension (e.g. it might be called something like file-downloader.php)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When using the keyman.com language search. the initial link is PHP, though that ends up resolving to a .kmp that this can then intercept.

I'm completely unfamiliar with such headers; have any good example ones you could point me at for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Like, for an actual .kmp.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't have one handy, but you could throw this onto a local website together with khmer_angkor.kmp (which you can even rename if you want, e.g. in the script below I renamed the copy I am reading from to example_khmer_angkor.dat just to illustrate). You could save this as download.php.

<?php
  $file = './example_khmer_angkor.dat';
  header('Content-Type: application/octet-stream');
  header('Content-Disposition: attachment; filename=khmer_angkor.kmp');
  header('Content-Length: ' . filesize($file));
  readfile($file);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll test with that shortly, but first I wanted to see what I can get from the keyboard search.

From the keyboard page for sil_ipa, the link seems to process in three stages:

Stage 1:

▿ https://keyman.com/keyboard/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
  ▿ url : Optional<URL>
    ▿ some : https://keyman.com/keyboard/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
      - _url : https://keyman.com/keyboard/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
  - cachePolicy : 0
  - timeoutInterval : 2147483647.0
  ▿ mainDocumentURL : Optional<URL>
    ▿ some : https://keyman.com/keyboard/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
      - _url : https://keyman.com/keyboard/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
  - networkServiceType : __C.NSURLRequestNetworkServiceType
  - allowsCellularAccess : true
  ▿ httpMethod : Optional<String>
    - some : "GET"
  ▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
    ▿ some : 3 elements
      ▿ 0 : 2 elements
        - key : "Accept"
        - value : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
      ▿ 1 : 2 elements
        - key : "User-Agent"
        - value : "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
      ▿ 2 : 2 elements
        - key : "Referer"
        - value : "https://keyman.com/keyboards/sil_ipa"
  - httpBody : nil
  - httpBodyStream : nil
  - httpShouldHandleCookies : true
  - httpShouldUsePipelining : true

Stage 2:

▿ https://keyman.com/keyboards/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
  ▿ url : Optional<URL>
    ▿ some : https://keyman.com/keyboards/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
      - _url : https://keyman.com/keyboards/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
  - cachePolicy : 0
  - timeoutInterval : 2147483647.0
  ▿ mainDocumentURL : Optional<URL>
    ▿ some : https://keyman.com/keyboards/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
      - _url : https://keyman.com/keyboards/download?id=sil_ipa&platform=ios&mode=standalone&cid=760108319.1607477546
  - networkServiceType : __C.NSURLRequestNetworkServiceType
  - allowsCellularAccess : true
  ▿ httpMethod : Optional<String>
    - some : "GET"
  ▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
    ▿ some : 5 elements
      ▿ 0 : 2 elements
        - key : "Accept-Encoding"
        - value : "gzip, deflate, br"
      ▿ 1 : 2 elements
        - key : "Accept"
        - value : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
      ▿ 2 : 2 elements
        - key : "Referer"
        - value : "https://keyman.com/keyboards/sil_ipa"
      ▿ 3 : 2 elements
        - key : "User-Agent"
        - value : "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
      ▿ 4 : 2 elements
        - key : "Accept-Language"
        - value : "en-us"
  - httpBody : nil
  - httpBodyStream : nil
  - httpShouldHandleCookies : true
  - httpShouldUsePipelining : true

Stage 3:

▿ https://downloads.keyman.com/keyboards/sil_ipa/1.8.4/sil_ipa.kmp
  ▿ url : Optional<URL>
    ▿ some : https://downloads.keyman.com/keyboards/sil_ipa/1.8.4/sil_ipa.kmp
      - _url : https://downloads.keyman.com/keyboards/sil_ipa/1.8.4/sil_ipa.kmp
  - cachePolicy : 0
  - timeoutInterval : 2147483647.0
  ▿ mainDocumentURL : Optional<URL>
    ▿ some : https://downloads.keyman.com/keyboards/sil_ipa/1.8.4/sil_ipa.kmp
      - _url : https://downloads.keyman.com/keyboards/sil_ipa/1.8.4/sil_ipa.kmp
  - networkServiceType : __C.NSURLRequestNetworkServiceType
  - allowsCellularAccess : true
  ▿ httpMethod : Optional<String>
    - some : "GET"
  ▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
    ▿ some : 5 elements
      ▿ 0 : 2 elements
        - key : "Accept-Encoding"
        - value : "gzip, deflate, br"
      ▿ 1 : 2 elements
        - key : "Accept-Language"
        - value : "en-us"
      ▿ 2 : 2 elements
        - key : "Referer"
        - value : "https://keyman.com/keyboards/sil_ipa"
      ▿ 3 : 2 elements
        - key : "Accept"
        - value : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
      ▿ 4 : 2 elements
        - key : "User-Agent"
        - value : "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
  - httpBody : nil
  - httpBodyStream : nil
  - httpShouldHandleCookies : true
  - httpShouldUsePipelining : true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Setting that up under my local keymanweb.com development 'server' setup...

▿ http://keymanweb.com.local/test.php
  ▿ url : Optional<URL>
    ▿ some : http://keymanweb.com.local/test.php
      - _url : http://keymanweb.com.local/test.php
  - cachePolicy : 0
  - timeoutInterval : 60.0
  ▿ mainDocumentURL : Optional<URL>
    ▿ some : http://keymanweb.com.local/test.php
      - _url : http://keymanweb.com.local/test.php
  - networkServiceType : __C.NSURLRequestNetworkServiceType
  - allowsCellularAccess : true
  ▿ httpMethod : Optional<String>
    - some : "GET"
  ▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
    ▿ some : 2 elements
      ▿ 0 : 2 elements
        - key : "Accept"
        - value : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
      ▿ 1 : 2 elements
        - key : "User-Agent"
        - value : "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
  - httpBody : nil
  - httpBodyStream : nil
  - httpShouldHandleCookies : true
  - httpShouldUsePipelining : true

There are no further stages; the next thing that occurs is a "Frame load interrupted" error. So that's all iOS will give me without very significant changes, unless I'm missing something.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just to make clear the testing setup I used:

Screen Shot 2021-03-04 at 2 59 46 PM

@jahorton
Copy link
Copy Markdown
Contributor Author

jahorton commented Mar 4, 2021

So... while the solution implemented by this PR isn't optimal, as you've well pointed out... I think something only partly broken is still much better than something entirely broken. We can always implement a follow-up, altering our perspective on this PR from "fixes" to a "mitigates".

@jahorton jahorton changed the title fix(ios): fixes downloads of .kmp packages from within the in-app browser fix(ios): support for direct downloads of .kmp packages from within the in-app browser Mar 4, 2021
@jahorton
Copy link
Copy Markdown
Contributor Author

jahorton commented Mar 4, 2021

Alternatively, if you feel comfortable addressing this, please be my guest; I feel like I'm in over my head with your request.

Copy link
Copy Markdown
Member

@mcdurdin mcdurdin left a comment

Choose a reason for hiding this comment

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

So... while the solution implemented by this PR isn't optimal, as you've well pointed out... I think something only partly broken is still much better than something entirely broken. We can always implement a follow-up, altering our perspective on this PR from "fixes" to a "mitigates".

I did some more research online and it seems like this is an open problem with both WKWebView and UIWebView. All sorts of esoteric ideas on how to work around it but no simple onFileDownload approach such as one would hope for.

Given that, let's go ahead with your implementation as it stands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants