chromium/android_webview/java/src/org/chromium/android_webview/AwCookieManager.java

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview;

import android.os.Handler;
import android.os.Looper;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.Callback;
import org.chromium.base.library_loader.LibraryLoader;

import java.util.Arrays;
import java.util.List;

/**
 * AwCookieManager manages cookies according to RFC2109 spec.
 *
 * Methods in this class are thread safe.
 *
 * The default profile's cookie manager has a singleton lifetime, whereas a non-default
 * profile has a cookie manager that is lifetime scoped to the profile.
 */
@JNINamespace("android_webview")
public final class AwCookieManager {
    private final long mNativeCookieManager;

    /**
     * The class loader will take care of synchronization as each class
     * is only loaded once at the time it is needed. Meaning that the first time
     * {@link AwCookieManager#getDefaultCookieManager()} is called, the static instance
     * of the default cookie manager will be initialized within the holder class.
     */
    private static final class DefaultCookieManagerHolder {
        private static final AwCookieManager sDefaultCookieManager = new AwCookieManager();
    }

    public static AwCookieManager getDefaultCookieManager() {
        return DefaultCookieManagerHolder.sDefaultCookieManager;
    }

    @VisibleForTesting
    public AwCookieManager() {
        this(AwCookieManagerJni.get().getDefaultCookieManager());
    }

    public AwCookieManager(long nativeCookieManager) {
        LibraryLoader.getInstance().ensureInitialized();
        mNativeCookieManager = nativeCookieManager;
    }

    @CalledByNative
    private static AwCookieManager create(long nativeCookieManager) {
        return new AwCookieManager(nativeCookieManager);
    }

    /**
     * Control whether cookie is enabled or disabled
     * @param accept TRUE if accept cookie
     */
    public void setAcceptCookie(boolean accept) {
        AwCookieManagerJni.get()
                .setShouldAcceptCookies(mNativeCookieManager, AwCookieManager.this, accept);
    }

    /**
     * Return whether cookie is enabled
     * @return TRUE if accept cookie
     */
    public boolean acceptCookie() {
        return AwCookieManagerJni.get()
                .getShouldAcceptCookies(mNativeCookieManager, AwCookieManager.this);
    }

    /** Synchronous version of setCookie. */
    public void setCookie(String url, String value) {
        UrlValue pair = fixupUrlValue(url, value);
        AwCookieManagerJni.get()
                .setCookieSync(mNativeCookieManager, AwCookieManager.this, pair.mUrl, pair.mValue);
    }

    /** Deprecated synchronous version of removeSessionCookies. */
    public void removeSessionCookies() {
        AwCookieManagerJni.get()
                .removeSessionCookiesSync(mNativeCookieManager, AwCookieManager.this);
    }

    /** Deprecated synchronous version of removeAllCookies. */
    public void removeAllCookies() {
        AwCookieManagerJni.get().removeAllCookiesSync(mNativeCookieManager, AwCookieManager.this);
    }

    /**
     * Set cookie for a given url. The old cookie with same host/path/name will
     * be removed. The new cookie will be added if it is not expired or it does
     * not have expiration which implies it is session cookie.
     * @param url The url which cookie is set for.
     * @param value The value for set-cookie: in http response header.
     * @param callback A callback called with the success status after the cookie is set.
     */
    public void setCookie(final String url, final String value, final Callback<Boolean> callback) {
        try {
            UrlValue pair = fixupUrlValue(url, value);
            AwCookieManagerJni.get()
                    .setCookie(
                            mNativeCookieManager,
                            AwCookieManager.this,
                            pair.mUrl,
                            pair.mValue,
                            new CookieCallback(callback));
        } catch (IllegalStateException e) {
            throw new IllegalStateException(
                    "SetCookie must be called on a thread with a running Looper.");
        }
    }

    /**
     * Get cookie(s) for a given url so that it can be set to "cookie:" in http
     * request header.
     * @param url The url needs cookie
     * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
     */
    public String getCookie(final String url) {
        String cookie =
                AwCookieManagerJni.get().getCookie(mNativeCookieManager, AwCookieManager.this, url);
        // Return null if the string is empty to match legacy behavior
        return cookie == null || cookie.trim().isEmpty() ? null : cookie;
    }

    /**
     * Get the attributes of any cookie(s) for a given url.
     * @param url The url for which the cookies are set.
     * @return The cookies as a list of Strings formatted like http set cookie headers.
     */
    public List<String> getCookieInfo(final String url) {
        String[] cookies =
                AwCookieManagerJni.get()
                        .getCookieInfo(mNativeCookieManager, AwCookieManager.this, url);
        return Arrays.asList(cookies);
    }

    /**
     * Remove all session cookies, the cookies without an expiration date.
     * The value of the callback is true iff at least one cookie was removed.
     * @param callback A callback called after the cookies (if any) are removed.
     */
    public void removeSessionCookies(Callback<Boolean> callback) {
        try {
            AwCookieManagerJni.get()
                    .removeSessionCookies(
                            mNativeCookieManager,
                            AwCookieManager.this,
                            new CookieCallback(callback));
        } catch (IllegalStateException e) {
            throw new IllegalStateException(
                    "removeSessionCookies must be called on a thread with a running Looper.");
        }
    }

    /**
     * Remove all cookies.
     * The value of the callback is true iff at least one cookie was removed.
     * @param callback A callback called after the cookies (if any) are removed.
     */
    public void removeAllCookies(Callback<Boolean> callback) {
        try {
            AwCookieManagerJni.get()
                    .removeAllCookies(
                            mNativeCookieManager,
                            AwCookieManager.this,
                            new CookieCallback(callback));
        } catch (IllegalStateException e) {
            throw new IllegalStateException(
                    "removeAllCookies must be called on a thread with a running Looper.");
        }
    }

    /**  Return true if there are stored cookies. */
    public boolean hasCookies() {
        return AwCookieManagerJni.get().hasCookies(mNativeCookieManager, AwCookieManager.this);
    }

    /** Remove all expired cookies */
    public void removeExpiredCookies() {
        AwCookieManagerJni.get().removeExpiredCookies(mNativeCookieManager, AwCookieManager.this);
    }

    public void flushCookieStore() {
        AwCookieManagerJni.get().flushCookieStore(mNativeCookieManager, AwCookieManager.this);
    }

    /** Whether cookies are accepted for file scheme URLs. */
    public boolean allowFileSchemeCookies() {
        return AwCookieManagerJni.get()
                .getAllowFileSchemeCookies(mNativeCookieManager, AwCookieManager.this);
    }

    /**
     * Sets whether cookies are accepted for file scheme URLs.
     *
     * Use of cookies with file scheme URLs is potentially insecure. Do not use this feature unless
     * you can be sure that no unintentional sharing of cookie data can take place.
     * <p>
     * Note that calls to this method will have no effect if made after a WebView or CookieManager
     * instance has been created.
     */
    public void setAcceptFileSchemeCookies(boolean accept) {
        AwCookieManagerJni.get()
                .setAllowFileSchemeCookies(mNativeCookieManager, AwCookieManager.this, accept);
    }

    /**
     * Sets whether cookies for insecure schemes (http:) are permitted to include the "Secure"
     * directive.
     */
    public void setWorkaroundHttpSecureCookiesForTesting(boolean allow) {
        AwCookieManagerJni.get()
                .setWorkaroundHttpSecureCookiesForTesting(
                        mNativeCookieManager, AwCookieManager.this, allow);
    }

    /**
     * CookieCallback is a bridge that knows how to call a Callback on its original thread.
     * We need to arrange for the users Callback#onResult to be called on the original
     * thread after the work is done. When the API is called we construct a CookieCallback which
     * remembers the handler of the current thread. Later the native code uses
     * the native method |RunBooleanCallbackAndroid| to call CookieCallback#onResult which posts a
     * Runnable on the handler of the original thread which in turn calls Callback#onResult.
     */
    static class CookieCallback implements Callback<Boolean> {
        @Nullable Callback<Boolean> mCallback;
        @Nullable Handler mHandler;

        public CookieCallback(@Nullable Callback<Boolean> callback) {
            if (callback != null) {
                if (Looper.myLooper() == null) {
                    throw new IllegalStateException(
                            "new CookieCallback should be called on "
                                    + "a thread with a running Looper.");
                }
                mCallback = callback;
                mHandler = new Handler();
            }
        }

        @Override
        public void onResult(final Boolean result) {
            if (mHandler == null) return;
            assert mCallback != null;
            mHandler.post(mCallback.bind(result));
        }
    }

    /** A tuple to hold a URL and Value when setting a cookie. */
    private static class UrlValue {
        public String mUrl;
        public String mValue;

        public UrlValue(String url, String value) {
            mUrl = url;
            mValue = value;
        }
    }

    private static String appendDomain(String value, String domain) {
        // Prefer the explicit Domain attribute, if available. We allow any case for "Domain".
        if (value.matches("^.*(?i);[\\t ]*Domain[\\t ]*=.*$")) {
            return value;
        } else if (value.matches("^.*;\\s*$")) {
            return value + " Domain=" + domain;
        }
        return value + "; Domain=" + domain;
    }

    private static UrlValue fixupUrlValue(String url, String value) {
        final String leadingHttpTripleSlashDot = "http:///.";

        // The app passed a domain instead of a real URL (and the glue layer "fixed" it into this
        // form). For backwards compatibility, we fix this into a well-formed URL and add a Domain
        // attribute to the cookie value.
        if (url.startsWith(leadingHttpTripleSlashDot)) {
            String domain = url.substring(leadingHttpTripleSlashDot.length() - 1);
            url = "http://" + url.substring(leadingHttpTripleSlashDot.length());
            value = appendDomain(value, domain);
        }
        return new UrlValue(url, value);
    }

    @NativeMethods
    interface Natives {
        long getDefaultCookieManager();

        void setShouldAcceptCookies(
                long nativeCookieManager, AwCookieManager caller, boolean accept);

        boolean getShouldAcceptCookies(long nativeCookieManager, AwCookieManager caller);

        void setCookie(
                long nativeCookieManager,
                AwCookieManager caller,
                String url,
                String value,
                CookieCallback callback);

        void setCookieSync(
                long nativeCookieManager, AwCookieManager caller, String url, String value);

        String getCookie(long nativeCookieManager, AwCookieManager caller, String url);

        String[] getCookieInfo(long nativeCookieManager, AwCookieManager caller, String url);

        void removeSessionCookies(
                long nativeCookieManager, AwCookieManager caller, CookieCallback callback);

        void removeSessionCookiesSync(long nativeCookieManager, AwCookieManager caller);

        void removeAllCookies(
                long nativeCookieManager, AwCookieManager caller, CookieCallback callback);

        void removeAllCookiesSync(long nativeCookieManager, AwCookieManager caller);

        void removeExpiredCookies(long nativeCookieManager, AwCookieManager caller);

        void flushCookieStore(long nativeCookieManager, AwCookieManager caller);

        boolean hasCookies(long nativeCookieManager, AwCookieManager caller);

        boolean getAllowFileSchemeCookies(long nativeCookieManager, AwCookieManager caller);

        void setAllowFileSchemeCookies(
                long nativeCookieManager, AwCookieManager caller, boolean allow);

        void setWorkaroundHttpSecureCookiesForTesting(
                long nativeCookieManager, AwCookieManager caller, boolean allow);
    }
}