blob: 4352961cbd189f15596311bc1b1c55635a72850d [file] [log] [blame]
// Copyright 2015 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.components.browser_ui.site_settings;
import static org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge.SITE_WILDCARD;
import androidx.annotation.Nullable;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge.StorageInfoClearedCallback;
import org.chromium.components.content_settings.ContentSettingValues;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.BrowserContextHandle;
import org.chromium.url.GURL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Website is a class for storing information about a website and its associated permissions. */
public final class Website implements WebsiteEntry {
private final WebsiteAddress mOrigin;
private final WebsiteAddress mEmbedder;
/** Indexed by ContentSettingsType. */
private Map<Integer, ContentSettingException> mContentSettingExceptions = new HashMap<>();
/** Indexed by ContentSettingsType. */
private Map<Integer, PermissionInfo> mPermissionInfos = new HashMap<>();
/**
* Indexed by ContentSettingsType. For Permissions like the StorageAccess API that are keyed by
* requesting and embedding site.
*/
private Map<Integer, List<ContentSettingException>> mEmbeddedPermissionInfos = new HashMap<>();
private LocalStorageInfo mLocalStorageInfo;
private FPSCookieInfo mFPSCookieInfo;
private CookiesInfo mCookiesInfo;
private double mZoomFactor;
private final List<StorageInfo> mStorageInfo = new ArrayList<>();
private final List<SharedDictionaryInfo> mSharedDictionaryInfo = new ArrayList<>();
// The collection of chooser-based permissions (e.g. USB device access) granted to this site.
// Each entry declares its own ContentSettingsType and so depending on how this object was
// built this list could contain multiple types of objects.
private final List<ChosenObjectInfo> mObjectInfo = new ArrayList<ChosenObjectInfo>();
private static final String SCHEME_SUFFIX = "://";
/**
* Removes the scheme in a given URL, if present.
*
* Examples:
* - "google.com" -> "google.com"
* - "https://google.com" -> "google.com"
*/
public static String omitProtocolIfPresent(String url) {
if (url.indexOf(SCHEME_SUFFIX) == -1) return url;
return UrlFormatter.formatUrlForDisplayOmitScheme(url);
}
// Returns whether exceptions for this type are retrieved through
// getEmbeddedPermissions().
public static boolean isEmbeddedPermission(@ContentSettingsType.EnumType int type) {
return type == ContentSettingsType.STORAGE_ACCESS;
}
public Website(WebsiteAddress origin, WebsiteAddress embedder) {
mOrigin = origin;
mEmbedder = embedder;
}
public WebsiteAddress getAddress() {
return mOrigin;
}
public WebsiteAddress getEmbedder() {
return mEmbedder;
}
public String getTitle() {
return getMainAddress().getTitle();
}
public boolean representsThirdPartiesOnSite() {
return mOrigin.getTitle().equals(SITE_WILDCARD)
&& mEmbedder != null
&& !mEmbedder.getTitle().equals(SITE_WILDCARD);
}
public WebsiteAddress getMainAddress() {
if (representsThirdPartiesOnSite()) {
return mEmbedder;
}
return mOrigin;
}
/**
* Returns whichever WebsiteAddress is used to provide additional information. This will
* either return null (representing the wildcard) if it a 3P exception, or the mEmbedder
* (which may not be null, such as in the "a.com, b.com" origin combination).
*/
private WebsiteAddress getAdditionalInformationAddress() {
if (representsThirdPartiesOnSite()) return null;
return mEmbedder;
}
/**
* A comparison function for sorting by address (first by origin and then
* by embedder).
*/
public int compareByAddressTo(Website to) {
if (this == to) return 0;
// We want Website instances that represent third parties to be ordered beside other
// Website instances with the same "main site".
int originComparison = getMainAddress().compareTo(to.getMainAddress());
if (originComparison == 0) {
if (getAdditionalInformationAddress() == null) {
return to.getAdditionalInformationAddress() == null ? 0 : -1;
} else if (to.getAdditionalInformationAddress() == null) {
return 1;
}
return getAdditionalInformationAddress()
.compareTo(to.getAdditionalInformationAddress());
}
return originComparison;
}
/**
* A comparison function for sorting by storage (most used first).
* @return which site uses more storage.
*/
public int compareByStorageTo(Website to) {
if (this == to) return 0;
return Long.compare(to.getTotalUsage(), getTotalUsage());
}
/** @return Collection of PermissionInfos stored for the site */
public Collection<PermissionInfo> getPermissionInfos() {
return mPermissionInfos.values();
}
/**
* @return PermissionInfo with permission details of specified type (Camera, Clipboard, etc.).
*/
public PermissionInfo getPermissionInfo(@ContentSettingsType.EnumType int type) {
return mPermissionInfos.get(type);
}
/**
* Set PermissionInfo for permission details of specified type
* (Camera, Clipboard, etc.).
*/
public void setPermissionInfo(PermissionInfo info) {
mPermissionInfos.put(info.getContentSettingsType(), info);
}
public Map<Integer, List<ContentSettingException>> getEmbeddedPermissions() {
return mEmbeddedPermissionInfos;
}
public void addEmbeddedPermission(ContentSettingException info) {
assert (info.getSecondaryPattern() == null && info.isEmbargoed())
|| (info.getSecondaryPattern() != null
&& !info.getSecondaryPattern().equals("*")
&& !info.isEmbargoed());
var list =
mEmbeddedPermissionInfos.computeIfAbsent(
info.getContentSettingType(), k -> new ArrayList<>());
for (var existing_info : list) {
if (existing_info.getContentSettingType() == info.getContentSettingType()
&& existing_info.getPrimaryPattern().equals(info.getPrimaryPattern())
&& existing_info.getSecondaryPattern().equals(info.getSecondaryPattern())) {
// In incognito mode we can have two exceptions with the same pattern. Only keep
// the first one.
return;
}
}
list.add(info);
}
public Collection<ContentSettingException> getContentSettingExceptions() {
return mContentSettingExceptions.values();
}
/** Returns the exception info for this Website for specified type. */
public ContentSettingException getContentSettingException(
@ContentSettingsType.EnumType int type) {
return mContentSettingExceptions.get(type);
}
/** Sets the exception info for this Website for specified type. */
public void setContentSettingException(
@ContentSettingsType.EnumType int type, ContentSettingException exception) {
mContentSettingExceptions.put(type, exception);
}
/**
* Returns the ContentSettingValue associated with the list of embedded exceptions associated
* with the website. Each website object should only have embedded exceptions of a single
* ContentSettingValue.
*
* @param exceptions Website embedded exceptions.
* @return the ContentSettingValue of the list of |exceptions|.
*/
private @ContentSettingValues @Nullable Integer getContentSetting(
List<ContentSettingException> exceptions) {
assert !exceptions.isEmpty();
@ContentSettingValues
@Nullable
Integer contentSetting = exceptions.get(0).getContentSetting();
for (ContentSettingException exception : exceptions) {
assert exception.getContentSetting().equals(contentSetting);
}
return contentSetting;
}
public List<ContentSettingException> getEmbeddedContentSettings(
@ContentSettingsType.EnumType int type) {
assert isEmbeddedPermission(type);
return getEmbeddedPermissions().get(type);
}
/**
* @return ContentSettingValue for specified ContentSettingsType. (Camera, Clipboard, etc.).
*/
public @ContentSettingValues @Nullable Integer getContentSetting(
BrowserContextHandle browserContextHandle, @ContentSettingsType.EnumType int type) {
if (isEmbeddedPermission(type)) {
var exceptions = getEmbeddedContentSettings(type);
if (exceptions == null || exceptions.isEmpty()) return null;
// If multiple embedded permissions have been merged into one website like for
// SingleWebsiteSettings, this function may only be used if all permission exceptions
// have been grouped by content setting.
return getContentSetting(exceptions);
} else if (getPermissionInfo(type) != null) {
return getPermissionInfo(type).getContentSetting(browserContextHandle);
} else if (getContentSettingException(type) != null) {
return getContentSettingException(type).getContentSetting();
}
return null;
}
/** Sets the ContentSettingValue on the appropriate PermissionInfo or ContentSettingException */
public void setContentSetting(
BrowserContextHandle browserContextHandle,
@ContentSettingsType.EnumType int type,
@ContentSettingValues int value) {
if (getPermissionInfo(type) != null) {
getPermissionInfo(type).setContentSetting(browserContextHandle, value);
return;
}
if (isEmbeddedPermission(type)) {
setAllEmbeddedContentSettings(browserContextHandle, type, value);
return;
}
ContentSettingException exception = getContentSettingException(type);
if (type == ContentSettingsType.ADS) {
// It is possible to set the permission without having an existing exception,
// because we can show the BLOCK state even when this permission is set to the
// default. In that case, just set an exception now to BLOCK to enable changing the
// permission.
if (exception == null) {
exception =
new ContentSettingException(
ContentSettingsType.ADS,
getAddress().getOrigin(),
ContentSettingValues.BLOCK,
"",
/* isEmbargoed= */ false);
setContentSettingException(type, exception);
}
} else if (type == ContentSettingsType.JAVASCRIPT) {
// It is possible to set the permission without having an existing exception,
// because we show the javascript permission in Site Settings if javascript
// is blocked by default.
if (exception == null) {
exception =
new ContentSettingException(
ContentSettingsType.JAVASCRIPT,
getAddress().getHost(),
value,
"",
/* isEmbargoed= */ false);
setContentSettingException(type, exception);
}
// It's possible for either action to be emitted. This code path is hit
// regardless of whether there was an existing permission or not.
if (value == ContentSettingValues.BLOCK) {
RecordUserAction.record("JavascriptContentSetting.EnableBy.SiteSettings");
} else {
RecordUserAction.record("JavascriptContentSetting.DisableBy.SiteSettings");
}
} else if (type == ContentSettingsType.SOUND) {
// It is possible to set the permission without having an existing exception,
// because we always show the sound permission in Site Settings.
if (exception == null) {
exception =
new ContentSettingException(
ContentSettingsType.SOUND,
getAddress().getHost(),
value,
"",
/* isEmbargoed= */ false);
setContentSettingException(type, exception);
}
if (value == ContentSettingValues.BLOCK) {
RecordUserAction.record("SoundContentSetting.MuteBy.SiteSettings");
} else {
RecordUserAction.record("SoundContentSetting.UnmuteBy.SiteSettings");
}
}
// We want to call setContentSetting even after explicitly setting
// mContentSettingException above because this will trigger the actual change
// on the PrefServiceBridge.
if (exception != null) {
exception.setContentSetting(browserContextHandle, value);
}
}
private void setAllEmbeddedContentSettings(
BrowserContextHandle browserContextHandle,
@ContentSettingsType.EnumType int type,
@ContentSettingValues int value) {
List<ContentSettingException> exceptions = getEmbeddedPermissions().get(type);
if (exceptions == null) {
return;
}
for (ContentSettingException exception : exceptions) {
exception.setContentSetting(browserContextHandle, value);
}
}
/**
* Returns whether either the permission or the content setting for the associated {@link type}
* is embargoed.
*/
public boolean isEmbargoed(@ContentSettingsType.EnumType int type) {
if (isEmbeddedPermission(type)) {
List<ContentSettingException> exceptions = getEmbeddedContentSettings(type);
assert exceptions.size() == 1;
return exceptions.get(0).isEmbargoed();
}
PermissionInfo permissionInfo = getPermissionInfo(type);
if (permissionInfo != null && permissionInfo.isEmbargoed()) return true;
ContentSettingException exception = getContentSettingException(type);
return (exception != null && exception.isEmbargoed());
}
public void setLocalStorageInfo(LocalStorageInfo info) {
mLocalStorageInfo = info;
}
public LocalStorageInfo getLocalStorageInfo() {
return mLocalStorageInfo;
}
public FPSCookieInfo getFPSCookieInfo() {
return mFPSCookieInfo;
}
public void setFPSCookieInfo(FPSCookieInfo fpsCookieInfo) {
mFPSCookieInfo = fpsCookieInfo;
}
public void addStorageInfo(StorageInfo info) {
mStorageInfo.add(info);
}
/**
* Sets the zoom factor for the website (see PageZoomUtils.java).
*
* @param zoomFactor The zoom factor to set the website to.
*/
public void setZoomFactor(double zoomFactor) {
mZoomFactor = zoomFactor;
}
public List<StorageInfo> getStorageInfo() {
return new ArrayList<StorageInfo>(mStorageInfo);
}
public void addSharedDictionaryInfo(SharedDictionaryInfo info) {
mSharedDictionaryInfo.add(info);
}
public List<SharedDictionaryInfo> getSharedDictionaryInfo() {
return new ArrayList<SharedDictionaryInfo>(mSharedDictionaryInfo);
}
public void setCookiesInfo(CookiesInfo info) {
mCookiesInfo = info;
}
public CookiesInfo getCookiesInfo() {
return mCookiesInfo;
}
public void clearAllStoredData(
SiteSettingsDelegate siteSettingsDelegate, final StoredDataClearedCallback callback) {
var browserContextHandle = siteSettingsDelegate.getBrowserContextHandle();
if (siteSettingsDelegate.isBrowsingDataModelFeatureEnabled()) {
siteSettingsDelegate.getBrowsingDataModel(
(model) -> {
String host = getAddress().getHost();
model.removeBrowsingData(
host,
() -> {
callback.onStoredDataCleared();
mStorageInfo.clear();
});
});
} else {
// Here we need to delete localstorage e cookie info.
// Wait for callbacks from each mStorageInfo and mSharedDictionaryInfo
// and a callback from mLocalStorageInfo.
int[] storageInfoCallbacksLeft = {
mStorageInfo.size() + mSharedDictionaryInfo.size() + 1
};
StorageInfoClearedCallback clearedCallback =
() -> {
if (--storageInfoCallbacksLeft[0] == 0) callback.onStoredDataCleared();
};
if (mLocalStorageInfo != null) {
mLocalStorageInfo.clear(browserContextHandle, clearedCallback);
mLocalStorageInfo = null;
} else {
clearedCallback.onStorageInfoCleared();
}
for (StorageInfo info : mStorageInfo) info.clear(browserContextHandle, clearedCallback);
mStorageInfo.clear();
for (SharedDictionaryInfo info : mSharedDictionaryInfo) {
info.clear(browserContextHandle, clearedCallback);
}
mSharedDictionaryInfo.clear();
}
}
/** An interface to implement to get a callback when storage info has been cleared. */
public interface StoredDataClearedCallback {
public void onStoredDataCleared();
}
/** Add information about an object the user has granted permission for this site to access. */
public void addChosenObjectInfo(ChosenObjectInfo info) {
mObjectInfo.add(info);
}
/** Returns the set of objects this website has been granted permission to access. */
public List<ChosenObjectInfo> getChosenObjectInfo() {
return new ArrayList<ChosenObjectInfo>(mObjectInfo);
}
public String getTitleForEmbeddedPreferenceRow() {
return omitProtocolIfPresent(mEmbedder.getTitle());
}
// WebsiteEntry implementation.
@Override
public String getTitleForPreferenceRow() {
return omitProtocolIfPresent(getTitle());
}
@Override
public GURL getFaviconUrl() {
return new GURL(getAddress().getOrigin());
}
@Override
public long getTotalUsage() {
long usage = 0;
if (mLocalStorageInfo != null) usage += mLocalStorageInfo.getSize();
for (StorageInfo info : mStorageInfo) usage += info.getSize();
for (SharedDictionaryInfo info : mSharedDictionaryInfo) usage += info.getSize();
return usage;
}
public double getZoomFactor() {
return mZoomFactor;
}
@Override
public int getNumberOfCookies() {
if (mCookiesInfo == null) return 0;
return mCookiesInfo.getCount();
}
@Override
public boolean matches(String search) {
return getTitle().contains(search);
}
/** {@inheritDoc} */
@Override
public boolean isPartOfRws() {
return getFPSCookieInfo() != null;
}
/** {@inheritDoc} */
@Override
public String getRwsOwner() {
return isPartOfRws() ? getFPSCookieInfo().getOwner() : null;
}
/** {@inheritDoc} */
@Override
public int getRwsSize() {
return isPartOfRws() ? getFPSCookieInfo().getMembersCount() : 0;
}
@Override
public boolean isCookieDeletionDisabled(BrowserContextHandle browserContextHandle) {
return WebsitePreferenceBridge.isCookieDeletionDisabled(
browserContextHandle, mOrigin.getOrigin());
}
}