Files
openide/platform/ui.jcef/jcef/JBCefCookieManager.java
Daniil Ovchinnikov 36b34c4e18 extract intellij.platform.ui.jcef module
GitOrigin-RevId: 560932e829c0bbf85d51b22a80963a978cf89553
2024-09-03 17:43:45 +00:00

447 lines
16 KiB
Java

// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ui.jcef;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Ref;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.containers.ContainerUtil;
import org.cef.callback.CefCookieVisitor;
import org.cef.misc.BoolRef;
import org.cef.network.CefCookie;
import org.cef.network.CefCookieManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntFunction;
import java.util.function.Supplier;
import static com.intellij.openapi.util.Clock.getTime;
import static com.intellij.util.ObjectUtils.notNull;
/**
* A wrapper over {@link CefCookieManager}.
* <p>
* Use {@link #getCookies(String, Boolean)} for retrieving cookies.
* <p>
* Use {@link #setCookie(String, JBCefCookie)} for setting cookie.
* <p>
* Use {@link #deleteCookies(String, String)} for deleting cookies.
*
* @author Aleksey.Rostovskiy
* @author tav
*/
public final class JBCefCookieManager {
private static final int DEFAULT_TIMEOUT_MS = 200;
private static final int BOUNCE_TIMEOUT_MS = 50;
private static final Logger LOG = Logger.getInstance(JBCefCookieManager.class);
private final ReentrantLock myLock = new ReentrantLock();
private final ExecutorService myExecutorService = AppExecutorUtil.getAppScheduledExecutorService();
private final @NotNull CefCookieManager myCefCookieManager;
static {
// assure initialized
JBCefApp.getInstance();
}
public JBCefCookieManager() {
this(CefCookieManager.getGlobalManager());
}
JBCefCookieManager(@NotNull CefCookieManager cefCookieManager) {
myCefCookieManager = cefCookieManager;
}
@SuppressWarnings("unused")
public @NotNull CefCookieManager getCefCookieManager() {
return myCefCookieManager;
}
/**
* Retrieves cookies asynchronously.
*
* @param url filter by the given URL scheme, host, domain and path.
* @param includeHttpOnly include only true HTTP-only cookies.
* @return a future with the list of {@link JBCefCookie} which can be empty if cookies cannot be accessed or do not exist
*/
public @NotNull Future<@NotNull List<JBCefCookie>> getCookies(@Nullable String url, @Nullable Boolean includeHttpOnly) {
JBCookieVisitor cookieVisitor = new JBCookieVisitor();
boolean result;
if (url != null) {
result = myCefCookieManager.visitUrlCookies(url, notNull(includeHttpOnly, Boolean.FALSE), cookieVisitor);
}
else {
result = myCefCookieManager.visitAllCookies(cookieVisitor);
}
if (!result) {
LOG.debug("Cookies cannot be accessed");
return CompletableFuture.completedFuture(Collections.emptyList());
}
Ref<Future<@NotNull List<JBCefCookie>>> futureRef = new Ref<>();
futureRef.set(myExecutorService.submit(() -> cookieVisitor.get(() -> futureRef.get() == null || !futureRef.get().isDone())));
return futureRef.get();
}
/**
* @see JBCefCookieManager#getCookies(String, Boolean, Integer)
* @deprecated use {@link #getCookies(String, Boolean)}
*/
@Deprecated
public @NotNull List<JBCefCookie> getCookies() {
return getCookies(null, false, null);
}
/**
* WARNING: The method can lead to a freeze when called from a browser callback.
* <p>
* Gets cookies. Underlying native method is asynchronous.
* This method is executed with synchronization and can take up to {@code maxTimeToWait} ms.
*
* @param url filter by the given url scheme, host, domain and path.
* @param includeHttpOnly include only true HTTP-only cookies.
* @param maxTimeToWait time to wait getting cookies in ms, or default
* @return list of {@link JBCefCookie} or null if cookies cannot be accessed
*
* @deprecated use {@link #getCookies(String, Boolean)}
*/
@Deprecated(forRemoval = true)
private @NotNull List<JBCefCookie> getCookies(@Nullable String url, @Nullable Boolean includeHttpOnly, @Nullable Integer maxTimeToWait) {
boolean httpOnly = notNull(includeHttpOnly, Boolean.FALSE);
JBCookieVisitor cookieVisitor = new JBCookieVisitor();
boolean result;
if (url != null) {
result = myCefCookieManager.visitUrlCookies(url, httpOnly, cookieVisitor);
}
else {
result = myCefCookieManager.visitAllCookies(cookieVisitor);
}
if (!result) {
LOG.debug("Cookies cannot be accessed");
return Collections.emptyList();
}
return cookieVisitor.get(notNull(maxTimeToWait, DEFAULT_TIMEOUT_MS));
}
/**
* Sets a cookie asynchronously given a valid URL and explicit user-provided cookie attributes.
* The method expects each attribute to be well-formed. It will check for disallowed characters
* (e.g. the ';' character is disallowed within the cookie value attribute) and fail without setting
* the cookie if such characters are found.
* <p>
* It's recommended that a caller of the method either waits for the returned {@code Future} to complete
* or cancels it when no confirmation of the success is required. Otherwise, it is possible that
* the confirmation task performs infinitely in case something went wrong with the setting.
*
* @param url the cookie URL (should match the cookie's domain)
* @param jbCefCookie the cookie
* @return a future with {@code false} if an invalid URL is specified or if cookies cannot be accessed.
*/
public @NotNull Future<@NotNull Boolean> setCookie(@NotNull String url, @NotNull JBCefCookie jbCefCookie) {
if (!checkArgs(url, jbCefCookie)) {
return CompletableFuture.completedFuture(false);
}
if (!myCefCookieManager.setCookie(url, jbCefCookie.getCefCookie())) {
LOG.error("Posting task to set cookie is failed");
return CompletableFuture.completedFuture(false);
}
Ref<Future<@NotNull Boolean>> futureRef = new Ref<>();
futureRef.set(myExecutorService.submit(() -> {
while (futureRef.get() == null || !futureRef.get().isDone()) {
if (getCookies(url, null, BOUNCE_TIMEOUT_MS).contains(jbCefCookie)) {
return true;
}
}
return false;
}));
return futureRef.get();
}
/**
* @see JBCefCookieManager#setCookie(String, JBCefCookie, Integer)
*
* @deprecated use {@link #setCookie(String, JBCefCookie)}
*/
@Deprecated
public boolean setCookie(@NotNull String url, @NotNull JBCefCookie jbCefCookie, boolean doSync) {
if (doSync) {
return setCookie(url, jbCefCookie, null);
}
else {
return myCefCookieManager.setCookie(url, jbCefCookie.getCefCookie());
}
}
/**
* WARNING: The method can lead to a freeze when called from a browser callback.
* <p>
* Sets a cookie given a valid URL and explicit user-provided cookie attributes.
* Underlying native method {@link CefCookieManager#setCookie(String, CefCookie)} is asynchronous.
* This method is synchronous and will wait up to `maxTimeToWait` ms.
*
* @param maxTimeToWait time to wait setting cookie in ms, or default
* @return {@code true} if setting the cookie was successful.
*
* @deprecated use {@link #setCookie(String, JBCefCookie)}
*/
@Deprecated(forRemoval = true)
private boolean setCookie(@NotNull String url, @NotNull JBCefCookie jbCefCookie, @Nullable Integer maxTimeToWait) {
if (!checkArgs(url, jbCefCookie)) return false;
int timeout = notNull(maxTimeToWait, DEFAULT_TIMEOUT_MS);
IntFunction<Boolean> checkFunction = (timeoutForCheck) -> getCookies(url, null, timeoutForCheck).contains(jbCefCookie);
myLock.lock();
try {
Future<@NotNull Boolean> future = myExecutorService.submit(() -> {
if (checkFunction.apply(timeout / 2)) {
LOG.debug("Cookie is already set");
return true;
}
if (!myCefCookieManager.setCookie(url, jbCefCookie.getCefCookie())) {
LOG.error("Posting task to set cookie is failed");
return false;
}
while (myLock.isLocked()) {
boolean result = checkFunction.apply(timeout / 2);
if (result) return true;
}
return false;
});
try {
return future.get(timeout, TimeUnit.MILLISECONDS);
}
catch (TimeoutException e) {
LOG.error("Cookie setting took more than " + timeout + " ms");
return false;
}
catch (InterruptedException e) {
LOG.error("Cookie setting is interrupted");
return false;
}
}
catch (Exception e) {
LOG.error(e);
return false;
}
finally {
myLock.unlock();
}
}
private static boolean checkArgs(@NotNull String url, @NotNull JBCefCookie jbCefCookie) {
try {
URI uri = new URI(url);
String scheme = uri.getScheme();
String domain = uri.getHost();
domain = domain.startsWith("www") ? domain : "." + domain;
if (scheme.equals("https") && !jbCefCookie.isSecure()) {
LOG.warn("Cannot set cookie without secure flag for HTTPS web-site");
return false;
}
if (!domain.contains(jbCefCookie.getDomain())) {
LOG.warn("Cookie domain `" + jbCefCookie.getDomain() + "` doesn't match URL host `" + domain + "`");
return false;
}
}
catch (URISyntaxException e) {
LOG.error(e);
return false;
}
return true;
}
/**
* Deletes asynchronously all cookies that match the specified parameters. If both {@code url} and {@code cookieName} values
* are specified all host and domain cookies matching both will be deleted. If only {@code url} is
* specified all host cookies (but not domain cookies) irrespective of path will be deleted. If
* {@code url} is empty all cookies for all hosts and domains will be deleted. Cookies can alternately
* be deleted using the visit*Cookies() methods.
* <p>
* It's recommended that a caller of the method either waits for the returned {@code Future} to complete
* or cancels it when no confirmation of the success is required. Otherwise, it is possible that
* the confirmation task performs infinitely in case something went wrong with the deletion.
*
* @param url The cookie URL to delete or {@code null}.
* @param cookieName The cookie name to delete or {@code null}.
* @return a future with {@code false} if a non-empty invalid URL is specified or if cookies cannot be accessed.
*/
public @NotNull Future<@NotNull Boolean> deleteCookies(@Nullable String url, @Nullable String cookieName) {
if (!myCefCookieManager.deleteCookies(url, cookieName)) {
LOG.error("Posting task to delete cookies is failed");
return CompletableFuture.completedFuture(false);
}
Ref<Future<@NotNull Boolean>> futureRef = new Ref<>();
futureRef.set(myExecutorService.submit(() -> {
while (futureRef.get() == null || !futureRef.get().isDone()) {
if (!ContainerUtil.exists(getCookies(url, false, BOUNCE_TIMEOUT_MS),
cookie -> cookie.getName().equals(cookieName)))
{
return true;
}
}
return false;
}));
return futureRef.get();
}
/**
* Deletes all cookies for all hosts and domains.
*
* @param doSync if false - underlying asynchronous native method {@link CefCookieManager#deleteCookies(String, String)} is used,
* true - synchronous {@link JBCefCookieManager#deleteCookies(String, String, IntFunction, Integer)}.
* @deprecated use {@link #deleteCookies(String, String)}
*/
@Deprecated
public boolean deleteCookies(boolean doSync) {
if (doSync) {
return deleteCookies(null, null, (timeout) -> getCookies(null, false, timeout).isEmpty(), null);
}
else {
return myCefCookieManager.deleteCookies("", "");
}
}
/**
* Deletes all host cookies (but not domain cookies) irrespective of path will be deleted.
*
* @param doSync if false - underlying asynchronous native method {@link CefCookieManager#deleteCookies(String, String)} is used,
* true - synchronous {@link JBCefCookieManager#deleteCookies(String, String, IntFunction, Integer)}.
*
* @deprecated use {@link #deleteCookies(String, String)}
*/
@Deprecated
public boolean deleteCookies(@Nullable String url, boolean doSync) {
if (doSync) {
return deleteCookies(url, "", (timeout) -> getCookies(url, false, timeout).isEmpty(), null);
}
else {
return myCefCookieManager.deleteCookies(url, "");
}
}
/**
* Deletes synchronously all host and domain cookies matching |url| and |cookieName| values
* with specified function for checking and specified timeout.
* <p>
* Underlying method {@link CefCookieManager#deleteCookies(String, String)} is asynchronous.
* This method is synchronous and will wait up to `maxTimeToWait` ms.
*
* @param checkFunction function will be used for checking whether cookie deleted
* @param maxTimeToWait time to wait setting cookie in ms, default value is
* {@link JBCefCookieManager#DEFAULT_TIMEOUT_MS} ms.
* @return true if deleting cookies was successful.
*/
private boolean deleteCookies(@Nullable String url,
@Nullable String cookieName,
@NotNull IntFunction<Boolean> checkFunction,
@Nullable Integer maxTimeToWait) {
int timeout = notNull(maxTimeToWait, DEFAULT_TIMEOUT_MS);
myLock.lock();
try {
Future<@NotNull Boolean> future = myExecutorService.submit(() -> {
if (checkFunction.apply(timeout / 2)) {
LOG.debug("No cookies to be deleted");
return true;
}
if (!myCefCookieManager.deleteCookies(url, cookieName)) {
LOG.error("Posting task to delete cookies is failed");
return false;
}
while (myLock.isLocked()) {
boolean result = checkFunction.apply(timeout / 2);
if (result) return true;
}
return false;
});
try {
return future.get(timeout, TimeUnit.MILLISECONDS);
}
catch (TimeoutException e) {
LOG.error("Cookie deleting took more than " + timeout + " ms");
return false;
}
catch (InterruptedException e) {
LOG.error("Cookie deleting is interrupted");
return false;
}
}
catch (Exception e) {
LOG.error(e);
return false;
}
finally {
myLock.unlock();
}
}
private static class JBCookieVisitor implements CefCookieVisitor {
final @NotNull List<JBCefCookie> myCefCookies = Collections.synchronizedList(new ArrayList<>());
final @NotNull CountDownLatch myCountDownLatch;
long myStartTime = getTime();
JBCookieVisitor() {
myCountDownLatch = new CountDownLatch(1);
}
@Override
// TODO[hatari]: This method may never be called if no cookies are found.
// So CountDownLatch can't countDown() well except timeout.
public boolean visit(CefCookie cookie, int count, int total, BoolRef delete) {
myCefCookies.add(new JBCefCookie(cookie));
if (myCefCookies.size() >= total) {
myCountDownLatch.countDown();
}
return true;
}
public @NotNull List<JBCefCookie> get(@NotNull Supplier<Boolean> condition) {
while (condition.get()) {
try {
if (myCountDownLatch.await(BOUNCE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
return myCefCookies;
}
}
catch (InterruptedException e) {
LOG.error("Cookie visiting is interrupted");
break;
}
}
return Collections.emptyList();
}
public @NotNull List<JBCefCookie> get(int timeout) {
boolean result;
try {
result = myCountDownLatch.await(timeout, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
LOG.error("Cookie visiting is interrupted");
return Collections.emptyList();
}
long timeSpent = getTime() - myStartTime;
if (!result) {
LOG.debug("Timeout for cookie visiting is reached, " + timeSpent + " ms time spent");
}
else {
LOG.debug("Cookie getting took " + timeSpent + " ms");
}
return myCefCookies;
}
}
}