diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/StorageController.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/geckoview/StorageController.java | 405 |
1 files changed, 405 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/StorageController.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/StorageController.java new file mode 100644 index 0000000000..a49cdf26a5 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/StorageController.java @@ -0,0 +1,405 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * vim: ts=4 sw=4 expandtab: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview; + +import android.util.Log; +import androidx.annotation.AnyThread; +import androidx.annotation.LongDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Locale; +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.util.GeckoBundle; +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission; + +/** + * Manage runtime storage data. + * + * <p>Retrieve an instance via {@link GeckoRuntime#getStorageController}. + */ +public final class StorageController { + private static final String LOGTAG = "StorageController"; + + // Keep in sync with GeckoViewStorageController.ClearFlags. + /** Flags used for data clearing operations. */ + public static class ClearFlags { + /** Cookies. */ + public static final long COOKIES = 1 << 0; + + /** Network cache. */ + public static final long NETWORK_CACHE = 1 << 1; + + /** Image cache. */ + public static final long IMAGE_CACHE = 1 << 2; + + /** DOM storages. */ + public static final long DOM_STORAGES = 1 << 4; + + /** Auth tokens and caches. */ + public static final long AUTH_SESSIONS = 1 << 5; + + /** Site permissions. */ + public static final long PERMISSIONS = 1 << 6; + + /** All caches. */ + public static final long ALL_CACHES = NETWORK_CACHE | IMAGE_CACHE; + + /** All site settings (permissions, content preferences, security settings, etc.). */ + public static final long SITE_SETTINGS = 1 << 7 | PERMISSIONS; + + /** All site-related data (cookies, storages, caches, permissions, etc.). */ + public static final long SITE_DATA = + 1 << 8 | COOKIES | DOM_STORAGES | ALL_CACHES | PERMISSIONS | SITE_SETTINGS; + + /** All data. */ + public static final long ALL = 1 << 9; + } + + @Retention(RetentionPolicy.SOURCE) + @LongDef( + flag = true, + value = { + ClearFlags.COOKIES, + ClearFlags.NETWORK_CACHE, + ClearFlags.IMAGE_CACHE, + ClearFlags.DOM_STORAGES, + ClearFlags.AUTH_SESSIONS, + ClearFlags.PERMISSIONS, + ClearFlags.ALL_CACHES, + ClearFlags.SITE_SETTINGS, + ClearFlags.SITE_DATA, + ClearFlags.ALL + }) + public @interface StorageControllerClearFlags {} + + /** + * Clear data for all hosts. + * + * <p>Note: Any open session may re-accumulate previously cleared data. To ensure that no + * persistent data is left behind, you need to close all sessions prior to clearing data. + * + * @param flags Combination of {@link ClearFlags}. + * @return A {@link GeckoResult} that will complete when clearing has finished. + */ + @AnyThread + public @NonNull GeckoResult<Void> clearData(final @StorageControllerClearFlags long flags) { + final GeckoBundle bundle = new GeckoBundle(1); + bundle.putLong("flags", flags); + + return EventDispatcher.getInstance().queryVoid("GeckoView:ClearData", bundle); + } + + /** + * Clear data owned by the given host. Clearing data for a host will not clear data created by its + * third-party origins. + * + * <p>Note: Any open session may re-accumulate previously cleared data. To ensure that no + * persistent data is left behind, you need to close all sessions prior to clearing data. + * + * @param host The host to be used. + * @param flags Combination of {@link ClearFlags}. + * @return A {@link GeckoResult} that will complete when clearing has finished. + */ + @AnyThread + public @NonNull GeckoResult<Void> clearDataFromHost( + final @NonNull String host, final @StorageControllerClearFlags long flags) { + final GeckoBundle bundle = new GeckoBundle(2); + bundle.putString("host", host); + bundle.putLong("flags", flags); + + return EventDispatcher.getInstance().queryVoid("GeckoView:ClearHostData", bundle); + } + + /** + * Clear data owned by the given base domain (eTLD+1). Clearing data for a base domain will also + * clear any associated third-party storage. This includes clearing for third-parties embedded by + * the domain and for the given domain embedded under other sites. + * + * <p>Note: Any open session may re-accumulate previously cleared data. To ensure that no + * persistent data is left behind, you need to close all sessions prior to clearing data. + * + * @param baseDomain The base domain to be used. + * @param flags Combination of {@link ClearFlags}. + * @return A {@link GeckoResult} that will complete when clearing has finished. + */ + @AnyThread + public @NonNull GeckoResult<Void> clearDataFromBaseDomain( + final @NonNull String baseDomain, final @StorageControllerClearFlags long flags) { + final GeckoBundle bundle = new GeckoBundle(2); + bundle.putString("baseDomain", baseDomain); + bundle.putLong("flags", flags); + + return EventDispatcher.getInstance().queryVoid("GeckoView:ClearBaseDomainData", bundle); + } + + /** + * Clear data for the given context ID. Use {@link GeckoSessionSettings.Builder#contextId}.to set + * a context ID for a session. + * + * <p>Note: Any open session may re-accumulate previously cleared data. To ensure that no + * persistent data is left behind, you need to close all sessions for the given context prior to + * clearing data. + * + * @param contextId The context ID for the storage data to be deleted. + */ + @AnyThread + public void clearDataForSessionContext(final @NonNull String contextId) { + final GeckoBundle bundle = new GeckoBundle(1); + bundle.putString("contextId", createSafeSessionContextId(contextId)); + + EventDispatcher.getInstance().dispatch("GeckoView:ClearSessionContextData", bundle); + } + + /* package */ static @Nullable String createSafeSessionContextId( + final @Nullable String contextId) { + if (contextId == null) { + return null; + } + if (contextId.isEmpty()) { + // Let's avoid empty strings for Gecko. + return "gvctxempty"; + } + // We don't want to restrict the session context ID string options, so to + // ensure that the string is safe for Gecko processing, we translate it to + // its hex representation. + return String.format("gvctx%x", new BigInteger(contextId.getBytes())).toLowerCase(Locale.ROOT); + } + + /* package */ static @Nullable String retrieveUnsafeSessionContextId( + final @Nullable String contextId) { + if (contextId == null || contextId.isEmpty()) { + return null; + } + if ("gvctxempty".equals(contextId)) { + return ""; + } + final byte[] bytes = new BigInteger(contextId.substring(5), 16).toByteArray(); + return new String(bytes, Charset.forName("UTF-8")); + } + + /** + * Get all currently stored permissions. + * + * @return A {@link GeckoResult} that will complete with a list of all currently stored {@link + * ContentPermission}s. + */ + @AnyThread + public @NonNull GeckoResult<List<ContentPermission>> getAllPermissions() { + return EventDispatcher.getInstance() + .queryBundle("GeckoView:GetAllPermissions") + .map( + bundle -> { + final GeckoBundle[] permsArray = bundle.getBundleArray("permissions"); + return ContentPermission.fromBundleArray(permsArray); + }); + } + + /** + * Get all currently stored permissions for a given URI and default (unset) context ID, in normal + * mode This API will be deprecated in the future + * https://bugzilla.mozilla.org/show_bug.cgi?id=1797379 + * + * @param uri A String representing the URI to get permissions for. + * @return A {@link GeckoResult} that will complete with a list of all currently stored {@link + * ContentPermission}s for the URI. + */ + @AnyThread + public @NonNull GeckoResult<List<ContentPermission>> getPermissions(final @NonNull String uri) { + return getPermissions(uri, null, false); + } + + /** + * Get all currently stored permissions for a given URI and default (unset) context ID. + * + * @param uri A String representing the URI to get permissions for. + * @param privateMode indicate where the {@link ContentPermission}s should be in private or normal + * mode. + * @return A {@link GeckoResult} that will complete with a list of all currently stored {@link + * ContentPermission}s for the URI. + */ + @AnyThread + public @NonNull GeckoResult<List<ContentPermission>> getPermissions( + final @NonNull String uri, final boolean privateMode) { + return getPermissions(uri, null, privateMode); + } + + /** + * Get all currently stored permissions for a given URI and context ID. + * + * @param uri A String representing the URI to get permissions for. + * @param contextId A String specifying the context ID. + * @param privateMode indicate where the {@link ContentPermission}s should be in private or normal + * mode + * @return A {@link GeckoResult} that will complete with a list of all currently stored {@link + * ContentPermission}s for the URI. + */ + @AnyThread + public @NonNull GeckoResult<List<ContentPermission>> getPermissions( + final @NonNull String uri, final @Nullable String contextId, final boolean privateMode) { + final GeckoBundle msg = new GeckoBundle(2); + final int privateBrowsingId = (privateMode) ? 1 : 0; + msg.putString("uri", uri); + msg.putString("contextId", createSafeSessionContextId(contextId)); + msg.putInt("privateBrowsingId", privateBrowsingId); + return EventDispatcher.getInstance() + .queryBundle("GeckoView:GetPermissionsByURI", msg) + .map( + bundle -> { + final GeckoBundle[] permsArray = bundle.getBundleArray("permissions"); + return ContentPermission.fromBundleArray(permsArray); + }); + } + + /** + * Set a new value for an existing permission. + * + * <p>Note: in private browsing, this value will only be cleared at the end of the session to add + * permanent permissions in private browsing, you can use {@link + * #setPrivateBrowsingPermanentPermission}. + * + * @param perm A {@link ContentPermission} that you wish to update the value of. + * @param value The new value for the permission. + */ + @AnyThread + public void setPermission( + final @NonNull ContentPermission perm, final @ContentPermission.Value int value) { + setPermissionInternal(perm, value, /* allowPermanentPrivateBrowsing */ false); + } + + /** + * Set a permanent value for a permission in a private browsing session. + * + * <p>Normally permissions in private browsing are cleared at the end of the session. This method + * allows you to set a permanent permission bypassing this behavior. + * + * <p>Note: permanent permissions in private browsing are web discoverable and might make the user + * more easily trackable. + * + * @see #setPermission + * @param perm A {@link ContentPermission} that you wish to update the value of. + * @param value The new value for the permission. + */ + @AnyThread + public void setPrivateBrowsingPermanentPermission( + final @NonNull ContentPermission perm, final @ContentPermission.Value int value) { + setPermissionInternal(perm, value, /* allowPermanentPrivateBrowsing */ true); + } + + private void setPermissionInternal( + final @NonNull ContentPermission perm, + final @ContentPermission.Value int value, + final boolean allowPermanentPrivateBrowsing) { + if (perm.permission == GeckoSession.PermissionDelegate.PERMISSION_TRACKING + && value == ContentPermission.VALUE_PROMPT) { + Log.w(LOGTAG, "Cannot set a tracking permission to VALUE_PROMPT, aborting."); + return; + } + final GeckoBundle msg = perm.toGeckoBundle(); + msg.putInt("newValue", value); + msg.putBoolean("allowPermanentPrivateBrowsing", allowPermanentPrivateBrowsing); + EventDispatcher.getInstance().dispatch("GeckoView:SetPermission", msg); + } + + /** + * Set a permanent {@link ContentBlocking.CBCookieBannerMode} for the given uri and browsing mode. + * + * @param uri An uri for which you want change the {@link ContentBlocking.CBCookieBannerMode} + * value. + * @param mode A new {@link ContentBlocking.CBCookieBannerMode} for the given uri. + * @param isPrivateBrowsing Indicates in which browsing mode the given {@link + * ContentBlocking.CBCookieBannerMode} should be applied. + * @return A {@link GeckoResult} that will complete when the mode has been set. + */ + @AnyThread + public @NonNull GeckoResult<Void> setCookieBannerModeForDomain( + final @NonNull String uri, + final @ContentBlocking.CBCookieBannerMode int mode, + final boolean isPrivateBrowsing) { + final GeckoBundle data = new GeckoBundle(3); + data.putString("uri", uri); + data.putInt("mode", mode); + data.putBoolean("allowPermanentPrivateBrowsing", false); + data.putBoolean("isPrivateBrowsing", isPrivateBrowsing); + return EventDispatcher.getInstance().queryVoid("GeckoView:SetCookieBannerModeForDomain", data); + } + + /** + * Set a permanent {@link ContentBlocking.CBCookieBannerMode} for the given uri in private mode. + * + * @param uri for which you want to change the {@link ContentBlocking.CBCookieBannerMode} value. + * @param mode A new {@link ContentBlocking.CBCookieBannerMode} for the given uri. + * @return A {@link GeckoResult} that will complete when the mode has been set. + */ + @AnyThread + public @NonNull GeckoResult<Void> setCookieBannerModeAndPersistInPrivateBrowsingForDomain( + final @NonNull String uri, final @ContentBlocking.CBCookieBannerMode int mode) { + final GeckoBundle data = new GeckoBundle(3); + data.putString("uri", uri); + data.putInt("mode", mode); + data.putBoolean("allowPermanentPrivateBrowsing", true); + return EventDispatcher.getInstance().queryVoid("GeckoView:SetCookieBannerModeForDomain", data); + } + + /** + * Removes a {@link ContentBlocking.CBCookieBannerMode} for the given uri and and browsing mode. + * + * @param uri An uri for which you want change the {@link ContentBlocking.CBCookieBannerMode} + * value. + * @param isPrivateBrowsing Indicates in which mode the given mode should be applied. + * @return A {@link GeckoResult} that will complete when the mode has been removed. + */ + @AnyThread + public @NonNull GeckoResult<Void> removeCookieBannerModeForDomain( + final @NonNull String uri, final boolean isPrivateBrowsing) { + + final GeckoBundle data = new GeckoBundle(3); + data.putString("uri", uri); + data.putBoolean("isPrivateBrowsing", isPrivateBrowsing); + return EventDispatcher.getInstance() + .queryVoid("GeckoView:RemoveCookieBannerModeForDomain", data); + } + + /** + * Gets the actual {@link ContentBlocking.CBCookieBannerMode} for the given uri and browsing mode. + * + * @param uri An uri for which you want get the {@link ContentBlocking.CBCookieBannerMode}. + * @param isPrivateBrowsing Indicates in which browsing mode the given uri should be. + * @return A {@link GeckoResult} that resolves to a {@link ContentBlocking.CBCookieBannerMode} for + * the given uri and browsing mode. + */ + @AnyThread + public @NonNull @ContentBlocking.CBCookieBannerMode GeckoResult<Integer> + getCookieBannerModeForDomain(final @NonNull String uri, final boolean isPrivateBrowsing) { + + final GeckoBundle data = new GeckoBundle(2); + data.putString("uri", uri); + data.putBoolean("isPrivateBrowsing", isPrivateBrowsing); + return EventDispatcher.getInstance() + .queryBundle("GeckoView:GetCookieBannerModeForDomain", data) + .map(StorageController::cookieBannerModeFromBundle, StorageController::fromQueryException); + } + + private static @ContentBlocking.CBCookieBannerMode int cookieBannerModeFromBundle( + final GeckoBundle bundle) throws Exception { + if (bundle == null) { + throw new Exception("Unable to parse cookie banner mode"); + } + return bundle.getInt("mode"); + } + + private static Throwable fromQueryException(final Throwable exception) { + final EventDispatcher.QueryException queryException = + (EventDispatcher.QueryException) exception; + final Object response = queryException.data; + return new Exception(response.toString()); + } +} |