summaryrefslogtreecommitdiffstats
path: root/comm/suite/modules
diff options
context:
space:
mode:
Diffstat (limited to 'comm/suite/modules')
-rw-r--r--comm/suite/modules/Feeds.jsm50
-rw-r--r--comm/suite/modules/OfflineAppCacheHelper.jsm16
-rw-r--r--comm/suite/modules/OpenInTabsUtils.jsm83
-rw-r--r--comm/suite/modules/PermissionUI.jsm612
-rw-r--r--comm/suite/modules/RecentWindow.jsm66
-rw-r--r--comm/suite/modules/SitePermissions.jsm796
-rw-r--r--comm/suite/modules/ThemeVariableMap.jsm29
-rw-r--r--comm/suite/modules/WindowsJumpLists.jsm604
-rw-r--r--comm/suite/modules/WindowsPreviewPerTab.jsm872
-rw-r--r--comm/suite/modules/moz.build20
-rw-r--r--comm/suite/modules/test/unit/head.js135
-rw-r--r--comm/suite/modules/test/unit/test_browser_sanitizer.js339
-rw-r--r--comm/suite/modules/test/unit/xpcshell.ini6
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]