// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.preferences.website; import android.util.Pair; import org.chromium.base.Callback; import org.chromium.chrome.browser.ContentSettingsType; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; /** * Utility class that asynchronously fetches any Websites and the permissions * that the user has set for them. */ public class WebsitePermissionsFetcher { /** * A callback to pass to WebsitePermissionsFetcher. This is run when the * website permissions have been fetched. */ public interface WebsitePermissionsCallback { void onWebsitePermissionsAvailable(Collection<Website> sites); } // This map looks up Websites by their origin and embedder. private final Map<Pair<WebsiteAddress, WebsiteAddress>, Website> mSites = new HashMap<>(); // The callback to run when the permissions have been fetched. private final WebsitePermissionsCallback mCallback; /** * @param callback The callback to run when the fetch is complete. */ public WebsitePermissionsFetcher(WebsitePermissionsCallback callback) { mCallback = callback; } /** * Fetches preferences for all sites that have them. * TODO(mvanouwerkerk): Add an argument |url| to only fetch permissions for * sites from the same origin as that of |url| - https://crbug.com/459222. */ public void fetchAllPreferences() { TaskQueue queue = new TaskQueue(); // Populate features from more specific to less specific. // Geolocation lookup permission is per-origin and per-embedder. queue.add(new GeolocationInfoFetcher()); // Midi sysex access permission is per-origin and per-embedder. queue.add(new MidiInfoFetcher()); // Cookies are stored per-host. queue.add(new CookieExceptionInfoFetcher()); // Fullscreen are stored per-origin. queue.add(new FullscreenInfoFetcher()); // Keygen permissions are per-origin. queue.add(new KeygenInfoFetcher()); // Local storage info is per-origin. queue.add(new LocalStorageInfoFetcher()); // Website storage is per-host. queue.add(new WebStorageInfoFetcher()); // Popup exceptions are host-based patterns (unless we start // synchronizing popup exceptions with desktop Chrome). queue.add(new PopupExceptionInfoFetcher()); // JavaScript exceptions are host-based patterns. queue.add(new JavaScriptExceptionInfoFetcher()); // Protected media identifier permission is per-origin and per-embedder. queue.add(new ProtectedMediaIdentifierInfoFetcher()); // Notification permission is per-origin. queue.add(new NotificationInfoFetcher()); // Camera capture permission is per-origin and per-embedder. queue.add(new CameraCaptureInfoFetcher()); // Micropohone capture permission is per-origin and per-embedder. queue.add(new MicrophoneCaptureInfoFetcher()); // Background sync permission is per-origin. queue.add(new BackgroundSyncExceptionInfoFetcher()); // Autoplay permission is per-origin. queue.add(new AutoplayExceptionInfoFetcher()); // USB device permission is per-origin and per-embedder. queue.add(new UsbInfoFetcher()); queue.add(new PermissionsAvailableCallbackRunner()); queue.next(); } /** * Fetches all preferences within a specific category. * * @param catgory A category to fetch. */ public void fetchPreferencesForCategory(SiteSettingsCategory category) { if (category.showAllSites()) { fetchAllPreferences(); return; } TaskQueue queue = new TaskQueue(); // Populate features from more specific to less specific. if (category.showGeolocationSites()) { // Geolocation lookup permission is per-origin and per-embedder. queue.add(new GeolocationInfoFetcher()); } else if (category.showCookiesSites()) { // Cookies exceptions are patterns. queue.add(new CookieExceptionInfoFetcher()); } else if (category.showStorageSites()) { // Local storage info is per-origin. queue.add(new LocalStorageInfoFetcher()); // Website storage is per-host. queue.add(new WebStorageInfoFetcher()); } else if (category.showFullscreenSites()) { // Full screen is per-origin. queue.add(new FullscreenInfoFetcher()); } else if (category.showCameraSites()) { // Camera capture permission is per-origin and per-embedder. queue.add(new CameraCaptureInfoFetcher()); } else if (category.showMicrophoneSites()) { // Micropohone capture permission is per-origin and per-embedder. queue.add(new MicrophoneCaptureInfoFetcher()); } else if (category.showPopupSites()) { // Popup exceptions are host-based patterns (unless we start // synchronizing popup exceptions with desktop Chrome.) queue.add(new PopupExceptionInfoFetcher()); } else if (category.showJavaScriptSites()) { // JavaScript exceptions are host-based patterns. queue.add(new JavaScriptExceptionInfoFetcher()); } else if (category.showNotificationsSites()) { // Push notification permission is per-origin. queue.add(new NotificationInfoFetcher()); } else if (category.showBackgroundSyncSites()) { // Background sync info is per-origin. queue.add(new BackgroundSyncExceptionInfoFetcher()); } else if (category.showProtectedMediaSites()) { // Protected media identifier permission is per-origin and per-embedder. queue.add(new ProtectedMediaIdentifierInfoFetcher()); } else if (category.showAutoplaySites()) { // Autoplay permission is per-origin. queue.add(new AutoplayExceptionInfoFetcher()); } else if (category.showUsbDevices()) { // USB device permission is per-origin. queue.add(new UsbInfoFetcher()); } queue.add(new PermissionsAvailableCallbackRunner()); queue.next(); } private Website findOrCreateSite(WebsiteAddress origin, WebsiteAddress embedder) { // In Jelly Bean a null value triggers a NullPointerException in Pair.hashCode(). Storing // the origin twice works around it and won't conflict with other entries as this is how the // native code indicates to this class that embedder == origin. https://crbug.com/636330 Pair<WebsiteAddress, WebsiteAddress> key = Pair.create(origin, embedder == null ? origin : embedder); Website site = mSites.get(key); if (site == null) { site = new Website(origin, embedder); mSites.put(key, site); } return site; } private void setException(int contentSettingsType) { for (ContentSettingException exception : WebsitePreferenceBridge.getContentSettingsExceptions(contentSettingsType)) { // The pattern "*" represents the default setting, not a specific website. if (exception.getPattern().equals("*")) continue; WebsiteAddress address = WebsiteAddress.create(exception.getPattern()); if (address == null) continue; Website site = findOrCreateSite(address, null); switch (contentSettingsType) { case ContentSettingsType.CONTENT_SETTINGS_TYPE_AUTOPLAY: site.setAutoplayException(exception); break; case ContentSettingsType.CONTENT_SETTINGS_TYPE_BACKGROUND_SYNC: site.setBackgroundSyncException(exception); break; case ContentSettingsType.CONTENT_SETTINGS_TYPE_COOKIES: site.setCookieException(exception); break; case ContentSettingsType.CONTENT_SETTINGS_TYPE_JAVASCRIPT: site.setJavaScriptException(exception); break; case ContentSettingsType.CONTENT_SETTINGS_TYPE_POPUPS: site.setPopupException(exception); break; default: assert false : "Unexpected content setting type received: " + contentSettingsType; break; } } } /** * A single task in the WebsitePermissionsFetcher task queue. We need fetching of features to be * serialized, as we need to have all the origins in place prior to populating the hosts. */ private abstract class Task { /** Override this method to implement a synchronous task. */ void run() {} /** * Override this method to implement an asynchronous task. Call queue.next() once execution * is complete. */ void runAsync(TaskQueue queue) { run(); queue.next(); } } /** * A queue used to store the sequence of tasks to run to fetch the website preferences. Each * task is run sequentially, and some of the tasks may run asynchronously. */ private static class TaskQueue extends LinkedList<Task> { void next() { if (!isEmpty()) removeFirst().runAsync(this); } } private class AutoplayExceptionInfoFetcher extends Task { @Override public void run() { setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_AUTOPLAY); } } private class GeolocationInfoFetcher extends Task { @Override public void run() { for (GeolocationInfo info : WebsitePreferenceBridge.getGeolocationInfo()) { WebsiteAddress origin = WebsiteAddress.create(info.getOrigin()); if (origin == null) continue; WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder()); findOrCreateSite(origin, embedder).setGeolocationInfo(info); } } } private class MidiInfoFetcher extends Task { @Override public void run() { for (MidiInfo info : WebsitePreferenceBridge.getMidiInfo()) { WebsiteAddress origin = WebsiteAddress.create(info.getOrigin()); if (origin == null) continue; WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder()); findOrCreateSite(origin, embedder).setMidiInfo(info); } } } private class PopupExceptionInfoFetcher extends Task { @Override public void run() { setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_POPUPS); } } private class JavaScriptExceptionInfoFetcher extends Task { @Override public void run() { setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_JAVASCRIPT); } } private class CookieExceptionInfoFetcher extends Task { @Override public void run() { setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_COOKIES); } } private class KeygenInfoFetcher extends Task { @Override public void run() { for (KeygenInfo info : WebsitePreferenceBridge.getKeygenInfo()) { WebsiteAddress origin = WebsiteAddress.create(info.getOrigin()); if (origin == null) continue; WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder()); findOrCreateSite(origin, embedder).setKeygenInfo(info); } } } /** * Class for fetching the fullscreen information. */ private class FullscreenInfoFetcher extends Task { @Override public void run() { for (FullscreenInfo info : WebsitePreferenceBridge.getFullscreenInfo()) { WebsiteAddress origin = WebsiteAddress.create(info.getOrigin()); if (origin == null) continue; WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder()); findOrCreateSite(origin, embedder).setFullscreenInfo(info); } } } private class LocalStorageInfoFetcher extends Task { @Override public void runAsync(final TaskQueue queue) { WebsitePreferenceBridge.fetchLocalStorageInfo(new Callback<HashMap>() { @Override public void onResult(HashMap result) { for (Object o : result.entrySet()) { @SuppressWarnings("unchecked") Map.Entry<String, LocalStorageInfo> entry = (Map.Entry<String, LocalStorageInfo>) o; WebsiteAddress address = WebsiteAddress.create(entry.getKey()); if (address == null) continue; findOrCreateSite(address, null).setLocalStorageInfo(entry.getValue()); } queue.next(); } }); } } private class WebStorageInfoFetcher extends Task { @Override public void runAsync(final TaskQueue queue) { WebsitePreferenceBridge.fetchStorageInfo(new Callback<ArrayList>() { @Override public void onResult(ArrayList result) { @SuppressWarnings("unchecked") ArrayList<StorageInfo> infoArray = result; for (StorageInfo info : infoArray) { WebsiteAddress address = WebsiteAddress.create(info.getHost()); if (address == null) continue; findOrCreateSite(address, null).addStorageInfo(info); } queue.next(); } }); } } private class ProtectedMediaIdentifierInfoFetcher extends Task { @Override public void run() { for (ProtectedMediaIdentifierInfo info : WebsitePreferenceBridge.getProtectedMediaIdentifierInfo()) { WebsiteAddress origin = WebsiteAddress.create(info.getOrigin()); if (origin == null) continue; WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder()); findOrCreateSite(origin, embedder).setProtectedMediaIdentifierInfo(info); } } } private class NotificationInfoFetcher extends Task { @Override public void run() { for (NotificationInfo info : WebsitePreferenceBridge.getNotificationInfo()) { WebsiteAddress origin = WebsiteAddress.create(info.getOrigin()); if (origin == null) continue; WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder()); findOrCreateSite(origin, embedder).setNotificationInfo(info); } } } private class CameraCaptureInfoFetcher extends Task { @Override public void run() { for (CameraInfo info : WebsitePreferenceBridge.getCameraInfo()) { WebsiteAddress origin = WebsiteAddress.create(info.getOrigin()); if (origin == null) continue; WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder()); findOrCreateSite(origin, embedder).setCameraInfo(info); } } } private class MicrophoneCaptureInfoFetcher extends Task { @Override public void run() { for (MicrophoneInfo info : WebsitePreferenceBridge.getMicrophoneInfo()) { WebsiteAddress origin = WebsiteAddress.create(info.getOrigin()); if (origin == null) continue; WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder()); findOrCreateSite(origin, embedder).setMicrophoneInfo(info); } } } private class BackgroundSyncExceptionInfoFetcher extends Task { @Override public void run() { setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_BACKGROUND_SYNC); } } private class UsbInfoFetcher extends Task { @Override public void run() { for (UsbInfo info : WebsitePreferenceBridge.getUsbInfo()) { WebsiteAddress origin = WebsiteAddress.create(info.getOrigin()); if (origin == null) continue; WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder()); findOrCreateSite(origin, embedder).addUsbInfo(info); } } } private class PermissionsAvailableCallbackRunner extends Task { @Override public void run() { mCallback.onWebsitePermissionsAvailable(mSites.values()); } } }