diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/modules | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | comm/suite/modules/Feeds.jsm | 50 | ||||
-rw-r--r-- | comm/suite/modules/OfflineAppCacheHelper.jsm | 16 | ||||
-rw-r--r-- | comm/suite/modules/OpenInTabsUtils.jsm | 83 | ||||
-rw-r--r-- | comm/suite/modules/PermissionUI.jsm | 612 | ||||
-rw-r--r-- | comm/suite/modules/RecentWindow.jsm | 66 | ||||
-rw-r--r-- | comm/suite/modules/SitePermissions.jsm | 796 | ||||
-rw-r--r-- | comm/suite/modules/ThemeVariableMap.jsm | 29 | ||||
-rw-r--r-- | comm/suite/modules/WindowsJumpLists.jsm | 604 | ||||
-rw-r--r-- | comm/suite/modules/WindowsPreviewPerTab.jsm | 872 | ||||
-rw-r--r-- | comm/suite/modules/moz.build | 20 | ||||
-rw-r--r-- | comm/suite/modules/test/unit/head.js | 135 | ||||
-rw-r--r-- | comm/suite/modules/test/unit/test_browser_sanitizer.js | 339 | ||||
-rw-r--r-- | comm/suite/modules/test/unit/xpcshell.ini | 6 |
13 files changed, 3628 insertions, 0 deletions
diff --git a/comm/suite/modules/Feeds.jsm b/comm/suite/modules/Feeds.jsm new file mode 100644 index 0000000000..6c24dcfea8 --- /dev/null +++ b/comm/suite/modules/Feeds.jsm @@ -0,0 +1,50 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* 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/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = [ "Feeds" ]; + +ChromeUtils.defineModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); + +var Feeds = { + + /** + * isValidFeed: checks whether the given data represents a valid feed. + * + * @param aLink + * An object representing a feed with title, href and type. + * @param aPrincipal + * The principal of the document, used for security check. + * @param aIsFeed + * Whether this is already a known feed or not, if true only a security + * check will be performed. + */ + isValidFeed(aLink, aPrincipal, aIsFeed) { + if (!aLink || !aPrincipal) + return false; + + var type = aLink.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, ""); + if (!aIsFeed) { + aIsFeed = (type == "application/rss+xml" || + type == "application/atom+xml"); + } + + if (aIsFeed) { + try { + let href = BrowserUtils.makeURI(aLink.href, aLink.ownerDocument.characterSet) + BrowserUtils.urlSecurityCheck(href, aPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + return type || "application/rss+xml"; + } catch (ex) { + } + } + + return null; + }, + +}; diff --git a/comm/suite/modules/OfflineAppCacheHelper.jsm b/comm/suite/modules/OfflineAppCacheHelper.jsm new file mode 100644 index 0000000000..840012b87c --- /dev/null +++ b/comm/suite/modules/OfflineAppCacheHelper.jsm @@ -0,0 +1,16 @@ +/* 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/. */ + +var EXPORTED_SYMBOLS = ["OfflineAppCacheHelper"]; + +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var OfflineAppCacheHelper = { + clear() { + var appCacheStorage = Services.cache2.appCacheStorage(Services.loadContextInfo.default, null); + try { + appCacheStorage.asyncEvictStorage(null); + } catch (er) {} + } +}; diff --git a/comm/suite/modules/OpenInTabsUtils.jsm b/comm/suite/modules/OpenInTabsUtils.jsm new file mode 100644 index 0000000000..a04aa8d137 --- /dev/null +++ b/comm/suite/modules/OpenInTabsUtils.jsm @@ -0,0 +1,83 @@ +/* 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/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["OpenInTabsUtils"]; + +const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyGetter(this, "bundle", function() { + return Services.strings.createBundle("chrome://navigator/locale/tabbrowser.properties"); +}); + +/** + * Utility functions that can be used when opening multiple tabs, that can be + * called without any tabbrowser instance. + */ +var OpenInTabsUtils = { + getString(key) { + return bundle.GetStringFromName(key); + }, + + getFormattedString(key, params) { + return bundle.formatStringFromName(key, params, params.length); + }, + + /** + * Gives the user a chance to cancel loading lots of tabs at once. + */ + confirmOpenInTabs(numTabsToOpen, aWindow) { + const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen"; + const MAX_OPNE_PREF = "browser.tabs.maxOpenBeforeWarn"; + if (!Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) { + return true; + } + if (numTabsToOpen < Services.prefs.getIntPref(MAX_OPNE_PREF)) { + return true; + } + + // default to true: if it were false, we wouldn't get this far + let warnOnOpen = { value: true }; + + const messageKey = "tabs.openWarningMultipleBranded"; + const openKey = "tabs.openButtonMultiple"; + const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; + let brandShortName = Services.strings + .createBundle(BRANDING_BUNDLE_URI) + .GetStringFromName("brandShortName"); + + let buttonPressed = Services.prompt.confirmEx( + aWindow, + this.getString("tabs.openWarningTitle"), + this.getFormattedString(messageKey, [numTabsToOpen, brandShortName]), + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1), + this.getString(openKey), null, null, + this.getFormattedString("tabs.openWarningPromptMeBranded", + [brandShortName]), + warnOnOpen + ); + + let reallyOpen = (buttonPressed == 0); + // don't set the pref unless they press OK and it's false + if (reallyOpen && !warnOnOpen.value) { + Services.prefs.setBoolPref(WARN_ON_OPEN_PREF, false); + } + + return reallyOpen; + }, + + /* + * Async version of confirmOpenInTabs. + */ + promiseConfirmOpenInTabs(numTabsToOpen, aWindow) { + return new Promise(resolve => { + Services.tm.dispatchToMainThread(() => { + resolve(this.confirmOpenInTabs(numTabsToOpen, aWindow)); + }); + }); + } +}; diff --git a/comm/suite/modules/PermissionUI.jsm b/comm/suite/modules/PermissionUI.jsm new file mode 100644 index 0000000000..09d24a9a82 --- /dev/null +++ b/comm/suite/modules/PermissionUI.jsm @@ -0,0 +1,612 @@ +/* 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/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = [ + "PermissionUI", +]; + +/** + * PermissionUI is responsible for exposing both a prototype + * PermissionPrompt that can be used by arbitrary browser + * components and add-ons, but also hosts the implementations of + * built-in permission prompts. + * + * If you're developing a feature that requires web content to ask + * for special permissions from the user, this module is for you. + * + * Suppose a system add-on wants to add a new prompt for a new request + * for getting more low-level access to the user's sound card, and the + * permission request is coming up from content by way of the + * nsContentPermissionHelper. The system add-on could then do the following: + * + * ChromeUtils.import("resource://gre/modules/Integration.jsm"); + * ChromeUtils.import("resource:///modules/PermissionUI.jsm"); + * + * const SoundCardIntegration = (base) => ({ + * __proto__: base, + * createPermissionPrompt(type, request) { + * if (type != "sound-api") { + * return super.createPermissionPrompt(...arguments); + * } + * + * return { + * __proto__: PermissionUI.PermissionPromptForRequestPrototype, + * get permissionKey() { + * return "sound-permission"; + * } + * // etc - see the documentation for PermissionPrompt for + * // a better idea of what things one can and should override. + * } + * }, + * }); + * + * // Add-on startup: + * Integration.contentPermission.register(SoundCardIntegration); + * // ... + * // Add-on shutdown: + * Integration.contentPermission.unregister(SoundCardIntegration); + * + * Note that PermissionPromptForRequestPrototype must be used as the + * prototype, since the prompt is wrapping an nsIContentPermissionRequest, + * and going through nsIContentPermissionPrompt. + * + * It is, however, possible to take advantage of PermissionPrompt without + * having to go through nsIContentPermissionPrompt or with a + * nsIContentPermissionRequest. The PermissionPromptPrototype can be + * imported, subclassed, and have prompt() called directly, without + * the caller having called into createPermissionPrompt. + */ +const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter(this, "SitePermissions", + "resource:///modules/SitePermissions.jsm"); +ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + + +XPCOMUtils.defineLazyGetter(this, "gNotificationBundle", function() { + return Services.strings + .createBundle("chrome://communicator/locale/notification.properties"); +}); + +var PermissionUI = {}; + +/** + * PermissionPromptPrototype should be subclassed by callers that + * want to display prompts to the user. See each method and property + * below for guidance on what to override. + * + * Note that if you're creating a prompt for an + * nsIContentPermissionRequest, you'll want to subclass + * PermissionPromptForRequestPrototype instead. + */ +var PermissionPromptPrototype = { + /** + * Returns the associated <xul:browser> for the request. This should + * work for the e10s and non-e10s case. + * + * Subclasses must override this. + * + * @return {<xul:browser>} + */ + get browser() { + throw new Error("Not implemented."); + }, + + /** + * Returns the nsIPrincipal associated with the request. + * + * Subclasses must override this. + * + * @return {nsIPrincipal} + */ + get principal() { + throw new Error("Not implemented."); + }, + + /** + * If the nsIPermissionManager is being queried and written + * to for this permission request, set this to the key to be + * used. If this is undefined, user permissions will not be + * read from or written to. + * + * Note that if a permission is set, in any follow-up + * prompting within the expiry window of that permission, + * the prompt will be skipped and the allow or deny choice + * will be selected automatically. + */ + get permissionKey() { + return undefined; + }, + + /** + * These are the options that will be passed to the + * PopupNotification when it is shown. See the documentation + * for PopupNotification for more details. + * + * Note that prompt() will automatically set displayURI to + * be the URI of the requesting pricipal, unless the displayURI is exactly + * set to false. + */ + get popupOptions() { + return {}; + }, + + /** + * PopupNotification requires a unique ID to open the notification. + * You must return a unique ID string here, for which PopupNotification + * will then create a <xul:popupnotification> node with the ID + * "<notificationID>-notification". + * + * If there's a custom <xul:popupnotification> you're hoping to show, + * then you need to make sure its ID has the "-notification" suffix, + * and then return the prefix here. + * + * See PopupNotification.jsm for more details. + * + * @return {string} + * The unique ID that will be used to as the + * "<unique ID>-notification" ID for the <xul:popupnotification> + * to use or create. + */ + get notificationID() { + throw new Error("Not implemented."); + }, + + /** + * The ID of the element to anchor the PopupNotification to. + * + * @return {string} + */ + get anchorID() { + return "default-notification-icon"; + }, + + /** + * The message to show to the user in the PopupNotification. A string + * with "<>" as a placeholder that is usually replaced by an addon name + * or a host name, formatted in bold, to describe the permission that is being requested. + * + * Subclasses must override this. + * + * @return {string} + */ + get message() { + throw new Error("Not implemented."); + }, + + /** + * This will be called if the request is to be cancelled. + * + * Subclasses only need to override this if they provide a + * permissionKey. + */ + cancel() { + throw new Error("Not implemented.") + }, + + /** + * This will be called if the request is to be allowed. + * + * Subclasses only need to override this if they provide a + * permissionKey. + */ + allow() { + throw new Error("Not implemented."); + }, + + /** + * The actions that will be displayed in the PopupNotification + * via a dropdown menu. The first item in this array will be + * the default selection. Each action is an Object with the + * following properties: + * + * label (string): + * The label that will be displayed for this choice. + * accessKey (string): + * The access key character that will be used for this choice. + * action (SitePermissions state) + * The action that will be associated with this choice. + * This should be either SitePermissions.ALLOW or SitePermissions.BLOCK. + * + * callback (function, optional) + * A callback function that will fire if the user makes this choice, with + * a single parameter, state. State is an Object that contains the property + * checkboxChecked, which identifies whether the checkbox to remember this + * decision was checked. + */ + get promptActions() { + return []; + }, + + /** + * Will determine if a prompt should be shown to the user, and if so, + * will show it. + * + * If a permissionKey is defined prompt() might automatically + * allow or cancel itself based on the user's current + * permission settings without displaying the prompt. + * + * If the permission is not already set and the <xul:browser> that the request + * is associated with does not belong to a browser window with the + * PopupNotifications global set, the prompt request is ignored. + */ + prompt() { + // We ignore requests from non-nsIStandardURLs + let requestingURI = this.principal.URI; + if (!(requestingURI instanceof Ci.nsIStandardURL)) { + return; + } + + if (this.permissionKey) { + // If we're reading and setting permissions, then we need + // to check to see if we already have a permission setting + // for this particular principal. + let {state} = SitePermissions.getForPrincipal(this.principal, + this.permissionKey, + this.browser); + + if (state == SitePermissions.BLOCK) { + this.cancel(); + return; + } + + if (state == SitePermissions.ALLOW) { + this.allow(); + return; + } + + // Tell the browser to refresh the identity block display in case there + // are expired permission states. + this.browser.dispatchEvent(new this.browser.ownerGlobal + .CustomEvent("PermissionStateChange")); + } + + let chromeWin = this.browser.ownerGlobal; + if (!chromeWin.PopupNotifications) { + this.cancel(); + return; + } + + // Transform the PermissionPrompt actions into PopupNotification actions. + let popupNotificationActions = []; + for (let promptAction of this.promptActions) { + let action = { + label: promptAction.label, + accessKey: promptAction.accessKey, + callback: state => { + if (promptAction.callback) { + promptAction.callback(); + } + + if (this.permissionKey) { + + // Permanently store permission. + if ((state && state.checkboxChecked) || + promptAction.scope == SitePermissions.SCOPE_PERSISTENT) { + let scope = SitePermissions.SCOPE_PERSISTENT; + // Only remember permission for session if in PB mode. + if (PrivateBrowsingUtils.isBrowserPrivate(this.browser)) { + scope = SitePermissions.SCOPE_SESSION; + } + SitePermissions.setForPrincipal(this.principal, + this.permissionKey, + promptAction.action, + scope); + } else if (promptAction.action == SitePermissions.BLOCK) { + // Temporarily store BLOCK permissions only. + // SitePermissions does not consider subframes when storing temporary + // permissions on a tab, thus storing ALLOW could be exploited. + SitePermissions.setForPrincipal(this.principal, + this.permissionKey, + promptAction.action, + SitePermissions.SCOPE_TEMPORARY, + this.browser); + } + + // Grant permission if action is ALLOW. + if (promptAction.action == SitePermissions.ALLOW) { + this.allow(); + } else { + this.cancel(); + } + } + }, + }; + if (promptAction.dismiss) { + action.dismiss = promptAction.dismiss + } + + popupNotificationActions.push(action); + } + + let mainAction = popupNotificationActions.length ? + popupNotificationActions[0] : null; + let secondaryActions = popupNotificationActions.splice(1); + + let options = this.popupOptions; + + if (!options.hasOwnProperty("displayURI") || options.displayURI) { + options.displayURI = this.principal.URI; + } + // Permission prompts are always persistent; the close button is controlled by a pref. + options.persistent = true; + options.hideClose = !Services.prefs.getBoolPref("privacy.permissionPrompts.showCloseButton"); + // When the docshell of the browser is aboout to be swapped to another one, + // the "swapping" event is called. Returning true causes the notification + // to be moved to the new browser. + options.eventCallback = topic => topic == "swapping"; + + chromeWin.PopupNotifications.show(this.browser, + this.notificationID, + this.message, + this.anchorID, + mainAction, + secondaryActions, + options); + }, +}; + +PermissionUI.PermissionPromptPrototype = PermissionPromptPrototype; + +/** + * A subclass of PermissionPromptPrototype that assumes + * that this.request is an nsIContentPermissionRequest + * and fills in some of the required properties on the + * PermissionPrompt. For callers that are wrapping an + * nsIContentPermissionRequest, this should be subclassed + * rather than PermissionPromptPrototype. + */ +var PermissionPromptForRequestPrototype = { + __proto__: PermissionPromptPrototype, + + get browser() { + // In the e10s-case, the <xul:browser> will be at request.element. + // In the single-process case, we have to use some XPCOM incantations + // to resolve to the <xul:browser>. + if (this.request.element) { + return this.request.element; + } + return this.request + .window + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + }, + + get principal() { + return this.request.principal; + }, + + cancel() { + this.request.cancel(); + }, + + allow() { + this.request.allow(); + }, +}; + +PermissionUI.PermissionPromptForRequestPrototype = + PermissionPromptForRequestPrototype; + +/** + * Creates a PermissionPrompt for a nsIContentPermissionRequest for + * the GeoLocation API. + * + * @param request (nsIContentPermissionRequest) + * The request for a permission from content. + */ +function GeolocationPermissionPrompt(request) { + this.request = request; +} + +GeolocationPermissionPrompt.prototype = { + __proto__: PermissionPromptForRequestPrototype, + + get permissionKey() { + return "geo"; + }, + + get popupOptions() { + let options = { + displayURI: false, + name: this.principal.URI.hostPort, + }; + + if (this.principal.URI.schemeIs("file")) { + options.checkbox = { show: false }; + } else { + // Don't offer "always remember" action in PB mode + options.checkbox = { + show: !PrivateBrowsingUtils.isWindowPrivate(this.browser.ownerGlobal) + }; + } + + if (options.checkbox.show) { + options.checkbox.label = gNotificationBundle.GetStringFromName("geolocation.remember"); + } + + return options; + }, + + get notificationID() { + return "geolocation"; + }, + + get anchorID() { + return "geo-notification-icon"; + }, + + get message() { + if (this.principal.URI.schemeIs("file")) { + return gNotificationBundle.GetStringFromName("geolocation.shareWithFile3"); + } + + return gNotificationBundle.formatStringFromName("geolocation.shareWithSite3", + ["<>"], 1); + }, + + get promptActions() { + return [{ + label: gNotificationBundle.GetStringFromName("geolocation.allowLocation"), + accessKey: + gNotificationBundle.GetStringFromName("geolocation.allowLocation.accesskey"), + action: SitePermissions.ALLOW, + callback(state) { + }, + }, { + label: gNotificationBundle.GetStringFromName("geolocation.dontAllowLocation"), + accessKey: + gNotificationBundle.GetStringFromName("geolocation.dontAllowLocation.accesskey"), + action: SitePermissions.BLOCK, + callback(state) { + }, + }]; + }, +}; + +PermissionUI.GeolocationPermissionPrompt = GeolocationPermissionPrompt; + +/** + * Creates a PermissionPrompt for a nsIContentPermissionRequest for + * the Desktop Notification API. + * + * @param request (nsIContentPermissionRequest) + * The request for a permission from content. + * @return {PermissionPrompt} (see documentation in header) + */ +function DesktopNotificationPermissionPrompt(request) { + this.request = request; +} + +DesktopNotificationPermissionPrompt.prototype = { + __proto__: PermissionPromptForRequestPrototype, + + get permissionKey() { + return "desktop-notification"; + }, + + get popupOptions() { + return { + displayURI: false, + name: this.principal.URI.hostPort, + }; + }, + + get notificationID() { + return "web-notifications"; + }, + + get anchorID() { + return "web-notifications-notification-icon"; + }, + + get message() { + return gNotificationBundle.formatStringFromName("webNotifications.receiveFromSite2", + ["<>"], 1); + }, + + get promptActions() { + let actions = [ + { + label: gNotificationBundle.GetStringFromName("webNotifications.allow"), + accessKey: + gNotificationBundle.GetStringFromName("webNotifications.allow.accesskey"), + action: SitePermissions.ALLOW, + scope: SitePermissions.SCOPE_PERSISTENT, + }, + { + label: gNotificationBundle.GetStringFromName("webNotifications.notNow"), + accessKey: + gNotificationBundle.GetStringFromName("webNotifications.notNow.accesskey"), + action: SitePermissions.BLOCK, + }, + ]; + if (!PrivateBrowsingUtils.isBrowserPrivate(this.browser)) { + actions.push({ + label: gNotificationBundle.GetStringFromName("webNotifications.never"), + accessKey: + gNotificationBundle.GetStringFromName("webNotifications.never.accesskey"), + action: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_PERSISTENT, + }); + } + return actions; + }, +}; + +PermissionUI.DesktopNotificationPermissionPrompt = + DesktopNotificationPermissionPrompt; + +/** + * Creates a PermissionPrompt for a nsIContentPermissionRequest for + * the persistent-storage API. + * + * @param request (nsIContentPermissionRequest) + * The request for a permission from content. + */ +function PersistentStoragePermissionPrompt(request) { + this.request = request; +} + +PersistentStoragePermissionPrompt.prototype = { + __proto__: PermissionPromptForRequestPrototype, + + get permissionKey() { + return "persistent-storage"; + }, + + get popupOptions() { + let checkbox = { + // In PB mode, we don't want the "always remember" checkbox + show: !PrivateBrowsingUtils.isWindowPrivate(this.browser.ownerGlobal) + }; + if (checkbox.show) { + checkbox.checked = true; + checkbox.label = gNotificationBundle.GetStringFromName("persistentStorage.remember"); + } + return { + checkbox, + displayURI: false, + name: this.principal.URI.hostPort, + }; + }, + + get notificationID() { + return "persistent-storage"; + }, + + get anchorID() { + return "persistent-storage-notification-icon"; + }, + + get message() { + return gNotificationBundle.formatStringFromName("persistentStorage.allowWithSite", + ["<>"], 1); + }, + + get promptActions() { + return [ + { + label: gNotificationBundle.GetStringFromName("persistentStorage.allow"), + accessKey: + gNotificationBundle.GetStringFromName("persistentStorage.allow.accesskey"), + action: Ci.nsIPermissionManager.ALLOW_ACTION + }, + { + label: gNotificationBundle.GetStringFromName("persistentStorage.dontAllow"), + accessKey: + gNotificationBundle.GetStringFromName("persistentStorage.dontAllow.accesskey"), + action: Ci.nsIPermissionManager.DENY_ACTION + } + ]; + } +}; + +PermissionUI.PersistentStoragePermissionPrompt = PersistentStoragePermissionPrompt; diff --git a/comm/suite/modules/RecentWindow.jsm b/comm/suite/modules/RecentWindow.jsm new file mode 100644 index 0000000000..9f3e6d669a --- /dev/null +++ b/comm/suite/modules/RecentWindow.jsm @@ -0,0 +1,66 @@ +/* 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/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["RecentWindow"]; + +const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {PrivateBrowsingUtils} = ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +var RecentWindow = { + /* + * Get the most recent browser window. + * + * @param aOptions an object accepting the arguments for the search. + * * private: true to restrict the search to private windows + * only, false to restrict the search to non-private only. + * Omit the property to search in both groups. + * * allowPopups: true if popup windows are permissable. + */ + getMostRecentBrowserWindow: function RW_getMostRecentBrowserWindow(aOptions) { + let checkPrivacy = typeof aOptions == "object" && + "private" in aOptions; + + let allowPopups = typeof aOptions == "object" && !!aOptions.allowPopups; + + function isSuitableBrowserWindow(win) { + return (!win.closed && + (allowPopups || win.toolbar.visible) && + (!checkPrivacy || + PrivateBrowsingUtils.permanentPrivateBrowsing || + PrivateBrowsingUtils.isWindowPrivate(win) == aOptions.private)); + } + + let broken_wm_z_order = + AppConstants.platform != "macosx" && AppConstants.platform != "win"; + + if (broken_wm_z_order) { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + + // if we're lucky, this isn't a popup, and we can just return this + if (win && !isSuitableBrowserWindow(win)) { + win = null; + let windowList = Services.wm.getEnumerator("navigator:browser"); + // this is oldest to newest, so this gets a bit ugly + while (windowList.hasMoreElements()) { + let nextWin = windowList.getNext(); + if (isSuitableBrowserWindow(nextWin)) + win = nextWin; + } + } + return win; + } else { + let windowList = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true); + while (windowList.hasMoreElements()) { + let win = windowList.getNext(); + if (isSuitableBrowserWindow(win)) + return win; + } + return null; + } + } +}; + diff --git a/comm/suite/modules/SitePermissions.jsm b/comm/suite/modules/SitePermissions.jsm new file mode 100644 index 0000000000..40d9a2d44a --- /dev/null +++ b/comm/suite/modules/SitePermissions.jsm @@ -0,0 +1,796 @@ +/* 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/. */ + +var EXPORTED_SYMBOLS = [ "SitePermissions" ]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +var gStringBundle = + Services.strings.createBundle("chrome://communicator/locale/sitePermissions.properties"); + +/** + * A helper module to manage temporarily blocked permissions. + * + * Permissions are keyed by browser, so methods take a Browser + * element to identify the corresponding permission set. + * + * This uses a WeakMap to key browsers, so that entries are + * automatically cleared once the browser stops existing + * (once there are no other references to the browser object); + */ +var TemporaryBlockedPermissions = { + // This is a three level deep map with the following structure: + // + // Browser => { + // <prePath>: { + // <permissionID>: {Number} <timeStamp> + // } + // } + // + // Only the top level browser elements are stored via WeakMap. The WeakMap + // value is an object with URI prePaths as keys. The keys of that object + // are ids that identify permissions that were set for the specific URI. + // The final value is an object containing the timestamp of when the permission + // was set (in order to invalidate after a certain amount of time has passed). + _stateByBrowser: new WeakMap(), + + // Private helper method that bundles some shared behavior for + // get() and getAll(), e.g. deleting permissions when they have expired. + _get(entry, prePath, id, timeStamp) { + if (timeStamp == null) { + delete entry[prePath][id]; + return null; + } + if (timeStamp + SitePermissions.temporaryPermissionExpireTime < Date.now()) { + delete entry[prePath][id]; + return null; + } + return {id, state: SitePermissions.BLOCK, scope: SitePermissions.SCOPE_TEMPORARY}; + }, + + // Sets a new permission for the specified browser. + set(browser, id) { + if (!browser) { + return; + } + if (!this._stateByBrowser.has(browser)) { + this._stateByBrowser.set(browser, {}); + } + let entry = this._stateByBrowser.get(browser); + let prePath = browser.currentURI.prePath; + if (!entry[prePath]) { + entry[prePath] = {}; + } + entry[prePath][id] = Date.now(); + }, + + // Removes a permission with the specified id for the specified browser. + remove(browser, id) { + if (!browser) { + return; + } + let entry = this._stateByBrowser.get(browser); + let prePath = browser.currentURI.prePath; + if (entry && entry[prePath]) { + delete entry[prePath][id]; + } + }, + + // Gets a permission with the specified id for the specified browser. + get(browser, id) { + if (!browser || !browser.currentURI) { + return null; + } + let entry = this._stateByBrowser.get(browser); + let prePath = browser.currentURI.prePath; + if (entry && entry[prePath]) { + let permission = entry[prePath][id]; + return this._get(entry, prePath, id, permission); + } + return null; + }, + + // Gets all permissions for the specified browser. + // Note that only permissions that apply to the current URI + // of the passed browser element will be returned. + getAll(browser) { + let permissions = []; + let entry = this._stateByBrowser.get(browser); + let prePath = browser.currentURI.prePath; + if (entry && entry[prePath]) { + let timeStamps = entry[prePath]; + for (let id of Object.keys(timeStamps)) { + let permission = this._get(entry, prePath, id, timeStamps[id]); + // _get() returns null when the permission has expired. + if (permission) { + permissions.push(permission); + } + } + } + return permissions; + }, + + // Clears all permissions for the specified browser. + // Unlike other methods, this does NOT clear only for + // the currentURI but the whole browser state. + clear(browser) { + this._stateByBrowser.delete(browser); + }, + + // Copies the temporary permission state of one browser + // into a new entry for the other browser. + copy(browser, newBrowser) { + let entry = this._stateByBrowser.get(browser); + if (entry) { + this._stateByBrowser.set(newBrowser, entry); + } + }, +}; + +/** + * A module to manage permanent and temporary permissions + * by URI and browser. + * + * Some methods have the side effect of dispatching a "PermissionStateChange" + * event on changes to temporary permissions, as mentioned in the respective docs. + */ +var SitePermissions = { + // Permission states. + UNKNOWN: Services.perms.UNKNOWN_ACTION, + ALLOW: Services.perms.ALLOW_ACTION, + BLOCK: Services.perms.DENY_ACTION, + PROMPT: Services.perms.PROMPT_ACTION, + ALLOW_COOKIES_FOR_SESSION: Ci.nsICookiePermission.ACCESS_SESSION, + + // Permission scopes. + SCOPE_REQUEST: "{SitePermissions.SCOPE_REQUEST}", + SCOPE_TEMPORARY: "{SitePermissions.SCOPE_TEMPORARY}", + SCOPE_SESSION: "{SitePermissions.SCOPE_SESSION}", + SCOPE_PERSISTENT: "{SitePermissions.SCOPE_PERSISTENT}", + + _defaultPrefBranch: Services.prefs.getBranch("permissions.default."), + + /** + * Deprecated! Please use getAllByPrincipal(principal) instead. + * Gets all custom permissions for a given URI. + * Install addon permission is excluded, check bug 1303108. + * + * @return {Array} a list of objects with the keys: + * - id: the permissionId of the permission + * - scope: the scope of the permission (e.g. SitePermissions.SCOPE_TEMPORARY) + * - state: a constant representing the current permission state + * (e.g. SitePermissions.ALLOW) + */ + getAllByURI(uri) { + let principal = uri ? Services.scriptSecurityManager.createCodebasePrincipal(uri, {}) : null; + return this.getAllByPrincipal(principal); + }, + + /** + * Gets all custom permissions for a given principal. + * Install addon permission is excluded, check bug 1303108. + * + * @return {Array} a list of objects with the keys: + * - id: the permissionId of the permission + * - scope: the scope of the permission (e.g. SitePermissions.SCOPE_TEMPORARY) + * - state: a constant representing the current permission state + * (e.g. SitePermissions.ALLOW) + */ + getAllByPrincipal(principal) { + let result = []; + if (!this.isSupportedPrincipal(principal)) { + return result; + } + + let permissions = Services.perms.getAllForPrincipal(principal); + while (permissions.hasMoreElements()) { + let permission = permissions.getNext(); + + // filter out unknown permissions + if (gPermissionObject[permission.type]) { + // XXX Bug 1303108 - Control Center should only show non-default permissions + if (permission.type == "install") { + continue; + } + let scope = this.SCOPE_PERSISTENT; + if (permission.expireType == Services.perms.EXPIRE_SESSION) { + scope = this.SCOPE_SESSION; + } + result.push({ + id: permission.type, + scope, + state: permission.capability, + }); + } + } + + return result; + }, + + /** + * Returns all custom permissions for a given browser. + * + * To receive a more detailed, albeit less performant listing see + * SitePermissions.getAllPermissionDetailsForBrowser(). + * + * @param {Browser} browser + * The browser to fetch permission for. + * + * @return {Array} a list of objects with the keys: + * - id: the permissionId of the permission + * - state: a constant representing the current permission state + * (e.g. SitePermissions.ALLOW) + * - scope: a constant representing how long the permission will + * be kept. + */ + getAllForBrowser(browser) { + let permissions = {}; + + for (let permission of TemporaryBlockedPermissions.getAll(browser)) { + permission.scope = this.SCOPE_TEMPORARY; + permissions[permission.id] = permission; + } + + for (let permission of this.getAllByPrincipal(browser.contentPrincipal)) { + permissions[permission.id] = permission; + } + + return Object.values(permissions); + }, + + /** + * Returns a list of objects with detailed information on all permissions + * that are currently set for the given browser. + * + * @param {Browser} browser + * The browser to fetch permission for. + * + * @return {Array<Object>} a list of objects with the keys: + * - id: the permissionID of the permission + * - state: a constant representing the current permission state + * (e.g. SitePermissions.ALLOW) + * - scope: a constant representing how long the permission will + * be kept. + * - label: the localized label + */ + getAllPermissionDetailsForBrowser(browser) { + return this.getAllForBrowser(browser).map(({id, scope, state}) => + ({id, scope, state, label: this.getPermissionLabel(id)})); + }, + + /** + * Deprecated! Please use isSupportedPrincipal(principal) instead. + * Checks whether a UI for managing permissions should be exposed for a given + * URI. + * + * @param {nsIURI} uri + * The URI to check. + * + * @return {boolean} if the URI is supported. + */ + isSupportedURI(uri) { + return uri && ["file", "http", "https", "moz-extension"].includes(uri.scheme); + }, + + /** + * Checks whether a UI for managing permissions should be exposed for a given + * principal. + * + * @param {nsIPrincipal} principal + * The principal to check. + * + * @return {boolean} if the principal is supported. + */ + isSupportedPrincipal(principal) { + return principal && principal.URI && + ["file", "http", "https", "moz-extension"].includes(principal.URI.scheme); + }, + + /** + * Gets an array of all permission IDs. + * + * @return {Array<String>} an array of all permission IDs. + */ + listPermissions() { + return Object.keys(gPermissionObject); + }, + + /** + * Returns an array of permission states to be exposed to the user for a + * permission with the given ID. + * + * @param {string} permissionID + * The ID to get permission states for. + * + * @return {Array<SitePermissions state>} an array of all permission states. + */ + getAvailableStates(permissionID) { + if (permissionID in gPermissionObject && + gPermissionObject[permissionID].states) + return gPermissionObject[permissionID].states; + + /* Since the permissions we are dealing with have adopted the convention + * of treating UNKNOWN == PROMPT, we only include one of either UNKNOWN + * or PROMPT in this list, to avoid duplicating states. */ + if (this.getDefault(permissionID) == this.UNKNOWN) + return [ SitePermissions.UNKNOWN, SitePermissions.ALLOW, SitePermissions.BLOCK ]; + + return [ SitePermissions.PROMPT, SitePermissions.ALLOW, SitePermissions.BLOCK ]; + }, + + /** + * Returns the default state of a particular permission. + * + * @param {string} permissionID + * The ID to get the default for. + * + * @return {SitePermissions.state} the default state. + */ + getDefault(permissionID) { + // If the permission has custom logic for getting its default value, + // try that first. + if (permissionID in gPermissionObject && + gPermissionObject[permissionID].getDefault) + return gPermissionObject[permissionID].getDefault(); + + // Otherwise try to get the default preference for that permission. + return this._defaultPrefBranch.getIntPref(permissionID, this.UNKNOWN); + }, + + /** + * Set the default state of a particular permission. + * + * @param {string} permissionID + * The ID to set the default for. + * + * @param {string} state + * The state to set. + */ + setDefault(permissionID, state) { + if (permissionID in gPermissionObject && + gPermissionObject[permissionID].setDefault) { + return gPermissionObject[permissionID].setDefault(state); + } + let key = "permissions.default." + permissionID; + return Services.prefs.setIntPref(key, state); + }, + /** + * Returns the state and scope of a particular permission for a given URI. + * + * This method will NOT dispatch a "PermissionStateChange" event on the specified + * browser if a temporary permission was removed because it has expired. + * + * @param {nsIURI} uri + * The URI to check. + * @param {String} permissionID + * The id of the permission. + * @param {Browser} browser (optional) + * The browser object to check for temporary permissions. + * + * @return {Object} an object with the keys: + * - state: The current state of the permission + * (e.g. SitePermissions.ALLOW) + * - scope: The scope of the permission + * (e.g. SitePermissions.SCOPE_PERSISTENT) + */ + get(uri, permissionID, browser) { + let principal = uri ? Services.scriptSecurityManager.createCodebasePrincipal(uri, {}) : null; + return this.getForPrincipal(principal, permissionID, browser); + }, + + /** + * Returns the state and scope of a particular permission for a given + * principal. + * + * This method will NOT dispatch a "PermissionStateChange" event on the + * specified browser if a temporary permission was removed because it has + * expired. + * + * @param {nsIPrincipal} principal + * The principal to check. + * @param {String} permissionID + * The id of the permission. + * @param {Browser} browser (optional) + * The browser object to check for temporary permissions. + * + * @return {Object} an object with the keys: + * - state: The current state of the permission + * (e.g. SitePermissions.ALLOW) + * - scope: The scope of the permission + * (e.g. SitePermissions.SCOPE_PERSISTENT) + */ + getForPrincipal(principal, permissionID, browser) { + let defaultState = this.getDefault(permissionID); + let result = { state: defaultState, scope: this.SCOPE_PERSISTENT }; + if (this.isSupportedPrincipal(principal)) { + let permission = null; + if (permissionID in gPermissionObject && + gPermissionObject[permissionID].exactHostMatch) { + permission = Services.perms.getPermissionObject(principal, permissionID, true); + } else { + permission = Services.perms.getPermissionObject(principal, permissionID, false); + } + + if (permission) { + result.state = permission.capability; + if (permission.expireType == Services.perms.EXPIRE_SESSION) { + result.scope = this.SCOPE_SESSION; + } + } + } + + if (result.state == defaultState) { + // If there's no persistent permission saved, check if we have something + // set temporarily. + let value = TemporaryBlockedPermissions.get(browser, permissionID); + + if (value) { + result.state = value.state; + result.scope = this.SCOPE_TEMPORARY; + } + } + + return result; + }, + + /** + * Deprecated! Use setForPrincipal(...) instead. + * Sets the state of a particular permission for a given URI or browser. + * This method will dispatch a "PermissionStateChange" event on the specified + * browser if a temporary permission was set + * + * @param {nsIURI} uri + * The URI to set the permission for. + * Note that this will be ignored if the scope is set to SCOPE_TEMPORARY + * @param {String} permissionID + * The id of the permission. + * @param {SitePermissions state} state + * The state of the permission. + * @param {SitePermissions scope} scope (optional) + * The scope of the permission. Defaults to SCOPE_PERSISTENT. + * @param {Browser} browser (optional) + * The browser object to set temporary permissions on. + * This needs to be provided if the scope is SCOPE_TEMPORARY! + */ + set(uri, permissionID, state, scope = this.SCOPE_PERSISTENT, browser = null) { + let principal = uri ? Services.scriptSecurityManager.createCodebasePrincipal(uri, {}) : null; + return this.setForPrincipal(principal, permissionID, state, scope, browser); + }, + + /** + * Sets the state of a particular permission for a given principal or browser. + * This method will dispatch a "PermissionStateChange" event on the specified + * browser if a temporary permission was set + * + * @param {nsIPrincipal} principal + * The principal to set the permission for. + * Note that this will be ignored if the scope is set to SCOPE_TEMPORARY + * @param {String} permissionID + * The id of the permission. + * @param {SitePermissions state} state + * The state of the permission. + * @param {SitePermissions scope} scope (optional) + * The scope of the permission. Defaults to SCOPE_PERSISTENT. + * @param {Browser} browser (optional) + * The browser object to set temporary permissions on. + * This needs to be provided if the scope is SCOPE_TEMPORARY! + */ + setForPrincipal(principal, permissionID, state, + scope = this.SCOPE_PERSISTENT, browser = null) { + if (state == this.UNKNOWN || state == this.getDefault(permissionID)) { + // Because they are controlled by two prefs with many states that do not + // correspond to the classical ALLOW/DENY/PROMPT model, we want to always + // allow the user to add exceptions to their cookie rules without + // removing them. + if (permissionID != "cookie") { + this.removeFromPrincipal(principal, permissionID, browser); + return; + } + } + + if (state == this.ALLOW_COOKIES_FOR_SESSION && permissionID != "cookie") { + throw "ALLOW_COOKIES_FOR_SESSION can only be set on the cookie permission"; + } + + // Save temporary permissions. + if (scope == this.SCOPE_TEMPORARY) { + // We do not support setting temp ALLOW for security reasons. + // In its current state, this permission could be exploited by subframes + // on the same page. This is because for BLOCK we ignore the request + // principal and only consider the current browser principal, to avoid + // notification spamming. + // + // If you ever consider removing this line, you likely want to implement + // a more fine-grained TemporaryBlockedPermissions that temporarily blocks for the + // entire browser, but temporarily allows only for specific frames. + if (state != this.BLOCK) { + throw "'Block' is the only permission we can save temporarily on a browser"; + } + + if (!browser) { + throw "TEMPORARY scoped permissions require a browser object"; + } + + TemporaryBlockedPermissions.set(browser, permissionID); + + browser.dispatchEvent(new browser.ownerGlobal + .CustomEvent("PermissionStateChange")); + } else if (this.isSupportedPrincipal(principal)) { + let perms_scope = Services.perms.EXPIRE_NEVER; + if (scope == this.SCOPE_SESSION) { + perms_scope = Services.perms.EXPIRE_SESSION; + } + + Services.perms.addFromPrincipal(principal, permissionID, state, perms_scope); + } + }, + + /** + * Deprecated! Please use removeFromPrincipal(principal, permissionID, browser). + * Removes the saved state of a particular permission for a given URI and/or browser. + * This method will dispatch a "PermissionStateChange" event on the specified + * browser if a temporary permission was removed. + * + * @param {nsIURI} uri + * The URI to remove the permission for. + * @param {String} permissionID + * The id of the permission. + * @param {Browser} browser (optional) + * The browser object to remove temporary permissions on. + */ + remove(uri, permissionID, browser) { + let principal = uri ? Services.scriptSecurityManager.createCodebasePrincipal(uri, {}) : null; + return this.removeFromPrincipal(principal, permissionID, browser); + }, + + /** + * Removes the saved state of a particular permission for a given principal + * and/or browser. + * This method will dispatch a "PermissionStateChange" event on the specified + * browser if a temporary permission was removed. + * + * @param {nsIPrincipal} principal + * The principal to remove the permission for. + * @param {String} permissionID + * The id of the permission. + * @param {Browser} browser (optional) + * The browser object to remove temporary permissions on. + */ + removeFromPrincipal(principal, permissionID, browser) { + if (this.isSupportedPrincipal(principal)) + Services.perms.removeFromPrincipal(principal, permissionID); + + // TemporaryBlockedPermissions.get() deletes expired permissions automatically, + if (TemporaryBlockedPermissions.get(browser, permissionID)) { + // If it exists but has not expired, remove it explicitly. + TemporaryBlockedPermissions.remove(browser, permissionID); + // Send a PermissionStateChange event only if the permission hasn't expired. + browser.dispatchEvent(new browser.ownerGlobal + .CustomEvent("PermissionStateChange")); + } + }, + + /** + * Clears all permissions that were temporarily saved. + * + * @param {Browser} browser + * The browser object to clear. + */ + clearTemporaryPermissions(browser) { + TemporaryBlockedPermissions.clear(browser); + }, + + /** + * Copy all permissions that were temporarily saved on one + * browser object to a new browser. + * + * @param {Browser} browser + * The browser object to copy from. + * @param {Browser} newBrowser + * The browser object to copy to. + */ + copyTemporaryPermissions(browser, newBrowser) { + TemporaryBlockedPermissions.copy(browser, newBrowser); + }, + + /** + * Returns the localized label for the permission with the given ID, to be + * used in a UI for managing permissions. + * + * @param {string} permissionID + * The permission to get the label for. + * + * @return {String} the localized label. + */ + getPermissionLabel(permissionID) { + let labelID = gPermissionObject[permissionID].labelID || permissionID; + return gStringBundle.GetStringFromName("permission." + labelID + ".label"); + }, + + /** + * Returns the localized label for the given permission state, to be used in + * a UI for managing permissions. + * + * @param {string} permissionID + * The permission to get the label for. + * + * @param {SitePermissions state} state + * The state to get the label for. + * + * @return {String|null} the localized label or null if an + * unknown state was passed. + */ + getMultichoiceStateLabel(permissionID, state) { + // If the permission has custom logic for getting its default value, + // try that first. + if (permissionID in gPermissionObject && + gPermissionObject[permissionID].getMultichoiceStateLabel) { + return gPermissionObject[permissionID].getMultichoiceStateLabel(state); + } + + switch (state) { + case this.UNKNOWN: + case this.PROMPT: + return gStringBundle.GetStringFromName("state.multichoice.alwaysAsk"); + case this.ALLOW: + return gStringBundle.GetStringFromName("state.multichoice.allow"); + case this.ALLOW_COOKIES_FOR_SESSION: + return gStringBundle.GetStringFromName("state.multichoice.allowForSession"); + case this.BLOCK: + return gStringBundle.GetStringFromName("state.multichoice.block"); + default: + return null; + } + }, + + /** + * Returns the localized label for a permission's current state. + * + * @param {SitePermissions state} state + * The state to get the label for. + * @param {string} id + * The permission to get the state label for. + * @param {SitePermissions scope} scope (optional) + * The scope to get the label for. + * + * @return {String|null} the localized label or null if an + * unknown state was passed. + */ + getCurrentStateLabel(state, id, scope = null) { + switch (state) { + case this.PROMPT: + return gStringBundle.GetStringFromName("state.current.prompt"); + case this.ALLOW: + if (scope && scope != this.SCOPE_PERSISTENT) + return gStringBundle.GetStringFromName("state.current.allowedTemporarily"); + return gStringBundle.GetStringFromName("state.current.allowed"); + case this.ALLOW_COOKIES_FOR_SESSION: + return gStringBundle.GetStringFromName("state.current.allowedForSession"); + case this.BLOCK: + if (scope && scope != this.SCOPE_PERSISTENT) + return gStringBundle.GetStringFromName("state.current.blockedTemporarily"); + return gStringBundle.GetStringFromName("state.current.blocked"); + default: + return null; + } + }, +}; + +var gPermissionObject = { + /* Holds permission ID => options pairs. + * + * Supported options: + * + * - exactHostMatch + * Allows sub domains to have their own permissions. + * Defaults to false. + * + * - getDefault + * Called to get the permission's default state. + * Defaults to UNKNOWN, indicating that the user will be asked each time + * a page asks for that permissions. + * + * - labelID + * Use the given ID instead of the permission name for looking up strings. + * e.g. "desktop-notification2" to use permission.desktop-notification2.label + * + * - states + * Array of permission states to be exposed to the user. + * Defaults to ALLOW, BLOCK and the default state (see getDefault). + * + * - getMultichoiceStateLabel + * Allows for custom logic for getting its default value + */ + + "image": { + states: [ + SitePermissions.ALLOW, + SitePermissions.PROMPT, + SitePermissions.BLOCK + ], + getMultichoiceStateLabel(state) { + switch (state) { + case SitePermissions.ALLOW: + return gStringBundle.GetStringFromName("state.multichoice.allow"); + // Equates to BEHAVIOR_NOFOREIGN from nsContentBlocker.cpp + case SitePermissions.PROMPT: + return gStringBundle.GetStringFromName("state.multichoice.allowForSameDomain"); + case SitePermissions.BLOCK: + return gStringBundle.GetStringFromName("state.multichoice.block"); + } + throw new Error(`Unknown state: ${state}`); + }, + }, + + "cookie": { + states: [ SitePermissions.ALLOW, SitePermissions.ALLOW_COOKIES_FOR_SESSION, SitePermissions.BLOCK ], + getDefault() { + if (Services.prefs.getIntPref("network.cookie.cookieBehavior") == Ci.nsICookieService.BEHAVIOR_REJECT) + return SitePermissions.BLOCK; + + if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") == Ci.nsICookieService.ACCEPT_SESSION) + return SitePermissions.ALLOW_COOKIES_FOR_SESSION; + + return SitePermissions.ALLOW; + } + }, + + "desktop-notification": { + exactHostMatch: true, + labelID: "desktop-notification2", + }, + + "camera": { + exactHostMatch: true, + }, + + "microphone": { + exactHostMatch: true, + }, + + "screen": { + exactHostMatch: true, + states: [ SitePermissions.UNKNOWN, SitePermissions.BLOCK ], + }, + + "popup": { + getDefault() { + return Services.prefs.getBoolPref("dom.disable_open_during_load") ? + SitePermissions.BLOCK : SitePermissions.ALLOW; + }, + states: [ SitePermissions.ALLOW, SitePermissions.BLOCK ], + }, + + "install": { + getDefault() { + return Services.prefs.getBoolPref("xpinstall.whitelist.required") ? + SitePermissions.BLOCK : SitePermissions.ALLOW; + }, + states: [ SitePermissions.ALLOW, SitePermissions.BLOCK ], + }, + + "geo": { + exactHostMatch: true + }, + + "focus-tab-by-prompt": { + exactHostMatch: true, + states: [ SitePermissions.UNKNOWN, SitePermissions.ALLOW ], + }, + + "persistent-storage": { + exactHostMatch: true + }, + +}; + +// Delete this entry while being pre-off +// or the persistent-storage permission would appear in Page info's Permission section +if (!Services.prefs.getBoolPref("browser.storageManager.enabled")) { + delete gPermissionObject["persistent-storage"]; +} + +XPCOMUtils.defineLazyPreferenceGetter(SitePermissions, "temporaryPermissionExpireTime", + "privacy.temporary_permission_expire_time_ms", 3600 * 1000); diff --git a/comm/suite/modules/ThemeVariableMap.jsm b/comm/suite/modules/ThemeVariableMap.jsm new file mode 100644 index 0000000000..8da6c4e476 --- /dev/null +++ b/comm/suite/modules/ThemeVariableMap.jsm @@ -0,0 +1,29 @@ +/* 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/. */ + +var EXPORTED_SYMBOLS = ["ThemeVariableMap"]; + +const ThemeVariableMap = [ + ["--lwt-accent-color-inactive", "accentcolorInactive"], + ["--lwt-background-alignment", "backgroundsAlignment"], + ["--lwt-background-tiling", "backgroundsTiling"], + ["--tab-loading-fill", "tab_loading"], + ["--lwt-tab-text", "tab_text"], + ["--toolbar-bgcolor", "toolbarColor"], + ["--toolbar-color", "toolbar_text"], + ["--url-and-searchbar-background-color", "toolbar_field"], + ["--url-and-searchbar-color", "toolbar_field_text"], + ["--lwt-toolbar-field-border-color", "toolbar_field_border"], + ["--urlbar-separator-color", "toolbar_field_separator"], + ["--tabs-border-color", "toolbar_top_separator"], + ["--lwt-toolbar-vertical-separator", "toolbar_vertical_separator"], + ["--toolbox-border-bottom-color", "toolbar_bottom_separator"], + ["--lwt-toolbarbutton-icon-fill", "icon_color"], + ["--lwt-toolbarbutton-icon-fill-attention", "icon_attention_color"], + ["--lwt-toolbarbutton-hover-background", "button_background_hover"], + ["--lwt-toolbarbutton-active-background", "button_background_active"], + ["--arrowpanel-background", "popup"], + ["--arrowpanel-color", "popup_text"], + ["--arrowpanel-border-color", "popup_border"], +]; diff --git a/comm/suite/modules/WindowsJumpLists.jsm b/comm/suite/modules/WindowsJumpLists.jsm new file mode 100644 index 0000000000..2a19f71628 --- /dev/null +++ b/comm/suite/modules/WindowsJumpLists.jsm @@ -0,0 +1,604 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +/** + * Constants + */ + +// Stop updating jumplists after some idle time. +const IDLE_TIMEOUT_SECONDS = 5 * 60; + +// Prefs +const PREF_TASKBAR_BRANCH = "browser.taskbar.lists."; +const PREF_TASKBAR_ENABLED = "enabled"; +const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount"; +const PREF_TASKBAR_FREQUENT = "frequent.enabled"; +const PREF_TASKBAR_RECENT = "recent.enabled"; +const PREF_TASKBAR_TASKS = "tasks.enabled"; +const PREF_TASKBAR_REFRESH = "refreshInSeconds"; + +// Hash keys for pendingStatements. +const LIST_TYPE = { + FREQUENT: 0 +, RECENT: 1 +} + +/** + * Exports + */ + +var EXPORTED_SYMBOLS = [ + "WinTaskbarJumpList", +]; + +/** + * Smart getters + */ + +XPCOMUtils.defineLazyGetter(this, "_prefs", function() { + return Services.prefs.getBranch(PREF_TASKBAR_BRANCH); +}); + +XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() { + return Services.strings + .createBundle("chrome://navigator/locale/taskbar.properties"); +}); + +XPCOMUtils.defineLazyServiceGetter(this, "_idle", + "@mozilla.org/widget/idleservice;1", + "nsIIdleService"); + +XPCOMUtils.defineLazyServiceGetter(this, "_taskbarService", + "@mozilla.org/windows-taskbar;1", + "nsIWinTaskbar"); + +ChromeUtils.defineModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +/** + * Global functions + */ + +function _getString(name) { + return _stringBundle.GetStringFromName(name); +} + +///////////////////////////////////////////////////////////////////////////// +// Task list configuration data object. + +var tasksCfg = [ + /** + * Task configuration options: title, description, args, iconIndex, open, close. + * + * title - Task title displayed in the list. (strings in the table are temp fillers.) + * description - Tooltip description on the list item. + * args - Command line args to invoke the task. + * iconIndex - Optional win icon index into the main application for the + * list item. + * open - Boolean indicates if the command should be visible after the browser opens. + * close - Boolean indicates if the command should be visible after the browser closes. + */ + // Open new tab + { + get title() { return _getString("taskbar.tasks.newTab.label"); }, + get description() { return _getString("taskbar.tasks.newTab.description"); }, + args: "-new-tab about:blank", + iconIndex: 0, // SeaMonkey app icon + open: true, + close: true, // The jump list already has an app launch icon, but + // we don't always update the list on shutdown. + // Thus true for consistency. + }, + + // Open new window + { + get title() { return _getString("taskbar.tasks.newWindow.label"); }, + get description() { return _getString("taskbar.tasks.newWindow.description"); }, + args: "-browser", + iconIndex: 0, // SeaMonkey app icon + open: true, + close: true, // No point, but we don't always update the list on + // shutdown. Thus true for consistency. + }, + + // Open private window + { + get title() { return _getString("taskbar.tasks.newPrivate.label"); }, + get description() { return _getString("taskbar.tasks.newPrivate.description"); }, + args: "-private", + iconIndex: 0, // SeaMonkey app icon + open: true, + close: true, // No point, but we don't always update the list on + // shutdown. Thus true for consistency. + }, + + // Open mailnews + { + get title() { return _getString("taskbar.tasks.mailWindow.label"); }, + get description() { return _getString("taskbar.tasks.mailWindow.description"); }, + args: "-mail", + iconIndex: 0, // SeaMonkey app icon + open: true, + close: true, // No point, but we don't always update the list on + // shutdown. Thus true for consistency. + }, + + // Compose Message + { + get title() { return _getString("taskbar.tasks.composeMessage.label"); }, + get description() { return _getString("taskbar.tasks.composeMessage.description"); }, + args: "-compose", + iconIndex: 0, // SeaMonkey app icon + open: true, + close: true, // No point, but we don't always update the list on + // shutdown. Thus true for consistency. + }, + + // Address Book + { + get title() { return _getString("taskbar.tasks.openAddressBook.label"); }, + get description() { return _getString("taskbar.tasks.openAddressBook.description"); }, + args: "-addressbook", + iconIndex: 0, // SeaMonkey app icon + open: true, + close: true, // No point, but we don't always update the list on + // shutdown. Thus true for consistency. + }, + + // Composer + { + get title() { return _getString("taskbar.tasks.openEditor.label"); }, + get description() { return _getString("taskbar.tasks.openEditor.description"); }, + args: "-edit", + iconIndex: 0, // SeaMonkey app icon + open: true, + close: true, // No point, but we don't always update the list on + // shutdown. Thus true for consistency. + }, +]; + +///////////////////////////////////////////////////////////////////////////// +// Implementation + +var WinTaskbarJumpList = +{ + _builder: null, + _tasks: null, + _shuttingDown: false, + + /** + * Startup, shutdown, and update + */ + + startup: function WTBJL_startup() { + // exit if this isn't win7 or higher. + if (!this._initTaskbar()) + return; + + // Store our task list config data + this._tasks = tasksCfg; + + // retrieve taskbar related prefs. + this._refreshPrefs(); + + // observer for our prefs branch + this._initObs(); + + // jump list refresh timer + this._updateTimer(); + }, + + update: function WTBJL_update() { + // are we disabled via prefs? don't do anything! + if (!this._enabled) + return; + + // do what we came here to do, update the taskbar jumplist + this._buildList(); + }, + + _shutdown: function WTBJL__shutdown() { + this._shuttingDown = true; + + // Correctly handle a clear history on shutdown. If there are no + // entries be sure to empty all history lists. Luckily Places caches + // this value, so it's a pretty fast call. + if (!PlacesUtils.history.hasHistoryEntries) { + this.update(); + } + + this._free(); + }, + + /** + * List building + * + * @note Async builders must add their mozIStoragePendingStatement to + * _pendingStatements object, using a different LIST_TYPE entry for + * each statement. Once finished they must remove it and call + * commitBuild(). When there will be no more _pendingStatements, + * commitBuild() will commit for real. + */ + + _pendingStatements: {}, + _hasPendingStatements: function WTBJL__hasPendingStatements() { + return Object.keys(this._pendingStatements).length > 0; + }, + + _buildList: function WTBJL__buildList() { + if (this._hasPendingStatements()) { + // We were requested to update the list while another update was in + // progress, this could happen at shutdown or idle. + // Abort the current list building. + for (let listType in this._pendingStatements) { + this._pendingStatements[listType].cancel(); + delete this._pendingStatements[listType]; + } + this._builder.abortListBuild(); + } + + // anything to build? + if (!this._showFrequent && !this._showRecent && !this._showTasks) { + // don't leave the last list hanging on the taskbar. + this._deleteActiveJumpList(); + return; + } + + if (!this._startBuild()) + return; + + if (this._showTasks) + this._buildTasks(); + + // Space for frequent items takes priority over recent. + if (this._showFrequent) + this._buildFrequent(); + + if (this._showRecent) + this._buildRecent(); + + this._commitBuild(); + }, + + /** + * Taskbar api wrappers + */ + + _startBuild: function WTBJL__startBuild() { + var removedItems = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + this._builder.abortListBuild(); + if (this._builder.initListBuild(removedItems)) { + // Prior to building, delete removed items from history. + this._clearHistory(removedItems); + return true; + } + return false; + }, + + _commitBuild: function WTBJL__commitBuild() { + + if (this._hasPendingStatements()) { + return; + } + + this._builder.commitListBuild(succeed => { + if (!succeed) { + this._builder.abortListBuild(); + } + }); + }, + + _buildTasks: function WTBJL__buildTasks() { + var items = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + this._tasks.forEach(function (task) { + if ((this._shuttingDown && !task.close) || (!this._shuttingDown && !task.open)) + return; + var item = this._getHandlerAppItem(task.title, task.description, + task.args, task.iconIndex, null); + items.appendElement(item); + }, this); + + if (items.length > 0) + this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_TASKS, items); + }, + + _buildCustom: function WTBJL__buildCustom(title, items) { + if (items.length > 0) + this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_CUSTOMLIST, items, title); + }, + + _buildFrequent: function WTBJL__buildFrequent() { + // If history is empty, just bail out. + if (!PlacesUtils.history.hasHistoryEntries) { + return; + } + + // Windows supports default frequent and recent lists, + // but those depend on internal windows visit tracking + // which we don't populate. So we build our own custom + // frequent and recent lists using our nav history data. + + var items = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + // track frequent items so that we don't add them to + // the recent list. + this._frequentHashList = []; + + this._pendingStatements[LIST_TYPE.FREQUENT] = this._getHistoryResults( + Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING, + this._maxItemCount, + function (aResult) { + if (!aResult) { + delete this._pendingStatements[LIST_TYPE.FREQUENT]; + // The are no more results, build the list. + this._buildCustom(_getString("taskbar.frequent.label"), items); + this._commitBuild(); + return; + } + + let title = aResult.title || aResult.uri; + let faviconPageUri = Services.io.newURI(aResult.uri); + let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 2, + faviconPageUri); + items.appendElement(shortcut); + this._frequentHashList.push(aResult.uri); + }, + this + ); + }, + + _buildRecent: function WTBJL__buildRecent() { + // If history is empty, just bail out. + if (!PlacesUtils.history.hasHistoryEntries) { + return; + } + + var items = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + // Frequent items will be skipped, so we select a double amount of + // entries and stop fetching results at _maxItemCount. + var count = 0; + + this._pendingStatements[LIST_TYPE.RECENT] = this._getHistoryResults( + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING, + this._maxItemCount * 2, + function (aResult) { + if (!aResult) { + // The are no more results, build the list. + this._buildCustom(_getString("taskbar.recent.label"), items); + delete this._pendingStatements[LIST_TYPE.RECENT]; + this._commitBuild(); + return; + } + + if (count >= this._maxItemCount) { + return; + } + + // Do not add items to recent that have already been added to frequent. + if (this._frequentHashList && + this._frequentHashList.includes(aResult.uri)) { + return; + } + + let title = aResult.title || aResult.uri; + let faviconPageUri = Services.io.newURI(aResult.uri); + let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 2, + faviconPageUri); + items.appendElement(shortcut); + count++; + }, + this + ); + }, + + _deleteActiveJumpList: function WTBJL__deleteAJL() { + this._builder.deleteActiveList(); + }, + + /** + * Jump list item creation helpers + */ + + _getHandlerAppItem: function WTBJL__getHandlerAppItem(name, description, + args, iconIndex, + faviconPageUri) { + var file = Services.dirsvc.get("XREExeF", Ci.nsIFile); + + var handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + handlerApp.executable = file; + // handlers default to the leaf name if a name is not specified + if (name && name.length != 0) + handlerApp.name = name; + handlerApp.detailedDescription = description; + handlerApp.appendParameter(args); + + var item = Cc["@mozilla.org/windows-jumplistshortcut;1"]. + createInstance(Ci.nsIJumpListShortcut); + item.app = handlerApp; + item.iconIndex = iconIndex; + item.faviconPageUri = faviconPageUri; + return item; + }, + + _getSeparatorItem: function WTBJL__getSeparatorItem() { + var item = Cc["@mozilla.org/windows-jumplistseparator;1"]. + createInstance(Ci.nsIJumpListSeparator); + return item; + }, + + /** + * Nav history helpers + */ + + _getHistoryResults: + function WTBLJL__getHistoryResults(aSortingMode, aLimit, aCallback, aScope) { + var options = PlacesUtils.history.getNewQueryOptions(); + options.maxResults = aLimit; + options.sortingMode = aSortingMode; + var query = PlacesUtils.history.getNewQuery(); + + // Return the pending statement to the caller, to allow cancelation. + return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .asyncExecuteLegacyQueries([query], 1, options, { + handleResult: function (aResultSet) { + for (let row; (row = aResultSet.getNextRow());) { + try { + aCallback.call(aScope, + { uri: row.getResultByIndex(1) + , title: row.getResultByIndex(2) + }); + } catch (e) {} + } + }, + handleError: function (aError) { + Cu.reportError( + "Async execution error (" + aError.result + "): " + aError.message); + }, + handleCompletion: function (aReason) { + aCallback.call(WinTaskbarJumpList, null); + }, + }); + }, + + _clearHistory: function WTBJL__clearHistory(items) { + if (!items) + return; + var URIsToRemove = []; + var e = items.enumerate(); + while (e.hasMoreElements()) { + let oldItem = e.getNext().QueryInterface(Ci.nsIJumpListShortcut); + if (oldItem) { + try { // in case we get a bad uri + let uriSpec = oldItem.app.getParameter(0); + URIsToRemove.push(Services.io.newURI(uriSpec)); + } catch (err) { } + } + } + if (URIsToRemove.length > 0) { + PlacesUtils.history.remove(URIsToRemove).catch(Cu.reportError); + } + }, + + /** + * Prefs utilities + */ + + _refreshPrefs: function WTBJL__refreshPrefs() { + this._enabled = _prefs.getBoolPref(PREF_TASKBAR_ENABLED); + this._showFrequent = _prefs.getBoolPref(PREF_TASKBAR_FREQUENT); + this._showRecent = _prefs.getBoolPref(PREF_TASKBAR_RECENT); + this._showTasks = _prefs.getBoolPref(PREF_TASKBAR_TASKS); + this._maxItemCount = _prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT); + }, + + /** + * Init and shutdown utilities + */ + + _initTaskbar: function WTBJL__initTaskbar() { + this._builder = _taskbarService.createJumpListBuilder(); + if (!this._builder || !this._builder.available) + return false; + + return true; + }, + + _initObs: function WTBJL__initObs() { + // If the browser is closed while in private browsing mode, the "exit" + // notification is fired on quit-application-granted. + // History cleanup can happen at profile-change-teardown. + Services.obs.addObserver(this, "profile-before-change"); + Services.obs.addObserver(this, "browser:purge-session-history"); + _prefs.addObserver("", this); + }, + + _freeObs: function WTBJL__freeObs() { + Services.obs.removeObserver(this, "profile-before-change"); + Services.obs.removeObserver(this, "browser:purge-session-history"); + _prefs.removeObserver("", this); + }, + + _updateTimer: function WTBJL__updateTimer() { + if (this._enabled && !this._shuttingDown && !this._timer) { + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._timer.initWithCallback(this, + _prefs.getIntPref(PREF_TASKBAR_REFRESH)*1000, + this._timer.TYPE_REPEATING_SLACK); + } + else if ((!this._enabled || this._shuttingDown) && this._timer) { + this._timer.cancel(); + delete this._timer; + } + }, + + _hasIdleObserver: false, + _updateIdleObserver: function WTBJL__updateIdleObserver() { + if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) { + _idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); + this._hasIdleObserver = true; + } + else if ((!this._enabled || this._shuttingDown) && this._hasIdleObserver) { + _idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); + this._hasIdleObserver = false; + } + }, + + _free: function WTBJL__free() { + this._freeObs(); + this._updateTimer(); + this._updateIdleObserver(); + delete this._builder; + }, + + /** + * Notification handlers + */ + + notify: function WTBJL_notify(aTimer) { + // Add idle observer on the first notification so it doesn't hit startup. + this._updateIdleObserver(); + Services.tm.idleDispatchToMainThread(() => { this.update(); }); + }, + + observe: function WTBJL_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "nsPref:changed": + if (this._enabled == true && !_prefs.getBoolPref(PREF_TASKBAR_ENABLED)) + this._deleteActiveJumpList(); + this._refreshPrefs(); + this._updateTimer(); + this._updateIdleObserver(); + Services.tm.idleDispatchToMainThread(() => { this.update(); }); + break; + + case "profile-before-change": + this._shutdown(); + break; + + case "browser:purge-session-history": + this.update(); + break; + + case "idle": + if (this._timer) { + this._timer.cancel(); + delete this._timer; + } + break; + + case "active": + this._updateTimer(); + break; + } + }, +}; + diff --git a/comm/suite/modules/WindowsPreviewPerTab.jsm b/comm/suite/modules/WindowsPreviewPerTab.jsm new file mode 100644 index 0000000000..d526331a97 --- /dev/null +++ b/comm/suite/modules/WindowsPreviewPerTab.jsm @@ -0,0 +1,872 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* 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/. */ +/* + * This module implements the front end behavior for AeroPeek. Starting in + * Windows Vista, the taskbar began showing live thumbnail previews of windows + * when the user hovered over the window icon in the taskbar. Starting with + * Windows 7, the taskbar allows an application to expose its tabbed interface + * in the taskbar by showing thumbnail previews rather than the default window + * preview. Additionally, when a user hovers over a thumbnail (tab or window), + * they are shown a live preview of the window (or tab + its containing window). + * + * In Windows 7, a title, icon, close button and optional toolbar are shown for + * each preview. This feature does not make use of the toolbar. For window + * previews, the title is the window title and the icon the window icon. For + * tab previews, the title is the page title and the page's favicon. In both + * cases, the close button "does the right thing." + * + * The primary objects behind this feature are nsITaskbarTabPreview and + * nsITaskbarPreviewController. Each preview has a controller. The controller + * responds to the user's interactions on the taskbar and provides the required + * data to the preview for determining the size of the tab and thumbnail. The + * PreviewController class implements this interface. The preview will request + * the controller to provide a thumbnail or preview when the user interacts with + * the taskbar. To reduce the overhead of drawing the tab area, the controller + * implementation caches the tab's contents in a <canvas> element. If no + * previews or thumbnails have been requested for some time, the controller will + * discard its cached tab contents. + * + * Screen real estate is limited so when there are too many thumbnails to fit + * on the screen, the taskbar stops displaying thumbnails and instead displays + * just the title, icon and close button in a similar fashion to previous + * versions of the taskbar. If there are still too many previews to fit on the + * screen, the taskbar resorts to a scroll up and scroll down button pair to let + * the user scroll through the list of tabs. Since this is undoubtedly + * inconvenient for users with many tabs, the AeroPeek objects turns off all of + * the tab previews. This tells the taskbar to revert to one preview per window. + * If the number of tabs falls below this magic threshold, the preview-per-tab + * behavior returns. There is no reliable way to determine when the scroll + * buttons appear on the taskbar, so a magic pref-controlled number determines + * when this threshold has been crossed. + */ +var EXPORTED_SYMBOLS = ["AeroPeek"]; + + +const {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const {PlacesUtils} = ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm"); +const {PrivateBrowsingUtils} = ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +// Pref to enable/disable preview-per-tab. +const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable"; +// Pref to determine the magic auto-disable threshold. +const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max"; +// Pref to control the time in seconds that tab contents live in the cache. +const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime"; + +const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; + +//////////////////////////////////////////////////////////////////////////////// +//// Various utility properties. +XPCOMUtils.defineLazyServiceGetter(this, "imgTools", + "@mozilla.org/image/tools;1", + "imgITools"); +ChromeUtils.defineModuleGetter(this, "PageThumbs", + "resource://gre/modules/PageThumbs.jsm"); + +// nsIURI -> imgIContainer +function _imageFromURI(uri, privateMode, callback) { + let channel = NetUtil.newChannel({ + uri: uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE + }); + + try { + channel.QueryInterface(Ci.nsIPrivateBrowsingChannel); + channel.setPrivate(privateMode); + } catch (e) { + // Ignore channels which do not support nsIPrivateBrowsingChannel. + } + NetUtil.asyncFetch(channel, function(inputStream, resultCode) { + if (!Components.isSuccessCode(resultCode)) { + return; + } + + const decodeCallback = { + onImageReady(image, status) { + if (!image) { + // We failed, so use the default favicon (only if this wasn't the + // default favicon). + let defaultURI = PlacesUtils.favicons.defaultFavicon; + if (!defaultURI.equals(uri)) { + _imageFromURI(defaultURI, privateMode, callback); + return; + } + } + + callback(image); + } + }; + + try { + let threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + imgTools.decodeImageAsync(inputStream, channel.contentType, + decodeCallback, threadManager.currentThread); + } catch (e) { + // We failed, so use the default favicon (only if this wasn't the default + // favicon). + let defaultURI = PlacesUtils.favicons.defaultFavicon; + if (!defaultURI.equals(uri)) + _imageFromURI(defaultURI, privateMode, callback); + } + }); +} + +// string? -> imgIContainer +function getFaviconAsImage(iconurl, privateMode, callback) { + if (iconurl) { + _imageFromURI(NetUtil.newURI(iconurl), privateMode, callback); + } else { + _imageFromURI(PlacesUtils.favicons.defaultFavicon, privateMode, callback); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// PreviewController + +/* + * This class manages the behavior of thumbnails and previews. It has the following + * responsibilities: + * 1) Responding to requests from Windows taskbar for a thumbnail or window + * preview. + * 2) Listens for dom events that result in a thumbnail or window preview needing + * to be refreshed and communicates this to the taskbar. + * 3) Handles querying and returning to the taskbar new thumbnail or window + * preview images through PageThumbs. + * + * @param win + * The TabWindow (see below) that owns the preview that this controls. + * @param tab + * The <tab> that this preview is associated with. + */ +function PreviewController(win, tab) { + this.win = win; + this.tab = tab; + this.linkedBrowser = tab.linkedBrowser; + this.preview = this.win.createTabPreview(this); + + this.tab.addEventListener("TabAttrModified", this); + + XPCOMUtils.defineLazyGetter(this, "canvasPreview", function () { + let canvas = PageThumbs.createCanvas(); + canvas.mozOpaque = true; + return canvas; + }); +} + +PreviewController.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController, + Ci.nsIDOMEventListener]), + + _cachedWidth: 0, + _cachedHeight: 0, + + destroy: function () { + this.tab.removeEventListener("TabAttrModified", this); + + // Break cycles, otherwise we end up leaking the window with everything + // attached to it. + delete this.win; + delete this.preview; + }, + + get wrappedJSObject() { + return this; + }, + + // Resizes the canvasPreview to 0x0, essentially freeing its memory. + resetCanvasPreview: function () { + this.canvasPreview.width = 0; + this.canvasPreview.height = 0; + }, + + // Set the canvas dimensions. + resizeCanvasPreview: function (aRequestedWidth, aRequestedHeight) { + this.canvasPreview.width = aRequestedWidth; + this.canvasPreview.height = aRequestedHeight; + }, + + get zoom() { + // Note that winutils.fullZoom accounts for "quantization" of the zoom factor + // from nsIContentViewer due to conversion through appUnits. + // We do -not- want screenPixelsPerCSSPixel here, because that would -also- + // incorporate any scaling that is applied due to hi-dpi resolution options. + return this.tab.linkedBrowser.fullZoom; + }, + + get screenPixelsPerCSSPixel() { + let chromeWin = this.tab.ownerGlobal; + let windowUtils = chromeWin.getInterface(Ci.nsIDOMWindowUtils); + return windowUtils.screenPixelsPerCSSPixel; + }, + + get browserDims() { + return this.tab.linkedBrowser.getBoundingClientRect(); + }, + + cacheBrowserDims: function () { + let dims = this.browserDims; + this._cachedWidth = dims.width; + this._cachedHeight = dims.height; + }, + + testCacheBrowserDims: function () { + let dims = this.browserDims; + return this._cachedWidth == dims.width && + this._cachedHeight == dims.height; + }, + + // Capture a new thumbnail image for this preview. Called by the controller + // in response to a request for a new thumbnail image. + updateCanvasPreview: function (aFullScale, aCallback) { + // Update our cached browser dims so that delayed resize + // events don't trigger another invalidation if this tab becomes active. + this.cacheBrowserDims(); + PageThumbs.captureToCanvas(this.linkedBrowser, this.canvasPreview, + aCallback, { fullScale: aFullScale }); + // If we're updating the canvas, then we're in the middle of a peek so + // don't discard the cache of previews. + AeroPeek.resetCacheTimer(); + }, + + updateTitleAndTooltip: function () { + let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser); + this.preview.title = title; + this.preview.tooltip = title; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsITaskbarPreviewController + + // Window width, not browser. + get width() { + return this.win.width; + }, + + // Window height, not browser. + get height() { + return this.win.height; + }, + + get thumbnailAspectRatio() { + let browserDims = this.browserDims; + // Avoid returning 0. + let tabWidth = browserDims.width || 1; + // Avoid divide by 0. + let tabHeight = browserDims.height || 1; + return tabWidth / tabHeight; + }, + + /** + * Responds to taskbar requests for window previews. Returns the results asynchronously + * through updateCanvasPreview. + * + * @param aTaskbarCallback nsITaskbarPreviewCallback results callback + */ + requestPreview: function (aTaskbarCallback) { + // Grab a high res content preview. + this.resetCanvasPreview(); + this.updateCanvasPreview(true, (aPreviewCanvas) => { + let winWidth = this.win.width; + let winHeight = this.win.height; + + let composite = PageThumbs.createCanvas(); + + // Use transparency, Aero glass is drawn black without it. + composite.mozOpaque = false; + + let ctx = composite.getContext('2d'); + let scale = this.screenPixelsPerCSSPixel / this.zoom; + + composite.width = winWidth * scale; + composite.height = winHeight * scale; + + ctx.save(); + ctx.scale(scale, scale); + + // Draw chrome. Note we currently do not get scrollbars for remote frames + // in the image above. + ctx.drawWindow(this.win.win, 0, 0, winWidth, winHeight, "rgba(0,0,0,0)"); + + // Draw the content are into the composite canvas at the right location. + ctx.drawImage(aPreviewCanvas, this.browserDims.x, this.browserDims.y, + aPreviewCanvas.width, aPreviewCanvas.height); + ctx.restore(); + + // Deliver the resulting composite canvas to Windows. + this.win.tabbrowser.previewTab(this.tab, function () { + aTaskbarCallback.done(composite, false); + }); + }); + }, + + /** + * Responds to taskbar requests for tab thumbnails. Returns the results asynchronously + * through updateCanvasPreview. + * + * Note Windows requests a specific width and height here, if the resulting thumbnail + * does not match these dimensions thumbnail display will fail. + * + * @param aTaskbarCallback nsITaskbarPreviewCallback results callback + * @param aRequestedWidth width of the requested thumbnail + * @param aRequestedHeight height of the requested thumbnail + */ + requestThumbnail: function (aTaskbarCallback, aRequestedWidth, aRequestedHeight) { + this.resizeCanvasPreview(aRequestedWidth, aRequestedHeight); + this.updateCanvasPreview(false, (aThumbnailCanvas) => { + aTaskbarCallback.done(aThumbnailCanvas, false); + }); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Event handling + + onClose: function () { + this.win.tabbrowser.removeTab(this.tab); + }, + + onActivate: function () { + this.win.tabbrowser.selectedTab = this.tab; + + // Accept activation - this will restore the browser window + // if it's minimized. + return true; + }, + + // nsIDOMEventListener + handleEvent: function (evt) { + switch (evt.type) { + case "TabAttrModified": + this.updateTitleAndTooltip(); + break; + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// TabWindow + +/* + * This class monitors a browser window for changes to its tabs. + * + * @param win + * The nsIDOMWindow browser window + */ +function TabWindow(win) { + this.win = win; + this.tabbrowser = win.getBrowser(); + + this.previews = new Map(); + + for (let i = 0; i < this.tabEvents.length; i++) + this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this); + + for (let i = 0; i < this.winEvents.length; i++) + this.win.addEventListener(this.winEvents[i], this); + + this.tabbrowser.addTabsProgressListener(this); + + AeroPeek.windows.push(this); + let tabs = this.tabbrowser.tabs; + for (let i = 0; i < tabs.length; i++) + this.newTab(tabs[i]); + + this.updateTabOrdering(); + AeroPeek.checkPreviewCount(); +} + +TabWindow.prototype = { + _enabled: false, + _cachedWidth: 0, + _cachedHeight: 0, + tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"], + winEvents: ["resize"], + + destroy: function () { + this._destroying = true; + + let tabs = this.tabbrowser.tabs; + + this.tabbrowser.removeTabsProgressListener(this); + + for (let i = 0; i < this.winEvents.length; i++) + this.win.removeEventListener(this.winEvents[i], this); + + for (let i = 0; i < this.tabEvents.length; i++) + this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this); + + for (let i = 0; i < tabs.length; i++) + this.removeTab(tabs[i]); + + let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup); + AeroPeek.windows.splice(idx, 1); + AeroPeek.checkPreviewCount(); + }, + + get width () { + return this.win.innerWidth; + }, + get height () { + return this.win.innerHeight; + }, + + cacheDims: function () { + this._cachedWidth = this.width; + this._cachedHeight = this.height; + }, + + testCacheDims: function () { + return this._cachedWidth == this.width && this._cachedHeight == this.height; + }, + + // Invoked when the given tab is added to this window. + newTab: function (tab) { + let controller = new PreviewController(this, tab); + // It's OK to add the preview now while the favicon still loads. + this.previews.set(tab, controller.preview); + AeroPeek.addPreview(controller.preview); + // updateTitleAndTooltip relies on having controller.preview which is lazily resolved. + // Now that we've updated this.previews, it will resolve successfully. + controller.updateTitleAndTooltip(); + }, + + createTabPreview: function (controller) { + let docShell = this.win + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + let preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller); + preview.visible = AeroPeek.enabled; + preview.active = this.tabbrowser.selectedTab == controller.tab; + this.onLinkIconAvailable(controller.tab.linkedBrowser, + controller.tab.getAttribute("image")); + + return preview; + }, + + // Invoked when the given tab is closed. + removeTab: function (tab) { + let preview = this.previewFromTab(tab); + preview.active = false; + preview.visible = false; + preview.move(null); + preview.controller.wrappedJSObject.destroy(); + + this.previews.delete(tab); + AeroPeek.removePreview(preview); + }, + + get enabled () { + return this._enabled; + }, + + set enabled (enable) { + this._enabled = enable; + // Because making a tab visible requires that the tab it is next to be + // visible, it is far simpler to unset the 'next' tab and recreate them all + // at once. + for (let [tab, preview] of this.previews) { + preview.move(null); + preview.visible = enable; + } + this.updateTabOrdering(); + }, + + previewFromTab: function (tab) { + return this.previews.get(tab); + }, + + updateTabOrdering: function () { + let previews = this.previews; + let tabs = this.tabbrowser.tabs; + + // Previews are internally stored using a map, so we need to iterate the + // tabbrowser's array of tabs to retrieve previews in the same order. + let inorder = []; + for (let t of tabs) { + if (previews.has(t)) { + inorder.push(previews.get(t)); + } + } + + // Since the internal taskbar array has not yet been updated we must force + // on it the sorting order of our local array. To do so we must walk + // the local array backwards, otherwise we would send move requests in the + // wrong order. See bug 522610 for details. + for (let i = inorder.length - 1; i >= 0; i--) { + inorder[i].move(inorder[i + 1] || null); + } + }, + + //// nsIDOMEventListener + handleEvent: function (evt) { + let tab = evt.originalTarget; + switch (evt.type) { + case "TabOpen": + this.newTab(tab); + this.updateTabOrdering(); + break; + case "TabClose": + this.removeTab(tab); + this.updateTabOrdering(); + break; + case "TabSelect": + this.previewFromTab(tab).active = true; + break; + case "TabMove": + this.updateTabOrdering(); + break; + case "resize": + if (!AeroPeek._prefenabled) + return; + this.onResize(); + break; + } + }, + + // Set or reset a timer that will invalidate visible thumbnails soon. + setInvalidationTimer: function () { + if (!this.invalidateTimer) { + this.invalidateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + this.invalidateTimer.cancel(); + + // Delay 1 second before invalidating. + this.invalidateTimer.initWithCallback(() => { + // Invalidate every preview. Note the internal implementation of + // invalidate ignores thumbnails that aren't visible. + this.previews.forEach(function (aPreview) { + let controller = aPreview.controller.wrappedJSObject; + if (!controller.testCacheBrowserDims()) { + controller.cacheBrowserDims(); + aPreview.invalidate(); + } + }); + }, 1000, Ci.nsITimer.TYPE_ONE_SHOT); + }, + + onResize: function () { + // Specific to a window. + + // Call invalidate on each tab thumbnail so that Windows will request an + // updated image. However don't do this repeatedly across multiple resize + // events triggered during window border drags. + if (this.testCacheDims()) { + return; + } + + // Update the window dims on our TabWindow object. + this.cacheDims(); + + // Invalidate soon. + this.setInvalidationTimer(); + }, + + invalidateTabPreview: function(aBrowser) { + for (let [tab, preview] of this.previews) { + if (aBrowser == tab.linkedBrowser) { + preview.invalidate(); + break; + } + } + }, + + //// Browser progress listener + + onLocationChange: function (aBrowser) { + // I'm not sure we need this, onStateChange does a really good job + // of picking up page changes. + // this.invalidateTabPreview(aBrowser); + }, + + onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + this.invalidateTabPreview(aBrowser); + } + }, + + directRequestProtocols: new Set([ + "file", "chrome", "resource", "about" + ]), + + onLinkIconAvailable: function (aBrowser, aIconURL) { + + let requestURL = null; + if (aIconURL) { + let shouldRequestFaviconURL = true; + try { + let urlObject = NetUtil.newURI(aIconURL); + shouldRequestFaviconURL = + !this.directRequestProtocols.has(urlObject.scheme); + } catch (ex) {} + + requestURL = shouldRequestFaviconURL ? + "moz-anno:favicon:" + aIconURL : + aIconURL; + } + + let isDefaultFavicon = !requestURL; + + getFaviconAsImage( + requestURL, + PrivateBrowsingUtils.isWindowPrivate(this.win), + img => { + let index = this.tabbrowser.browsers.indexOf(aBrowser); + // Only add it if we've found the index and the URI is still the same. + // The tab could have closed, and there's no guarantee the icons + // will have finished fetching 'in order'. + if (index != -1) { + let tab = this.tabbrowser.tabs[index]; + + let preview = this.previews.get(tab); + + if (tab.getAttribute("image") == aIconURL || + (!preview.icon && isDefaultFavicon)) { + preview.icon = img; + } + } + } + ); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// AeroPeek + +// This object acts as global storage and external interface for this feature. +// It maintains the values of the prefs. +var AeroPeek = { + available: false, + // Does the pref say we're enabled? + _prefenabled: false, + + _enabled: true, + + initialized: false, + + // nsITaskbarTabPreview array + previews: [], + + // TabWindow array + windows: [], + + // nsIWinTaskbar service + taskbar: null, + + // Maximum number of previews. + maxpreviews: 20, + + // Length of time in seconds that previews are cached. + cacheLifespan: 20, + + initialize: function () { + if (!(WINTASKBAR_CONTRACTID in Cc)) + return; + this.taskbar = Cc[WINTASKBAR_CONTRACTID] + .getService(Ci.nsIWinTaskbar); + this.available = this.taskbar.available; + if (!this.available) + return; + + this.prefs.addObserver(TOGGLE_PREF_NAME, this, true); + this.enabled = this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME); + this.initialized = true; + }, + + destroy: function destroy() { + this._enabled = false; + + this.prefs.removeObserver(TOGGLE_PREF_NAME, this); + this.prefs.removeObserver(DISABLE_THRESHOLD_PREF_NAME, this); + this.prefs.removeObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this); + + if (this.cacheTimer) + this.cacheTimer.cancel(); + }, + + get enabled() { + return this._enabled; + }, + + set enabled(enable) { + if (this._enabled == enable) + return; + + this._enabled = enable; + + this.windows.forEach(function (win) { + win.enabled = enable; + }); + }, + get _prefenabled() { + return this.__prefenabled; + }, + + set _prefenabled(enable) { + if (enable == this.__prefenabled) { + return; + } + this.__prefenabled = enable; + + if (enable) { + this.enable(); + } else { + this.disable(); + } + }, + + _observersAdded: false, + + enable() { + if (!this._observersAdded) { + this.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, true); + this.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, true); + PlacesUtils.history.addObserver(this, true); + this._observersAdded = true; + } + + this.cacheLifespan = this.prefs.getIntPref(CACHE_EXPIRATION_TIME_PREF_NAME); + + this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME); + + // If the user toggled us on/off while the browser was already up + // (rather than this code running on startup because the pref was + // already set to true), we must initialize previews for open windows. + if (this.initialized) { + let browserWindows = Services.wm.getEnumerator("navigator:browser"); + while (browserWindows.hasMoreElements()) { + let win = browserWindows.getNext(); + if (!win.closed) { + this.onOpenWindow(win); + } + } + } + }, + + disable() { + while (this.windows.length) { + // We can't call onCloseWindow here because it'll bail if we're not + // enabled. + let tabWinObject = this.windows[0]; + tabWinObject.destroy(); // This will remove us from the array. + delete tabWinObject.win.gTaskbarTabGroup; // Tidy up the window. + } + }, + + addPreview: function (preview) { + this.previews.push(preview); + this.checkPreviewCount(); + }, + + removePreview: function (preview) { + let idx = this.previews.indexOf(preview); + this.previews.splice(idx, 1); + this.checkPreviewCount(); + }, + + checkPreviewCount: function () { + if (!this._prefenabled) { + return; + } + this.enabled = this.previews.length <= this.maxpreviews; + }, + + onOpenWindow: function (win) { + // This occurs when the taskbar service is not available (xp, vista). + if (!this.available || !this._prefenabled) + return; + + win.gTaskbarTabGroup = new TabWindow(win); + }, + + onCloseWindow: function (win) { + // This occurs when the taskbar service is not available (xp, vista). + if (!this.available || !this._prefenabled) + return; + + win.gTaskbarTabGroup.destroy(); + delete win.gTaskbarTabGroup; + + if (this.windows.length == 0) + this.destroy(); + }, + + resetCacheTimer: function () { + this.cacheTimer.cancel(); + this.cacheTimer.init(this, 1000 * this.cacheLifespan, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + //// nsIObserver + observe: function (aSubject, aTopic, aData) { + + if (aTopic == "nsPref:changed" && aData == TOGGLE_PREF_NAME) { + this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME); + } + + // Bail out. Nothing more to do here if previews are disabled. + if (!this._prefenabled) { + return; + } + + switch (aTopic) { + case "nsPref:changed": + if (aData == CACHE_EXPIRATION_TIME_PREF_NAME) + break; + + if (aData == DISABLE_THRESHOLD_PREF_NAME) + this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME); + // Might need to enable/disable ourselves. + this.checkPreviewCount(); + break; + case "timer-callback": + this.previews.forEach(function (preview) { + let controller = preview.controller.wrappedJSObject; + controller.resetCanvasPreview(); + }); + break; + } + }, + + // nsINavHistoryObserver implementation + onBeginUpdateBatch() {}, + onEndUpdateBatch() {}, + onVisit() {}, + onTitleChanged() {}, + onFrecencyChanged() {}, + onManyFrecenciesChanged() {}, + onDeleteURI() {}, + onClearHistory() {}, + onDeleteVisits() {}, + onPageChanged(uri, changedConst, newValue) { + if (this.enabled && changedConst == Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) { + for (let win of this.windows) { + for (let [tab, preview] of win.previews) { + if (tab.getAttribute("image") == newValue) { + win.onLinkIconAvailable(tab.linkedBrowser, newValue); + } + } + } + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, + Ci.nsINavHistoryObserver, + Ci.nsIObserver]), +}; + +XPCOMUtils.defineLazyGetter(AeroPeek, "cacheTimer", () => + Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer) +); + +XPCOMUtils.defineLazyServiceGetter(AeroPeek, "prefs", + "@mozilla.org/preferences-service;1", + "nsIPrefBranch"); + +AeroPeek.initialize(); diff --git a/comm/suite/modules/moz.build b/comm/suite/modules/moz.build new file mode 100644 index 0000000000..830c03e703 --- /dev/null +++ b/comm/suite/modules/moz.build @@ -0,0 +1,20 @@ +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +EXTRA_JS_MODULES += [ + "Feeds.jsm", + "OfflineAppCacheHelper.jsm", + "OpenInTabsUtils.jsm", + "PermissionUI.jsm", + "RecentWindow.jsm", + "SitePermissions.jsm", + "ThemeVariableMap.jsm", + "WindowsPreviewPerTab.jsm", +] + +if CONFIG["OS_ARCH"] == "WINNT": + EXTRA_JS_MODULES += ["WindowsJumpLists.jsm"] diff --git a/comm/suite/modules/test/unit/head.js b/comm/suite/modules/test/unit/head.js new file mode 100644 index 0000000000..c947a4533c --- /dev/null +++ b/comm/suite/modules/test/unit/head.js @@ -0,0 +1,135 @@ +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +ChromeUtils.defineModuleGetter(this, "Promise", + "resource://gre/modules/commonjs/sdk/core/promise.js"); + +// Need to explicitly load profile for Places +do_get_profile(); + +/** + * Waits until a new cache entry has been opened + * + * @return {Promise} + * @resolves When the new cache entry has been opened. + * @rejects Never. + * + */ +function promiseOpenCacheEntry(aKey, aAccessMode, aCacheSession) +{ + let deferred = Promise.defer(); + + let cacheListener = { + onCacheEntryAvailable: function (entry, access, status) { + deferred.resolve(entry); + }, + + onCacheEntryDoomed: function (status) { + } + }; + + aCacheSession.asyncOpenCacheEntry(aKey, aAccessMode, cacheListener); + + return deferred.promise; +} + +/** + * Waits for all pending async statements on the default connection. + * + * @return {Promise} + * @resolves When all pending async statements finished. + * @rejects Never. + * + * @note The result is achieved by asynchronously executing a query requiring + * a write lock. Since all statements on the same connection are + * serialized, the end of this write operation means that all writes are + * complete. Note that WAL makes so that writers don't block readers, but + * this is a problem only across different connections. + */ +function promiseAsyncUpdates() +{ + let deferred = Promise.defer(); + + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; + let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); + begin.executeAsync(); + begin.finalize(); + + let commit = db.createAsyncStatement("COMMIT"); + commit.executeAsync({ + handleResult: function() {}, + handleError: function() {}, + handleCompletion: function(aReason) + { + deferred.resolve(); + } + }); + commit.finalize(); + + return deferred.promise; +} + +/** + * Asynchronously adds visits to a page. + * + * @param aPlaceInfo + * Can be an nsIURI, in such a case a single LINK visit will be added. + * Otherwise can be an object describing the visit to add, or an array + * of these objects: + * { uri: nsIURI of the page, + * transition: one of the TRANSITION_* from nsINavHistoryService, + * [optional] title: title of the page, + * [optional] visitDate: visit date in microseconds from the epoch + * [optional] referrer: nsIURI of the referrer for this visit + * } + * + * @return {Promise} + * @resolves When all visits have been added successfully. + * @rejects JavaScript exception. + */ +function promiseAddVisits(aPlaceInfo) +{ + let deferred = Promise.defer(); + let places = []; + if (aPlaceInfo instanceof Ci.nsIURI) { + places.push({ uri: aPlaceInfo }); + } + else if (Array.isArray(aPlaceInfo)) { + places = places.concat(aPlaceInfo); + } else { + places.push(aPlaceInfo) + } + + // Create mozIVisitInfo for each entry. + let now = Date.now(); + for (let i = 0; i < places.length; i++) { + if (!places[i].title) { + places[i].title = "test visit for " + places[i].uri.spec; + } + places[i].visits = [{ + transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK + : places[i].transition, + visitDate: places[i].visitDate || (now++) * 1000, + referrerURI: places[i].referrer + }]; + } + + PlacesUtils.asyncHistory.updatePlaces( + places, + { + handleError: function AAV_handleError(aResultCode, aPlaceInfo) { + let ex = new Components.Exception("Unexpected error in adding visits.", + aResultCode); + deferred.reject(ex); + }, + handleResult: function () {}, + handleCompletion: function UP_handleCompletion() { + deferred.resolve(); + } + } + ); + + return deferred.promise; +} diff --git a/comm/suite/modules/test/unit/test_browser_sanitizer.js b/comm/suite/modules/test/unit/test_browser_sanitizer.js new file mode 100644 index 0000000000..9e6ad39d08 --- /dev/null +++ b/comm/suite/modules/test/unit/test_browser_sanitizer.js @@ -0,0 +1,339 @@ +ChromeUtils.import("resource:///modules/Sanitizer.jsm", this); +ChromeUtils.defineModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); + +var sanTests = { + cache: { + desc: "Cache", + async setup() { + var entry = null; + this.cs = Services.cache.createSession("SanitizerTest", Ci.nsICache.STORE_ANYWHERE, true); + entry = await promiseOpenCacheEntry("http://santizer.test", Ci.nsICache.ACCESS_READ_WRITE, this.cs); + entry.setMetaDataElement("Foo", "Bar"); + entry.markValid(); + entry.close(); + }, + + async check(aShouldBeCleared) { + let entry = null; + entry = await promiseOpenCacheEntry("http://santizer.test", Ci.nsICache.ACCESS_READ, this.cs); + + if (entry) { + entry.close(); + } + + Assert.equal(!entry, aShouldBeCleared); + } + }, + + offlineApps: { + desc: "Offline app cache", + async setup() { + //XXX test offline DOMStorage + var entry = null; + this.cs = Services.cache.createSession("SanitizerTest", Ci.nsICache.STORE_OFFLINE, true); + entry = await promiseOpenCacheEntry("http://santizer.test", Ci.nsICache.ACCESS_READ_WRITE, this.cs); + entry.setMetaDataElement("Foo", "Bar"); + entry.markValid(); + entry.close(); + }, + + async check(aShouldBeCleared) { + var entry = null; + entry = await promiseOpenCacheEntry("http://santizer.test", Ci.nsICache.ACCESS_READ, this.cs); + if (entry) { + entry.close(); + } + + Assert.equal(!entry, aShouldBeCleared); + } + }, + + cookies: { + desc: "Cookie", + setup: function() { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + this.uri = Services.io.newURI("http://sanitizer.test/"); + this.cs = Cc["@mozilla.org/cookieService;1"] + .getService(Ci.nsICookieService); + this.cs.setCookieString(this.uri, null, "Sanitizer!", null); + }, + + check: function(aShouldBeCleared) { + if (aShouldBeCleared) + Assert.notEqual(this.cs.getCookieString(this.uri, null), "Sanitizer!"); + else + Assert.equal(this.cs.getCookieString(this.uri, null), "Sanitizer!"); + } + }, + + history: { + desc: "History", + async setup() { + var uri = Services.io.newURI("http://sanitizer.test/"); + await promiseAddVisits({ + uri: uri, + title: "Sanitizer!" + }); + }, + + check: function(aShouldBeCleared) { + var rv = false; + var history = Cc["@mozilla.org/browser/nav-history-service;1"] + .getService(Ci.nsINavHistoryService); + var options = history.getNewQueryOptions(); + var query = history.getNewQuery(); + query.searchTerms = "Sanitizer!"; + var results = history.executeQuery(query, options).root; + results.containerOpen = true; + for (var i = 0; i < results.childCount; i++) { + if (results.getChild(i).uri == "http://sanitizer.test/") { + rv = true; + break; + } + } + + // Close container after reading from it + results.containerOpen = false; + + Assert.equal(rv, !aShouldBeCleared); + } + }, + + urlbar: { + desc: "Location bar history", + setup: function() { + // Create urlbarhistory file first otherwise tests will fail. + var file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("urlbarhistory.sqlite"); + if (!file.exists()) { + var connection = Cc["@mozilla.org/storage/service;1"] + .getService(Ci.mozIStorageService) + .openDatabase(file); + connection.createTable("urlbarhistory", "url TEXT"); + connection.executeSimpleSQL( + "INSERT INTO urlbarhistory (url) VALUES ('Sanitizer')"); + connection.close(); + } + + // Open location dialog. + Services.prefs.setStringPref("general.open_location.last_url", "Sanitizer!"); + }, + + check: function(aShouldBeCleared) { + let locData; + try { + locData = Services.prefs.getStringPref("general.open_location.last_url"); + } catch(ex) {} + + Assert.equal(locData == "Sanitizer!", !aShouldBeCleared); + + var file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append("urlbarhistory.sqlite"); + + var connection = Cc["@mozilla.org/storage/service;1"] + .getService(Ci.mozIStorageService) + .openDatabase(file); + var urlbar = connection.tableExists("urlbarhistory"); + if (urlbar) { + var handle = connection.createStatement( + "SELECT url FROM urlbarhistory"); + if (handle.executeStep()) + urlbar = (handle.getString(0) == "Sanitizer"); + handle.reset(); + handle.finalize(); + } + connection.close(); + + Assert.equal(urlbar, !aShouldBeCleared); + } + }, + + formdata: { + desc: "Form history", + async setup() { + // Adds a form entry to history. + function promiseAddFormEntry(aName, aValue) { + return new Promise((resolve, reject) => + FormHistory.update({ op: "add", fieldname: aName, value: aValue }, + { handleError(error) { + reject(); + throw new Error("Error occurred updating form history: " + error); + }, + handleCompletion(reason) { + resolve(); + } + }) + ) + } + await promiseAddFormEntry("Sanitizer", "Foo"); + }, + async check(aShouldBeCleared) { + // Check if a form name exists. + function formNameExists(aName) { + return new Promise((resolve, reject) => { + let count = 0; + FormHistory.count({ fieldname: aName }, + { handleResult: result => count = result, + handleError(error) { + reject(error); + throw new Error("Error occurred searching form history: " + error); + }, + handleCompletion(reason) { + if (!reason) { + resolve(count); + } + } + }); + }); + } + + // Checking for Sanitizer form history entry creation. + let exists = await formNameExists("Sanitizer"); + Assert.equal(exists, !aShouldBeCleared); + } + }, + + downloads: { + desc: "Download", + setup: function() { + var uri = Services.io.newURI("http://sanitizer.test/"); + var file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append("sanitizer.file"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8)); + var dest = Services.io.newFileURI(file); + + this.dm = Cc["@mozilla.org/download-manager;1"] + .getService(Ci.nsIDownloadManager); + + const nsIWBP = Ci.nsIWebBrowserPersist; + var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] + .createInstance(nsIWBP); + persist.persistFlags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES | + nsIWBP.PERSIST_FLAGS_BYPASS_CACHE | + nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; + + this.dl = this.dm.addDownload(this.dm.DOWNLOAD_CANCELED, uri, dest, + "Sanitizer!", null, + Math.round(Date.now() * 1000), null, + persist, false); + + // Stupid DM... + this.dm.cancelDownload(this.dl.id); + }, + + check: function(aShouldBeCleared) { + var dl = null; + try { + dl = this.dm.getDownload(this.dl.id); + } catch(ex) {} + + if (aShouldBeCleared) + Assert.equal(!dl, aShouldBeCleared) + else + Assert.equal(dl.displayName, "Sanitizer!"); + } + }, + + passwords: { + desc: "Login manager", + setup: function() { + this.pm = Cc["@mozilla.org/login-manager;1"] + .getService(Ci.nsILoginManager); + var info = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + var login = new info("http://sanitizer.test", null, "Rick Astley Fan Club", + "dolske", "iliketurtles1", "", ""); + this.pm.addLogin(login); + }, + + check: function(aShouldBeCleared) { + let rv = false; + let logins = this.pm.findLogins({}, "http://sanitizer.test", null, "Rick Astley Fan Club"); + for (var i = 0; i < logins.length; i++) { + if (logins[i].username == "dolske") { + rv = true; + break; + } + } + + Assert.equal(rv, !aShouldBeCleared); + } + }, + + sessions: { + desc: "HTTP auth session", + setup: function() { + this.authMgr = Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager); + + this.authMgr.setAuthIdentity("http", "sanitizer.test", 80, "basic", "Sanitizer", + "", "Foo", "fooo", "foo12"); + }, + + check: function(aShouldBeCleared) { + var domain = {}; + var user = {}; + var password = {}; + + try { + this.authMgr.getAuthIdentity("http", "sanitizer.test", 80, "basic", "Sanitizer", + "", domain, user, password); + } catch(ex) {} + + Assert.equal(domain.value == "Foo", !aShouldBeCleared); + } + } +} + +async function fullSanitize() { + info("Now doing a full sanitize run"); + var prefs = Services.prefs.getBranch("privacy.clearOnShutdown."); + + Services.prefs.setBoolPref("privacy.sanitize.promptOnSanitize", false); + + for (var testName in sanTests) { + var test = sanTests[testName]; + await test.setup(); + prefs.setBoolPref(testName, true); + } + + Sanitizer.sanitize(); + + for (var testName in sanTests) { + var test = sanTests[testName]; + await test.check(true); + info(test.desc + " data cleared by full sanitize"); + try { + prefs.clearUserPref(testName); + } catch (ex) {} + } + + try { + Services.prefs.clearUserPref("privacy.sanitize.promptOnSanitize"); + } catch(ex) {} +} + +function run_test() +{ + run_next_test(); +} + +add_task(async function test_browser_sanitizer() +{ + for (var testName in sanTests) { + let test = sanTests[testName]; + dump("\nExecuting test: " + testName + "\n" + "*** " + test.desc + "\n"); + await test.setup(); + await test.check(false); + + Sanitizer.items[testName].clear(); + info(test.desc + " data cleared"); + + await test.check(true); + } +}); + +add_task(fullSanitize); diff --git a/comm/suite/modules/test/unit/xpcshell.ini b/comm/suite/modules/test/unit/xpcshell.ini new file mode 100644 index 0000000000..4f6c1dad63 --- /dev/null +++ b/comm/suite/modules/test/unit/xpcshell.ini @@ -0,0 +1,6 @@ +[DEFAULT] +head = head.js +tail = +run-sequentially = Avoid bustage. + +[test_browser_sanitizer.js] |