From 695e1b0f02d4d8a3ac454cffecb2a0100774abcd Mon Sep 17 00:00:00 2001 From: Niklas Merz Date: Wed, 13 Dec 2017 22:17:45 +0100 Subject: [PATCH 01/53] CB-13659 (iOS) Add hidespinner option Signed-off-by: Niklas Merz --- README.md | 5 +++-- src/ios/CDVInAppBrowser.h | 6 +++--- src/ios/CDVInAppBrowser.m | 7 +++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a4b7cd9..de4c8ee 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ instance, or the system browser. - __options__: Options for the `InAppBrowser`. Optional, defaulting to: `location=yes`. _(String)_ - The `options` string must not contain any blank space, and each feature's name/value pairs must be separated by a comma. Feature names are case insensitive. - + The `options` string must not contain any blank space, and each feature's name/value pairs must be separated by a comma. Feature names are case insensitive. + All platforms support: - __location__: Set to `yes` or `no` to turn the `InAppBrowser`'s location bar on or off. @@ -134,6 +134,7 @@ instance, or the system browser. - __presentationstyle__: Set to `pagesheet`, `formsheet` or `fullscreen` to set the [presentation style](http://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/modalPresentationStyle) (defaults to `fullscreen`). - __transitionstyle__: Set to `fliphorizontal`, `crossdissolve` or `coververtical` to set the [transition style](http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/modalTransitionStyle) (defaults to `coververtical`). - __toolbarposition__: Set to `top` or `bottom` (default is `bottom`). Causes the toolbar to be at the top or bottom of the window. + - __hidespinner__: Set to `yes` or `no` to change the visibility of the loading indicator (defaults to `no`). Windows supports these additional options: diff --git a/src/ios/CDVInAppBrowser.h b/src/ios/CDVInAppBrowser.h index d258eb0..0ad9bd5 100644 --- a/src/ios/CDVInAppBrowser.h +++ b/src/ios/CDVInAppBrowser.h @@ -52,6 +52,7 @@ @property (nonatomic, copy) NSString* toolbarposition; @property (nonatomic, assign) BOOL clearcache; @property (nonatomic, assign) BOOL clearsessioncache; +@property (nonatomic, assign) BOOL hidespinner; @property (nonatomic, copy) NSString* presentationstyle; @property (nonatomic, copy) NSString* transitionstyle; @@ -74,13 +75,13 @@ NSString* _prevUserAgent; NSInteger _userAgentLockToken; CDVInAppBrowserOptions *_browserOptions; - + #ifdef __CORDOVA_4_0_0 CDVUIWebViewDelegate* _webViewDelegate; #else CDVWebViewDelegate* _webViewDelegate; #endif - + } @property (nonatomic, strong) IBOutlet UIWebView* webView; @@ -110,4 +111,3 @@ @property (nonatomic, weak) id orientationDelegate; @end - diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index f5d05f0..3daa86d 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -887,7 +887,10 @@ self.backButton.enabled = theWebView.canGoBack; self.forwardButton.enabled = theWebView.canGoForward; - [self.spinner startAnimating]; + NSLog(_browserOptions.hidespinner ? @"Yes" : @"No"); + if(!_browserOptions.hidespinner) { + [self.spinner startAnimating]; + } return [self.navigationDelegate webViewDidStartLoad:theWebView]; } @@ -987,6 +990,7 @@ self.toolbarposition = kInAppBrowserToolbarBarPositionBottom; self.clearcache = NO; self.clearsessioncache = NO; + self.hidespinner = NO; self.enableviewportscale = NO; self.mediaplaybackrequiresuseraction = NO; @@ -1104,4 +1108,3 @@ @end - From a3fca87ee524d9fe5a0dbec24278f195c6836cac Mon Sep 17 00:00:00 2001 From: Bentley O'Kane-Chase Date: Thu, 22 Feb 2018 15:09:09 +1000 Subject: [PATCH 02/53] Fix navigation buttons on iOS --- src/ios/CDVInAppBrowser.m | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index a581472..56d227b 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -654,7 +654,6 @@ } else { [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; } - [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton]]; self.view.backgroundColor = [UIColor grayColor]; [self.view addSubview:self.toolbar]; From 65a825a1934ef93d886225c9c56f98fcec50ef38 Mon Sep 17 00:00:00 2001 From: Bentley O'Kane-Chase Date: Wed, 21 Mar 2018 21:50:25 +1000 Subject: [PATCH 03/53] Add customisation of the navigation buttons for iOS --- README.md | 1 + src/ios/CDVInAppBrowser.h | 1 + src/ios/CDVInAppBrowser.m | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 936f1dc..3f3967b 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ instance, or the system browser. - __closebuttoncaption__: set to a string to use as the __Done__ button's caption. Note that you need to localize this value yourself. - __disallowoverscroll__: Set to `yes` or `no` (default is `no`). Turns on/off the UIWebViewBounce property. - __hidenavigationbuttons__: set to `yes` or `no` to turn the toolbar navigation buttons on or off (defaults to `no`). Only applicable if toolbar is not disabled. + - __navigationbuttoncolor__: set as a valid hex color string, for example: `#00ff00`, to change from the default color. Only applicable if navigation buttons are visible. - __toolbar__: set to `yes` or `no` to turn the toolbar on or off for the InAppBrowser (defaults to `yes`) - __toolbarcolor__: set as a valid hex color string, for example: `#00ff00`, to change from the default color of the toolbar. Only applicable if toolbar is not disabled. - __toolbartranslucent__: set to `yes` or `no` to make the toolbar translucent(semi-transparent) (defaults to `yes`). Only applicable if toolbar is not disabled. diff --git a/src/ios/CDVInAppBrowser.h b/src/ios/CDVInAppBrowser.h index 9338c55..cb36d34 100644 --- a/src/ios/CDVInAppBrowser.h +++ b/src/ios/CDVInAppBrowser.h @@ -54,6 +54,7 @@ @property (nonatomic, copy) NSString* toolbarcolor; @property (nonatomic, assign) BOOL toolbartranslucent; @property (nonatomic, assign) BOOL hidenavigationbuttons; +@property (nonatomic, copy) NSString* navigationbuttoncolor; @property (nonatomic, assign) BOOL clearcache; @property (nonatomic, assign) BOOL clearsessioncache; diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index 56d227b..8847b44 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -642,11 +642,17 @@ self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; self.forwardButton.enabled = YES; self.forwardButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.forwardButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } NSString* backArrowString = NSLocalizedString(@"◄", nil); // create arrow from Unicode char self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; self.backButton.enabled = YES; self.backButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.backButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } // Filter out Navigation Buttons if user requests so if (_browserOptions.hidenavigationbuttons) { From 44d9bb0f6a4deff139a1968479030664bf49eff2 Mon Sep 17 00:00:00 2001 From: stevepodell Date: Wed, 4 Apr 2018 08:22:23 -0700 Subject: [PATCH 04/53] InAppBrowser.java: New method isURLWhileListed to check for whitelisting. Newtest in shouldOverrideUrlLoading, to allow whitelisted custom schemes like"mycoolapp://" inappbrowser.js: Added "customscheme" channel. --- src/android/InAppBrowser.java | 88 ++++++++++++++++++++++++----------- www/inappbrowser.js | 1 + 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 2b0dbe0..3a03057 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -170,33 +170,10 @@ public class InAppBrowser extends CordovaPlugin { Boolean shouldAllowNavigation = null; if (url.startsWith("javascript:")) { shouldAllowNavigation = true; + } else { + shouldAllowNavigation = isURLWhiteListed(url); } - if (shouldAllowNavigation == null) { - try { - Method iuw = Config.class.getMethod("isUrlWhiteListed", String.class); - shouldAllowNavigation = (Boolean)iuw.invoke(null, url); - } catch (NoSuchMethodException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } catch (IllegalAccessException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } catch (InvocationTargetException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } - } - if (shouldAllowNavigation == null) { - try { - Method gpm = webView.getClass().getMethod("getPluginManager"); - PluginManager pm = (PluginManager)gpm.invoke(webView); - Method san = pm.getClass().getMethod("shouldAllowNavigation", String.class); - shouldAllowNavigation = (Boolean)san.invoke(pm, url); - } catch (NoSuchMethodException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } catch (IllegalAccessException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } catch (InvocationTargetException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } - } + // load in webview if (Boolean.TRUE.equals(shouldAllowNavigation)) { LOG.d(LOG_TAG, "loading in webview"); @@ -302,6 +279,47 @@ public class InAppBrowser extends CordovaPlugin { return true; } + /** + * Is the URL or Scheme WhiteListed + * This code exists for compatibility between 3.x and 4.x versions of Cordova. + * Previously the Config class had a static method, isUrlWhitelisted(). That + * responsibility has been moved to the plugins, with an aggregating method in + * PluginManager. + */ + * @param url, the URL as a String + * @return true if WhiteListed, otherwise null or false + */ + private Boolean isURLWhiteListed(String url) { + Boolean shouldAllowNavigation = null; + if (shouldAllowNavigation == null) { + try { + Method iuw = Config.class.getMethod("isUrlWhiteListed", String.class); + shouldAllowNavigation = (Boolean)iuw.invoke(null, url); + } catch (NoSuchMethodException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (IllegalAccessException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (InvocationTargetException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + } + if (shouldAllowNavigation == null) { + try { + Method gpm = webView.getClass().getMethod("getPluginManager"); + PluginManager pm = (PluginManager)gpm.invoke(webView); + Method san = pm.getClass().getMethod("shouldAllowNavigation", String.class); + shouldAllowNavigation = (Boolean)san.invoke(pm, url); + } catch (NoSuchMethodException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (IllegalAccessException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (InvocationTargetException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + } + return shouldAllowNavigation; + } + /** * Called when the view navigates. */ @@ -1110,6 +1128,24 @@ public class InAppBrowser extends CordovaPlugin { LOG.e(LOG_TAG, "Error sending sms " + url + ":" + e.toString()); } } + // Test for whitelisted custom scheme names, less than 20 chars long, like mycoolapp: or twitteroauthresponse: (Twitter Oauth Response) + else if (url.matches("^[a-z]{0,20}://.*?$")) { + if (Boolean.TRUE.equals(isURLWhiteListed(url))) { + try { + LOG.w("STEVE IN InAppBrowser.java, whiteliste url SUCCESS: ", url ); + JSONObject obj = new JSONObject(); + obj.put("type", "customscheme"); + obj.put("url", url); + sendUpdate(obj, true); + return true; + } catch (JSONException ex) { + LOG.e(LOG_TAG, "Custom Scheme URI passed in has caused a JSON error."); + } + } else { + LOG.w("STEVE IN InAppBrowser.java, whitelisted url FAILURE: ", url ); + } + } + return false; } diff --git a/www/inappbrowser.js b/www/inappbrowser.js index 7c3e749..08f96ab 100644 --- a/www/inappbrowser.js +++ b/www/inappbrowser.js @@ -36,6 +36,7 @@ 'loadstart': channel.create('loadstart'), 'loadstop': channel.create('loadstop'), 'loaderror': channel.create('loaderror'), + 'customscheme': channel.create('customscheme'), 'exit': channel.create('exit') }; } From a6c7b54998deb1a582c9550f4a4e78fc18e8c137 Mon Sep 17 00:00:00 2001 From: stevepodell Date: Wed, 4 Apr 2018 08:42:00 -0700 Subject: [PATCH 05/53] InAppBrowser.java: New method isURLWhileListed to check for whitelisting. Newtest in shouldOverrideUrlLoading, to allow whitelisted custom schemes like"mycoolapp://" inappbrowser.js: Added "customscheme" channel. --- src/android/InAppBrowser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 3a03057..be75d16 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -285,7 +285,7 @@ public class InAppBrowser extends CordovaPlugin { * Previously the Config class had a static method, isUrlWhitelisted(). That * responsibility has been moved to the plugins, with an aggregating method in * PluginManager. - */ + * * @param url, the URL as a String * @return true if WhiteListed, otherwise null or false */ From 2d69afcd7c30e6ac86ef0d5519af3d12ca3a6252 Mon Sep 17 00:00:00 2001 From: stevepodell Date: Wed, 4 Apr 2018 15:57:13 -0700 Subject: [PATCH 06/53] InAppBrowser.java: New method isURLWhileListed to check for whitelisting. Newtest in shouldOverrideUrlLoading, to allow whitelisted custom schemes like"mycoolapp://" inappbrowser.js: Added "customscheme" channel. --- src/android/InAppBrowser.java | 102 +++++++++++++++------------------- 1 file changed, 45 insertions(+), 57 deletions(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index be75d16..e53614d 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -133,6 +133,7 @@ public class InAppBrowser extends CordovaPlugin { private boolean hideUrlBar = false; private boolean showFooter = false; private String footerColor = ""; + private String[] allowedSchemes; /** * Executes the request and returns PluginResult. @@ -170,10 +171,33 @@ public class InAppBrowser extends CordovaPlugin { Boolean shouldAllowNavigation = null; if (url.startsWith("javascript:")) { shouldAllowNavigation = true; - } else { - shouldAllowNavigation = isURLWhiteListed(url); } - + if (shouldAllowNavigation == null) { + try { + Method iuw = Config.class.getMethod("isUrlWhiteListed", String.class); + shouldAllowNavigation = (Boolean)iuw.invoke(null, url); + } catch (NoSuchMethodException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (IllegalAccessException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (InvocationTargetException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + } + if (shouldAllowNavigation == null) { + try { + Method gpm = webView.getClass().getMethod("getPluginManager"); + PluginManager pm = (PluginManager)gpm.invoke(webView); + Method san = pm.getClass().getMethod("shouldAllowNavigation", String.class); + shouldAllowNavigation = (Boolean)san.invoke(pm, url); + } catch (NoSuchMethodException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (IllegalAccessException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (InvocationTargetException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + } // load in webview if (Boolean.TRUE.equals(shouldAllowNavigation)) { LOG.d(LOG_TAG, "loading in webview"); @@ -279,47 +303,6 @@ public class InAppBrowser extends CordovaPlugin { return true; } - /** - * Is the URL or Scheme WhiteListed - * This code exists for compatibility between 3.x and 4.x versions of Cordova. - * Previously the Config class had a static method, isUrlWhitelisted(). That - * responsibility has been moved to the plugins, with an aggregating method in - * PluginManager. - * - * @param url, the URL as a String - * @return true if WhiteListed, otherwise null or false - */ - private Boolean isURLWhiteListed(String url) { - Boolean shouldAllowNavigation = null; - if (shouldAllowNavigation == null) { - try { - Method iuw = Config.class.getMethod("isUrlWhiteListed", String.class); - shouldAllowNavigation = (Boolean)iuw.invoke(null, url); - } catch (NoSuchMethodException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } catch (IllegalAccessException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } catch (InvocationTargetException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } - } - if (shouldAllowNavigation == null) { - try { - Method gpm = webView.getClass().getMethod("getPluginManager"); - PluginManager pm = (PluginManager)gpm.invoke(webView); - Method san = pm.getClass().getMethod("shouldAllowNavigation", String.class); - shouldAllowNavigation = (Boolean)san.invoke(pm, url); - } catch (NoSuchMethodException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } catch (IllegalAccessException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } catch (InvocationTargetException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); - } - } - return shouldAllowNavigation; - } - /** * Called when the view navigates. */ @@ -1129,20 +1112,25 @@ public class InAppBrowser extends CordovaPlugin { } } // Test for whitelisted custom scheme names, less than 20 chars long, like mycoolapp: or twitteroauthresponse: (Twitter Oauth Response) - else if (url.matches("^[a-z]{0,20}://.*?$")) { - if (Boolean.TRUE.equals(isURLWhiteListed(url))) { - try { - LOG.w("STEVE IN InAppBrowser.java, whiteliste url SUCCESS: ", url ); - JSONObject obj = new JSONObject(); - obj.put("type", "customscheme"); - obj.put("url", url); - sendUpdate(obj, true); - return true; - } catch (JSONException ex) { - LOG.e(LOG_TAG, "Custom Scheme URI passed in has caused a JSON error."); + else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[a-z]{0,20}://.*?$")) { + if (allowedSchemes == null) { + String allowed = preferences.getString("AllowedSchemes", ""); + allowedSchemes = allowed.split(","); + } + if (allowedSchemes != null) { + for (String scheme : allowedSchemes) { + if (url.startsWith(scheme)) { + try { + JSONObject obj = new JSONObject(); + obj.put("type", "customscheme"); + obj.put("url", url); + sendUpdate(obj, true); + return true; + } catch (JSONException ex) { + LOG.e(LOG_TAG, "Custom Scheme URI passed in has caused a JSON error."); + } + } } - } else { - LOG.w("STEVE IN InAppBrowser.java, whitelisted url FAILURE: ", url ); } } From 019ec3963f6b9884a5dd61e1b7565d83e0adfdba Mon Sep 17 00:00:00 2001 From: stevepodell Date: Thu, 5 Apr 2018 10:14:54 -0700 Subject: [PATCH 07/53] InAppBrowser.java: New method isURLWhileListed to check for whitelisting of "AllowedSchemes" in a new preference configuration item. There is a new check in shouldOverrideUrlLoading, to allow whitelisted custom schemes like "mycoolapp://" inappbrowser.js: Added "customscheme" channel. --- src/android/InAppBrowser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index e53614d..6682a36 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -1111,7 +1111,7 @@ public class InAppBrowser extends CordovaPlugin { LOG.e(LOG_TAG, "Error sending sms " + url + ":" + e.toString()); } } - // Test for whitelisted custom scheme names, less than 20 chars long, like mycoolapp: or twitteroauthresponse: (Twitter Oauth Response) + // Test for whitelisted custom scheme names, less than 20 chars long, like mycoolapp:// or twitteroauthresponse:// (Twitter Oauth Response) else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[a-z]{0,20}://.*?$")) { if (allowedSchemes == null) { String allowed = preferences.getString("AllowedSchemes", ""); From 4c4bee528edcd98a9f8fdad677a9a9938a7c992d Mon Sep 17 00:00:00 2001 From: stevepodell Date: Thu, 5 Apr 2018 10:31:00 -0700 Subject: [PATCH 08/53] InAppBrowser.java: New method isURLWhileListed to check for whitelisting of "AllowedSchemes" in a new preference configuration item. There is a new check in shouldOverrideUrlLoading, to allow whitelisted custom schemes like "mycoolapp://" inappbrowser.js: Added "customscheme" channel. --- src/android/InAppBrowser.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 6682a36..2d9c144 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -1111,8 +1111,8 @@ public class InAppBrowser extends CordovaPlugin { LOG.e(LOG_TAG, "Error sending sms " + url + ":" + e.toString()); } } - // Test for whitelisted custom scheme names, less than 20 chars long, like mycoolapp:// or twitteroauthresponse:// (Twitter Oauth Response) - else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[a-z]{0,20}://.*?$")) { + // Test for whitelisted custom scheme names like mycoolapp:// or twitteroauthresponse:// (Twitter Oauth Response) + else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[a-z]://.*?$")) { if (allowedSchemes == null) { String allowed = preferences.getString("AllowedSchemes", ""); allowedSchemes = allowed.split(","); From 27500c2990249f23c90dbb066ce6312e440e28f5 Mon Sep 17 00:00:00 2001 From: stevepodell Date: Thu, 5 Apr 2018 11:54:32 -0700 Subject: [PATCH 09/53] In file AppBrowser.java: New code within shouldOverrideUrlLoading() to check for whitelisting custom schemes via a new "AllowedSchemes" preference configuration item. Allows custom schemes like "mycoolapp://" or "wevotetwitterscheme://" In file inappbrowser.js: Added new "customscheme" channel. --- src/android/InAppBrowser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 2d9c144..86a6745 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -1112,7 +1112,7 @@ public class InAppBrowser extends CordovaPlugin { } } // Test for whitelisted custom scheme names like mycoolapp:// or twitteroauthresponse:// (Twitter Oauth Response) - else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[a-z]://.*?$")) { + else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[a-z]*://.*?$")) { if (allowedSchemes == null) { String allowed = preferences.getString("AllowedSchemes", ""); allowedSchemes = allowed.split(","); From 42df2977240fe3cc716c882eaefff9e2048f0705 Mon Sep 17 00:00:00 2001 From: stevepodell Date: Thu, 5 Apr 2018 12:45:02 -0700 Subject: [PATCH 10/53] In file AppBrowser.java: New code within shouldOverrideUrlLoading() to check for whitelisting custom schemes via a new "AllowedSchemes" preference configuration item. Allows custom schemes like "mycoolapp://" or "wevotetwitterscheme://" In file inappbrowser.js: Added new "customscheme" channel. --- src/android/InAppBrowser.java | 2 +- www/inappbrowser.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 86a6745..9b3388c 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -1256,4 +1256,4 @@ public class InAppBrowser extends CordovaPlugin { super.onReceivedHttpAuthRequest(view, handler, host, realm); } } -} +} \ No newline at end of file diff --git a/www/inappbrowser.js b/www/inappbrowser.js index 08f96ab..3619f17 100644 --- a/www/inappbrowser.js +++ b/www/inappbrowser.js @@ -36,8 +36,8 @@ 'loadstart': channel.create('loadstart'), 'loadstop': channel.create('loadstop'), 'loaderror': channel.create('loaderror'), - 'customscheme': channel.create('customscheme'), - 'exit': channel.create('exit') + 'exit': channel.create('exit'), + 'customscheme': channel.create('customscheme') }; } From 200f7c19235e1956458f3df58dcce07fe93ece66 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Thu, 12 Apr 2018 17:06:22 -0700 Subject: [PATCH 11/53] CB-14030 Updated version and RELEASENOTES.md for release 3.0.0 (via coho) --- RELEASENOTES.md | 7 +++++++ package.json | 2 +- plugin.xml | 2 +- tests/plugin.xml | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4949f7c..6a06956 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,13 @@ --> # Release Notes +### 3.0.0 (Apr 12, 2018) +* [CB-13659](https://issues.apache.org/jira/browse/CB-13659) **iOS** Add hidespinner option +* In file `AppBrowser.java`: New code within `shouldOverrideUrlLoading()` to check for whitelisting custom schemes via a new `AllowedSchemes` preference configuration item. Allows custom schemes like `mycoolapp://` or `wevotetwitterscheme://` +* `InAppBrowser.java`: New method `isURLWhileListed` to check for whitelisting of `AllowedSchemes` in a new preference configuration item. There is a new check in `shouldOverrideUrlLoading`, to allow whitelisted custom schemes like "mycoolapp://" +* Add customisation of the navigation buttons for **iOS** +* Fix navigation buttons on **iOS** + ### 2.0.2 (Jan 24, 2018) * [CB-13791](https://issues.apache.org/jira/browse/CB-13791) Add **Android** support for a footer close button * [CB-13409](https://issues.apache.org/jira/browse/CB-13409) restore gitignore to default diff --git a/package.json b/package.json index 12c04ab..222492c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-inappbrowser", - "version": "2.0.3-dev", + "version": "3.0.0", "description": "Cordova InAppBrowser Plugin", "types": "./types/index.d.ts", "cordova": { diff --git a/plugin.xml b/plugin.xml index 10bf94e..6d57d9c 100644 --- a/plugin.xml +++ b/plugin.xml @@ -20,7 +20,7 @@ + version="3.0.0"> InAppBrowser Cordova InAppBrowser Plugin diff --git a/tests/plugin.xml b/tests/plugin.xml index f5b5d20..13f255f 100644 --- a/tests/plugin.xml +++ b/tests/plugin.xml @@ -20,7 +20,7 @@ + version="3.0.0"> Cordova InAppBrowser Plugin Tests Apache 2.0 From 220cb39f93c468922d788ed02694729594e7e8fa Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Thu, 12 Apr 2018 17:16:16 -0700 Subject: [PATCH 12/53] Set VERSION to 3.0.1-dev (via coho) --- package.json | 2 +- plugin.xml | 2 +- tests/plugin.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 222492c..a388a23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-inappbrowser", - "version": "3.0.0", + "version": "3.0.1-dev", "description": "Cordova InAppBrowser Plugin", "types": "./types/index.d.ts", "cordova": { diff --git a/plugin.xml b/plugin.xml index 6d57d9c..4a820dd 100644 --- a/plugin.xml +++ b/plugin.xml @@ -20,7 +20,7 @@ + version="3.0.1-dev"> InAppBrowser Cordova InAppBrowser Plugin diff --git a/tests/plugin.xml b/tests/plugin.xml index 13f255f..f9ab2d3 100644 --- a/tests/plugin.xml +++ b/tests/plugin.xml @@ -20,7 +20,7 @@ + version="3.0.1-dev"> Cordova InAppBrowser Plugin Tests Apache 2.0 From 4a5608a7a0d40345d7eaa93eaded1c2de18de4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar?= Date: Fri, 13 Apr 2018 11:26:53 +0200 Subject: [PATCH 13/53] Update cordovaDependencies to allow plugin install --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a388a23..a7f57c3 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "0.2.3": { "cordova": ">=3.1.0" }, - "3.0.0": { + "4.0.0": { "cordova": ">100" } } From 757022d85a65900354fbc5cb355db3651e054a05 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Fri, 13 Apr 2018 12:24:16 -0700 Subject: [PATCH 14/53] CB-14030 Updated version and RELEASENOTES.md for release 3.0.0 (via coho) --- package.json | 2 +- plugin.xml | 2 +- tests/plugin.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a7f57c3..f6bbcdb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-inappbrowser", - "version": "3.0.1-dev", + "version": "3.0.0", "description": "Cordova InAppBrowser Plugin", "types": "./types/index.d.ts", "cordova": { diff --git a/plugin.xml b/plugin.xml index 4a820dd..6d57d9c 100644 --- a/plugin.xml +++ b/plugin.xml @@ -20,7 +20,7 @@ + version="3.0.0"> InAppBrowser Cordova InAppBrowser Plugin diff --git a/tests/plugin.xml b/tests/plugin.xml index f9ab2d3..13f255f 100644 --- a/tests/plugin.xml +++ b/tests/plugin.xml @@ -20,7 +20,7 @@ + version="3.0.0"> Cordova InAppBrowser Plugin Tests Apache 2.0 From 5581957078c58893450b8252ab23d6a73ef53425 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Fri, 13 Apr 2018 12:28:14 -0700 Subject: [PATCH 15/53] CB-14030 Set VERSION to 3.0.1-dev (via coho) --- package.json | 2 +- plugin.xml | 2 +- tests/plugin.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f6bbcdb..a7f57c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-inappbrowser", - "version": "3.0.0", + "version": "3.0.1-dev", "description": "Cordova InAppBrowser Plugin", "types": "./types/index.d.ts", "cordova": { diff --git a/plugin.xml b/plugin.xml index 6d57d9c..4a820dd 100644 --- a/plugin.xml +++ b/plugin.xml @@ -20,7 +20,7 @@ + version="3.0.1-dev"> InAppBrowser Cordova InAppBrowser Plugin diff --git a/tests/plugin.xml b/tests/plugin.xml index 13f255f..f9ab2d3 100644 --- a/tests/plugin.xml +++ b/tests/plugin.xml @@ -20,7 +20,7 @@ + version="3.0.1-dev"> Cordova InAppBrowser Plugin Tests Apache 2.0 From 57eda786e072bd54385319fc932cc6b95f363aa5 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Fri, 27 Apr 2018 00:07:57 +0100 Subject: [PATCH 16/53] CB-14048: (android) allowedSchemes check empty string fix --- src/android/InAppBrowser.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 9b3388c..e630928 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -1114,8 +1114,10 @@ public class InAppBrowser extends CordovaPlugin { // Test for whitelisted custom scheme names like mycoolapp:// or twitteroauthresponse:// (Twitter Oauth Response) else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[a-z]*://.*?$")) { if (allowedSchemes == null) { - String allowed = preferences.getString("AllowedSchemes", ""); - allowedSchemes = allowed.split(","); + String allowed = preferences.getString("AllowedSchemes", null); + if(allowed != null) { + allowedSchemes = allowed.split(","); + } } if (allowedSchemes != null) { for (String scheme : allowedSchemes) { From f57ede9be2a824d699c0fa5d6156441e0e0b901b Mon Sep 17 00:00:00 2001 From: Tim Brust Date: Wed, 2 May 2018 20:16:44 +0200 Subject: [PATCH 17/53] CB-14061: (android) comply with RFC 3986 for custom URL scheme handling --- src/android/InAppBrowser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 9b3388c..1453155 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -1112,7 +1112,7 @@ public class InAppBrowser extends CordovaPlugin { } } // Test for whitelisted custom scheme names like mycoolapp:// or twitteroauthresponse:// (Twitter Oauth Response) - else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[a-z]*://.*?$")) { + else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[A-Za-z0-9+.-]*://.*?$")) { if (allowedSchemes == null) { String allowed = preferences.getString("AllowedSchemes", ""); allowedSchemes = allowed.split(","); From dac06aa3f3ad825cbcd326c0aaf0f36ebddd0cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar?= Date: Sun, 1 Jul 2018 18:09:47 +0200 Subject: [PATCH 18/53] CB-12922 (ios): fix In-app browser does not cede control --- src/ios/CDVInAppBrowser.h | 1 + src/ios/CDVInAppBrowser.m | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/ios/CDVInAppBrowser.h b/src/ios/CDVInAppBrowser.h index 25fae3f..66066b9 100644 --- a/src/ios/CDVInAppBrowser.h +++ b/src/ios/CDVInAppBrowser.h @@ -30,6 +30,7 @@ @class CDVInAppBrowserViewController; @interface CDVInAppBrowser : CDVPlugin { + UIWindow * tmpWindow; } @property (nonatomic, retain) CDVInAppBrowserViewController* inAppBrowserViewController; diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index c65e3e1..5bdb3e1 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -239,8 +239,10 @@ // Run later to avoid the "took a long time" log message. dispatch_async(dispatch_get_main_queue(), ^{ if (weakSelf.inAppBrowserViewController != nil) { - CGRect frame = [[UIScreen mainScreen] bounds]; - UIWindow *tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + if (!tmpWindow) { + CGRect frame = [[UIScreen mainScreen] bounds]; + tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + } UIViewController *tmpController = [[UIViewController alloc] init]; [tmpWindow setRootViewController:tmpController]; [tmpWindow setWindowLevel:UIWindowLevelNormal]; @@ -270,7 +272,9 @@ dispatch_async(dispatch_get_main_queue(), ^{ if (self.inAppBrowserViewController != nil) { _previousStatusBarStyle = -1; - [self.inAppBrowserViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil]; + [self.inAppBrowserViewController.presentingViewController dismissViewControllerAnimated:YES completion:^{ + [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; + }]; } }); } @@ -835,9 +839,13 @@ // Run later to avoid the "took a long time" log message. dispatch_async(dispatch_get_main_queue(), ^{ if ([weakSelf respondsToSelector:@selector(presentingViewController)]) { - [[weakSelf presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + [[weakSelf presentingViewController] dismissViewControllerAnimated:YES completion:^{ + [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; + }]; } else { - [[weakSelf parentViewController] dismissViewControllerAnimated:YES completion:nil]; + [[weakSelf parentViewController] dismissViewControllerAnimated:YES completion:^{ + [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; + }]; } }); } From eb245ecaf7362f7e2c8b29f4aa4f4442bd2cecc4 Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Tue, 31 Jul 2018 16:54:01 -0400 Subject: [PATCH 19/53] .travis.yml add blank lines --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6098fe3..a8220a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,14 @@ sudo: false + addons: jwt: secure: TZ88IEvAw1bsWPWxvDzXdpi2NK0i3PN4hG15+vDpIt6wXGVPknjxuXWJeLj7TqBpAIvP7XDfS8ZvHVPLe7fe8oOchZPLuiDw9VVIk6cnHjE6wpoavdGc/1mDJ3Bi4PDcHwRUr5ng5spYQqqlTwcECkH/q7iPgudiFM6rlOlGRyA= + env: global: - SAUCE_USERNAME=snay - TRAVIS_NODE_VERSION="4.2" + matrix: include: - env: PLATFORM=browser-chrome @@ -74,6 +77,7 @@ matrix: - platform-tools - tools - build-tools-26.0.2 + before_install: - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm @@ -86,8 +90,10 @@ before_install: - git clone https://github.com/apache/cordova-paramedic /tmp/paramedic && pushd /tmp/paramedic && npm install && popd - npm install -g cordova + install: - npm install + script: - npm test - node /tmp/paramedic/main.js --config pr/$PLATFORM --plugin $(pwd) --shouldUseSauce From e75fe14c4ba01403c85c8f6a600d879af06c681f Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Tue, 31 Jul 2018 18:03:47 -0400 Subject: [PATCH 20/53] .appveyor.yml add npm i & add eslint to test (consistent with test script in package.json) --- .appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index a7b2426..80e8353 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,8 +21,10 @@ install: - node --version - npm install -g cordova-paramedic@https://github.com/apache/cordova-paramedic.git - npm install -g cordova + - npm install build: off test_script: + - npm run eslint - cordova-paramedic --config pr\%PLATFORM% --plugin . %JUST_BUILD% From a464ea6875142744a310d418a9d8d92f5d4097b4 Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Tue, 31 Jul 2018 19:02:30 -0400 Subject: [PATCH 21/53] .travis.yml quick fixes - skip some platform versions for now - mark FUTURE TBD platform versions --- .travis.yml | 68 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/.travis.yml b/.travis.yml index a8220a1..c0f3581 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,10 +19,11 @@ matrix: os: linux language: node_js node_js: '4.2' - - env: PLATFORM=browser-safari - os: linux - language: node_js - node_js: '4.2' + # TBD SKIP for now: + # - env: PLATFORM=browser-safari + # os: linux + # language: node_js + # node_js: '4.2' - env: PLATFORM=browser-edge os: linux language: node_js @@ -32,11 +33,15 @@ matrix: osx_image: xcode7.3 language: node_js node_js: '4.2' - - env: PLATFORM=ios-10.0 - os: osx - osx_image: xcode7.3 - language: node_js - node_js: '4.2' + # TBD SKIP for now: + # - env: PLATFORM=ios-10.0 + # os: osx + # osx_image: xcode7.3 + # language: node_js + # node_js: '4.2' + # FUTURE TBD: + # - env: PLATFORM=ios-11.4 + # ... - env: PLATFORM=android-4.4 os: linux language: android @@ -47,16 +52,17 @@ matrix: - platform-tools - tools - build-tools-26.0.2 - - env: PLATFORM=android-5.1 - os: linux - language: android - jdk: oraclejdk8 - android: - components: - - tools - - platform-tools - - tools - - build-tools-26.0.2 + # TBD SKIP for now: + # - env: PLATFORM=android-5.1 + # os: linux + # language: android + # jdk: oraclejdk8 + # android: + # components: + # - tools + # - platform-tools + # - tools + # - build-tools-26.0.2 - env: PLATFORM=android-6.0 os: linux language: android @@ -67,16 +73,20 @@ matrix: - platform-tools - tools - build-tools-26.0.2 - - env: PLATFORM=android-7.0 - os: linux - language: android - jdk: oraclejdk8 - android: - components: - - tools - - platform-tools - - tools - - build-tools-26.0.2 + # TBD SKIP for now: + # - env: PLATFORM=android-7.0 + # os: linux + # language: android + # jdk: oraclejdk8 + # android: + # components: + # - tools + # - platform-tools + # - tools + # - build-tools-26.0.2 + # FUTURE TBD: + # - env: PLATFORM=android-8.? + # ... before_install: - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm From 81f1437b6da838031611ad8cb05294929c49bf57 Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Tue, 31 Jul 2018 19:05:28 -0400 Subject: [PATCH 22/53] README.md add AppVeyor CI & Travis CI details --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 26ed518..f3e567a 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,11 @@ description: Open an in-app browser window. |AppVeyor|Travis CI| |:-:|:-:| |[![Build status](https://ci.appveyor.com/api/projects/status/github/apache/cordova-plugin-inappbrowser?branch=master)](https://ci.appveyor.com/project/ApacheSoftwareFoundation/cordova-plugin-inappbrowser)|[![Build Status](https://travis-ci.org/apache/cordova-plugin-inappbrowser.svg?branch=master)](https://travis-ci.org/apache/cordova-plugin-inappbrowser)| +|npm install|npm install| +|eslint|eslint| +||browser (chrome, firefox, edge)| +||iOS (9.3) on Xcode 7.3| +||Android (4.4, 6.0)| # cordova-plugin-inappbrowser From 07d9a9939b4a10bd6c323565fc356628942db8ba Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Tue, 31 Jul 2018 19:18:29 -0400 Subject: [PATCH 23/53] Skip Android 6.0 on Travis CI (for now) --- .travis.yml | 21 ++++++++++----------- README.md | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index c0f3581..00def86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,17 +63,16 @@ matrix: # - platform-tools # - tools # - build-tools-26.0.2 - - env: PLATFORM=android-6.0 - os: linux - language: android - jdk: oraclejdk8 - android: - components: - - tools - - platform-tools - - tools - - build-tools-26.0.2 - # TBD SKIP for now: + # - env: PLATFORM=android-6.0 + # os: linux + # language: android + # jdk: oraclejdk8 + # android: + # components: + # - tools + # - platform-tools + # - tools + # - build-tools-26.0.2 # - env: PLATFORM=android-7.0 # os: linux # language: android diff --git a/README.md b/README.md index f3e567a..eade58a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ description: Open an in-app browser window. |eslint|eslint| ||browser (chrome, firefox, edge)| ||iOS (9.3) on Xcode 7.3| -||Android (4.4, 6.0)| +||Android (4.4)| # cordova-plugin-inappbrowser From dc5329d15757cca1c327e87a309fddbb58e554ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Norstr=C3=B6m?= Date: Tue, 21 Aug 2018 10:28:56 +0200 Subject: [PATCH 24/53] CB-12875: (iOS) Pushes the inappbrowser window to a higher ui level than the existing apps window. (#284) --- .gitignore | 1 + src/ios/CDVInAppBrowser.m | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4474e73..f63dbbf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ Thumbs.db *.log *.swp *.user +*.idea node_modules diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index 5bdb3e1..fcd8d98 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -244,8 +244,9 @@ tmpWindow = [[UIWindow alloc] initWithFrame:frame]; } UIViewController *tmpController = [[UIViewController alloc] init]; + double baseWindowLevel = [UIApplication sharedApplication].keyWindow.windowLevel; [tmpWindow setRootViewController:tmpController]; - [tmpWindow setWindowLevel:UIWindowLevelNormal]; + [tmpWindow setWindowLevel:baseWindowLevel+1]; [tmpWindow makeKeyAndVisible]; [tmpController presentViewController:nav animated:YES completion:nil]; From cf58b04097ad53151f4c456d0fef27dd5cecd966 Mon Sep 17 00:00:00 2001 From: Dan Polivy Date: Wed, 22 Aug 2018 14:43:06 -0700 Subject: [PATCH 25/53] CB-14234: Don't call handleOpenURL for system URLs (#278) When calling `.open()` with a target of `_system`, the InAppBrowser on iOS is both launching the URL in the system browser AND also broadcasting to open the URL within the app (calling handleOpenURL). The latter behavior is problematic in many circumstances (e.g. when you want to explicitly open a link in a browser which is a universal link handled by the app). This commit attempts to address this by checking the return value from openURL -- if it does not open the URL successfully, then (and only then) the code falls back to broadcasting the event within the app to handleOpenURL. --- src/ios/CDVInAppBrowser.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index fcd8d98..0ca3feb 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -299,8 +299,9 @@ - (void)openInSystem:(NSURL*)url { - [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; - [[UIApplication sharedApplication] openURL:url]; + if ([[UIApplication sharedApplication] openURL:url] == NO) { + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + } } // This is a helper method for the inject{Script|Style}{Code|File} API calls, which From a0783378fc901a84294dc66908a91f78014d3fb6 Mon Sep 17 00:00:00 2001 From: Dan Polivy Date: Thu, 20 Sep 2018 13:43:52 -0700 Subject: [PATCH 26/53] [CB-10795] Exclude current app from external intent list (#154) On Android, if the app defines an intent-filter for a given URL, and then tries to use inappbrowser to launch that URL via the _system target, the default handler for that intent is the app itself. That behavior can lead to circular loops, and ultimately is not what the developer wants -- the link should be launched in a browser. Because there is no easy way to find the "default" system browser on a device, this solution will do two things: 1) Check if the app is one of the targets for this intent 2) If so, create a custom chooser with all other targets, excluding the current app. If the app is not a target, then the current (existing) behavior is preserved. The only real "downside" to this approach is that a default handler can no longer be set for these URLs within the app, and a chooser will be shown each time the user taps a link that opens in a new browser. Fixes https://issues.apache.org/jira/browse/CB-10795 --- src/android/InAppBrowser.java | 48 ++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 9a695db..92ca3c1 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -19,8 +19,12 @@ package org.apache.cordova.inappbrowser; import android.annotation.SuppressLint; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Parcelable; import android.provider.Browser; import android.content.res.Resources; import android.graphics.Bitmap; @@ -71,6 +75,7 @@ import org.json.JSONObject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.HashMap; @@ -432,7 +437,8 @@ public class InAppBrowser extends CordovaPlugin { intent.setData(uri); } intent.putExtra(Browser.EXTRA_APPLICATION_ID, cordova.getActivity().getPackageName()); - this.cordova.getActivity().startActivity(intent); + // CB-10795: Avoid circular loops by preventing it from opening in the current app + this.openExternalExcludeCurrentApp(intent); return ""; // not catching FileUriExposedException explicitly because buildtools<24 doesn't know about it } catch (java.lang.RuntimeException e) { @@ -441,6 +447,46 @@ public class InAppBrowser extends CordovaPlugin { } } + /** + * Opens the intent, providing a chooser that excludes the current app to avoid + * circular loops. + */ + private void openExternalExcludeCurrentApp(Intent intent) { + String currentPackage = cordova.getActivity().getPackageName(); + boolean hasCurrentPackage = false; + + PackageManager pm = cordova.getActivity().getPackageManager(); + List activities = pm.queryIntentActivities(intent, 0); + ArrayList targetIntents = new ArrayList(); + + for (ResolveInfo ri : activities) { + if (!currentPackage.equals(ri.activityInfo.packageName)) { + Intent targetIntent = (Intent)intent.clone(); + targetIntent.setPackage(ri.activityInfo.packageName); + targetIntents.add(targetIntent); + } + else { + hasCurrentPackage = true; + } + } + + // If the current app package isn't a target for this URL, then use + // the normal launch behavior + if (hasCurrentPackage == false || targetIntents.size() == 0) { + this.cordova.getActivity().startActivity(intent); + } + // If there's only one possible intent, launch it directly + else if (targetIntents.size() == 1) { + this.cordova.getActivity().startActivity(targetIntents.get(0)); + } + // Otherwise, show a custom chooser without the current app listed + else if (targetIntents.size() > 0) { + Intent chooser = Intent.createChooser(targetIntents.remove(targetIntents.size()-1), null); + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetIntents.toArray(new Parcelable[] {})); + this.cordova.getActivity().startActivity(chooser); + } + } + /** * Closes the dialog */ From 27e6c67909d5fc6e03020648bf48ef9b3c4a95f5 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Mon, 3 Sep 2018 13:12:15 +0100 Subject: [PATCH 27/53] CB-7179 (iOS): Add support to optionally use WKWebView for iOS --- .travis.yml | 4 +- README.md | 14 +- RELEASENOTES.md | 3 + package.json | 2 +- plugin.xml | 22 +- src/ios/CDVInAppBrowser.h | 90 +- src/ios/CDVInAppBrowser.m | 1133 +--------------- src/ios/CDVInAppBrowserNavigationController.h | 27 + src/ios/CDVInAppBrowserNavigationController.m | 85 ++ src/ios/CDVInAppBrowserOptions.h | 51 + src/ios/CDVInAppBrowserOptions.m | 91 ++ src/ios/CDVUIInAppBrowser.h | 86 ++ src/ios/CDVUIInAppBrowser.m | 1029 +++++++++++++++ src/ios/CDVWKInAppBrowser.h | 76 ++ src/ios/CDVWKInAppBrowser.m | 1164 +++++++++++++++++ src/ios/CDVWKInAppBrowserUIDelegate.h | 32 + src/ios/CDVWKInAppBrowserUIDelegate.m | 127 ++ tests/plugin.xml | 6 + tests/tests.js | 292 +++-- 19 files changed, 3035 insertions(+), 1299 deletions(-) create mode 100644 src/ios/CDVInAppBrowserNavigationController.h create mode 100644 src/ios/CDVInAppBrowserNavigationController.m create mode 100644 src/ios/CDVInAppBrowserOptions.h create mode 100644 src/ios/CDVInAppBrowserOptions.m create mode 100644 src/ios/CDVUIInAppBrowser.h create mode 100644 src/ios/CDVUIInAppBrowser.m create mode 100644 src/ios/CDVWKInAppBrowser.h create mode 100644 src/ios/CDVWKInAppBrowser.m create mode 100644 src/ios/CDVWKInAppBrowserUIDelegate.h create mode 100644 src/ios/CDVWKInAppBrowserUIDelegate.m diff --git a/.travis.yml b/.travis.yml index 00def86..2be2bec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,9 +28,9 @@ matrix: os: linux language: node_js node_js: '4.2' - - env: PLATFORM=ios-9.3 + - env: PLATFORM=ios-10.0 os: osx - osx_image: xcode7.3 + osx_image: xcode8.3 language: node_js node_js: '4.2' # TBD SKIP for now: diff --git a/README.md b/README.md index eade58a..b7ff468 100644 --- a/README.md +++ b/README.md @@ -136,9 +136,11 @@ instance, or the system browser. iOS supports these additional options: + - __usewkwebview__: set to `yes` to use WKWebView engine for the InappBrowser. Omit or set to `no` (default) to use UIWebView. Note: Using `usewkwebview=yes` requires that a WKWebView engine plugin be installed in the Cordova project (e.g. [cordova-plugin-wkwebview-engine](https://github.com/apache/cordova-plugin-wkwebview-engine) or [cordova-plugin-ionic-webview](https://github.com/ionic-team/cordova-plugin-ionic-webview)). - __hidden__: set to `yes` to create the browser and load the page, but not show it. The loadstop event fires when loading is complete. Omit or set to `no` (default) to have the browser open and load normally. - __clearcache__: set to `yes` to have the browser's cookie cache cleared before the new window is opened - - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened + - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened. For WKWebView, requires iOS 11+ on target device. + - __cleardata__: set to `yes` to have the browser's entire local storage cleared (cookies, HTML5 local storage, IndexedDB, etc.) before the new window is opened - __closebuttoncolor__: set as a valid hex color string, for example: `#00ff00`, to change from the default __Done__ button's color. Only applicable if toolbar is not disabled. - __closebuttoncaption__: set to a string to use as the __Done__ button's caption. Note that you need to localize this value yourself. - __disallowoverscroll__: Set to `yes` or `no` (default is `no`). Turns on/off the UIWebViewBounce property. @@ -147,11 +149,11 @@ instance, or the system browser. - __toolbar__: set to `yes` or `no` to turn the toolbar on or off for the InAppBrowser (defaults to `yes`) - __toolbarcolor__: set as a valid hex color string, for example: `#00ff00`, to change from the default color of the toolbar. Only applicable if toolbar is not disabled. - __toolbartranslucent__: set to `yes` or `no` to make the toolbar translucent(semi-transparent) (defaults to `yes`). Only applicable if toolbar is not disabled. - - __enableViewportScale__: Set to `yes` or `no` to prevent viewport scaling through a meta tag (defaults to `no`). - - __mediaPlaybackRequiresUserAction__: Set to `yes` to prevent HTML5 audio or video from autoplaying (defaults to `no`). - - __allowInlineMediaPlayback__: Set to `yes` or `no` to allow in-line HTML5 media playback, displaying within the browser window rather than a device-specific playback interface. The HTML's `video` element must also include the `webkit-playsinline` attribute (defaults to `no`) - - __keyboardDisplayRequiresUserAction__: Set to `yes` or `no` to open the keyboard when form elements receive focus via JavaScript's `focus()` call (defaults to `yes`). - - __suppressesIncrementalRendering__: Set to `yes` or `no` to wait until all new view content is received before being rendered (defaults to `no`). + - __enableViewportScale__: Set to `yes` or `no` to prevent viewport scaling through a meta tag (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`). + - __mediaPlaybackRequiresUserAction__: Set to `yes` to prevent HTML5 audio or video from autoplaying (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`). + - __allowInlineMediaPlayback__: Set to `yes` or `no` to allow in-line HTML5 media playback, displaying within the browser window rather than a device-specific playback interface. The HTML's `video` element must also include the `webkit-playsinline` attribute (defaults to `no`) Only applicable to UIWebView (`usewkwebview=no`). + - __keyboardDisplayRequiresUserAction__: Set to `yes` or `no` to open the keyboard when form elements receive focus via JavaScript's `focus()` call (defaults to `yes`). Only applicable to UIWebView (`usewkwebview=no`). + - __suppressesIncrementalRendering__: Set to `yes` or `no` to wait until all new view content is received before being rendered (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`). - __presentationstyle__: Set to `pagesheet`, `formsheet` or `fullscreen` to set the [presentation style](http://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/modalPresentationStyle) (defaults to `fullscreen`). - __transitionstyle__: Set to `fliphorizontal`, `crossdissolve` or `coververtical` to set the [transition style](http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/modalTransitionStyle) (defaults to `coververtical`). - __toolbarposition__: Set to `top` or `bottom` (default is `bottom`). Causes the toolbar to be at the top or bottom of the window. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6a06956..0812b8d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,9 @@ --> # Release Notes +### 3.1.0-dev (Unreleased) +* CB-7179 (iOS): Add support to optionally use WKWebView for iOS + ### 3.0.0 (Apr 12, 2018) * [CB-13659](https://issues.apache.org/jira/browse/CB-13659) **iOS** Add hidespinner option * In file `AppBrowser.java`: New code within `shouldOverrideUrlLoading()` to check for whitelisting custom schemes via a new `AllowedSchemes` preference configuration item. Allows custom schemes like `mycoolapp://` or `wevotetwitterscheme://` diff --git a/package.json b/package.json index a7f57c3..f3be23f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-inappbrowser", - "version": "3.0.1-dev", + "version": "3.1.0-dev", "description": "Cordova InAppBrowser Plugin", "types": "./types/index.d.ts", "cordova": { diff --git a/plugin.xml b/plugin.xml index 4a820dd..8775d61 100644 --- a/plugin.xml +++ b/plugin.xml @@ -20,7 +20,7 @@ + version="3.1.0-dev"> InAppBrowser Cordova InAppBrowser Plugin @@ -76,11 +76,31 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/ios/CDVInAppBrowser.h b/src/ios/CDVInAppBrowser.h index 66066b9..d40e92b 100644 --- a/src/ios/CDVInAppBrowser.h +++ b/src/ios/CDVInAppBrowser.h @@ -19,23 +19,11 @@ #import #import -#import -#ifdef __CORDOVA_4_0_0 - #import -#else - #import -#endif +@interface CDVInAppBrowser : CDVPlugin {} -@class CDVInAppBrowserViewController; - -@interface CDVInAppBrowser : CDVPlugin { - UIWindow * tmpWindow; -} - -@property (nonatomic, retain) CDVInAppBrowserViewController* inAppBrowserViewController; -@property (nonatomic, copy) NSString* callbackId; -@property (nonatomic, copy) NSRegularExpression *callbackIdPattern; +@property (nonatomic, assign) BOOL wkwebviewavailable; +@property (nonatomic, assign) BOOL usewkwebview; - (void)open:(CDVInvokedUrlCommand*)command; - (void)close:(CDVInvokedUrlCommand*)command; @@ -45,75 +33,3 @@ @end -@interface CDVInAppBrowserOptions : NSObject {} - -@property (nonatomic, assign) BOOL location; -@property (nonatomic, assign) BOOL toolbar; -@property (nonatomic, copy) NSString* closebuttoncaption; -@property (nonatomic, copy) NSString* closebuttoncolor; -@property (nonatomic, copy) NSString* toolbarposition; -@property (nonatomic, copy) NSString* toolbarcolor; -@property (nonatomic, assign) BOOL toolbartranslucent; -@property (nonatomic, assign) BOOL hidenavigationbuttons; -@property (nonatomic, copy) NSString* navigationbuttoncolor; -@property (nonatomic, assign) BOOL clearcache; -@property (nonatomic, assign) BOOL clearsessioncache; -@property (nonatomic, assign) BOOL hidespinner; - -@property (nonatomic, copy) NSString* presentationstyle; -@property (nonatomic, copy) NSString* transitionstyle; - -@property (nonatomic, assign) BOOL enableviewportscale; -@property (nonatomic, assign) BOOL mediaplaybackrequiresuseraction; -@property (nonatomic, assign) BOOL allowinlinemediaplayback; -@property (nonatomic, assign) BOOL keyboarddisplayrequiresuseraction; -@property (nonatomic, assign) BOOL suppressesincrementalrendering; -@property (nonatomic, assign) BOOL hidden; -@property (nonatomic, assign) BOOL disallowoverscroll; - -+ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; - -@end - -@interface CDVInAppBrowserViewController : UIViewController { - @private - NSString* _userAgent; - NSString* _prevUserAgent; - NSInteger _userAgentLockToken; - CDVInAppBrowserOptions *_browserOptions; - -#ifdef __CORDOVA_4_0_0 - CDVUIWebViewDelegate* _webViewDelegate; -#else - CDVWebViewDelegate* _webViewDelegate; -#endif - -} - -@property (nonatomic, strong) IBOutlet UIWebView* webView; -@property (nonatomic, strong) IBOutlet UIBarButtonItem* closeButton; -@property (nonatomic, strong) IBOutlet UILabel* addressLabel; -@property (nonatomic, strong) IBOutlet UIBarButtonItem* backButton; -@property (nonatomic, strong) IBOutlet UIBarButtonItem* forwardButton; -@property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; -@property (nonatomic, strong) IBOutlet UIToolbar* toolbar; - -@property (nonatomic, weak) id orientationDelegate; -@property (nonatomic, weak) CDVInAppBrowser* navigationDelegate; -@property (nonatomic) NSURL* currentURL; - -- (void)close; -- (void)navigateTo:(NSURL*)url; -- (void)showLocationBar:(BOOL)show; -- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition; -- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString; - -- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions; - -@end - -@interface CDVInAppBrowserNavigationController : UINavigationController - -@property (nonatomic, weak) id orientationDelegate; - -@end diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index 0ca3feb..ad30b23 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -18,1136 +18,107 @@ */ #import "CDVInAppBrowser.h" +#import "CDVInAppBrowserOptions.h" +#import "CDVUIInAppBrowser.h" +#import "CDVWKInAppBrowser.h" #import -#import -#define kInAppBrowserTargetSelf @"_self" -#define kInAppBrowserTargetSystem @"_system" -#define kInAppBrowserTargetBlank @"_blank" - -#define kInAppBrowserToolbarBarPositionBottom @"bottom" -#define kInAppBrowserToolbarBarPositionTop @"top" - -#define TOOLBAR_HEIGHT 44.0 -#define STATUSBAR_HEIGHT 20.0 -#define LOCATIONBAR_HEIGHT 21.0 -#define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) #pragma mark CDVInAppBrowser -@interface CDVInAppBrowser () { - NSInteger _previousStatusBarStyle; -} -@end - @implementation CDVInAppBrowser - (void)pluginInitialize { - _previousStatusBarStyle = -1; - _callbackIdPattern = nil; -} + // default values + self.usewkwebview = NO; -- (id)settingForKey:(NSString*)key -{ - return [self.commandDelegate.settings objectForKey:[key lowercaseString]]; -} - -- (void)onReset -{ - [self close:nil]; -} - -- (void)close:(CDVInvokedUrlCommand*)command -{ - if (self.inAppBrowserViewController == nil) { - NSLog(@"IAB.close() called but it was already closed."); - return; - } - // Things are cleaned up in browserExit. - [self.inAppBrowserViewController close]; -} - -- (BOOL) isSystemUrl:(NSURL*)url -{ - if ([[url host] isEqualToString:@"itunes.apple.com"]) { - return YES; - } - - return NO; +#if __has_include("CDVWKWebViewEngine.h") + self.wkwebviewavailable = YES; +#else + self.wkwebviewavailable = NO; +#endif } - (void)open:(CDVInvokedUrlCommand*)command { - CDVPluginResult* pluginResult; - - NSString* url = [command argumentAtIndex:0]; - NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf]; NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; - - self.callbackId = command.callbackId; - - if (url != nil) { -#ifdef __CORDOVA_4_0_0 - NSURL* baseUrl = [self.webViewEngine URL]; -#else - NSURL* baseUrl = [self.webView.request URL]; -#endif - NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; - - if ([self isSystemUrl:absoluteUrl]) { - target = kInAppBrowserTargetSystem; - } - - if ([target isEqualToString:kInAppBrowserTargetSelf]) { - [self openInCordovaWebView:absoluteUrl withOptions:options]; - } else if ([target isEqualToString:kInAppBrowserTargetSystem]) { - [self openInSystem:absoluteUrl]; - } else { // _blank or anything else - [self openInInAppBrowser:absoluteUrl withOptions:options]; - } - - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; - } else { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"]; - } - - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; -} - -- (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options -{ CDVInAppBrowserOptions* browserOptions = [CDVInAppBrowserOptions parseOptions:options]; - - if (browserOptions.clearcache) { - NSHTTPCookie *cookie; - NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - for (cookie in [storage cookies]) - { - if (![cookie.domain isEqual: @".^filecookies^"]) { - [storage deleteCookie:cookie]; - } - } + if(browserOptions.usewkwebview && !self.wkwebviewavailable){ + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:@{@"type":@"loaderror", @"message": @"usewkwebview option specified but but no plugin that supplies a WKWebView engine is present"}] callbackId:command.callbackId]; + return; } - - if (browserOptions.clearsessioncache) { - NSHTTPCookie *cookie; - NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - for (cookie in [storage cookies]) - { - if (![cookie.domain isEqual: @".^filecookies^"] && cookie.isSessionOnly) { - [storage deleteCookie:cookie]; - } - } - } - - if (self.inAppBrowserViewController == nil) { - NSString* userAgent = [CDVUserAgentUtil originalUserAgent]; - NSString* overrideUserAgent = [self settingForKey:@"OverrideUserAgent"]; - NSString* appendUserAgent = [self settingForKey:@"AppendUserAgent"]; - if(overrideUserAgent){ - userAgent = overrideUserAgent; - } - if(appendUserAgent){ - userAgent = [userAgent stringByAppendingString: appendUserAgent]; - } - self.inAppBrowserViewController = [[CDVInAppBrowserViewController alloc] initWithUserAgent:userAgent prevUserAgent:[self.commandDelegate userAgent] browserOptions: browserOptions]; - self.inAppBrowserViewController.navigationDelegate = self; - - if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { - self.inAppBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; - } - } - - [self.inAppBrowserViewController showLocationBar:browserOptions.location]; - [self.inAppBrowserViewController showToolBar:browserOptions.toolbar :browserOptions.toolbarposition]; - if (browserOptions.closebuttoncaption != nil || browserOptions.closebuttoncolor != nil) { - [self.inAppBrowserViewController setCloseButtonTitle:browserOptions.closebuttoncaption :browserOptions.closebuttoncolor]; - } - // Set Presentation Style - UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default - if (browserOptions.presentationstyle != nil) { - if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"pagesheet"]) { - presentationStyle = UIModalPresentationPageSheet; - } else if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"formsheet"]) { - presentationStyle = UIModalPresentationFormSheet; - } - } - self.inAppBrowserViewController.modalPresentationStyle = presentationStyle; - - // Set Transition Style - UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default - if (browserOptions.transitionstyle != nil) { - if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"fliphorizontal"]) { - transitionStyle = UIModalTransitionStyleFlipHorizontal; - } else if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"crossdissolve"]) { - transitionStyle = UIModalTransitionStyleCrossDissolve; - } - } - self.inAppBrowserViewController.modalTransitionStyle = transitionStyle; - - // prevent webView from bouncing - if (browserOptions.disallowoverscroll) { - if ([self.inAppBrowserViewController.webView respondsToSelector:@selector(scrollView)]) { - ((UIScrollView*)[self.inAppBrowserViewController.webView scrollView]).bounces = NO; - } else { - for (id subview in self.inAppBrowserViewController.webView.subviews) { - if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { - ((UIScrollView*)subview).bounces = NO; - } - } - } - } - - // UIWebView options - self.inAppBrowserViewController.webView.scalesPageToFit = browserOptions.enableviewportscale; - self.inAppBrowserViewController.webView.mediaPlaybackRequiresUserAction = browserOptions.mediaplaybackrequiresuseraction; - self.inAppBrowserViewController.webView.allowsInlineMediaPlayback = browserOptions.allowinlinemediaplayback; - if (IsAtLeastiOSVersion(@"6.0")) { - self.inAppBrowserViewController.webView.keyboardDisplayRequiresUserAction = browserOptions.keyboarddisplayrequiresuseraction; - self.inAppBrowserViewController.webView.suppressesIncrementalRendering = browserOptions.suppressesincrementalrendering; - } - - [self.inAppBrowserViewController navigateTo:url]; - if (!browserOptions.hidden) { - [self show:nil]; + self.usewkwebview = browserOptions.usewkwebview; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] open:command]; + }else{ + [[CDVUIInAppBrowser getInstance] open:command]; } } +- (void)close:(CDVInvokedUrlCommand*)command +{ + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] close:command]; + }else{ + [[CDVUIInAppBrowser getInstance] close:command]; + } +} + + - (void)show:(CDVInvokedUrlCommand*)command { - if (self.inAppBrowserViewController == nil) { - NSLog(@"Tried to show IAB after it was closed."); - return; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] show:command]; + }else{ + [[CDVUIInAppBrowser getInstance] show:command]; } - if (_previousStatusBarStyle != -1) { - NSLog(@"Tried to show IAB while already shown"); - return; - } - - _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; - - __block CDVInAppBrowserNavigationController* nav = [[CDVInAppBrowserNavigationController alloc] - initWithRootViewController:self.inAppBrowserViewController]; - nav.orientationDelegate = self.inAppBrowserViewController; - nav.navigationBarHidden = YES; - nav.modalPresentationStyle = self.inAppBrowserViewController.modalPresentationStyle; - - __weak CDVInAppBrowser* weakSelf = self; - - // Run later to avoid the "took a long time" log message. - dispatch_async(dispatch_get_main_queue(), ^{ - if (weakSelf.inAppBrowserViewController != nil) { - if (!tmpWindow) { - CGRect frame = [[UIScreen mainScreen] bounds]; - tmpWindow = [[UIWindow alloc] initWithFrame:frame]; - } - UIViewController *tmpController = [[UIViewController alloc] init]; - double baseWindowLevel = [UIApplication sharedApplication].keyWindow.windowLevel; - [tmpWindow setRootViewController:tmpController]; - [tmpWindow setWindowLevel:baseWindowLevel+1]; - - [tmpWindow makeKeyAndVisible]; - [tmpController presentViewController:nav animated:YES completion:nil]; - } - }); } - (void)hide:(CDVInvokedUrlCommand*)command { - if (self.inAppBrowserViewController == nil) { - NSLog(@"Tried to hide IAB after it was closed."); - return; - - - } - if (_previousStatusBarStyle == -1) { - NSLog(@"Tried to hide IAB while already hidden"); - return; - } - - _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; - - // Run later to avoid the "took a long time" log message. - dispatch_async(dispatch_get_main_queue(), ^{ - if (self.inAppBrowserViewController != nil) { - _previousStatusBarStyle = -1; - [self.inAppBrowserViewController.presentingViewController dismissViewControllerAnimated:YES completion:^{ - [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; - }]; - } - }); -} - -- (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options -{ - NSURLRequest* request = [NSURLRequest requestWithURL:url]; - -#ifdef __CORDOVA_4_0_0 - // the webview engine itself will filter for this according to policy - // in config.xml for cordova-ios-4.0 - [self.webViewEngine loadRequest:request]; -#else - if ([self.commandDelegate URLIsWhitelisted:url]) { - [self.webView loadRequest:request]; - } else { // this assumes the InAppBrowser can be excepted from the white-list - [self openInInAppBrowser:url withOptions:options]; - } -#endif -} - -- (void)openInSystem:(NSURL*)url -{ - if ([[UIApplication sharedApplication] openURL:url] == NO) { - [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] hide:command]; + }else{ + [[CDVUIInAppBrowser getInstance] hide:command]; } } -// This is a helper method for the inject{Script|Style}{Code|File} API calls, which -// provides a consistent method for injecting JavaScript code into the document. -// -// If a wrapper string is supplied, then the source string will be JSON-encoded (adding -// quotes) and wrapped using string formatting. (The wrapper string should have a single -// '%@' marker). -// -// If no wrapper is supplied, then the source string is executed directly. - -- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper -{ - // Ensure an iframe bridge is created to communicate with the CDVInAppBrowserViewController - [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){_cdvIframeBridge=d.getElementById('_cdvIframeBridge');if(!_cdvIframeBridge) {var e = _cdvIframeBridge = d.createElement('iframe');e.id='_cdvIframeBridge'; e.style.display='none';d.body.appendChild(e);}})(document)"]; - - if (jsWrapper != nil) { - NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; - NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - if (sourceArrayString) { - NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)]; - NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString]; - [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:jsToInject]; - } - } else { - [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:source]; - } -} - (void)injectScriptCode:(CDVInvokedUrlCommand*)command { - NSString* jsWrapper = nil; - - if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { - jsWrapper = [NSString stringWithFormat:@"_cdvIframeBridge.src='gap-iab://%@/'+encodeURIComponent(JSON.stringify([eval(%%@)]));", command.callbackId]; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] injectScriptCode:command]; + }else{ + [[CDVUIInAppBrowser getInstance] injectScriptCode:command]; } - [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; } - (void)injectScriptFile:(CDVInvokedUrlCommand*)command { - NSString* jsWrapper; - - if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { - jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; - } else { - jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)"; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] injectScriptFile:command]; + }else{ + [[CDVUIInAppBrowser getInstance] injectScriptFile:command]; } - [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; } - (void)injectStyleCode:(CDVInvokedUrlCommand*)command { - NSString* jsWrapper; - - if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { - jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; - } else { - jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)"; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] injectStyleCode:command]; + }else{ + [[CDVUIInAppBrowser getInstance] injectStyleCode:command]; } - [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; } - (void)injectStyleFile:(CDVInvokedUrlCommand*)command { - NSString* jsWrapper; - - if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { - jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; - } else { - jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)"; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] injectStyleFile:command]; + }else{ + [[CDVUIInAppBrowser getInstance] injectStyleFile:command]; } - [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; -} - -- (BOOL)isValidCallbackId:(NSString *)callbackId -{ - NSError *err = nil; - // Initialize on first use - if (self.callbackIdPattern == nil) { - self.callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"^InAppBrowser[0-9]{1,10}$" options:0 error:&err]; - if (err != nil) { - // Couldn't initialize Regex; No is safer than Yes. - return NO; - } - } - if ([self.callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) { - return YES; - } - return NO; -} - -/** - * The iframe bridge provided for the InAppBrowser is capable of executing any oustanding callback belonging - * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no - * other code execution is possible. - * - * To trigger the bridge, the iframe (or any other resource) should attempt to load a url of the form: - * - * gap-iab:/// - * - * where is the string id of the callback to trigger (something like "InAppBrowser0123456789") - * - * If present, the path component of the special gap-iab:// url is expected to be a URL-escaped JSON-encoded - * value to pass to the callback. [NSURL path] should take care of the URL-unescaping, and a JSON_EXCEPTION - * is returned if the JSON is invalid. - */ -- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType -{ - NSURL* url = request.URL; - BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; - - // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute, - // and the path, if present, should be a JSON-encoded value to pass to the callback. - if ([[url scheme] isEqualToString:@"gap-iab"]) { - NSString* scriptCallbackId = [url host]; - CDVPluginResult* pluginResult = nil; - - if ([self isValidCallbackId:scriptCallbackId]) { - NSString* scriptResult = [url path]; - NSError* __autoreleasing error = nil; - - // The message should be a JSON-encoded array of the result of the script which executed. - if ((scriptResult != nil) && ([scriptResult length] > 1)) { - scriptResult = [scriptResult substringFromIndex:1]; - NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; - if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; - } else { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; - } - } else { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; - } - [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; - return NO; - } - } - //if is an app store link, let the system handle it, otherwise it fails to load it - else if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { - [theWebView stopLoading]; - [self openInSystem:url]; - return NO; - } - else if ((self.callbackId != nil) && isTopLevelNavigation) { - // Send a loadstart event for each top-level navigation (includes redirects). - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK - messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}]; - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; - } - - return YES; -} - -- (void)webViewDidStartLoad:(UIWebView*)theWebView -{ -} - -- (void)webViewDidFinishLoad:(UIWebView*)theWebView -{ - if (self.callbackId != nil) { - // TODO: It would be more useful to return the URL the page is actually on (e.g. if it's been redirected). - NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK - messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; - } -} - -- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error -{ - if (self.callbackId != nil) { - NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR - messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInteger:error.code], @"message": error.localizedDescription}]; - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; - } -} - -- (void)browserExit -{ - if (self.callbackId != nil) { - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK - messageAsDictionary:@{@"type":@"exit"}]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; - self.callbackId = nil; - } - // Set navigationDelegate to nil to ensure no callbacks are received from it. - self.inAppBrowserViewController.navigationDelegate = nil; - // Don't recycle the ViewController since it may be consuming a lot of memory. - // Also - this is required for the PDF/User-Agent bug work-around. - self.inAppBrowserViewController = nil; - - if (IsAtLeastiOSVersion(@"7.0")) { - if (_previousStatusBarStyle != -1) { - [[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle]; - } - } - - _previousStatusBarStyle = -1; // this value was reset before reapplying it. caused statusbar to stay black on ios7 -} - -@end - -#pragma mark CDVInAppBrowserViewController - -@implementation CDVInAppBrowserViewController - -@synthesize currentURL; - -- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions -{ - self = [super init]; - if (self != nil) { - _userAgent = userAgent; - _prevUserAgent = prevUserAgent; - _browserOptions = browserOptions; -#ifdef __CORDOVA_4_0_0 - _webViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self]; -#else - _webViewDelegate = [[CDVWebViewDelegate alloc] initWithDelegate:self]; -#endif - - [self createViews]; - } - - return self; -} - -// Prevent crashes on closing windows --(void)dealloc { - self.webView.delegate = nil; -} - -- (void)createViews -{ - // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included - - CGRect webViewBounds = self.view.bounds; - BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]; - webViewBounds.size.height -= _browserOptions.location ? FOOTER_HEIGHT : TOOLBAR_HEIGHT; - self.webView = [[UIWebView alloc] initWithFrame:webViewBounds]; - - self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); - - [self.view addSubview:self.webView]; - [self.view sendSubviewToBack:self.webView]; - - self.webView.delegate = _webViewDelegate; - self.webView.backgroundColor = [UIColor whiteColor]; - - self.webView.clearsContextBeforeDrawing = YES; - self.webView.clipsToBounds = YES; - self.webView.contentMode = UIViewContentModeScaleToFill; - self.webView.multipleTouchEnabled = YES; - self.webView.opaque = YES; - self.webView.scalesPageToFit = NO; - self.webView.userInteractionEnabled = YES; - - self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; - self.spinner.alpha = 1.000; - self.spinner.autoresizesSubviews = YES; - self.spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin); - self.spinner.clearsContextBeforeDrawing = NO; - self.spinner.clipsToBounds = NO; - self.spinner.contentMode = UIViewContentModeScaleToFill; - self.spinner.frame = CGRectMake(CGRectGetMidX(self.webView.frame), CGRectGetMidY(self.webView.frame), 20.0, 20.0); - self.spinner.hidden = NO; - self.spinner.hidesWhenStopped = YES; - self.spinner.multipleTouchEnabled = NO; - self.spinner.opaque = NO; - self.spinner.userInteractionEnabled = NO; - [self.spinner stopAnimating]; - - self.closeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; - self.closeButton.enabled = YES; - - UIBarButtonItem* flexibleSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; - - UIBarButtonItem* fixedSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; - fixedSpaceButton.width = 20; - - float toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - TOOLBAR_HEIGHT : 0.0; - CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, TOOLBAR_HEIGHT); - - self.toolbar = [[UIToolbar alloc] initWithFrame:toolbarFrame]; - self.toolbar.alpha = 1.000; - self.toolbar.autoresizesSubviews = YES; - self.toolbar.autoresizingMask = toolbarIsAtBottom ? (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin) : UIViewAutoresizingFlexibleWidth; - self.toolbar.barStyle = UIBarStyleBlackOpaque; - self.toolbar.clearsContextBeforeDrawing = NO; - self.toolbar.clipsToBounds = NO; - self.toolbar.contentMode = UIViewContentModeScaleToFill; - self.toolbar.hidden = NO; - self.toolbar.multipleTouchEnabled = NO; - self.toolbar.opaque = NO; - self.toolbar.userInteractionEnabled = YES; - if (_browserOptions.toolbarcolor != nil) { // Set toolbar color if user sets it in options - self.toolbar.barTintColor = [self colorFromHexString:_browserOptions.toolbarcolor]; - } - if (!_browserOptions.toolbartranslucent) { // Set toolbar translucent to no if user sets it in options - self.toolbar.translucent = NO; - } - - CGFloat labelInset = 5.0; - float locationBarY = toolbarIsAtBottom ? self.view.bounds.size.height - FOOTER_HEIGHT : self.view.bounds.size.height - LOCATIONBAR_HEIGHT; - - self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, locationBarY, self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)]; - self.addressLabel.adjustsFontSizeToFitWidth = NO; - self.addressLabel.alpha = 1.000; - self.addressLabel.autoresizesSubviews = YES; - self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; - self.addressLabel.backgroundColor = [UIColor clearColor]; - self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; - self.addressLabel.clearsContextBeforeDrawing = YES; - self.addressLabel.clipsToBounds = YES; - self.addressLabel.contentMode = UIViewContentModeScaleToFill; - self.addressLabel.enabled = YES; - self.addressLabel.hidden = NO; - self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail; - - if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumScaleFactor:")]) { - [self.addressLabel setValue:@(10.0/[UIFont labelFontSize]) forKey:@"minimumScaleFactor"]; - } else if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumFontSize:")]) { - [self.addressLabel setValue:@(10.0) forKey:@"minimumFontSize"]; - } - - self.addressLabel.multipleTouchEnabled = NO; - self.addressLabel.numberOfLines = 1; - self.addressLabel.opaque = NO; - self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0); - self.addressLabel.text = NSLocalizedString(@"Loading...", nil); - self.addressLabel.textAlignment = NSTextAlignmentLeft; - self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000]; - self.addressLabel.userInteractionEnabled = NO; - - NSString* frontArrowString = NSLocalizedString(@"►", nil); // create arrow from Unicode char - self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; - self.forwardButton.enabled = YES; - self.forwardButton.imageInsets = UIEdgeInsetsZero; - if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options - self.forwardButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; - } - - NSString* backArrowString = NSLocalizedString(@"◄", nil); // create arrow from Unicode char - self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; - self.backButton.enabled = YES; - self.backButton.imageInsets = UIEdgeInsetsZero; - if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options - self.backButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; - } - - // Filter out Navigation Buttons if user requests so - if (_browserOptions.hidenavigationbuttons) { - [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton]]; - } else { - [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; - } - - self.view.backgroundColor = [UIColor grayColor]; - [self.view addSubview:self.toolbar]; - [self.view addSubview:self.addressLabel]; - [self.view addSubview:self.spinner]; -} - -- (void) setWebViewFrame : (CGRect) frame { - NSLog(@"Setting the WebView's frame to %@", NSStringFromCGRect(frame)); - [self.webView setFrame:frame]; -} - -- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString -{ - // the advantage of using UIBarButtonSystemItemDone is the system will localize it for you automatically - // but, if you want to set this yourself, knock yourself out (we can't set the title for a system Done button, so we have to create a new one) - self.closeButton = nil; - // Initialize with title if title is set, otherwise the title will be 'Done' localized - self.closeButton = title != nil ? [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)] : [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; - self.closeButton.enabled = YES; - // If color on closebutton is requested then initialize with that that color, otherwise use initialize with default - self.closeButton.tintColor = colorString != nil ? [self colorFromHexString:colorString] : [UIColor colorWithRed:60.0 / 255.0 green:136.0 / 255.0 blue:230.0 / 255.0 alpha:1]; - - NSMutableArray* items = [self.toolbar.items mutableCopy]; - [items replaceObjectAtIndex:0 withObject:self.closeButton]; - [self.toolbar setItems:items]; -} - -- (void)showLocationBar:(BOOL)show -{ - CGRect locationbarFrame = self.addressLabel.frame; - - BOOL toolbarVisible = !self.toolbar.hidden; - - // prevent double show/hide - if (show == !(self.addressLabel.hidden)) { - return; - } - - if (show) { - self.addressLabel.hidden = NO; - - if (toolbarVisible) { - // toolBar at the bottom, leave as is - // put locationBar on top of the toolBar - - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= FOOTER_HEIGHT; - [self setWebViewFrame:webViewBounds]; - - locationbarFrame.origin.y = webViewBounds.size.height; - self.addressLabel.frame = locationbarFrame; - } else { - // no toolBar, so put locationBar at the bottom - - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= LOCATIONBAR_HEIGHT; - [self setWebViewFrame:webViewBounds]; - - locationbarFrame.origin.y = webViewBounds.size.height; - self.addressLabel.frame = locationbarFrame; - } - } else { - self.addressLabel.hidden = YES; - - if (toolbarVisible) { - // locationBar is on top of toolBar, hide locationBar - - // webView take up whole height less toolBar height - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= TOOLBAR_HEIGHT; - [self setWebViewFrame:webViewBounds]; - } else { - // no toolBar, expand webView to screen dimensions - [self setWebViewFrame:self.view.bounds]; - } - } -} - -- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition -{ - CGRect toolbarFrame = self.toolbar.frame; - CGRect locationbarFrame = self.addressLabel.frame; - - BOOL locationbarVisible = !self.addressLabel.hidden; - - // prevent double show/hide - if (show == !(self.toolbar.hidden)) { - return; - } - - if (show) { - self.toolbar.hidden = NO; - CGRect webViewBounds = self.view.bounds; - - if (locationbarVisible) { - // locationBar at the bottom, move locationBar up - // put toolBar at the bottom - webViewBounds.size.height -= FOOTER_HEIGHT; - locationbarFrame.origin.y = webViewBounds.size.height; - self.addressLabel.frame = locationbarFrame; - self.toolbar.frame = toolbarFrame; - } else { - // no locationBar, so put toolBar at the bottom - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= TOOLBAR_HEIGHT; - self.toolbar.frame = toolbarFrame; - } - - if ([toolbarPosition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { - toolbarFrame.origin.y = 0; - webViewBounds.origin.y += toolbarFrame.size.height; - [self setWebViewFrame:webViewBounds]; - } else { - toolbarFrame.origin.y = (webViewBounds.size.height + LOCATIONBAR_HEIGHT); - } - [self setWebViewFrame:webViewBounds]; - - } else { - self.toolbar.hidden = YES; - - if (locationbarVisible) { - // locationBar is on top of toolBar, hide toolBar - // put locationBar at the bottom - - // webView take up whole height less locationBar height - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= LOCATIONBAR_HEIGHT; - [self setWebViewFrame:webViewBounds]; - - // move locationBar down - locationbarFrame.origin.y = webViewBounds.size.height; - self.addressLabel.frame = locationbarFrame; - } else { - // no locationBar, expand webView to screen dimensions - [self setWebViewFrame:self.view.bounds]; - } - } -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; -} - -- (void)viewDidUnload -{ - [self.webView loadHTMLString:nil baseURL:nil]; - [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; - [super viewDidUnload]; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return UIStatusBarStyleDefault; -} - -- (BOOL)prefersStatusBarHidden { - return NO; -} - -- (void)close -{ - [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; - self.currentURL = nil; - - if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { - [self.navigationDelegate browserExit]; - } - - __weak UIViewController* weakSelf = self; - - // Run later to avoid the "took a long time" log message. - dispatch_async(dispatch_get_main_queue(), ^{ - if ([weakSelf respondsToSelector:@selector(presentingViewController)]) { - [[weakSelf presentingViewController] dismissViewControllerAnimated:YES completion:^{ - [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; - }]; - } else { - [[weakSelf parentViewController] dismissViewControllerAnimated:YES completion:^{ - [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; - }]; - } - }); -} - -- (void)navigateTo:(NSURL*)url -{ - NSURLRequest* request = [NSURLRequest requestWithURL:url]; - - if (_userAgentLockToken != 0) { - [self.webView loadRequest:request]; - } else { - __weak CDVInAppBrowserViewController* weakSelf = self; - [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { - _userAgentLockToken = lockToken; - [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken]; - [weakSelf.webView loadRequest:request]; - }]; - } -} - -- (void)goBack:(id)sender -{ - [self.webView goBack]; -} - -- (void)goForward:(id)sender -{ - [self.webView goForward]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - if (IsAtLeastiOSVersion(@"7.0")) { - [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; - } - [self rePositionViews]; - - [super viewWillAppear:animated]; -} - -// -// On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account. -// The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't -// change that value. -// -- (float) getStatusBarOffset { - CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; - float statusBarOffset = IsAtLeastiOSVersion(@"7.0") ? MIN(statusBarFrame.size.width, statusBarFrame.size.height) : 0.0; - return statusBarOffset; -} - -- (void) rePositionViews { - if ([_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { - [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, TOOLBAR_HEIGHT, self.webView.frame.size.width, self.webView.frame.size.height)]; - [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; - } -} - -// Helper function to convert hex color string to UIColor -// Assumes input like "#00FF00" (#RRGGBB). -// Taken from https://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string -- (UIColor *)colorFromHexString:(NSString *)hexString { - unsigned rgbValue = 0; - NSScanner *scanner = [NSScanner scannerWithString:hexString]; - [scanner setScanLocation:1]; // bypass '#' character - [scanner scanHexInt:&rgbValue]; - return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; -} - -#pragma mark UIWebViewDelegate - -- (void)webViewDidStartLoad:(UIWebView*)theWebView -{ - // loading url, start spinner, update back/forward - - self.addressLabel.text = NSLocalizedString(@"Loading...", nil); - self.backButton.enabled = theWebView.canGoBack; - self.forwardButton.enabled = theWebView.canGoForward; - - NSLog(_browserOptions.hidespinner ? @"Yes" : @"No"); - if(!_browserOptions.hidespinner) { - [self.spinner startAnimating]; - } - - return [self.navigationDelegate webViewDidStartLoad:theWebView]; -} - -- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType -{ - BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; - - if (isTopLevelNavigation) { - self.currentURL = request.URL; - } - return [self.navigationDelegate webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType]; -} - -- (void)webViewDidFinishLoad:(UIWebView*)theWebView -{ - // update url, stop spinner, update back/forward - - self.addressLabel.text = [self.currentURL absoluteString]; - self.backButton.enabled = theWebView.canGoBack; - self.forwardButton.enabled = theWebView.canGoForward; - - [self.spinner stopAnimating]; - - // Work around a bug where the first time a PDF is opened, all UIWebViews - // reload their User-Agent from NSUserDefaults. - // This work-around makes the following assumptions: - // 1. The app has only a single Cordova Webview. If not, then the app should - // take it upon themselves to load a PDF in the background as a part of - // their start-up flow. - // 2. That the PDF does not require any additional network requests. We change - // the user-agent here back to that of the CDVViewController, so requests - // from it must pass through its white-list. This *does* break PDFs that - // contain links to other remote PDF/websites. - // More info at https://issues.apache.org/jira/browse/CB-2225 - BOOL isPDF = [@"true" isEqualToString :[theWebView stringByEvaluatingJavaScriptFromString:@"document.body==null"]]; - if (isPDF) { - [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; - } - - [self.navigationDelegate webViewDidFinishLoad:theWebView]; -} - -- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error -{ - // log fail message, stop spinner, update back/forward - NSLog(@"webView:didFailLoadWithError - %ld: %@", (long)error.code, [error localizedDescription]); - - self.backButton.enabled = theWebView.canGoBack; - self.forwardButton.enabled = theWebView.canGoForward; - [self.spinner stopAnimating]; - - self.addressLabel.text = NSLocalizedString(@"Load Error", nil); - - [self.navigationDelegate webView:theWebView didFailLoadWithError:error]; -} - -#pragma mark CDVScreenOrientationDelegate - -- (BOOL)shouldAutorotate -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { - return [self.orientationDelegate shouldAutorotate]; - } - return YES; -} - -- (NSUInteger)supportedInterfaceOrientations -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { - return [self.orientationDelegate supportedInterfaceOrientations]; - } - - return 1 << UIInterfaceOrientationPortrait; -} - -- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { - return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; - } - - return YES; -} - -@end - -@implementation CDVInAppBrowserOptions - -- (id)init -{ - if (self = [super init]) { - // default values - self.location = YES; - self.toolbar = YES; - self.closebuttoncaption = nil; - self.toolbarposition = kInAppBrowserToolbarBarPositionBottom; - self.clearcache = NO; - self.clearsessioncache = NO; - self.hidespinner = NO; - - self.enableviewportscale = NO; - self.mediaplaybackrequiresuseraction = NO; - self.allowinlinemediaplayback = NO; - self.keyboarddisplayrequiresuseraction = YES; - self.suppressesincrementalrendering = NO; - self.hidden = NO; - self.disallowoverscroll = NO; - self.hidenavigationbuttons = NO; - self.closebuttoncolor = nil; - self.toolbarcolor = nil; - self.toolbartranslucent = YES; - } - - return self; -} - -+ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options -{ - CDVInAppBrowserOptions* obj = [[CDVInAppBrowserOptions alloc] init]; - - // NOTE: this parsing does not handle quotes within values - NSArray* pairs = [options componentsSeparatedByString:@","]; - - // parse keys and values, set the properties - for (NSString* pair in pairs) { - NSArray* keyvalue = [pair componentsSeparatedByString:@"="]; - - if ([keyvalue count] == 2) { - NSString* key = [[keyvalue objectAtIndex:0] lowercaseString]; - NSString* value = [keyvalue objectAtIndex:1]; - NSString* value_lc = [value lowercaseString]; - - BOOL isBoolean = [value_lc isEqualToString:@"yes"] || [value_lc isEqualToString:@"no"]; - NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; - [numberFormatter setAllowsFloats:YES]; - BOOL isNumber = [numberFormatter numberFromString:value_lc] != nil; - - // set the property according to the key name - if ([obj respondsToSelector:NSSelectorFromString(key)]) { - if (isNumber) { - [obj setValue:[numberFormatter numberFromString:value_lc] forKey:key]; - } else if (isBoolean) { - [obj setValue:[NSNumber numberWithBool:[value_lc isEqualToString:@"yes"]] forKey:key]; - } else { - [obj setValue:value forKey:key]; - } - } - } - } - - return obj; -} - -@end - -@implementation CDVInAppBrowserNavigationController : UINavigationController - -- (void) dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion { - if ( self.presentedViewController) { - [super dismissViewControllerAnimated:flag completion:completion]; - } -} - -- (void) viewDidLoad { - - CGRect statusBarFrame = [self invertFrameIfNeeded:[UIApplication sharedApplication].statusBarFrame]; - statusBarFrame.size.height = STATUSBAR_HEIGHT; - // simplified from: http://stackoverflow.com/a/25669695/219684 - - UIToolbar* bgToolbar = [[UIToolbar alloc] initWithFrame:statusBarFrame]; - bgToolbar.barStyle = UIBarStyleDefault; - [bgToolbar setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; - [self.view addSubview:bgToolbar]; - - [super viewDidLoad]; -} - -- (CGRect) invertFrameIfNeeded:(CGRect)rect { - // We need to invert since on iOS 7 frames are always in Portrait context - if (!IsAtLeastiOSVersion(@"8.0")) { - if (UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])) { - CGFloat temp = rect.size.width; - rect.size.width = rect.size.height; - rect.size.height = temp; - } - rect.origin = CGPointZero; - } - return rect; -} - -#pragma mark CDVScreenOrientationDelegate - -- (BOOL)shouldAutorotate -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { - return [self.orientationDelegate shouldAutorotate]; - } - return YES; -} - -- (NSUInteger)supportedInterfaceOrientations -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { - return [self.orientationDelegate supportedInterfaceOrientations]; - } - - return 1 << UIInterfaceOrientationPortrait; -} - -- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { - return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; - } - - return YES; } diff --git a/src/ios/CDVInAppBrowserNavigationController.h b/src/ios/CDVInAppBrowserNavigationController.h new file mode 100644 index 0000000..bd186a2 --- /dev/null +++ b/src/ios/CDVInAppBrowserNavigationController.h @@ -0,0 +1,27 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + + +@interface CDVInAppBrowserNavigationController : UINavigationController + +@property (nonatomic, weak) id orientationDelegate; + +@end diff --git a/src/ios/CDVInAppBrowserNavigationController.m b/src/ios/CDVInAppBrowserNavigationController.m new file mode 100644 index 0000000..c77a3e6 --- /dev/null +++ b/src/ios/CDVInAppBrowserNavigationController.m @@ -0,0 +1,85 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVInAppBrowserNavigationController.h" + +#define STATUSBAR_HEIGHT 20.0 + +@implementation CDVInAppBrowserNavigationController : UINavigationController + +- (void) dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion { + if ( self.presentedViewController) { + [super dismissViewControllerAnimated:flag completion:completion]; + } +} + +- (void) viewDidLoad { + + CGRect statusBarFrame = [self invertFrameIfNeeded:[UIApplication sharedApplication].statusBarFrame]; + statusBarFrame.size.height = STATUSBAR_HEIGHT; + // simplified from: http://stackoverflow.com/a/25669695/219684 + + UIToolbar* bgToolbar = [[UIToolbar alloc] initWithFrame:statusBarFrame]; + bgToolbar.barStyle = UIBarStyleDefault; + [bgToolbar setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; + [self.view addSubview:bgToolbar]; + + [super viewDidLoad]; +} + +- (CGRect) invertFrameIfNeeded:(CGRect)rect { + if (UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])) { + CGFloat temp = rect.size.width; + rect.size.width = rect.size.height; + rect.size.height = temp; + } + rect.origin = CGPointZero; + return rect; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { + return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + + return YES; +} + + +@end diff --git a/src/ios/CDVInAppBrowserOptions.h b/src/ios/CDVInAppBrowserOptions.h new file mode 100644 index 0000000..93e48d4 --- /dev/null +++ b/src/ios/CDVInAppBrowserOptions.h @@ -0,0 +1,51 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + + +@interface CDVInAppBrowserOptions : NSObject {} + +@property (nonatomic, assign) BOOL usewkwebview; +@property (nonatomic, assign) BOOL location; +@property (nonatomic, assign) BOOL toolbar; +@property (nonatomic, copy) NSString* closebuttoncaption; +@property (nonatomic, copy) NSString* closebuttoncolor; +@property (nonatomic, copy) NSString* toolbarposition; +@property (nonatomic, copy) NSString* toolbarcolor; +@property (nonatomic, assign) BOOL toolbartranslucent; +@property (nonatomic, assign) BOOL hidenavigationbuttons; +@property (nonatomic, copy) NSString* navigationbuttoncolor; +@property (nonatomic, assign) BOOL cleardata; +@property (nonatomic, assign) BOOL clearcache; +@property (nonatomic, assign) BOOL clearsessioncache; +@property (nonatomic, assign) BOOL hidespinner; + +@property (nonatomic, copy) NSString* presentationstyle; +@property (nonatomic, copy) NSString* transitionstyle; + +@property (nonatomic, assign) BOOL enableviewportscale; +@property (nonatomic, assign) BOOL mediaplaybackrequiresuseraction; +@property (nonatomic, assign) BOOL allowinlinemediaplayback; +@property (nonatomic, assign) BOOL keyboarddisplayrequiresuseraction; +@property (nonatomic, assign) BOOL suppressesincrementalrendering; +@property (nonatomic, assign) BOOL hidden; +@property (nonatomic, assign) BOOL disallowoverscroll; + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; + +@end diff --git a/src/ios/CDVInAppBrowserOptions.m b/src/ios/CDVInAppBrowserOptions.m new file mode 100644 index 0000000..e1cc7d3 --- /dev/null +++ b/src/ios/CDVInAppBrowserOptions.m @@ -0,0 +1,91 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVInAppBrowserOptions.h" + +@implementation CDVInAppBrowserOptions + +- (id)init +{ + if (self = [super init]) { + // default values + self.usewkwebview = NO; + self.location = YES; + self.toolbar = YES; + self.closebuttoncaption = nil; + self.toolbarposition = @"bottom"; + self.cleardata = NO; + self.clearcache = NO; + self.clearsessioncache = NO; + self.hidespinner = NO; + + self.enableviewportscale = NO; + self.mediaplaybackrequiresuseraction = NO; + self.allowinlinemediaplayback = NO; + self.keyboarddisplayrequiresuseraction = YES; + self.suppressesincrementalrendering = NO; + self.hidden = NO; + self.disallowoverscroll = NO; + self.hidenavigationbuttons = NO; + self.closebuttoncolor = nil; + self.toolbarcolor = nil; + self.toolbartranslucent = YES; + } + + return self; +} + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options +{ + CDVInAppBrowserOptions* obj = [[CDVInAppBrowserOptions alloc] init]; + + // NOTE: this parsing does not handle quotes within values + NSArray* pairs = [options componentsSeparatedByString:@","]; + + // parse keys and values, set the properties + for (NSString* pair in pairs) { + NSArray* keyvalue = [pair componentsSeparatedByString:@"="]; + + if ([keyvalue count] == 2) { + NSString* key = [[keyvalue objectAtIndex:0] lowercaseString]; + NSString* value = [keyvalue objectAtIndex:1]; + NSString* value_lc = [value lowercaseString]; + + BOOL isBoolean = [value_lc isEqualToString:@"yes"] || [value_lc isEqualToString:@"no"]; + NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setAllowsFloats:YES]; + BOOL isNumber = [numberFormatter numberFromString:value_lc] != nil; + + // set the property according to the key name + if ([obj respondsToSelector:NSSelectorFromString(key)]) { + if (isNumber) { + [obj setValue:[numberFormatter numberFromString:value_lc] forKey:key]; + } else if (isBoolean) { + [obj setValue:[NSNumber numberWithBool:[value_lc isEqualToString:@"yes"]] forKey:key]; + } else { + [obj setValue:value forKey:key]; + } + } + } + } + + return obj; +} + +@end diff --git a/src/ios/CDVUIInAppBrowser.h b/src/ios/CDVUIInAppBrowser.h new file mode 100644 index 0000000..64afe3b --- /dev/null +++ b/src/ios/CDVUIInAppBrowser.h @@ -0,0 +1,86 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import "CDVInAppBrowserOptions.h" +#import "CDVInAppBrowserNavigationController.h" + +#ifdef __CORDOVA_4_0_0 + #import +#else + #import +#endif + +@class CDVUIInAppBrowserViewController; + +@interface CDVUIInAppBrowser : CDVPlugin { + UIWindow * tmpWindow; +} + +@property (nonatomic, retain) CDVUIInAppBrowserViewController* inAppBrowserViewController; +@property (nonatomic, copy) NSString* callbackId; +@property (nonatomic, copy) NSRegularExpression *callbackIdPattern; + ++ (id) getInstance; +- (void)open:(CDVInvokedUrlCommand*)command; +- (void)close:(CDVInvokedUrlCommand*)command; +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command; +- (void)show:(CDVInvokedUrlCommand*)command; +- (void)hide:(CDVInvokedUrlCommand*)command; + +@end + +@interface CDVUIInAppBrowserViewController : UIViewController { + @private + NSString* _userAgent; + NSString* _prevUserAgent; + NSInteger _userAgentLockToken; + CDVInAppBrowserOptions *_browserOptions; + +#ifdef __CORDOVA_4_0_0 + CDVUIWebViewDelegate* _webViewDelegate; +#else + CDVWebViewDelegate* _webViewDelegate; +#endif + +} + +@property (nonatomic, strong) IBOutlet UIWebView* webView; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* closeButton; +@property (nonatomic, strong) IBOutlet UILabel* addressLabel; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* backButton; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* forwardButton; +@property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; +@property (nonatomic, strong) IBOutlet UIToolbar* toolbar; + +@property (nonatomic, weak) id orientationDelegate; +@property (nonatomic, weak) CDVUIInAppBrowser* navigationDelegate; +@property (nonatomic) NSURL* currentURL; + +- (void)close; +- (void)navigateTo:(NSURL*)url; +- (void)showLocationBar:(BOOL)show; +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition; +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions; + +@end diff --git a/src/ios/CDVUIInAppBrowser.m b/src/ios/CDVUIInAppBrowser.m new file mode 100644 index 0000000..2ae8611 --- /dev/null +++ b/src/ios/CDVUIInAppBrowser.m @@ -0,0 +1,1029 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVUIInAppBrowser.h" +#import +#import + +#define kInAppBrowserTargetSelf @"_self" +#define kInAppBrowserTargetSystem @"_system" +#define kInAppBrowserTargetBlank @"_blank" + +#define kInAppBrowserToolbarBarPositionBottom @"bottom" +#define kInAppBrowserToolbarBarPositionTop @"top" + +#define TOOLBAR_HEIGHT 44.0 +#define STATUSBAR_HEIGHT 20.0 +#define LOCATIONBAR_HEIGHT 21.0 +#define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) + +#pragma mark CDVUIInAppBrowser + +@interface CDVUIInAppBrowser () { + NSInteger _previousStatusBarStyle; +} +@end + +@implementation CDVUIInAppBrowser + +static CDVUIInAppBrowser* instance = nil; + ++ (id) getInstance{ + return instance; +} + +- (void)pluginInitialize +{ + instance = self; + _previousStatusBarStyle = -1; + _callbackIdPattern = nil; +} + +- (id)settingForKey:(NSString*)key +{ + return [self.commandDelegate.settings objectForKey:[key lowercaseString]]; +} + +- (void)onReset +{ + [self close:nil]; +} + +- (void)close:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"IAB.close() called but it was already closed."); + return; + } + // Things are cleaned up in browserExit. + [self.inAppBrowserViewController close]; +} + +- (BOOL) isSystemUrl:(NSURL*)url +{ + if ([[url host] isEqualToString:@"itunes.apple.com"]) { + return YES; + } + + return NO; +} + +- (void)open:(CDVInvokedUrlCommand*)command +{ + CDVPluginResult* pluginResult; + + NSString* url = [command argumentAtIndex:0]; + NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf]; + NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; + + self.callbackId = command.callbackId; + + if (url != nil) { +#ifdef __CORDOVA_4_0_0 + NSURL* baseUrl = [self.webViewEngine URL]; +#else + NSURL* baseUrl = [self.webView.request URL]; +#endif + NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; + + if ([self isSystemUrl:absoluteUrl]) { + target = kInAppBrowserTargetSystem; + } + + if ([target isEqualToString:kInAppBrowserTargetSelf]) { + [self openInCordovaWebView:absoluteUrl withOptions:options]; + } else if ([target isEqualToString:kInAppBrowserTargetSystem]) { + [self openInSystem:absoluteUrl]; + } else { // _blank or anything else + [self openInInAppBrowser:absoluteUrl withOptions:options]; + } + + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"]; + } + + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options +{ + CDVInAppBrowserOptions* browserOptions = [CDVInAppBrowserOptions parseOptions:options]; + + if (browserOptions.clearcache) { + NSHTTPCookie *cookie; + NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + for (cookie in [storage cookies]) + { + if (![cookie.domain isEqual: @".^filecookies^"]) { + [storage deleteCookie:cookie]; + } + } + } + + if (browserOptions.clearsessioncache) { + NSHTTPCookie *cookie; + NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + for (cookie in [storage cookies]) + { + if (![cookie.domain isEqual: @".^filecookies^"] && cookie.isSessionOnly) { + [storage deleteCookie:cookie]; + } + } + } + + if (self.inAppBrowserViewController == nil) { + NSString* userAgent = [CDVUserAgentUtil originalUserAgent]; + NSString* overrideUserAgent = [self settingForKey:@"OverrideUserAgent"]; + NSString* appendUserAgent = [self settingForKey:@"AppendUserAgent"]; + if(overrideUserAgent){ + userAgent = overrideUserAgent; + } + if(appendUserAgent){ + userAgent = [userAgent stringByAppendingString: appendUserAgent]; + } + self.inAppBrowserViewController = [[CDVUIInAppBrowserViewController alloc] initWithUserAgent:userAgent prevUserAgent:[self.commandDelegate userAgent] browserOptions: browserOptions]; + self.inAppBrowserViewController.navigationDelegate = self; + + if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { + self.inAppBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; + } + } + + [self.inAppBrowserViewController showLocationBar:browserOptions.location]; + [self.inAppBrowserViewController showToolBar:browserOptions.toolbar :browserOptions.toolbarposition]; + if (browserOptions.closebuttoncaption != nil || browserOptions.closebuttoncolor != nil) { + [self.inAppBrowserViewController setCloseButtonTitle:browserOptions.closebuttoncaption :browserOptions.closebuttoncolor]; + } + // Set Presentation Style + UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default + if (browserOptions.presentationstyle != nil) { + if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"pagesheet"]) { + presentationStyle = UIModalPresentationPageSheet; + } else if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"formsheet"]) { + presentationStyle = UIModalPresentationFormSheet; + } + } + self.inAppBrowserViewController.modalPresentationStyle = presentationStyle; + + // Set Transition Style + UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default + if (browserOptions.transitionstyle != nil) { + if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"fliphorizontal"]) { + transitionStyle = UIModalTransitionStyleFlipHorizontal; + } else if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"crossdissolve"]) { + transitionStyle = UIModalTransitionStyleCrossDissolve; + } + } + self.inAppBrowserViewController.modalTransitionStyle = transitionStyle; + + // prevent webView from bouncing + if (browserOptions.disallowoverscroll) { + if ([self.inAppBrowserViewController.webView respondsToSelector:@selector(scrollView)]) { + ((UIScrollView*)[self.inAppBrowserViewController.webView scrollView]).bounces = NO; + } else { + for (id subview in self.inAppBrowserViewController.webView.subviews) { + if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { + ((UIScrollView*)subview).bounces = NO; + } + } + } + } + + // UIWebView options + self.inAppBrowserViewController.webView.scalesPageToFit = browserOptions.enableviewportscale; + self.inAppBrowserViewController.webView.mediaPlaybackRequiresUserAction = browserOptions.mediaplaybackrequiresuseraction; + self.inAppBrowserViewController.webView.allowsInlineMediaPlayback = browserOptions.allowinlinemediaplayback; + if (IsAtLeastiOSVersion(@"6.0")) { + self.inAppBrowserViewController.webView.keyboardDisplayRequiresUserAction = browserOptions.keyboarddisplayrequiresuseraction; + self.inAppBrowserViewController.webView.suppressesIncrementalRendering = browserOptions.suppressesincrementalrendering; + } + + [self.inAppBrowserViewController navigateTo:url]; + if (!browserOptions.hidden) { + [self show:nil]; + } +} + +- (void)show:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to show IAB after it was closed."); + return; + } + if (_previousStatusBarStyle != -1) { + NSLog(@"Tried to show IAB while already shown"); + return; + } + + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + + __block CDVInAppBrowserNavigationController* nav = [[CDVInAppBrowserNavigationController alloc] + initWithRootViewController:self.inAppBrowserViewController]; + nav.orientationDelegate = self.inAppBrowserViewController; + nav.navigationBarHidden = YES; + nav.modalPresentationStyle = self.inAppBrowserViewController.modalPresentationStyle; + + __weak CDVUIInAppBrowser* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.inAppBrowserViewController != nil) { + if (!tmpWindow) { + CGRect frame = [[UIScreen mainScreen] bounds]; + tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + } + UIViewController *tmpController = [[UIViewController alloc] init]; + [tmpWindow setRootViewController:tmpController]; + double baseWindowLevel = [UIApplication sharedApplication].keyWindow.windowLevel; + [tmpWindow setWindowLevel:baseWindowLevel+1]; + + [tmpWindow makeKeyAndVisible]; + [tmpController presentViewController:nav animated:YES completion:nil]; + } + }); +} + +- (void)hide:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to hide IAB after it was closed."); + return; + + + } + if (_previousStatusBarStyle == -1) { + NSLog(@"Tried to hide IAB while already hidden"); + return; + } + + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.inAppBrowserViewController != nil) { + _previousStatusBarStyle = -1; + [self.inAppBrowserViewController.presentingViewController dismissViewControllerAnimated:YES completion:^{ + [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; + }]; + } + }); +} + +- (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + +#ifdef __CORDOVA_4_0_0 + // the webview engine itself will filter for this according to policy + // in config.xml for cordova-ios-4.0 + [self.webViewEngine loadRequest:request]; +#else + if ([self.commandDelegate URLIsWhitelisted:url]) { + [self.webView loadRequest:request]; + } else { // this assumes the InAppBrowser can be excepted from the white-list + [self openInInAppBrowser:url withOptions:options]; + } +#endif +} + +- (void)openInSystem:(NSURL*)url +{ + if ([[UIApplication sharedApplication] openURL:url] == NO) { + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + [[UIApplication sharedApplication] openURL:url]; + } +} + +// This is a helper method for the inject{Script|Style}{Code|File} API calls, which +// provides a consistent method for injecting JavaScript code into the document. +// +// If a wrapper string is supplied, then the source string will be JSON-encoded (adding +// quotes) and wrapped using string formatting. (The wrapper string should have a single +// '%@' marker). +// +// If no wrapper is supplied, then the source string is executed directly. + +- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper +{ + // Ensure an iframe bridge is created to communicate with the CDVUIInAppBrowserViewController + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){_cdvIframeBridge=d.getElementById('_cdvIframeBridge');if(!_cdvIframeBridge) {var e = _cdvIframeBridge = d.createElement('iframe');e.id='_cdvIframeBridge'; e.style.display='none';d.body.appendChild(e);}})(document)"]; + + if (jsWrapper != nil) { + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; + NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + if (sourceArrayString) { + NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)]; + NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString]; + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:jsToInject]; + } + } else { + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:source]; + } +} + +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper = nil; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"_cdvIframeBridge.src='gap-iab://%@/'+encodeURIComponent(JSON.stringify([eval(%%@)]));", command.callbackId]; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectScriptFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (BOOL)isValidCallbackId:(NSString *)callbackId +{ + NSError *err = nil; + // Initialize on first use + if (self.callbackIdPattern == nil) { + self.callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"^InAppBrowser[0-9]{1,10}$" options:0 error:&err]; + if (err != nil) { + // Couldn't initialize Regex; No is safer than Yes. + return NO; + } + } + if ([self.callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) { + return YES; + } + return NO; +} + +/** + * The iframe bridge provided for the InAppBrowser is capable of executing any oustanding callback belonging + * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no + * other code execution is possible. + * + * To trigger the bridge, the iframe (or any other resource) should attempt to load a url of the form: + * + * gap-iab:/// + * + * where is the string id of the callback to trigger (something like "InAppBrowser0123456789") + * + * If present, the path component of the special gap-iab:// url is expected to be a URL-escaped JSON-encoded + * value to pass to the callback. [NSURL path] should take care of the URL-unescaping, and a JSON_EXCEPTION + * is returned if the JSON is invalid. + */ +- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + NSURL* url = request.URL; + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute, + // and the path, if present, should be a JSON-encoded value to pass to the callback. + if ([[url scheme] isEqualToString:@"gap-iab"]) { + NSString* scriptCallbackId = [url host]; + CDVPluginResult* pluginResult = nil; + + if ([self isValidCallbackId:scriptCallbackId]) { + NSString* scriptResult = [url path]; + NSError* __autoreleasing error = nil; + + // The message should be a JSON-encoded array of the result of the script which executed. + if ((scriptResult != nil) && ([scriptResult length] > 1)) { + scriptResult = [scriptResult substringFromIndex:1]; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; + } + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; + } + [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; + return NO; + } + } + //if is an app store link, let the system handle it, otherwise it fails to load it + else if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { + [theWebView stopLoading]; + [self openInSystem:url]; + return NO; + } + else if ((self.callbackId != nil) && isTopLevelNavigation) { + // Send a loadstart event for each top-level navigation (includes redirects). + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + + return YES; +} + +- (void)webViewDidStartLoad:(UIWebView*)theWebView +{ +} + +- (void)webViewDidFinishLoad:(UIWebView*)theWebView +{ + if (self.callbackId != nil) { + // TODO: It would be more useful to return the URL the page is actually on (e.g. if it's been redirected). + NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +{ + if (self.callbackId != nil) { + NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInteger:error.code], @"message": error.localizedDescription}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)browserExit +{ + if (self.callbackId != nil) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"exit"}]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + self.callbackId = nil; + } + // Set navigationDelegate to nil to ensure no callbacks are received from it. + self.inAppBrowserViewController.navigationDelegate = nil; + // Don't recycle the ViewController since it may be consuming a lot of memory. + // Also - this is required for the PDF/User-Agent bug work-around. + self.inAppBrowserViewController = nil; + + if (IsAtLeastiOSVersion(@"7.0")) { + if (_previousStatusBarStyle != -1) { + [[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle]; + } + } + + _previousStatusBarStyle = -1; // this value was reset before reapplying it. caused statusbar to stay black on ios7 +} + +@end + +#pragma mark CDVUIInAppBrowserViewController + +@implementation CDVUIInAppBrowserViewController + +@synthesize currentURL; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions +{ + self = [super init]; + if (self != nil) { + _userAgent = userAgent; + _prevUserAgent = prevUserAgent; + _browserOptions = browserOptions; +#ifdef __CORDOVA_4_0_0 + _webViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self]; +#else + _webViewDelegate = [[CDVWebViewDelegate alloc] initWithDelegate:self]; +#endif + + [self createViews]; + } + + return self; +} + +// Prevent crashes on closing windows +-(void)dealloc { + self.webView.delegate = nil; +} + +- (void)createViews +{ + // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included + + CGRect webViewBounds = self.view.bounds; + BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]; + webViewBounds.size.height -= _browserOptions.location ? FOOTER_HEIGHT : TOOLBAR_HEIGHT; + self.webView = [[UIWebView alloc] initWithFrame:webViewBounds]; + + self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); + + [self.view addSubview:self.webView]; + [self.view sendSubviewToBack:self.webView]; + + self.webView.delegate = _webViewDelegate; + self.webView.backgroundColor = [UIColor whiteColor]; + + self.webView.clearsContextBeforeDrawing = YES; + self.webView.clipsToBounds = YES; + self.webView.contentMode = UIViewContentModeScaleToFill; + self.webView.multipleTouchEnabled = YES; + self.webView.opaque = YES; + self.webView.scalesPageToFit = NO; + self.webView.userInteractionEnabled = YES; + + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + self.spinner.alpha = 1.000; + self.spinner.autoresizesSubviews = YES; + self.spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin); + self.spinner.clearsContextBeforeDrawing = NO; + self.spinner.clipsToBounds = NO; + self.spinner.contentMode = UIViewContentModeScaleToFill; + self.spinner.frame = CGRectMake(CGRectGetMidX(self.webView.frame), CGRectGetMidY(self.webView.frame), 20.0, 20.0); + self.spinner.hidden = NO; + self.spinner.hidesWhenStopped = YES; + self.spinner.multipleTouchEnabled = NO; + self.spinner.opaque = NO; + self.spinner.userInteractionEnabled = NO; + [self.spinner stopAnimating]; + + self.closeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + + UIBarButtonItem* flexibleSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + + UIBarButtonItem* fixedSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + fixedSpaceButton.width = 20; + + float toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - TOOLBAR_HEIGHT : 0.0; + CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, TOOLBAR_HEIGHT); + + self.toolbar = [[UIToolbar alloc] initWithFrame:toolbarFrame]; + self.toolbar.alpha = 1.000; + self.toolbar.autoresizesSubviews = YES; + self.toolbar.autoresizingMask = toolbarIsAtBottom ? (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin) : UIViewAutoresizingFlexibleWidth; + self.toolbar.barStyle = UIBarStyleBlackOpaque; + self.toolbar.clearsContextBeforeDrawing = NO; + self.toolbar.clipsToBounds = NO; + self.toolbar.contentMode = UIViewContentModeScaleToFill; + self.toolbar.hidden = NO; + self.toolbar.multipleTouchEnabled = NO; + self.toolbar.opaque = NO; + self.toolbar.userInteractionEnabled = YES; + if (_browserOptions.toolbarcolor != nil) { // Set toolbar color if user sets it in options + self.toolbar.barTintColor = [self colorFromHexString:_browserOptions.toolbarcolor]; + } + if (!_browserOptions.toolbartranslucent) { // Set toolbar translucent to no if user sets it in options + self.toolbar.translucent = NO; + } + + CGFloat labelInset = 5.0; + float locationBarY = toolbarIsAtBottom ? self.view.bounds.size.height - FOOTER_HEIGHT : self.view.bounds.size.height - LOCATIONBAR_HEIGHT; + + self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, locationBarY, self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)]; + self.addressLabel.adjustsFontSizeToFitWidth = NO; + self.addressLabel.alpha = 1.000; + self.addressLabel.autoresizesSubviews = YES; + self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; + self.addressLabel.backgroundColor = [UIColor clearColor]; + self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; + self.addressLabel.clearsContextBeforeDrawing = YES; + self.addressLabel.clipsToBounds = YES; + self.addressLabel.contentMode = UIViewContentModeScaleToFill; + self.addressLabel.enabled = YES; + self.addressLabel.hidden = NO; + self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail; + + if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumScaleFactor:")]) { + [self.addressLabel setValue:@(10.0/[UIFont labelFontSize]) forKey:@"minimumScaleFactor"]; + } else if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumFontSize:")]) { + [self.addressLabel setValue:@(10.0) forKey:@"minimumFontSize"]; + } + + self.addressLabel.multipleTouchEnabled = NO; + self.addressLabel.numberOfLines = 1; + self.addressLabel.opaque = NO; + self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0); + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.addressLabel.textAlignment = NSTextAlignmentLeft; + self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000]; + self.addressLabel.userInteractionEnabled = NO; + + NSString* frontArrowString = NSLocalizedString(@"►", nil); // create arrow from Unicode char + self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; + self.forwardButton.enabled = YES; + self.forwardButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.forwardButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + NSString* backArrowString = NSLocalizedString(@"◄", nil); // create arrow from Unicode char + self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; + self.backButton.enabled = YES; + self.backButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.backButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + // Filter out Navigation Buttons if user requests so + if (_browserOptions.hidenavigationbuttons) { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton]]; + } else { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; + } + + self.view.backgroundColor = [UIColor grayColor]; + [self.view addSubview:self.toolbar]; + [self.view addSubview:self.addressLabel]; + [self.view addSubview:self.spinner]; +} + +- (void) setWebViewFrame : (CGRect) frame { + NSLog(@"Setting the WebView's frame to %@", NSStringFromCGRect(frame)); + [self.webView setFrame:frame]; +} + +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString +{ + // the advantage of using UIBarButtonSystemItemDone is the system will localize it for you automatically + // but, if you want to set this yourself, knock yourself out (we can't set the title for a system Done button, so we have to create a new one) + self.closeButton = nil; + // Initialize with title if title is set, otherwise the title will be 'Done' localized + self.closeButton = title != nil ? [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)] : [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + // If color on closebutton is requested then initialize with that that color, otherwise use initialize with default + self.closeButton.tintColor = colorString != nil ? [self colorFromHexString:colorString] : [UIColor colorWithRed:60.0 / 255.0 green:136.0 / 255.0 blue:230.0 / 255.0 alpha:1]; + + NSMutableArray* items = [self.toolbar.items mutableCopy]; + [items replaceObjectAtIndex:0 withObject:self.closeButton]; + [self.toolbar setItems:items]; +} + +- (void)showLocationBar:(BOOL)show +{ + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL toolbarVisible = !self.toolbar.hidden; + + // prevent double show/hide + if (show == !(self.addressLabel.hidden)) { + return; + } + + if (show) { + self.addressLabel.hidden = NO; + + if (toolbarVisible) { + // toolBar at the bottom, leave as is + // put locationBar on top of the toolBar + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= FOOTER_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no toolBar, so put locationBar at the bottom + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } + } else { + self.addressLabel.hidden = YES; + + if (toolbarVisible) { + // locationBar is on top of toolBar, hide locationBar + + // webView take up whole height less toolBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + } else { + // no toolBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition +{ + CGRect toolbarFrame = self.toolbar.frame; + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL locationbarVisible = !self.addressLabel.hidden; + + // prevent double show/hide + if (show == !(self.toolbar.hidden)) { + return; + } + + if (show) { + self.toolbar.hidden = NO; + CGRect webViewBounds = self.view.bounds; + + if (locationbarVisible) { + // locationBar at the bottom, move locationBar up + // put toolBar at the bottom + webViewBounds.size.height -= FOOTER_HEIGHT; + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + self.toolbar.frame = toolbarFrame; + } else { + // no locationBar, so put toolBar at the bottom + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + self.toolbar.frame = toolbarFrame; + } + + if ([toolbarPosition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + toolbarFrame.origin.y = 0; + webViewBounds.origin.y += toolbarFrame.size.height; + [self setWebViewFrame:webViewBounds]; + } else { + toolbarFrame.origin.y = (webViewBounds.size.height + LOCATIONBAR_HEIGHT); + } + [self setWebViewFrame:webViewBounds]; + + } else { + self.toolbar.hidden = YES; + + if (locationbarVisible) { + // locationBar is on top of toolBar, hide toolBar + // put locationBar at the bottom + + // webView take up whole height less locationBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + // move locationBar down + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no locationBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; +} + +- (void)viewDidUnload +{ + [self.webView loadHTMLString:nil baseURL:nil]; + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + [super viewDidUnload]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return UIStatusBarStyleDefault; +} + +- (BOOL)prefersStatusBarHidden { + return NO; +} + +- (void)close +{ + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + self.currentURL = nil; + + if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { + [self.navigationDelegate browserExit]; + } + + __weak UIViewController* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if ([weakSelf respondsToSelector:@selector(presentingViewController)]) { + [[weakSelf presentingViewController] dismissViewControllerAnimated:YES completion:^{ + [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; + }]; + } else { + [[weakSelf parentViewController] dismissViewControllerAnimated:YES completion:^{ + [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; + }]; + } + }); +} + +- (void)navigateTo:(NSURL*)url +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + + if (_userAgentLockToken != 0) { + [self.webView loadRequest:request]; + } else { + __weak CDVUIInAppBrowserViewController* weakSelf = self; + [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { + _userAgentLockToken = lockToken; + [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken]; + [weakSelf.webView loadRequest:request]; + }]; + } +} + +- (void)goBack:(id)sender +{ + [self.webView goBack]; +} + +- (void)goForward:(id)sender +{ + [self.webView goForward]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + if (IsAtLeastiOSVersion(@"7.0")) { + [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; + } + [self rePositionViews]; + + [super viewWillAppear:animated]; +} + +// +// On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account. +// The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't +// change that value. +// +- (float) getStatusBarOffset { + CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; + float statusBarOffset = IsAtLeastiOSVersion(@"7.0") ? MIN(statusBarFrame.size.width, statusBarFrame.size.height) : 0.0; + return statusBarOffset; +} + +- (void) rePositionViews { + if ([_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, TOOLBAR_HEIGHT, self.webView.frame.size.width, self.webView.frame.size.height)]; + [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; + } +} + +// Helper function to convert hex color string to UIColor +// Assumes input like "#00FF00" (#RRGGBB). +// Taken from https://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string +- (UIColor *)colorFromHexString:(NSString *)hexString { + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:1]; // bypass '#' character + [scanner scanHexInt:&rgbValue]; + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; +} + +#pragma mark UIWebViewDelegate + +- (void)webViewDidStartLoad:(UIWebView*)theWebView +{ + // loading url, start spinner, update back/forward + + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + NSLog(_browserOptions.hidespinner ? @"Yes" : @"No"); + if(!_browserOptions.hidespinner) { + [self.spinner startAnimating]; + } + + return [self.navigationDelegate webViewDidStartLoad:theWebView]; +} + +- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + if (isTopLevelNavigation) { + self.currentURL = request.URL; + } + return [self.navigationDelegate webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType]; +} + +- (void)webViewDidFinishLoad:(UIWebView*)theWebView +{ + // update url, stop spinner, update back/forward + + self.addressLabel.text = [self.currentURL absoluteString]; + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + [self.spinner stopAnimating]; + + // Work around a bug where the first time a PDF is opened, all UIWebViews + // reload their User-Agent from NSUserDefaults. + // This work-around makes the following assumptions: + // 1. The app has only a single Cordova Webview. If not, then the app should + // take it upon themselves to load a PDF in the background as a part of + // their start-up flow. + // 2. That the PDF does not require any additional network requests. We change + // the user-agent here back to that of the CDVViewController, so requests + // from it must pass through its white-list. This *does* break PDFs that + // contain links to other remote PDF/websites. + // More info at https://issues.apache.org/jira/browse/CB-2225 + BOOL isPDF = [@"true" isEqualToString :[theWebView stringByEvaluatingJavaScriptFromString:@"document.body==null"]]; + if (isPDF) { + [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; + } + + [self.navigationDelegate webViewDidFinishLoad:theWebView]; +} + +- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +{ + // log fail message, stop spinner, update back/forward + NSLog(@"webView:didFailLoadWithError - %ld: %@", (long)error.code, [error localizedDescription]); + + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + [self.spinner stopAnimating]; + + self.addressLabel.text = NSLocalizedString(@"Load Error", nil); + + [self.navigationDelegate webView:theWebView didFailLoadWithError:error]; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { + return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + + return YES; +} + +@end + + diff --git a/src/ios/CDVWKInAppBrowser.h b/src/ios/CDVWKInAppBrowser.h new file mode 100644 index 0000000..4de7824 --- /dev/null +++ b/src/ios/CDVWKInAppBrowser.h @@ -0,0 +1,76 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import "CDVWKInAppBrowserUIDelegate.h" +#import "CDVInAppBrowserOptions.h" +#import "CDVInAppBrowserNavigationController.h" + +@class CDVWKInAppBrowserViewController; + +@interface CDVWKInAppBrowser : CDVPlugin { +} + +@property (nonatomic, retain) CDVWKInAppBrowser* instance; +@property (nonatomic, retain) CDVWKInAppBrowserViewController* inAppBrowserViewController; +@property (nonatomic, copy) NSString* callbackId; +@property (nonatomic, copy) NSRegularExpression *callbackIdPattern; + ++ (id) getInstance; +- (void)open:(CDVInvokedUrlCommand*)command; +- (void)close:(CDVInvokedUrlCommand*)command; +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command; +- (void)show:(CDVInvokedUrlCommand*)command; +- (void)hide:(CDVInvokedUrlCommand*)command; + +@end + +@interface CDVWKInAppBrowserViewController : UIViewController { + @private + NSString* _userAgent; + NSString* _prevUserAgent; + NSInteger _userAgentLockToken; + CDVInAppBrowserOptions *_browserOptions; +} + +@property (nonatomic, strong) IBOutlet WKWebView* webView; +@property (nonatomic, strong) IBOutlet WKWebViewConfiguration* configuration; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* closeButton; +@property (nonatomic, strong) IBOutlet UILabel* addressLabel; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* backButton; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* forwardButton; +@property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; +@property (nonatomic, strong) IBOutlet UIToolbar* toolbar; +@property (nonatomic, strong) IBOutlet CDVWKInAppBrowserUIDelegate* webViewUIDelegate; + +@property (nonatomic, weak) id orientationDelegate; +@property (nonatomic, weak) CDVWKInAppBrowser* navigationDelegate; +@property (nonatomic) NSURL* currentURL; + +- (void)close; +- (void)navigateTo:(NSURL*)url; +- (void)showLocationBar:(BOOL)show; +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition; +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions; + +@end diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m new file mode 100644 index 0000000..4d59bc2 --- /dev/null +++ b/src/ios/CDVWKInAppBrowser.m @@ -0,0 +1,1164 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVWKInAppBrowser.h" + +#if __has_include("CDVWKProcessPoolFactory.h") +#import "CDVWKProcessPoolFactory.h" +#endif + +#import +#import + +#define kInAppBrowserTargetSelf @"_self" +#define kInAppBrowserTargetSystem @"_system" +#define kInAppBrowserTargetBlank @"_blank" + +#define kInAppBrowserToolbarBarPositionBottom @"bottom" +#define kInAppBrowserToolbarBarPositionTop @"top" + +#define IAB_BRIDGE_NAME @"cordova_iab" + +#define TOOLBAR_HEIGHT 44.0 +#define STATUSBAR_HEIGHT 20.0 +#define LOCATIONBAR_HEIGHT 21.0 +#define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) + +#pragma mark CDVWKInAppBrowser + +@interface CDVWKInAppBrowser () { + NSInteger _previousStatusBarStyle; +} +@end + +@implementation CDVWKInAppBrowser + +static CDVWKInAppBrowser* instance = nil; + ++ (id) getInstance{ + return instance; +} + +- (void)pluginInitialize +{ + instance = self; + _previousStatusBarStyle = -1; + _callbackIdPattern = nil; +} + +- (id)settingForKey:(NSString*)key +{ + return [self.commandDelegate.settings objectForKey:[key lowercaseString]]; +} + +- (void)onReset +{ + [self close:nil]; +} + +- (void)close:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"IAB.close() called but it was already closed."); + return; + } + // Things are cleaned up in browserExit. + [self.inAppBrowserViewController close]; +} + +- (BOOL) isSystemUrl:(NSURL*)url +{ + if ([[url host] isEqualToString:@"itunes.apple.com"]) { + return YES; + } + + return NO; +} + +- (void)open:(CDVInvokedUrlCommand*)command +{ + CDVPluginResult* pluginResult; + + NSString* url = [command argumentAtIndex:0]; + NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf]; + NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; + + self.callbackId = command.callbackId; + + if (url != nil) { +#ifdef __CORDOVA_4_0_0 + NSURL* baseUrl = [self.webViewEngine URL]; +#else + NSURL* baseUrl = [self.webView.request URL]; +#endif + NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; + + if ([self isSystemUrl:absoluteUrl]) { + target = kInAppBrowserTargetSystem; + } + + if ([target isEqualToString:kInAppBrowserTargetSelf]) { + [self openInCordovaWebView:absoluteUrl withOptions:options]; + } else if ([target isEqualToString:kInAppBrowserTargetSystem]) { + [self openInSystem:absoluteUrl]; + } else { // _blank or anything else + [self openInInAppBrowser:absoluteUrl withOptions:options]; + } + + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"]; + } + + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options +{ + CDVInAppBrowserOptions* browserOptions = [CDVInAppBrowserOptions parseOptions:options]; + + WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore]; + if (browserOptions.cleardata) { + + NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; + [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:dateFrom completionHandler:^{ + NSLog(@"Removed all WKWebView data"); + self.inAppBrowserViewController.webView.configuration.processPool = [[WKProcessPool alloc] init]; // create new process pool to flush all data + }]; + } + + if (browserOptions.clearcache) { + if (@available(iOS 11.0, *)) { + // Deletes all cookies + WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; + [cookieStore getAllCookies:^(NSArray* cookies) { + NSHTTPCookie* cookie; + for(cookie in cookies){ + [cookieStore deleteCookie:cookie completionHandler:nil]; + } + }]; + }else{ + // https://stackoverflow.com/a/31803708/777265 + // Only deletes domain cookies (not session cookies) + [dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] + completionHandler:^(NSArray * __nonnull records) { + for (WKWebsiteDataRecord *record in records){ + NSSet* dataTypes = record.dataTypes; + if([dataTypes containsObject:WKWebsiteDataTypeCookies]){ + [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes + forDataRecords:@[record] + completionHandler:^{}]; + } + } + }]; + } + } + + if (browserOptions.clearsessioncache) { + if (@available(iOS 11.0, *)) { + // Deletes session cookies + WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; + [cookieStore getAllCookies:^(NSArray* cookies) { + NSHTTPCookie* cookie; + for(cookie in cookies){ + if(cookie.sessionOnly){ + [cookieStore deleteCookie:cookie completionHandler:nil]; + } + } + }]; + }else{ + NSLog(@"clearsessioncache not available below iOS 11.0"); + } + } + + if (self.inAppBrowserViewController == nil) { + NSString* userAgent = [CDVUserAgentUtil originalUserAgent]; + NSString* overrideUserAgent = [self settingForKey:@"OverrideUserAgent"]; + NSString* appendUserAgent = [self settingForKey:@"AppendUserAgent"]; + if(overrideUserAgent){ + userAgent = overrideUserAgent; + } + if(appendUserAgent){ + userAgent = [userAgent stringByAppendingString: appendUserAgent]; + } + self.inAppBrowserViewController = [[CDVWKInAppBrowserViewController alloc] initWithUserAgent:userAgent prevUserAgent:[self.commandDelegate userAgent] browserOptions: browserOptions]; + self.inAppBrowserViewController.navigationDelegate = self; + + if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { + self.inAppBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; + } + } + + [self.inAppBrowserViewController showLocationBar:browserOptions.location]; + [self.inAppBrowserViewController showToolBar:browserOptions.toolbar :browserOptions.toolbarposition]; + if (browserOptions.closebuttoncaption != nil || browserOptions.closebuttoncolor != nil) { + [self.inAppBrowserViewController setCloseButtonTitle:browserOptions.closebuttoncaption :browserOptions.closebuttoncolor]; + } + // Set Presentation Style + UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default + if (browserOptions.presentationstyle != nil) { + if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"pagesheet"]) { + presentationStyle = UIModalPresentationPageSheet; + } else if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"formsheet"]) { + presentationStyle = UIModalPresentationFormSheet; + } + } + self.inAppBrowserViewController.modalPresentationStyle = presentationStyle; + + // Set Transition Style + UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default + if (browserOptions.transitionstyle != nil) { + if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"fliphorizontal"]) { + transitionStyle = UIModalTransitionStyleFlipHorizontal; + } else if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"crossdissolve"]) { + transitionStyle = UIModalTransitionStyleCrossDissolve; + } + } + self.inAppBrowserViewController.modalTransitionStyle = transitionStyle; + + //prevent webView from bouncing + if (browserOptions.disallowoverscroll) { + if ([self.inAppBrowserViewController.webView respondsToSelector:@selector(scrollView)]) { + ((UIScrollView*)[self.inAppBrowserViewController.webView scrollView]).bounces = NO; + } else { + for (id subview in self.inAppBrowserViewController.webView.subviews) { + if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { + ((UIScrollView*)subview).bounces = NO; + } + } + } + } + + + [self.inAppBrowserViewController navigateTo:url]; + [self show:nil withNoAnimate:browserOptions.hidden]; +} + +- (void)show:(CDVInvokedUrlCommand*)command{ + [self show:command withNoAnimate:NO]; +} + +- (void)show:(CDVInvokedUrlCommand*)command withNoAnimate:(BOOL)noAnimate +{ + BOOL initHidden = NO; + if(command == nil && noAnimate == YES){ + initHidden = YES; + } + + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to show IAB after it was closed."); + return; + } + if (_previousStatusBarStyle != -1) { + NSLog(@"Tried to show IAB while already shown"); + return; + } + + if(!initHidden){ + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + } + + __block CDVInAppBrowserNavigationController* nav = [[CDVInAppBrowserNavigationController alloc] + initWithRootViewController:self.inAppBrowserViewController]; + nav.orientationDelegate = self.inAppBrowserViewController; + nav.navigationBarHidden = YES; + nav.modalPresentationStyle = self.inAppBrowserViewController.modalPresentationStyle; + + __weak CDVWKInAppBrowser* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.inAppBrowserViewController != nil) { + float osVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; + CGRect frame = [[UIScreen mainScreen] bounds]; + if(initHidden && osVersion < 11){ + frame.origin.x = -10000; + } + + UIWindow *tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + UIViewController *tmpController = [[UIViewController alloc] init]; + + [tmpWindow setRootViewController:tmpController]; + [tmpWindow setWindowLevel:UIWindowLevelNormal]; + + if(!initHidden || osVersion < 11){ + [tmpWindow makeKeyAndVisible]; + } + [tmpController presentViewController:nav animated:!noAnimate completion:nil]; + } + }); +} + +- (void)hide:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to hide IAB after it was closed."); + return; + + + } + if (_previousStatusBarStyle == -1) { + NSLog(@"Tried to hide IAB while already hidden"); + return; + } + + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.inAppBrowserViewController != nil) { + _previousStatusBarStyle = -1; + [self.inAppBrowserViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil]; + } + }); +} + +- (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + +#ifdef __CORDOVA_4_0_0 + // the webview engine itself will filter for this according to policy + // in config.xml for cordova-ios-4.0 + [self.webViewEngine loadRequest:request]; +#else + if ([self.commandDelegate URLIsWhitelisted:url]) { + [self.webView loadRequest:request]; + } else { // this assumes the InAppBrowser can be excepted from the white-list + [self openInInAppBrowser:url withOptions:options]; + } +#endif +} + +- (void)openInSystem:(NSURL*)url +{ + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + [[UIApplication sharedApplication] openURL:url]; +} + +// This is a helper method for the inject{Script|Style}{Code|File} API calls, which +// provides a consistent method for injecting JavaScript code into the document. +// +// If a wrapper string is supplied, then the source string will be JSON-encoded (adding +// quotes) and wrapped using string formatting. (The wrapper string should have a single +// '%@' marker). +// +// If no wrapper is supplied, then the source string is executed directly. + +- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper +{ + // Ensure a message handler bridge is created to communicate with the CDVWKInAppBrowserViewController + [self evaluateJavaScript: [NSString stringWithFormat:@"(function(w){if(!w._cdvMessageHandler) {w._cdvMessageHandler = function(id,d){w.webkit.messageHandlers.%@.postMessage({d:d, id:id});}}})(window)", IAB_BRIDGE_NAME]]; + + if (jsWrapper != nil) { + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; + NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + if (sourceArrayString) { + NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)]; + NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString]; + [self evaluateJavaScript:jsToInject]; + } + } else { + [self evaluateJavaScript:source]; + } +} + + +//Synchronus helper for javascript evaluation + +- (NSString *)evaluateJavaScript:(NSString *)script { + __block NSString *resultString = nil; + __block BOOL finished = NO; + __block NSString* _script = script; + + [self.inAppBrowserViewController.webView evaluateJavaScript:script completionHandler:^(id result, NSError *error) { + if (error == nil) { + if (result != nil) { + resultString = result; + NSLog(@"%@", resultString); + } + } else { + NSLog(@"evaluateJavaScript error : %@ : %@", error.localizedDescription, _script); + } + finished = YES; + }]; + + while (!finished) + { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + + return resultString; +} + +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper = nil; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"_cdvMessageHandler('%@',JSON.stringify([eval(%%@)]));", command.callbackId]; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectScriptFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvMessageHandler('%@'); }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvMessageHandler('%@'); }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvMessageHandler('%@'); }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (BOOL)isValidCallbackId:(NSString *)callbackId +{ + NSError *err = nil; + // Initialize on first use + if (self.callbackIdPattern == nil) { + self.callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"^InAppBrowser[0-9]{1,10}$" options:0 error:&err]; + if (err != nil) { + // Couldn't initialize Regex; No is safer than Yes. + return NO; + } + } + if ([self.callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) { + return YES; + } + return NO; +} + +/** + * The message handler bridge provided for the InAppBrowser is capable of executing any oustanding callback belonging + * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no + * other code execution is possible. + */ +- (BOOL)webView:(WKWebView*)theWebView decidePolicyForNavigationAction:(NSURLRequest*)request +{ + NSURL* url = request.URL; + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + //if is an app store link, let the system handle it, otherwise it fails to load it + if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { + [theWebView stopLoading]; + [self openInSystem:url]; + return NO; + } + else if ((self.callbackId != nil) && isTopLevelNavigation) { + // Send a loadstart event for each top-level navigation (includes redirects). + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + + return YES; +} + +#pragma mark WKScriptMessageHandler delegate +- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + + CDVPluginResult* pluginResult = nil; + + NSDictionary* messageContent = (NSDictionary*) message.body; + NSString* scriptCallbackId = messageContent[@"id"]; + + if([messageContent objectForKey:@"d"]){ + NSString* scriptResult = messageContent[@"d"]; + NSError* __autoreleasing error = nil; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; + } + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; + } + + [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; +} + +- (void)didStartProvisionalNavigation:(WKWebView*)theWebView +{ + NSLog(@"didStartProvisionalNavigation"); +// self.inAppBrowserViewController.currentURL = theWebView.URL; +} + +- (void)didFinishNavigation:(WKWebView*)theWebView +{ + if (self.callbackId != nil) { + NSString* url = [theWebView.URL absoluteString]; + if(url == nil){ + if(self.inAppBrowserViewController.currentURL != nil){ + url = [self.inAppBrowserViewController.currentURL absoluteString]; + }else{ + url = @""; + } + } + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)webView:(WKWebView*)theWebView didFailNavigation:(NSError*)error +{ + if (self.callbackId != nil) { + NSString* url = [theWebView.URL absoluteString]; + if(url == nil){ + if(self.inAppBrowserViewController.currentURL != nil){ + url = [self.inAppBrowserViewController.currentURL absoluteString]; + }else{ + url = @""; + } + } + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInteger:error.code], @"message": error.localizedDescription}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)browserExit +{ + if (self.callbackId != nil) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"exit"}]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + self.callbackId = nil; + } + + [self.inAppBrowserViewController.configuration.userContentController removeScriptMessageHandlerForName:IAB_BRIDGE_NAME]; + self.inAppBrowserViewController.configuration = nil; + + [self.inAppBrowserViewController.webView stopLoading]; + [self.inAppBrowserViewController.webView removeFromSuperview]; + [self.inAppBrowserViewController.webView setUIDelegate:nil]; + [self.inAppBrowserViewController.webView setNavigationDelegate:nil]; + self.inAppBrowserViewController.webView = nil; + + // Set navigationDelegate to nil to ensure no callbacks are received from it. + self.inAppBrowserViewController.navigationDelegate = nil; + self.inAppBrowserViewController = nil; + + if (IsAtLeastiOSVersion(@"7.0")) { + if (_previousStatusBarStyle != -1) { + [[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle]; + + } + } + + _previousStatusBarStyle = -1; // this value was reset before reapplying it. caused statusbar to stay black on ios7 +} + +@end //CDVWKInAppBrowser + +#pragma mark CDVWKInAppBrowserViewController + +@implementation CDVWKInAppBrowserViewController + +@synthesize currentURL; + +BOOL viewRenderedAtLeastOnce = FALSE; +BOOL isExiting = FALSE; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions +{ + self = [super init]; + if (self != nil) { + _userAgent = userAgent; + _prevUserAgent = prevUserAgent; + _browserOptions = browserOptions; + self.webViewUIDelegate = [[CDVWKInAppBrowserUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]]; + [self.webViewUIDelegate setViewController:self]; + + [self createViews]; + } + + return self; +} + +-(void)dealloc { + //NSLog(@"dealloc"); +} + +- (void)createViews +{ + // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included + + CGRect webViewBounds = self.view.bounds; + BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]; + webViewBounds.size.height -= _browserOptions.location ? FOOTER_HEIGHT : TOOLBAR_HEIGHT; + WKUserContentController* userContentController = [[WKUserContentController alloc] init]; + + WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; + configuration.userContentController = userContentController; +#if __has_include("CDVWKProcessPoolFactory.h") + configuration.processPool = [[CDVWKProcessPoolFactory sharedFactory] sharedProcessPool]; +#endif + [configuration.userContentController addScriptMessageHandler:self name:IAB_BRIDGE_NAME]; + + //WKWebView options + configuration.ignoresViewportScaleLimits = _browserOptions.enableviewportscale; + configuration.allowsInlineMediaPlayback = _browserOptions.allowinlinemediaplayback; + if(_browserOptions.mediaplaybackrequiresuseraction == YES){ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + }else{ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } + + self.webView = [[WKWebView alloc] initWithFrame:webViewBounds configuration:configuration]; + + [self.view addSubview:self.webView]; + [self.view sendSubviewToBack:self.webView]; + + + self.webView.navigationDelegate = self; + self.webView.UIDelegate = self.webViewUIDelegate; + self.webView.backgroundColor = [UIColor whiteColor]; + + self.webView.clearsContextBeforeDrawing = YES; + self.webView.clipsToBounds = YES; + self.webView.contentMode = UIViewContentModeScaleToFill; + self.webView.multipleTouchEnabled = YES; + self.webView.opaque = YES; + self.webView.userInteractionEnabled = YES; + self.automaticallyAdjustsScrollViewInsets = YES ; + [self.webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth]; + self.webView.allowsLinkPreview = NO; + self.webView.allowsBackForwardNavigationGestures = NO; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if (@available(iOS 11.0, *)) { + [self.webView.scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever]; + } +#endif + + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + self.spinner.alpha = 1.000; + self.spinner.autoresizesSubviews = YES; + self.spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin); + self.spinner.clearsContextBeforeDrawing = NO; + self.spinner.clipsToBounds = NO; + self.spinner.contentMode = UIViewContentModeScaleToFill; + self.spinner.frame = CGRectMake(CGRectGetMidX(self.webView.frame), CGRectGetMidY(self.webView.frame), 20.0, 20.0); + self.spinner.hidden = NO; + self.spinner.hidesWhenStopped = YES; + self.spinner.multipleTouchEnabled = NO; + self.spinner.opaque = NO; + self.spinner.userInteractionEnabled = NO; + [self.spinner stopAnimating]; + + self.closeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + + UIBarButtonItem* flexibleSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + + UIBarButtonItem* fixedSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + fixedSpaceButton.width = 20; + + float toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - TOOLBAR_HEIGHT : 0.0; + CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, TOOLBAR_HEIGHT); + + self.toolbar = [[UIToolbar alloc] initWithFrame:toolbarFrame]; + self.toolbar.alpha = 1.000; + self.toolbar.autoresizesSubviews = YES; + self.toolbar.autoresizingMask = toolbarIsAtBottom ? (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin) : UIViewAutoresizingFlexibleWidth; + self.toolbar.barStyle = UIBarStyleBlackOpaque; + self.toolbar.clearsContextBeforeDrawing = NO; + self.toolbar.clipsToBounds = NO; + self.toolbar.contentMode = UIViewContentModeScaleToFill; + self.toolbar.hidden = NO; + self.toolbar.multipleTouchEnabled = NO; + self.toolbar.opaque = NO; + self.toolbar.userInteractionEnabled = YES; + if (_browserOptions.toolbarcolor != nil) { // Set toolbar color if user sets it in options + self.toolbar.barTintColor = [self colorFromHexString:_browserOptions.toolbarcolor]; + } + if (!_browserOptions.toolbartranslucent) { // Set toolbar translucent to no if user sets it in options + self.toolbar.translucent = NO; + } + + CGFloat labelInset = 5.0; + float locationBarY = toolbarIsAtBottom ? self.view.bounds.size.height - FOOTER_HEIGHT : self.view.bounds.size.height - LOCATIONBAR_HEIGHT; + + self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, locationBarY, self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)]; + self.addressLabel.adjustsFontSizeToFitWidth = NO; + self.addressLabel.alpha = 1.000; + self.addressLabel.autoresizesSubviews = YES; + self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; + self.addressLabel.backgroundColor = [UIColor clearColor]; + self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; + self.addressLabel.clearsContextBeforeDrawing = YES; + self.addressLabel.clipsToBounds = YES; + self.addressLabel.contentMode = UIViewContentModeScaleToFill; + self.addressLabel.enabled = YES; + self.addressLabel.hidden = NO; + self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail; + + if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumScaleFactor:")]) { + [self.addressLabel setValue:@(10.0/[UIFont labelFontSize]) forKey:@"minimumScaleFactor"]; + } else if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumFontSize:")]) { + [self.addressLabel setValue:@(10.0) forKey:@"minimumFontSize"]; + } + + self.addressLabel.multipleTouchEnabled = NO; + self.addressLabel.numberOfLines = 1; + self.addressLabel.opaque = NO; + self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0); + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.addressLabel.textAlignment = NSTextAlignmentLeft; + self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000]; + self.addressLabel.userInteractionEnabled = NO; + + NSString* frontArrowString = NSLocalizedString(@"►", nil); // create arrow from Unicode char + self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; + self.forwardButton.enabled = YES; + self.forwardButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.forwardButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + NSString* backArrowString = NSLocalizedString(@"◄", nil); // create arrow from Unicode char + self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; + self.backButton.enabled = YES; + self.backButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.backButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + // Filter out Navigation Buttons if user requests so + if (_browserOptions.hidenavigationbuttons) { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton]]; + } else { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; + } + + self.view.backgroundColor = [UIColor grayColor]; + [self.view addSubview:self.toolbar]; + [self.view addSubview:self.addressLabel]; + [self.view addSubview:self.spinner]; +} + +- (void) setWebViewFrame : (CGRect) frame { + NSLog(@"Setting the WebView's frame to %@", NSStringFromCGRect(frame)); + [self.webView setFrame:frame]; +} + +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString +{ + // the advantage of using UIBarButtonSystemItemDone is the system will localize it for you automatically + // but, if you want to set this yourself, knock yourself out (we can't set the title for a system Done button, so we have to create a new one) + self.closeButton = nil; + // Initialize with title if title is set, otherwise the title will be 'Done' localized + self.closeButton = title != nil ? [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)] : [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + // If color on closebutton is requested then initialize with that that color, otherwise use initialize with default + self.closeButton.tintColor = colorString != nil ? [self colorFromHexString:colorString] : [UIColor colorWithRed:60.0 / 255.0 green:136.0 / 255.0 blue:230.0 / 255.0 alpha:1]; + + NSMutableArray* items = [self.toolbar.items mutableCopy]; + [items replaceObjectAtIndex:0 withObject:self.closeButton]; + [self.toolbar setItems:items]; +} + +- (void)showLocationBar:(BOOL)show +{ + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL toolbarVisible = !self.toolbar.hidden; + + // prevent double show/hide + if (show == !(self.addressLabel.hidden)) { + return; + } + + if (show) { + self.addressLabel.hidden = NO; + + if (toolbarVisible) { + // toolBar at the bottom, leave as is + // put locationBar on top of the toolBar + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= FOOTER_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no toolBar, so put locationBar at the bottom + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } + } else { + self.addressLabel.hidden = YES; + + if (toolbarVisible) { + // locationBar is on top of toolBar, hide locationBar + + // webView take up whole height less toolBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + } else { + // no toolBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition +{ + CGRect toolbarFrame = self.toolbar.frame; + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL locationbarVisible = !self.addressLabel.hidden; + + // prevent double show/hide + if (show == !(self.toolbar.hidden)) { + return; + } + + if (show) { + self.toolbar.hidden = NO; + CGRect webViewBounds = self.view.bounds; + + if (locationbarVisible) { + // locationBar at the bottom, move locationBar up + // put toolBar at the bottom + webViewBounds.size.height -= FOOTER_HEIGHT; + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + self.toolbar.frame = toolbarFrame; + } else { + // no locationBar, so put toolBar at the bottom + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + self.toolbar.frame = toolbarFrame; + } + + if ([toolbarPosition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + toolbarFrame.origin.y = 0; + webViewBounds.origin.y += toolbarFrame.size.height; + [self setWebViewFrame:webViewBounds]; + } else { + toolbarFrame.origin.y = (webViewBounds.size.height + LOCATIONBAR_HEIGHT); + } + [self setWebViewFrame:webViewBounds]; + + } else { + self.toolbar.hidden = YES; + + if (locationbarVisible) { + // locationBar is on top of toolBar, hide toolBar + // put locationBar at the bottom + + // webView take up whole height less locationBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + // move locationBar down + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no locationBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)viewDidLoad +{ + viewRenderedAtLeastOnce = FALSE; + [super viewDidLoad]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + if (isExiting && (self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { + [self.navigationDelegate browserExit]; + isExiting = FALSE; + } +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return UIStatusBarStyleDefault; +} + +- (BOOL)prefersStatusBarHidden { + return NO; +} + +- (void)close +{ + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + self.currentURL = nil; + + __weak UIViewController* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + isExiting = TRUE; + if ([weakSelf respondsToSelector:@selector(presentingViewController)]) { + [[weakSelf presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + } else { + [[weakSelf parentViewController] dismissViewControllerAnimated:YES completion:nil]; + } + }); +} + +- (void)navigateTo:(NSURL*)url +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + + if (_userAgentLockToken != 0) { + [self.webView loadRequest:request]; + } else { + __weak CDVWKInAppBrowserViewController* weakSelf = self; + [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { + _userAgentLockToken = lockToken; + [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken]; + [weakSelf.webView loadRequest:request]; + }]; + } +} + +- (void)goBack:(id)sender +{ + [self.webView goBack]; +} + +- (void)goForward:(id)sender +{ + [self.webView goForward]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + if (IsAtLeastiOSVersion(@"7.0") && !viewRenderedAtLeastOnce) { + viewRenderedAtLeastOnce = TRUE; + CGRect viewBounds = [self.webView bounds]; + viewBounds.origin.y = STATUSBAR_HEIGHT; + viewBounds.size.height = viewBounds.size.height - STATUSBAR_HEIGHT; + self.webView.frame = viewBounds; + [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; + } + [self rePositionViews]; + + [super viewWillAppear:animated]; +} + +// +// On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account. +// The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't +// change that value. +// +- (float) getStatusBarOffset { + CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; + float statusBarOffset = IsAtLeastiOSVersion(@"7.0") ? MIN(statusBarFrame.size.width, statusBarFrame.size.height) : 0.0; + return statusBarOffset; +} + +- (void) rePositionViews { + if ([_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, TOOLBAR_HEIGHT, self.webView.frame.size.width, self.webView.frame.size.height)]; + [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; + } +} + +// Helper function to convert hex color string to UIColor +// Assumes input like "#00FF00" (#RRGGBB). +// Taken from https://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string +- (UIColor *)colorFromHexString:(NSString *)hexString { + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:1]; // bypass '#' character + [scanner scanHexInt:&rgbValue]; + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; +} + +#pragma mark WKNavigationDelegate + +- (void)webView:(WKWebView *)theWebView didStartProvisionalNavigation:(WKNavigation *)navigation{ + + // loading url, start spinner, update back/forward + + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + NSLog(_browserOptions.hidespinner ? @"Yes" : @"No"); + if(!_browserOptions.hidespinner) { + [self.spinner startAnimating]; + } + + return [self.navigationDelegate didStartProvisionalNavigation:theWebView]; +} + +- (void)webView:(WKWebView *)theWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + NSURL *url = navigationAction.request.URL; + NSURL *mainDocumentURL = navigationAction.request.mainDocumentURL; + + BOOL isTopLevelNavigation = [url isEqual:mainDocumentURL]; + + if (isTopLevelNavigation) { + self.currentURL = url; + } + + [self.navigationDelegate webView:theWebView decidePolicyForNavigationAction:navigationAction.request]; + + decisionHandler(WKNavigationActionPolicyAllow); + +} + +- (void)webView:(WKWebView *)theWebView didFinishNavigation:(WKNavigation *)navigation +{ + // update url, stop spinner, update back/forward + + self.addressLabel.text = [self.currentURL absoluteString]; + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + theWebView.scrollView.contentInset = UIEdgeInsetsZero; + + [self.spinner stopAnimating]; + + // Work around a bug where the first time a PDF is opened, all UIWebViews + // reload their User-Agent from NSUserDefaults. + // This work-around makes the following assumptions: + // 1. The app has only a single Cordova Webview. If not, then the app should + // take it upon themselves to load a PDF in the background as a part of + // their start-up flow. + // 2. That the PDF does not require any additional network requests. We change + // the user-agent here back to that of the CDVViewController, so requests + // from it must pass through its white-list. This *does* break PDFs that + // contain links to other remote PDF/websites. + // More info at https://issues.apache.org/jira/browse/CB-2225 + BOOL isPDF = NO; + //TODO webview class + //BOOL isPDF = [@"true" isEqualToString :[theWebView evaluateJavaScript:@"document.body==null"]]; + if (isPDF) { + [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; + } + + [self.navigationDelegate didFinishNavigation:theWebView]; +} + +- (void)webView:(WKWebView*)theWebView failedNavigation:(NSString*) delegateName withError:(nonnull NSError *)error{ + // log fail message, stop spinner, update back/forward + NSLog(@"webView:%@ - %ld: %@", delegateName, (long)error.code, [error localizedDescription]); + + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + [self.spinner stopAnimating]; + + self.addressLabel.text = NSLocalizedString(@"Load Error", nil); + + [self.navigationDelegate webView:theWebView didFailNavigation:error]; +} + +- (void)webView:(WKWebView*)theWebView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error +{ + [self webView:theWebView failedNavigation:@"didFailNavigation" withError:error]; +} + +- (void)webView:(WKWebView*)theWebView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error +{ + [self webView:theWebView failedNavigation:@"didFailProvisionalNavigation" withError:error]; +} + +#pragma mark WKScriptMessageHandler delegate +- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + if (![message.name isEqualToString:IAB_BRIDGE_NAME]) { + return; + } + //NSLog(@"Received script message %@", message.body); + [self.navigationDelegate userContentController:userContentController didReceiveScriptMessage:message]; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { + return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + + return YES; +} + + +@end //CDVWKInAppBrowserViewController diff --git a/src/ios/CDVWKInAppBrowserUIDelegate.h b/src/ios/CDVWKInAppBrowserUIDelegate.h new file mode 100644 index 0000000..1a6ea22 --- /dev/null +++ b/src/ios/CDVWKInAppBrowserUIDelegate.h @@ -0,0 +1,32 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVWKInAppBrowserUIDelegate : NSObject { + @private + UIViewController* _viewController; +} + +@property (nonatomic, copy) NSString* title; + +- (instancetype)initWithTitle:(NSString*)title; +-(void) setViewController:(UIViewController*) viewController; + +@end diff --git a/src/ios/CDVWKInAppBrowserUIDelegate.m b/src/ios/CDVWKInAppBrowserUIDelegate.m new file mode 100644 index 0000000..4bc7a76 --- /dev/null +++ b/src/ios/CDVWKInAppBrowserUIDelegate.m @@ -0,0 +1,127 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVWKInAppBrowserUIDelegate.h" + +@implementation CDVWKInAppBrowserUIDelegate + +- (instancetype)initWithTitle:(NSString*)title +{ + self = [super init]; + if (self) { + self.title = title; + } + + return self; +} + +- (void) webView:(WKWebView*)webView runJavaScriptAlertPanelWithMessage:(NSString*)message + initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(void))completionHandler +{ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + + [alert addAction:ok]; + + [[self getViewController] presentViewController:alert animated:YES completion:nil]; +} + +- (void) webView:(WKWebView*)webView runJavaScriptConfirmPanelWithMessage:(NSString*)message + initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(BOOL result))completionHandler +{ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(YES); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + + [alert addAction:ok]; + + UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(NO); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + [alert addAction:cancel]; + + [[self getViewController] presentViewController:alert animated:YES completion:nil]; +} + +- (void) webView:(WKWebView*)webView runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt + defaultText:(NSString*)defaultText initiatedByFrame:(WKFrameInfo*)frame + completionHandler:(void (^)(NSString* result))completionHandler +{ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title + message:prompt + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(((UITextField*)alert.textFields[0]).text); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + + [alert addAction:ok]; + + UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(nil); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + [alert addAction:cancel]; + + [alert addTextFieldWithConfigurationHandler:^(UITextField* textField) { + textField.text = defaultText; + }]; + + [[self getViewController] presentViewController:alert animated:YES completion:nil]; +} + +-(UIViewController*) getViewController +{ + return _viewController; +} + +-(void) setViewController:(UIViewController*) viewController +{ + _viewController = viewController; +} + +@end diff --git a/tests/plugin.xml b/tests/plugin.xml index f9ab2d3..5bf5600 100644 --- a/tests/plugin.xml +++ b/tests/plugin.xml @@ -29,5 +29,11 @@ + + + + + + diff --git a/tests/tests.js b/tests/tests.js index abb2a6d..69ff46c 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -19,10 +19,11 @@ * */ -/* global MSApp */ +/* global MSApp, alert */ var cordova = require('cordova'); var isWindows = cordova.platformId === 'windows'; +var isIos = cordova.platformId === 'ios'; var isBrowser = cordova.platformId === 'browser'; window.alert = window.alert || navigator.notification.alert; @@ -33,122 +34,133 @@ if (isWindows && navigator && navigator.notification && navigator.notification.a exports.defineAutoTests = function () { - describe('cordova.InAppBrowser', function () { + var createTests = function (platformOpts) { + platformOpts = platformOpts || ''; - it('inappbrowser.spec.1 should exist', function () { - expect(cordova.InAppBrowser).toBeDefined(); - }); + describe('cordova.InAppBrowser', function () { - it('inappbrowser.spec.2 should contain open function', function () { - expect(cordova.InAppBrowser.open).toBeDefined(); - expect(cordova.InAppBrowser.open).toEqual(jasmine.any(Function)); - }); - }); - - describe('open method', function () { - - if (cordova.platformId === 'osx') { - pending('Open method not fully supported on OSX.'); - return; - } - - var iabInstance; - var originalTimeout; - var url = 'https://dist.apache.org/repos/dist/dev/cordova/'; - var badUrl = 'http://bad-uri/'; - - beforeEach(function () { - // increase timeout to ensure test url could be loaded within test time - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; - jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; - - iabInstance = null; - }); - - afterEach(function (done) { - // restore original timeout - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; - - if (iabInstance !== null && iabInstance.close) { - iabInstance.close(); - } - iabInstance = null; - // add some extra time so that iab dialog is closed - setTimeout(done, 2000); - }); - - function verifyEvent (evt, type) { - expect(evt).toBeDefined(); - expect(evt.type).toEqual(type); - // `exit` event does not have url field, browser returns null url for CORS requests - if (type !== 'exit' && !isBrowser) { - expect(evt.url).toEqual(url); - } - } - - function verifyLoadErrorEvent (evt) { - expect(evt).toBeDefined(); - expect(evt.type).toEqual('loaderror'); - expect(evt.url).toEqual(badUrl); - expect(evt.code).toEqual(jasmine.any(Number)); - expect(evt.message).toEqual(jasmine.any(String)); - } - - it('inappbrowser.spec.3 should return InAppBrowser instance with required methods', function () { - iabInstance = cordova.InAppBrowser.open(url, '_blank'); - - expect(iabInstance).toBeDefined(); - - expect(iabInstance.addEventListener).toEqual(jasmine.any(Function)); - expect(iabInstance.removeEventListener).toEqual(jasmine.any(Function)); - expect(iabInstance.close).toEqual(jasmine.any(Function)); - expect(iabInstance.show).toEqual(jasmine.any(Function)); - expect(iabInstance.hide).toEqual(jasmine.any(Function)); - expect(iabInstance.executeScript).toEqual(jasmine.any(Function)); - expect(iabInstance.insertCSS).toEqual(jasmine.any(Function)); - }); - - it('inappbrowser.spec.4 should support loadstart and loadstop events', function (done) { - var onLoadStart = jasmine.createSpy('loadstart event callback').and.callFake(function (evt) { - verifyEvent(evt, 'loadstart'); + it('inappbrowser.spec.1 should exist', function () { + expect(cordova.InAppBrowser).toBeDefined(); }); - iabInstance = cordova.InAppBrowser.open(url, '_blank'); - iabInstance.addEventListener('loadstart', onLoadStart); - iabInstance.addEventListener('loadstop', function (evt) { - verifyEvent(evt, 'loadstop'); - if (!isBrowser) { - // according to documentation, "loadstart" event is not supported on browser - // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 - expect(onLoadStart).toHaveBeenCalled(); + it('inappbrowser.spec.2 should contain open function', function () { + expect(cordova.InAppBrowser.open).toBeDefined(); + expect(cordova.InAppBrowser.open).toEqual(jasmine.any(Function)); + }); + }); + + describe('open method', function () { + + if (cordova.platformId === 'osx') { + pending('Open method not fully supported on OSX.'); + return; + } + + var iabInstance; + var originalTimeout; + var url = 'https://dist.apache.org/repos/dist/dev/cordova/'; + var badUrl = 'http://bad-uri/'; + + beforeEach(function () { + // increase timeout to ensure test url could be loaded within test time + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; + + iabInstance = null; + }); + + afterEach(function (done) { + // restore original timeout + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + + if (iabInstance !== null && iabInstance.close) { + iabInstance.close(); } - done(); + iabInstance = null; + // add some extra time so that iab dialog is closed + setTimeout(done, 2000); }); - }); - it('inappbrowser.spec.5 should support exit event', function (done) { - iabInstance = cordova.InAppBrowser.open(url, '_blank'); - iabInstance.addEventListener('exit', function (evt) { - verifyEvent(evt, 'exit'); - done(); - }); - iabInstance.close(); - iabInstance = null; - }); - - it('inappbrowser.spec.6 should support loaderror event', function (done) { - if (isBrowser) { - // according to documentation, "loaderror" event is not supported on browser - // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 - pending('Browser platform doesn\'t support loaderror event'); + function verifyEvent (evt, type) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual(type); + // `exit` event does not have url field, browser returns null url for CORS requests + if (type !== 'exit' && !isBrowser) { + expect(evt.url).toEqual(url); + } } - iabInstance = cordova.InAppBrowser.open(badUrl, '_blank'); - iabInstance.addEventListener('loaderror', function (evt) { - verifyLoadErrorEvent(evt); - done(); + + function verifyLoadErrorEvent (evt) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual('loaderror'); + expect(evt.url).toEqual(badUrl); + expect(evt.code).toEqual(jasmine.any(Number)); + expect(evt.message).toEqual(jasmine.any(String)); + } + + it('inappbrowser.spec.3 should return InAppBrowser instance with required methods', function () { + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + + expect(iabInstance).toBeDefined(); + + expect(iabInstance.addEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.removeEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.close).toEqual(jasmine.any(Function)); + expect(iabInstance.show).toEqual(jasmine.any(Function)); + expect(iabInstance.hide).toEqual(jasmine.any(Function)); + expect(iabInstance.executeScript).toEqual(jasmine.any(Function)); + expect(iabInstance.insertCSS).toEqual(jasmine.any(Function)); + }); + + it('inappbrowser.spec.4 should support loadstart and loadstop events', function (done) { + var onLoadStart = jasmine.createSpy('loadstart event callback').and.callFake(function (evt) { + verifyEvent(evt, 'loadstart'); + }); + + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('loadstart', onLoadStart); + iabInstance.addEventListener('loadstop', function (evt) { + verifyEvent(evt, 'loadstop'); + if (!isBrowser) { + // according to documentation, "loadstart" event is not supported on browser + // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 + expect(onLoadStart).toHaveBeenCalled(); + } + done(); + }); + }); + + it('inappbrowser.spec.5 should support exit event', function (done) { + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('exit', function (evt) { + verifyEvent(evt, 'exit'); + done(); + }); + iabInstance.close(); + iabInstance = null; + }); + + it('inappbrowser.spec.6 should support loaderror event', function (done) { + if (isBrowser) { + // according to documentation, "loaderror" event is not supported on browser + // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 + pending('Browser platform doesn\'t support loaderror event'); + } + iabInstance = cordova.InAppBrowser.open(badUrl, '_blank', platformOpts); + iabInstance.addEventListener('loaderror', function (evt) { + verifyLoadErrorEvent(evt); + done(); + }); }); }); - }); + }; + + if (isIos) { + createTests('usewkwebview=no'); + createTests('usewkwebview=yes'); + } else { + createTests(); + } }; exports.defineManualTests = function (contentEl, createActionButton) { @@ -161,6 +173,7 @@ exports.defineManualTests = function (contentEl, createActionButton) { var counts; var lastLoadStartURL; var wasReset = false; + function reset () { counts = { 'loaderror': 0, @@ -170,6 +183,7 @@ exports.defineManualTests = function (contentEl, createActionButton) { }; lastLoadStartURL = ''; } + reset(); var iab; @@ -183,6 +197,9 @@ exports.defineManualTests = function (contentEl, createActionButton) { console.log('Use window.open() for url'); iab = window.open(url, target, params, callbacks); } else { + if (platformOpts) { + params += (params ? ',' : '') + platformOpts; + } iab = cordova.InAppBrowser.open(url, target, params, callbacks); } if (!iab) { @@ -260,11 +277,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { }; if (cssUrl) { iab.addEventListener('loadstop', function (event) { - iab.insertCSS({ file: cssUrl }, useCallback && callback); + iab.insertCSS({file: cssUrl}, useCallback && callback); }); } else { iab.addEventListener('loadstop', function (event) { - iab.insertCSS({ code: '#style-update-literal { \ndisplay: block !important; \n}' }, + iab.insertCSS({code: '#style-update-literal { \ndisplay: block !important; \n}'}, useCallback && callback); }); } @@ -274,7 +291,7 @@ exports.defineManualTests = function (contentEl, createActionButton) { var iab = doOpen(url, '_blank', 'location=yes'); if (jsUrl) { iab.addEventListener('loadstop', function (event) { - iab.executeScript({ file: jsUrl }, useCallback && function (results) { + iab.executeScript({file: jsUrl}, useCallback && function (results) { if (results && results.length === 0) { alert('Results verified'); // eslint-disable-line no-undef } else { @@ -286,11 +303,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { } else { iab.addEventListener('loadstop', function (event) { var code = '(function(){\n' + - ' var header = document.getElementById("header");\n' + - ' header.innerHTML = "Script literal successfully injected";\n' + - ' return "abc";\n' + - '})()'; - iab.executeScript({ code: code }, useCallback && function (results) { + ' var header = document.getElementById("header");\n' + + ' header.innerHTML = "Script literal successfully injected";\n' + + ' return "abc";\n' + + '})()'; + iab.executeScript({code: code}, useCallback && function (results) { if (results && results.length === 1 && results[0] === 'abc') { alert('Results verified'); // eslint-disable-line no-undef } else { @@ -301,10 +318,16 @@ exports.defineManualTests = function (contentEl, createActionButton) { }); } } + var hiddenwnd = null; - var loadlistener = function (event) { alert('background window loaded '); }; // eslint-disable-line no-undef + var loadlistener = function (event) { + alert('background window loaded '); + }; // eslint-disable-line no-undef function openHidden (url, startHidden) { var shopt = (startHidden) ? 'hidden=yes' : ''; + if (platformOpts) { + shopt += (shopt ? ',' : '') + platformOpts; + } hiddenwnd = cordova.InAppBrowser.open(url, 'random_string', shopt); if (!hiddenwnd) { alert('cordova.InAppBrowser.open returned ' + hiddenwnd); // eslint-disable-line no-undef @@ -312,11 +335,13 @@ exports.defineManualTests = function (contentEl, createActionButton) { } if (startHidden) hiddenwnd.addEventListener('loadstop', loadlistener); } + function showHidden () { if (hiddenwnd) { hiddenwnd.show(); } } + function closeHidden () { if (hiddenwnd) { hiddenwnd.removeEventListener('loadstop', loadlistener); @@ -333,6 +358,15 @@ exports.defineManualTests = function (contentEl, createActionButton) { '

User-Agent: ' + ''; + var platformOpts = ''; + var platform_info = ''; + if (isIos) { + platformOpts = 'usewkwebview=no'; + platform_info = '

Webview

' + + '

Use this button to toggle the Webview implementation.

' + + '
'; + } + var local_tests = '

Local URL

' + '
' + 'Expected result: opens successfully in CordovaWebView.' + @@ -471,11 +505,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { // see http://msdn.microsoft.com/en-us/library/windows/apps/hh465380.aspx#differences for details if (window.MSApp && window.MSApp.execUnsafeLocalFunction) { MSApp.execUnsafeLocalFunction(function () { - contentEl.innerHTML = info_div + local_tests + white_listed_tests + non_white_listed_tests + page_with_redirects_tests + pdf_url_tests + invalid_url_tests + + contentEl.innerHTML = info_div + platform_info + local_tests + white_listed_tests + non_white_listed_tests + page_with_redirects_tests + pdf_url_tests + invalid_url_tests + css_js_injection_tests + open_hidden_tests + clearing_cache_tests + video_tag_tests + local_with_anchor_tag_tests + hardwareback_tests; }); } else { - contentEl.innerHTML = info_div + local_tests + white_listed_tests + non_white_listed_tests + page_with_redirects_tests + pdf_url_tests + invalid_url_tests + + contentEl.innerHTML = info_div + platform_info + local_tests + white_listed_tests + non_white_listed_tests + page_with_redirects_tests + pdf_url_tests + invalid_url_tests + css_js_injection_tests + open_hidden_tests + clearing_cache_tests + video_tag_tests + local_with_anchor_tag_tests + hardwareback_tests; } @@ -490,6 +524,22 @@ exports.defineManualTests = function (contentEl, createActionButton) { var injectcss = isWindows ? basePath + 'inject.css' : 'inject.css'; var videohtml = basePath + 'video.html'; + if (isIos) { + createActionButton('Webview=UIWebView', function () { + var webviewOption = 'usewkwebview='; + var webviewToggle = document.getElementById('webviewToggle'); + var button = webviewToggle.getElementsByClassName('topcoat-button')[0]; + + if (platformOpts === webviewOption + 'yes') { + platformOpts = webviewOption + 'no'; + button.textContent = 'Webview=UIWebView'; + } else { + platformOpts = webviewOption + 'yes'; + button.textContent = 'Webview=WKWebView'; + } + }, 'webviewToggle'); + } + // Local createActionButton('target=Default', function () { doOpen(localhtml); @@ -679,17 +729,17 @@ exports.defineManualTests = function (contentEl, createActionButton) { doOpen('http://cordova.apache.org', '_blank', 'hardwareback=no'); }, 'openHardwareBackNo'); createActionButton('no hardwareback -> hardwareback=no -> no hardwareback', function () { - var ref = cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes'); + var ref = cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes' + (platformOpts ? ',' + platformOpts : '')); ref.addEventListener('loadstop', function () { ref.close(); }); ref.addEventListener('exit', function () { - var ref2 = cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes,hardwareback=no'); + var ref2 = cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes,hardwareback=no' + (platformOpts ? ',' + platformOpts : '')); ref2.addEventListener('loadstop', function () { ref2.close(); }); ref2.addEventListener('exit', function () { - cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes'); + cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes' + (platformOpts ? ',' + platformOpts : '')); }); }); }, 'openHardwareBackDefaultAfterNo'); From b48e02fcd5298c56f000e716f5492f7dc7ab73f1 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Sun, 30 Sep 2018 08:03:16 +0100 Subject: [PATCH 28/53] fix iOS 10 build --- src/ios/CDVWKInAppBrowser.m | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m index 4d59bc2..9b515dd 100644 --- a/src/ios/CDVWKInAppBrowser.m +++ b/src/ios/CDVWKInAppBrowser.m @@ -145,7 +145,15 @@ static CDVWKInAppBrowser* instance = nil; } if (browserOptions.clearcache) { + bool isAtLeastiOS11 = false; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 if (@available(iOS 11.0, *)) { + isAtLeastiOS11 = true; + } +#endif + + if(isAtLeastiOS11){ +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 // Deletes all cookies WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; [cookieStore getAllCookies:^(NSArray* cookies) { @@ -154,6 +162,7 @@ static CDVWKInAppBrowser* instance = nil; [cookieStore deleteCookie:cookie completionHandler:nil]; } }]; +#endif }else{ // https://stackoverflow.com/a/31803708/777265 // Only deletes domain cookies (not session cookies) @@ -172,7 +181,14 @@ static CDVWKInAppBrowser* instance = nil; } if (browserOptions.clearsessioncache) { + bool isAtLeastiOS11 = false; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 if (@available(iOS 11.0, *)) { + isAtLeastiOS11 = true; + } +#endif + if (isAtLeastiOS11) { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 // Deletes session cookies WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; [cookieStore getAllCookies:^(NSArray* cookies) { @@ -183,11 +199,12 @@ static CDVWKInAppBrowser* instance = nil; } } }]; +#endif }else{ NSLog(@"clearsessioncache not available below iOS 11.0"); } } - + if (self.inAppBrowserViewController == nil) { NSString* userAgent = [CDVUserAgentUtil originalUserAgent]; NSString* overrideUserAgent = [self settingForKey:@"OverrideUserAgent"]; From dc7fa34bbe08cf521ac01f603dace16a17091bff Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Mon, 3 Sep 2018 13:12:15 +0100 Subject: [PATCH 29/53] CB-7179 (iOS): Add support to optionally use WKWebView for iOS --- .travis.yml | 4 +- README.md | 14 +- RELEASENOTES.md | 3 + package.json | 2 +- plugin.xml | 22 +- src/ios/CDVInAppBrowser.h | 90 +- src/ios/CDVInAppBrowser.m | 1133 +--------------- src/ios/CDVInAppBrowserNavigationController.h | 27 + src/ios/CDVInAppBrowserNavigationController.m | 85 ++ src/ios/CDVInAppBrowserOptions.h | 51 + src/ios/CDVInAppBrowserOptions.m | 91 ++ src/ios/CDVUIInAppBrowser.h | 86 ++ src/ios/CDVUIInAppBrowser.m | 1029 ++++++++++++++ src/ios/CDVWKInAppBrowser.h | 76 ++ src/ios/CDVWKInAppBrowser.m | 1181 +++++++++++++++++ src/ios/CDVWKInAppBrowserUIDelegate.h | 32 + src/ios/CDVWKInAppBrowserUIDelegate.m | 127 ++ tests/plugin.xml | 6 + tests/tests.js | 292 ++-- 19 files changed, 3052 insertions(+), 1299 deletions(-) create mode 100644 src/ios/CDVInAppBrowserNavigationController.h create mode 100644 src/ios/CDVInAppBrowserNavigationController.m create mode 100644 src/ios/CDVInAppBrowserOptions.h create mode 100644 src/ios/CDVInAppBrowserOptions.m create mode 100644 src/ios/CDVUIInAppBrowser.h create mode 100644 src/ios/CDVUIInAppBrowser.m create mode 100644 src/ios/CDVWKInAppBrowser.h create mode 100644 src/ios/CDVWKInAppBrowser.m create mode 100644 src/ios/CDVWKInAppBrowserUIDelegate.h create mode 100644 src/ios/CDVWKInAppBrowserUIDelegate.m diff --git a/.travis.yml b/.travis.yml index 00def86..2be2bec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,9 +28,9 @@ matrix: os: linux language: node_js node_js: '4.2' - - env: PLATFORM=ios-9.3 + - env: PLATFORM=ios-10.0 os: osx - osx_image: xcode7.3 + osx_image: xcode8.3 language: node_js node_js: '4.2' # TBD SKIP for now: diff --git a/README.md b/README.md index eade58a..b7ff468 100644 --- a/README.md +++ b/README.md @@ -136,9 +136,11 @@ instance, or the system browser. iOS supports these additional options: + - __usewkwebview__: set to `yes` to use WKWebView engine for the InappBrowser. Omit or set to `no` (default) to use UIWebView. Note: Using `usewkwebview=yes` requires that a WKWebView engine plugin be installed in the Cordova project (e.g. [cordova-plugin-wkwebview-engine](https://github.com/apache/cordova-plugin-wkwebview-engine) or [cordova-plugin-ionic-webview](https://github.com/ionic-team/cordova-plugin-ionic-webview)). - __hidden__: set to `yes` to create the browser and load the page, but not show it. The loadstop event fires when loading is complete. Omit or set to `no` (default) to have the browser open and load normally. - __clearcache__: set to `yes` to have the browser's cookie cache cleared before the new window is opened - - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened + - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened. For WKWebView, requires iOS 11+ on target device. + - __cleardata__: set to `yes` to have the browser's entire local storage cleared (cookies, HTML5 local storage, IndexedDB, etc.) before the new window is opened - __closebuttoncolor__: set as a valid hex color string, for example: `#00ff00`, to change from the default __Done__ button's color. Only applicable if toolbar is not disabled. - __closebuttoncaption__: set to a string to use as the __Done__ button's caption. Note that you need to localize this value yourself. - __disallowoverscroll__: Set to `yes` or `no` (default is `no`). Turns on/off the UIWebViewBounce property. @@ -147,11 +149,11 @@ instance, or the system browser. - __toolbar__: set to `yes` or `no` to turn the toolbar on or off for the InAppBrowser (defaults to `yes`) - __toolbarcolor__: set as a valid hex color string, for example: `#00ff00`, to change from the default color of the toolbar. Only applicable if toolbar is not disabled. - __toolbartranslucent__: set to `yes` or `no` to make the toolbar translucent(semi-transparent) (defaults to `yes`). Only applicable if toolbar is not disabled. - - __enableViewportScale__: Set to `yes` or `no` to prevent viewport scaling through a meta tag (defaults to `no`). - - __mediaPlaybackRequiresUserAction__: Set to `yes` to prevent HTML5 audio or video from autoplaying (defaults to `no`). - - __allowInlineMediaPlayback__: Set to `yes` or `no` to allow in-line HTML5 media playback, displaying within the browser window rather than a device-specific playback interface. The HTML's `video` element must also include the `webkit-playsinline` attribute (defaults to `no`) - - __keyboardDisplayRequiresUserAction__: Set to `yes` or `no` to open the keyboard when form elements receive focus via JavaScript's `focus()` call (defaults to `yes`). - - __suppressesIncrementalRendering__: Set to `yes` or `no` to wait until all new view content is received before being rendered (defaults to `no`). + - __enableViewportScale__: Set to `yes` or `no` to prevent viewport scaling through a meta tag (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`). + - __mediaPlaybackRequiresUserAction__: Set to `yes` to prevent HTML5 audio or video from autoplaying (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`). + - __allowInlineMediaPlayback__: Set to `yes` or `no` to allow in-line HTML5 media playback, displaying within the browser window rather than a device-specific playback interface. The HTML's `video` element must also include the `webkit-playsinline` attribute (defaults to `no`) Only applicable to UIWebView (`usewkwebview=no`). + - __keyboardDisplayRequiresUserAction__: Set to `yes` or `no` to open the keyboard when form elements receive focus via JavaScript's `focus()` call (defaults to `yes`). Only applicable to UIWebView (`usewkwebview=no`). + - __suppressesIncrementalRendering__: Set to `yes` or `no` to wait until all new view content is received before being rendered (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`). - __presentationstyle__: Set to `pagesheet`, `formsheet` or `fullscreen` to set the [presentation style](http://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/modalPresentationStyle) (defaults to `fullscreen`). - __transitionstyle__: Set to `fliphorizontal`, `crossdissolve` or `coververtical` to set the [transition style](http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/modalTransitionStyle) (defaults to `coververtical`). - __toolbarposition__: Set to `top` or `bottom` (default is `bottom`). Causes the toolbar to be at the top or bottom of the window. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6a06956..0812b8d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,9 @@ --> # Release Notes +### 3.1.0-dev (Unreleased) +* CB-7179 (iOS): Add support to optionally use WKWebView for iOS + ### 3.0.0 (Apr 12, 2018) * [CB-13659](https://issues.apache.org/jira/browse/CB-13659) **iOS** Add hidespinner option * In file `AppBrowser.java`: New code within `shouldOverrideUrlLoading()` to check for whitelisting custom schemes via a new `AllowedSchemes` preference configuration item. Allows custom schemes like `mycoolapp://` or `wevotetwitterscheme://` diff --git a/package.json b/package.json index a7f57c3..f3be23f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-inappbrowser", - "version": "3.0.1-dev", + "version": "3.1.0-dev", "description": "Cordova InAppBrowser Plugin", "types": "./types/index.d.ts", "cordova": { diff --git a/plugin.xml b/plugin.xml index 4a820dd..8775d61 100644 --- a/plugin.xml +++ b/plugin.xml @@ -20,7 +20,7 @@ + version="3.1.0-dev"> InAppBrowser Cordova InAppBrowser Plugin @@ -76,11 +76,31 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/ios/CDVInAppBrowser.h b/src/ios/CDVInAppBrowser.h index 66066b9..d40e92b 100644 --- a/src/ios/CDVInAppBrowser.h +++ b/src/ios/CDVInAppBrowser.h @@ -19,23 +19,11 @@ #import #import -#import -#ifdef __CORDOVA_4_0_0 - #import -#else - #import -#endif +@interface CDVInAppBrowser : CDVPlugin {} -@class CDVInAppBrowserViewController; - -@interface CDVInAppBrowser : CDVPlugin { - UIWindow * tmpWindow; -} - -@property (nonatomic, retain) CDVInAppBrowserViewController* inAppBrowserViewController; -@property (nonatomic, copy) NSString* callbackId; -@property (nonatomic, copy) NSRegularExpression *callbackIdPattern; +@property (nonatomic, assign) BOOL wkwebviewavailable; +@property (nonatomic, assign) BOOL usewkwebview; - (void)open:(CDVInvokedUrlCommand*)command; - (void)close:(CDVInvokedUrlCommand*)command; @@ -45,75 +33,3 @@ @end -@interface CDVInAppBrowserOptions : NSObject {} - -@property (nonatomic, assign) BOOL location; -@property (nonatomic, assign) BOOL toolbar; -@property (nonatomic, copy) NSString* closebuttoncaption; -@property (nonatomic, copy) NSString* closebuttoncolor; -@property (nonatomic, copy) NSString* toolbarposition; -@property (nonatomic, copy) NSString* toolbarcolor; -@property (nonatomic, assign) BOOL toolbartranslucent; -@property (nonatomic, assign) BOOL hidenavigationbuttons; -@property (nonatomic, copy) NSString* navigationbuttoncolor; -@property (nonatomic, assign) BOOL clearcache; -@property (nonatomic, assign) BOOL clearsessioncache; -@property (nonatomic, assign) BOOL hidespinner; - -@property (nonatomic, copy) NSString* presentationstyle; -@property (nonatomic, copy) NSString* transitionstyle; - -@property (nonatomic, assign) BOOL enableviewportscale; -@property (nonatomic, assign) BOOL mediaplaybackrequiresuseraction; -@property (nonatomic, assign) BOOL allowinlinemediaplayback; -@property (nonatomic, assign) BOOL keyboarddisplayrequiresuseraction; -@property (nonatomic, assign) BOOL suppressesincrementalrendering; -@property (nonatomic, assign) BOOL hidden; -@property (nonatomic, assign) BOOL disallowoverscroll; - -+ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; - -@end - -@interface CDVInAppBrowserViewController : UIViewController { - @private - NSString* _userAgent; - NSString* _prevUserAgent; - NSInteger _userAgentLockToken; - CDVInAppBrowserOptions *_browserOptions; - -#ifdef __CORDOVA_4_0_0 - CDVUIWebViewDelegate* _webViewDelegate; -#else - CDVWebViewDelegate* _webViewDelegate; -#endif - -} - -@property (nonatomic, strong) IBOutlet UIWebView* webView; -@property (nonatomic, strong) IBOutlet UIBarButtonItem* closeButton; -@property (nonatomic, strong) IBOutlet UILabel* addressLabel; -@property (nonatomic, strong) IBOutlet UIBarButtonItem* backButton; -@property (nonatomic, strong) IBOutlet UIBarButtonItem* forwardButton; -@property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; -@property (nonatomic, strong) IBOutlet UIToolbar* toolbar; - -@property (nonatomic, weak) id orientationDelegate; -@property (nonatomic, weak) CDVInAppBrowser* navigationDelegate; -@property (nonatomic) NSURL* currentURL; - -- (void)close; -- (void)navigateTo:(NSURL*)url; -- (void)showLocationBar:(BOOL)show; -- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition; -- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString; - -- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions; - -@end - -@interface CDVInAppBrowserNavigationController : UINavigationController - -@property (nonatomic, weak) id orientationDelegate; - -@end diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index 0ca3feb..ad30b23 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -18,1136 +18,107 @@ */ #import "CDVInAppBrowser.h" +#import "CDVInAppBrowserOptions.h" +#import "CDVUIInAppBrowser.h" +#import "CDVWKInAppBrowser.h" #import -#import -#define kInAppBrowserTargetSelf @"_self" -#define kInAppBrowserTargetSystem @"_system" -#define kInAppBrowserTargetBlank @"_blank" - -#define kInAppBrowserToolbarBarPositionBottom @"bottom" -#define kInAppBrowserToolbarBarPositionTop @"top" - -#define TOOLBAR_HEIGHT 44.0 -#define STATUSBAR_HEIGHT 20.0 -#define LOCATIONBAR_HEIGHT 21.0 -#define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) #pragma mark CDVInAppBrowser -@interface CDVInAppBrowser () { - NSInteger _previousStatusBarStyle; -} -@end - @implementation CDVInAppBrowser - (void)pluginInitialize { - _previousStatusBarStyle = -1; - _callbackIdPattern = nil; -} + // default values + self.usewkwebview = NO; -- (id)settingForKey:(NSString*)key -{ - return [self.commandDelegate.settings objectForKey:[key lowercaseString]]; -} - -- (void)onReset -{ - [self close:nil]; -} - -- (void)close:(CDVInvokedUrlCommand*)command -{ - if (self.inAppBrowserViewController == nil) { - NSLog(@"IAB.close() called but it was already closed."); - return; - } - // Things are cleaned up in browserExit. - [self.inAppBrowserViewController close]; -} - -- (BOOL) isSystemUrl:(NSURL*)url -{ - if ([[url host] isEqualToString:@"itunes.apple.com"]) { - return YES; - } - - return NO; +#if __has_include("CDVWKWebViewEngine.h") + self.wkwebviewavailable = YES; +#else + self.wkwebviewavailable = NO; +#endif } - (void)open:(CDVInvokedUrlCommand*)command { - CDVPluginResult* pluginResult; - - NSString* url = [command argumentAtIndex:0]; - NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf]; NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; - - self.callbackId = command.callbackId; - - if (url != nil) { -#ifdef __CORDOVA_4_0_0 - NSURL* baseUrl = [self.webViewEngine URL]; -#else - NSURL* baseUrl = [self.webView.request URL]; -#endif - NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; - - if ([self isSystemUrl:absoluteUrl]) { - target = kInAppBrowserTargetSystem; - } - - if ([target isEqualToString:kInAppBrowserTargetSelf]) { - [self openInCordovaWebView:absoluteUrl withOptions:options]; - } else if ([target isEqualToString:kInAppBrowserTargetSystem]) { - [self openInSystem:absoluteUrl]; - } else { // _blank or anything else - [self openInInAppBrowser:absoluteUrl withOptions:options]; - } - - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; - } else { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"]; - } - - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; -} - -- (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options -{ CDVInAppBrowserOptions* browserOptions = [CDVInAppBrowserOptions parseOptions:options]; - - if (browserOptions.clearcache) { - NSHTTPCookie *cookie; - NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - for (cookie in [storage cookies]) - { - if (![cookie.domain isEqual: @".^filecookies^"]) { - [storage deleteCookie:cookie]; - } - } + if(browserOptions.usewkwebview && !self.wkwebviewavailable){ + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:@{@"type":@"loaderror", @"message": @"usewkwebview option specified but but no plugin that supplies a WKWebView engine is present"}] callbackId:command.callbackId]; + return; } - - if (browserOptions.clearsessioncache) { - NSHTTPCookie *cookie; - NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - for (cookie in [storage cookies]) - { - if (![cookie.domain isEqual: @".^filecookies^"] && cookie.isSessionOnly) { - [storage deleteCookie:cookie]; - } - } - } - - if (self.inAppBrowserViewController == nil) { - NSString* userAgent = [CDVUserAgentUtil originalUserAgent]; - NSString* overrideUserAgent = [self settingForKey:@"OverrideUserAgent"]; - NSString* appendUserAgent = [self settingForKey:@"AppendUserAgent"]; - if(overrideUserAgent){ - userAgent = overrideUserAgent; - } - if(appendUserAgent){ - userAgent = [userAgent stringByAppendingString: appendUserAgent]; - } - self.inAppBrowserViewController = [[CDVInAppBrowserViewController alloc] initWithUserAgent:userAgent prevUserAgent:[self.commandDelegate userAgent] browserOptions: browserOptions]; - self.inAppBrowserViewController.navigationDelegate = self; - - if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { - self.inAppBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; - } - } - - [self.inAppBrowserViewController showLocationBar:browserOptions.location]; - [self.inAppBrowserViewController showToolBar:browserOptions.toolbar :browserOptions.toolbarposition]; - if (browserOptions.closebuttoncaption != nil || browserOptions.closebuttoncolor != nil) { - [self.inAppBrowserViewController setCloseButtonTitle:browserOptions.closebuttoncaption :browserOptions.closebuttoncolor]; - } - // Set Presentation Style - UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default - if (browserOptions.presentationstyle != nil) { - if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"pagesheet"]) { - presentationStyle = UIModalPresentationPageSheet; - } else if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"formsheet"]) { - presentationStyle = UIModalPresentationFormSheet; - } - } - self.inAppBrowserViewController.modalPresentationStyle = presentationStyle; - - // Set Transition Style - UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default - if (browserOptions.transitionstyle != nil) { - if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"fliphorizontal"]) { - transitionStyle = UIModalTransitionStyleFlipHorizontal; - } else if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"crossdissolve"]) { - transitionStyle = UIModalTransitionStyleCrossDissolve; - } - } - self.inAppBrowserViewController.modalTransitionStyle = transitionStyle; - - // prevent webView from bouncing - if (browserOptions.disallowoverscroll) { - if ([self.inAppBrowserViewController.webView respondsToSelector:@selector(scrollView)]) { - ((UIScrollView*)[self.inAppBrowserViewController.webView scrollView]).bounces = NO; - } else { - for (id subview in self.inAppBrowserViewController.webView.subviews) { - if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { - ((UIScrollView*)subview).bounces = NO; - } - } - } - } - - // UIWebView options - self.inAppBrowserViewController.webView.scalesPageToFit = browserOptions.enableviewportscale; - self.inAppBrowserViewController.webView.mediaPlaybackRequiresUserAction = browserOptions.mediaplaybackrequiresuseraction; - self.inAppBrowserViewController.webView.allowsInlineMediaPlayback = browserOptions.allowinlinemediaplayback; - if (IsAtLeastiOSVersion(@"6.0")) { - self.inAppBrowserViewController.webView.keyboardDisplayRequiresUserAction = browserOptions.keyboarddisplayrequiresuseraction; - self.inAppBrowserViewController.webView.suppressesIncrementalRendering = browserOptions.suppressesincrementalrendering; - } - - [self.inAppBrowserViewController navigateTo:url]; - if (!browserOptions.hidden) { - [self show:nil]; + self.usewkwebview = browserOptions.usewkwebview; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] open:command]; + }else{ + [[CDVUIInAppBrowser getInstance] open:command]; } } +- (void)close:(CDVInvokedUrlCommand*)command +{ + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] close:command]; + }else{ + [[CDVUIInAppBrowser getInstance] close:command]; + } +} + + - (void)show:(CDVInvokedUrlCommand*)command { - if (self.inAppBrowserViewController == nil) { - NSLog(@"Tried to show IAB after it was closed."); - return; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] show:command]; + }else{ + [[CDVUIInAppBrowser getInstance] show:command]; } - if (_previousStatusBarStyle != -1) { - NSLog(@"Tried to show IAB while already shown"); - return; - } - - _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; - - __block CDVInAppBrowserNavigationController* nav = [[CDVInAppBrowserNavigationController alloc] - initWithRootViewController:self.inAppBrowserViewController]; - nav.orientationDelegate = self.inAppBrowserViewController; - nav.navigationBarHidden = YES; - nav.modalPresentationStyle = self.inAppBrowserViewController.modalPresentationStyle; - - __weak CDVInAppBrowser* weakSelf = self; - - // Run later to avoid the "took a long time" log message. - dispatch_async(dispatch_get_main_queue(), ^{ - if (weakSelf.inAppBrowserViewController != nil) { - if (!tmpWindow) { - CGRect frame = [[UIScreen mainScreen] bounds]; - tmpWindow = [[UIWindow alloc] initWithFrame:frame]; - } - UIViewController *tmpController = [[UIViewController alloc] init]; - double baseWindowLevel = [UIApplication sharedApplication].keyWindow.windowLevel; - [tmpWindow setRootViewController:tmpController]; - [tmpWindow setWindowLevel:baseWindowLevel+1]; - - [tmpWindow makeKeyAndVisible]; - [tmpController presentViewController:nav animated:YES completion:nil]; - } - }); } - (void)hide:(CDVInvokedUrlCommand*)command { - if (self.inAppBrowserViewController == nil) { - NSLog(@"Tried to hide IAB after it was closed."); - return; - - - } - if (_previousStatusBarStyle == -1) { - NSLog(@"Tried to hide IAB while already hidden"); - return; - } - - _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; - - // Run later to avoid the "took a long time" log message. - dispatch_async(dispatch_get_main_queue(), ^{ - if (self.inAppBrowserViewController != nil) { - _previousStatusBarStyle = -1; - [self.inAppBrowserViewController.presentingViewController dismissViewControllerAnimated:YES completion:^{ - [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; - }]; - } - }); -} - -- (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options -{ - NSURLRequest* request = [NSURLRequest requestWithURL:url]; - -#ifdef __CORDOVA_4_0_0 - // the webview engine itself will filter for this according to policy - // in config.xml for cordova-ios-4.0 - [self.webViewEngine loadRequest:request]; -#else - if ([self.commandDelegate URLIsWhitelisted:url]) { - [self.webView loadRequest:request]; - } else { // this assumes the InAppBrowser can be excepted from the white-list - [self openInInAppBrowser:url withOptions:options]; - } -#endif -} - -- (void)openInSystem:(NSURL*)url -{ - if ([[UIApplication sharedApplication] openURL:url] == NO) { - [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] hide:command]; + }else{ + [[CDVUIInAppBrowser getInstance] hide:command]; } } -// This is a helper method for the inject{Script|Style}{Code|File} API calls, which -// provides a consistent method for injecting JavaScript code into the document. -// -// If a wrapper string is supplied, then the source string will be JSON-encoded (adding -// quotes) and wrapped using string formatting. (The wrapper string should have a single -// '%@' marker). -// -// If no wrapper is supplied, then the source string is executed directly. - -- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper -{ - // Ensure an iframe bridge is created to communicate with the CDVInAppBrowserViewController - [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){_cdvIframeBridge=d.getElementById('_cdvIframeBridge');if(!_cdvIframeBridge) {var e = _cdvIframeBridge = d.createElement('iframe');e.id='_cdvIframeBridge'; e.style.display='none';d.body.appendChild(e);}})(document)"]; - - if (jsWrapper != nil) { - NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; - NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - if (sourceArrayString) { - NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)]; - NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString]; - [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:jsToInject]; - } - } else { - [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:source]; - } -} - (void)injectScriptCode:(CDVInvokedUrlCommand*)command { - NSString* jsWrapper = nil; - - if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { - jsWrapper = [NSString stringWithFormat:@"_cdvIframeBridge.src='gap-iab://%@/'+encodeURIComponent(JSON.stringify([eval(%%@)]));", command.callbackId]; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] injectScriptCode:command]; + }else{ + [[CDVUIInAppBrowser getInstance] injectScriptCode:command]; } - [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; } - (void)injectScriptFile:(CDVInvokedUrlCommand*)command { - NSString* jsWrapper; - - if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { - jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; - } else { - jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)"; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] injectScriptFile:command]; + }else{ + [[CDVUIInAppBrowser getInstance] injectScriptFile:command]; } - [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; } - (void)injectStyleCode:(CDVInvokedUrlCommand*)command { - NSString* jsWrapper; - - if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { - jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; - } else { - jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)"; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] injectStyleCode:command]; + }else{ + [[CDVUIInAppBrowser getInstance] injectStyleCode:command]; } - [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; } - (void)injectStyleFile:(CDVInvokedUrlCommand*)command { - NSString* jsWrapper; - - if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { - jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; - } else { - jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)"; + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] injectStyleFile:command]; + }else{ + [[CDVUIInAppBrowser getInstance] injectStyleFile:command]; } - [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; -} - -- (BOOL)isValidCallbackId:(NSString *)callbackId -{ - NSError *err = nil; - // Initialize on first use - if (self.callbackIdPattern == nil) { - self.callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"^InAppBrowser[0-9]{1,10}$" options:0 error:&err]; - if (err != nil) { - // Couldn't initialize Regex; No is safer than Yes. - return NO; - } - } - if ([self.callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) { - return YES; - } - return NO; -} - -/** - * The iframe bridge provided for the InAppBrowser is capable of executing any oustanding callback belonging - * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no - * other code execution is possible. - * - * To trigger the bridge, the iframe (or any other resource) should attempt to load a url of the form: - * - * gap-iab:/// - * - * where is the string id of the callback to trigger (something like "InAppBrowser0123456789") - * - * If present, the path component of the special gap-iab:// url is expected to be a URL-escaped JSON-encoded - * value to pass to the callback. [NSURL path] should take care of the URL-unescaping, and a JSON_EXCEPTION - * is returned if the JSON is invalid. - */ -- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType -{ - NSURL* url = request.URL; - BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; - - // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute, - // and the path, if present, should be a JSON-encoded value to pass to the callback. - if ([[url scheme] isEqualToString:@"gap-iab"]) { - NSString* scriptCallbackId = [url host]; - CDVPluginResult* pluginResult = nil; - - if ([self isValidCallbackId:scriptCallbackId]) { - NSString* scriptResult = [url path]; - NSError* __autoreleasing error = nil; - - // The message should be a JSON-encoded array of the result of the script which executed. - if ((scriptResult != nil) && ([scriptResult length] > 1)) { - scriptResult = [scriptResult substringFromIndex:1]; - NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; - if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; - } else { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; - } - } else { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; - } - [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; - return NO; - } - } - //if is an app store link, let the system handle it, otherwise it fails to load it - else if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { - [theWebView stopLoading]; - [self openInSystem:url]; - return NO; - } - else if ((self.callbackId != nil) && isTopLevelNavigation) { - // Send a loadstart event for each top-level navigation (includes redirects). - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK - messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}]; - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; - } - - return YES; -} - -- (void)webViewDidStartLoad:(UIWebView*)theWebView -{ -} - -- (void)webViewDidFinishLoad:(UIWebView*)theWebView -{ - if (self.callbackId != nil) { - // TODO: It would be more useful to return the URL the page is actually on (e.g. if it's been redirected). - NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK - messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; - } -} - -- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error -{ - if (self.callbackId != nil) { - NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR - messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInteger:error.code], @"message": error.localizedDescription}]; - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; - } -} - -- (void)browserExit -{ - if (self.callbackId != nil) { - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK - messageAsDictionary:@{@"type":@"exit"}]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; - self.callbackId = nil; - } - // Set navigationDelegate to nil to ensure no callbacks are received from it. - self.inAppBrowserViewController.navigationDelegate = nil; - // Don't recycle the ViewController since it may be consuming a lot of memory. - // Also - this is required for the PDF/User-Agent bug work-around. - self.inAppBrowserViewController = nil; - - if (IsAtLeastiOSVersion(@"7.0")) { - if (_previousStatusBarStyle != -1) { - [[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle]; - } - } - - _previousStatusBarStyle = -1; // this value was reset before reapplying it. caused statusbar to stay black on ios7 -} - -@end - -#pragma mark CDVInAppBrowserViewController - -@implementation CDVInAppBrowserViewController - -@synthesize currentURL; - -- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions -{ - self = [super init]; - if (self != nil) { - _userAgent = userAgent; - _prevUserAgent = prevUserAgent; - _browserOptions = browserOptions; -#ifdef __CORDOVA_4_0_0 - _webViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self]; -#else - _webViewDelegate = [[CDVWebViewDelegate alloc] initWithDelegate:self]; -#endif - - [self createViews]; - } - - return self; -} - -// Prevent crashes on closing windows --(void)dealloc { - self.webView.delegate = nil; -} - -- (void)createViews -{ - // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included - - CGRect webViewBounds = self.view.bounds; - BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]; - webViewBounds.size.height -= _browserOptions.location ? FOOTER_HEIGHT : TOOLBAR_HEIGHT; - self.webView = [[UIWebView alloc] initWithFrame:webViewBounds]; - - self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); - - [self.view addSubview:self.webView]; - [self.view sendSubviewToBack:self.webView]; - - self.webView.delegate = _webViewDelegate; - self.webView.backgroundColor = [UIColor whiteColor]; - - self.webView.clearsContextBeforeDrawing = YES; - self.webView.clipsToBounds = YES; - self.webView.contentMode = UIViewContentModeScaleToFill; - self.webView.multipleTouchEnabled = YES; - self.webView.opaque = YES; - self.webView.scalesPageToFit = NO; - self.webView.userInteractionEnabled = YES; - - self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; - self.spinner.alpha = 1.000; - self.spinner.autoresizesSubviews = YES; - self.spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin); - self.spinner.clearsContextBeforeDrawing = NO; - self.spinner.clipsToBounds = NO; - self.spinner.contentMode = UIViewContentModeScaleToFill; - self.spinner.frame = CGRectMake(CGRectGetMidX(self.webView.frame), CGRectGetMidY(self.webView.frame), 20.0, 20.0); - self.spinner.hidden = NO; - self.spinner.hidesWhenStopped = YES; - self.spinner.multipleTouchEnabled = NO; - self.spinner.opaque = NO; - self.spinner.userInteractionEnabled = NO; - [self.spinner stopAnimating]; - - self.closeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; - self.closeButton.enabled = YES; - - UIBarButtonItem* flexibleSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; - - UIBarButtonItem* fixedSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; - fixedSpaceButton.width = 20; - - float toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - TOOLBAR_HEIGHT : 0.0; - CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, TOOLBAR_HEIGHT); - - self.toolbar = [[UIToolbar alloc] initWithFrame:toolbarFrame]; - self.toolbar.alpha = 1.000; - self.toolbar.autoresizesSubviews = YES; - self.toolbar.autoresizingMask = toolbarIsAtBottom ? (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin) : UIViewAutoresizingFlexibleWidth; - self.toolbar.barStyle = UIBarStyleBlackOpaque; - self.toolbar.clearsContextBeforeDrawing = NO; - self.toolbar.clipsToBounds = NO; - self.toolbar.contentMode = UIViewContentModeScaleToFill; - self.toolbar.hidden = NO; - self.toolbar.multipleTouchEnabled = NO; - self.toolbar.opaque = NO; - self.toolbar.userInteractionEnabled = YES; - if (_browserOptions.toolbarcolor != nil) { // Set toolbar color if user sets it in options - self.toolbar.barTintColor = [self colorFromHexString:_browserOptions.toolbarcolor]; - } - if (!_browserOptions.toolbartranslucent) { // Set toolbar translucent to no if user sets it in options - self.toolbar.translucent = NO; - } - - CGFloat labelInset = 5.0; - float locationBarY = toolbarIsAtBottom ? self.view.bounds.size.height - FOOTER_HEIGHT : self.view.bounds.size.height - LOCATIONBAR_HEIGHT; - - self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, locationBarY, self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)]; - self.addressLabel.adjustsFontSizeToFitWidth = NO; - self.addressLabel.alpha = 1.000; - self.addressLabel.autoresizesSubviews = YES; - self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; - self.addressLabel.backgroundColor = [UIColor clearColor]; - self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; - self.addressLabel.clearsContextBeforeDrawing = YES; - self.addressLabel.clipsToBounds = YES; - self.addressLabel.contentMode = UIViewContentModeScaleToFill; - self.addressLabel.enabled = YES; - self.addressLabel.hidden = NO; - self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail; - - if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumScaleFactor:")]) { - [self.addressLabel setValue:@(10.0/[UIFont labelFontSize]) forKey:@"minimumScaleFactor"]; - } else if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumFontSize:")]) { - [self.addressLabel setValue:@(10.0) forKey:@"minimumFontSize"]; - } - - self.addressLabel.multipleTouchEnabled = NO; - self.addressLabel.numberOfLines = 1; - self.addressLabel.opaque = NO; - self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0); - self.addressLabel.text = NSLocalizedString(@"Loading...", nil); - self.addressLabel.textAlignment = NSTextAlignmentLeft; - self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000]; - self.addressLabel.userInteractionEnabled = NO; - - NSString* frontArrowString = NSLocalizedString(@"►", nil); // create arrow from Unicode char - self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; - self.forwardButton.enabled = YES; - self.forwardButton.imageInsets = UIEdgeInsetsZero; - if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options - self.forwardButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; - } - - NSString* backArrowString = NSLocalizedString(@"◄", nil); // create arrow from Unicode char - self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; - self.backButton.enabled = YES; - self.backButton.imageInsets = UIEdgeInsetsZero; - if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options - self.backButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; - } - - // Filter out Navigation Buttons if user requests so - if (_browserOptions.hidenavigationbuttons) { - [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton]]; - } else { - [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; - } - - self.view.backgroundColor = [UIColor grayColor]; - [self.view addSubview:self.toolbar]; - [self.view addSubview:self.addressLabel]; - [self.view addSubview:self.spinner]; -} - -- (void) setWebViewFrame : (CGRect) frame { - NSLog(@"Setting the WebView's frame to %@", NSStringFromCGRect(frame)); - [self.webView setFrame:frame]; -} - -- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString -{ - // the advantage of using UIBarButtonSystemItemDone is the system will localize it for you automatically - // but, if you want to set this yourself, knock yourself out (we can't set the title for a system Done button, so we have to create a new one) - self.closeButton = nil; - // Initialize with title if title is set, otherwise the title will be 'Done' localized - self.closeButton = title != nil ? [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)] : [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; - self.closeButton.enabled = YES; - // If color on closebutton is requested then initialize with that that color, otherwise use initialize with default - self.closeButton.tintColor = colorString != nil ? [self colorFromHexString:colorString] : [UIColor colorWithRed:60.0 / 255.0 green:136.0 / 255.0 blue:230.0 / 255.0 alpha:1]; - - NSMutableArray* items = [self.toolbar.items mutableCopy]; - [items replaceObjectAtIndex:0 withObject:self.closeButton]; - [self.toolbar setItems:items]; -} - -- (void)showLocationBar:(BOOL)show -{ - CGRect locationbarFrame = self.addressLabel.frame; - - BOOL toolbarVisible = !self.toolbar.hidden; - - // prevent double show/hide - if (show == !(self.addressLabel.hidden)) { - return; - } - - if (show) { - self.addressLabel.hidden = NO; - - if (toolbarVisible) { - // toolBar at the bottom, leave as is - // put locationBar on top of the toolBar - - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= FOOTER_HEIGHT; - [self setWebViewFrame:webViewBounds]; - - locationbarFrame.origin.y = webViewBounds.size.height; - self.addressLabel.frame = locationbarFrame; - } else { - // no toolBar, so put locationBar at the bottom - - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= LOCATIONBAR_HEIGHT; - [self setWebViewFrame:webViewBounds]; - - locationbarFrame.origin.y = webViewBounds.size.height; - self.addressLabel.frame = locationbarFrame; - } - } else { - self.addressLabel.hidden = YES; - - if (toolbarVisible) { - // locationBar is on top of toolBar, hide locationBar - - // webView take up whole height less toolBar height - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= TOOLBAR_HEIGHT; - [self setWebViewFrame:webViewBounds]; - } else { - // no toolBar, expand webView to screen dimensions - [self setWebViewFrame:self.view.bounds]; - } - } -} - -- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition -{ - CGRect toolbarFrame = self.toolbar.frame; - CGRect locationbarFrame = self.addressLabel.frame; - - BOOL locationbarVisible = !self.addressLabel.hidden; - - // prevent double show/hide - if (show == !(self.toolbar.hidden)) { - return; - } - - if (show) { - self.toolbar.hidden = NO; - CGRect webViewBounds = self.view.bounds; - - if (locationbarVisible) { - // locationBar at the bottom, move locationBar up - // put toolBar at the bottom - webViewBounds.size.height -= FOOTER_HEIGHT; - locationbarFrame.origin.y = webViewBounds.size.height; - self.addressLabel.frame = locationbarFrame; - self.toolbar.frame = toolbarFrame; - } else { - // no locationBar, so put toolBar at the bottom - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= TOOLBAR_HEIGHT; - self.toolbar.frame = toolbarFrame; - } - - if ([toolbarPosition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { - toolbarFrame.origin.y = 0; - webViewBounds.origin.y += toolbarFrame.size.height; - [self setWebViewFrame:webViewBounds]; - } else { - toolbarFrame.origin.y = (webViewBounds.size.height + LOCATIONBAR_HEIGHT); - } - [self setWebViewFrame:webViewBounds]; - - } else { - self.toolbar.hidden = YES; - - if (locationbarVisible) { - // locationBar is on top of toolBar, hide toolBar - // put locationBar at the bottom - - // webView take up whole height less locationBar height - CGRect webViewBounds = self.view.bounds; - webViewBounds.size.height -= LOCATIONBAR_HEIGHT; - [self setWebViewFrame:webViewBounds]; - - // move locationBar down - locationbarFrame.origin.y = webViewBounds.size.height; - self.addressLabel.frame = locationbarFrame; - } else { - // no locationBar, expand webView to screen dimensions - [self setWebViewFrame:self.view.bounds]; - } - } -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; -} - -- (void)viewDidUnload -{ - [self.webView loadHTMLString:nil baseURL:nil]; - [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; - [super viewDidUnload]; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return UIStatusBarStyleDefault; -} - -- (BOOL)prefersStatusBarHidden { - return NO; -} - -- (void)close -{ - [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; - self.currentURL = nil; - - if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { - [self.navigationDelegate browserExit]; - } - - __weak UIViewController* weakSelf = self; - - // Run later to avoid the "took a long time" log message. - dispatch_async(dispatch_get_main_queue(), ^{ - if ([weakSelf respondsToSelector:@selector(presentingViewController)]) { - [[weakSelf presentingViewController] dismissViewControllerAnimated:YES completion:^{ - [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; - }]; - } else { - [[weakSelf parentViewController] dismissViewControllerAnimated:YES completion:^{ - [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; - }]; - } - }); -} - -- (void)navigateTo:(NSURL*)url -{ - NSURLRequest* request = [NSURLRequest requestWithURL:url]; - - if (_userAgentLockToken != 0) { - [self.webView loadRequest:request]; - } else { - __weak CDVInAppBrowserViewController* weakSelf = self; - [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { - _userAgentLockToken = lockToken; - [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken]; - [weakSelf.webView loadRequest:request]; - }]; - } -} - -- (void)goBack:(id)sender -{ - [self.webView goBack]; -} - -- (void)goForward:(id)sender -{ - [self.webView goForward]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - if (IsAtLeastiOSVersion(@"7.0")) { - [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; - } - [self rePositionViews]; - - [super viewWillAppear:animated]; -} - -// -// On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account. -// The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't -// change that value. -// -- (float) getStatusBarOffset { - CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; - float statusBarOffset = IsAtLeastiOSVersion(@"7.0") ? MIN(statusBarFrame.size.width, statusBarFrame.size.height) : 0.0; - return statusBarOffset; -} - -- (void) rePositionViews { - if ([_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { - [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, TOOLBAR_HEIGHT, self.webView.frame.size.width, self.webView.frame.size.height)]; - [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; - } -} - -// Helper function to convert hex color string to UIColor -// Assumes input like "#00FF00" (#RRGGBB). -// Taken from https://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string -- (UIColor *)colorFromHexString:(NSString *)hexString { - unsigned rgbValue = 0; - NSScanner *scanner = [NSScanner scannerWithString:hexString]; - [scanner setScanLocation:1]; // bypass '#' character - [scanner scanHexInt:&rgbValue]; - return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; -} - -#pragma mark UIWebViewDelegate - -- (void)webViewDidStartLoad:(UIWebView*)theWebView -{ - // loading url, start spinner, update back/forward - - self.addressLabel.text = NSLocalizedString(@"Loading...", nil); - self.backButton.enabled = theWebView.canGoBack; - self.forwardButton.enabled = theWebView.canGoForward; - - NSLog(_browserOptions.hidespinner ? @"Yes" : @"No"); - if(!_browserOptions.hidespinner) { - [self.spinner startAnimating]; - } - - return [self.navigationDelegate webViewDidStartLoad:theWebView]; -} - -- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType -{ - BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; - - if (isTopLevelNavigation) { - self.currentURL = request.URL; - } - return [self.navigationDelegate webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType]; -} - -- (void)webViewDidFinishLoad:(UIWebView*)theWebView -{ - // update url, stop spinner, update back/forward - - self.addressLabel.text = [self.currentURL absoluteString]; - self.backButton.enabled = theWebView.canGoBack; - self.forwardButton.enabled = theWebView.canGoForward; - - [self.spinner stopAnimating]; - - // Work around a bug where the first time a PDF is opened, all UIWebViews - // reload their User-Agent from NSUserDefaults. - // This work-around makes the following assumptions: - // 1. The app has only a single Cordova Webview. If not, then the app should - // take it upon themselves to load a PDF in the background as a part of - // their start-up flow. - // 2. That the PDF does not require any additional network requests. We change - // the user-agent here back to that of the CDVViewController, so requests - // from it must pass through its white-list. This *does* break PDFs that - // contain links to other remote PDF/websites. - // More info at https://issues.apache.org/jira/browse/CB-2225 - BOOL isPDF = [@"true" isEqualToString :[theWebView stringByEvaluatingJavaScriptFromString:@"document.body==null"]]; - if (isPDF) { - [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; - } - - [self.navigationDelegate webViewDidFinishLoad:theWebView]; -} - -- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error -{ - // log fail message, stop spinner, update back/forward - NSLog(@"webView:didFailLoadWithError - %ld: %@", (long)error.code, [error localizedDescription]); - - self.backButton.enabled = theWebView.canGoBack; - self.forwardButton.enabled = theWebView.canGoForward; - [self.spinner stopAnimating]; - - self.addressLabel.text = NSLocalizedString(@"Load Error", nil); - - [self.navigationDelegate webView:theWebView didFailLoadWithError:error]; -} - -#pragma mark CDVScreenOrientationDelegate - -- (BOOL)shouldAutorotate -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { - return [self.orientationDelegate shouldAutorotate]; - } - return YES; -} - -- (NSUInteger)supportedInterfaceOrientations -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { - return [self.orientationDelegate supportedInterfaceOrientations]; - } - - return 1 << UIInterfaceOrientationPortrait; -} - -- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { - return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; - } - - return YES; -} - -@end - -@implementation CDVInAppBrowserOptions - -- (id)init -{ - if (self = [super init]) { - // default values - self.location = YES; - self.toolbar = YES; - self.closebuttoncaption = nil; - self.toolbarposition = kInAppBrowserToolbarBarPositionBottom; - self.clearcache = NO; - self.clearsessioncache = NO; - self.hidespinner = NO; - - self.enableviewportscale = NO; - self.mediaplaybackrequiresuseraction = NO; - self.allowinlinemediaplayback = NO; - self.keyboarddisplayrequiresuseraction = YES; - self.suppressesincrementalrendering = NO; - self.hidden = NO; - self.disallowoverscroll = NO; - self.hidenavigationbuttons = NO; - self.closebuttoncolor = nil; - self.toolbarcolor = nil; - self.toolbartranslucent = YES; - } - - return self; -} - -+ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options -{ - CDVInAppBrowserOptions* obj = [[CDVInAppBrowserOptions alloc] init]; - - // NOTE: this parsing does not handle quotes within values - NSArray* pairs = [options componentsSeparatedByString:@","]; - - // parse keys and values, set the properties - for (NSString* pair in pairs) { - NSArray* keyvalue = [pair componentsSeparatedByString:@"="]; - - if ([keyvalue count] == 2) { - NSString* key = [[keyvalue objectAtIndex:0] lowercaseString]; - NSString* value = [keyvalue objectAtIndex:1]; - NSString* value_lc = [value lowercaseString]; - - BOOL isBoolean = [value_lc isEqualToString:@"yes"] || [value_lc isEqualToString:@"no"]; - NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; - [numberFormatter setAllowsFloats:YES]; - BOOL isNumber = [numberFormatter numberFromString:value_lc] != nil; - - // set the property according to the key name - if ([obj respondsToSelector:NSSelectorFromString(key)]) { - if (isNumber) { - [obj setValue:[numberFormatter numberFromString:value_lc] forKey:key]; - } else if (isBoolean) { - [obj setValue:[NSNumber numberWithBool:[value_lc isEqualToString:@"yes"]] forKey:key]; - } else { - [obj setValue:value forKey:key]; - } - } - } - } - - return obj; -} - -@end - -@implementation CDVInAppBrowserNavigationController : UINavigationController - -- (void) dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion { - if ( self.presentedViewController) { - [super dismissViewControllerAnimated:flag completion:completion]; - } -} - -- (void) viewDidLoad { - - CGRect statusBarFrame = [self invertFrameIfNeeded:[UIApplication sharedApplication].statusBarFrame]; - statusBarFrame.size.height = STATUSBAR_HEIGHT; - // simplified from: http://stackoverflow.com/a/25669695/219684 - - UIToolbar* bgToolbar = [[UIToolbar alloc] initWithFrame:statusBarFrame]; - bgToolbar.barStyle = UIBarStyleDefault; - [bgToolbar setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; - [self.view addSubview:bgToolbar]; - - [super viewDidLoad]; -} - -- (CGRect) invertFrameIfNeeded:(CGRect)rect { - // We need to invert since on iOS 7 frames are always in Portrait context - if (!IsAtLeastiOSVersion(@"8.0")) { - if (UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])) { - CGFloat temp = rect.size.width; - rect.size.width = rect.size.height; - rect.size.height = temp; - } - rect.origin = CGPointZero; - } - return rect; -} - -#pragma mark CDVScreenOrientationDelegate - -- (BOOL)shouldAutorotate -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { - return [self.orientationDelegate shouldAutorotate]; - } - return YES; -} - -- (NSUInteger)supportedInterfaceOrientations -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { - return [self.orientationDelegate supportedInterfaceOrientations]; - } - - return 1 << UIInterfaceOrientationPortrait; -} - -- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { - return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; - } - - return YES; } diff --git a/src/ios/CDVInAppBrowserNavigationController.h b/src/ios/CDVInAppBrowserNavigationController.h new file mode 100644 index 0000000..bd186a2 --- /dev/null +++ b/src/ios/CDVInAppBrowserNavigationController.h @@ -0,0 +1,27 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + + +@interface CDVInAppBrowserNavigationController : UINavigationController + +@property (nonatomic, weak) id orientationDelegate; + +@end diff --git a/src/ios/CDVInAppBrowserNavigationController.m b/src/ios/CDVInAppBrowserNavigationController.m new file mode 100644 index 0000000..c77a3e6 --- /dev/null +++ b/src/ios/CDVInAppBrowserNavigationController.m @@ -0,0 +1,85 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVInAppBrowserNavigationController.h" + +#define STATUSBAR_HEIGHT 20.0 + +@implementation CDVInAppBrowserNavigationController : UINavigationController + +- (void) dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion { + if ( self.presentedViewController) { + [super dismissViewControllerAnimated:flag completion:completion]; + } +} + +- (void) viewDidLoad { + + CGRect statusBarFrame = [self invertFrameIfNeeded:[UIApplication sharedApplication].statusBarFrame]; + statusBarFrame.size.height = STATUSBAR_HEIGHT; + // simplified from: http://stackoverflow.com/a/25669695/219684 + + UIToolbar* bgToolbar = [[UIToolbar alloc] initWithFrame:statusBarFrame]; + bgToolbar.barStyle = UIBarStyleDefault; + [bgToolbar setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; + [self.view addSubview:bgToolbar]; + + [super viewDidLoad]; +} + +- (CGRect) invertFrameIfNeeded:(CGRect)rect { + if (UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])) { + CGFloat temp = rect.size.width; + rect.size.width = rect.size.height; + rect.size.height = temp; + } + rect.origin = CGPointZero; + return rect; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { + return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + + return YES; +} + + +@end diff --git a/src/ios/CDVInAppBrowserOptions.h b/src/ios/CDVInAppBrowserOptions.h new file mode 100644 index 0000000..93e48d4 --- /dev/null +++ b/src/ios/CDVInAppBrowserOptions.h @@ -0,0 +1,51 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + + +@interface CDVInAppBrowserOptions : NSObject {} + +@property (nonatomic, assign) BOOL usewkwebview; +@property (nonatomic, assign) BOOL location; +@property (nonatomic, assign) BOOL toolbar; +@property (nonatomic, copy) NSString* closebuttoncaption; +@property (nonatomic, copy) NSString* closebuttoncolor; +@property (nonatomic, copy) NSString* toolbarposition; +@property (nonatomic, copy) NSString* toolbarcolor; +@property (nonatomic, assign) BOOL toolbartranslucent; +@property (nonatomic, assign) BOOL hidenavigationbuttons; +@property (nonatomic, copy) NSString* navigationbuttoncolor; +@property (nonatomic, assign) BOOL cleardata; +@property (nonatomic, assign) BOOL clearcache; +@property (nonatomic, assign) BOOL clearsessioncache; +@property (nonatomic, assign) BOOL hidespinner; + +@property (nonatomic, copy) NSString* presentationstyle; +@property (nonatomic, copy) NSString* transitionstyle; + +@property (nonatomic, assign) BOOL enableviewportscale; +@property (nonatomic, assign) BOOL mediaplaybackrequiresuseraction; +@property (nonatomic, assign) BOOL allowinlinemediaplayback; +@property (nonatomic, assign) BOOL keyboarddisplayrequiresuseraction; +@property (nonatomic, assign) BOOL suppressesincrementalrendering; +@property (nonatomic, assign) BOOL hidden; +@property (nonatomic, assign) BOOL disallowoverscroll; + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; + +@end diff --git a/src/ios/CDVInAppBrowserOptions.m b/src/ios/CDVInAppBrowserOptions.m new file mode 100644 index 0000000..e1cc7d3 --- /dev/null +++ b/src/ios/CDVInAppBrowserOptions.m @@ -0,0 +1,91 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVInAppBrowserOptions.h" + +@implementation CDVInAppBrowserOptions + +- (id)init +{ + if (self = [super init]) { + // default values + self.usewkwebview = NO; + self.location = YES; + self.toolbar = YES; + self.closebuttoncaption = nil; + self.toolbarposition = @"bottom"; + self.cleardata = NO; + self.clearcache = NO; + self.clearsessioncache = NO; + self.hidespinner = NO; + + self.enableviewportscale = NO; + self.mediaplaybackrequiresuseraction = NO; + self.allowinlinemediaplayback = NO; + self.keyboarddisplayrequiresuseraction = YES; + self.suppressesincrementalrendering = NO; + self.hidden = NO; + self.disallowoverscroll = NO; + self.hidenavigationbuttons = NO; + self.closebuttoncolor = nil; + self.toolbarcolor = nil; + self.toolbartranslucent = YES; + } + + return self; +} + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options +{ + CDVInAppBrowserOptions* obj = [[CDVInAppBrowserOptions alloc] init]; + + // NOTE: this parsing does not handle quotes within values + NSArray* pairs = [options componentsSeparatedByString:@","]; + + // parse keys and values, set the properties + for (NSString* pair in pairs) { + NSArray* keyvalue = [pair componentsSeparatedByString:@"="]; + + if ([keyvalue count] == 2) { + NSString* key = [[keyvalue objectAtIndex:0] lowercaseString]; + NSString* value = [keyvalue objectAtIndex:1]; + NSString* value_lc = [value lowercaseString]; + + BOOL isBoolean = [value_lc isEqualToString:@"yes"] || [value_lc isEqualToString:@"no"]; + NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setAllowsFloats:YES]; + BOOL isNumber = [numberFormatter numberFromString:value_lc] != nil; + + // set the property according to the key name + if ([obj respondsToSelector:NSSelectorFromString(key)]) { + if (isNumber) { + [obj setValue:[numberFormatter numberFromString:value_lc] forKey:key]; + } else if (isBoolean) { + [obj setValue:[NSNumber numberWithBool:[value_lc isEqualToString:@"yes"]] forKey:key]; + } else { + [obj setValue:value forKey:key]; + } + } + } + } + + return obj; +} + +@end diff --git a/src/ios/CDVUIInAppBrowser.h b/src/ios/CDVUIInAppBrowser.h new file mode 100644 index 0000000..64afe3b --- /dev/null +++ b/src/ios/CDVUIInAppBrowser.h @@ -0,0 +1,86 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import "CDVInAppBrowserOptions.h" +#import "CDVInAppBrowserNavigationController.h" + +#ifdef __CORDOVA_4_0_0 + #import +#else + #import +#endif + +@class CDVUIInAppBrowserViewController; + +@interface CDVUIInAppBrowser : CDVPlugin { + UIWindow * tmpWindow; +} + +@property (nonatomic, retain) CDVUIInAppBrowserViewController* inAppBrowserViewController; +@property (nonatomic, copy) NSString* callbackId; +@property (nonatomic, copy) NSRegularExpression *callbackIdPattern; + ++ (id) getInstance; +- (void)open:(CDVInvokedUrlCommand*)command; +- (void)close:(CDVInvokedUrlCommand*)command; +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command; +- (void)show:(CDVInvokedUrlCommand*)command; +- (void)hide:(CDVInvokedUrlCommand*)command; + +@end + +@interface CDVUIInAppBrowserViewController : UIViewController { + @private + NSString* _userAgent; + NSString* _prevUserAgent; + NSInteger _userAgentLockToken; + CDVInAppBrowserOptions *_browserOptions; + +#ifdef __CORDOVA_4_0_0 + CDVUIWebViewDelegate* _webViewDelegate; +#else + CDVWebViewDelegate* _webViewDelegate; +#endif + +} + +@property (nonatomic, strong) IBOutlet UIWebView* webView; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* closeButton; +@property (nonatomic, strong) IBOutlet UILabel* addressLabel; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* backButton; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* forwardButton; +@property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; +@property (nonatomic, strong) IBOutlet UIToolbar* toolbar; + +@property (nonatomic, weak) id orientationDelegate; +@property (nonatomic, weak) CDVUIInAppBrowser* navigationDelegate; +@property (nonatomic) NSURL* currentURL; + +- (void)close; +- (void)navigateTo:(NSURL*)url; +- (void)showLocationBar:(BOOL)show; +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition; +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions; + +@end diff --git a/src/ios/CDVUIInAppBrowser.m b/src/ios/CDVUIInAppBrowser.m new file mode 100644 index 0000000..2ae8611 --- /dev/null +++ b/src/ios/CDVUIInAppBrowser.m @@ -0,0 +1,1029 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVUIInAppBrowser.h" +#import +#import + +#define kInAppBrowserTargetSelf @"_self" +#define kInAppBrowserTargetSystem @"_system" +#define kInAppBrowserTargetBlank @"_blank" + +#define kInAppBrowserToolbarBarPositionBottom @"bottom" +#define kInAppBrowserToolbarBarPositionTop @"top" + +#define TOOLBAR_HEIGHT 44.0 +#define STATUSBAR_HEIGHT 20.0 +#define LOCATIONBAR_HEIGHT 21.0 +#define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) + +#pragma mark CDVUIInAppBrowser + +@interface CDVUIInAppBrowser () { + NSInteger _previousStatusBarStyle; +} +@end + +@implementation CDVUIInAppBrowser + +static CDVUIInAppBrowser* instance = nil; + ++ (id) getInstance{ + return instance; +} + +- (void)pluginInitialize +{ + instance = self; + _previousStatusBarStyle = -1; + _callbackIdPattern = nil; +} + +- (id)settingForKey:(NSString*)key +{ + return [self.commandDelegate.settings objectForKey:[key lowercaseString]]; +} + +- (void)onReset +{ + [self close:nil]; +} + +- (void)close:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"IAB.close() called but it was already closed."); + return; + } + // Things are cleaned up in browserExit. + [self.inAppBrowserViewController close]; +} + +- (BOOL) isSystemUrl:(NSURL*)url +{ + if ([[url host] isEqualToString:@"itunes.apple.com"]) { + return YES; + } + + return NO; +} + +- (void)open:(CDVInvokedUrlCommand*)command +{ + CDVPluginResult* pluginResult; + + NSString* url = [command argumentAtIndex:0]; + NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf]; + NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; + + self.callbackId = command.callbackId; + + if (url != nil) { +#ifdef __CORDOVA_4_0_0 + NSURL* baseUrl = [self.webViewEngine URL]; +#else + NSURL* baseUrl = [self.webView.request URL]; +#endif + NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; + + if ([self isSystemUrl:absoluteUrl]) { + target = kInAppBrowserTargetSystem; + } + + if ([target isEqualToString:kInAppBrowserTargetSelf]) { + [self openInCordovaWebView:absoluteUrl withOptions:options]; + } else if ([target isEqualToString:kInAppBrowserTargetSystem]) { + [self openInSystem:absoluteUrl]; + } else { // _blank or anything else + [self openInInAppBrowser:absoluteUrl withOptions:options]; + } + + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"]; + } + + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options +{ + CDVInAppBrowserOptions* browserOptions = [CDVInAppBrowserOptions parseOptions:options]; + + if (browserOptions.clearcache) { + NSHTTPCookie *cookie; + NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + for (cookie in [storage cookies]) + { + if (![cookie.domain isEqual: @".^filecookies^"]) { + [storage deleteCookie:cookie]; + } + } + } + + if (browserOptions.clearsessioncache) { + NSHTTPCookie *cookie; + NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + for (cookie in [storage cookies]) + { + if (![cookie.domain isEqual: @".^filecookies^"] && cookie.isSessionOnly) { + [storage deleteCookie:cookie]; + } + } + } + + if (self.inAppBrowserViewController == nil) { + NSString* userAgent = [CDVUserAgentUtil originalUserAgent]; + NSString* overrideUserAgent = [self settingForKey:@"OverrideUserAgent"]; + NSString* appendUserAgent = [self settingForKey:@"AppendUserAgent"]; + if(overrideUserAgent){ + userAgent = overrideUserAgent; + } + if(appendUserAgent){ + userAgent = [userAgent stringByAppendingString: appendUserAgent]; + } + self.inAppBrowserViewController = [[CDVUIInAppBrowserViewController alloc] initWithUserAgent:userAgent prevUserAgent:[self.commandDelegate userAgent] browserOptions: browserOptions]; + self.inAppBrowserViewController.navigationDelegate = self; + + if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { + self.inAppBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; + } + } + + [self.inAppBrowserViewController showLocationBar:browserOptions.location]; + [self.inAppBrowserViewController showToolBar:browserOptions.toolbar :browserOptions.toolbarposition]; + if (browserOptions.closebuttoncaption != nil || browserOptions.closebuttoncolor != nil) { + [self.inAppBrowserViewController setCloseButtonTitle:browserOptions.closebuttoncaption :browserOptions.closebuttoncolor]; + } + // Set Presentation Style + UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default + if (browserOptions.presentationstyle != nil) { + if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"pagesheet"]) { + presentationStyle = UIModalPresentationPageSheet; + } else if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"formsheet"]) { + presentationStyle = UIModalPresentationFormSheet; + } + } + self.inAppBrowserViewController.modalPresentationStyle = presentationStyle; + + // Set Transition Style + UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default + if (browserOptions.transitionstyle != nil) { + if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"fliphorizontal"]) { + transitionStyle = UIModalTransitionStyleFlipHorizontal; + } else if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"crossdissolve"]) { + transitionStyle = UIModalTransitionStyleCrossDissolve; + } + } + self.inAppBrowserViewController.modalTransitionStyle = transitionStyle; + + // prevent webView from bouncing + if (browserOptions.disallowoverscroll) { + if ([self.inAppBrowserViewController.webView respondsToSelector:@selector(scrollView)]) { + ((UIScrollView*)[self.inAppBrowserViewController.webView scrollView]).bounces = NO; + } else { + for (id subview in self.inAppBrowserViewController.webView.subviews) { + if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { + ((UIScrollView*)subview).bounces = NO; + } + } + } + } + + // UIWebView options + self.inAppBrowserViewController.webView.scalesPageToFit = browserOptions.enableviewportscale; + self.inAppBrowserViewController.webView.mediaPlaybackRequiresUserAction = browserOptions.mediaplaybackrequiresuseraction; + self.inAppBrowserViewController.webView.allowsInlineMediaPlayback = browserOptions.allowinlinemediaplayback; + if (IsAtLeastiOSVersion(@"6.0")) { + self.inAppBrowserViewController.webView.keyboardDisplayRequiresUserAction = browserOptions.keyboarddisplayrequiresuseraction; + self.inAppBrowserViewController.webView.suppressesIncrementalRendering = browserOptions.suppressesincrementalrendering; + } + + [self.inAppBrowserViewController navigateTo:url]; + if (!browserOptions.hidden) { + [self show:nil]; + } +} + +- (void)show:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to show IAB after it was closed."); + return; + } + if (_previousStatusBarStyle != -1) { + NSLog(@"Tried to show IAB while already shown"); + return; + } + + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + + __block CDVInAppBrowserNavigationController* nav = [[CDVInAppBrowserNavigationController alloc] + initWithRootViewController:self.inAppBrowserViewController]; + nav.orientationDelegate = self.inAppBrowserViewController; + nav.navigationBarHidden = YES; + nav.modalPresentationStyle = self.inAppBrowserViewController.modalPresentationStyle; + + __weak CDVUIInAppBrowser* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.inAppBrowserViewController != nil) { + if (!tmpWindow) { + CGRect frame = [[UIScreen mainScreen] bounds]; + tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + } + UIViewController *tmpController = [[UIViewController alloc] init]; + [tmpWindow setRootViewController:tmpController]; + double baseWindowLevel = [UIApplication sharedApplication].keyWindow.windowLevel; + [tmpWindow setWindowLevel:baseWindowLevel+1]; + + [tmpWindow makeKeyAndVisible]; + [tmpController presentViewController:nav animated:YES completion:nil]; + } + }); +} + +- (void)hide:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to hide IAB after it was closed."); + return; + + + } + if (_previousStatusBarStyle == -1) { + NSLog(@"Tried to hide IAB while already hidden"); + return; + } + + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.inAppBrowserViewController != nil) { + _previousStatusBarStyle = -1; + [self.inAppBrowserViewController.presentingViewController dismissViewControllerAnimated:YES completion:^{ + [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; + }]; + } + }); +} + +- (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + +#ifdef __CORDOVA_4_0_0 + // the webview engine itself will filter for this according to policy + // in config.xml for cordova-ios-4.0 + [self.webViewEngine loadRequest:request]; +#else + if ([self.commandDelegate URLIsWhitelisted:url]) { + [self.webView loadRequest:request]; + } else { // this assumes the InAppBrowser can be excepted from the white-list + [self openInInAppBrowser:url withOptions:options]; + } +#endif +} + +- (void)openInSystem:(NSURL*)url +{ + if ([[UIApplication sharedApplication] openURL:url] == NO) { + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + [[UIApplication sharedApplication] openURL:url]; + } +} + +// This is a helper method for the inject{Script|Style}{Code|File} API calls, which +// provides a consistent method for injecting JavaScript code into the document. +// +// If a wrapper string is supplied, then the source string will be JSON-encoded (adding +// quotes) and wrapped using string formatting. (The wrapper string should have a single +// '%@' marker). +// +// If no wrapper is supplied, then the source string is executed directly. + +- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper +{ + // Ensure an iframe bridge is created to communicate with the CDVUIInAppBrowserViewController + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){_cdvIframeBridge=d.getElementById('_cdvIframeBridge');if(!_cdvIframeBridge) {var e = _cdvIframeBridge = d.createElement('iframe');e.id='_cdvIframeBridge'; e.style.display='none';d.body.appendChild(e);}})(document)"]; + + if (jsWrapper != nil) { + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; + NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + if (sourceArrayString) { + NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)]; + NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString]; + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:jsToInject]; + } + } else { + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:source]; + } +} + +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper = nil; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"_cdvIframeBridge.src='gap-iab://%@/'+encodeURIComponent(JSON.stringify([eval(%%@)]));", command.callbackId]; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectScriptFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (BOOL)isValidCallbackId:(NSString *)callbackId +{ + NSError *err = nil; + // Initialize on first use + if (self.callbackIdPattern == nil) { + self.callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"^InAppBrowser[0-9]{1,10}$" options:0 error:&err]; + if (err != nil) { + // Couldn't initialize Regex; No is safer than Yes. + return NO; + } + } + if ([self.callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) { + return YES; + } + return NO; +} + +/** + * The iframe bridge provided for the InAppBrowser is capable of executing any oustanding callback belonging + * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no + * other code execution is possible. + * + * To trigger the bridge, the iframe (or any other resource) should attempt to load a url of the form: + * + * gap-iab:/// + * + * where is the string id of the callback to trigger (something like "InAppBrowser0123456789") + * + * If present, the path component of the special gap-iab:// url is expected to be a URL-escaped JSON-encoded + * value to pass to the callback. [NSURL path] should take care of the URL-unescaping, and a JSON_EXCEPTION + * is returned if the JSON is invalid. + */ +- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + NSURL* url = request.URL; + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute, + // and the path, if present, should be a JSON-encoded value to pass to the callback. + if ([[url scheme] isEqualToString:@"gap-iab"]) { + NSString* scriptCallbackId = [url host]; + CDVPluginResult* pluginResult = nil; + + if ([self isValidCallbackId:scriptCallbackId]) { + NSString* scriptResult = [url path]; + NSError* __autoreleasing error = nil; + + // The message should be a JSON-encoded array of the result of the script which executed. + if ((scriptResult != nil) && ([scriptResult length] > 1)) { + scriptResult = [scriptResult substringFromIndex:1]; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; + } + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; + } + [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; + return NO; + } + } + //if is an app store link, let the system handle it, otherwise it fails to load it + else if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { + [theWebView stopLoading]; + [self openInSystem:url]; + return NO; + } + else if ((self.callbackId != nil) && isTopLevelNavigation) { + // Send a loadstart event for each top-level navigation (includes redirects). + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + + return YES; +} + +- (void)webViewDidStartLoad:(UIWebView*)theWebView +{ +} + +- (void)webViewDidFinishLoad:(UIWebView*)theWebView +{ + if (self.callbackId != nil) { + // TODO: It would be more useful to return the URL the page is actually on (e.g. if it's been redirected). + NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +{ + if (self.callbackId != nil) { + NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInteger:error.code], @"message": error.localizedDescription}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)browserExit +{ + if (self.callbackId != nil) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"exit"}]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + self.callbackId = nil; + } + // Set navigationDelegate to nil to ensure no callbacks are received from it. + self.inAppBrowserViewController.navigationDelegate = nil; + // Don't recycle the ViewController since it may be consuming a lot of memory. + // Also - this is required for the PDF/User-Agent bug work-around. + self.inAppBrowserViewController = nil; + + if (IsAtLeastiOSVersion(@"7.0")) { + if (_previousStatusBarStyle != -1) { + [[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle]; + } + } + + _previousStatusBarStyle = -1; // this value was reset before reapplying it. caused statusbar to stay black on ios7 +} + +@end + +#pragma mark CDVUIInAppBrowserViewController + +@implementation CDVUIInAppBrowserViewController + +@synthesize currentURL; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions +{ + self = [super init]; + if (self != nil) { + _userAgent = userAgent; + _prevUserAgent = prevUserAgent; + _browserOptions = browserOptions; +#ifdef __CORDOVA_4_0_0 + _webViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self]; +#else + _webViewDelegate = [[CDVWebViewDelegate alloc] initWithDelegate:self]; +#endif + + [self createViews]; + } + + return self; +} + +// Prevent crashes on closing windows +-(void)dealloc { + self.webView.delegate = nil; +} + +- (void)createViews +{ + // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included + + CGRect webViewBounds = self.view.bounds; + BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]; + webViewBounds.size.height -= _browserOptions.location ? FOOTER_HEIGHT : TOOLBAR_HEIGHT; + self.webView = [[UIWebView alloc] initWithFrame:webViewBounds]; + + self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); + + [self.view addSubview:self.webView]; + [self.view sendSubviewToBack:self.webView]; + + self.webView.delegate = _webViewDelegate; + self.webView.backgroundColor = [UIColor whiteColor]; + + self.webView.clearsContextBeforeDrawing = YES; + self.webView.clipsToBounds = YES; + self.webView.contentMode = UIViewContentModeScaleToFill; + self.webView.multipleTouchEnabled = YES; + self.webView.opaque = YES; + self.webView.scalesPageToFit = NO; + self.webView.userInteractionEnabled = YES; + + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + self.spinner.alpha = 1.000; + self.spinner.autoresizesSubviews = YES; + self.spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin); + self.spinner.clearsContextBeforeDrawing = NO; + self.spinner.clipsToBounds = NO; + self.spinner.contentMode = UIViewContentModeScaleToFill; + self.spinner.frame = CGRectMake(CGRectGetMidX(self.webView.frame), CGRectGetMidY(self.webView.frame), 20.0, 20.0); + self.spinner.hidden = NO; + self.spinner.hidesWhenStopped = YES; + self.spinner.multipleTouchEnabled = NO; + self.spinner.opaque = NO; + self.spinner.userInteractionEnabled = NO; + [self.spinner stopAnimating]; + + self.closeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + + UIBarButtonItem* flexibleSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + + UIBarButtonItem* fixedSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + fixedSpaceButton.width = 20; + + float toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - TOOLBAR_HEIGHT : 0.0; + CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, TOOLBAR_HEIGHT); + + self.toolbar = [[UIToolbar alloc] initWithFrame:toolbarFrame]; + self.toolbar.alpha = 1.000; + self.toolbar.autoresizesSubviews = YES; + self.toolbar.autoresizingMask = toolbarIsAtBottom ? (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin) : UIViewAutoresizingFlexibleWidth; + self.toolbar.barStyle = UIBarStyleBlackOpaque; + self.toolbar.clearsContextBeforeDrawing = NO; + self.toolbar.clipsToBounds = NO; + self.toolbar.contentMode = UIViewContentModeScaleToFill; + self.toolbar.hidden = NO; + self.toolbar.multipleTouchEnabled = NO; + self.toolbar.opaque = NO; + self.toolbar.userInteractionEnabled = YES; + if (_browserOptions.toolbarcolor != nil) { // Set toolbar color if user sets it in options + self.toolbar.barTintColor = [self colorFromHexString:_browserOptions.toolbarcolor]; + } + if (!_browserOptions.toolbartranslucent) { // Set toolbar translucent to no if user sets it in options + self.toolbar.translucent = NO; + } + + CGFloat labelInset = 5.0; + float locationBarY = toolbarIsAtBottom ? self.view.bounds.size.height - FOOTER_HEIGHT : self.view.bounds.size.height - LOCATIONBAR_HEIGHT; + + self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, locationBarY, self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)]; + self.addressLabel.adjustsFontSizeToFitWidth = NO; + self.addressLabel.alpha = 1.000; + self.addressLabel.autoresizesSubviews = YES; + self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; + self.addressLabel.backgroundColor = [UIColor clearColor]; + self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; + self.addressLabel.clearsContextBeforeDrawing = YES; + self.addressLabel.clipsToBounds = YES; + self.addressLabel.contentMode = UIViewContentModeScaleToFill; + self.addressLabel.enabled = YES; + self.addressLabel.hidden = NO; + self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail; + + if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumScaleFactor:")]) { + [self.addressLabel setValue:@(10.0/[UIFont labelFontSize]) forKey:@"minimumScaleFactor"]; + } else if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumFontSize:")]) { + [self.addressLabel setValue:@(10.0) forKey:@"minimumFontSize"]; + } + + self.addressLabel.multipleTouchEnabled = NO; + self.addressLabel.numberOfLines = 1; + self.addressLabel.opaque = NO; + self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0); + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.addressLabel.textAlignment = NSTextAlignmentLeft; + self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000]; + self.addressLabel.userInteractionEnabled = NO; + + NSString* frontArrowString = NSLocalizedString(@"►", nil); // create arrow from Unicode char + self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; + self.forwardButton.enabled = YES; + self.forwardButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.forwardButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + NSString* backArrowString = NSLocalizedString(@"◄", nil); // create arrow from Unicode char + self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; + self.backButton.enabled = YES; + self.backButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.backButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + // Filter out Navigation Buttons if user requests so + if (_browserOptions.hidenavigationbuttons) { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton]]; + } else { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; + } + + self.view.backgroundColor = [UIColor grayColor]; + [self.view addSubview:self.toolbar]; + [self.view addSubview:self.addressLabel]; + [self.view addSubview:self.spinner]; +} + +- (void) setWebViewFrame : (CGRect) frame { + NSLog(@"Setting the WebView's frame to %@", NSStringFromCGRect(frame)); + [self.webView setFrame:frame]; +} + +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString +{ + // the advantage of using UIBarButtonSystemItemDone is the system will localize it for you automatically + // but, if you want to set this yourself, knock yourself out (we can't set the title for a system Done button, so we have to create a new one) + self.closeButton = nil; + // Initialize with title if title is set, otherwise the title will be 'Done' localized + self.closeButton = title != nil ? [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)] : [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + // If color on closebutton is requested then initialize with that that color, otherwise use initialize with default + self.closeButton.tintColor = colorString != nil ? [self colorFromHexString:colorString] : [UIColor colorWithRed:60.0 / 255.0 green:136.0 / 255.0 blue:230.0 / 255.0 alpha:1]; + + NSMutableArray* items = [self.toolbar.items mutableCopy]; + [items replaceObjectAtIndex:0 withObject:self.closeButton]; + [self.toolbar setItems:items]; +} + +- (void)showLocationBar:(BOOL)show +{ + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL toolbarVisible = !self.toolbar.hidden; + + // prevent double show/hide + if (show == !(self.addressLabel.hidden)) { + return; + } + + if (show) { + self.addressLabel.hidden = NO; + + if (toolbarVisible) { + // toolBar at the bottom, leave as is + // put locationBar on top of the toolBar + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= FOOTER_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no toolBar, so put locationBar at the bottom + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } + } else { + self.addressLabel.hidden = YES; + + if (toolbarVisible) { + // locationBar is on top of toolBar, hide locationBar + + // webView take up whole height less toolBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + } else { + // no toolBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition +{ + CGRect toolbarFrame = self.toolbar.frame; + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL locationbarVisible = !self.addressLabel.hidden; + + // prevent double show/hide + if (show == !(self.toolbar.hidden)) { + return; + } + + if (show) { + self.toolbar.hidden = NO; + CGRect webViewBounds = self.view.bounds; + + if (locationbarVisible) { + // locationBar at the bottom, move locationBar up + // put toolBar at the bottom + webViewBounds.size.height -= FOOTER_HEIGHT; + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + self.toolbar.frame = toolbarFrame; + } else { + // no locationBar, so put toolBar at the bottom + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + self.toolbar.frame = toolbarFrame; + } + + if ([toolbarPosition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + toolbarFrame.origin.y = 0; + webViewBounds.origin.y += toolbarFrame.size.height; + [self setWebViewFrame:webViewBounds]; + } else { + toolbarFrame.origin.y = (webViewBounds.size.height + LOCATIONBAR_HEIGHT); + } + [self setWebViewFrame:webViewBounds]; + + } else { + self.toolbar.hidden = YES; + + if (locationbarVisible) { + // locationBar is on top of toolBar, hide toolBar + // put locationBar at the bottom + + // webView take up whole height less locationBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + // move locationBar down + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no locationBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; +} + +- (void)viewDidUnload +{ + [self.webView loadHTMLString:nil baseURL:nil]; + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + [super viewDidUnload]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return UIStatusBarStyleDefault; +} + +- (BOOL)prefersStatusBarHidden { + return NO; +} + +- (void)close +{ + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + self.currentURL = nil; + + if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { + [self.navigationDelegate browserExit]; + } + + __weak UIViewController* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if ([weakSelf respondsToSelector:@selector(presentingViewController)]) { + [[weakSelf presentingViewController] dismissViewControllerAnimated:YES completion:^{ + [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; + }]; + } else { + [[weakSelf parentViewController] dismissViewControllerAnimated:YES completion:^{ + [[[[UIApplication sharedApplication] delegate] window] makeKeyAndVisible]; + }]; + } + }); +} + +- (void)navigateTo:(NSURL*)url +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + + if (_userAgentLockToken != 0) { + [self.webView loadRequest:request]; + } else { + __weak CDVUIInAppBrowserViewController* weakSelf = self; + [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { + _userAgentLockToken = lockToken; + [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken]; + [weakSelf.webView loadRequest:request]; + }]; + } +} + +- (void)goBack:(id)sender +{ + [self.webView goBack]; +} + +- (void)goForward:(id)sender +{ + [self.webView goForward]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + if (IsAtLeastiOSVersion(@"7.0")) { + [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; + } + [self rePositionViews]; + + [super viewWillAppear:animated]; +} + +// +// On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account. +// The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't +// change that value. +// +- (float) getStatusBarOffset { + CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; + float statusBarOffset = IsAtLeastiOSVersion(@"7.0") ? MIN(statusBarFrame.size.width, statusBarFrame.size.height) : 0.0; + return statusBarOffset; +} + +- (void) rePositionViews { + if ([_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, TOOLBAR_HEIGHT, self.webView.frame.size.width, self.webView.frame.size.height)]; + [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; + } +} + +// Helper function to convert hex color string to UIColor +// Assumes input like "#00FF00" (#RRGGBB). +// Taken from https://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string +- (UIColor *)colorFromHexString:(NSString *)hexString { + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:1]; // bypass '#' character + [scanner scanHexInt:&rgbValue]; + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; +} + +#pragma mark UIWebViewDelegate + +- (void)webViewDidStartLoad:(UIWebView*)theWebView +{ + // loading url, start spinner, update back/forward + + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + NSLog(_browserOptions.hidespinner ? @"Yes" : @"No"); + if(!_browserOptions.hidespinner) { + [self.spinner startAnimating]; + } + + return [self.navigationDelegate webViewDidStartLoad:theWebView]; +} + +- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + if (isTopLevelNavigation) { + self.currentURL = request.URL; + } + return [self.navigationDelegate webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType]; +} + +- (void)webViewDidFinishLoad:(UIWebView*)theWebView +{ + // update url, stop spinner, update back/forward + + self.addressLabel.text = [self.currentURL absoluteString]; + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + [self.spinner stopAnimating]; + + // Work around a bug where the first time a PDF is opened, all UIWebViews + // reload their User-Agent from NSUserDefaults. + // This work-around makes the following assumptions: + // 1. The app has only a single Cordova Webview. If not, then the app should + // take it upon themselves to load a PDF in the background as a part of + // their start-up flow. + // 2. That the PDF does not require any additional network requests. We change + // the user-agent here back to that of the CDVViewController, so requests + // from it must pass through its white-list. This *does* break PDFs that + // contain links to other remote PDF/websites. + // More info at https://issues.apache.org/jira/browse/CB-2225 + BOOL isPDF = [@"true" isEqualToString :[theWebView stringByEvaluatingJavaScriptFromString:@"document.body==null"]]; + if (isPDF) { + [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; + } + + [self.navigationDelegate webViewDidFinishLoad:theWebView]; +} + +- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +{ + // log fail message, stop spinner, update back/forward + NSLog(@"webView:didFailLoadWithError - %ld: %@", (long)error.code, [error localizedDescription]); + + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + [self.spinner stopAnimating]; + + self.addressLabel.text = NSLocalizedString(@"Load Error", nil); + + [self.navigationDelegate webView:theWebView didFailLoadWithError:error]; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { + return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + + return YES; +} + +@end + + diff --git a/src/ios/CDVWKInAppBrowser.h b/src/ios/CDVWKInAppBrowser.h new file mode 100644 index 0000000..4de7824 --- /dev/null +++ b/src/ios/CDVWKInAppBrowser.h @@ -0,0 +1,76 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import "CDVWKInAppBrowserUIDelegate.h" +#import "CDVInAppBrowserOptions.h" +#import "CDVInAppBrowserNavigationController.h" + +@class CDVWKInAppBrowserViewController; + +@interface CDVWKInAppBrowser : CDVPlugin { +} + +@property (nonatomic, retain) CDVWKInAppBrowser* instance; +@property (nonatomic, retain) CDVWKInAppBrowserViewController* inAppBrowserViewController; +@property (nonatomic, copy) NSString* callbackId; +@property (nonatomic, copy) NSRegularExpression *callbackIdPattern; + ++ (id) getInstance; +- (void)open:(CDVInvokedUrlCommand*)command; +- (void)close:(CDVInvokedUrlCommand*)command; +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command; +- (void)show:(CDVInvokedUrlCommand*)command; +- (void)hide:(CDVInvokedUrlCommand*)command; + +@end + +@interface CDVWKInAppBrowserViewController : UIViewController { + @private + NSString* _userAgent; + NSString* _prevUserAgent; + NSInteger _userAgentLockToken; + CDVInAppBrowserOptions *_browserOptions; +} + +@property (nonatomic, strong) IBOutlet WKWebView* webView; +@property (nonatomic, strong) IBOutlet WKWebViewConfiguration* configuration; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* closeButton; +@property (nonatomic, strong) IBOutlet UILabel* addressLabel; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* backButton; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* forwardButton; +@property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; +@property (nonatomic, strong) IBOutlet UIToolbar* toolbar; +@property (nonatomic, strong) IBOutlet CDVWKInAppBrowserUIDelegate* webViewUIDelegate; + +@property (nonatomic, weak) id orientationDelegate; +@property (nonatomic, weak) CDVWKInAppBrowser* navigationDelegate; +@property (nonatomic) NSURL* currentURL; + +- (void)close; +- (void)navigateTo:(NSURL*)url; +- (void)showLocationBar:(BOOL)show; +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition; +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions; + +@end diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m new file mode 100644 index 0000000..9b515dd --- /dev/null +++ b/src/ios/CDVWKInAppBrowser.m @@ -0,0 +1,1181 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVWKInAppBrowser.h" + +#if __has_include("CDVWKProcessPoolFactory.h") +#import "CDVWKProcessPoolFactory.h" +#endif + +#import +#import + +#define kInAppBrowserTargetSelf @"_self" +#define kInAppBrowserTargetSystem @"_system" +#define kInAppBrowserTargetBlank @"_blank" + +#define kInAppBrowserToolbarBarPositionBottom @"bottom" +#define kInAppBrowserToolbarBarPositionTop @"top" + +#define IAB_BRIDGE_NAME @"cordova_iab" + +#define TOOLBAR_HEIGHT 44.0 +#define STATUSBAR_HEIGHT 20.0 +#define LOCATIONBAR_HEIGHT 21.0 +#define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) + +#pragma mark CDVWKInAppBrowser + +@interface CDVWKInAppBrowser () { + NSInteger _previousStatusBarStyle; +} +@end + +@implementation CDVWKInAppBrowser + +static CDVWKInAppBrowser* instance = nil; + ++ (id) getInstance{ + return instance; +} + +- (void)pluginInitialize +{ + instance = self; + _previousStatusBarStyle = -1; + _callbackIdPattern = nil; +} + +- (id)settingForKey:(NSString*)key +{ + return [self.commandDelegate.settings objectForKey:[key lowercaseString]]; +} + +- (void)onReset +{ + [self close:nil]; +} + +- (void)close:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"IAB.close() called but it was already closed."); + return; + } + // Things are cleaned up in browserExit. + [self.inAppBrowserViewController close]; +} + +- (BOOL) isSystemUrl:(NSURL*)url +{ + if ([[url host] isEqualToString:@"itunes.apple.com"]) { + return YES; + } + + return NO; +} + +- (void)open:(CDVInvokedUrlCommand*)command +{ + CDVPluginResult* pluginResult; + + NSString* url = [command argumentAtIndex:0]; + NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf]; + NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; + + self.callbackId = command.callbackId; + + if (url != nil) { +#ifdef __CORDOVA_4_0_0 + NSURL* baseUrl = [self.webViewEngine URL]; +#else + NSURL* baseUrl = [self.webView.request URL]; +#endif + NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; + + if ([self isSystemUrl:absoluteUrl]) { + target = kInAppBrowserTargetSystem; + } + + if ([target isEqualToString:kInAppBrowserTargetSelf]) { + [self openInCordovaWebView:absoluteUrl withOptions:options]; + } else if ([target isEqualToString:kInAppBrowserTargetSystem]) { + [self openInSystem:absoluteUrl]; + } else { // _blank or anything else + [self openInInAppBrowser:absoluteUrl withOptions:options]; + } + + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"]; + } + + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options +{ + CDVInAppBrowserOptions* browserOptions = [CDVInAppBrowserOptions parseOptions:options]; + + WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore]; + if (browserOptions.cleardata) { + + NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; + [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:dateFrom completionHandler:^{ + NSLog(@"Removed all WKWebView data"); + self.inAppBrowserViewController.webView.configuration.processPool = [[WKProcessPool alloc] init]; // create new process pool to flush all data + }]; + } + + if (browserOptions.clearcache) { + bool isAtLeastiOS11 = false; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if (@available(iOS 11.0, *)) { + isAtLeastiOS11 = true; + } +#endif + + if(isAtLeastiOS11){ +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + // Deletes all cookies + WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; + [cookieStore getAllCookies:^(NSArray* cookies) { + NSHTTPCookie* cookie; + for(cookie in cookies){ + [cookieStore deleteCookie:cookie completionHandler:nil]; + } + }]; +#endif + }else{ + // https://stackoverflow.com/a/31803708/777265 + // Only deletes domain cookies (not session cookies) + [dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] + completionHandler:^(NSArray * __nonnull records) { + for (WKWebsiteDataRecord *record in records){ + NSSet* dataTypes = record.dataTypes; + if([dataTypes containsObject:WKWebsiteDataTypeCookies]){ + [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes + forDataRecords:@[record] + completionHandler:^{}]; + } + } + }]; + } + } + + if (browserOptions.clearsessioncache) { + bool isAtLeastiOS11 = false; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if (@available(iOS 11.0, *)) { + isAtLeastiOS11 = true; + } +#endif + if (isAtLeastiOS11) { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + // Deletes session cookies + WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; + [cookieStore getAllCookies:^(NSArray* cookies) { + NSHTTPCookie* cookie; + for(cookie in cookies){ + if(cookie.sessionOnly){ + [cookieStore deleteCookie:cookie completionHandler:nil]; + } + } + }]; +#endif + }else{ + NSLog(@"clearsessioncache not available below iOS 11.0"); + } + } + + if (self.inAppBrowserViewController == nil) { + NSString* userAgent = [CDVUserAgentUtil originalUserAgent]; + NSString* overrideUserAgent = [self settingForKey:@"OverrideUserAgent"]; + NSString* appendUserAgent = [self settingForKey:@"AppendUserAgent"]; + if(overrideUserAgent){ + userAgent = overrideUserAgent; + } + if(appendUserAgent){ + userAgent = [userAgent stringByAppendingString: appendUserAgent]; + } + self.inAppBrowserViewController = [[CDVWKInAppBrowserViewController alloc] initWithUserAgent:userAgent prevUserAgent:[self.commandDelegate userAgent] browserOptions: browserOptions]; + self.inAppBrowserViewController.navigationDelegate = self; + + if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { + self.inAppBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; + } + } + + [self.inAppBrowserViewController showLocationBar:browserOptions.location]; + [self.inAppBrowserViewController showToolBar:browserOptions.toolbar :browserOptions.toolbarposition]; + if (browserOptions.closebuttoncaption != nil || browserOptions.closebuttoncolor != nil) { + [self.inAppBrowserViewController setCloseButtonTitle:browserOptions.closebuttoncaption :browserOptions.closebuttoncolor]; + } + // Set Presentation Style + UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default + if (browserOptions.presentationstyle != nil) { + if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"pagesheet"]) { + presentationStyle = UIModalPresentationPageSheet; + } else if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"formsheet"]) { + presentationStyle = UIModalPresentationFormSheet; + } + } + self.inAppBrowserViewController.modalPresentationStyle = presentationStyle; + + // Set Transition Style + UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default + if (browserOptions.transitionstyle != nil) { + if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"fliphorizontal"]) { + transitionStyle = UIModalTransitionStyleFlipHorizontal; + } else if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"crossdissolve"]) { + transitionStyle = UIModalTransitionStyleCrossDissolve; + } + } + self.inAppBrowserViewController.modalTransitionStyle = transitionStyle; + + //prevent webView from bouncing + if (browserOptions.disallowoverscroll) { + if ([self.inAppBrowserViewController.webView respondsToSelector:@selector(scrollView)]) { + ((UIScrollView*)[self.inAppBrowserViewController.webView scrollView]).bounces = NO; + } else { + for (id subview in self.inAppBrowserViewController.webView.subviews) { + if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { + ((UIScrollView*)subview).bounces = NO; + } + } + } + } + + + [self.inAppBrowserViewController navigateTo:url]; + [self show:nil withNoAnimate:browserOptions.hidden]; +} + +- (void)show:(CDVInvokedUrlCommand*)command{ + [self show:command withNoAnimate:NO]; +} + +- (void)show:(CDVInvokedUrlCommand*)command withNoAnimate:(BOOL)noAnimate +{ + BOOL initHidden = NO; + if(command == nil && noAnimate == YES){ + initHidden = YES; + } + + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to show IAB after it was closed."); + return; + } + if (_previousStatusBarStyle != -1) { + NSLog(@"Tried to show IAB while already shown"); + return; + } + + if(!initHidden){ + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + } + + __block CDVInAppBrowserNavigationController* nav = [[CDVInAppBrowserNavigationController alloc] + initWithRootViewController:self.inAppBrowserViewController]; + nav.orientationDelegate = self.inAppBrowserViewController; + nav.navigationBarHidden = YES; + nav.modalPresentationStyle = self.inAppBrowserViewController.modalPresentationStyle; + + __weak CDVWKInAppBrowser* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.inAppBrowserViewController != nil) { + float osVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; + CGRect frame = [[UIScreen mainScreen] bounds]; + if(initHidden && osVersion < 11){ + frame.origin.x = -10000; + } + + UIWindow *tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + UIViewController *tmpController = [[UIViewController alloc] init]; + + [tmpWindow setRootViewController:tmpController]; + [tmpWindow setWindowLevel:UIWindowLevelNormal]; + + if(!initHidden || osVersion < 11){ + [tmpWindow makeKeyAndVisible]; + } + [tmpController presentViewController:nav animated:!noAnimate completion:nil]; + } + }); +} + +- (void)hide:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to hide IAB after it was closed."); + return; + + + } + if (_previousStatusBarStyle == -1) { + NSLog(@"Tried to hide IAB while already hidden"); + return; + } + + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.inAppBrowserViewController != nil) { + _previousStatusBarStyle = -1; + [self.inAppBrowserViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil]; + } + }); +} + +- (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + +#ifdef __CORDOVA_4_0_0 + // the webview engine itself will filter for this according to policy + // in config.xml for cordova-ios-4.0 + [self.webViewEngine loadRequest:request]; +#else + if ([self.commandDelegate URLIsWhitelisted:url]) { + [self.webView loadRequest:request]; + } else { // this assumes the InAppBrowser can be excepted from the white-list + [self openInInAppBrowser:url withOptions:options]; + } +#endif +} + +- (void)openInSystem:(NSURL*)url +{ + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + [[UIApplication sharedApplication] openURL:url]; +} + +// This is a helper method for the inject{Script|Style}{Code|File} API calls, which +// provides a consistent method for injecting JavaScript code into the document. +// +// If a wrapper string is supplied, then the source string will be JSON-encoded (adding +// quotes) and wrapped using string formatting. (The wrapper string should have a single +// '%@' marker). +// +// If no wrapper is supplied, then the source string is executed directly. + +- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper +{ + // Ensure a message handler bridge is created to communicate with the CDVWKInAppBrowserViewController + [self evaluateJavaScript: [NSString stringWithFormat:@"(function(w){if(!w._cdvMessageHandler) {w._cdvMessageHandler = function(id,d){w.webkit.messageHandlers.%@.postMessage({d:d, id:id});}}})(window)", IAB_BRIDGE_NAME]]; + + if (jsWrapper != nil) { + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; + NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + if (sourceArrayString) { + NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)]; + NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString]; + [self evaluateJavaScript:jsToInject]; + } + } else { + [self evaluateJavaScript:source]; + } +} + + +//Synchronus helper for javascript evaluation + +- (NSString *)evaluateJavaScript:(NSString *)script { + __block NSString *resultString = nil; + __block BOOL finished = NO; + __block NSString* _script = script; + + [self.inAppBrowserViewController.webView evaluateJavaScript:script completionHandler:^(id result, NSError *error) { + if (error == nil) { + if (result != nil) { + resultString = result; + NSLog(@"%@", resultString); + } + } else { + NSLog(@"evaluateJavaScript error : %@ : %@", error.localizedDescription, _script); + } + finished = YES; + }]; + + while (!finished) + { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + + return resultString; +} + +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper = nil; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"_cdvMessageHandler('%@',JSON.stringify([eval(%%@)]));", command.callbackId]; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectScriptFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvMessageHandler('%@'); }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvMessageHandler('%@'); }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvMessageHandler('%@'); }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (BOOL)isValidCallbackId:(NSString *)callbackId +{ + NSError *err = nil; + // Initialize on first use + if (self.callbackIdPattern == nil) { + self.callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"^InAppBrowser[0-9]{1,10}$" options:0 error:&err]; + if (err != nil) { + // Couldn't initialize Regex; No is safer than Yes. + return NO; + } + } + if ([self.callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) { + return YES; + } + return NO; +} + +/** + * The message handler bridge provided for the InAppBrowser is capable of executing any oustanding callback belonging + * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no + * other code execution is possible. + */ +- (BOOL)webView:(WKWebView*)theWebView decidePolicyForNavigationAction:(NSURLRequest*)request +{ + NSURL* url = request.URL; + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + //if is an app store link, let the system handle it, otherwise it fails to load it + if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { + [theWebView stopLoading]; + [self openInSystem:url]; + return NO; + } + else if ((self.callbackId != nil) && isTopLevelNavigation) { + // Send a loadstart event for each top-level navigation (includes redirects). + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + + return YES; +} + +#pragma mark WKScriptMessageHandler delegate +- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + + CDVPluginResult* pluginResult = nil; + + NSDictionary* messageContent = (NSDictionary*) message.body; + NSString* scriptCallbackId = messageContent[@"id"]; + + if([messageContent objectForKey:@"d"]){ + NSString* scriptResult = messageContent[@"d"]; + NSError* __autoreleasing error = nil; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; + } + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; + } + + [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; +} + +- (void)didStartProvisionalNavigation:(WKWebView*)theWebView +{ + NSLog(@"didStartProvisionalNavigation"); +// self.inAppBrowserViewController.currentURL = theWebView.URL; +} + +- (void)didFinishNavigation:(WKWebView*)theWebView +{ + if (self.callbackId != nil) { + NSString* url = [theWebView.URL absoluteString]; + if(url == nil){ + if(self.inAppBrowserViewController.currentURL != nil){ + url = [self.inAppBrowserViewController.currentURL absoluteString]; + }else{ + url = @""; + } + } + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)webView:(WKWebView*)theWebView didFailNavigation:(NSError*)error +{ + if (self.callbackId != nil) { + NSString* url = [theWebView.URL absoluteString]; + if(url == nil){ + if(self.inAppBrowserViewController.currentURL != nil){ + url = [self.inAppBrowserViewController.currentURL absoluteString]; + }else{ + url = @""; + } + } + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInteger:error.code], @"message": error.localizedDescription}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)browserExit +{ + if (self.callbackId != nil) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"exit"}]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + self.callbackId = nil; + } + + [self.inAppBrowserViewController.configuration.userContentController removeScriptMessageHandlerForName:IAB_BRIDGE_NAME]; + self.inAppBrowserViewController.configuration = nil; + + [self.inAppBrowserViewController.webView stopLoading]; + [self.inAppBrowserViewController.webView removeFromSuperview]; + [self.inAppBrowserViewController.webView setUIDelegate:nil]; + [self.inAppBrowserViewController.webView setNavigationDelegate:nil]; + self.inAppBrowserViewController.webView = nil; + + // Set navigationDelegate to nil to ensure no callbacks are received from it. + self.inAppBrowserViewController.navigationDelegate = nil; + self.inAppBrowserViewController = nil; + + if (IsAtLeastiOSVersion(@"7.0")) { + if (_previousStatusBarStyle != -1) { + [[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle]; + + } + } + + _previousStatusBarStyle = -1; // this value was reset before reapplying it. caused statusbar to stay black on ios7 +} + +@end //CDVWKInAppBrowser + +#pragma mark CDVWKInAppBrowserViewController + +@implementation CDVWKInAppBrowserViewController + +@synthesize currentURL; + +BOOL viewRenderedAtLeastOnce = FALSE; +BOOL isExiting = FALSE; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions +{ + self = [super init]; + if (self != nil) { + _userAgent = userAgent; + _prevUserAgent = prevUserAgent; + _browserOptions = browserOptions; + self.webViewUIDelegate = [[CDVWKInAppBrowserUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]]; + [self.webViewUIDelegate setViewController:self]; + + [self createViews]; + } + + return self; +} + +-(void)dealloc { + //NSLog(@"dealloc"); +} + +- (void)createViews +{ + // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included + + CGRect webViewBounds = self.view.bounds; + BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]; + webViewBounds.size.height -= _browserOptions.location ? FOOTER_HEIGHT : TOOLBAR_HEIGHT; + WKUserContentController* userContentController = [[WKUserContentController alloc] init]; + + WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; + configuration.userContentController = userContentController; +#if __has_include("CDVWKProcessPoolFactory.h") + configuration.processPool = [[CDVWKProcessPoolFactory sharedFactory] sharedProcessPool]; +#endif + [configuration.userContentController addScriptMessageHandler:self name:IAB_BRIDGE_NAME]; + + //WKWebView options + configuration.ignoresViewportScaleLimits = _browserOptions.enableviewportscale; + configuration.allowsInlineMediaPlayback = _browserOptions.allowinlinemediaplayback; + if(_browserOptions.mediaplaybackrequiresuseraction == YES){ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + }else{ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } + + self.webView = [[WKWebView alloc] initWithFrame:webViewBounds configuration:configuration]; + + [self.view addSubview:self.webView]; + [self.view sendSubviewToBack:self.webView]; + + + self.webView.navigationDelegate = self; + self.webView.UIDelegate = self.webViewUIDelegate; + self.webView.backgroundColor = [UIColor whiteColor]; + + self.webView.clearsContextBeforeDrawing = YES; + self.webView.clipsToBounds = YES; + self.webView.contentMode = UIViewContentModeScaleToFill; + self.webView.multipleTouchEnabled = YES; + self.webView.opaque = YES; + self.webView.userInteractionEnabled = YES; + self.automaticallyAdjustsScrollViewInsets = YES ; + [self.webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth]; + self.webView.allowsLinkPreview = NO; + self.webView.allowsBackForwardNavigationGestures = NO; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if (@available(iOS 11.0, *)) { + [self.webView.scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever]; + } +#endif + + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + self.spinner.alpha = 1.000; + self.spinner.autoresizesSubviews = YES; + self.spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin); + self.spinner.clearsContextBeforeDrawing = NO; + self.spinner.clipsToBounds = NO; + self.spinner.contentMode = UIViewContentModeScaleToFill; + self.spinner.frame = CGRectMake(CGRectGetMidX(self.webView.frame), CGRectGetMidY(self.webView.frame), 20.0, 20.0); + self.spinner.hidden = NO; + self.spinner.hidesWhenStopped = YES; + self.spinner.multipleTouchEnabled = NO; + self.spinner.opaque = NO; + self.spinner.userInteractionEnabled = NO; + [self.spinner stopAnimating]; + + self.closeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + + UIBarButtonItem* flexibleSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + + UIBarButtonItem* fixedSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + fixedSpaceButton.width = 20; + + float toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - TOOLBAR_HEIGHT : 0.0; + CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, TOOLBAR_HEIGHT); + + self.toolbar = [[UIToolbar alloc] initWithFrame:toolbarFrame]; + self.toolbar.alpha = 1.000; + self.toolbar.autoresizesSubviews = YES; + self.toolbar.autoresizingMask = toolbarIsAtBottom ? (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin) : UIViewAutoresizingFlexibleWidth; + self.toolbar.barStyle = UIBarStyleBlackOpaque; + self.toolbar.clearsContextBeforeDrawing = NO; + self.toolbar.clipsToBounds = NO; + self.toolbar.contentMode = UIViewContentModeScaleToFill; + self.toolbar.hidden = NO; + self.toolbar.multipleTouchEnabled = NO; + self.toolbar.opaque = NO; + self.toolbar.userInteractionEnabled = YES; + if (_browserOptions.toolbarcolor != nil) { // Set toolbar color if user sets it in options + self.toolbar.barTintColor = [self colorFromHexString:_browserOptions.toolbarcolor]; + } + if (!_browserOptions.toolbartranslucent) { // Set toolbar translucent to no if user sets it in options + self.toolbar.translucent = NO; + } + + CGFloat labelInset = 5.0; + float locationBarY = toolbarIsAtBottom ? self.view.bounds.size.height - FOOTER_HEIGHT : self.view.bounds.size.height - LOCATIONBAR_HEIGHT; + + self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, locationBarY, self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)]; + self.addressLabel.adjustsFontSizeToFitWidth = NO; + self.addressLabel.alpha = 1.000; + self.addressLabel.autoresizesSubviews = YES; + self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; + self.addressLabel.backgroundColor = [UIColor clearColor]; + self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; + self.addressLabel.clearsContextBeforeDrawing = YES; + self.addressLabel.clipsToBounds = YES; + self.addressLabel.contentMode = UIViewContentModeScaleToFill; + self.addressLabel.enabled = YES; + self.addressLabel.hidden = NO; + self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail; + + if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumScaleFactor:")]) { + [self.addressLabel setValue:@(10.0/[UIFont labelFontSize]) forKey:@"minimumScaleFactor"]; + } else if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumFontSize:")]) { + [self.addressLabel setValue:@(10.0) forKey:@"minimumFontSize"]; + } + + self.addressLabel.multipleTouchEnabled = NO; + self.addressLabel.numberOfLines = 1; + self.addressLabel.opaque = NO; + self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0); + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.addressLabel.textAlignment = NSTextAlignmentLeft; + self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000]; + self.addressLabel.userInteractionEnabled = NO; + + NSString* frontArrowString = NSLocalizedString(@"►", nil); // create arrow from Unicode char + self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; + self.forwardButton.enabled = YES; + self.forwardButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.forwardButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + NSString* backArrowString = NSLocalizedString(@"◄", nil); // create arrow from Unicode char + self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; + self.backButton.enabled = YES; + self.backButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.backButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + // Filter out Navigation Buttons if user requests so + if (_browserOptions.hidenavigationbuttons) { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton]]; + } else { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; + } + + self.view.backgroundColor = [UIColor grayColor]; + [self.view addSubview:self.toolbar]; + [self.view addSubview:self.addressLabel]; + [self.view addSubview:self.spinner]; +} + +- (void) setWebViewFrame : (CGRect) frame { + NSLog(@"Setting the WebView's frame to %@", NSStringFromCGRect(frame)); + [self.webView setFrame:frame]; +} + +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString +{ + // the advantage of using UIBarButtonSystemItemDone is the system will localize it for you automatically + // but, if you want to set this yourself, knock yourself out (we can't set the title for a system Done button, so we have to create a new one) + self.closeButton = nil; + // Initialize with title if title is set, otherwise the title will be 'Done' localized + self.closeButton = title != nil ? [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)] : [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + // If color on closebutton is requested then initialize with that that color, otherwise use initialize with default + self.closeButton.tintColor = colorString != nil ? [self colorFromHexString:colorString] : [UIColor colorWithRed:60.0 / 255.0 green:136.0 / 255.0 blue:230.0 / 255.0 alpha:1]; + + NSMutableArray* items = [self.toolbar.items mutableCopy]; + [items replaceObjectAtIndex:0 withObject:self.closeButton]; + [self.toolbar setItems:items]; +} + +- (void)showLocationBar:(BOOL)show +{ + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL toolbarVisible = !self.toolbar.hidden; + + // prevent double show/hide + if (show == !(self.addressLabel.hidden)) { + return; + } + + if (show) { + self.addressLabel.hidden = NO; + + if (toolbarVisible) { + // toolBar at the bottom, leave as is + // put locationBar on top of the toolBar + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= FOOTER_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no toolBar, so put locationBar at the bottom + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } + } else { + self.addressLabel.hidden = YES; + + if (toolbarVisible) { + // locationBar is on top of toolBar, hide locationBar + + // webView take up whole height less toolBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + } else { + // no toolBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition +{ + CGRect toolbarFrame = self.toolbar.frame; + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL locationbarVisible = !self.addressLabel.hidden; + + // prevent double show/hide + if (show == !(self.toolbar.hidden)) { + return; + } + + if (show) { + self.toolbar.hidden = NO; + CGRect webViewBounds = self.view.bounds; + + if (locationbarVisible) { + // locationBar at the bottom, move locationBar up + // put toolBar at the bottom + webViewBounds.size.height -= FOOTER_HEIGHT; + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + self.toolbar.frame = toolbarFrame; + } else { + // no locationBar, so put toolBar at the bottom + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + self.toolbar.frame = toolbarFrame; + } + + if ([toolbarPosition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + toolbarFrame.origin.y = 0; + webViewBounds.origin.y += toolbarFrame.size.height; + [self setWebViewFrame:webViewBounds]; + } else { + toolbarFrame.origin.y = (webViewBounds.size.height + LOCATIONBAR_HEIGHT); + } + [self setWebViewFrame:webViewBounds]; + + } else { + self.toolbar.hidden = YES; + + if (locationbarVisible) { + // locationBar is on top of toolBar, hide toolBar + // put locationBar at the bottom + + // webView take up whole height less locationBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + // move locationBar down + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no locationBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)viewDidLoad +{ + viewRenderedAtLeastOnce = FALSE; + [super viewDidLoad]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + if (isExiting && (self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { + [self.navigationDelegate browserExit]; + isExiting = FALSE; + } +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return UIStatusBarStyleDefault; +} + +- (BOOL)prefersStatusBarHidden { + return NO; +} + +- (void)close +{ + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + self.currentURL = nil; + + __weak UIViewController* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + isExiting = TRUE; + if ([weakSelf respondsToSelector:@selector(presentingViewController)]) { + [[weakSelf presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + } else { + [[weakSelf parentViewController] dismissViewControllerAnimated:YES completion:nil]; + } + }); +} + +- (void)navigateTo:(NSURL*)url +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + + if (_userAgentLockToken != 0) { + [self.webView loadRequest:request]; + } else { + __weak CDVWKInAppBrowserViewController* weakSelf = self; + [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { + _userAgentLockToken = lockToken; + [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken]; + [weakSelf.webView loadRequest:request]; + }]; + } +} + +- (void)goBack:(id)sender +{ + [self.webView goBack]; +} + +- (void)goForward:(id)sender +{ + [self.webView goForward]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + if (IsAtLeastiOSVersion(@"7.0") && !viewRenderedAtLeastOnce) { + viewRenderedAtLeastOnce = TRUE; + CGRect viewBounds = [self.webView bounds]; + viewBounds.origin.y = STATUSBAR_HEIGHT; + viewBounds.size.height = viewBounds.size.height - STATUSBAR_HEIGHT; + self.webView.frame = viewBounds; + [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; + } + [self rePositionViews]; + + [super viewWillAppear:animated]; +} + +// +// On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account. +// The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't +// change that value. +// +- (float) getStatusBarOffset { + CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; + float statusBarOffset = IsAtLeastiOSVersion(@"7.0") ? MIN(statusBarFrame.size.width, statusBarFrame.size.height) : 0.0; + return statusBarOffset; +} + +- (void) rePositionViews { + if ([_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, TOOLBAR_HEIGHT, self.webView.frame.size.width, self.webView.frame.size.height)]; + [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; + } +} + +// Helper function to convert hex color string to UIColor +// Assumes input like "#00FF00" (#RRGGBB). +// Taken from https://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string +- (UIColor *)colorFromHexString:(NSString *)hexString { + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:1]; // bypass '#' character + [scanner scanHexInt:&rgbValue]; + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; +} + +#pragma mark WKNavigationDelegate + +- (void)webView:(WKWebView *)theWebView didStartProvisionalNavigation:(WKNavigation *)navigation{ + + // loading url, start spinner, update back/forward + + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + NSLog(_browserOptions.hidespinner ? @"Yes" : @"No"); + if(!_browserOptions.hidespinner) { + [self.spinner startAnimating]; + } + + return [self.navigationDelegate didStartProvisionalNavigation:theWebView]; +} + +- (void)webView:(WKWebView *)theWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + NSURL *url = navigationAction.request.URL; + NSURL *mainDocumentURL = navigationAction.request.mainDocumentURL; + + BOOL isTopLevelNavigation = [url isEqual:mainDocumentURL]; + + if (isTopLevelNavigation) { + self.currentURL = url; + } + + [self.navigationDelegate webView:theWebView decidePolicyForNavigationAction:navigationAction.request]; + + decisionHandler(WKNavigationActionPolicyAllow); + +} + +- (void)webView:(WKWebView *)theWebView didFinishNavigation:(WKNavigation *)navigation +{ + // update url, stop spinner, update back/forward + + self.addressLabel.text = [self.currentURL absoluteString]; + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + theWebView.scrollView.contentInset = UIEdgeInsetsZero; + + [self.spinner stopAnimating]; + + // Work around a bug where the first time a PDF is opened, all UIWebViews + // reload their User-Agent from NSUserDefaults. + // This work-around makes the following assumptions: + // 1. The app has only a single Cordova Webview. If not, then the app should + // take it upon themselves to load a PDF in the background as a part of + // their start-up flow. + // 2. That the PDF does not require any additional network requests. We change + // the user-agent here back to that of the CDVViewController, so requests + // from it must pass through its white-list. This *does* break PDFs that + // contain links to other remote PDF/websites. + // More info at https://issues.apache.org/jira/browse/CB-2225 + BOOL isPDF = NO; + //TODO webview class + //BOOL isPDF = [@"true" isEqualToString :[theWebView evaluateJavaScript:@"document.body==null"]]; + if (isPDF) { + [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; + } + + [self.navigationDelegate didFinishNavigation:theWebView]; +} + +- (void)webView:(WKWebView*)theWebView failedNavigation:(NSString*) delegateName withError:(nonnull NSError *)error{ + // log fail message, stop spinner, update back/forward + NSLog(@"webView:%@ - %ld: %@", delegateName, (long)error.code, [error localizedDescription]); + + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + [self.spinner stopAnimating]; + + self.addressLabel.text = NSLocalizedString(@"Load Error", nil); + + [self.navigationDelegate webView:theWebView didFailNavigation:error]; +} + +- (void)webView:(WKWebView*)theWebView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error +{ + [self webView:theWebView failedNavigation:@"didFailNavigation" withError:error]; +} + +- (void)webView:(WKWebView*)theWebView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error +{ + [self webView:theWebView failedNavigation:@"didFailProvisionalNavigation" withError:error]; +} + +#pragma mark WKScriptMessageHandler delegate +- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + if (![message.name isEqualToString:IAB_BRIDGE_NAME]) { + return; + } + //NSLog(@"Received script message %@", message.body); + [self.navigationDelegate userContentController:userContentController didReceiveScriptMessage:message]; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { + return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + + return YES; +} + + +@end //CDVWKInAppBrowserViewController diff --git a/src/ios/CDVWKInAppBrowserUIDelegate.h b/src/ios/CDVWKInAppBrowserUIDelegate.h new file mode 100644 index 0000000..1a6ea22 --- /dev/null +++ b/src/ios/CDVWKInAppBrowserUIDelegate.h @@ -0,0 +1,32 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVWKInAppBrowserUIDelegate : NSObject { + @private + UIViewController* _viewController; +} + +@property (nonatomic, copy) NSString* title; + +- (instancetype)initWithTitle:(NSString*)title; +-(void) setViewController:(UIViewController*) viewController; + +@end diff --git a/src/ios/CDVWKInAppBrowserUIDelegate.m b/src/ios/CDVWKInAppBrowserUIDelegate.m new file mode 100644 index 0000000..4bc7a76 --- /dev/null +++ b/src/ios/CDVWKInAppBrowserUIDelegate.m @@ -0,0 +1,127 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVWKInAppBrowserUIDelegate.h" + +@implementation CDVWKInAppBrowserUIDelegate + +- (instancetype)initWithTitle:(NSString*)title +{ + self = [super init]; + if (self) { + self.title = title; + } + + return self; +} + +- (void) webView:(WKWebView*)webView runJavaScriptAlertPanelWithMessage:(NSString*)message + initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(void))completionHandler +{ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + + [alert addAction:ok]; + + [[self getViewController] presentViewController:alert animated:YES completion:nil]; +} + +- (void) webView:(WKWebView*)webView runJavaScriptConfirmPanelWithMessage:(NSString*)message + initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(BOOL result))completionHandler +{ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(YES); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + + [alert addAction:ok]; + + UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(NO); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + [alert addAction:cancel]; + + [[self getViewController] presentViewController:alert animated:YES completion:nil]; +} + +- (void) webView:(WKWebView*)webView runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt + defaultText:(NSString*)defaultText initiatedByFrame:(WKFrameInfo*)frame + completionHandler:(void (^)(NSString* result))completionHandler +{ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title + message:prompt + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(((UITextField*)alert.textFields[0]).text); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + + [alert addAction:ok]; + + UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(nil); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + [alert addAction:cancel]; + + [alert addTextFieldWithConfigurationHandler:^(UITextField* textField) { + textField.text = defaultText; + }]; + + [[self getViewController] presentViewController:alert animated:YES completion:nil]; +} + +-(UIViewController*) getViewController +{ + return _viewController; +} + +-(void) setViewController:(UIViewController*) viewController +{ + _viewController = viewController; +} + +@end diff --git a/tests/plugin.xml b/tests/plugin.xml index f9ab2d3..5bf5600 100644 --- a/tests/plugin.xml +++ b/tests/plugin.xml @@ -29,5 +29,11 @@ + + + + + + diff --git a/tests/tests.js b/tests/tests.js index abb2a6d..69ff46c 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -19,10 +19,11 @@ * */ -/* global MSApp */ +/* global MSApp, alert */ var cordova = require('cordova'); var isWindows = cordova.platformId === 'windows'; +var isIos = cordova.platformId === 'ios'; var isBrowser = cordova.platformId === 'browser'; window.alert = window.alert || navigator.notification.alert; @@ -33,122 +34,133 @@ if (isWindows && navigator && navigator.notification && navigator.notification.a exports.defineAutoTests = function () { - describe('cordova.InAppBrowser', function () { + var createTests = function (platformOpts) { + platformOpts = platformOpts || ''; - it('inappbrowser.spec.1 should exist', function () { - expect(cordova.InAppBrowser).toBeDefined(); - }); + describe('cordova.InAppBrowser', function () { - it('inappbrowser.spec.2 should contain open function', function () { - expect(cordova.InAppBrowser.open).toBeDefined(); - expect(cordova.InAppBrowser.open).toEqual(jasmine.any(Function)); - }); - }); - - describe('open method', function () { - - if (cordova.platformId === 'osx') { - pending('Open method not fully supported on OSX.'); - return; - } - - var iabInstance; - var originalTimeout; - var url = 'https://dist.apache.org/repos/dist/dev/cordova/'; - var badUrl = 'http://bad-uri/'; - - beforeEach(function () { - // increase timeout to ensure test url could be loaded within test time - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; - jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; - - iabInstance = null; - }); - - afterEach(function (done) { - // restore original timeout - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; - - if (iabInstance !== null && iabInstance.close) { - iabInstance.close(); - } - iabInstance = null; - // add some extra time so that iab dialog is closed - setTimeout(done, 2000); - }); - - function verifyEvent (evt, type) { - expect(evt).toBeDefined(); - expect(evt.type).toEqual(type); - // `exit` event does not have url field, browser returns null url for CORS requests - if (type !== 'exit' && !isBrowser) { - expect(evt.url).toEqual(url); - } - } - - function verifyLoadErrorEvent (evt) { - expect(evt).toBeDefined(); - expect(evt.type).toEqual('loaderror'); - expect(evt.url).toEqual(badUrl); - expect(evt.code).toEqual(jasmine.any(Number)); - expect(evt.message).toEqual(jasmine.any(String)); - } - - it('inappbrowser.spec.3 should return InAppBrowser instance with required methods', function () { - iabInstance = cordova.InAppBrowser.open(url, '_blank'); - - expect(iabInstance).toBeDefined(); - - expect(iabInstance.addEventListener).toEqual(jasmine.any(Function)); - expect(iabInstance.removeEventListener).toEqual(jasmine.any(Function)); - expect(iabInstance.close).toEqual(jasmine.any(Function)); - expect(iabInstance.show).toEqual(jasmine.any(Function)); - expect(iabInstance.hide).toEqual(jasmine.any(Function)); - expect(iabInstance.executeScript).toEqual(jasmine.any(Function)); - expect(iabInstance.insertCSS).toEqual(jasmine.any(Function)); - }); - - it('inappbrowser.spec.4 should support loadstart and loadstop events', function (done) { - var onLoadStart = jasmine.createSpy('loadstart event callback').and.callFake(function (evt) { - verifyEvent(evt, 'loadstart'); + it('inappbrowser.spec.1 should exist', function () { + expect(cordova.InAppBrowser).toBeDefined(); }); - iabInstance = cordova.InAppBrowser.open(url, '_blank'); - iabInstance.addEventListener('loadstart', onLoadStart); - iabInstance.addEventListener('loadstop', function (evt) { - verifyEvent(evt, 'loadstop'); - if (!isBrowser) { - // according to documentation, "loadstart" event is not supported on browser - // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 - expect(onLoadStart).toHaveBeenCalled(); + it('inappbrowser.spec.2 should contain open function', function () { + expect(cordova.InAppBrowser.open).toBeDefined(); + expect(cordova.InAppBrowser.open).toEqual(jasmine.any(Function)); + }); + }); + + describe('open method', function () { + + if (cordova.platformId === 'osx') { + pending('Open method not fully supported on OSX.'); + return; + } + + var iabInstance; + var originalTimeout; + var url = 'https://dist.apache.org/repos/dist/dev/cordova/'; + var badUrl = 'http://bad-uri/'; + + beforeEach(function () { + // increase timeout to ensure test url could be loaded within test time + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; + + iabInstance = null; + }); + + afterEach(function (done) { + // restore original timeout + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + + if (iabInstance !== null && iabInstance.close) { + iabInstance.close(); } - done(); + iabInstance = null; + // add some extra time so that iab dialog is closed + setTimeout(done, 2000); }); - }); - it('inappbrowser.spec.5 should support exit event', function (done) { - iabInstance = cordova.InAppBrowser.open(url, '_blank'); - iabInstance.addEventListener('exit', function (evt) { - verifyEvent(evt, 'exit'); - done(); - }); - iabInstance.close(); - iabInstance = null; - }); - - it('inappbrowser.spec.6 should support loaderror event', function (done) { - if (isBrowser) { - // according to documentation, "loaderror" event is not supported on browser - // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 - pending('Browser platform doesn\'t support loaderror event'); + function verifyEvent (evt, type) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual(type); + // `exit` event does not have url field, browser returns null url for CORS requests + if (type !== 'exit' && !isBrowser) { + expect(evt.url).toEqual(url); + } } - iabInstance = cordova.InAppBrowser.open(badUrl, '_blank'); - iabInstance.addEventListener('loaderror', function (evt) { - verifyLoadErrorEvent(evt); - done(); + + function verifyLoadErrorEvent (evt) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual('loaderror'); + expect(evt.url).toEqual(badUrl); + expect(evt.code).toEqual(jasmine.any(Number)); + expect(evt.message).toEqual(jasmine.any(String)); + } + + it('inappbrowser.spec.3 should return InAppBrowser instance with required methods', function () { + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + + expect(iabInstance).toBeDefined(); + + expect(iabInstance.addEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.removeEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.close).toEqual(jasmine.any(Function)); + expect(iabInstance.show).toEqual(jasmine.any(Function)); + expect(iabInstance.hide).toEqual(jasmine.any(Function)); + expect(iabInstance.executeScript).toEqual(jasmine.any(Function)); + expect(iabInstance.insertCSS).toEqual(jasmine.any(Function)); + }); + + it('inappbrowser.spec.4 should support loadstart and loadstop events', function (done) { + var onLoadStart = jasmine.createSpy('loadstart event callback').and.callFake(function (evt) { + verifyEvent(evt, 'loadstart'); + }); + + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('loadstart', onLoadStart); + iabInstance.addEventListener('loadstop', function (evt) { + verifyEvent(evt, 'loadstop'); + if (!isBrowser) { + // according to documentation, "loadstart" event is not supported on browser + // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 + expect(onLoadStart).toHaveBeenCalled(); + } + done(); + }); + }); + + it('inappbrowser.spec.5 should support exit event', function (done) { + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('exit', function (evt) { + verifyEvent(evt, 'exit'); + done(); + }); + iabInstance.close(); + iabInstance = null; + }); + + it('inappbrowser.spec.6 should support loaderror event', function (done) { + if (isBrowser) { + // according to documentation, "loaderror" event is not supported on browser + // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 + pending('Browser platform doesn\'t support loaderror event'); + } + iabInstance = cordova.InAppBrowser.open(badUrl, '_blank', platformOpts); + iabInstance.addEventListener('loaderror', function (evt) { + verifyLoadErrorEvent(evt); + done(); + }); }); }); - }); + }; + + if (isIos) { + createTests('usewkwebview=no'); + createTests('usewkwebview=yes'); + } else { + createTests(); + } }; exports.defineManualTests = function (contentEl, createActionButton) { @@ -161,6 +173,7 @@ exports.defineManualTests = function (contentEl, createActionButton) { var counts; var lastLoadStartURL; var wasReset = false; + function reset () { counts = { 'loaderror': 0, @@ -170,6 +183,7 @@ exports.defineManualTests = function (contentEl, createActionButton) { }; lastLoadStartURL = ''; } + reset(); var iab; @@ -183,6 +197,9 @@ exports.defineManualTests = function (contentEl, createActionButton) { console.log('Use window.open() for url'); iab = window.open(url, target, params, callbacks); } else { + if (platformOpts) { + params += (params ? ',' : '') + platformOpts; + } iab = cordova.InAppBrowser.open(url, target, params, callbacks); } if (!iab) { @@ -260,11 +277,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { }; if (cssUrl) { iab.addEventListener('loadstop', function (event) { - iab.insertCSS({ file: cssUrl }, useCallback && callback); + iab.insertCSS({file: cssUrl}, useCallback && callback); }); } else { iab.addEventListener('loadstop', function (event) { - iab.insertCSS({ code: '#style-update-literal { \ndisplay: block !important; \n}' }, + iab.insertCSS({code: '#style-update-literal { \ndisplay: block !important; \n}'}, useCallback && callback); }); } @@ -274,7 +291,7 @@ exports.defineManualTests = function (contentEl, createActionButton) { var iab = doOpen(url, '_blank', 'location=yes'); if (jsUrl) { iab.addEventListener('loadstop', function (event) { - iab.executeScript({ file: jsUrl }, useCallback && function (results) { + iab.executeScript({file: jsUrl}, useCallback && function (results) { if (results && results.length === 0) { alert('Results verified'); // eslint-disable-line no-undef } else { @@ -286,11 +303,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { } else { iab.addEventListener('loadstop', function (event) { var code = '(function(){\n' + - ' var header = document.getElementById("header");\n' + - ' header.innerHTML = "Script literal successfully injected";\n' + - ' return "abc";\n' + - '})()'; - iab.executeScript({ code: code }, useCallback && function (results) { + ' var header = document.getElementById("header");\n' + + ' header.innerHTML = "Script literal successfully injected";\n' + + ' return "abc";\n' + + '})()'; + iab.executeScript({code: code}, useCallback && function (results) { if (results && results.length === 1 && results[0] === 'abc') { alert('Results verified'); // eslint-disable-line no-undef } else { @@ -301,10 +318,16 @@ exports.defineManualTests = function (contentEl, createActionButton) { }); } } + var hiddenwnd = null; - var loadlistener = function (event) { alert('background window loaded '); }; // eslint-disable-line no-undef + var loadlistener = function (event) { + alert('background window loaded '); + }; // eslint-disable-line no-undef function openHidden (url, startHidden) { var shopt = (startHidden) ? 'hidden=yes' : ''; + if (platformOpts) { + shopt += (shopt ? ',' : '') + platformOpts; + } hiddenwnd = cordova.InAppBrowser.open(url, 'random_string', shopt); if (!hiddenwnd) { alert('cordova.InAppBrowser.open returned ' + hiddenwnd); // eslint-disable-line no-undef @@ -312,11 +335,13 @@ exports.defineManualTests = function (contentEl, createActionButton) { } if (startHidden) hiddenwnd.addEventListener('loadstop', loadlistener); } + function showHidden () { if (hiddenwnd) { hiddenwnd.show(); } } + function closeHidden () { if (hiddenwnd) { hiddenwnd.removeEventListener('loadstop', loadlistener); @@ -333,6 +358,15 @@ exports.defineManualTests = function (contentEl, createActionButton) { '

User-Agent: ' + ''; + var platformOpts = ''; + var platform_info = ''; + if (isIos) { + platformOpts = 'usewkwebview=no'; + platform_info = '

Webview

' + + '

Use this button to toggle the Webview implementation.

' + + '
'; + } + var local_tests = '

Local URL

' + '
' + 'Expected result: opens successfully in CordovaWebView.' + @@ -471,11 +505,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { // see http://msdn.microsoft.com/en-us/library/windows/apps/hh465380.aspx#differences for details if (window.MSApp && window.MSApp.execUnsafeLocalFunction) { MSApp.execUnsafeLocalFunction(function () { - contentEl.innerHTML = info_div + local_tests + white_listed_tests + non_white_listed_tests + page_with_redirects_tests + pdf_url_tests + invalid_url_tests + + contentEl.innerHTML = info_div + platform_info + local_tests + white_listed_tests + non_white_listed_tests + page_with_redirects_tests + pdf_url_tests + invalid_url_tests + css_js_injection_tests + open_hidden_tests + clearing_cache_tests + video_tag_tests + local_with_anchor_tag_tests + hardwareback_tests; }); } else { - contentEl.innerHTML = info_div + local_tests + white_listed_tests + non_white_listed_tests + page_with_redirects_tests + pdf_url_tests + invalid_url_tests + + contentEl.innerHTML = info_div + platform_info + local_tests + white_listed_tests + non_white_listed_tests + page_with_redirects_tests + pdf_url_tests + invalid_url_tests + css_js_injection_tests + open_hidden_tests + clearing_cache_tests + video_tag_tests + local_with_anchor_tag_tests + hardwareback_tests; } @@ -490,6 +524,22 @@ exports.defineManualTests = function (contentEl, createActionButton) { var injectcss = isWindows ? basePath + 'inject.css' : 'inject.css'; var videohtml = basePath + 'video.html'; + if (isIos) { + createActionButton('Webview=UIWebView', function () { + var webviewOption = 'usewkwebview='; + var webviewToggle = document.getElementById('webviewToggle'); + var button = webviewToggle.getElementsByClassName('topcoat-button')[0]; + + if (platformOpts === webviewOption + 'yes') { + platformOpts = webviewOption + 'no'; + button.textContent = 'Webview=UIWebView'; + } else { + platformOpts = webviewOption + 'yes'; + button.textContent = 'Webview=WKWebView'; + } + }, 'webviewToggle'); + } + // Local createActionButton('target=Default', function () { doOpen(localhtml); @@ -679,17 +729,17 @@ exports.defineManualTests = function (contentEl, createActionButton) { doOpen('http://cordova.apache.org', '_blank', 'hardwareback=no'); }, 'openHardwareBackNo'); createActionButton('no hardwareback -> hardwareback=no -> no hardwareback', function () { - var ref = cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes'); + var ref = cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes' + (platformOpts ? ',' + platformOpts : '')); ref.addEventListener('loadstop', function () { ref.close(); }); ref.addEventListener('exit', function () { - var ref2 = cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes,hardwareback=no'); + var ref2 = cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes,hardwareback=no' + (platformOpts ? ',' + platformOpts : '')); ref2.addEventListener('loadstop', function () { ref2.close(); }); ref2.addEventListener('exit', function () { - cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes'); + cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes' + (platformOpts ? ',' + platformOpts : '')); }); }); }, 'openHardwareBackDefaultAfterNo'); From f8ad52894b3c732e448eb97a0881c674696a5f79 Mon Sep 17 00:00:00 2001 From: Jan Piotrowski Date: Mon, 1 Oct 2018 12:36:35 +0200 Subject: [PATCH 30/53] also accept terms for android sdk `android-27` --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 00def86..9a96bfc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -94,7 +94,7 @@ before_install: - node --version - if [[ "$PLATFORM" =~ android ]]; then gradle --version; fi - if [[ "$PLATFORM" =~ ios ]]; then npm install -g ios-deploy; fi -- if [[ "$PLATFORM" =~ android ]]; then echo y | android update sdk -u --filter android-22,android-23,android-24,android-25,android-26; +- if [[ "$PLATFORM" =~ android ]]; then echo y | android update sdk -u --filter android-22,android-23,android-24,android-25,android-26,android-27; fi - git clone https://github.com/apache/cordova-paramedic /tmp/paramedic && pushd /tmp/paramedic && npm install && popd From 0ed0bf543b00070f8c308c15a89744fa828e0bd5 Mon Sep 17 00:00:00 2001 From: Jan Piotrowski Date: Mon, 1 Oct 2018 12:46:54 +0200 Subject: [PATCH 31/53] reenable testing on all platforms was disabled in #277 --- .travis.yml | 87 ++++++++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9a96bfc..e854d35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,10 @@ matrix: os: linux language: node_js node_js: '4.2' - # TBD SKIP for now: - # - env: PLATFORM=browser-safari - # os: linux - # language: node_js - # node_js: '4.2' + - env: PLATFORM=browser-safari + os: linux + language: node_js + node_js: '4.2' - env: PLATFORM=browser-edge os: linux language: node_js @@ -33,15 +32,11 @@ matrix: osx_image: xcode7.3 language: node_js node_js: '4.2' - # TBD SKIP for now: - # - env: PLATFORM=ios-10.0 - # os: osx - # osx_image: xcode7.3 - # language: node_js - # node_js: '4.2' - # FUTURE TBD: - # - env: PLATFORM=ios-11.4 - # ... + - env: PLATFORM=ios-10.0 + os: osx + osx_image: xcode7.3 + language: node_js + node_js: '4.2' - env: PLATFORM=android-4.4 os: linux language: android @@ -52,40 +47,36 @@ matrix: - platform-tools - tools - build-tools-26.0.2 - # TBD SKIP for now: - # - env: PLATFORM=android-5.1 - # os: linux - # language: android - # jdk: oraclejdk8 - # android: - # components: - # - tools - # - platform-tools - # - tools - # - build-tools-26.0.2 - # - env: PLATFORM=android-6.0 - # os: linux - # language: android - # jdk: oraclejdk8 - # android: - # components: - # - tools - # - platform-tools - # - tools - # - build-tools-26.0.2 - # - env: PLATFORM=android-7.0 - # os: linux - # language: android - # jdk: oraclejdk8 - # android: - # components: - # - tools - # - platform-tools - # - tools - # - build-tools-26.0.2 - # FUTURE TBD: - # - env: PLATFORM=android-8.? - # ... + - env: PLATFORM=android-5.1 + os: linux + language: android + jdk: oraclejdk8 + android: + components: + - tools + - platform-tools + - tools + - build-tools-26.0.2 + - env: PLATFORM=android-6.0 + os: linux + language: android + jdk: oraclejdk8 + android: + components: + - tools + - platform-tools + - tools + - build-tools-26.0.2 + - env: PLATFORM=android-7.0 + os: linux + language: android + jdk: oraclejdk8 + android: + components: + - tools + - platform-tools + - tools + - build-tools-26.0.2 before_install: - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm From 228703a63b088cbb635aa58f8a1015a9d5d818ab Mon Sep 17 00:00:00 2001 From: wvengen Date: Fri, 13 Jul 2018 22:14:01 +0200 Subject: [PATCH 32/53] CB-14188: add beforeload event, catching navigation before it happens --- README.md | 21 ++++++++++++- src/android/InAppBrowser.java | 58 ++++++++++++++++++++++++++++++----- src/ios/CDVInAppBrowser.h | 8 ++++- src/ios/CDVInAppBrowser.m | 49 +++++++++++++++++++++++++++-- www/inappbrowser.js | 11 ++++++- 5 files changed, 133 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index eade58a..9aa1a43 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ instance, or the system browser. Android supports these additional options: - __hidden__: set to `yes` to create the browser and load the page, but not show it. The loadstop event fires when loading is complete. Omit or set to `no` (default) to have the browser open and load normally. + - __beforeload__: set to `yes` to enable the `beforeload` event to modify which pages are actually loaded in the browser. - __clearcache__: set to `yes` to have the browser's cookie cache cleared before the new window is opened - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened - __closebuttoncaption__: set to a string to use as the close button's caption instead of a X. Note that you need to localize this value yourself. @@ -137,6 +138,7 @@ instance, or the system browser. iOS supports these additional options: - __hidden__: set to `yes` to create the browser and load the page, but not show it. The loadstop event fires when loading is complete. Omit or set to `no` (default) to have the browser open and load normally. + - __beforeload__: set to `yes` to enable the `beforeload` event to modify which pages are actually loaded in the browser. - __clearcache__: set to `yes` to have the browser's cookie cache cleared before the new window is opened - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened - __closebuttoncolor__: set as a valid hex color string, for example: `#00ff00`, to change from the default __Done__ button's color. Only applicable if toolbar is not disabled. @@ -217,6 +219,7 @@ The object returned from a call to `cordova.InAppBrowser.open` when the target i - __loadstop__: event fires when the `InAppBrowser` finishes loading a URL. - __loaderror__: event fires when the `InAppBrowser` encounters an error when loading a URL. - __exit__: event fires when the `InAppBrowser` window is closed. + - __beforeload__: event fires when the `InAppBrowser` decides whether to load an URL or not (only with option `beforeload=yes`). - __callback__: the function that executes when the event fires. The function is passed an `InAppBrowserEvent` object as a parameter. @@ -230,7 +233,7 @@ function showHelp(url) { var target = "_blank"; - var options = "location=yes,hidden=yes"; + var options = "location=yes,hidden=yes,beforeload=yes"; inAppBrowserRef = cordova.InAppBrowser.open(url, target, options); @@ -240,6 +243,8 @@ function showHelp(url) { inAppBrowserRef.addEventListener('loaderror', loadErrorCallBack); + inAppBrowserRef.addEventListener('beforeload', beforeloadCallBack); + } function loadStartCallBack() { @@ -288,6 +293,20 @@ function executeScriptCallBack(params) { } +function beforeloadCallback(params, callback) { + + if (params.url.startsWith("http://www.example.com/")) { + + // Load this URL in the inAppBrowser. + callback(params.url); + } else { + + // The callback is not invoked, so the page will not be loaded. + $('#status-message').text("This browser only opens pages on http://www.example.com/"); + } + +} + ``` ### InAppBrowserEvent Properties diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 92ca3c1..93d9460 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -110,6 +110,7 @@ public class InAppBrowser extends CordovaPlugin { private static final String HIDE_URL = "hideurlbar"; private static final String FOOTER = "footer"; private static final String FOOTER_COLOR = "footercolor"; + private static final String BEFORELOAD = "beforeload"; private static final List customizableOptions = Arrays.asList(CLOSE_BUTTON_CAPTION, TOOLBAR_COLOR, NAVIGATION_COLOR, CLOSE_BUTTON_COLOR, FOOTER_COLOR); @@ -138,6 +139,7 @@ public class InAppBrowser extends CordovaPlugin { private boolean hideUrlBar = false; private boolean showFooter = false; private String footerColor = ""; + private boolean useBeforeload = false; private String[] allowedSchemes; /** @@ -246,6 +248,20 @@ public class InAppBrowser extends CordovaPlugin { else if (action.equals("close")) { closeDialog(); } + else if (action.equals("loadAfterBeforeload")) { + if (!useBeforeload) { + LOG.e(LOG_TAG, "unexpected loadAfterBeforeload called without feature beforeload=yes"); + } + final String url = args.getString(0); + this.cordova.getActivity().runOnUiThread(new Runnable() { + @SuppressLint("NewApi") + @Override + public void run() { + ((InAppBrowserClient)inAppWebView.getWebViewClient()).waitForBeforeload = false; + inAppWebView.loadUrl(url); + } + }); + } else if (action.equals("injectScriptCode")) { String jsWrapper = null; if (args.getBoolean(1)) { @@ -674,6 +690,10 @@ public class InAppBrowser extends CordovaPlugin { if (footerColorSet != null) { footerColor = footerColorSet; } + String beforeload = features.get(BEFORELOAD); + if (beforeload != null) { + useBeforeload = beforeload.equals("yes") ? true : false; + } } final CordovaWebView thatWebView = this.webView; @@ -924,7 +944,7 @@ public class InAppBrowser extends CordovaPlugin { } }); - WebViewClient client = new InAppBrowserClient(thatWebView, edittext); + WebViewClient client = new InAppBrowserClient(thatWebView, edittext, useBeforeload); inAppWebView.setWebViewClient(client); WebSettings settings = inAppWebView.getSettings(); settings.setJavaScriptEnabled(true); @@ -1085,6 +1105,8 @@ public class InAppBrowser extends CordovaPlugin { public class InAppBrowserClient extends WebViewClient { EditText edittext; CordovaWebView webView; + boolean useBeforeload; + boolean waitForBeforeload; /** * Constructor. @@ -1092,9 +1114,11 @@ public class InAppBrowser extends CordovaPlugin { * @param webView * @param mEditText */ - public InAppBrowserClient(CordovaWebView webView, EditText mEditText) { + public InAppBrowserClient(CordovaWebView webView, EditText mEditText, boolean useBeforeload) { this.webView = webView; this.edittext = mEditText; + this.useBeforeload = useBeforeload; + this.waitForBeforeload = useBeforeload; } /** @@ -1107,12 +1131,27 @@ public class InAppBrowser extends CordovaPlugin { */ @Override public boolean shouldOverrideUrlLoading(WebView webView, String url) { + boolean override = false; + + // On first URL change, initiate JS callback. Only after the beforeload event, continue. + if (this.waitForBeforeload) { + try { + JSONObject obj = new JSONObject(); + obj.put("type", "beforeload"); + obj.put("url", url); + sendUpdate(obj, true); + return true; + } catch (JSONException ex) { + LOG.e(LOG_TAG, "URI passed in has caused a JSON error."); + } + } + if (url.startsWith(WebView.SCHEME_TEL)) { try { Intent intent = new Intent(Intent.ACTION_DIAL); intent.setData(Uri.parse(url)); cordova.getActivity().startActivity(intent); - return true; + override = true; } catch (android.content.ActivityNotFoundException e) { LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString()); } @@ -1121,7 +1160,7 @@ public class InAppBrowser extends CordovaPlugin { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); cordova.getActivity().startActivity(intent); - return true; + override = true; } catch (android.content.ActivityNotFoundException e) { LOG.e(LOG_TAG, "Error with " + url + ": " + e.toString()); } @@ -1152,7 +1191,7 @@ public class InAppBrowser extends CordovaPlugin { intent.putExtra("address", address); intent.setType("vnd.android-dir/mms-sms"); cordova.getActivity().startActivity(intent); - return true; + override = true; } catch (android.content.ActivityNotFoundException e) { LOG.e(LOG_TAG, "Error sending sms " + url + ":" + e.toString()); } @@ -1173,7 +1212,7 @@ public class InAppBrowser extends CordovaPlugin { obj.put("type", "customscheme"); obj.put("url", url); sendUpdate(obj, true); - return true; + override = true; } catch (JSONException ex) { LOG.e(LOG_TAG, "Custom Scheme URI passed in has caused a JSON error."); } @@ -1182,7 +1221,10 @@ public class InAppBrowser extends CordovaPlugin { } } - return false; + if (this.useBeforeload) { + this.waitForBeforeload = true; + } + return override; } @@ -1304,4 +1346,4 @@ public class InAppBrowser extends CordovaPlugin { super.onReceivedHttpAuthRequest(view, handler, host, realm); } } -} \ No newline at end of file +} diff --git a/src/ios/CDVInAppBrowser.h b/src/ios/CDVInAppBrowser.h index 66066b9..e415f40 100644 --- a/src/ios/CDVInAppBrowser.h +++ b/src/ios/CDVInAppBrowser.h @@ -30,7 +30,11 @@ @class CDVInAppBrowserViewController; @interface CDVInAppBrowser : CDVPlugin { - UIWindow * tmpWindow; + UIWindow * tmpWindow; + + @private + BOOL _useBeforeload; + BOOL _waitForBeforeload; } @property (nonatomic, retain) CDVInAppBrowserViewController* inAppBrowserViewController; @@ -42,6 +46,7 @@ - (void)injectScriptCode:(CDVInvokedUrlCommand*)command; - (void)show:(CDVInvokedUrlCommand*)command; - (void)hide:(CDVInvokedUrlCommand*)command; +- (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command; @end @@ -70,6 +75,7 @@ @property (nonatomic, assign) BOOL suppressesincrementalrendering; @property (nonatomic, assign) BOOL hidden; @property (nonatomic, assign) BOOL disallowoverscroll; +@property (nonatomic, assign) BOOL beforeload; + (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index 0ca3feb..afb5d21 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -46,6 +46,8 @@ { _previousStatusBarStyle = -1; _callbackIdPattern = nil; + _useBeforeload = NO; + _waitForBeforeload = NO; } - (id)settingForKey:(NSString*)key @@ -209,6 +211,10 @@ self.inAppBrowserViewController.webView.suppressesIncrementalRendering = browserOptions.suppressesincrementalrendering; } + // use of beforeload event + _useBeforeload = browserOptions.beforeload; + _waitForBeforeload = browserOptions.beforeload; + [self.inAppBrowserViewController navigateTo:url]; if (!browserOptions.hidden) { [self show:nil]; @@ -304,6 +310,27 @@ } } +- (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command +{ + NSString* urlStr = [command argumentAtIndex:0]; + + if (!_useBeforeload) { + NSLog(@"unexpected loadAfterBeforeload called without feature beforeload=yes"); + } + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to invoke loadAfterBeforeload on IAB after it was closed."); + return; + } + if (urlStr == nil) { + NSLog(@"loadAfterBeforeload called with nil argument, ignoring."); + return; + } + + NSURL* url = [NSURL URLWithString:urlStr]; + _waitForBeforeload = NO; + [self.inAppBrowserViewController navigateTo:url]; +} + // This is a helper method for the inject{Script|Style}{Code|File} API calls, which // provides a consistent method for injecting JavaScript code into the document. // @@ -413,6 +440,7 @@ { NSURL* url = request.URL; BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + BOOL shouldStart = YES; // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute, // and the path, if present, should be a JSON-encoded value to pass to the callback. @@ -440,11 +468,22 @@ return NO; } } + + // When beforeload=yes, on first URL change, initiate JS callback. Only after the beforeload event, continue. + if (_waitForBeforeload && isTopLevelNavigation) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"beforeload", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + return NO; + } + //if is an app store link, let the system handle it, otherwise it fails to load it - else if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { + if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { [theWebView stopLoading]; [self openInSystem:url]; - return NO; + shouldStart = NO; } else if ((self.callbackId != nil) && isTopLevelNavigation) { // Send a loadstart event for each top-level navigation (includes redirects). @@ -455,7 +494,11 @@ [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; } - return YES; + if (_useBeforeload && isTopLevelNavigation) { + _waitForBeforeload = YES; + } + + return shouldStart; } - (void)webViewDidStartLoad:(UIWebView*)theWebView diff --git a/www/inappbrowser.js b/www/inappbrowser.js index 3619f17..7764765 100644 --- a/www/inappbrowser.js +++ b/www/inappbrowser.js @@ -33,6 +33,7 @@ function InAppBrowser () { this.channels = { + 'beforeload': channel.create('beforeload'), 'loadstart': channel.create('loadstart'), 'loadstop': channel.create('loadstop'), 'loaderror': channel.create('loaderror'), @@ -44,9 +45,17 @@ InAppBrowser.prototype = { _eventHandler: function (event) { if (event && (event.type in this.channels)) { - this.channels[event.type].fire(event); + if (event.type === 'beforeload') { + this.channels[event.type].fire(event, this._loadAfterBeforeload); + } else { + this.channels[event.type].fire(event); + } } }, + _loadAfterBeforeload: function (strUrl) { + strUrl = urlutil.makeAbsolute(strUrl); + exec(null, null, 'InAppBrowser', 'loadAfterBeforeload', [strUrl]); + }, close: function (eventname) { exec(null, null, 'InAppBrowser', 'close', []); }, From a0c267fe36be9a97c974f73b9d2160eca54b9828 Mon Sep 17 00:00:00 2001 From: Tim Brust Date: Tue, 2 Oct 2018 11:49:11 +0200 Subject: [PATCH 33/53] CB-12941: update typings (#267) ### Platforms affected n/a - development with TypeScript ### What does this PR do? Updates the typings to match the latest published version on DefinitelyTyped. See https://github.com/DefinitelyTyped/DefinitelyTyped/pull/17192 and https://stackoverflow.com/q/42095516/1902598 ### What testing has been done on this change? Project compiles again ;) ### Checklist - [x] [Reported an issue](http://cordova.apache.org/contribute/issues.html) in the JIRA database - [x] Commit message follows the format: "CB-3232: (android) Fix bug with resolving file paths", where CB-xxxx is the JIRA ID & "android" is the platform affected. - n/a: Added automated test coverage as appropriate for this change. fixes #300 --- types/index.d.ts | 168 +++++++---------------------------------------- 1 file changed, 25 insertions(+), 143 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index ea3d3ad..3bac670 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2,38 +2,13 @@ // Project: https://github.com/apache/cordova-plugin-inappbrowser // Definitions by: Microsoft Open Technologies Inc // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// +// // Copyright (c) Microsoft Open Technologies Inc // Licensed under the MIT license. +// TypeScript Version: 2.3 +type channel = "loadstart" | "loadstop" | "loaderror" | "exit"; interface Window { - /** - * Opens a URL in a new InAppBrowser instance, the current browser instance, or the system browser. - * @param url The URL to load. - * @param target The target in which to load the URL, an optional parameter that defaults to _self. - * @param options Options for the InAppBrowser. Optional, defaulting to: location=yes. - * The options string must not contain any blank space, and each feature's - * name/value pairs must be separated by a comma. Feature names are case insensitive. - */ - open(url: string, target?: "_self", options?: string): InAppBrowser; - /** - * Opens a URL in a new InAppBrowser instance, the current browser instance, or the system browser. - * @param url The URL to load. - * @param target The target in which to load the URL, an optional parameter that defaults to _self. - * @param options Options for the InAppBrowser. Optional, defaulting to: location=yes. - * The options string must not contain any blank space, and each feature's - * name/value pairs must be separated by a comma. Feature names are case insensitive. - */ - open(url: string, target?: "_blank", options?: string): InAppBrowser; - /** - * Opens a URL in a new InAppBrowser instance, the current browser instance, or the system browser. - * @param url The URL to load. - * @param target The target in which to load the URL, an optional parameter that defaults to _self. - * @param options Options for the InAppBrowser. Optional, defaulting to: location=yes. - * The options string must not contain any blank space, and each feature's - * name/value pairs must be separated by a comma. Feature names are case insensitive. - */ - open(url: string, target?: "_system", options?: string): InAppBrowser; /** * Opens a URL in a new InAppBrowser instance, the current browser instance, or the system browser. * @param url The URL to load. @@ -50,66 +25,21 @@ interface Window { * NOTE: The InAppBrowser window behaves like a standard web browser, and can't access Cordova APIs. */ interface InAppBrowser extends Window { - onloadstart: (type: InAppBrowserEvent) => void; - onloadstop: (type: InAppBrowserEvent) => void; - onloaderror: (type: InAppBrowserEvent) => void; - onexit: (type: InAppBrowserEvent) => void; + onloadstart(type: Event): void; + onloadstop(type: InAppBrowserEvent): void; + onloaderror(type: InAppBrowserEvent): void; + onexit(type: InAppBrowserEvent): void; // addEventListener overloads /** * Adds a listener for an event from the InAppBrowser. - * @param type the event to listen for - * loadstart: event fires when the InAppBrowser starts to load a URL. + * @param type loadstart: event fires when the InAppBrowser starts to load a URL. * loadstop: event fires when the InAppBrowser finishes loading a URL. * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. * exit: event fires when the InAppBrowser window is closed. * @param callback the function that executes when the event fires. The function is * passed an InAppBrowserEvent object as a parameter. */ - addEventListener(type: "loadstart", callback: (event: InAppBrowserEvent) => void): void; - /** - * Adds a listener for an event from the InAppBrowser. - * @param type the event to listen for - * loadstart: event fires when the InAppBrowser starts to load a URL. - * loadstop: event fires when the InAppBrowser finishes loading a URL. - * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. - * exit: event fires when the InAppBrowser window is closed. - * @param callback the function that executes when the event fires. The function is - * passed an InAppBrowserEvent object as a parameter. - */ - addEventListener(type: "loadstop", callback: (event: InAppBrowserEvent) => void): void; - /** - * Adds a listener for an event from the InAppBrowser. - * @param type the event to listen for - * loadstart: event fires when the InAppBrowser starts to load a URL. - * loadstop: event fires when the InAppBrowser finishes loading a URL. - * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. - * exit: event fires when the InAppBrowser window is closed. - * @param callback the function that executes when the event fires. The function is - * passed an InAppBrowserEvent object as a parameter. - */ - addEventListener(type: "loaderror", callback: (event: InAppBrowserEvent) => void): void; - /** - * Adds a listener for an event from the InAppBrowser. - * @param type the event to listen for - * loadstart: event fires when the InAppBrowser starts to load a URL. - * loadstop: event fires when the InAppBrowser finishes loading a URL. - * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. - * exit: event fires when the InAppBrowser window is closed. - * @param callback the function that executes when the event fires. The function is - * passed an InAppBrowserEvent object as a parameter. - */ - addEventListener(type: "exit", callback: (event: InAppBrowserEvent) => void): void; - /** - * Adds a listener for an event from the InAppBrowser. - * @param type the event to listen for - * loadstart: event fires when the InAppBrowser starts to load a URL. - * loadstop: event fires when the InAppBrowser finishes loading a URL. - * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. - * exit: event fires when the InAppBrowser window is closed. - * @param callback the function that executes when the event fires. The function is - * passed an Event object as a parameter. - */ - addEventListener(type: string, callback: (event: Event) => void): void; + addEventListener(type: channel, callback: InAppBrowserEventListenerOrEventListenerObject): void; // removeEventListener overloads /** * Removes a listener for an event from the InAppBrowser. @@ -121,51 +51,7 @@ interface InAppBrowser extends Window { * @param callback the function that executes when the event fires. The function is * passed an InAppBrowserEvent object as a parameter. */ - removeEventListener(type: "loadstart", callback: (event: InAppBrowserEvent) => void): void; - /** - * Removes a listener for an event from the InAppBrowser. - * @param type The event to stop listening for. - * loadstart: event fires when the InAppBrowser starts to load a URL. - * loadstop: event fires when the InAppBrowser finishes loading a URL. - * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. - * exit: event fires when the InAppBrowser window is closed. - * @param callback the function that executes when the event fires. The function is - * passed an InAppBrowserEvent object as a parameter. - */ - removeEventListener(type: "loadstop", callback: (event: InAppBrowserEvent) => void): void; - /** - * Removes a listener for an event from the InAppBrowser. - * @param type The event to stop listening for. - * loadstart: event fires when the InAppBrowser starts to load a URL. - * loadstop: event fires when the InAppBrowser finishes loading a URL. - * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. - * exit: event fires when the InAppBrowser window is closed. - * @param callback the function that executes when the event fires. The function is - * passed an InAppBrowserEvent object as a parameter. - */ - removeEventListener(type: "loaderror", callback: (event: InAppBrowserEvent) => void): void; - /** - * Removes a listener for an event from the InAppBrowser. - * @param type The event to stop listening for. - * loadstart: event fires when the InAppBrowser starts to load a URL. - * loadstop: event fires when the InAppBrowser finishes loading a URL. - * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. - * exit: event fires when the InAppBrowser window is closed. - * @param callback the function that executes when the event fires. The function is - * passed an InAppBrowserEvent object as a parameter. - */ - removeEventListener(type: "exit", callback: (event: InAppBrowserEvent) => void): void; - /** - * Removes a listener for an event from the InAppBrowser. - * @param type The event to stop listening for. - * loadstart: event fires when the InAppBrowser starts to load a URL. - * loadstop: event fires when the InAppBrowser finishes loading a URL. - * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. - * exit: event fires when the InAppBrowser window is closed. - * @param callback the function that executes when the event fires. The function is - * passed an Event object as a parameter. - */ - removeEventListener(type: string, callback: (event: Event) => void): void; + removeEventListener(type: channel, callback: InAppBrowserEventListenerOrEventListenerObject): void; /** Closes the InAppBrowser window. */ close(): void; /** Hides the InAppBrowser window. Calling this has no effect if the InAppBrowser was already hidden. */ @@ -184,29 +70,21 @@ interface InAppBrowser extends Window { * For multi-line scripts, this is the return value of the last statement, * or the last expression evaluated. */ - executeScript(script: { code: string }, callback: (result: any) => void): void; - /** - * Injects JavaScript code into the InAppBrowser window. - * @param script Details of the script to run, specifying either a file or code key. - * @param callback The function that executes after the JavaScript code is injected. - * If the injected script is of type code, the callback executes with - * a single parameter, which is the return value of the script, wrapped in an Array. - * For multi-line scripts, this is the return value of the last statement, - * or the last expression evaluated. - */ - executeScript(script: { file: string }, callback: (result: any) => void): void; + executeScript(script: { code: string } | { file: string }, callback: (result: any) => void): void; /** * Injects CSS into the InAppBrowser window. * @param css Details of the script to run, specifying either a file or code key. * @param callback The function that executes after the CSS is injected. */ - insertCSS(css: { code: string }, callback: () => void): void; - /** - * Injects CSS into the InAppBrowser window. - * @param css Details of the script to run, specifying either a file or code key. - * @param callback The function that executes after the CSS is injected. - */ - insertCSS(css: { file: string }, callback: () => void): void; + insertCSS(css: { code: string } | { file: string }, callback: () => void): void; +} + +type InAppBrowserEventListenerOrEventListenerObject = InAppBrowserEventListener | InAppBrowserEventListenerObject; + +type InAppBrowserEventListener = (evt: InAppBrowserEvent) => void; + +interface InAppBrowserEventListenerObject { + handleEvent(evt: InAppBrowserEvent): void; } interface InAppBrowserEvent extends Event { @@ -218,4 +96,8 @@ interface InAppBrowserEvent extends Event { code: number; /** the error message, only in the case of loaderror. */ message: string; -} \ No newline at end of file +} + +interface Cordova { + InAppBrowser: InAppBrowser; +} From 8165232a5acb1498d3a3b9f7bf8b8695052eee0b Mon Sep 17 00:00:00 2001 From: Jan Piotrowski Date: Wed, 3 Oct 2018 16:55:12 +0200 Subject: [PATCH 34/53] experimental commit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b7ff468..615f69a 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,8 @@ instance, or the system browser. Android supports these additional options: - __hidden__: set to `yes` to create the browser and load the page, but not show it. The loadstop event fires when loading is complete. Omit or set to `no` (default) to have the browser open and load normally. - - __clearcache__: set to `yes` to have the browser's cookie cache cleared before the new window is opened - - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened + - __clearcache__: set to `yes` to have the browser's cookie cache cleared before the new window is opened. + - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened. - __closebuttoncaption__: set to a string to use as the close button's caption instead of a X. Note that you need to localize this value yourself. - __closebuttoncolor__: set to a valid hex color string, for example: `#00ff00`, and it will change the close button color from default, regardless of being a text or default X. Only has effect if user has location set to `yes`. From 52e3534ca72566429e5f1e852bb92028c1fa24db Mon Sep 17 00:00:00 2001 From: Jan Piotrowski Date: Wed, 3 Oct 2018 17:34:12 +0200 Subject: [PATCH 35/53] update test configuration to xcode8.3 --- .travis.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2be2bec..d815b9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,17 +28,16 @@ matrix: os: linux language: node_js node_js: '4.2' + - env: PLATFORM=ios-9.3 + os: osx + osx_image: xcode8.3 + language: node_js + node_js: '4.2' - env: PLATFORM=ios-10.0 os: osx osx_image: xcode8.3 language: node_js node_js: '4.2' - # TBD SKIP for now: - # - env: PLATFORM=ios-10.0 - # os: osx - # osx_image: xcode7.3 - # language: node_js - # node_js: '4.2' # FUTURE TBD: # - env: PLATFORM=ios-11.4 # ... From d9cafcdb142b0cdb9fd14f410cbef840445440b9 Mon Sep 17 00:00:00 2001 From: Jan Piotrowski Date: Thu, 4 Oct 2018 01:16:19 +0200 Subject: [PATCH 36/53] remove JIRA link --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 9aa1a43..297732e 100644 --- a/README.md +++ b/README.md @@ -71,10 +71,6 @@ Although `window.open` is in the global scope, InAppBrowser is not available unt console.log("window.open works well"); } -Report issues with this plugin on the [Apache Cordova issue tracker](https://issues.apache.org/jira/issues/?jql=project%20%3D%20CB%20AND%20status%20in%20%28Open%2C%20%22In%20Progress%22%2C%20Reopened%29%20AND%20resolution%20%3D%20Unresolved%20AND%20component%20%3D%20%22cordova-plugin-inappbrowser%22%20ORDER%20BY%20priority%20DESC%2C%20summary%20ASC%2C%20updatedDate%20DESC) - - -## Reference ## Installation cordova plugin add cordova-plugin-inappbrowser From 86cc778c4ae70e61d21b4f97ef840b67d5fe12b4 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Fri, 5 Oct 2018 21:23:56 +0100 Subject: [PATCH 37/53] Fix tests so they auto-start when run via cordova-paramedic --- tests/tests.js | 247 +++++++++++++++++++++++-------------------------- 1 file changed, 118 insertions(+), 129 deletions(-) diff --git a/tests/tests.js b/tests/tests.js index 69ff46c..3ef8ca1 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -19,7 +19,7 @@ * */ -/* global MSApp, alert */ +/* global MSApp */ var cordova = require('cordova'); var isWindows = cordova.platformId === 'windows'; @@ -33,128 +33,126 @@ if (isWindows && navigator && navigator.notification && navigator.notification.a } exports.defineAutoTests = function () { - var createTests = function (platformOpts) { platformOpts = platformOpts || ''; - describe('cordova.InAppBrowser', function () { + describe('cordova.InAppBrowser', function () { - it('inappbrowser.spec.1 should exist', function () { - expect(cordova.InAppBrowser).toBeDefined(); - }); - - it('inappbrowser.spec.2 should contain open function', function () { - expect(cordova.InAppBrowser.open).toBeDefined(); - expect(cordova.InAppBrowser.open).toEqual(jasmine.any(Function)); - }); + it('inappbrowser.spec.1 should exist', function () { + expect(cordova.InAppBrowser).toBeDefined(); }); - describe('open method', function () { + it('inappbrowser.spec.2 should contain open function', function () { + expect(cordova.InAppBrowser.open).toBeDefined(); + expect(cordova.InAppBrowser.open).toEqual(jasmine.any(Function)); + }); + }); - if (cordova.platformId === 'osx') { - pending('Open method not fully supported on OSX.'); - return; - } + describe('open method', function () { - var iabInstance; - var originalTimeout; - var url = 'https://dist.apache.org/repos/dist/dev/cordova/'; - var badUrl = 'http://bad-uri/'; + if (cordova.platformId === 'osx') { + pending('Open method not fully supported on OSX.'); + return; + } - beforeEach(function () { - // increase timeout to ensure test url could be loaded within test time - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; - jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; + var iabInstance; + var originalTimeout; + var url = 'https://dist.apache.org/repos/dist/dev/cordova/'; + var badUrl = 'http://bad-uri/'; - iabInstance = null; - }); + beforeEach(function () { + // increase timeout to ensure test url could be loaded within test time + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; - afterEach(function (done) { - // restore original timeout - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + iabInstance = null; + }); - if (iabInstance !== null && iabInstance.close) { - iabInstance.close(); - } - iabInstance = null; - // add some extra time so that iab dialog is closed - setTimeout(done, 2000); - }); + afterEach(function (done) { + // restore original timeout + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; - function verifyEvent (evt, type) { - expect(evt).toBeDefined(); - expect(evt.type).toEqual(type); - // `exit` event does not have url field, browser returns null url for CORS requests - if (type !== 'exit' && !isBrowser) { - expect(evt.url).toEqual(url); - } - } - - function verifyLoadErrorEvent (evt) { - expect(evt).toBeDefined(); - expect(evt.type).toEqual('loaderror'); - expect(evt.url).toEqual(badUrl); - expect(evt.code).toEqual(jasmine.any(Number)); - expect(evt.message).toEqual(jasmine.any(String)); - } - - it('inappbrowser.spec.3 should return InAppBrowser instance with required methods', function () { - iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); - - expect(iabInstance).toBeDefined(); - - expect(iabInstance.addEventListener).toEqual(jasmine.any(Function)); - expect(iabInstance.removeEventListener).toEqual(jasmine.any(Function)); - expect(iabInstance.close).toEqual(jasmine.any(Function)); - expect(iabInstance.show).toEqual(jasmine.any(Function)); - expect(iabInstance.hide).toEqual(jasmine.any(Function)); - expect(iabInstance.executeScript).toEqual(jasmine.any(Function)); - expect(iabInstance.insertCSS).toEqual(jasmine.any(Function)); - }); - - it('inappbrowser.spec.4 should support loadstart and loadstop events', function (done) { - var onLoadStart = jasmine.createSpy('loadstart event callback').and.callFake(function (evt) { - verifyEvent(evt, 'loadstart'); - }); - - iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); - iabInstance.addEventListener('loadstart', onLoadStart); - iabInstance.addEventListener('loadstop', function (evt) { - verifyEvent(evt, 'loadstop'); - if (!isBrowser) { - // according to documentation, "loadstart" event is not supported on browser - // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 - expect(onLoadStart).toHaveBeenCalled(); - } - done(); - }); - }); - - it('inappbrowser.spec.5 should support exit event', function (done) { - iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); - iabInstance.addEventListener('exit', function (evt) { - verifyEvent(evt, 'exit'); - done(); - }); + if (iabInstance !== null && iabInstance.close) { iabInstance.close(); - iabInstance = null; + } + iabInstance = null; + // add some extra time so that iab dialog is closed + setTimeout(done, 2000); + }); + + function verifyEvent (evt, type) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual(type); + // `exit` event does not have url field, browser returns null url for CORS requests + if (type !== 'exit' && !isBrowser) { + expect(evt.url).toEqual(url); + } + } + + function verifyLoadErrorEvent (evt) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual('loaderror'); + expect(evt.url).toEqual(badUrl); + expect(evt.code).toEqual(jasmine.any(Number)); + expect(evt.message).toEqual(jasmine.any(String)); + } + + it('inappbrowser.spec.3 should return InAppBrowser instance with required methods', function () { + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + + expect(iabInstance).toBeDefined(); + + expect(iabInstance.addEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.removeEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.close).toEqual(jasmine.any(Function)); + expect(iabInstance.show).toEqual(jasmine.any(Function)); + expect(iabInstance.hide).toEqual(jasmine.any(Function)); + expect(iabInstance.executeScript).toEqual(jasmine.any(Function)); + expect(iabInstance.insertCSS).toEqual(jasmine.any(Function)); + }); + + it('inappbrowser.spec.4 should support loadstart and loadstop events', function (done) { + var onLoadStart = jasmine.createSpy('loadstart event callback').and.callFake(function (evt) { + verifyEvent(evt, 'loadstart'); }); - it('inappbrowser.spec.6 should support loaderror event', function (done) { - if (isBrowser) { - // according to documentation, "loaderror" event is not supported on browser + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('loadstart', onLoadStart); + iabInstance.addEventListener('loadstop', function (evt) { + verifyEvent(evt, 'loadstop'); + if (!isBrowser) { + // according to documentation, "loadstart" event is not supported on browser // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 - pending('Browser platform doesn\'t support loaderror event'); + expect(onLoadStart).toHaveBeenCalled(); } - iabInstance = cordova.InAppBrowser.open(badUrl, '_blank', platformOpts); - iabInstance.addEventListener('loaderror', function (evt) { - verifyLoadErrorEvent(evt); - done(); - }); + done(); }); }); - }; + it('inappbrowser.spec.5 should support exit event', function (done) { + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('exit', function (evt) { + verifyEvent(evt, 'exit'); + done(); + }); + iabInstance.close(); + iabInstance = null; + }); + + it('inappbrowser.spec.6 should support loaderror event', function (done) { + if (isBrowser) { + // according to documentation, "loaderror" event is not supported on browser + // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 + pending('Browser platform doesn\'t support loaderror event'); + } + iabInstance = cordova.InAppBrowser.open(badUrl, '_blank', platformOpts); + iabInstance.addEventListener('loaderror', function (evt) { + verifyLoadErrorEvent(evt); + done(); + }); + }); + }); + }; if (isIos) { createTests('usewkwebview=no'); createTests('usewkwebview=yes'); @@ -165,6 +163,15 @@ exports.defineAutoTests = function () { exports.defineManualTests = function (contentEl, createActionButton) { + var platformOpts = ''; + var platform_info = ''; + if (isIos) { + platformOpts = 'usewkwebview=no'; + platform_info = '

Webview

' + + '

Use this button to toggle the Webview implementation.

' + + '
'; + } + function doOpen (url, target, params, numExpectedRedirects, useWindowOpen) { numExpectedRedirects = numExpectedRedirects || 0; useWindowOpen = useWindowOpen || false; @@ -173,7 +180,6 @@ exports.defineManualTests = function (contentEl, createActionButton) { var counts; var lastLoadStartURL; var wasReset = false; - function reset () { counts = { 'loaderror': 0, @@ -183,7 +189,6 @@ exports.defineManualTests = function (contentEl, createActionButton) { }; lastLoadStartURL = ''; } - reset(); var iab; @@ -277,11 +282,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { }; if (cssUrl) { iab.addEventListener('loadstop', function (event) { - iab.insertCSS({file: cssUrl}, useCallback && callback); + iab.insertCSS({ file: cssUrl }, useCallback && callback); }); } else { iab.addEventListener('loadstop', function (event) { - iab.insertCSS({code: '#style-update-literal { \ndisplay: block !important; \n}'}, + iab.insertCSS({ code: '#style-update-literal { \ndisplay: block !important; \n}' }, useCallback && callback); }); } @@ -291,7 +296,7 @@ exports.defineManualTests = function (contentEl, createActionButton) { var iab = doOpen(url, '_blank', 'location=yes'); if (jsUrl) { iab.addEventListener('loadstop', function (event) { - iab.executeScript({file: jsUrl}, useCallback && function (results) { + iab.executeScript({ file: jsUrl }, useCallback && function (results) { if (results && results.length === 0) { alert('Results verified'); // eslint-disable-line no-undef } else { @@ -303,11 +308,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { } else { iab.addEventListener('loadstop', function (event) { var code = '(function(){\n' + - ' var header = document.getElementById("header");\n' + - ' header.innerHTML = "Script literal successfully injected";\n' + - ' return "abc";\n' + - '})()'; - iab.executeScript({code: code}, useCallback && function (results) { + ' var header = document.getElementById("header");\n' + + ' header.innerHTML = "Script literal successfully injected";\n' + + ' return "abc";\n' + + '})()'; + iab.executeScript({ code: code }, useCallback && function (results) { if (results && results.length === 1 && results[0] === 'abc') { alert('Results verified'); // eslint-disable-line no-undef } else { @@ -318,11 +323,8 @@ exports.defineManualTests = function (contentEl, createActionButton) { }); } } - var hiddenwnd = null; - var loadlistener = function (event) { - alert('background window loaded '); - }; // eslint-disable-line no-undef + var loadlistener = function (event) { alert('background window loaded '); }; // eslint-disable-line no-undef function openHidden (url, startHidden) { var shopt = (startHidden) ? 'hidden=yes' : ''; if (platformOpts) { @@ -335,13 +337,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { } if (startHidden) hiddenwnd.addEventListener('loadstop', loadlistener); } - function showHidden () { if (hiddenwnd) { hiddenwnd.show(); } } - function closeHidden () { if (hiddenwnd) { hiddenwnd.removeEventListener('loadstop', loadlistener); @@ -358,15 +358,6 @@ exports.defineManualTests = function (contentEl, createActionButton) { '

User-Agent: ' + ''; - var platformOpts = ''; - var platform_info = ''; - if (isIos) { - platformOpts = 'usewkwebview=no'; - platform_info = '

Webview

' + - '

Use this button to toggle the Webview implementation.

' + - '
'; - } - var local_tests = '

Local URL

' + '
' + 'Expected result: opens successfully in CordovaWebView.' + @@ -523,13 +514,11 @@ exports.defineManualTests = function (contentEl, createActionButton) { var injectjs = isWindows ? basePath + 'inject.js' : 'inject.js'; var injectcss = isWindows ? basePath + 'inject.css' : 'inject.css'; var videohtml = basePath + 'video.html'; - if (isIos) { createActionButton('Webview=UIWebView', function () { var webviewOption = 'usewkwebview='; var webviewToggle = document.getElementById('webviewToggle'); var button = webviewToggle.getElementsByClassName('topcoat-button')[0]; - if (platformOpts === webviewOption + 'yes') { platformOpts = webviewOption + 'no'; button.textContent = 'Webview=UIWebView'; From 7f5fa2a28f2af8c208f1cc9d59eefb40337d9558 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 9 Oct 2018 10:51:06 +0100 Subject: [PATCH 38/53] Add plugin to fix reporting of cordova-paramedic results when using WKWebView plugin. See https://github.com/apache/cordova-paramedic/issues/52 --- tests/plugin.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/plugin.xml b/tests/plugin.xml index 5bf5600..3afbb95 100644 --- a/tests/plugin.xml +++ b/tests/plugin.xml @@ -32,6 +32,7 @@ + From c41f5b51317e0070b3b163f5ccde52bc20ab0651 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 9 Oct 2018 11:31:26 +0100 Subject: [PATCH 39/53] Fix merge error --- src/ios/CDVUIInAppBrowser.m | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ios/CDVUIInAppBrowser.m b/src/ios/CDVUIInAppBrowser.m index ebd6607..51f8122 100644 --- a/src/ios/CDVUIInAppBrowser.m +++ b/src/ios/CDVUIInAppBrowser.m @@ -507,6 +507,7 @@ static CDVUIInAppBrowser* instance = nil; } return shouldStart; +} - (void)webViewDidStartLoad:(UIWebView*)theWebView { From 61014dd877e6c6edf1e2eb9b2068101af50f4e32 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 9 Oct 2018 11:48:15 +0100 Subject: [PATCH 40/53] Fix indentation of tests.js for eslint --- tests/tests.js | 200 ++++++++++++++++++++++++------------------------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/tests/tests.js b/tests/tests.js index 3ef8ca1..04b6a20 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -36,122 +36,122 @@ exports.defineAutoTests = function () { var createTests = function (platformOpts) { platformOpts = platformOpts || ''; - describe('cordova.InAppBrowser', function () { + describe('cordova.InAppBrowser', function () { - it('inappbrowser.spec.1 should exist', function () { - expect(cordova.InAppBrowser).toBeDefined(); - }); - - it('inappbrowser.spec.2 should contain open function', function () { - expect(cordova.InAppBrowser.open).toBeDefined(); - expect(cordova.InAppBrowser.open).toEqual(jasmine.any(Function)); - }); - }); - - describe('open method', function () { - - if (cordova.platformId === 'osx') { - pending('Open method not fully supported on OSX.'); - return; - } - - var iabInstance; - var originalTimeout; - var url = 'https://dist.apache.org/repos/dist/dev/cordova/'; - var badUrl = 'http://bad-uri/'; - - beforeEach(function () { - // increase timeout to ensure test url could be loaded within test time - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; - jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; - - iabInstance = null; - }); - - afterEach(function (done) { - // restore original timeout - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; - - if (iabInstance !== null && iabInstance.close) { - iabInstance.close(); - } - iabInstance = null; - // add some extra time so that iab dialog is closed - setTimeout(done, 2000); - }); - - function verifyEvent (evt, type) { - expect(evt).toBeDefined(); - expect(evt.type).toEqual(type); - // `exit` event does not have url field, browser returns null url for CORS requests - if (type !== 'exit' && !isBrowser) { - expect(evt.url).toEqual(url); - } - } - - function verifyLoadErrorEvent (evt) { - expect(evt).toBeDefined(); - expect(evt.type).toEqual('loaderror'); - expect(evt.url).toEqual(badUrl); - expect(evt.code).toEqual(jasmine.any(Number)); - expect(evt.message).toEqual(jasmine.any(String)); - } - - it('inappbrowser.spec.3 should return InAppBrowser instance with required methods', function () { - iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); - - expect(iabInstance).toBeDefined(); - - expect(iabInstance.addEventListener).toEqual(jasmine.any(Function)); - expect(iabInstance.removeEventListener).toEqual(jasmine.any(Function)); - expect(iabInstance.close).toEqual(jasmine.any(Function)); - expect(iabInstance.show).toEqual(jasmine.any(Function)); - expect(iabInstance.hide).toEqual(jasmine.any(Function)); - expect(iabInstance.executeScript).toEqual(jasmine.any(Function)); - expect(iabInstance.insertCSS).toEqual(jasmine.any(Function)); - }); - - it('inappbrowser.spec.4 should support loadstart and loadstop events', function (done) { - var onLoadStart = jasmine.createSpy('loadstart event callback').and.callFake(function (evt) { - verifyEvent(evt, 'loadstart'); + it('inappbrowser.spec.1 should exist', function () { + expect(cordova.InAppBrowser).toBeDefined(); }); + it('inappbrowser.spec.2 should contain open function', function () { + expect(cordova.InAppBrowser.open).toBeDefined(); + expect(cordova.InAppBrowser.open).toEqual(jasmine.any(Function)); + }); + }); + + describe('open method', function () { + + if (cordova.platformId === 'osx') { + pending('Open method not fully supported on OSX.'); + return; + } + + var iabInstance; + var originalTimeout; + var url = 'https://dist.apache.org/repos/dist/dev/cordova/'; + var badUrl = 'http://bad-uri/'; + + beforeEach(function () { + // increase timeout to ensure test url could be loaded within test time + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; + + iabInstance = null; + }); + + afterEach(function (done) { + // restore original timeout + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + + if (iabInstance !== null && iabInstance.close) { + iabInstance.close(); + } + iabInstance = null; + // add some extra time so that iab dialog is closed + setTimeout(done, 2000); + }); + + function verifyEvent (evt, type) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual(type); + // `exit` event does not have url field, browser returns null url for CORS requests + if (type !== 'exit' && !isBrowser) { + expect(evt.url).toEqual(url); + } + } + + function verifyLoadErrorEvent (evt) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual('loaderror'); + expect(evt.url).toEqual(badUrl); + expect(evt.code).toEqual(jasmine.any(Number)); + expect(evt.message).toEqual(jasmine.any(String)); + } + + it('inappbrowser.spec.3 should return InAppBrowser instance with required methods', function () { iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); - iabInstance.addEventListener('loadstart', onLoadStart); - iabInstance.addEventListener('loadstop', function (evt) { - verifyEvent(evt, 'loadstop'); - if (!isBrowser) { + + expect(iabInstance).toBeDefined(); + + expect(iabInstance.addEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.removeEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.close).toEqual(jasmine.any(Function)); + expect(iabInstance.show).toEqual(jasmine.any(Function)); + expect(iabInstance.hide).toEqual(jasmine.any(Function)); + expect(iabInstance.executeScript).toEqual(jasmine.any(Function)); + expect(iabInstance.insertCSS).toEqual(jasmine.any(Function)); + }); + + it('inappbrowser.spec.4 should support loadstart and loadstop events', function (done) { + var onLoadStart = jasmine.createSpy('loadstart event callback').and.callFake(function (evt) { + verifyEvent(evt, 'loadstart'); + }); + + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('loadstart', onLoadStart); + iabInstance.addEventListener('loadstop', function (evt) { + verifyEvent(evt, 'loadstop'); + if (!isBrowser) { // according to documentation, "loadstart" event is not supported on browser // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 - expect(onLoadStart).toHaveBeenCalled(); - } - done(); + expect(onLoadStart).toHaveBeenCalled(); + } + done(); + }); }); - }); - it('inappbrowser.spec.5 should support exit event', function (done) { + it('inappbrowser.spec.5 should support exit event', function (done) { iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); - iabInstance.addEventListener('exit', function (evt) { - verifyEvent(evt, 'exit'); - done(); + iabInstance.addEventListener('exit', function (evt) { + verifyEvent(evt, 'exit'); + done(); + }); + iabInstance.close(); + iabInstance = null; }); - iabInstance.close(); - iabInstance = null; - }); - it('inappbrowser.spec.6 should support loaderror event', function (done) { - if (isBrowser) { + it('inappbrowser.spec.6 should support loaderror event', function (done) { + if (isBrowser) { // according to documentation, "loaderror" event is not supported on browser // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 - pending('Browser platform doesn\'t support loaderror event'); - } + pending('Browser platform doesn\'t support loaderror event'); + } iabInstance = cordova.InAppBrowser.open(badUrl, '_blank', platformOpts); - iabInstance.addEventListener('loaderror', function (evt) { - verifyLoadErrorEvent(evt); - done(); + iabInstance.addEventListener('loaderror', function (evt) { + verifyLoadErrorEvent(evt); + done(); + }); }); }); - }); }; if (isIos) { createTests('usewkwebview=no'); From 82482158c5fff33a082ec8524384d605f0b17f13 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 9 Oct 2018 11:57:40 +0100 Subject: [PATCH 41/53] Comment out iOS 9 target for Travis tests since it's not supported by the WKWebView implementation --- .travis.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index c7b01fb..cfe39b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,11 +27,12 @@ matrix: os: linux language: node_js node_js: '4.2' - - env: PLATFORM=ios-9.3 - os: osx - osx_image: xcode8.3 - language: node_js - node_js: '4.2' +# iOS 9 not supported by iOS WKWebView implementation +# - env: PLATFORM=ios-9.3 +# os: osx +# osx_image: xcode8.3 +# language: node_js +# node_js: '4.2' - env: PLATFORM=ios-10.0 os: osx osx_image: xcode8.3 From c24bb4696536212898f2f6bd175143f8b92a6346 Mon Sep 17 00:00:00 2001 From: Jesse MacFadyen Date: Mon, 29 Oct 2018 13:43:37 -0700 Subject: [PATCH 42/53] This lets the default window layering do it's thing. Fixes #334. Fixes #314. --- src/ios/CDVUIInAppBrowser.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ios/CDVUIInAppBrowser.m b/src/ios/CDVUIInAppBrowser.m index 51f8122..501987a 100644 --- a/src/ios/CDVUIInAppBrowser.m +++ b/src/ios/CDVUIInAppBrowser.m @@ -258,8 +258,6 @@ static CDVUIInAppBrowser* instance = nil; } UIViewController *tmpController = [[UIViewController alloc] init]; [tmpWindow setRootViewController:tmpController]; - double baseWindowLevel = [UIApplication sharedApplication].keyWindow.windowLevel; - [tmpWindow setWindowLevel:baseWindowLevel+1]; [tmpWindow makeKeyAndVisible]; [tmpController presentViewController:nav animated:YES completion:nil]; From 3eadde65ab0bcd17cc2cd0231b1d8a3f68bbd5b8 Mon Sep 17 00:00:00 2001 From: Jan Piotrowski Date: Tue, 6 Nov 2018 19:53:54 +0100 Subject: [PATCH 43/53] remove unwanted information in README table (#308) remove unwanted information in README table --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 3ec2f3b..d93d36c 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,6 @@ description: Open an in-app browser window. |AppVeyor|Travis CI| |:-:|:-:| |[![Build status](https://ci.appveyor.com/api/projects/status/github/apache/cordova-plugin-inappbrowser?branch=master)](https://ci.appveyor.com/project/ApacheSoftwareFoundation/cordova-plugin-inappbrowser)|[![Build Status](https://travis-ci.org/apache/cordova-plugin-inappbrowser.svg?branch=master)](https://travis-ci.org/apache/cordova-plugin-inappbrowser)| -|npm install|npm install| -|eslint|eslint| -||browser (chrome, firefox, edge)| -||iOS (9.3) on Xcode 7.3| -||Android (4.4)| # cordova-plugin-inappbrowser From 978b14759824a024b85e6f7a10c97418a11eab34 Mon Sep 17 00:00:00 2001 From: Jonathan Li Date: Mon, 19 Nov 2018 12:28:25 -0500 Subject: [PATCH 44/53] Fix iOS CDVWKInAppBrowser evaluateJavascript method randomly gets blocked on ios 12 (#341) ### Platforms affected iOS ### What does this PR do? fix issue[ #340](https://github.com/apache/cordova-plugin-inappbrowser/issues/340) iOS CDVWKInAppBrowser evaluateJavascript method randomly gets blocked on iOS 12 ### What testing has been done on this change? manual testing ### Checklist - [x] [Reported an issue](http://cordova.apache.org/contribute/issues.html) in the JIRA database - [x] Commit message follows the format: "CB-3232: (android) Fix bug with resolving file paths", where CB-xxxx is the JIRA ID & "android" is the platform affected. - [ ] Added automated test coverage as appropriate for this change. --- src/ios/CDVWKInAppBrowser.m | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m index 9b515dd..a952114 100644 --- a/src/ios/CDVWKInAppBrowser.m +++ b/src/ios/CDVWKInAppBrowser.m @@ -399,30 +399,17 @@ static CDVWKInAppBrowser* instance = nil; //Synchronus helper for javascript evaluation - -- (NSString *)evaluateJavaScript:(NSString *)script { - __block NSString *resultString = nil; - __block BOOL finished = NO; +- (void)evaluateJavaScript:(NSString *)script { __block NSString* _script = script; - [self.inAppBrowserViewController.webView evaluateJavaScript:script completionHandler:^(id result, NSError *error) { if (error == nil) { if (result != nil) { - resultString = result; - NSLog(@"%@", resultString); + NSLog(@"%@", result); } } else { NSLog(@"evaluateJavaScript error : %@ : %@", error.localizedDescription, _script); } - finished = YES; }]; - - while (!finished) - { - [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; - } - - return resultString; } - (void)injectScriptCode:(CDVInvokedUrlCommand*)command From 3b82c160d953d421adab81480eae06f4f936ce56 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 20 Nov 2018 16:12:29 +0000 Subject: [PATCH 45/53] Fix crashes when using WKWebView implementation on iOS 9. (#337) Fixes #323. Fixes #324. --- README.md | 6 +++--- src/ios/CDVWKInAppBrowser.m | 16 +++++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d93d36c..503dd83 100644 --- a/README.md +++ b/README.md @@ -142,9 +142,9 @@ instance, or the system browser. - __toolbar__: set to `yes` or `no` to turn the toolbar on or off for the InAppBrowser (defaults to `yes`) - __toolbarcolor__: set as a valid hex color string, for example: `#00ff00`, to change from the default color of the toolbar. Only applicable if toolbar is not disabled. - __toolbartranslucent__: set to `yes` or `no` to make the toolbar translucent(semi-transparent) (defaults to `yes`). Only applicable if toolbar is not disabled. - - __enableViewportScale__: Set to `yes` or `no` to prevent viewport scaling through a meta tag (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`). - - __mediaPlaybackRequiresUserAction__: Set to `yes` to prevent HTML5 audio or video from autoplaying (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`). - - __allowInlineMediaPlayback__: Set to `yes` or `no` to allow in-line HTML5 media playback, displaying within the browser window rather than a device-specific playback interface. The HTML's `video` element must also include the `webkit-playsinline` attribute (defaults to `no`) Only applicable to UIWebView (`usewkwebview=no`). + - __enableViewportScale__: Set to `yes` or `no` to prevent viewport scaling through a meta tag (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`) and WKWebView (`usewkwebview=yes`) on iOS 10+. + - __mediaPlaybackRequiresUserAction__: Set to `yes` to prevent HTML5 audio or video from autoplaying (defaults to `no`). Applicable to UIWebView (`usewkwebview=no`) and WKWebView (`usewkwebview=yes`). + - __allowInlineMediaPlayback__: Set to `yes` or `no` to allow in-line HTML5 media playback, displaying within the browser window rather than a device-specific playback interface. The HTML's `video` element must also include the `webkit-playsinline` attribute (defaults to `no`). Applicable to UIWebView (`usewkwebview=no`) and WKWebView (`usewkwebview=yes`). - __keyboardDisplayRequiresUserAction__: Set to `yes` or `no` to open the keyboard when form elements receive focus via JavaScript's `focus()` call (defaults to `yes`). Only applicable to UIWebView (`usewkwebview=no`). - __suppressesIncrementalRendering__: Set to `yes` or `no` to wait until all new view content is received before being rendered (defaults to `no`). Only applicable to UIWebView (`usewkwebview=no`). - __presentationstyle__: Set to `pagesheet`, `formsheet` or `fullscreen` to set the [presentation style](http://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/modalPresentationStyle) (defaults to `fullscreen`). diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m index a952114..b7c2f3f 100644 --- a/src/ios/CDVWKInAppBrowser.m +++ b/src/ios/CDVWKInAppBrowser.m @@ -651,14 +651,20 @@ BOOL isExiting = FALSE; [configuration.userContentController addScriptMessageHandler:self name:IAB_BRIDGE_NAME]; //WKWebView options - configuration.ignoresViewportScaleLimits = _browserOptions.enableviewportscale; configuration.allowsInlineMediaPlayback = _browserOptions.allowinlinemediaplayback; - if(_browserOptions.mediaplaybackrequiresuseraction == YES){ - configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; - }else{ - configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + if (IsAtLeastiOSVersion(@"10.0")) { + configuration.ignoresViewportScaleLimits = _browserOptions.enableviewportscale; + if(_browserOptions.mediaplaybackrequiresuseraction == YES){ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + }else{ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } + }else{ // iOS 9 + configuration.mediaPlaybackRequiresUserAction = _browserOptions.mediaplaybackrequiresuseraction; } + + self.webView = [[WKWebView alloc] initWithFrame:webViewBounds configuration:configuration]; [self.view addSubview:self.webView]; From 0fd43ae64438a00bc0289aa2f7373bbff0c469fa Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Wed, 21 Nov 2018 18:04:04 +0000 Subject: [PATCH 46/53] Fixes loadAfterBeforeload on iOS. Resolves #349. (#350) For both UIWebView and WKWebView implementations on iOS. ### Platforms affected iOS ### What does this PR do? Fixes `beforeload` event (introduced by #276) for iOS ### What testing has been done on this change? Tested both allow & deny loading of URL with both iOS implementations in [test container app](https://github.com/dpa99c/cordova-plugin-inappbrowser-wkwebview-test) (ignore its README). - To test with UIWebView use options: `beforeload=yes,usewkwebview=no` - To test with WKWebView use options: `beforeload=yes,usewkwebview=yes` ### Checklist - [x] [Reported an issue](http://cordova.apache.org/contribute/issues.html) in the JIRA database - [x] Commit message follows the format: "CB-3232: (android) Fix bug with resolving file paths", where CB-xxxx is the JIRA ID & "android" is the platform affected. - [x] Added automated test coverage as appropriate for this change. closes #349 --- src/ios/CDVInAppBrowser.h | 1 + src/ios/CDVInAppBrowser.m | 9 ++++++ src/ios/CDVWKInAppBrowser.h | 4 +++ src/ios/CDVWKInAppBrowser.m | 64 +++++++++++++++++++++++++++++++------ 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/ios/CDVInAppBrowser.h b/src/ios/CDVInAppBrowser.h index d40e92b..2ca6704 100644 --- a/src/ios/CDVInAppBrowser.h +++ b/src/ios/CDVInAppBrowser.h @@ -30,6 +30,7 @@ - (void)injectScriptCode:(CDVInvokedUrlCommand*)command; - (void)show:(CDVInvokedUrlCommand*)command; - (void)hide:(CDVInvokedUrlCommand*)command; +- (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command; @end diff --git a/src/ios/CDVInAppBrowser.m b/src/ios/CDVInAppBrowser.m index ad30b23..5b7dc0e 100644 --- a/src/ios/CDVInAppBrowser.m +++ b/src/ios/CDVInAppBrowser.m @@ -121,5 +121,14 @@ } } +- (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command +{ + if(self.usewkwebview){ + [[CDVWKInAppBrowser getInstance] loadAfterBeforeload:command]; + }else{ + [[CDVUIInAppBrowser getInstance] loadAfterBeforeload:command]; + } +} + @end diff --git a/src/ios/CDVWKInAppBrowser.h b/src/ios/CDVWKInAppBrowser.h index 4de7824..8b5925e 100644 --- a/src/ios/CDVWKInAppBrowser.h +++ b/src/ios/CDVWKInAppBrowser.h @@ -27,6 +27,9 @@ @class CDVWKInAppBrowserViewController; @interface CDVWKInAppBrowser : CDVPlugin { + @private + BOOL _useBeforeload; + BOOL _waitForBeforeload; } @property (nonatomic, retain) CDVWKInAppBrowser* instance; @@ -40,6 +43,7 @@ - (void)injectScriptCode:(CDVInvokedUrlCommand*)command; - (void)show:(CDVInvokedUrlCommand*)command; - (void)hide:(CDVInvokedUrlCommand*)command; +- (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command; @end diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m index b7c2f3f..6c1b51a 100644 --- a/src/ios/CDVWKInAppBrowser.m +++ b/src/ios/CDVWKInAppBrowser.m @@ -60,6 +60,8 @@ static CDVWKInAppBrowser* instance = nil; instance = self; _previousStatusBarStyle = -1; _callbackIdPattern = nil; + _useBeforeload = NO; + _waitForBeforeload = NO; } - (id)settingForKey:(NSString*)key @@ -263,6 +265,9 @@ static CDVWKInAppBrowser* instance = nil; } } + // use of beforeload event + _useBeforeload = browserOptions.beforeload; + _waitForBeforeload = browserOptions.beforeload; [self.inAppBrowserViewController navigateTo:url]; [self show:nil withNoAnimate:browserOptions.hidden]; @@ -370,6 +375,27 @@ static CDVWKInAppBrowser* instance = nil; [[UIApplication sharedApplication] openURL:url]; } +- (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command +{ + NSString* urlStr = [command argumentAtIndex:0]; + + if (!_useBeforeload) { + NSLog(@"unexpected loadAfterBeforeload called without feature beforeload=yes"); + } + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to invoke loadAfterBeforeload on IAB after it was closed."); + return; + } + if (urlStr == nil) { + NSLog(@"loadAfterBeforeload called with nil argument, ignoring."); + return; + } + + NSURL* url = [NSURL URLWithString:urlStr]; + _waitForBeforeload = NO; + [self.inAppBrowserViewController navigateTo:url]; +} + // This is a helper method for the inject{Script|Style}{Code|File} API calls, which // provides a consistent method for injecting JavaScript code into the document. // @@ -480,16 +506,29 @@ static CDVWKInAppBrowser* instance = nil; * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no * other code execution is possible. */ -- (BOOL)webView:(WKWebView*)theWebView decidePolicyForNavigationAction:(NSURLRequest*)request -{ - NSURL* url = request.URL; - BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; +- (void)webView:(WKWebView *)theWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + + NSURL* url = navigationAction.request.URL; + NSURL* mainDocumentURL = navigationAction.request.mainDocumentURL; + BOOL isTopLevelNavigation = [url isEqual:mainDocumentURL]; + BOOL shouldStart = YES; + + // When beforeload=yes, on first URL change, initiate JS callback. Only after the beforeload event, continue. + if (_waitForBeforeload && isTopLevelNavigation) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"beforeload", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } //if is an app store link, let the system handle it, otherwise it fails to load it if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { [theWebView stopLoading]; [self openInSystem:url]; - return NO; + shouldStart = NO; } else if ((self.callbackId != nil) && isTopLevelNavigation) { // Send a loadstart event for each top-level navigation (includes redirects). @@ -499,8 +538,16 @@ static CDVWKInAppBrowser* instance = nil; [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; } + + if (_useBeforeload && isTopLevelNavigation) { + _waitForBeforeload = YES; + } - return YES; + if(shouldStart){ + decisionHandler(WKNavigationActionPolicyAllow); + }else{ + decisionHandler(WKNavigationActionPolicyCancel); + } } #pragma mark WKScriptMessageHandler delegate @@ -1072,10 +1119,7 @@ BOOL isExiting = FALSE; self.currentURL = url; } - [self.navigationDelegate webView:theWebView decidePolicyForNavigationAction:navigationAction.request]; - - decisionHandler(WKNavigationActionPolicyAllow); - + [self.navigationDelegate webView:theWebView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler]; } - (void)webView:(WKWebView *)theWebView didFinishNavigation:(WKNavigation *)navigation From c54d10052a42d5e68c76b53991c287a19cdce12c Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Thu, 13 Dec 2018 16:21:45 +0000 Subject: [PATCH 47/53] (iOS & Android) Add postMessage API support (#362) ### Platforms affected Android iOS (both UIWebView & WKWebView implementations) ### What does this PR do? Adds support for [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) enabling pages loaded into the InappBrowser to post messages back to the parent Webview of the Cordova app. For example, sending event messages associated with UI interactions such as button clicks from the wrapped page back to the parent app Webview. ### What testing has been done on this change? Automated tests have been extended to cover the `message` event. ### Checklist - [x ] Commit message follows the format: "GH-3232: (android) Fix bug with resolving file paths", where CB-xxxx is the JIRA ID & "android" is the platform affected. - [ x] Added automated test coverage as appropriate for this change. --- README.md | 24 ++++++++++++++++++-- src/android/InAppBrowser.java | 23 +++++++++++++++++++ src/ios/CDVUIInAppBrowser.m | 30 ++++++++++++++++++++++--- src/ios/CDVWKInAppBrowser.m | 42 +++++++++++++++++++++++------------ tests/tests.js | 27 ++++++++++++++++++++++ types/index.d.ts | 2 +- www/inappbrowser.js | 3 ++- 7 files changed, 130 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 503dd83..5e2a993 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,7 @@ The object returned from a call to `cordova.InAppBrowser.open` when the target i - __loaderror__: event fires when the `InAppBrowser` encounters an error when loading a URL. - __exit__: event fires when the `InAppBrowser` window is closed. - __beforeload__: event fires when the `InAppBrowser` decides whether to load an URL or not (only with option `beforeload=yes`). + - __message__: event fires when the `InAppBrowser` receives a message posted from the page loaded inside the `InAppBrowser` Webview. - __callback__: the function that executes when the event fires. The function is passed an `InAppBrowserEvent` object as a parameter. @@ -238,6 +239,7 @@ function showHelp(url) { inAppBrowserRef.addEventListener('beforeload', beforeloadCallBack); + inAppBrowserRef.addEventListener('message', messageCallBack); } function loadStartCallBack() { @@ -252,6 +254,13 @@ function loadStopCallBack() { inAppBrowserRef.insertCSS({ code: "body{font-size: 25px;" }); + inAppBrowserRef.executeScript({ code: "\ + var message = 'this is the message';\ + var messageObj = {my_message: message};\ + var stringifiedMessageObj = JSON.stringify(messageObj);\ + webkit.messageHandlers.cordova_iab.postMessage(stringifiedMessageObj);" + }); + $('#status-message').text(""); inAppBrowserRef.show(); @@ -300,11 +309,15 @@ function beforeloadCallback(params, callback) { } +function messageCallback(params){ + $('#status-message').text("message received: "+params.data.my_message); +} + ``` ### InAppBrowserEvent Properties -- __type__: the eventname, either `loadstart`, `loadstop`, `loaderror`, or `exit`. _(String)_ +- __type__: the eventname, either `loadstart`, `loadstop`, `loaderror`, `message` or `exit`. _(String)_ - __url__: the URL that was loaded. _(String)_ @@ -312,6 +325,8 @@ function beforeloadCallback(params, callback) { - __message__: the error message, only in the case of `loaderror`. _(String)_ +- __data__: the message contents , only in the case of `message`. A stringified JSON object. _(String)_ + ### Supported Platforms @@ -323,7 +338,11 @@ function beforeloadCallback(params, callback) { ### Browser Quirks -`loadstart` and `loaderror` events are not being fired. +`loadstart`, `loaderror`, `message` events are not fired. + +### Windows Quirks + +`message` event is not fired. ### Quick Example @@ -344,6 +363,7 @@ function beforeloadCallback(params, callback) { - __loadstop__: event fires when the `InAppBrowser` finishes loading a URL. - __loaderror__: event fires when the `InAppBrowser` encounters an error loading a URL. - __exit__: event fires when the `InAppBrowser` window is closed. + - __message__: event fires when the `InAppBrowser` receives a message posted from the page loaded inside the `InAppBrowser` Webview. - __callback__: the function to execute when the event fires. The function is passed an `InAppBrowserEvent` object. diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 93d9460..67ebf9e 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -48,6 +48,7 @@ import android.view.inputmethod.InputMethodManager; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.webkit.HttpAuthHandler; +import android.webkit.JavascriptInterface; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebSettings; @@ -95,6 +96,7 @@ public class InAppBrowser extends CordovaPlugin { private static final String LOAD_START_EVENT = "loadstart"; private static final String LOAD_STOP_EVENT = "loadstop"; private static final String LOAD_ERROR_EVENT = "loaderror"; + private static final String MESSAGE_EVENT = "message"; private static final String CLEAR_ALL_CACHE = "clearcache"; private static final String CLEAR_SESSION_CACHE = "clearsessioncache"; private static final String HARDWARE_BACK_BUTTON = "hardwareback"; @@ -952,8 +954,24 @@ public class InAppBrowser extends CordovaPlugin { settings.setBuiltInZoomControls(showZoomControls); settings.setPluginState(android.webkit.WebSettings.PluginState.ON); + // Add postMessage interface + class JsObject { + @JavascriptInterface + public void postMessage(String data) { + try { + JSONObject obj = new JSONObject(); + obj.put("type", MESSAGE_EVENT); + obj.put("data", new JSONObject(data)); + sendUpdate(obj, true); + } catch (JSONException ex) { + LOG.e(LOG_TAG, "data object passed to postMessage has caused a JSON error."); + } + } + } + if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { settings.setMediaPlaybackRequiresUserGesture(mediaPlaybackRequiresUserGesture); + inAppWebView.addJavascriptInterface(new JsObject(), "cordova_iab"); } String overrideUserAgent = preferences.getString("OverrideUserAgent", null); @@ -1270,6 +1288,11 @@ public class InAppBrowser extends CordovaPlugin { public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); + // Set the namespace for postMessage() + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1){ + injectDeferredObject("window.webkit={messageHandlers:{cordova_iab:cordova_iab}}", null); + } + // CB-10395 InAppBrowser's WebView not storing cookies reliable to local device storage if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { CookieManager.getInstance().flush(); diff --git a/src/ios/CDVUIInAppBrowser.m b/src/ios/CDVUIInAppBrowser.m index 501987a..133b6da 100644 --- a/src/ios/CDVUIInAppBrowser.m +++ b/src/ios/CDVUIInAppBrowser.m @@ -337,6 +337,16 @@ static CDVUIInAppBrowser* instance = nil; [self.inAppBrowserViewController navigateTo:url]; } +-(void)createIframeBridge +{ + // Create an iframe bridge in the new document to communicate with the CDVThemeableBrowserViewController + NSString* jsIframeBridge = @"var e = _cdvIframeBridge=d.getElementById('_cdvIframeBridge'); if(!_cdvIframeBridge) {e = _cdvIframeBridge = d.createElement('iframe'); e.id='_cdvIframeBridge'; e.style.display='none'; d.body.appendChild(e);}"; + // Add the postMessage API + NSString* jspostMessageApi = @"window.webkit={messageHandlers:{cordova_iab:{postMessage:function(message){_cdvIframeBridge.src='gap-iab://message/'+encodeURIComponent(message);}}}}"; + // Inject the JS to the webview + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"(function(d){%@%@})(document)", jsIframeBridge, jspostMessageApi]]; +} + // This is a helper method for the inject{Script|Style}{Code|File} API calls, which // provides a consistent method for injecting JavaScript code into the document. // @@ -348,9 +358,6 @@ static CDVUIInAppBrowser* instance = nil; - (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper { - // Ensure an iframe bridge is created to communicate with the CDVUIInAppBrowserViewController - [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){_cdvIframeBridge=d.getElementById('_cdvIframeBridge');if(!_cdvIframeBridge) {var e = _cdvIframeBridge = d.createElement('iframe');e.id='_cdvIframeBridge'; e.style.display='none';d.body.appendChild(e);}})(document)"]; - if (jsWrapper != nil) { NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; @@ -472,6 +479,22 @@ static CDVUIInAppBrowser* instance = nil; } [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; return NO; + }else if ([scriptCallbackId isEqualToString:@"message"] && (self.callbackId != nil)) { + // Send a message event + NSString* scriptResult = [url path]; + if ((scriptResult != nil) && ([scriptResult length] > 1)) { + scriptResult = [scriptResult substringFromIndex:1]; + NSError* __autoreleasing error = nil; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if (error == nil) { + NSMutableDictionary* dResult = [NSMutableDictionary new]; + [dResult setValue:@"message" forKey:@"type"]; + [dResult setObject:decodedResult forKey:@"data"]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dResult]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + } } } @@ -513,6 +536,7 @@ static CDVUIInAppBrowser* instance = nil; - (void)webViewDidFinishLoad:(UIWebView*)theWebView { + [self createIframeBridge]; if (self.callbackId != nil) { // TODO: It would be more useful to return the URL the page is actually on (e.g. if it's been redirected). NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m index 6c1b51a..89b7391 100644 --- a/src/ios/CDVWKInAppBrowser.m +++ b/src/ios/CDVWKInAppBrowser.m @@ -555,23 +555,37 @@ static CDVWKInAppBrowser* instance = nil; CDVPluginResult* pluginResult = nil; - NSDictionary* messageContent = (NSDictionary*) message.body; - NSString* scriptCallbackId = messageContent[@"id"]; - - if([messageContent objectForKey:@"d"]){ - NSString* scriptResult = messageContent[@"d"]; - NSError* __autoreleasing error = nil; - NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; - if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; + if([message.body isKindOfClass:[NSDictionary class]]){ + NSDictionary* messageContent = (NSDictionary*) message.body; + NSString* scriptCallbackId = messageContent[@"id"]; + + if([messageContent objectForKey:@"d"]){ + NSString* scriptResult = messageContent[@"d"]; + NSError* __autoreleasing error = nil; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; + } } else { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; + } + [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; + }else if(self.callbackId != nil){ + // Send a message event + NSString* messageContent = (NSString*) message.body; + NSError* __autoreleasing error = nil; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[messageContent dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if (error == nil) { + NSMutableDictionary* dResult = [NSMutableDictionary new]; + [dResult setValue:@"message" forKey:@"type"]; + [dResult setObject:decodedResult forKey:@"data"]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dResult]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; } - } else { - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; } - - [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; } - (void)didStartProvisionalNavigation:(WKWebView*)theWebView diff --git a/tests/tests.js b/tests/tests.js index 04b6a20..1a0619d 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -24,6 +24,7 @@ var cordova = require('cordova'); var isWindows = cordova.platformId === 'windows'; var isIos = cordova.platformId === 'ios'; +var isAndroid = cordova.platformId === 'android'; var isBrowser = cordova.platformId === 'browser'; window.alert = window.alert || navigator.notification.alert; @@ -151,6 +152,32 @@ exports.defineAutoTests = function () { done(); }); }); + + it('inappbrowser.spec.7 should support message event', function (done) { + if (!isAndroid && !isIos) { + return pending(cordova.platformId + ' platform doesn\'t support message event'); + } + var messageKey = 'my_message'; + var messageValue = 'is_this'; + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('message', function (evt) { + // Verify message event + expect(evt).toBeDefined(); + expect(evt.type).toEqual('message'); + expect(evt.data).toBeDefined(); + expect(evt.data[messageKey]).toBeDefined(); + expect(evt.data[messageKey]).toEqual(messageValue); + done(); + }); + iabInstance.addEventListener('loadstop', function (evt) { + var code = '(function(){\n' + + ' var message = {' + messageKey + ': "' + messageValue + '"};\n' + + ' webkit.messageHandlers.cordova_iab.postMessage(JSON.stringify(message));\n' + + '})()'; + iabInstance.executeScript({ code: code }); + }); + + }); }); }; if (isIos) { diff --git a/types/index.d.ts b/types/index.d.ts index 3bac670..b2dd0da 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -6,7 +6,7 @@ // Copyright (c) Microsoft Open Technologies Inc // Licensed under the MIT license. // TypeScript Version: 2.3 -type channel = "loadstart" | "loadstop" | "loaderror" | "exit"; +type channel = "loadstart" | "loadstop" | "loaderror" | "exit" | "message"; interface Window { /** diff --git a/www/inappbrowser.js b/www/inappbrowser.js index 7764765..9ef9618 100644 --- a/www/inappbrowser.js +++ b/www/inappbrowser.js @@ -38,7 +38,8 @@ 'loadstop': channel.create('loadstop'), 'loaderror': channel.create('loaderror'), 'exit': channel.create('exit'), - 'customscheme': channel.create('customscheme') + 'customscheme': channel.create('customscheme'), + 'message': channel.create('message') }; } From 632a395b3d9d8d29b3262c843ef5967e976856fa Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Thu, 20 Dec 2018 19:31:14 +0000 Subject: [PATCH 48/53] GH-359: Fix beforeload to work with POST requests (#367) ### Platforms affected iOS and Android ### What does this PR do? Fixes the behaviour of `beforeload` to resolve the problem with POST requests outlined in #359. The `beforeload` parameter has been changed from taking only a boolean (`yes` or not defined) to a discrete string with possible values of `get`, `post`, or `yes` which correspond to request types of GET, POST or GET&POST respectively. The `README.md` has been updated to reflect this. Note that use of `beforeload` to intercept POST requests is currently not supported on Android or iOS, so if `beforeload=yes` is specified and a POST request is detected as the HTTP request method, `beforeload` behaviour will not be applied. If `beforeload=post` is specified, a `loaderror` event will be dispatched which states that POST requests are not yet supported. #### Notes for Android The `shouldOverrideUrlLoading()` override method has been updated to support the [new method interface added in API 24 / Android 7][1] which receives the `WebResourceRequest` instead of just the `String url`, enabling the HTTP method of the request to be determined. The [deprecated method interface][2] has also been preserved for API <=23, but in this case the HTTP method cannot be determined so is passed as null. Also note that due to a [Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=155250), `shouldOverrideUrlLoading()` is currently not called for POST requests. It's possible this may be resolved in a future Chromium version in the Android System Webview (given that this is now self-updating and independent of Android version since Android 5.0) - in prospective anticipation of this, code to handle POST requests has been added to `shouldOverrideUrlLoading()`. However, it seems more likely that this won't be resolved any time soon given that [a Chromium dev said](https://bugs.chromium.org/p/chromium/issues/detail?id=155250#c39): > We're looking at implementing a better way to handle request interception in a future OS version. There's no way to just "fix" this, the API doesn't accommodate this usage at all. This will not be something you can use any time soon. Therefore if we want to go ahead and use `beforeload` to intercept request types other than GET, it's likely we'll instead need to use the `shouldInterceptRequest()` method override. As with `shouldOverrideUrlLoading()`, there are a two variants: the [new method interface][3] added in API 21 / Android 5.0 which which receives the `WebResourceRequest` object and the [deprecated one][4] which receives only `String url`. If we want to determine the HTTP request method, we'll need to use the new implementation. This has been empirically tested and *is* called for POST requests so would allow the possibility to intercept, delay, modify and send the POST request and its data via `beforeload`. Both `shouldInterceptRequest()` method interfaces have been exposed in the Android implentation for potential future use but they currently do nothing other than return the unadulterated request object. ### What testing has been done on this change? Manual testing of POST and GET requests on both platforms using a test app container: https://github.com/dpa99c/cordova-plugin-inappbrowser-test [1]: https://developer.android.com/reference/android/webkit/WebViewClient.html#shouldOverrideUrlLoading(android.webkit.WebView,%20android.webkit.WebResourceRequest) [2]: https://developer.android.com/reference/android/webkit/WebViewClient.html#shouldOverrideUrlLoading(android.webkit.WebView,%20java.lang.String) [3]: https://developer.android.com/reference/android/webkit/WebViewClient.html#shouldInterceptRequest(android.webkit.WebView,%20android.webkit.WebResourceRequest) [4]: https://developer.android.com/reference/android/webkit/WebViewClient.html#shouldInterceptRequest(android.webkit.WebView,%20java.lang.String) --- README.md | 6 +- src/android/InAppBrowser.java | 145 ++++++++++++++++++++++++++----- src/ios/CDVInAppBrowserOptions.h | 2 +- src/ios/CDVInAppBrowserOptions.m | 1 + src/ios/CDVUIInAppBrowser.h | 2 +- src/ios/CDVUIInAppBrowser.m | 44 ++++++++-- src/ios/CDVWKInAppBrowser.h | 2 +- src/ios/CDVWKInAppBrowser.m | 47 ++++++++-- 8 files changed, 203 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 5e2a993..708cc7e 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ instance, or the system browser. Android supports these additional options: - __hidden__: set to `yes` to create the browser and load the page, but not show it. The loadstop event fires when loading is complete. Omit or set to `no` (default) to have the browser open and load normally. - - __beforeload__: set to `yes` to enable the `beforeload` event to modify which pages are actually loaded in the browser. + - __beforeload__: set to enable the `beforeload` event to modify which pages are actually loaded in the browser. Accepted values are `get` to intercept only GET requests, `post` to intercept on POST requests or `yes` to intercept both GET & POST requests. Note that POST requests are not currently supported and will be ignored (if you set `beforeload=post` it will raise an error). - __clearcache__: set to `yes` to have the browser's cookie cache cleared before the new window is opened - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened - __closebuttoncaption__: set to a string to use as the close button's caption instead of a X. Note that you need to localize this value yourself. @@ -130,7 +130,7 @@ instance, or the system browser. - __usewkwebview__: set to `yes` to use WKWebView engine for the InappBrowser. Omit or set to `no` (default) to use UIWebView. Note: Using `usewkwebview=yes` requires that a WKWebView engine plugin be installed in the Cordova project (e.g. [cordova-plugin-wkwebview-engine](https://github.com/apache/cordova-plugin-wkwebview-engine) or [cordova-plugin-ionic-webview](https://github.com/ionic-team/cordova-plugin-ionic-webview)). - __hidden__: set to `yes` to create the browser and load the page, but not show it. The loadstop event fires when loading is complete. Omit or set to `no` (default) to have the browser open and load normally. - - __beforeload__: set to `yes` to enable the `beforeload` event to modify which pages are actually loaded in the browser. + - __beforeload__: set to enable the `beforeload` event to modify which pages are actually loaded in the browser. Accepted values are `get` to intercept only GET requests, `post` to intercept on POST requests or `yes` to intercept both GET & POST requests. Note that POST requests are not currently supported and will be ignored (if you set `beforeload=post` it will raise an error). - __clearcache__: set to `yes` to have the browser's cookie cache cleared before the new window is opened - __clearsessioncache__: set to `yes` to have the session cookie cache cleared before the new window is opened. For WKWebView, requires iOS 11+ on target device. - __cleardata__: set to `yes` to have the browser's entire local storage cleared (cookies, HTML5 local storage, IndexedDB, etc.) before the new window is opened @@ -212,7 +212,7 @@ The object returned from a call to `cordova.InAppBrowser.open` when the target i - __loadstop__: event fires when the `InAppBrowser` finishes loading a URL. - __loaderror__: event fires when the `InAppBrowser` encounters an error when loading a URL. - __exit__: event fires when the `InAppBrowser` window is closed. - - __beforeload__: event fires when the `InAppBrowser` decides whether to load an URL or not (only with option `beforeload=yes`). + - __beforeload__: event fires when the `InAppBrowser` decides whether to load an URL or not (only with option `beforeload` set). - __message__: event fires when the `InAppBrowser` receives a message posted from the page loaded inside the `InAppBrowser` Webview. - __callback__: the function that executes when the event fires. The function is passed an `InAppBrowserEvent` object as a parameter. diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index 67ebf9e..895b543 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -19,6 +19,7 @@ package org.apache.cordova.inappbrowser; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -51,6 +52,8 @@ import android.webkit.HttpAuthHandler; import android.webkit.JavascriptInterface; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -141,7 +144,7 @@ public class InAppBrowser extends CordovaPlugin { private boolean hideUrlBar = false; private boolean showFooter = false; private String footerColor = ""; - private boolean useBeforeload = false; + private String beforeload = ""; private String[] allowedSchemes; /** @@ -251,8 +254,8 @@ public class InAppBrowser extends CordovaPlugin { closeDialog(); } else if (action.equals("loadAfterBeforeload")) { - if (!useBeforeload) { - LOG.e(LOG_TAG, "unexpected loadAfterBeforeload called without feature beforeload=yes"); + if (beforeload == null) { + LOG.e(LOG_TAG, "unexpected loadAfterBeforeload called without feature beforeload=yes"); } final String url = args.getString(0); this.cordova.getActivity().runOnUiThread(new Runnable() { @@ -692,9 +695,8 @@ public class InAppBrowser extends CordovaPlugin { if (footerColorSet != null) { footerColor = footerColorSet; } - String beforeload = features.get(BEFORELOAD); - if (beforeload != null) { - useBeforeload = beforeload.equals("yes") ? true : false; + if (features.get(BEFORELOAD) != null) { + beforeload = features.get(BEFORELOAD); } } @@ -946,7 +948,7 @@ public class InAppBrowser extends CordovaPlugin { } }); - WebViewClient client = new InAppBrowserClient(thatWebView, edittext, useBeforeload); + WebViewClient client = new InAppBrowserClient(thatWebView, edittext, beforeload); inAppWebView.setWebViewClient(client); WebSettings settings = inAppWebView.getSettings(); settings.setJavaScriptEnabled(true); @@ -1123,7 +1125,7 @@ public class InAppBrowser extends CordovaPlugin { public class InAppBrowserClient extends WebViewClient { EditText edittext; CordovaWebView webView; - boolean useBeforeload; + String beforeload; boolean waitForBeforeload; /** @@ -1132,11 +1134,41 @@ public class InAppBrowser extends CordovaPlugin { * @param webView * @param mEditText */ - public InAppBrowserClient(CordovaWebView webView, EditText mEditText, boolean useBeforeload) { + public InAppBrowserClient(CordovaWebView webView, EditText mEditText, String beforeload) { this.webView = webView; this.edittext = mEditText; - this.useBeforeload = useBeforeload; - this.waitForBeforeload = useBeforeload; + this.beforeload = beforeload; + this.waitForBeforeload = beforeload != null; + } + + /** + * Override the URL that should be loaded + * + * Legacy (deprecated in API 24) + * For Android 6 and below. + * + * @param webView + * @param url + */ + @SuppressWarnings("deprecation") + @Override + public boolean shouldOverrideUrlLoading(WebView webView, String url) { + return shouldOverrideUrlLoading(url, null); + } + + /** + * Override the URL that should be loaded + * + * New (added in API 24) + * For Android 7 and above. + * + * @param webView + * @param request + */ + @TargetApi(Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading(WebView webView, WebResourceRequest request) { + return shouldOverrideUrlLoading(request.getUrl().toString(), request.getMethod()); } /** @@ -1144,23 +1176,44 @@ public class InAppBrowser extends CordovaPlugin { * * This handles a small subset of all the URIs that would be encountered. * - * @param webView * @param url + * @param method */ - @Override - public boolean shouldOverrideUrlLoading(WebView webView, String url) { + public boolean shouldOverrideUrlLoading(String url, String method) { boolean override = false; + boolean useBeforeload = false; + String errorMessage = null; + + if(beforeload.equals("yes") + //TODO handle POST requests then this condition can be removed: + && !method.equals("POST")) + { + useBeforeload = true; + }else if(beforeload.equals("get") && (method == null || method.equals("GET"))){ + useBeforeload = true; + }else if(beforeload.equals("post") && (method == null || method.equals("POST"))){ + //TODO handle POST requests + errorMessage = "beforeload doesn't yet support POST requests"; + } // On first URL change, initiate JS callback. Only after the beforeload event, continue. - if (this.waitForBeforeload) { - try { - JSONObject obj = new JSONObject(); - obj.put("type", "beforeload"); - obj.put("url", url); - sendUpdate(obj, true); + if (useBeforeload && this.waitForBeforeload) { + if(sendBeforeLoad(url, method)){ return true; - } catch (JSONException ex) { - LOG.e(LOG_TAG, "URI passed in has caused a JSON error."); + } + } + + if(errorMessage != null){ + try { + LOG.e(LOG_TAG, errorMessage); + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_ERROR_EVENT); + obj.put("url", url); + obj.put("code", -1); + obj.put("message", errorMessage); + sendUpdate(obj, true, PluginResult.Status.ERROR); + }catch(Exception e){ + LOG.e(LOG_TAG, "Error sending loaderror for " + url + ": " + e.toString()); } } @@ -1239,12 +1292,58 @@ public class InAppBrowser extends CordovaPlugin { } } - if (this.useBeforeload) { + if (useBeforeload) { this.waitForBeforeload = true; } return override; } + private boolean sendBeforeLoad(String url, String method){ + try { + JSONObject obj = new JSONObject(); + obj.put("type", "beforeload"); + obj.put("url", url); + if(method != null){ + obj.put("method", method); + } + sendUpdate(obj, true); + return true; + } catch (JSONException ex) { + LOG.e(LOG_TAG, "URI passed in has caused a JSON error."); + } + return false; + } + + + /** + * Legacy (deprecated in API 21) + * For Android 4.4 and below. + * @param view + * @param url + * @return + */ + @SuppressWarnings("deprecation") + @Override + public WebResourceResponse shouldInterceptRequest (final WebView view, String url) { + return shouldInterceptRequest(url, super.shouldInterceptRequest(view, url), null); + } + + /** + * New (added in API 21) + * For Android 5.0 and above. + * + * @param webView + * @param request + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + return shouldInterceptRequest(request.getUrl().toString(), super.shouldInterceptRequest(view, request), request.getMethod()); + } + + public WebResourceResponse shouldInterceptRequest(String url, WebResourceResponse response, String method){ + return response; + } /* * onPageStarted fires the LOAD_START_EVENT diff --git a/src/ios/CDVInAppBrowserOptions.h b/src/ios/CDVInAppBrowserOptions.h index 14b7a7a..29fd6e1 100644 --- a/src/ios/CDVInAppBrowserOptions.h +++ b/src/ios/CDVInAppBrowserOptions.h @@ -45,7 +45,7 @@ @property (nonatomic, assign) BOOL suppressesincrementalrendering; @property (nonatomic, assign) BOOL hidden; @property (nonatomic, assign) BOOL disallowoverscroll; -@property (nonatomic, assign) BOOL beforeload; +@property (nonatomic, copy) NSString* beforeload; + (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; diff --git a/src/ios/CDVInAppBrowserOptions.m b/src/ios/CDVInAppBrowserOptions.m index e1cc7d3..60e45fc 100644 --- a/src/ios/CDVInAppBrowserOptions.m +++ b/src/ios/CDVInAppBrowserOptions.m @@ -46,6 +46,7 @@ self.closebuttoncolor = nil; self.toolbarcolor = nil; self.toolbartranslucent = YES; + self.beforeload = @""; } return self; diff --git a/src/ios/CDVUIInAppBrowser.h b/src/ios/CDVUIInAppBrowser.h index 55c3662..a545833 100644 --- a/src/ios/CDVUIInAppBrowser.h +++ b/src/ios/CDVUIInAppBrowser.h @@ -35,7 +35,7 @@ UIWindow * tmpWindow; @private - BOOL _useBeforeload; + NSString* _beforeload; BOOL _waitForBeforeload; } diff --git a/src/ios/CDVUIInAppBrowser.m b/src/ios/CDVUIInAppBrowser.m index 133b6da..5e3e900 100644 --- a/src/ios/CDVUIInAppBrowser.m +++ b/src/ios/CDVUIInAppBrowser.m @@ -53,7 +53,7 @@ static CDVUIInAppBrowser* instance = nil; instance = self; _previousStatusBarStyle = -1; _callbackIdPattern = nil; - _useBeforeload = NO; + _beforeload = @""; _waitForBeforeload = NO; } @@ -219,8 +219,12 @@ static CDVUIInAppBrowser* instance = nil; } // use of beforeload event - _useBeforeload = browserOptions.beforeload; - _waitForBeforeload = browserOptions.beforeload; + if([browserOptions.beforeload isKindOfClass:[NSString class]]){ + _beforeload = browserOptions.beforeload; + }else{ + _beforeload = @"yes"; + } + _waitForBeforeload = ![_beforeload isEqualToString:@""]; [self.inAppBrowserViewController navigateTo:url]; if (!browserOptions.hidden) { @@ -320,7 +324,7 @@ static CDVUIInAppBrowser* instance = nil; { NSString* urlStr = [command argumentAtIndex:0]; - if (!_useBeforeload) { + if ([_beforeload isEqualToString:@""]) { NSLog(@"unexpected loadAfterBeforeload called without feature beforeload=yes"); } if (self.inAppBrowserViewController == nil) { @@ -454,6 +458,22 @@ static CDVUIInAppBrowser* instance = nil; NSURL* url = request.URL; BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; BOOL shouldStart = YES; + BOOL useBeforeLoad = NO; + NSString* httpMethod = request.HTTPMethod; + NSString* errorMessage = nil; + + if([_beforeload isEqualToString:@"post"]){ + //TODO handle POST requests by preserving POST data then remove this condition + errorMessage = @"beforeload doesn't yet support POST requests"; + } + else if(isTopLevelNavigation && ( + [_beforeload isEqualToString:@"yes"] + || ([_beforeload isEqualToString:@"get"] && [httpMethod isEqualToString:@"GET"]) + // TODO comment in when POST requests are handled + // || ([_beforeload isEqualToString:@"post"] && [httpMethod isEqualToString:@"POST"]) + )){ + useBeforeLoad = YES; + } // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute, // and the path, if present, should be a JSON-encoded value to pass to the callback. @@ -498,15 +518,23 @@ static CDVUIInAppBrowser* instance = nil; } } - // When beforeload=yes, on first URL change, initiate JS callback. Only after the beforeload event, continue. - if (_waitForBeforeload && isTopLevelNavigation) { + // When beforeload, on first URL change, initiate JS callback. Only after the beforeload event, continue. + if (_waitForBeforeload && useBeforeLoad) { CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:@{@"type":@"beforeload", @"url":[url absoluteString]}]; [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; return NO; } + + if(errorMessage != nil){ + NSLog(errorMessage); + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":[url absoluteString], @"code": @"-1", @"message": errorMessage}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } //if is an app store link, let the system handle it, otherwise it fails to load it if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { @@ -523,7 +551,7 @@ static CDVUIInAppBrowser* instance = nil; [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; } - if (_useBeforeload && isTopLevelNavigation) { + if (useBeforeLoad) { _waitForBeforeload = YES; } diff --git a/src/ios/CDVWKInAppBrowser.h b/src/ios/CDVWKInAppBrowser.h index 8b5925e..0015cea 100644 --- a/src/ios/CDVWKInAppBrowser.h +++ b/src/ios/CDVWKInAppBrowser.h @@ -28,7 +28,7 @@ @interface CDVWKInAppBrowser : CDVPlugin { @private - BOOL _useBeforeload; + NSString* _beforeload; BOOL _waitForBeforeload; } diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m index 89b7391..121223f 100644 --- a/src/ios/CDVWKInAppBrowser.m +++ b/src/ios/CDVWKInAppBrowser.m @@ -60,7 +60,7 @@ static CDVWKInAppBrowser* instance = nil; instance = self; _previousStatusBarStyle = -1; _callbackIdPattern = nil; - _useBeforeload = NO; + _beforeload = @""; _waitForBeforeload = NO; } @@ -266,8 +266,12 @@ static CDVWKInAppBrowser* instance = nil; } // use of beforeload event - _useBeforeload = browserOptions.beforeload; - _waitForBeforeload = browserOptions.beforeload; + if([browserOptions.beforeload isKindOfClass:[NSString class]]){ + _beforeload = browserOptions.beforeload; + }else{ + _beforeload = @"yes"; + } + _waitForBeforeload = ![_beforeload isEqualToString:@""]; [self.inAppBrowserViewController navigateTo:url]; [self show:nil withNoAnimate:browserOptions.hidden]; @@ -379,8 +383,8 @@ static CDVWKInAppBrowser* instance = nil; { NSString* urlStr = [command argumentAtIndex:0]; - if (!_useBeforeload) { - NSLog(@"unexpected loadAfterBeforeload called without feature beforeload=yes"); + if ([_beforeload isEqualToString:@""]) { + NSLog(@"unexpected loadAfterBeforeload called without feature beforeload=get|post"); } if (self.inAppBrowserViewController == nil) { NSLog(@"Tried to invoke loadAfterBeforeload on IAB after it was closed."); @@ -392,6 +396,7 @@ static CDVWKInAppBrowser* instance = nil; } NSURL* url = [NSURL URLWithString:urlStr]; + //_beforeload = @""; _waitForBeforeload = NO; [self.inAppBrowserViewController navigateTo:url]; } @@ -512,18 +517,42 @@ static CDVWKInAppBrowser* instance = nil; NSURL* mainDocumentURL = navigationAction.request.mainDocumentURL; BOOL isTopLevelNavigation = [url isEqual:mainDocumentURL]; BOOL shouldStart = YES; + BOOL useBeforeLoad = NO; + NSString* httpMethod = navigationAction.request.HTTPMethod; + NSString* errorMessage = nil; + + if([_beforeload isEqualToString:@"post"]){ + //TODO handle POST requests by preserving POST data then remove this condition + errorMessage = @"beforeload doesn't yet support POST requests"; + } + else if(isTopLevelNavigation && ( + [_beforeload isEqualToString:@"yes"] + || ([_beforeload isEqualToString:@"get"] && [httpMethod isEqualToString:@"GET"]) + // TODO comment in when POST requests are handled + // || ([_beforeload isEqualToString:@"post"] && [httpMethod isEqualToString:@"POST"]) + )){ + useBeforeLoad = YES; + } - // When beforeload=yes, on first URL change, initiate JS callback. Only after the beforeload event, continue. - if (_waitForBeforeload && isTopLevelNavigation) { + // When beforeload, on first URL change, initiate JS callback. Only after the beforeload event, continue. + if (_waitForBeforeload && useBeforeLoad) { CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:@{@"type":@"beforeload", @"url":[url absoluteString]}]; [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; decisionHandler(WKNavigationActionPolicyCancel); return; } + if(errorMessage != nil){ + NSLog(errorMessage); + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":[url absoluteString], @"code": @"-1", @"message": errorMessage}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + //if is an app store link, let the system handle it, otherwise it fails to load it if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { [theWebView stopLoading]; @@ -539,7 +568,7 @@ static CDVWKInAppBrowser* instance = nil; [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; } - if (_useBeforeload && isTopLevelNavigation) { + if (useBeforeLoad) { _waitForBeforeload = YES; } From 6db2f2d324e5f43316108bb20c0cb2960702dd3d Mon Sep 17 00:00:00 2001 From: Tim Brust Date: Tue, 29 Jan 2019 22:54:54 +0100 Subject: [PATCH 49/53] fix(typescript): remove unused replace parameter (#410) This closes #295 --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index b2dd0da..fa9e529 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -17,7 +17,7 @@ interface Window { * The options string must not contain any blank space, and each feature's * name/value pairs must be separated by a comma. Feature names are case insensitive. */ - open(url: string, target?: string, options?: string, replace?: boolean): InAppBrowser; + open(url: string, target?: string, options?: string): InAppBrowser; } /** From 9f4b72982abf50ddc54a34bd3748b6a3aad3be57 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Sun, 10 Feb 2019 17:06:35 +0000 Subject: [PATCH 50/53] GH-417: Handle non-default target attribute values (e.g. target=on links in WKWebView implementation on iOS (#418) --- src/ios/CDVWKInAppBrowser.m | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m index 121223f..7deb6d0 100644 --- a/src/ios/CDVWKInAppBrowser.m +++ b/src/ios/CDVWKInAppBrowser.m @@ -573,7 +573,14 @@ static CDVWKInAppBrowser* instance = nil; } if(shouldStart){ - decisionHandler(WKNavigationActionPolicyAllow); + // Fix GH-417: Handle non-default target attribute + // Based on https://stackoverflow.com/a/25853806/777265 + if (!navigationAction.targetFrame.isMainFrame){ + [theWebView loadRequest:navigationAction.request]; + decisionHandler(WKNavigationActionPolicyCancel); + }else{ + decisionHandler(WKNavigationActionPolicyAllow); + } }else{ decisionHandler(WKNavigationActionPolicyCancel); } From ae329bc4c2e618cb961039c89631181a3140fb84 Mon Sep 17 00:00:00 2001 From: Jan Piotrowski Date: Mon, 11 Feb 2019 18:39:42 +0100 Subject: [PATCH 51/53] Add or update GitHub pull request and issue template --- .github/ISSUE_TEMPLATE.md | 42 ++++++++++++++++++ .github/ISSUE_TEMPLATE/BUG_REPORT.md | 50 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md | 29 +++++++++++++ .github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md | 27 ++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 27 +++++++++--- 5 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.md create mode 100644 .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md create mode 100644 .github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..3220c25 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,42 @@ + + +### Issue Type + + +- [ ] Bug Report +- [ ] Feature Request +- [ ] Support Question + +## Description + +## Information + + +### Command or Code + + +### Environment, Platform, Device + + + + +### Version information + + + + +## Checklist + + +- [ ] I searched for already existing GitHub issues about this +- [ ] I updated all Cordova tooling to their most recent version +- [ ] I included all the necessary information above diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 0000000..bd8a3ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,50 @@ +--- +name: 🐛 Bug Report +about: If something isn't working as expected. + +--- + +# Bug Report + +## Problem + +### What is expected to happen? + + + +### What does actually happen? + + + +## Information + + + + +### Command or Code + + + + +### Environment, Platform, Device + + + + +### Version information + + + + +## Checklist + + +- [ ] I searched for existing GitHub issues +- [ ] I updated all Cordova tooling to most recent version +- [ ] I included all the necessary information above diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md new file mode 100644 index 0000000..381fc8a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -0,0 +1,29 @@ +--- +name: 🚀 Feature Request +about: A suggestion for a new functionality + +--- + +# Feature Request + +## Motivation Behind Feature + + + + +## Feature Description + + + + +## Alternatives or Workarounds + + + diff --git a/.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md b/.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md new file mode 100644 index 0000000..516c6e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md @@ -0,0 +1,27 @@ +--- +name: 💬 Support Question +about: If you have a question, please check out our Slack or StackOverflow! + +--- + + + +Apache Cordova uses GitHub Issues as a feature request and bug tracker _only_. +For usage and support questions, please check out the resources below. Thanks! + +--- + +You can get answers to your usage and support questions about **Apache Cordova** on: + +* Slack Community Chat: https://cordova.slack.com (you can sign-up at http://slack.cordova.io/) +* StackOverflow: https://stackoverflow.com/questions/tagged/cordova using the tag `cordova` + +--- + +If you are using a tool that uses Cordova internally, like e.g. Ionic, check their support channels: + +* **Ionic Framework** + * [Ionic Community Forum](https://forum.ionicframework.com/) + * [Ionic Worldwide Slack](https://ionicworldwide.herokuapp.com/) +* **PhoneGap** + * [PhoneGap Developer Community](https://forums.adobe.com/community/phonegap) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 91582f4..712b2ff 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,5 @@ + -### What testing has been done on this change? + +### Description + + + + +### Testing + + ### Checklist -- [ ] [Reported an issue](http://cordova.apache.org/contribute/issues.html) in the JIRA database -- [ ] Commit message follows the format: "CB-3232: (android) Fix bug with resolving file paths", where CB-xxxx is the JIRA ID & "android" is the platform affected. -- [ ] Added automated test coverage as appropriate for this change. + +- [ ] I've run the tests to see all new and existing tests pass +- [ ] I added automated test coverage as appropriate for this change +- [ ] Commit is prefixed with `(platform)` if this change only applies to one platform (e.g. `(android)`) +- [ ] If this Pull Request resolves an issue, I linked to the issue in the text above (and used the correct [keyword to close issues using keywords](https://help.github.com/articles/closing-issues-using-keywords/)) +- [ ] I've updated the documentation if necessary From 388e3f6ae74b7c59fee43e360427ab73325f9a77 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Mon, 11 Feb 2019 23:14:18 +0000 Subject: [PATCH 52/53] (iOS) Fix iframes in iOS/WKWebView which were broken by #418. Fixes #424. (#425) --- src/ios/CDVWKInAppBrowser.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m index 7deb6d0..6364f4f 100644 --- a/src/ios/CDVWKInAppBrowser.m +++ b/src/ios/CDVWKInAppBrowser.m @@ -573,9 +573,9 @@ static CDVWKInAppBrowser* instance = nil; } if(shouldStart){ - // Fix GH-417: Handle non-default target attribute - // Based on https://stackoverflow.com/a/25853806/777265 - if (!navigationAction.targetFrame.isMainFrame){ + // Fix GH-417 & GH-424: Handle non-default target attribute + // Based on https://stackoverflow.com/a/25713070/777265 + if (!navigationAction.targetFrame){ [theWebView loadRequest:navigationAction.request]; decisionHandler(WKNavigationActionPolicyCancel); }else{ From 92243cdcc29c31d03deb3928c721e6abd60c5536 Mon Sep 17 00:00:00 2001 From: Jan Piotrowski Date: Fri, 22 Feb 2019 18:27:30 +0100 Subject: [PATCH 53/53] Add headline to window.open documentation (#406) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 708cc7e..3ca029f 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ This plugin provides a web browser view that displays when calling `cordova.InAp var ref = cordova.InAppBrowser.open('http://apache.org', '_blank', 'location=yes'); +### `window.open` + The `cordova.InAppBrowser.open()` function is defined to be a drop-in replacement for the `window.open()` function. Existing `window.open()` calls can use the InAppBrowser window, by replacing window.open: