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