diff --git a/README.md b/README.md index ca18e627b..381c41562 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,18 @@ description: Open an in-app browser window. --> +# Reason for fork +[PR](https://github.com/apache/cordova-plugin-inappbrowser/pull/1024) for passing cookies to the InAppBrowser was already available. +However it is not getting merged. + +That's why we did following things: +- Created a [fork](https://github.com/edorex/cordova-plugin-inappbrowser): +- Created a feature branch `headers-cookies` +- Included all the changes from the inital [PR](https://github.com/apache/cordova-plugin-inappbrowser/pull/1024) +- Created [PR](https://github.com/edorex/cordova-plugin-inappbrowser/pull/1062) to merge it into master +- Referrencing our forked and updated master branch in the project. + + # cordova-plugin-inappbrowser [![Android Testsuite](https://github.com/apache/cordova-plugin-inappbrowser/actions/workflows/android.yml/badge.svg)](https://github.com/apache/cordova-plugin-inappbrowser/actions/workflows/android.yml) [![Chrome Testsuite](https://github.com/apache/cordova-plugin-inappbrowser/actions/workflows/chrome.yml/badge.svg)](https://github.com/apache/cordova-plugin-inappbrowser/actions/workflows/chrome.yml) [![iOS Testsuite](https://github.com/apache/cordova-plugin-inappbrowser/actions/workflows/ios.yml/badge.svg)](https://github.com/apache/cordova-plugin-inappbrowser/actions/workflows/ios.yml) [![Lint Test](https://github.com/apache/cordova-plugin-inappbrowser/actions/workflows/lint.yml/badge.svg)](https://github.com/apache/cordova-plugin-inappbrowser/actions/workflows/lint.yml) diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java index eed595016..9c500a605 100644 --- a/src/android/InAppBrowser.java +++ b/src/android/InAppBrowser.java @@ -24,6 +24,8 @@ Licensed to the Apache Software Foundation (ASF) under one import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.os.Handler; +import android.os.HandlerThread; import android.os.Parcelable; import android.provider.Browser; import android.content.res.Resources; @@ -35,6 +37,7 @@ Licensed to the Apache Software Foundation (ASF) under one import android.os.Build; import android.os.Bundle; import android.text.InputType; +import android.util.Base64; import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; @@ -63,6 +66,9 @@ Licensed to the Apache Software Foundation (ASF) under one import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.apache.cordova.CallbackContext; import org.apache.cordova.Config; import org.apache.cordova.CordovaArgs; @@ -80,9 +86,12 @@ Licensed to the Apache Software Foundation (ASF) under one import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.HashMap; +import java.util.Map; import java.util.StringTokenizer; +import java.util.concurrent.CountDownLatch; @SuppressLint("SetJavaScriptEnabled") public class InAppBrowser extends CordovaPlugin { @@ -120,8 +129,10 @@ public class InAppBrowser extends CordovaPlugin { private static final String FULLSCREEN = "fullscreen"; private static final int TOOLBAR_HEIGHT = 48; + private static final String COOKIES = "cookies"; + private static final String HEADERS = "headers"; - private static final List customizableOptions = Arrays.asList(CLOSE_BUTTON_CAPTION, TOOLBAR_COLOR, NAVIGATION_COLOR, CLOSE_BUTTON_COLOR, FOOTER_COLOR); + private static final List customizableOptions = Arrays.asList(CLOSE_BUTTON_CAPTION, TOOLBAR_COLOR, NAVIGATION_COLOR, CLOSE_BUTTON_COLOR, FOOTER_COLOR, COOKIES, HEADERS); private InAppBrowserDialog dialog; private WebView inAppWebView; @@ -152,6 +163,12 @@ public class InAppBrowser extends CordovaPlugin { private String[] allowedSchemes; private InAppBrowserClient currentClient; + @Nullable + private Map headers; + @Nullable + private Map cookies; + + /** * Executes the request and returns PluginResult. * @@ -170,12 +187,16 @@ public boolean execute(String action, CordovaArgs args, final CallbackContext ca } final String target = t; final HashMap features = parseFeature(args.optString(2)); + parseHeadersAndCookies(features); LOG.d(LOG_TAG, "target = " + target); this.cordova.getActivity().runOnUiThread(new Runnable() { @Override public void run() { + + setCookies(clearAllCache, clearSessionCache, cookies); + String result = ""; // SELF if (SELF.equals(target)) { @@ -273,7 +294,7 @@ public void run() { } else { ((InAppBrowserClient)inAppWebView.getWebViewClient()).waitForBeforeload = false; } - inAppWebView.loadUrl(url); + inAppWebView.loadUrl(url,headers); } }); @@ -380,6 +401,18 @@ public void onDestroy() { closeDialog(); } + private void parseHeadersAndCookies(HashMap features){ + String headersSet = features.get(HEADERS); + if (headersSet != null) { + headers = deserializeMapOption(headersSet); + } + + String cookiesSet = features.get(COOKIES); + if (cookiesSet != null) { + cookies = deserializeMapOption(cookiesSet); + } + } + /** * Inject an object (script or style) into the InAppBrowser WebView. * @@ -1011,16 +1044,10 @@ public void postMessage(String data) { } settings.setDomStorageEnabled(true); - if (clearAllCache) { - CookieManager.getInstance().removeAllCookie(); - } else if (clearSessionCache) { - CookieManager.getInstance().removeSessionCookie(); - } - // Enable Thirdparty Cookies CookieManager.getInstance().setAcceptThirdPartyCookies(inAppWebView,true); - inAppWebView.loadUrl(url); + inAppWebView.loadUrl(url, headers); inAppWebView.setId(Integer.valueOf(6)); inAppWebView.getSettings().setLoadWithOverviewMode(true); inAppWebView.getSettings().setUseWideViewPort(useWideViewPort); @@ -1071,10 +1098,107 @@ public void postMessage(String data) { } } }; + this.cordova.getActivity().runOnUiThread(runnable); return ""; } + private void setCookies(boolean clearAllCache, + boolean clearSessionCache, + @Nullable Map cookies) { + + int operations = 0; + if (clearAllCache) { + operations++; + } + if (clearSessionCache) { + operations++; + } + if (cookies != null) { + operations += cookies.size(); + } + + CountDownLatch cookiesCountdown = new CountDownLatch(operations); + + Runnable cookiesRunnable = cookiesRunnable( + cookiesCountdown, + clearAllCache, + clearSessionCache, + cookies); + + //CookieManager setCookie needs a background Looper to await completion + HandlerThread handlerThread = new HandlerThread("backgroundThread"); + if (!handlerThread.isAlive()) + handlerThread.start(); + Handler threadHandler = new Handler(handlerThread.getLooper()); + threadHandler.post(cookiesRunnable); //runs on main thread + try { + cookiesCountdown.await(); + } catch (InterruptedException e) { + LOG.e(LOG_TAG, "cookies set was interrupted by timeout.", e); + } + handlerThread.quitSafely(); + } + + @NonNull + private Runnable cookiesRunnable( + CountDownLatch cookiesCountdown, + boolean clearAllCache, + boolean clearSessionCache, + @Nullable Map cookies) { + + return () -> { + + if (clearAllCache) { + CookieManager.getInstance().removeAllCookies(value -> { + if (!value) { + LOG.e(LOG_TAG, "unable to removeAllCookies in CookieManager!"); + } + cookiesCountdown.countDown(); + }); + } else if (clearSessionCache) { + CookieManager.getInstance().removeSessionCookies(value -> { + if (!value) { + LOG.e(LOG_TAG, "unable to removeSessionCookies in CookieManager!"); + } + cookiesCountdown.countDown(); + }); + } + + //Set all cookies from options + if (cookies != null && cookies.size() > 0) { + + for (String cookieKey : cookies.keySet()) { + CookieManager.getInstance().setCookie(cookieKey, cookies.get(cookieKey), value -> { + if (!value) { + LOG.e(LOG_TAG, "unable to set the cookie in CookieManager!"); + } + cookiesCountdown.countDown(); + }); + } + } + + }; + } + + @Nullable + private Map deserializeMapOption(@NonNull String serializedMapOption) { + Map result = new HashMap<>(); + String base64 = serializedMapOption.replace("@", "="); + String json = new String(Base64.decode(base64, Base64.DEFAULT)); + try { + JSONObject jsonObject = new JSONObject(json); + for (Iterator keys = jsonObject.keys(); keys.hasNext(); ) { + String key = keys.next(); + result.put(key, jsonObject.getString(key)); + } + } catch (JSONException e) { + LOG.e(LOG_TAG, "headers options are not serialized as a valid json.", e); + return null; + } + return result; + } + /** * Create a new plugin success result and send it back to JavaScript * diff --git a/src/ios/CDVInAppBrowserOptions.h b/src/ios/CDVInAppBrowserOptions.h index 90c6e136f..21fb52665 100644 --- a/src/ios/CDVInAppBrowserOptions.h +++ b/src/ios/CDVInAppBrowserOptions.h @@ -46,6 +46,13 @@ @property (nonatomic, assign) BOOL disallowoverscroll; @property (nonatomic, copy) NSString* beforeload; +@property (nonatomic, copy) NSDictionary* headers; + +/** + Key is the cookie name, value is a Set-Cookie HTTP header string representation. + */ +@property (nonatomic, copy) NSDictionary* cookies; + + (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; @end diff --git a/src/ios/CDVInAppBrowserOptions.m b/src/ios/CDVInAppBrowserOptions.m index e20d1a8c9..d8da74f6b 100644 --- a/src/ios/CDVInAppBrowserOptions.m +++ b/src/ios/CDVInAppBrowserOptions.m @@ -45,6 +45,9 @@ - (id)init self.toolbarcolor = nil; self.toolbartranslucent = YES; self.beforeload = @""; + + self.headers = @{}; + self.cookies = @{}; } return self; @@ -71,12 +74,32 @@ + (CDVInAppBrowserOptions*)parseOptions:(NSString*)options [numberFormatter setAllowsFloats:YES]; BOOL isNumber = [numberFormatter numberFromString:value_lc] != nil; + //JSON values are base64 encoded + //We need to replace `=` to `@` cause `=` is used as key/value separator for options + //Cookies and headers are serialized in JSON and base64 encoding is need to include JSON into JS->Native options serialization. + BOOL isJson = false; + NSData *base64Value = [[NSData alloc] initWithBase64EncodedString:[value stringByReplacingOccurrencesOfString:@"@" withString:@"="] + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + NSError *error = nil; + id jsonValue = nil; + if(base64Value) { + jsonValue = [NSJSONSerialization JSONObjectWithData:base64Value + options:0 + error:&error]; + if(!error && jsonValue) { + isJson = true; + } + } + // 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 if (isJson) { + [obj setValue:jsonValue forKey:key]; } else { [obj setValue:value forKey:key]; } diff --git a/src/ios/CDVWKInAppBrowser.h b/src/ios/CDVWKInAppBrowser.h index 2f650b8c4..792de5b04 100644 --- a/src/ios/CDVWKInAppBrowser.h +++ b/src/ios/CDVWKInAppBrowser.h @@ -70,7 +70,7 @@ @property (nonatomic) NSURL* currentURL; - (void)close; -- (void)navigateTo:(NSURL*)url; +- (void)navigateTo:(NSURL*)url options:(CDVInAppBrowserOptions*)options; - (void)showLocationBar:(BOOL)show; - (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition; - (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString : (int) buttonIndex; diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m index a121fb19d..2bad29d36 100644 --- a/src/ios/CDVWKInAppBrowser.m +++ b/src/ios/CDVWKInAppBrowser.m @@ -151,6 +151,30 @@ - (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options }]; } + // Set all cookies + WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; + for(NSString* key in browserOptions.cookies){ + + NSURL* url = [NSURL URLWithString:key]; + if(!url){ + NSLog(@"Cookie key %@ is not a proper NSURL!",key); + continue; + } + + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:@{ @"Set-Cookie" : browserOptions.cookies[key] } forURL:url]; + + if(cookies.count == 0) { + NSLog(@"No cookies to process for url %@!",url); + } + + for(NSHTTPCookie* cookie in cookies){ + //Is not possible to wait for completion because it seems the handler is called after the WKWebView is loaded + //See: https://stackoverflow.com/questions/49452968/wkhttpcookiestore-setcookie-completion-handler-not-called + [cookieStore setCookie:cookie completionHandler:nil]; + } + + } + if (self.inAppBrowserViewController == nil) { self.inAppBrowserViewController = [[CDVWKInAppBrowserViewController alloc] initWithBrowserOptions: browserOptions andSettings:self.commandDelegate.settings]; self.inAppBrowserViewController.navigationDelegate = self; @@ -209,7 +233,7 @@ - (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options } _waitForBeforeload = ![_beforeload isEqualToString:@""]; - [self.inAppBrowserViewController navigateTo:url]; + [self.inAppBrowserViewController navigateTo:url options:browserOptions]; if (!browserOptions.hidden) { [self show:nil withNoAnimate:browserOptions.hidden]; } @@ -322,7 +346,7 @@ - (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command NSURL* url = [NSURL URLWithString:urlStr]; //_beforeload = @""; _waitForBeforeload = NO; - [self.inAppBrowserViewController navigateTo:url]; + [self.inAppBrowserViewController navigateTo:url options:nil]; } // This is a helper method for the inject{Script|Style}{Code|File} API calls, which @@ -1041,12 +1065,16 @@ - (void)close }); } -- (void)navigateTo:(NSURL*)url +- (void)navigateTo:(NSURL*)url options:(CDVInAppBrowserOptions*)options { if ([url.scheme isEqualToString:@"file"]) { [self.webView loadFileURL:url allowingReadAccessToURL:url]; } else { - NSURLRequest* request = [NSURLRequest requestWithURL:url]; + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; + for(NSString* key in options.headers) { + NSString* value = options.headers[key]; + [request setValue:value forHTTPHeaderField:key]; + } [self.webView loadRequest:request]; } }