diff --git a/Examples/UIExplorer/XHRExample.android.js b/Examples/UIExplorer/XHRExample.android.js
index ab272d133..ad4bbf772 100644
--- a/Examples/UIExplorer/XHRExample.android.js
+++ b/Examples/UIExplorer/XHRExample.android.js
@@ -27,6 +27,8 @@ var {
} = React;
var XHRExampleHeaders = require('./XHRExampleHeaders');
+var XHRExampleCookies = require('./XHRExampleCookies');
+
// TODO t7093728 This is a simlified XHRExample.ios.js.
// Once we have Camera roll, Toast, Intent (for opening URLs)
@@ -284,6 +286,11 @@ exports.examples = [{
render() {
return ;
}
+}, {
+ title: 'Cookies',
+ render() {
+ return ;
+ }
}];
var styles = StyleSheet.create({
diff --git a/Examples/UIExplorer/XHRExampleCookies.js b/Examples/UIExplorer/XHRExampleCookies.js
new file mode 100644
index 000000000..310accc19
--- /dev/null
+++ b/Examples/UIExplorer/XHRExampleCookies.js
@@ -0,0 +1,128 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ Text,
+ TouchableHighlight,
+ View,
+} = React;
+
+var RCTNetworking = require('RCTNetworking');
+
+class XHRExampleCookies extends React.Component {
+ constructor(props: any) {
+ super(props);
+ this.cancelled = false;
+ this.state = {
+ status: '',
+ a: 1,
+ b: 2,
+ };
+ }
+
+ setCookie(domain: string) {
+ var {a, b} = this.state;
+ var url = `https://${domain}/cookies/set?a=${a}&b=${b}`;
+ fetch(url).then((response) => {
+ this.setStatus(`Cookies a=${a}, b=${b} set`);
+ });
+
+ this.setState({
+ status: 'Setting cookies...',
+ a: a + 1,
+ b: b + 2,
+ });
+ }
+
+ getCookies(domain: string) {
+ fetch(`https://${domain}/cookies`).then((response) => {
+ return response.json();
+ }).then((data) => {
+ this.setStatus(`Got cookies ${JSON.stringify(data.cookies)} from server`);
+ });
+
+ this.setStatus('Getting cookies...');
+ }
+
+ clearCookies() {
+ RCTNetworking.clearCookies((cleared) => {
+ this.setStatus('Cookies cleared, had cookies=' + cleared);
+ });
+ }
+
+ setStatus(status: string) {
+ this.setState({status});
+ }
+
+ render() {
+ return (
+
+
+
+ Set cookie
+
+
+
+
+ Set cookie (EU)
+
+
+
+
+ Get cookies
+
+
+
+
+ Get cookies (EU)
+
+
+
+
+ Clear cookies
+
+
+ {this.state.status}
+
+ );
+ }
+}
+
+var styles = StyleSheet.create({
+ wrapper: {
+ borderRadius: 5,
+ marginBottom: 5,
+ },
+ button: {
+ backgroundColor: '#eeeeee',
+ padding: 8,
+ },
+});
+
+module.exports = XHRExampleCookies;
diff --git a/Libraries/Network/RCTNetworking.android.js b/Libraries/Network/RCTNetworking.android.js
index e8c4be0bd..351524eb9 100644
--- a/Libraries/Network/RCTNetworking.android.js
+++ b/Libraries/Network/RCTNetworking.android.js
@@ -40,6 +40,10 @@ class RCTNetworking {
static abortRequest(requestId) {
RCTNetworkingNative.abortRequest(requestId);
}
+
+ static clearCookies(callback) {
+ RCTNetworkingNative.clearCookies(callback);
+ }
}
module.exports = RCTNetworking;
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java
index 14b95c0ae..8a980cb46 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java
@@ -80,7 +80,8 @@ public class FrescoModule extends ReactContextBaseJavaModule implements
}
Context context = this.getReactApplicationContext().getApplicationContext();
- OkHttpClient okHttpClient = OkHttpClientProvider.getOkHttpClient();
+ OkHttpClient okHttpClient =
+ OkHttpClientProvider.getCookieAwareOkHttpClient(getReactApplicationContext());
ImagePipelineConfig.Builder builder =
OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient);
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ForwardingCookieHandler.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ForwardingCookieHandler.java
new file mode 100644
index 000000000..d24540cea
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ForwardingCookieHandler.java
@@ -0,0 +1,230 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+package com.facebook.react.modules.network;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.webkit.CookieManager;
+import android.webkit.CookieSyncManager;
+import android.webkit.ValueCallback;
+
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.GuardedAsyncTask;
+import com.facebook.react.bridge.GuardedResultAsyncTask;
+import com.facebook.react.bridge.ReactContext;
+
+/**
+ * Cookie handler that forwards all cookies to the WebView CookieManager.
+ *
+ * This class relies on CookieManager to persist cookies to disk so cookies may be lost if the
+ * application is terminated before it syncs.
+ */
+public class ForwardingCookieHandler extends CookieHandler {
+ private static final String VERSION_ZERO_HEADER = "Set-cookie";
+ private static final String VERSION_ONE_HEADER = "Set-cookie2";
+ private static final String COOKIE_HEADER = "Cookie";
+
+ // As CookieManager was synchronous before API 21 this class emulates the async behavior on <21.
+ private static final boolean USES_LEGACY_STORE = Build.VERSION.SDK_INT < 21;
+
+ private final CookieSaver mCookieSaver;
+ private final ReactContext mContext;
+ private @Nullable CookieManager mCookieManager;
+
+ public ForwardingCookieHandler(ReactContext context) {
+ mContext = context;
+ mCookieSaver = new CookieSaver();
+ }
+
+ @Override
+ public Map> get(URI uri, Map> headers)
+ throws IOException {
+ String cookies = getCookieManager().getCookie(uri.toString());
+ if (TextUtils.isEmpty(cookies)) {
+ return Collections.emptyMap();
+ }
+
+ return Collections.singletonMap(COOKIE_HEADER, Collections.singletonList(cookies));
+ }
+
+ @Override
+ public void put(URI uri, Map> headers) throws IOException {
+ String url = uri.toString();
+ for (Map.Entry> entry : headers.entrySet()) {
+ String key = entry.getKey();
+ if (key != null && isCookieHeader(key)) {
+ addCookies(url, entry.getValue());
+ }
+ }
+ }
+
+ public void clearCookies(final Callback callback) {
+ if (USES_LEGACY_STORE) {
+ new GuardedResultAsyncTask(mContext) {
+ @Override
+ protected Boolean doInBackgroundGuarded() {
+ getCookieManager().removeAllCookie();
+ mCookieSaver.onCookiesModified();
+ return true;
+ }
+
+ @Override
+ protected void onPostExecuteGuarded(Boolean result) {
+ callback.invoke(result);
+ }
+ }.execute();
+ } else {
+ clearCookiesAsync(callback);
+ }
+ }
+
+ private void clearCookiesAsync(final Callback callback) {
+ getCookieManager().removeAllCookies(
+ new ValueCallback() {
+ @Override
+ public void onReceiveValue(Boolean value) {
+ mCookieSaver.onCookiesModified();
+ callback.invoke(value);
+ }
+ });
+ }
+
+ public void destroy() {
+ if (USES_LEGACY_STORE) {
+ getCookieManager().removeExpiredCookie();
+ mCookieSaver.persistCookies();
+ }
+ }
+
+ private void addCookies(final String url, final List cookies) {
+ if (USES_LEGACY_STORE) {
+ runInBackground(
+ new Runnable() {
+ @Override
+ public void run() {
+ for (String cookie : cookies) {
+ getCookieManager().setCookie(url, cookie);
+ }
+ mCookieSaver.onCookiesModified();
+ }
+ });
+ } else {
+ for (String cookie : cookies) {
+ addCookieAsync(url, cookie);
+ }
+ mCookieSaver.onCookiesModified();
+ }
+ }
+
+ @TargetApi(21)
+ private void addCookieAsync(String url, String cookie) {
+ getCookieManager().setCookie(url, cookie, null);
+ }
+
+ private static boolean isCookieHeader(String name) {
+ return name.equalsIgnoreCase(VERSION_ZERO_HEADER) || name.equalsIgnoreCase(VERSION_ONE_HEADER);
+ }
+
+ private void runInBackground(final Runnable runnable) {
+ new GuardedAsyncTask(mContext) {
+ @Override
+ protected void doInBackgroundGuarded(Void... params) {
+ runnable.run();
+ }
+ }.execute();
+ }
+
+ /**
+ * Instantiating CookieManager in KitKat+ will load the Chromium task taking a 100ish ms so we
+ * do it lazily to make sure it's done on a background thread as needed.
+ */
+ private CookieManager getCookieManager() {
+ if (mCookieManager == null) {
+ possiblyWorkaroundSyncManager(mContext);
+ mCookieManager = CookieManager.getInstance();
+
+ if (USES_LEGACY_STORE) {
+ mCookieManager.removeExpiredCookie();
+ }
+ }
+
+ return mCookieManager;
+ }
+
+ private static void possiblyWorkaroundSyncManager(Context context) {
+ if (USES_LEGACY_STORE) {
+ // This is to work around a bug where CookieManager may fail to instantiate if
+ // CookieSyncManager has never been created. Note that the sync() may not be required but is
+ // here of legacy reasons.
+ CookieSyncManager syncManager = CookieSyncManager.createInstance(context);
+ syncManager.sync();
+ }
+ }
+
+ /**
+ * Responsible for flushing cookies to disk. Flushes to disk with a maximum delay of 30 seconds.
+ * This class is only active if we are on API < 21.
+ */
+ private class CookieSaver {
+ private static final int MSG_PERSIST_COOKIES = 1;
+
+ private static final int TIMEOUT = 30 * 1000; // 30 seconds
+
+ private final Handler mHandler;
+
+ public CookieSaver() {
+ mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MSG_PERSIST_COOKIES) {
+ persistCookies();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+ }
+
+ public void onCookiesModified() {
+ if (USES_LEGACY_STORE) {
+ mHandler.sendEmptyMessageDelayed(MSG_PERSIST_COOKIES, TIMEOUT);
+ }
+ }
+
+ public void persistCookies() {
+ mHandler.removeMessages(MSG_PERSIST_COOKIES);
+ runInBackground(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (USES_LEGACY_STORE) {
+ CookieSyncManager syncManager = CookieSyncManager.getInstance();
+ syncManager.sync();
+ } else {
+ flush();
+ }
+ }
+ });
+ }
+
+ @TargetApi(21)
+ private void flush() {
+ getCookieManager().flush();
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
index 4ddbf1f60..db316e371 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
@@ -14,6 +14,7 @@ import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
+import java.net.CookieHandler;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.GuardedAsyncTask;
@@ -72,19 +73,19 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
}
/**
- * @param reactContext the ReactContext of the application
+ * @param context the ReactContext of the application
*/
- public NetworkingModule(ReactApplicationContext reactContext) {
- this(reactContext, null, OkHttpClientProvider.getOkHttpClient());
+ public NetworkingModule(final ReactApplicationContext context) {
+ this(context, null, OkHttpClientProvider.getCookieAwareOkHttpClient(context));
}
/**
- * @param reactContext the ReactContext of the application
+ * @param context the ReactContext of the application
* @param defaultUserAgent the User-Agent header that will be set for all requests where the
* caller does not provide one explicitly
*/
- public NetworkingModule(ReactApplicationContext reactContext, String defaultUserAgent) {
- this(reactContext, defaultUserAgent, OkHttpClientProvider.getOkHttpClient());
+ public NetworkingModule(ReactApplicationContext context, String defaultUserAgent) {
+ this(context, defaultUserAgent, OkHttpClientProvider.getCookieAwareOkHttpClient(context));
}
public NetworkingModule(ReactApplicationContext reactContext, OkHttpClient client) {
@@ -100,6 +101,11 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
public void onCatalystInstanceDestroy() {
mShuttingDown = true;
mClient.cancel(null);
+
+ CookieHandler cookieHandler = mClient.getCookieHandler();
+ if (cookieHandler instanceof ForwardingCookieHandler) {
+ ((ForwardingCookieHandler) cookieHandler).destroy();
+ }
}
@ReactMethod
@@ -311,6 +317,14 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
}.execute();
}
+ @ReactMethod
+ public void clearCookies(com.facebook.react.bridge.Callback callback) {
+ CookieHandler cookieHandler = mClient.getCookieHandler();
+ if (cookieHandler instanceof ForwardingCookieHandler) {
+ ((ForwardingCookieHandler) cookieHandler).clearCookies(callback);
+ }
+ }
+
private @Nullable MultipartBuilder constructMultipartBody(
ReadableArray body,
String contentType,
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java
index fb7002013..699ffa525 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java
@@ -9,7 +9,12 @@
package com.facebook.react.modules.network;
+import javax.annotation.Nullable;
+
import java.util.concurrent.TimeUnit;
+
+import com.facebook.react.bridge.ReactContext;
+
import com.squareup.okhttp.OkHttpClient;
/**
@@ -19,18 +24,33 @@ import com.squareup.okhttp.OkHttpClient;
public class OkHttpClientProvider {
// Centralized OkHttpClient for all networking requests.
- private static OkHttpClient sClient;
+ private static @Nullable OkHttpClient sClient;
+ private static ForwardingCookieHandler sCookieHandler;
public static OkHttpClient getOkHttpClient() {
if (sClient == null) {
- // TODO: #7108751 plug in stetho
- sClient = new OkHttpClient();
-
- // No timeouts by default
- sClient.setConnectTimeout(0, TimeUnit.MILLISECONDS);
- sClient.setReadTimeout(0, TimeUnit.MILLISECONDS);
- sClient.setWriteTimeout(0, TimeUnit.MILLISECONDS);
+ sClient = createClient();
}
return sClient;
}
+
+ public static OkHttpClient getCookieAwareOkHttpClient(ReactContext context) {
+ if (sCookieHandler == null) {
+ sCookieHandler = new ForwardingCookieHandler(context);
+ getOkHttpClient().setCookieHandler(sCookieHandler);
+ }
+ return getOkHttpClient();
+ }
+
+ private static OkHttpClient createClient() {
+ // TODO: #7108751 plug in stetho
+ OkHttpClient client = new OkHttpClient();
+
+ // No timeouts by default
+ client.setConnectTimeout(0, TimeUnit.MILLISECONDS);
+ client.setReadTimeout(0, TimeUnit.MILLISECONDS);
+ client.setWriteTimeout(0, TimeUnit.MILLISECONDS);
+
+ return client;
+ }
}