// 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(); } } }