// 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<String, List<String>> get(URI uri, Map<String, List<String>> 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<String, List<String>> headers) throws IOException {
String url = uri.toString();
for (Map.Entry<String, List<String>> 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<Boolean>(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<Boolean>() {
@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<String> 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<Void, Void>(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();
}
}
}