summaryrefslogtreecommitdiff
path: root/components/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'components/extensions')
-rw-r--r--components/extensions/content/OpenH264-license.txt59
-rw-r--r--components/extensions/content/about.js97
-rw-r--r--components/extensions/content/about.xul57
-rw-r--r--components/extensions/content/blocklist.css11
-rw-r--r--components/extensions/content/blocklist.js72
-rw-r--r--components/extensions/content/blocklist.xml58
-rw-r--r--components/extensions/content/blocklist.xul46
-rw-r--r--components/extensions/content/eula.js21
-rw-r--r--components/extensions/content/eula.xul35
-rw-r--r--components/extensions/content/extensions.css271
-rw-r--r--components/extensions/content/extensions.js3568
-rw-r--r--components/extensions/content/extensions.xml2084
-rw-r--r--components/extensions/content/extensions.xul685
-rw-r--r--components/extensions/content/gmpPrefs.xul8
-rw-r--r--components/extensions/content/list.js165
-rw-r--r--components/extensions/content/list.xul44
-rw-r--r--components/extensions/content/newaddon.js129
-rw-r--r--components/extensions/content/newaddon.xul66
-rw-r--r--components/extensions/content/pluginPrefs.xul20
-rw-r--r--components/extensions/content/selectAddons.css22
-rw-r--r--components/extensions/content/selectAddons.js347
-rw-r--r--components/extensions/content/selectAddons.xml235
-rw-r--r--components/extensions/content/selectAddons.xul124
-rw-r--r--components/extensions/content/setting.xml508
-rw-r--r--components/extensions/content/update.js691
-rw-r--r--components/extensions/content/update.xul180
-rw-r--r--components/extensions/content/updateinfo.xsl41
-rw-r--r--components/extensions/content/xpinstallConfirm.css8
-rw-r--r--components/extensions/content/xpinstallConfirm.js192
-rw-r--r--components/extensions/content/xpinstallConfirm.xul37
-rw-r--r--components/extensions/content/xpinstallItem.xml51
-rw-r--r--components/extensions/extensions.manifest16
-rw-r--r--components/extensions/jar.mn37
-rw-r--r--components/extensions/locale/about.dtd9
-rw-r--r--components/extensions/locale/blocklist.dtd17
-rw-r--r--components/extensions/locale/extensions.dtd230
-rw-r--r--components/extensions/locale/extensions.properties177
-rw-r--r--components/extensions/locale/newaddon.dtd15
-rw-r--r--components/extensions/locale/newaddon.properties10
-rw-r--r--components/extensions/locale/selectAddons.dtd49
-rw-r--r--components/extensions/locale/selectAddons.properties21
-rw-r--r--components/extensions/locale/update.dtd65
-rw-r--r--components/extensions/locale/update.properties21
-rw-r--r--components/extensions/locale/xpinstallConfirm.dtd13
-rw-r--r--components/extensions/locale/xpinstallConfirm.properties16
-rw-r--r--components/extensions/moz.build66
-rw-r--r--components/extensions/public/amIAddonManager.idl29
-rw-r--r--components/extensions/public/amIAddonPathService.idl37
-rw-r--r--components/extensions/public/amIWebInstallListener.idl134
-rw-r--r--components/extensions/public/amIWebInstaller.idl82
-rw-r--r--components/extensions/src/AddonLogging.jsm187
-rw-r--r--components/extensions/src/AddonManager.jsm2902
-rw-r--r--components/extensions/src/AddonPathService.cpp243
-rw-r--r--components/extensions/src/AddonPathService.h55
-rw-r--r--components/extensions/src/AddonRepository.jsm2005
-rw-r--r--components/extensions/src/AddonRepository_SQLiteMigrator.jsm522
-rw-r--r--components/extensions/src/AddonUpdateChecker.jsm955
-rw-r--r--components/extensions/src/ChromeManifestParser.jsm157
-rw-r--r--components/extensions/src/Content.js38
-rw-r--r--components/extensions/src/DeferredSave.jsm270
-rw-r--r--components/extensions/src/GMPInstallManager.jsm917
-rw-r--r--components/extensions/src/GMPProvider.jsm605
-rw-r--r--components/extensions/src/GMPUtils.jsm187
-rw-r--r--components/extensions/src/LightweightThemeConsumer.jsm164
-rw-r--r--components/extensions/src/LightweightThemeImageOptimizer.jsm199
-rw-r--r--components/extensions/src/LightweightThemeManager.jsm801
-rw-r--r--components/extensions/src/PluginProvider.jsm595
-rw-r--r--components/extensions/src/ProductAddonChecker.jsm464
-rw-r--r--components/extensions/src/SpellCheckDictionaryBootstrap.js17
-rw-r--r--components/extensions/src/XPIProvider.jsm7787
-rw-r--r--components/extensions/src/XPIProviderUtils.js1430
-rw-r--r--components/extensions/src/addonManager.js200
-rw-r--r--components/extensions/src/amContentHandler.js100
-rw-r--r--components/extensions/src/amInstallTrigger.js230
-rw-r--r--components/extensions/src/amWebInstallListener.js338
75 files changed, 32344 insertions, 0 deletions
diff --git a/components/extensions/content/OpenH264-license.txt b/components/extensions/content/OpenH264-license.txt
new file mode 100644
index 000000000..ad37989b8
--- /dev/null
+++ b/components/extensions/content/OpenH264-license.txt
@@ -0,0 +1,59 @@
+-------------------------------------------------------
+About The Cisco-Provided Binary of OpenH264 Video Codec
+-------------------------------------------------------
+
+Cisco provides this program under the terms of the BSD license.
+
+Additionally, this binary is licensed under Cisco’s AVC/H.264 Patent Portfolio License from MPEG LA, at no cost to you, provided that the requirements and conditions shown below in the AVC/H.264 Patent Portfolio sections are met.
+
+As with all AVC/H.264 codecs, you may also obtain your own patent license from MPEG LA or from the individual patent owners, or proceed at your own risk. Your rights from Cisco under the BSD license are not affected by this choice.
+
+For more information on the OpenH264 binary licensing, please see the OpenH264 FAQ found at http://www.openh264.org/faq.html#binary
+
+A corresponding source code to this binary program is available under the same BSD terms, which can be found at http://www.openh264.org
+
+-----------
+BSD License
+-----------
+
+Copyright © 2014 Cisco Systems, Inc.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-----------------------------------------
+AVC/H.264 Patent Portfolio License Notice
+-----------------------------------------
+
+The binary form of this Software is distributed by Cisco under the AVC/H.264 Patent Portfolio License from MPEG LA, and is subject to the following requirements, which may or may not be applicable to your use of this software:
+
+THIS PRODUCT IS LICENSED UNDER THE AVC PATENT PORTFOLIO LICENSE FOR THE PERSONAL USE OF A CONSUMER OR OTHER USES IN WHICH IT DOES NOT RECEIVE REMUNERATION TO (i) ENCODE VIDEO IN COMPLIANCE WITH THE AVC STANDARD (“AVC VIDEO”) AND/OR (ii) DECODE AVC VIDEO THAT WAS ENCODED BY A CONSUMER ENGAGED IN A PERSONAL ACTIVITY AND/OR WAS OBTAINED FROM A VIDEO PROVIDER LICENSED TO PROVIDE AVC VIDEO. NO LICENSE IS GRANTED OR SHALL BE IMPLIED FOR ANY OTHER USE. ADDITIONAL INFORMATION MAY BE OBTAINED FROM MPEG LA, L.L.C. SEE HTTP://WWW.MPEGLA.COM
+
+Accordingly, please be advised that content providers and broadcasters using AVC/H.264 in their service may be required to obtain a separate use license from MPEG LA, referred to as "(b) sublicenses" in the SUMMARY OF AVC/H.264 LICENSE TERMS from MPEG LA found at http://www.openh264.org/mpegla
+
+---------------------------------------------
+AVC/H.264 Patent Portfolio License Conditions
+---------------------------------------------
+
+In addition, the Cisco-provided binary of this Software is licensed under Cisco's license from MPEG LA only if the following conditions are met:
+
+1. The Cisco-provided binary is separately downloaded to an end user’s device, and not integrated into or combined with third party software prior to being downloaded to the end user’s device;
+
+2. The end user must have the ability to control (e.g., to enable, disable, or re-enable) the use of the Cisco-provided binary;
+
+3. Third party software, in the location where end users can control the use of the Cisco-provided binary, must display the following text:
+
+ "OpenH264 Video Codec provided by Cisco Systems, Inc."
+
+4. Any third-party software that makes use of the Cisco-provided binary must reproduce all of the above text, as well as this last condition, in the EULA and/or in another location where licensing information is to be presented to the end user.
+
+
+
+ v1.0
diff --git a/components/extensions/content/about.js b/components/extensions/content/about.js
new file mode 100644
index 000000000..49ca4acc1
--- /dev/null
+++ b/components/extensions/content/about.js
@@ -0,0 +1,97 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+"use strict";
+
+function init() {
+ var addon = window.arguments[0];
+ var extensionsStrings = document.getElementById("extensionsStrings");
+
+ document.documentElement.setAttribute("addontype", addon.type);
+
+ if (addon.iconURL) {
+ var extensionIcon = document.getElementById("extensionIcon");
+ extensionIcon.src = addon.iconURL;
+ }
+
+ document.title = extensionsStrings.getFormattedString("aboutWindowTitle", [addon.name]);
+ var extensionName = document.getElementById("extensionName");
+ extensionName.textContent = addon.name;
+
+ var extensionVersion = document.getElementById("extensionVersion");
+ if (addon.version)
+ extensionVersion.setAttribute("value", extensionsStrings.getFormattedString("aboutWindowVersionString", [addon.version]));
+ else
+ extensionVersion.hidden = true;
+
+ var extensionDescription = document.getElementById("extensionDescription");
+ if (addon.description)
+ extensionDescription.textContent = addon.description;
+ else
+ extensionDescription.hidden = true;
+
+ var numDetails = 0;
+
+ var extensionCreator = document.getElementById("extensionCreator");
+ if (addon.creator) {
+ extensionCreator.setAttribute("value", addon.creator);
+ numDetails++;
+ } else {
+ extensionCreator.hidden = true;
+ var extensionCreatorLabel = document.getElementById("extensionCreatorLabel");
+ extensionCreatorLabel.hidden = true;
+ }
+
+ var extensionHomepage = document.getElementById("extensionHomepage");
+ var homepageURL = addon.homepageURL;
+ if (homepageURL) {
+ extensionHomepage.setAttribute("homepageURL", homepageURL);
+ extensionHomepage.setAttribute("tooltiptext", homepageURL);
+ numDetails++;
+ } else {
+ extensionHomepage.hidden = true;
+ }
+
+ numDetails += appendToList("extensionDevelopers", "developersBox", addon.developers);
+ numDetails += appendToList("extensionTranslators", "translatorsBox", addon.translators);
+ numDetails += appendToList("extensionContributors", "contributorsBox", addon.contributors);
+
+ if (numDetails == 0) {
+ var groove = document.getElementById("groove");
+ groove.hidden = true;
+ var extensionDetailsBox = document.getElementById("extensionDetailsBox");
+ extensionDetailsBox.hidden = true;
+ }
+
+ var acceptButton = document.documentElement.getButton("accept");
+ acceptButton.label = extensionsStrings.getString("aboutWindowCloseButton");
+
+ setTimeout(sizeToContent, 0);
+}
+
+function appendToList(aHeaderId, aNodeId, aItems) {
+ var header = document.getElementById(aHeaderId);
+ var node = document.getElementById(aNodeId);
+
+ if (!aItems || aItems.length == 0) {
+ header.hidden = true;
+ return 0;
+ }
+
+ for (let currentItem of aItems) {
+ var label = document.createElement("label");
+ label.textContent = currentItem;
+ label.setAttribute("class", "contributor");
+ node.appendChild(label);
+ }
+
+ return aItems.length;
+}
+
+function loadHomepage(aEvent) {
+ window.close();
+ openURL(aEvent.target.getAttribute("homepageURL"));
+}
diff --git a/components/extensions/content/about.xul b/components/extensions/content/about.xul
new file mode 100644
index 000000000..6effcf37a
--- /dev/null
+++ b/components/extensions/content/about.xul
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/extensions/about.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://mozapps/locale/extensions/about.dtd">
+
+<dialog id="genericAbout"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="init();"
+ buttons="accept"
+ buttoniconaccept="close"
+ onaccept="close();">
+
+ <script type="application/javascript" src="chrome://mozapps/content/extensions/about.js"/>
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+
+ <stringbundleset id="aboutSet">
+ <stringbundle id="extensionsStrings" src="chrome://mozapps/locale/extensions/extensions.properties"/>
+ </stringbundleset>
+
+ <vbox id="clientBox" flex="1">
+ <hbox class="basic-info">
+ <vbox pack="center">
+ <image id="extensionIcon"/>
+ </vbox>
+ <vbox flex="1">
+ <label id="extensionName"/>
+ <label id="extensionVersion" crop="end"/>
+ </vbox>
+ </hbox>
+ <description id="extensionDescription" class="boxIndent"/>
+
+ <separator id="groove" class="groove"/>
+
+ <vbox id="extensionDetailsBox" flex="1">
+ <label id="extensionCreatorLabel" class="sectionTitle">&creator.label;</label>
+ <hbox id="creatorBox" class="boxIndent">
+ <label id="extensionCreator" flex="1" crop="end"/>
+ <label id="extensionHomepage" onclick="if (event.button == 0) { loadHomepage(event); }"
+ class="text-link" value="&homepage.label;"/>
+ </hbox>
+
+ <label id="extensionDevelopers" class="sectionTitle">&developers.label;</label>
+ <vbox flex="1" id="developersBox" class="boxIndent"/>
+ <label id="extensionTranslators" class="sectionTitle">&translators.label;</label>
+ <vbox flex="1" id="translatorsBox" class="boxIndent"/>
+ <label id="extensionContributors" class="sectionTitle">&contributors.label;</label>
+ <vbox flex="1" id="contributorsBox" class="boxIndent"/>
+ </vbox>
+ </vbox>
+
+</dialog>
diff --git a/components/extensions/content/blocklist.css b/components/extensions/content/blocklist.css
new file mode 100644
index 000000000..cb48005a2
--- /dev/null
+++ b/components/extensions/content/blocklist.css
@@ -0,0 +1,11 @@
+/* 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/. */
+
+.hardBlockedAddon {
+ -moz-binding: url("chrome://mozapps/content/extensions/blocklist.xml#hardblockedaddon");
+}
+
+.softBlockedAddon {
+ -moz-binding: url("chrome://mozapps/content/extensions/blocklist.xml#softblockedaddon");
+}
diff --git a/components/extensions/content/blocklist.js b/components/extensions/content/blocklist.js
new file mode 100644
index 000000000..6d524e6ee
--- /dev/null
+++ b/components/extensions/content/blocklist.js
@@ -0,0 +1,72 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var gArgs;
+
+function init() {
+ var hasHardBlocks = false;
+ var hasSoftBlocks = false;
+ gArgs = window.arguments[0].wrappedJSObject;
+
+ // NOTE: We use strings from the "updates.properties" bundleset to change the
+ // text on the "Cancel" button to "Restart Later". (bug 523784)
+ let bundle = Services.strings.
+ createBundle("chrome://mozapps/locale/update/updates.properties");
+ let cancelButton = document.documentElement.getButton("cancel");
+ cancelButton.setAttribute("label", bundle.GetStringFromName("restartLaterButton"));
+ cancelButton.setAttribute("accesskey",
+ bundle.GetStringFromName("restartLaterButton.accesskey"));
+
+ var richlist = document.getElementById("addonList");
+ var list = gArgs.list;
+ list.sort(function(a, b) { return String.localeCompare(a.name, b.name); });
+ for (let listItem of list) {
+ let item = document.createElement("richlistitem");
+ item.setAttribute("name", listItem.name);
+ item.setAttribute("version", listItem.version);
+ item.setAttribute("icon", listItem.icon);
+ if (listItem.blocked) {
+ item.setAttribute("class", "hardBlockedAddon");
+ hasHardBlocks = true;
+ }
+ else {
+ item.setAttribute("class", "softBlockedAddon");
+ hasSoftBlocks = true;
+ }
+ richlist.appendChild(item);
+ }
+
+ if (hasHardBlocks && hasSoftBlocks)
+ document.getElementById("bothMessage").hidden = false;
+ else if (hasHardBlocks)
+ document.getElementById("hardBlockMessage").hidden = false;
+ else
+ document.getElementById("softBlockMessage").hidden = false;
+
+ var link = document.getElementById("moreInfo");
+ if (list.length == 1 && list[0].url) {
+ link.setAttribute("href", list[0].url);
+ }
+ else {
+ var url = Services.urlFormatter.formatURLPref("extensions.blocklist.detailsURL");
+ link.setAttribute("href", url);
+ }
+}
+
+function finish(shouldRestartNow) {
+ gArgs.restart = shouldRestartNow;
+ var list = gArgs.list;
+ var items = document.getElementById("addonList").childNodes;
+ for (let i = 0; i < list.length; i++) {
+ if (!list[i].blocked)
+ list[i].disable = items[i].checked;
+ }
+ return true;
+}
diff --git a/components/extensions/content/blocklist.xml b/components/extensions/content/blocklist.xml
new file mode 100644
index 000000000..74474392f
--- /dev/null
+++ b/components/extensions/content/blocklist.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE bindings [
+ <!ENTITY % blocklistDTD SYSTEM "chrome://mozapps/locale/extensions/blocklist.dtd" >
+ %blocklistDTD;
+]>
+
+<bindings id="blocklistBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="hardblockedaddon">
+ <content align="start">
+ <xul:image xbl:inherits="src=icon"/>
+ <xul:vbox flex="1">
+ <xul:hbox class="addon-name-version">
+ <xul:label class="addonName" crop="end" xbl:inherits="value=name"/>
+ <xul:label class="addonVersion" xbl:inherits="value=version"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:spacer flex="1"/>
+ <xul:label class="blockedLabel" value="&blocklist.blocked.label;"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ </binding>
+
+ <binding id="softblockedaddon">
+ <content align="start">
+ <xul:image xbl:inherits="src=icon"/>
+ <xul:vbox flex="1">
+ <xul:hbox class="addon-name-version">
+ <xul:label class="addonName" crop="end" xbl:inherits="value=name"/>
+ <xul:label class="addonVersion" xbl:inherits="value=version"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:spacer flex="1"/>
+ <xul:checkbox class="disableCheckbox" checked="true" label="&blocklist.checkbox.label;"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <field name="_checkbox">
+ document.getAnonymousElementByAttribute(this, "class", "disableCheckbox")
+ </field>
+ <property name="checked" readonly="true">
+ <getter>
+ return this._checkbox.checked;
+ </getter>
+ </property>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/components/extensions/content/blocklist.xul b/components/extensions/content/blocklist.xul
new file mode 100644
index 000000000..240d9e4e1
--- /dev/null
+++ b/components/extensions/content/blocklist.xul
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/"?>
+<?xml-stylesheet href="chrome://mozapps/skin/extensions/blocklist.css"?>
+<?xml-stylesheet href="chrome://mozapps/content/extensions/blocklist.css"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/blocklist.dtd">
+%extensionsDTD;
+]>
+
+<dialog windowtype="Addons:Blocklist" title="&blocklist.title;" align="stretch"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="init();" ondialogaccept="return finish(true)"
+ ondialogcancel="return finish(false)"
+ buttons="accept,cancel" style="&blocklist.style;"
+ buttonlabelaccept="&blocklist.accept.label;"
+ buttonaccesskeyaccept="&blocklist.accept.accesskey;">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript" src="chrome://mozapps/content/extensions/blocklist.js"/>
+
+ <hbox align="stretch" flex="1">
+ <vbox pack="start">
+ <image class="error-icon"/>
+ </vbox>
+ <vbox flex="1">
+ <label>&blocklist.summary;</label>
+ <separator class="thin"/>
+ <richlistbox id="addonList" flex="1"/>
+ <separator class="thin"/>
+ <description id="bothMessage" hidden="true" class="bold">&blocklist.softandhard;</description>
+ <description id="hardBlockMessage" hidden="true" class="bold">&blocklist.hardblocked;</description>
+ <description id="softBlockMessage" hidden="true" class="bold">&blocklist.softblocked;</description>
+ <hbox pack="start">
+ <label id="moreInfo" class="text-link" value="&blocklist.moreinfo;"/>
+ </hbox>
+ </vbox>
+ </hbox>
+</dialog>
diff --git a/components/extensions/content/eula.js b/components/extensions/content/eula.js
new file mode 100644
index 000000000..a05f7fe1c
--- /dev/null
+++ b/components/extensions/content/eula.js
@@ -0,0 +1,21 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+"use strict";
+
+function Startup() {
+ var bundle = document.getElementById("extensionsStrings");
+ var addon = window.arguments[0].addon;
+
+ document.documentElement.setAttribute("addontype", addon.type);
+
+ if (addon.iconURL)
+ document.getElementById("icon").src = addon.iconURL;
+
+ var label = document.createTextNode(bundle.getFormattedString("eulaHeader", [addon.name]));
+ document.getElementById("heading").appendChild(label);
+ document.getElementById("eula").value = addon.eula;
+}
diff --git a/components/extensions/content/eula.xul b/components/extensions/content/eula.xul
new file mode 100644
index 000000000..10e657951
--- /dev/null
+++ b/components/extensions/content/eula.xul
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/extensions/eula.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/extensions.dtd">
+%extensionsDTD;
+]>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&eula.title;" width="&eula.width;" height="&eula.height;"
+ buttons="accept,cancel" buttonlabelaccept="&eula.accept;"
+ ondialogaccept="window.arguments[0].accepted = true"
+ onload="Startup();">
+
+ <script type="application/javascript" src="chrome://mozapps/content/extensions/eula.js"/>
+
+ <stringbundleset id="extensionsSet">
+ <stringbundle id="extensionsStrings" src="chrome://mozapps/locale/extensions/extensions.properties"/>
+ </stringbundleset>
+
+ <hbox id="heading-container">
+ <image id="icon"/>
+ <label id="heading" flex="1"/>
+ </hbox>
+
+ <textbox id="eula" multiline="true" readonly="true" flex="1"/>
+</dialog>
diff --git a/components/extensions/content/extensions.css b/components/extensions/content/extensions.css
new file mode 100644
index 000000000..51828d544
--- /dev/null
+++ b/components/extensions/content/extensions.css
@@ -0,0 +1,271 @@
+/* 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/. */
+
+@namespace xhtml "http://www.w3.org/1999/xhtml";
+
+/* HTML link elements do weird things to the layout if they are not hidden */
+xhtml|link {
+ display: none;
+}
+
+#categories {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#categories-list");
+}
+
+.category {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#category");
+}
+
+.sort-controls {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#sorters");
+}
+
+.addon[status="installed"] {
+ -moz-box-orient: vertical;
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#addon-generic");
+}
+
+.addon[status="installing"] {
+ -moz-box-orient: vertical;
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#addon-installing");
+}
+
+.addon[pending="uninstall"] {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#addon-uninstalled");
+}
+
+.creator {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#creator-link");
+}
+
+.translators {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#translators-list");
+}
+
+.meta-rating {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#rating");
+}
+
+.download-progress, .download-progress[mode="undetermined"] {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#download-progress");
+}
+
+.install-status {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#install-status");
+}
+
+.detail-row {
+ -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#detail-row");
+}
+
+.text-list {
+ white-space: pre-line;
+ -moz-user-select: element;
+}
+
+setting, row[unsupported="true"] {
+ display: none;
+}
+
+setting[type="bool"] {
+ display: -moz-grid-line;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-bool");
+}
+
+setting[type="bool"][localized="true"] {
+ display: -moz-grid-line;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-localized-bool");
+}
+
+setting[type="bool"]:not([learnmore]) .preferences-learnmore {
+ visibility: collapse;
+}
+
+setting[type="boolint"] {
+ display: -moz-grid-line;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-boolint");
+}
+
+setting[type="integer"] {
+ display: -moz-grid-line;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-integer");
+}
+
+setting[type="integer"]:not([size]) textbox {
+ -moz-box-flex: 1;
+}
+
+setting[type="control"] {
+ display: -moz-grid-line;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-control");
+}
+
+setting[type="string"] {
+ display: -moz-grid-line;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-string");
+}
+
+setting[type="color"] {
+ display: -moz-grid-line;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-color");
+}
+
+setting[type="file"],
+setting[type="directory"] {
+ display: -moz-grid-line;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-path");
+}
+
+setting[type="radio"],
+setting[type="menulist"] {
+ display: -moz-grid-line;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-multi");
+}
+
+#addonitem-popup > menuitem[disabled="true"] {
+ display: none;
+}
+
+#addonitem-popup[addontype="theme"] > #menuitem_enableItem,
+#addonitem-popup[addontype="theme"] > #menuitem_disableItem,
+#addonitem-popup:not([addontype="theme"]) > #menuitem_enableTheme,
+#addonitem-popup:not([addontype="theme"]) > #menuitem_disableTheme {
+ display: none;
+}
+
+#header-searching:not([active]) {
+ visibility: hidden;
+}
+
+#search-list[local="false"] > .addon[remote="false"],
+#search-list[remote="false"] > .addon[remote="true"] {
+ visibility: collapse;
+}
+
+#detail-view {
+ overflow: auto;
+}
+
+.addon:not([notification="warning"]) .warning,
+.addon:not([notification="error"]) .error,
+.addon:not([notification="info"]) .info,
+.addon:not([pending]) .pending,
+.addon:not([upgrade="true"]) .update-postfix,
+.addon[active="true"] .disabled-postfix,
+.addon[pending="install"] .update-postfix,
+.addon[pending="install"] .disabled-postfix,
+#detail-view:not([notification="warning"]) .warning,
+#detail-view:not([notification="error"]) .error,
+#detail-view:not([notification="info"]) .info,
+#detail-view:not([pending]) .pending,
+#detail-view:not([upgrade="true"]) .update-postfix,
+#detail-view[active="true"] .disabled-postfix,
+#detail-view[loading] .detail-view-container,
+#detail-view:not([loading]) .alert-container,
+.detail-row:not([value]),
+#search-list[remote="false"] #search-allresults-link {
+ display: none;
+}
+
+#addons-page:not([warning]) #list-view > .global-warning-container {
+ display: none;
+}
+#addon-list .date-updated {
+ display: none;
+}
+
+.view-pane:not(#updates-view) .addon .relnotes-toggle,
+.view-pane:not(#updates-view) .addon .include-update,
+#updates-view:not([updatetype="available"]) .addon .include-update,
+#updates-view[updatetype="available"] .addon .update-available-notice {
+ display: none;
+}
+
+#addons-page:not([warning]) .global-warning,
+#addons-page:not([warning="safemode"]) .global-warning-safemode,
+#addons-page:not([warning="checkcompatibility"]) .global-warning-checkcompatibility,
+#addons-page:not([warning="updatesecurity"]) .global-warning-updatesecurity {
+ display: none;
+}
+
+/* Plugins aren't yet disabled by safemode (bug 342333),
+ so don't show that warning when viewing plugins. */
+#addons-page[warning="safemode"] .view-pane[type="plugin"] .global-warning-container,
+#addons-page[warning="safemode"] #detail-view[loading="true"] .global-warning {
+ display: none;
+}
+
+#addons-page .view-pane:not([type="plugin"]) .plugin-info-container {
+ display: none;
+}
+
+#addons-page .view-pane:not([type="experiment"]) .experiment-info-container {
+ display: none;
+}
+
+.addon .relnotes {
+ -moz-user-select: text;
+}
+#detail-name, #detail-desc, #detail-fulldesc {
+ -moz-user-select: text;
+}
+
+/* Make sure we're not animating hidden images. See bug 623739. */
+#view-port:not([selectedIndex="0"]) #discover-view .loading,
+#discover-view:not([selectedIndex="0"]) .loading {
+ display: none;
+}
+
+/* Elements in unselected richlistitems cannot be focused */
+richlistitem:not([selected]) * {
+ -moz-user-focus: ignore;
+}
+
+#header-search {
+ width: 22em;
+}
+
+#header-utils-btn {
+ -moz-user-focus: normal;
+}
+
+.discover-button[disabled="true"] {
+ display: none;
+}
+
+#experiments-learn-more[disabled="true"] {
+ display: none;
+}
+
+#experiments-change-telemetry[disabled="true"] {
+ display: none;
+}
+
+.view-pane[type="experiment"] .error,
+.view-pane[type="experiment"] .warning,
+.view-pane[type="experiment"] .addon:not([pending="uninstall"]) .pending,
+.view-pane[type="experiment"] .disabled-postfix,
+.view-pane[type="experiment"] .update-postfix,
+.view-pane[type="experiment"] .version,
+#detail-view[type="experiment"] .alert-container,
+#detail-view[type="experiment"] #detail-version,
+#detail-view[type="experiment"] #detail-creator {
+ display: none;
+}
+
+.view-pane:not([type="experiment"]) .experiment-container,
+.view-pane:not([type="experiment"]) #detail-experiment-container {
+ display: none;
+}
+
+.addon[type="experiment"][status="installing"] .experiment-time,
+.addon[type="experiment"][status="installing"] .experiment-state {
+ display: none;
+}
+
+/* Translators for Language Pack details */
+.translators > label {
+ -moz-margin-start: 0px;
+ -moz-margin-end: 0px;
+}
diff --git a/components/extensions/content/extensions.js b/components/extensions/content/extensions.js
new file mode 100644
index 000000000..fe84bc460
--- /dev/null
+++ b/components/extensions/content/extensions.js
@@ -0,0 +1,3568 @@
+/* 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 Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/DownloadUtils.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+#ifdef MOZ_DEVTOOLS
+XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function () {
+ return Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {}).
+ BrowserToolboxProcess;
+});
+#endif
+
+const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
+const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
+const PREF_XPI_ENABLED = "xpinstall.enabled";
+const PREF_MAXRESULTS = "extensions.getAddons.maxResults";
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled";
+const PREF_UI_TYPE_HIDDEN = "extensions.ui.%TYPE%.hidden";
+const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
+const PREF_ADDON_DEBUGGING_ENABLED = "devtools.chrome.enabled";
+const PREF_REMOTE_DEBUGGING_ENABLED = "devtools.debugger.remote-enabled";
+
+const LOADING_MSG_DELAY = 100;
+
+const SEARCH_SCORE_MULTIPLIER_NAME = 2;
+const SEARCH_SCORE_MULTIPLIER_DESCRIPTION = 2;
+
+// Use integers so search scores are sortable by nsIXULSortService
+const SEARCH_SCORE_MATCH_WHOLEWORD = 10;
+const SEARCH_SCORE_MATCH_WORDBOUNDRY = 6;
+const SEARCH_SCORE_MATCH_SUBSTRING = 3;
+
+const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000; // 2 days (in milliseconds)
+const UPDATES_RELEASENOTES_TRANSFORMFILE = "chrome://mozapps/content/extensions/updateinfo.xsl";
+
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
+
+var gViewDefault = "addons://discover/";
+
+var gStrings = {};
+XPCOMUtils.defineLazyServiceGetter(gStrings, "bundleSvc",
+ "@mozilla.org/intl/stringbundle;1",
+ "nsIStringBundleService");
+
+XPCOMUtils.defineLazyGetter(gStrings, "brand", function brandLazyGetter() {
+ return this.bundleSvc.createBundle("chrome://branding/locale/brand.properties");
+});
+XPCOMUtils.defineLazyGetter(gStrings, "ext", function extLazyGetter() {
+ return this.bundleSvc.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
+});
+XPCOMUtils.defineLazyGetter(gStrings, "dl", function dlLazyGetter() {
+ return this.bundleSvc.createBundle("chrome://mozapps/locale/downloads/downloads.properties");
+});
+
+XPCOMUtils.defineLazyGetter(gStrings, "brandShortName", function brandShortNameLazyGetter() {
+ return this.brand.GetStringFromName("brandShortName");
+});
+XPCOMUtils.defineLazyGetter(gStrings, "appVersion", function appVersionLazyGetter() {
+ return Services.appinfo.version;
+});
+
+document.addEventListener("load", initialize, true);
+window.addEventListener("unload", shutdown, false);
+
+var gPendingInitializations = 1;
+this.__defineGetter__("gIsInitializing", function gIsInitializingGetter() gPendingInitializations > 0);
+
+function initialize(event) {
+ // XXXbz this listener gets _all_ load events for all nodes in the
+ // document... but relies on not being called "too early".
+ if (event.target instanceof XMLStylesheetProcessingInstruction) {
+ return;
+ }
+ document.removeEventListener("load", initialize, true);
+
+ let globalCommandSet = document.getElementById("globalCommandSet");
+ globalCommandSet.addEventListener("command", function(event) {
+ gViewController.doCommand(event.target.id);
+ });
+
+ let viewCommandSet = document.getElementById("viewCommandSet");
+ viewCommandSet.addEventListener("commandupdate", function(event) {
+ gViewController.updateCommands();
+ });
+ viewCommandSet.addEventListener("command", function(event) {
+ gViewController.doCommand(event.target.id);
+ });
+
+ let detailScreenshot = document.getElementById("detail-screenshot");
+ detailScreenshot.addEventListener("load", function(event) {
+ this.removeAttribute("loading");
+ });
+ detailScreenshot.addEventListener("error", function(event) {
+ this.setAttribute("loading", "error");
+ });
+
+ let addonPage = document.getElementById("addons-page");
+ addonPage.addEventListener("dragenter", function(event) {
+ gDragDrop.onDragOver(event);
+ });
+ addonPage.addEventListener("dragover", function(event) {
+ gDragDrop.onDragOver(event);
+ });
+ addonPage.addEventListener("drop", function(event) {
+ gDragDrop.onDrop(event);
+ });
+ addonPage.addEventListener("keypress", function(event) {
+ gHeader.onKeyPress(event);
+ });
+
+ if (!isDiscoverEnabled()) {
+ gViewDefault = "addons://list/extension";
+ }
+
+ gViewController.initialize();
+ gCategories.initialize();
+ gHeader.initialize();
+ gEventManager.initialize();
+ Services.obs.addObserver(sendEMPong, "EM-ping", false);
+ Services.obs.notifyObservers(window, "EM-loaded", "");
+
+ // If the initial view has already been selected (by a call to loadView from
+ // the above notifications) then bail out now
+ if (gViewController.initialViewSelected)
+ return;
+
+ // If there is a history state to restore then use that
+ if (window.history.state) {
+ gViewController.updateState(window.history.state);
+ return;
+ }
+
+ // Default to the last selected category
+ var view = gCategories.node.value;
+
+ // Allow passing in a view through the window arguments
+ if ("arguments" in window && window.arguments.length > 0 &&
+ window.arguments[0] !== null && "view" in window.arguments[0]) {
+ view = window.arguments[0].view;
+ }
+
+ gViewController.loadInitialView(view);
+
+ Services.prefs.addObserver(PREF_ADDON_DEBUGGING_ENABLED, debuggingPrefChanged, false);
+ Services.prefs.addObserver(PREF_REMOTE_DEBUGGING_ENABLED, debuggingPrefChanged, false);
+}
+
+function notifyInitialized() {
+ if (!gIsInitializing)
+ return;
+
+ gPendingInitializations--;
+ if (!gIsInitializing) {
+ var event = document.createEvent("Events");
+ event.initEvent("Initialized", true, true);
+ document.dispatchEvent(event);
+ }
+}
+
+function shutdown() {
+ gCategories.shutdown();
+ gSearchView.shutdown();
+ gEventManager.shutdown();
+ gViewController.shutdown();
+ Services.obs.removeObserver(sendEMPong, "EM-ping");
+ Services.prefs.removeObserver(PREF_ADDON_DEBUGGING_ENABLED, debuggingPrefChanged);
+ Services.prefs.removeObserver(PREF_REMOTE_DEBUGGING_ENABLED, debuggingPrefChanged);
+}
+
+function sendEMPong(aSubject, aTopic, aData) {
+ Services.obs.notifyObservers(window, "EM-pong", "");
+}
+
+// Used by external callers to load a specific view into the manager
+function loadView(aViewId) {
+ if (!gViewController.initialViewSelected) {
+ // The caller opened the window and immediately loaded the view so it
+ // should be the initial history entry
+
+ gViewController.loadInitialView(aViewId);
+ } else {
+ gViewController.loadView(aViewId);
+ }
+}
+
+function isDiscoverEnabled() {
+ if (Services.prefs.getPrefType(PREF_DISCOVERURL) == Services.prefs.PREF_INVALID)
+ return false;
+
+ try {
+ if (!Services.prefs.getBoolPref(PREF_DISCOVER_ENABLED))
+ return false;
+ } catch (e) {}
+
+ try {
+ if (!Services.prefs.getBoolPref(PREF_XPI_ENABLED))
+ return false;
+ } catch (e) {}
+
+ return true;
+}
+
+/**
+ * Obtain the main DOMWindow for the current context.
+ */
+function getMainWindow() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+}
+
+function getBrowserElement() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+}
+
+/**
+ * Obtain the DOMWindow that can open a preferences pane.
+ *
+ * This is essentially "get the browser chrome window" with the added check
+ * that the supposed browser chrome window is capable of opening a preferences
+ * pane.
+ *
+ * This may return null if we can't find the browser chrome window.
+ */
+function getMainWindowWithPreferencesPane() {
+ let mainWindow = getMainWindow();
+ if (mainWindow && "openAdvancedPreferences" in mainWindow) {
+ return mainWindow;
+ } else {
+ return null;
+ }
+}
+
+/**
+ * A wrapper around the HTML5 session history service that allows the browser
+ * back/forward controls to work within the manager
+ */
+var HTML5History = {
+ get index() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .sessionHistory.index;
+ },
+
+ get canGoBack() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .canGoBack;
+ },
+
+ get canGoForward() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .canGoForward;
+ },
+
+ back: function HTML5History_back() {
+ window.history.back();
+ gViewController.updateCommand("cmd_back");
+ gViewController.updateCommand("cmd_forward");
+ },
+
+ forward: function HTML5History_forward() {
+ window.history.forward();
+ gViewController.updateCommand("cmd_back");
+ gViewController.updateCommand("cmd_forward");
+ },
+
+ pushState: function HTML5History_pushState(aState) {
+ window.history.pushState(aState, document.title);
+ },
+
+ replaceState: function HTML5History_replaceState(aState) {
+ window.history.replaceState(aState, document.title);
+ },
+
+ popState: function HTML5History_popState() {
+ function onStatePopped(aEvent) {
+ window.removeEventListener("popstate", onStatePopped, true);
+ // TODO To ensure we can't go forward again we put an additional entry
+ // for the current state into the history. Ideally we would just strip
+ // the history but there doesn't seem to be a way to do that. Bug 590661
+ window.history.pushState(aEvent.state, document.title);
+ }
+ window.addEventListener("popstate", onStatePopped, true);
+ window.history.back();
+ gViewController.updateCommand("cmd_back");
+ gViewController.updateCommand("cmd_forward");
+ }
+};
+
+/**
+ * A wrapper around a fake history service
+ */
+var FakeHistory = {
+ pos: 0,
+ states: [null],
+
+ get index() {
+ return this.pos;
+ },
+
+ get canGoBack() {
+ return this.pos > 0;
+ },
+
+ get canGoForward() {
+ return (this.pos + 1) < this.states.length;
+ },
+
+ back: function FakeHistory_back() {
+ if (this.pos == 0)
+ throw Components.Exception("Cannot go back from this point");
+
+ this.pos--;
+ gViewController.updateState(this.states[this.pos]);
+ gViewController.updateCommand("cmd_back");
+ gViewController.updateCommand("cmd_forward");
+ },
+
+ forward: function FakeHistory_forward() {
+ if ((this.pos + 1) >= this.states.length)
+ throw Components.Exception("Cannot go forward from this point");
+
+ this.pos++;
+ gViewController.updateState(this.states[this.pos]);
+ gViewController.updateCommand("cmd_back");
+ gViewController.updateCommand("cmd_forward");
+ },
+
+ pushState: function FakeHistory_pushState(aState) {
+ this.pos++;
+ this.states.splice(this.pos, this.states.length);
+ this.states.push(aState);
+ },
+
+ replaceState: function FakeHistory_replaceState(aState) {
+ this.states[this.pos] = aState;
+ },
+
+ popState: function FakeHistory_popState() {
+ if (this.pos == 0)
+ throw Components.Exception("Cannot popState from this view");
+
+ this.states.splice(this.pos, this.states.length);
+ this.pos--;
+
+ gViewController.updateState(this.states[this.pos]);
+ gViewController.updateCommand("cmd_back");
+ gViewController.updateCommand("cmd_forward");
+ }
+};
+
+// If the window has a session history then use the HTML5 History wrapper
+// otherwise use our fake history implementation
+if (window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .sessionHistory) {
+ var gHistory = HTML5History;
+}
+else {
+ gHistory = FakeHistory;
+}
+
+var gEventManager = {
+ _listeners: {},
+ _installListeners: [],
+
+ initialize: function gEM_initialize() {
+ var self = this;
+ const ADDON_EVENTS = ["onEnabling", "onEnabled", "onDisabling",
+ "onDisabled", "onUninstalling", "onUninstalled",
+ "onInstalled", "onOperationCancelled",
+ "onUpdateAvailable", "onUpdateFinished",
+ "onCompatibilityUpdateAvailable",
+ "onPropertyChanged"];
+ for (let evt of ADDON_EVENTS) {
+ let event = evt;
+ self[event] = function initialize_delegateAddonEvent(...aArgs) {
+ self.delegateAddonEvent(event, aArgs);
+ };
+ }
+
+ const INSTALL_EVENTS = ["onNewInstall", "onDownloadStarted",
+ "onDownloadEnded", "onDownloadFailed",
+ "onDownloadProgress", "onDownloadCancelled",
+ "onInstallStarted", "onInstallEnded",
+ "onInstallFailed", "onInstallCancelled",
+ "onExternalInstall"];
+ for (let evt of INSTALL_EVENTS) {
+ let event = evt;
+ self[event] = function initialize_delegateInstallEvent(...aArgs) {
+ self.delegateInstallEvent(event, aArgs);
+ };
+ }
+
+ AddonManager.addManagerListener(this);
+ AddonManager.addInstallListener(this);
+ AddonManager.addAddonListener(this);
+
+ this.refreshGlobalWarning();
+ this.refreshAutoUpdateDefault();
+
+ var contextMenu = document.getElementById("addonitem-popup");
+ contextMenu.addEventListener("popupshowing", function contextMenu_onPopupshowing() {
+ var addon = gViewController.currentViewObj.getSelectedAddon();
+ contextMenu.setAttribute("addontype", addon.type);
+
+ var menuSep = document.getElementById("addonitem-menuseparator");
+ var countMenuItemsBeforeSep = 0;
+ for (let child of contextMenu.children) {
+ if (child == menuSep) {
+ break;
+ }
+ if (child.nodeName == "menuitem" &&
+ gViewController.isCommandEnabled(child.command)) {
+ countMenuItemsBeforeSep++;
+ }
+ }
+
+ // Hide the separator if there are no visible menu items before it
+ menuSep.hidden = (countMenuItemsBeforeSep == 0);
+
+ }, false);
+ },
+
+ shutdown: function gEM_shutdown() {
+ AddonManager.removeManagerListener(this);
+ AddonManager.removeInstallListener(this);
+ AddonManager.removeAddonListener(this);
+ },
+
+ registerAddonListener: function gEM_registerAddonListener(aListener, aAddonId) {
+ if (!(aAddonId in this._listeners))
+ this._listeners[aAddonId] = [];
+ else if (this._listeners[aAddonId].indexOf(aListener) != -1)
+ return;
+ this._listeners[aAddonId].push(aListener);
+ },
+
+ unregisterAddonListener: function gEM_unregisterAddonListener(aListener, aAddonId) {
+ if (!(aAddonId in this._listeners))
+ return;
+ var index = this._listeners[aAddonId].indexOf(aListener);
+ if (index == -1)
+ return;
+ this._listeners[aAddonId].splice(index, 1);
+ },
+
+ registerInstallListener: function gEM_registerInstallListener(aListener) {
+ if (this._installListeners.indexOf(aListener) != -1)
+ return;
+ this._installListeners.push(aListener);
+ },
+
+ unregisterInstallListener: function gEM_unregisterInstallListener(aListener) {
+ var i = this._installListeners.indexOf(aListener);
+ if (i == -1)
+ return;
+ this._installListeners.splice(i, 1);
+ },
+
+ delegateAddonEvent: function gEM_delegateAddonEvent(aEvent, aParams) {
+ var addon = aParams.shift();
+ if (!(addon.id in this._listeners))
+ return;
+
+ var listeners = this._listeners[addon.id];
+ for (let listener of listeners) {
+ if (!(aEvent in listener))
+ continue;
+ try {
+ listener[aEvent].apply(listener, aParams);
+ } catch(e) {
+ // this shouldn't be fatal
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ delegateInstallEvent: function gEM_delegateInstallEvent(aEvent, aParams) {
+ var existingAddon = aEvent == "onExternalInstall" ? aParams[1] : aParams[0].existingAddon;
+ // If the install is an update then send the event to all listeners
+ // registered for the existing add-on
+ if (existingAddon)
+ this.delegateAddonEvent(aEvent, [existingAddon].concat(aParams));
+
+ for (let listener of this._installListeners) {
+ if (!(aEvent in listener))
+ continue;
+ try {
+ listener[aEvent].apply(listener, aParams);
+ } catch(e) {
+ // this shouldn't be fatal
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ refreshGlobalWarning: function gEM_refreshGlobalWarning() {
+ var page = document.getElementById("addons-page");
+
+ if (Services.appinfo.inSafeMode) {
+ page.setAttribute("warning", "safemode");
+ return;
+ }
+
+ if (AddonManager.checkUpdateSecurityDefault &&
+ !AddonManager.checkUpdateSecurity) {
+ page.setAttribute("warning", "updatesecurity");
+ return;
+ }
+
+ if (!AddonManager.checkCompatibility) {
+ page.setAttribute("warning", "checkcompatibility");
+ return;
+ }
+
+ page.removeAttribute("warning");
+ },
+
+ refreshAutoUpdateDefault: function gEM_refreshAutoUpdateDefault() {
+ var updateEnabled = AddonManager.updateEnabled;
+ var autoUpdateDefault = AddonManager.autoUpdateDefault;
+
+ // The checkbox needs to reflect that both prefs need to be true
+ // for updates to be checked for and applied automatically
+ document.getElementById("utils-autoUpdateDefault")
+ .setAttribute("checked", updateEnabled && autoUpdateDefault);
+
+ document.getElementById("utils-resetAddonUpdatesToAutomatic").hidden = !autoUpdateDefault;
+ document.getElementById("utils-resetAddonUpdatesToManual").hidden = autoUpdateDefault;
+ },
+
+ onCompatibilityModeChanged: function gEM_onCompatibilityModeChanged() {
+ this.refreshGlobalWarning();
+ },
+
+ onCheckUpdateSecurityChanged: function gEM_onCheckUpdateSecurityChanged() {
+ this.refreshGlobalWarning();
+ },
+
+ onUpdateModeChanged: function gEM_onUpdateModeChanged() {
+ this.refreshAutoUpdateDefault();
+ }
+};
+
+
+var gViewController = {
+ viewPort: null,
+ currentViewId: "",
+ currentViewObj: null,
+ currentViewRequest: 0,
+ viewObjects: {},
+ viewChangeCallback: null,
+ initialViewSelected: false,
+ lastHistoryIndex: -1,
+
+ initialize: function gVC_initialize() {
+ this.viewPort = document.getElementById("view-port");
+
+ this.viewObjects["search"] = gSearchView;
+ this.viewObjects["discover"] = gDiscoverView;
+ this.viewObjects["list"] = gListView;
+ this.viewObjects["detail"] = gDetailView;
+ this.viewObjects["updates"] = gUpdatesView;
+
+ for each (let view in this.viewObjects)
+ view.initialize();
+
+ window.controllers.appendController(this);
+
+ window.addEventListener("popstate",
+ function window_onStatePopped(e) {
+ gViewController.updateState(e.state);
+ },
+ false);
+ },
+
+ shutdown: function gVC_shutdown() {
+ if (this.currentViewObj)
+ this.currentViewObj.hide();
+ this.currentViewRequest = 0;
+
+ for each(let view in this.viewObjects) {
+ if ("shutdown" in view) {
+ try {
+ view.shutdown();
+ } catch(e) {
+ // this shouldn't be fatal
+ Cu.reportError(e);
+ }
+ }
+ }
+
+ window.controllers.removeController(this);
+ },
+
+ updateState: function gVC_updateState(state) {
+ try {
+ this.loadViewInternal(state.view, state.previousView, state);
+ this.lastHistoryIndex = gHistory.index;
+ }
+ catch (e) {
+ // The attempt to load the view failed, try moving further along history
+ if (this.lastHistoryIndex > gHistory.index) {
+ if (gHistory.canGoBack)
+ gHistory.back();
+ else
+ gViewController.replaceView(gViewDefault);
+ } else {
+ if (gHistory.canGoForward)
+ gHistory.forward();
+ else
+ gViewController.replaceView(gViewDefault);
+ }
+ }
+ },
+
+ parseViewId: function gVC_parseViewId(aViewId) {
+ var matchRegex = /^addons:\/\/([^\/]+)\/(.*)$/;
+ var [,viewType, viewParam] = aViewId.match(matchRegex) || [];
+ return {type: viewType, param: decodeURIComponent(viewParam)};
+ },
+
+ get isLoading() {
+ return !this.currentViewObj || this.currentViewObj.node.hasAttribute("loading");
+ },
+
+ loadView: function gVC_loadView(aViewId) {
+ var isRefresh = false;
+ if (aViewId == this.currentViewId) {
+ if (this.isLoading)
+ return;
+ if (!("refresh" in this.currentViewObj))
+ return;
+ if (!this.currentViewObj.canRefresh())
+ return;
+ isRefresh = true;
+ }
+
+ var state = {
+ view: aViewId,
+ previousView: this.currentViewId
+ };
+ if (!isRefresh) {
+ gHistory.pushState(state);
+ this.lastHistoryIndex = gHistory.index;
+ }
+ this.loadViewInternal(aViewId, this.currentViewId, state);
+ },
+
+ // Replaces the existing view with a new one, rewriting the current history
+ // entry to match.
+ replaceView: function gVC_replaceView(aViewId) {
+ if (aViewId == this.currentViewId)
+ return;
+
+ var state = {
+ view: aViewId,
+ previousView: null
+ };
+ gHistory.replaceState(state);
+ this.loadViewInternal(aViewId, null, state);
+ },
+
+ loadInitialView: function gVC_loadInitialView(aViewId) {
+ var state = {
+ view: aViewId,
+ previousView: null
+ };
+ gHistory.replaceState(state);
+
+ this.loadViewInternal(aViewId, null, state);
+ this.initialViewSelected = true;
+ notifyInitialized();
+ },
+
+ loadViewInternal: function gVC_loadViewInternal(aViewId, aPreviousView, aState) {
+ var view = this.parseViewId(aViewId);
+
+ if (!view.type || !(view.type in this.viewObjects))
+ throw Components.Exception("Invalid view: " + view.type);
+
+ var viewObj = this.viewObjects[view.type];
+ if (!viewObj.node)
+ throw Components.Exception("Root node doesn't exist for '" + view.type + "' view");
+
+ if (this.currentViewObj && aViewId != aPreviousView) {
+ try {
+ let canHide = this.currentViewObj.hide();
+ if (canHide === false)
+ return;
+ this.viewPort.selectedPanel.removeAttribute("loading");
+ } catch (e) {
+ // this shouldn't be fatal
+ Cu.reportError(e);
+ }
+ }
+
+ gCategories.select(aViewId, aPreviousView);
+
+ this.currentViewId = aViewId;
+ this.currentViewObj = viewObj;
+
+ this.viewPort.selectedPanel = this.currentViewObj.node;
+ this.viewPort.selectedPanel.setAttribute("loading", "true");
+ this.currentViewObj.node.focus();
+
+ if (aViewId == aPreviousView)
+ this.currentViewObj.refresh(view.param, ++this.currentViewRequest, aState);
+ else
+ this.currentViewObj.show(view.param, ++this.currentViewRequest, aState);
+ },
+
+ // Moves back in the document history and removes the current history entry
+ popState: function gVC_popState(aCallback) {
+ this.viewChangeCallback = aCallback;
+ gHistory.popState();
+ },
+
+ notifyViewChanged: function gVC_notifyViewChanged() {
+ this.viewPort.selectedPanel.removeAttribute("loading");
+
+ if (this.viewChangeCallback) {
+ this.viewChangeCallback();
+ this.viewChangeCallback = null;
+ }
+
+ var event = document.createEvent("Events");
+ event.initEvent("ViewChanged", true, true);
+ this.currentViewObj.node.dispatchEvent(event);
+ },
+
+ commands: {
+ cmd_back: {
+ isEnabled: function cmd_back_isEnabled() {
+ return gHistory.canGoBack;
+ },
+ doCommand: function cmd_back_doCommand() {
+ gHistory.back();
+ }
+ },
+
+ cmd_forward: {
+ isEnabled: function cmd_forward_isEnabled() {
+ return gHistory.canGoForward;
+ },
+ doCommand: function cmd_forward_doCommand() {
+ gHistory.forward();
+ }
+ },
+
+ cmd_focusSearch: {
+ isEnabled: () => true,
+ doCommand: function cmd_focusSearch_doCommand() {
+ gHeader.focusSearchBox();
+ }
+ },
+
+ cmd_restartApp: {
+ isEnabled: function cmd_restartApp_isEnabled() true,
+ doCommand: function cmd_restartApp_doCommand() {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested",
+ "restart");
+ if (cancelQuit.data)
+ return; // somebody canceled our quit request
+
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].
+ getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+ }
+ },
+
+ cmd_enableCheckCompatibility: {
+ isEnabled: function cmd_enableCheckCompatibility_isEnabled() true,
+ doCommand: function cmd_enableCheckCompatibility_doCommand() {
+ AddonManager.checkCompatibility = true;
+ }
+ },
+
+ cmd_enableUpdateSecurity: {
+ isEnabled: function cmd_enableUpdateSecurity_isEnabled() true,
+ doCommand: function cmd_enableUpdateSecurity_doCommand() {
+ AddonManager.checkUpdateSecurity = true;
+ }
+ },
+
+ cmd_toggleAutoUpdateDefault: {
+ isEnabled: function cmd_toggleAutoUpdateDefault_isEnabled() true,
+ doCommand: function cmd_toggleAutoUpdateDefault_doCommand() {
+ if (!AddonManager.updateEnabled || !AddonManager.autoUpdateDefault) {
+ // One or both of the prefs is false, i.e. the checkbox is not checked.
+ // Now toggle both to true. If the user wants us to auto-update
+ // add-ons, we also need to auto-check for updates.
+ AddonManager.updateEnabled = true;
+ AddonManager.autoUpdateDefault = true;
+ } else {
+ // Both prefs are true, i.e. the checkbox is checked.
+ // Toggle the auto pref to false, but don't touch the enabled check.
+ AddonManager.autoUpdateDefault = false;
+ }
+ }
+ },
+
+ cmd_resetAddonAutoUpdate: {
+ isEnabled: function cmd_resetAddonAutoUpdate_isEnabled() true,
+ doCommand: function cmd_resetAddonAutoUpdate_doCommand() {
+ AddonManager.getAllAddons(function cmd_resetAddonAutoUpdate_getAllAddons(aAddonList) {
+ for (let addon of aAddonList) {
+ if ("applyBackgroundUpdates" in addon)
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+ }
+ });
+ }
+ },
+
+ cmd_goToDiscoverPane: {
+ isEnabled: function cmd_goToDiscoverPane_isEnabled() {
+ return gDiscoverView.enabled;
+ },
+ doCommand: function cmd_goToDiscoverPane_doCommand() {
+ gViewController.loadView("addons://discover/");
+ }
+ },
+
+ cmd_goToRecentUpdates: {
+ isEnabled: function cmd_goToRecentUpdates_isEnabled() true,
+ doCommand: function cmd_goToRecentUpdates_doCommand() {
+ gViewController.loadView("addons://updates/recent");
+ }
+ },
+
+ cmd_goToAvailableUpdates: {
+ isEnabled: function cmd_goToAvailableUpdates_isEnabled() true,
+ doCommand: function cmd_goToAvailableUpdates_doCommand() {
+ gViewController.loadView("addons://updates/available");
+ }
+ },
+
+ cmd_showItemDetails: {
+ isEnabled: function cmd_showItemDetails_isEnabled(aAddon) {
+ return !!aAddon && (gViewController.currentViewObj != gDetailView);
+ },
+ doCommand: function cmd_showItemDetails_doCommand(aAddon, aScrollToPreferences) {
+ gViewController.loadView("addons://detail/" +
+ encodeURIComponent(aAddon.id) +
+ (aScrollToPreferences ? "/preferences" : ""));
+ }
+ },
+
+ cmd_findAllUpdates: {
+ inProgress: false,
+ isEnabled: function cmd_findAllUpdates_isEnabled() !this.inProgress,
+ doCommand: function cmd_findAllUpdates_doCommand() {
+ this.inProgress = true;
+ gViewController.updateCommand("cmd_findAllUpdates");
+ document.getElementById("updates-noneFound").hidden = true;
+ document.getElementById("updates-progress").hidden = false;
+ document.getElementById("updates-manualUpdatesFound-btn").hidden = true;
+
+ var pendingChecks = 0;
+ var numUpdated = 0;
+ var numManualUpdates = 0;
+ var restartNeeded = false;
+ var self = this;
+
+ function updateStatus() {
+ if (pendingChecks > 0)
+ return;
+
+ self.inProgress = false;
+ gViewController.updateCommand("cmd_findAllUpdates");
+ document.getElementById("updates-progress").hidden = true;
+ gUpdatesView.maybeRefresh();
+
+ if (numManualUpdates > 0 && numUpdated == 0) {
+ document.getElementById("updates-manualUpdatesFound-btn").hidden = false;
+ return;
+ }
+
+ if (numUpdated == 0) {
+ document.getElementById("updates-noneFound").hidden = false;
+ return;
+ }
+
+ if (restartNeeded) {
+ document.getElementById("updates-downloaded").hidden = false;
+ document.getElementById("updates-restart-btn").hidden = false;
+ } else {
+ document.getElementById("updates-installed").hidden = false;
+ }
+ }
+
+ var updateInstallListener = {
+ onDownloadFailed: function cmd_findAllUpdates_downloadFailed() {
+ pendingChecks--;
+ updateStatus();
+ },
+ onInstallFailed: function cmd_findAllUpdates_installFailed() {
+ pendingChecks--;
+ updateStatus();
+ },
+ onInstallEnded: function cmd_findAllUpdates_installEnded(aInstall, aAddon) {
+ pendingChecks--;
+ numUpdated++;
+ if (isPending(aInstall.existingAddon, "upgrade"))
+ restartNeeded = true;
+ updateStatus();
+ }
+ };
+
+ var updateCheckListener = {
+ onUpdateAvailable: function cmd_findAllUpdates_updateAvailable(aAddon, aInstall) {
+ gEventManager.delegateAddonEvent("onUpdateAvailable",
+ [aAddon, aInstall]);
+ if (AddonManager.shouldAutoUpdate(aAddon)) {
+ aInstall.addListener(updateInstallListener);
+ aInstall.install();
+ } else {
+ pendingChecks--;
+ numManualUpdates++;
+ updateStatus();
+ }
+ },
+ onNoUpdateAvailable: function cmd_findAllUpdates_noUpdateAvailable(aAddon) {
+ pendingChecks--;
+ updateStatus();
+ },
+ onUpdateFinished: function cmd_findAllUpdates_updateFinished(aAddon, aError) {
+ gEventManager.delegateAddonEvent("onUpdateFinished",
+ [aAddon, aError]);
+ }
+ };
+
+ AddonManager.getAddonsByTypes(null, function cmd_findAllUpdates_getAddonsByTypes(aAddonList) {
+ for (let addon of aAddonList) {
+ if (addon.permissions & AddonManager.PERM_CAN_UPGRADE) {
+ pendingChecks++;
+ addon.findUpdates(updateCheckListener,
+ AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ }
+ }
+
+ if (pendingChecks == 0)
+ updateStatus();
+ });
+ }
+ },
+
+ cmd_findItemUpdates: {
+ isEnabled: function cmd_findItemUpdates_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ return hasPermission(aAddon, "upgrade");
+ },
+ doCommand: function cmd_findItemUpdates_doCommand(aAddon) {
+ var listener = {
+ onUpdateAvailable: function cmd_findItemUpdates_updateAvailable(aAddon, aInstall) {
+ gEventManager.delegateAddonEvent("onUpdateAvailable",
+ [aAddon, aInstall]);
+ if (AddonManager.shouldAutoUpdate(aAddon))
+ aInstall.install();
+ },
+ onNoUpdateAvailable: function cmd_findItemUpdates_noUpdateAvailable(aAddon) {
+ gEventManager.delegateAddonEvent("onNoUpdateAvailable",
+ [aAddon]);
+ }
+ };
+ gEventManager.delegateAddonEvent("onCheckingUpdate", [aAddon]);
+ aAddon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ }
+ },
+
+#ifdef MOZ_DEVTOOLS
+ cmd_debugItem: {
+ doCommand: function cmd_debugItem_doCommand(aAddon) {
+ BrowserToolboxProcess.init({ addonID: aAddon.id });
+ },
+
+ isEnabled: function cmd_debugItem_isEnabled(aAddon) {
+ let debuggerEnabled = Services.prefs.
+ getBoolPref(PREF_ADDON_DEBUGGING_ENABLED);
+ let remoteEnabled = Services.prefs.
+ getBoolPref(PREF_REMOTE_DEBUGGING_ENABLED);
+ return aAddon && aAddon.isDebuggable && debuggerEnabled && remoteEnabled;
+ }
+ },
+#endif
+
+ cmd_showItemPreferences: {
+ isEnabled: function cmd_showItemPreferences_isEnabled(aAddon) {
+ if (!aAddon ||
+ (!aAddon.isActive && !aAddon.isGMPlugin) ||
+ !aAddon.optionsURL) {
+ return false;
+ }
+ if (gViewController.currentViewObj == gDetailView &&
+ aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE) {
+ return false;
+ }
+ if (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO)
+ return false;
+ return true;
+ },
+ doCommand: function cmd_showItemPreferences_doCommand(aAddon) {
+ if (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE) {
+ gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true);
+ return;
+ }
+ var optionsURL = aAddon.optionsURL;
+ if (aAddon.optionsType == AddonManager.OPTIONS_TYPE_TAB &&
+ openOptionsInTab(optionsURL)) {
+ return;
+ }
+ var windows = Services.wm.getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ var win = windows.getNext();
+ if (win.closed) {
+ continue;
+ }
+ if (win.document.documentURI == optionsURL) {
+ win.focus();
+ return;
+ }
+ }
+ var features = "chrome,titlebar,toolbar,centerscreen";
+ try {
+ var instantApply = Services.prefs.getBoolPref("browser.preferences.instantApply");
+ features += instantApply ? ",dialog=no" : ",modal";
+ } catch (e) {
+ features += ",modal";
+ }
+ openDialog(optionsURL, "", features);
+ }
+ },
+
+ cmd_showItemAbout: {
+ isEnabled: function cmd_showItemAbout_isEnabled(aAddon) {
+ // XXXunf This may be applicable to install items too. See bug 561260
+ return !!aAddon;
+ },
+ doCommand: function cmd_showItemAbout_doCommand(aAddon) {
+ var aboutURL = aAddon.aboutURL;
+ if (aboutURL)
+ openDialog(aboutURL, "", "chrome,centerscreen,modal", aAddon);
+ else
+ openDialog("chrome://mozapps/content/extensions/about.xul",
+ "", "chrome,centerscreen,modal", aAddon);
+ }
+ },
+
+ cmd_enableItem: {
+ isEnabled: function cmd_enableItem_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ let addonType = AddonManager.addonTypes[aAddon.type];
+ return (!(addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) &&
+ hasPermission(aAddon, "enable"));
+ },
+ doCommand: function cmd_enableItem_doCommand(aAddon) {
+ aAddon.userDisabled = false;
+ },
+ getTooltip: function cmd_enableItem_getTooltip(aAddon) {
+ if (!aAddon)
+ return "";
+ if (aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE)
+ return gStrings.ext.GetStringFromName("enableAddonRestartRequiredTooltip");
+ return gStrings.ext.GetStringFromName("enableAddonTooltip");
+ }
+ },
+
+ cmd_disableItem: {
+ isEnabled: function cmd_disableItem_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ let addonType = AddonManager.addonTypes[aAddon.type];
+ return (!(addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) &&
+ hasPermission(aAddon, "disable"));
+ },
+ doCommand: function cmd_disableItem_doCommand(aAddon) {
+ aAddon.userDisabled = true;
+ },
+ getTooltip: function cmd_disableItem_getTooltip(aAddon) {
+ if (!aAddon)
+ return "";
+ if (aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE)
+ return gStrings.ext.GetStringFromName("disableAddonRestartRequiredTooltip");
+ return gStrings.ext.GetStringFromName("disableAddonTooltip");
+ }
+ },
+
+ cmd_installItem: {
+ isEnabled: function cmd_installItem_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ return aAddon.install && aAddon.install.state == AddonManager.STATE_AVAILABLE;
+ },
+ doCommand: function cmd_installItem_doCommand(aAddon) {
+ function doInstall() {
+ gViewController.currentViewObj.getListItemForID(aAddon.id)._installStatus.installRemote();
+ }
+
+ if (gViewController.currentViewObj == gDetailView)
+ gViewController.popState(doInstall);
+ else
+ doInstall();
+ }
+ },
+
+ cmd_purchaseItem: {
+ isEnabled: function cmd_purchaseItem_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ return !!aAddon.purchaseURL;
+ },
+ doCommand: function cmd_purchaseItem_doCommand(aAddon) {
+ openURL(aAddon.purchaseURL);
+ }
+ },
+
+ cmd_uninstallItem: {
+ isEnabled: function cmd_uninstallItem_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ return hasPermission(aAddon, "uninstall");
+ },
+ doCommand: function cmd_uninstallItem_doCommand(aAddon) {
+ if (gViewController.currentViewObj != gDetailView) {
+ aAddon.uninstall();
+ return;
+ }
+
+ gViewController.popState(function cmd_uninstallItem_popState() {
+ gViewController.currentViewObj.getListItemForID(aAddon.id).uninstall();
+ });
+ },
+ getTooltip: function cmd_uninstallItem_getTooltip(aAddon) {
+ if (!aAddon)
+ return "";
+ if (aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL)
+ return gStrings.ext.GetStringFromName("uninstallAddonRestartRequiredTooltip");
+ return gStrings.ext.GetStringFromName("uninstallAddonTooltip");
+ }
+ },
+
+ cmd_cancelUninstallItem: {
+ isEnabled: function cmd_cancelUninstallItem_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ return isPending(aAddon, "uninstall");
+ },
+ doCommand: function cmd_cancelUninstallItem_doCommand(aAddon) {
+ aAddon.cancelUninstall();
+ }
+ },
+
+ cmd_installFromFile: {
+ isEnabled: function cmd_installFromFile_isEnabled() true,
+ doCommand: function cmd_installFromFile_doCommand() {
+ const nsIFilePicker = Ci.nsIFilePicker;
+ var fp = Cc["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+ fp.init(window,
+ gStrings.ext.GetStringFromName("installFromFile.dialogTitle"),
+ nsIFilePicker.modeOpenMultiple);
+ try {
+ fp.appendFilter(gStrings.ext.GetStringFromName("installFromFile.filterName"),
+ "*.xpi;*.jar");
+ fp.appendFilters(nsIFilePicker.filterAll);
+ } catch (e) { }
+
+ if (fp.show() != nsIFilePicker.returnOK)
+ return;
+
+ var files = fp.files;
+ var installs = [];
+
+ function buildNextInstall() {
+ if (!files.hasMoreElements()) {
+ if (installs.length > 0) {
+ // Display the normal install confirmation for the installs
+ let webInstaller = Cc["@mozilla.org/addons/web-install-listener;1"].
+ getService(Ci.amIWebInstallListener);
+ webInstaller.onWebInstallRequested(getBrowserElement(),
+ document.documentURIObject,
+ installs, installs.length);
+ }
+ return;
+ }
+
+ var file = files.getNext();
+ AddonManager.getInstallForFile(file, function cmd_installFromFile_getInstallForFile(aInstall) {
+ installs.push(aInstall);
+ buildNextInstall();
+ });
+ }
+
+ buildNextInstall();
+ }
+ },
+
+ cmd_cancelOperation: {
+ isEnabled: function cmd_cancelOperation_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ return aAddon.pendingOperations != AddonManager.PENDING_NONE;
+ },
+ doCommand: function cmd_cancelOperation_doCommand(aAddon) {
+ if (isPending(aAddon, "install")) {
+ aAddon.install.cancel();
+ } else if (isPending(aAddon, "upgrade")) {
+ aAddon.pendingUpgrade.install.cancel();
+ } else if (isPending(aAddon, "uninstall")) {
+ aAddon.cancelUninstall();
+ } else if (isPending(aAddon, "enable")) {
+ aAddon.userDisabled = true;
+ } else if (isPending(aAddon, "disable")) {
+ aAddon.userDisabled = false;
+ }
+ }
+ },
+
+ cmd_contribute: {
+ isEnabled: function cmd_contribute_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ return ("contributionURL" in aAddon && aAddon.contributionURL);
+ },
+ doCommand: function cmd_contribute_doCommand(aAddon) {
+ openURL(aAddon.contributionURL);
+ }
+ },
+
+ cmd_askToActivateItem: {
+ isEnabled: function cmd_askToActivateItem_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ let addonType = AddonManager.addonTypes[aAddon.type];
+ return ((addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) &&
+ hasPermission(aAddon, "ask_to_activate"));
+ },
+ doCommand: function cmd_askToActivateItem_doCommand(aAddon) {
+ aAddon.userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE;
+ }
+ },
+
+ cmd_alwaysActivateItem: {
+ isEnabled: function cmd_alwaysActivateItem_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ let addonType = AddonManager.addonTypes[aAddon.type];
+ return ((addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) &&
+ hasPermission(aAddon, "enable"));
+ },
+ doCommand: function cmd_alwaysActivateItem_doCommand(aAddon) {
+ aAddon.userDisabled = false;
+ }
+ },
+
+ cmd_neverActivateItem: {
+ isEnabled: function cmd_neverActivateItem_isEnabled(aAddon) {
+ if (!aAddon)
+ return false;
+ let addonType = AddonManager.addonTypes[aAddon.type];
+ return ((addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) &&
+ hasPermission(aAddon, "disable"));
+ },
+ doCommand: function cmd_neverActivateItem_doCommand(aAddon) {
+ aAddon.userDisabled = true;
+ }
+ }
+ },
+
+ supportsCommand: function gVC_supportsCommand(aCommand) {
+ return (aCommand in this.commands);
+ },
+
+ isCommandEnabled: function gVC_isCommandEnabled(aCommand) {
+ if (!this.supportsCommand(aCommand))
+ return false;
+ var addon = this.currentViewObj.getSelectedAddon();
+ return this.commands[aCommand].isEnabled(addon);
+ },
+
+ updateCommands: function gVC_updateCommands() {
+ // wait until the view is initialized
+ if (!this.currentViewObj)
+ return;
+ var addon = this.currentViewObj.getSelectedAddon();
+ for (let commandId in this.commands)
+ this.updateCommand(commandId, addon);
+ },
+
+ updateCommand: function gVC_updateCommand(aCommandId, aAddon) {
+ if (typeof aAddon == "undefined")
+ aAddon = this.currentViewObj.getSelectedAddon();
+ var cmd = this.commands[aCommandId];
+ var cmdElt = document.getElementById(aCommandId);
+ cmdElt.setAttribute("disabled", !cmd.isEnabled(aAddon));
+ if ("getTooltip" in cmd) {
+ let tooltip = cmd.getTooltip(aAddon);
+ if (tooltip)
+ cmdElt.setAttribute("tooltiptext", tooltip);
+ else
+ cmdElt.removeAttribute("tooltiptext");
+ }
+ },
+
+ doCommand: function gVC_doCommand(aCommand, aAddon) {
+ if (!this.supportsCommand(aCommand))
+ return;
+ var cmd = this.commands[aCommand];
+ if (!aAddon)
+ aAddon = this.currentViewObj.getSelectedAddon();
+ if (!cmd.isEnabled(aAddon))
+ return;
+ cmd.doCommand(aAddon);
+ },
+
+ onEvent: function gVC_onEvent() {}
+};
+
+function hasInlineOptions(aAddon) {
+ return (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE ||
+ aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO);
+}
+
+function openOptionsInTab(optionsURL) {
+ let mainWindow = getMainWindow();
+ if ("switchToTabHavingURI" in mainWindow) {
+ mainWindow.switchToTabHavingURI(optionsURL, true);
+ return true;
+ }
+ return false;
+}
+
+function formatDate(aDate) {
+ return Cc["@mozilla.org/intl/scriptabledateformat;1"]
+ .getService(Ci.nsIScriptableDateFormat)
+ .FormatDate("",
+ Ci.nsIScriptableDateFormat.dateFormatLong,
+ aDate.getFullYear(),
+ aDate.getMonth() + 1,
+ aDate.getDate()
+ );
+}
+
+
+function hasPermission(aAddon, aPerm) {
+ var perm = AddonManager["PERM_CAN_" + aPerm.toUpperCase()];
+ return !!(aAddon.permissions & perm);
+}
+
+
+function isPending(aAddon, aAction) {
+ var action = AddonManager["PENDING_" + aAction.toUpperCase()];
+ return !!(aAddon.pendingOperations & action);
+}
+
+function isInState(aInstall, aState) {
+ var state = AddonManager["STATE_" + aState.toUpperCase()];
+ return aInstall.state == state;
+}
+
+function shouldShowVersionNumber(aAddon) {
+ if (!aAddon.version)
+ return false;
+
+ // The version number is hidden for lightweight themes.
+ if (aAddon.type == "theme")
+ return !/@personas\.mozilla\.org$/.test(aAddon.id);
+
+ return true;
+}
+
+function createItem(aObj, aIsInstall, aIsRemote) {
+ let item = document.createElement("richlistitem");
+
+ item.setAttribute("class", "addon addon-view");
+ item.setAttribute("name", aObj.name);
+ item.setAttribute("type", aObj.type);
+ item.setAttribute("remote", !!aIsRemote);
+
+ if (aIsInstall) {
+ item.mInstall = aObj;
+
+ if (aObj.state != AddonManager.STATE_INSTALLED) {
+ item.setAttribute("status", "installing");
+ return item;
+ }
+ aObj = aObj.addon;
+ }
+
+ item.mAddon = aObj;
+
+ item.setAttribute("status", "installed");
+
+ // set only attributes needed for sorting and XBL binding,
+ // the binding handles the rest
+ item.setAttribute("value", aObj.id);
+
+ return item;
+}
+
+function sortElements(aElements, aSortBy, aAscending) {
+ // aSortBy is an Array of attributes to sort by, in decending
+ // order of priority.
+
+ const DATE_FIELDS = ["updateDate"];
+ const NUMERIC_FIELDS = ["size", "relevancescore", "purchaseAmount"];
+
+ // We're going to group add-ons into the following buckets:
+ //
+ // enabledInstalled
+ // * Enabled
+ // * Incompatible but enabled because compatibility checking is off
+ // * Waiting to be installed
+ // * Waiting to be enabled
+ //
+ // pendingDisable
+ // * Waiting to be disabled
+ //
+ // pendingUninstall
+ // * Waiting to be removed
+ //
+ // disabledIncompatibleBlocked
+ // * Disabled
+ // * Incompatible
+ // * Blocklisted
+
+ const UISTATE_ORDER = ["enabled", "askToActivate", "pendingDisable",
+ "pendingUninstall", "disabled"];
+
+ function dateCompare(a, b) {
+ var aTime = a.getTime();
+ var bTime = b.getTime();
+ if (aTime < bTime)
+ return -1;
+ if (aTime > bTime)
+ return 1;
+ return 0;
+ }
+
+ function numberCompare(a, b) {
+ return a - b;
+ }
+
+ function stringCompare(a, b) {
+ return a.localeCompare(b);
+ }
+
+ function uiStateCompare(a, b) {
+ // If we're in descending order, swap a and b, because
+ // we don't ever want to have descending uiStates
+ if (!aAscending)
+ [a, b] = [b, a];
+
+ return (UISTATE_ORDER.indexOf(a) - UISTATE_ORDER.indexOf(b));
+ }
+
+ function getValue(aObj, aKey) {
+ if (!aObj)
+ return null;
+
+ if (aObj.hasAttribute(aKey))
+ return aObj.getAttribute(aKey);
+
+ var addon = aObj.mAddon || aObj.mInstall;
+ var addonType = aObj.mAddon && AddonManager.addonTypes[aObj.mAddon.type];
+
+ if (!addon)
+ return null;
+
+ if (aKey == "uiState") {
+ if (addon.pendingOperations == AddonManager.PENDING_DISABLE)
+ return "pendingDisable";
+ if (addon.pendingOperations == AddonManager.PENDING_UNINSTALL)
+ return "pendingUninstall";
+ if (!addon.isActive &&
+ (addon.pendingOperations != AddonManager.PENDING_ENABLE &&
+ addon.pendingOperations != AddonManager.PENDING_INSTALL))
+ return "disabled";
+ if (addonType && (addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) &&
+ addon.userDisabled == AddonManager.STATE_ASK_TO_ACTIVATE)
+ return "askToActivate";
+ else
+ return "enabled";
+ }
+
+ return addon[aKey];
+ }
+
+ // aSortFuncs will hold the sorting functions that we'll
+ // use per element, in the correct order.
+ var aSortFuncs = [];
+
+ for (let i = 0; i < aSortBy.length; i++) {
+ var sortBy = aSortBy[i];
+
+ aSortFuncs[i] = stringCompare;
+
+ if (sortBy == "uiState")
+ aSortFuncs[i] = uiStateCompare;
+ else if (DATE_FIELDS.indexOf(sortBy) != -1)
+ aSortFuncs[i] = dateCompare;
+ else if (NUMERIC_FIELDS.indexOf(sortBy) != -1)
+ aSortFuncs[i] = numberCompare;
+ }
+
+
+ aElements.sort(function elementsSort(a, b) {
+ if (!aAscending)
+ [a, b] = [b, a];
+
+ for (let i = 0; i < aSortFuncs.length; i++) {
+ var sortBy = aSortBy[i];
+ var aValue = getValue(a, sortBy);
+ var bValue = getValue(b, sortBy);
+
+ if (!aValue && !bValue)
+ return 0;
+ if (!aValue)
+ return -1;
+ if (!bValue)
+ return 1;
+ if (aValue != bValue) {
+ var result = aSortFuncs[i](aValue, bValue);
+
+ if (result != 0)
+ return result;
+ }
+ }
+
+ // If we got here, then all values of a and b
+ // must have been equal.
+ return 0;
+
+ });
+}
+
+function sortList(aList, aSortBy, aAscending) {
+ var elements = Array.slice(aList.childNodes, 0);
+ sortElements(elements, [aSortBy], aAscending);
+
+ while (aList.listChild)
+ aList.removeChild(aList.lastChild);
+
+ for (let element of elements)
+ aList.appendChild(element);
+}
+
+function getAddonsAndInstalls(aType, aCallback) {
+ let addons = null, installs = null;
+ let types = (aType != null) ? [aType] : null;
+
+ AddonManager.getAddonsByTypes(types, function getAddonsAndInstalls_getAddonsByTypes(aAddonsList) {
+ addons = aAddonsList;
+ if (installs != null)
+ aCallback(addons, installs);
+ });
+
+ AddonManager.getInstallsByTypes(types, function getAddonsAndInstalls_getInstallsByTypes(aInstallsList) {
+ // skip over upgrade installs and non-active installs
+ installs = aInstallsList.filter(function installsFilter(aInstall) {
+ return !(aInstall.existingAddon ||
+ aInstall.state == AddonManager.STATE_AVAILABLE);
+ });
+
+ if (addons != null)
+ aCallback(addons, installs)
+ });
+}
+
+function doPendingUninstalls(aListBox) {
+ // Uninstalling add-ons can mutate the list so find the add-ons first then
+ // uninstall them
+ var items = [];
+ var listitem = aListBox.firstChild;
+ while (listitem) {
+ if (listitem.getAttribute("pending") == "uninstall" &&
+ !(listitem.opRequiresRestart("uninstall")))
+ items.push(listitem.mAddon);
+ listitem = listitem.nextSibling;
+ }
+
+ for (let addon of items)
+ addon.uninstall();
+}
+
+var gCategories = {
+ node: null,
+ _search: null,
+
+ initialize: function gCategories_initialize() {
+ this.node = document.getElementById("categories");
+ this._search = this.get("addons://search/");
+
+ var types = AddonManager.addonTypes;
+ for (var type in types)
+ this.onTypeAdded(types[type]);
+
+ AddonManager.addTypeListener(this);
+
+ try {
+ this.node.value = Services.prefs.getCharPref(PREF_UI_LASTCATEGORY);
+ } catch (e) { }
+
+ // If there was no last view or no existing category matched the last view
+ // then the list will default to selecting the search category and we never
+ // want to show that as the first view so switch to the default category
+ if (!this.node.selectedItem || this.node.selectedItem == this._search)
+ this.node.value = gViewDefault;
+
+ var self = this;
+ this.node.addEventListener("select", function node_onSelected() {
+ self.maybeHideSearch();
+ gViewController.loadView(self.node.selectedItem.value);
+ }, false);
+
+ this.node.addEventListener("click", function node_onClicked(aEvent) {
+ var selectedItem = self.node.selectedItem;
+ if (aEvent.target.localName == "richlistitem" &&
+ aEvent.target == selectedItem) {
+ var viewId = selectedItem.value;
+
+ if (gViewController.parseViewId(viewId).type == "search") {
+ viewId += encodeURIComponent(gHeader.searchQuery);
+ }
+
+ gViewController.loadView(viewId);
+ }
+ }, false);
+ },
+
+ shutdown: function gCategories_shutdown() {
+ AddonManager.removeTypeListener(this);
+ },
+
+ _insertCategory: function gCategories_insertCategory(aId, aName, aView, aPriority, aStartHidden) {
+ // If this category already exists then don't re-add it
+ if (document.getElementById("category-" + aId))
+ return;
+
+ var category = document.createElement("richlistitem");
+ category.setAttribute("id", "category-" + aId);
+ category.setAttribute("value", aView);
+ category.setAttribute("class", "category");
+ category.setAttribute("name", aName);
+ category.setAttribute("tooltiptext", aName);
+ category.setAttribute("priority", aPriority);
+ category.setAttribute("hidden", aStartHidden);
+
+ var node;
+ for (node of this.node.children) {
+ var nodePriority = parseInt(node.getAttribute("priority"));
+ // If the new type's priority is higher than this one then this is the
+ // insertion point
+ if (aPriority < nodePriority)
+ break;
+ // If the new type's priority is lower than this one then this is isn't
+ // the insertion point
+ if (aPriority > nodePriority)
+ continue;
+ // If the priorities are equal and the new type's name is earlier
+ // alphabetically then this is the insertion point
+ if (String.localeCompare(aName, node.getAttribute("name")) < 0)
+ break;
+ }
+
+ this.node.insertBefore(category, node);
+ },
+
+ _removeCategory: function gCategories_removeCategory(aId) {
+ var category = document.getElementById("category-" + aId);
+ if (!category)
+ return;
+
+ // If this category is currently selected then switch to the default view
+ if (this.node.selectedItem == category)
+ gViewController.replaceView(gViewDefault);
+
+ this.node.removeChild(category);
+ },
+
+ onTypeAdded: function gCategories_onTypeAdded(aType) {
+ // Ignore types that we don't have a view object for
+ if (!(aType.viewType in gViewController.viewObjects))
+ return;
+
+ var aViewId = "addons://" + aType.viewType + "/" + aType.id;
+
+ var startHidden = false;
+ if (aType.flags & AddonManager.TYPE_UI_HIDE_EMPTY) {
+ var prefName = PREF_UI_TYPE_HIDDEN.replace("%TYPE%", aType.id);
+ try {
+ startHidden = Services.prefs.getBoolPref(prefName);
+ }
+ catch (e) {
+ // Default to hidden
+ startHidden = true;
+ }
+
+ var self = this;
+ gPendingInitializations++;
+ getAddonsAndInstalls(aType.id, function onTypeAdded_getAddonsAndInstalls(aAddonsList, aInstallsList) {
+ var hidden = (aAddonsList.length == 0 && aInstallsList.length == 0);
+ var item = self.get(aViewId);
+
+ // Don't load view that is becoming hidden
+ if (hidden && aViewId == gViewController.currentViewId)
+ gViewController.loadView(gViewDefault);
+
+ item.hidden = hidden;
+ Services.prefs.setBoolPref(prefName, hidden);
+
+ if (aAddonsList.length > 0 || aInstallsList.length > 0) {
+ notifyInitialized();
+ return;
+ }
+
+ gEventManager.registerInstallListener({
+ onDownloadStarted: function gCategories_onDownloadStarted(aInstall) {
+ this._maybeShowCategory(aInstall);
+ },
+
+ onInstallStarted: function gCategories_onInstallStarted(aInstall) {
+ this._maybeShowCategory(aInstall);
+ },
+
+ onInstallEnded: function gCategories_onInstallEnded(aInstall, aAddon) {
+ this._maybeShowCategory(aAddon);
+ },
+
+ onExternalInstall: function gCategories_onExternalInstall(aAddon, aExistingAddon, aRequiresRestart) {
+ this._maybeShowCategory(aAddon);
+ },
+
+ _maybeShowCategory: function gCategories_maybeShowCategory(aAddon) {
+ if (aType.id == aAddon.type) {
+ self.get(aViewId).hidden = false;
+ Services.prefs.setBoolPref(prefName, false);
+ gEventManager.unregisterInstallListener(this);
+ }
+ }
+ });
+
+ notifyInitialized();
+ });
+ }
+
+ this._insertCategory(aType.id, aType.name, aViewId, aType.uiPriority,
+ startHidden);
+ },
+
+ onTypeRemoved: function gCategories_onTypeRemoved(aType) {
+ this._removeCategory(aType.id);
+ },
+
+ get selected() {
+ return this.node.selectedItem ? this.node.selectedItem.value : null;
+ },
+
+ select: function gCategories_select(aId, aPreviousView) {
+ var view = gViewController.parseViewId(aId);
+ if (view.type == "detail" && aPreviousView) {
+ aId = aPreviousView;
+ view = gViewController.parseViewId(aPreviousView);
+ }
+
+ Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, aId);
+
+ if (this.node.selectedItem &&
+ this.node.selectedItem.value == aId) {
+ this.node.selectedItem.hidden = false;
+ this.node.selectedItem.disabled = false;
+ return;
+ }
+
+ if (view.type == "search")
+ var item = this._search;
+ else
+ var item = this.get(aId);
+
+ if (item) {
+ item.hidden = false;
+ item.disabled = false;
+ this.node.suppressOnSelect = true;
+ this.node.selectedItem = item;
+ this.node.suppressOnSelect = false;
+ this.node.ensureElementIsVisible(item);
+
+ this.maybeHideSearch();
+ }
+ },
+
+ get: function gCategories_get(aId) {
+ var items = document.getElementsByAttribute("value", aId);
+ if (items.length)
+ return items[0];
+ return null;
+ },
+
+ setBadge: function gCategories_setBadge(aId, aCount) {
+ let item = this.get(aId);
+ if (item)
+ item.badgeCount = aCount;
+ },
+
+ maybeHideSearch: function gCategories_maybeHideSearch() {
+ var view = gViewController.parseViewId(this.node.selectedItem.value);
+ this._search.disabled = view.type != "search";
+ }
+};
+
+
+var gHeader = {
+ _search: null,
+ _dest: "",
+
+ initialize: function gHeader_initialize() {
+ this._search = document.getElementById("header-search");
+
+ this._search.addEventListener("command", function search_onCommand(aEvent) {
+ var query = aEvent.target.value;
+ if (query.length == 0)
+ return;
+
+ gViewController.loadView("addons://search/" + encodeURIComponent(query));
+ }, false);
+
+ function updateNavButtonVisibility() {
+ var shouldShow = gHeader.shouldShowNavButtons;
+ document.getElementById("back-btn").hidden = !shouldShow;
+ document.getElementById("forward-btn").hidden = !shouldShow;
+ }
+
+ window.addEventListener("focus", function window_onFocus(aEvent) {
+ if (aEvent.target == window)
+ updateNavButtonVisibility();
+ }, false);
+
+ updateNavButtonVisibility();
+ },
+
+ focusSearchBox: function gHeader_focusSearchBox() {
+ this._search.focus();
+ },
+
+ onKeyPress: function gHeader_onKeyPress(aEvent) {
+ if (String.fromCharCode(aEvent.charCode) == "/") {
+ this.focusSearchBox();
+ return;
+ }
+
+ // XXXunf Temporary until bug 371900 is fixed.
+ let key = document.getElementById("focusSearch").getAttribute("key");
+ let keyModifier = aEvent.ctrlKey;
+ if (String.fromCharCode(aEvent.charCode) == key && keyModifier) {
+ this.focusSearchBox();
+ return;
+ }
+ },
+
+ get shouldShowNavButtons() {
+ var docshellItem = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem);
+
+ // If there is no outer frame then make the buttons visible
+ if (docshellItem.rootTreeItem == docshellItem)
+ return true;
+
+ var outerWin = docshellItem.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ var outerDoc = outerWin.document;
+ var node = outerDoc.getElementById("back-button");
+ // If the outer frame has no back-button then make the buttons visible
+ if (!node)
+ return true;
+
+ // If the back-button or any of its parents are hidden then make the buttons
+ // visible
+ while (node != outerDoc) {
+ var style = outerWin.getComputedStyle(node, "");
+ if (style.display == "none")
+ return true;
+ if (style.visibility != "visible")
+ return true;
+ node = node.parentNode;
+ }
+
+ return false;
+ },
+
+ get searchQuery() {
+ return this._search.value;
+ },
+
+ set searchQuery(aQuery) {
+ this._search.value = aQuery;
+ },
+};
+
+
+var gDiscoverView = {
+ node: null,
+ enabled: true,
+ // Set to true after the view is first shown. If initialization completes
+ // after this then it must also load the discover homepage
+ loaded: false,
+ _browser: null,
+ _loading: null,
+ _error: null,
+ homepageURL: null,
+ _loadListeners: [],
+
+ initialize: function gDiscoverView_initialize() {
+ this.enabled = isDiscoverEnabled();
+ if (!this.enabled) {
+ gCategories.get("addons://discover/").hidden = true;
+ return;
+ }
+
+ this.node = document.getElementById("discover-view");
+ this._loading = document.getElementById("discover-loading");
+ this._error = document.getElementById("discover-error");
+ this._browser = document.getElementById("discover-browser");
+
+ let compatMode = "normal";
+ if (!AddonManager.checkCompatibility)
+ compatMode = "ignore";
+ else if (AddonManager.strictCompatibility)
+ compatMode = "strict";
+
+ var url = Services.prefs.getCharPref(PREF_DISCOVERURL);
+ url = url.replace("%COMPATIBILITY_MODE%", compatMode);
+ url = Services.urlFormatter.formatURL(url);
+
+ var self = this;
+
+ function setURL(aURL) {
+ try {
+ self.homepageURL = Services.io.newURI(aURL, null, null);
+ } catch (e) {
+ self.showError();
+ notifyInitialized();
+ return;
+ }
+
+ self._browser.homePage = self.homepageURL.spec;
+ self._browser.addProgressListener(self);
+
+ if (self.loaded)
+ self._loadURL(self.homepageURL.spec, false, notifyInitialized);
+ else
+ notifyInitialized();
+ }
+
+ if (Services.prefs.getBoolPref(PREF_GETADDONS_CACHE_ENABLED) == false) {
+ setURL(url);
+ return;
+ }
+
+ gPendingInitializations++;
+ AddonManager.getAllAddons(function initialize_getAllAddons(aAddons) {
+ var list = {};
+ for (let addon of aAddons) {
+ var prefName = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%",
+ addon.id);
+ try {
+ if (!Services.prefs.getBoolPref(prefName))
+ continue;
+ } catch (e) { }
+ list[addon.id] = {
+ name: addon.name,
+ version: addon.version,
+ type: addon.type,
+ userDisabled: addon.userDisabled,
+ isCompatible: addon.isCompatible,
+ isBlocklisted: addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED
+ }
+ }
+
+ setURL(url + "#" + JSON.stringify(list));
+ });
+ },
+
+ destroy: function gDiscoverView_destroy() {
+ try {
+ this._browser.removeProgressListener(this);
+ }
+ catch (e) {
+ // Ignore the case when the listener wasn't already registered
+ }
+ },
+
+ show: function gDiscoverView_show(aParam, aRequest, aState, aIsRefresh) {
+ gViewController.updateCommands();
+
+ // If we're being told to load a specific URL then just do that
+ if (aState && "url" in aState) {
+ this.loaded = true;
+ this._loadURL(aState.url);
+ }
+
+ // If the view has loaded before and still at the homepage (if refreshing),
+ // and the error page is not visible then there is nothing else to do
+ if (this.loaded && this.node.selectedPanel != this._error &&
+ (!aIsRefresh || (this._browser.currentURI &&
+ this._browser.currentURI.spec == this._browser.homePage))) {
+ gViewController.notifyViewChanged();
+ return;
+ }
+
+ this.loaded = true;
+
+ // No homepage means initialization isn't complete, the browser will get
+ // loaded once initialization is complete
+ if (!this.homepageURL) {
+ this._loadListeners.push(gViewController.notifyViewChanged.bind(gViewController));
+ return;
+ }
+
+ this._loadURL(this.homepageURL.spec, aIsRefresh,
+ gViewController.notifyViewChanged.bind(gViewController));
+ },
+
+ canRefresh: function gDiscoverView_canRefresh() {
+ if (this._browser.currentURI &&
+ this._browser.currentURI.spec == this._browser.homePage)
+ return false;
+ return true;
+ },
+
+ refresh: function gDiscoverView_refresh(aParam, aRequest, aState) {
+ this.show(aParam, aRequest, aState, true);
+ },
+
+ hide: function gDiscoverView_hide() { },
+
+ showError: function gDiscoverView_showError() {
+ this.node.selectedPanel = this._error;
+ },
+
+ _loadURL: function gDiscoverView_loadURL(aURL, aKeepHistory, aCallback) {
+ if (this._browser.currentURI.spec == aURL) {
+ if (aCallback)
+ aCallback();
+ return;
+ }
+
+ if (aCallback)
+ this._loadListeners.push(aCallback);
+
+ var flags = 0;
+ if (!aKeepHistory)
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+
+ this._browser.loadURIWithFlags(aURL, flags);
+ },
+
+ onLocationChange: function gDiscoverView_onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // Ignore the about:blank load
+ if (aLocation.spec == "about:blank")
+ return;
+
+ // When using the real session history the inner-frame will update the
+ // session history automatically, if using the fake history though it must
+ // be manually updated
+ if (gHistory == FakeHistory) {
+ var docshell = aWebProgress.QueryInterface(Ci.nsIDocShell);
+
+ var state = {
+ view: "addons://discover/",
+ url: aLocation.spec
+ };
+
+ var replaceHistory = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY << 16;
+ if (docshell.loadType & replaceHistory)
+ gHistory.replaceState(state);
+ else
+ gHistory.pushState(state);
+ gViewController.lastHistoryIndex = gHistory.index;
+ }
+
+ gViewController.updateCommands();
+
+ // If the hostname is the same as the new location's host and either the
+ // default scheme is insecure or the new location is secure then continue
+ // with the load
+ if (aLocation.host == this.homepageURL.host &&
+ (!this.homepageURL.schemeIs("https") || aLocation.schemeIs("https")))
+ return;
+
+ // Canceling the request will send an error to onStateChange which will show
+ // the error page
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ },
+
+ onSecurityChange: function gDiscoverView_onSecurityChange(aWebProgress, aRequest, aState) {
+ // Don't care about security if the page is not https
+ if (!this.homepageURL.schemeIs("https"))
+ return;
+
+ // If the request was secure then it is ok
+ if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE)
+ return;
+
+ // Canceling the request will send an error to onStateChange which will show
+ // the error page
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ },
+
+ onStateChange: function gDiscoverView_onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ let transferStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
+ Ci.nsIWebProgressListener.STATE_IS_REQUEST |
+ Ci.nsIWebProgressListener.STATE_TRANSFERRING;
+ // Once transferring begins show the content
+ if ((aStateFlags & transferStart) === transferStart)
+ this.node.selectedPanel = this._browser;
+
+ // Only care about the network events
+ if (!(aStateFlags & (Ci.nsIWebProgressListener.STATE_IS_NETWORK)))
+ return;
+
+ // If this is the start of network activity then show the loading page
+ if (aStateFlags & (Ci.nsIWebProgressListener.STATE_START))
+ this.node.selectedPanel = this._loading;
+
+ // Ignore anything except stop events
+ if (!(aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP)))
+ return;
+
+ // Consider the successful load of about:blank as still loading
+ if (aRequest instanceof Ci.nsIChannel && aRequest.URI.spec == "about:blank")
+ return;
+
+ // If there was an error loading the page or the new hostname is not the
+ // same as the default hostname or the default scheme is secure and the new
+ // scheme is insecure then show the error page
+ const NS_ERROR_PARSED_DATA_CACHED = 0x805D0021;
+ if (!(Components.isSuccessCode(aStatus) || aStatus == NS_ERROR_PARSED_DATA_CACHED) ||
+ (aRequest && aRequest instanceof Ci.nsIHttpChannel && !aRequest.requestSucceeded)) {
+ this.showError();
+ } else {
+ // Got a successful load, make sure the browser is visible
+ this.node.selectedPanel = this._browser;
+ gViewController.updateCommands();
+ }
+
+ var listeners = this._loadListeners;
+ this._loadListeners = [];
+
+ for (let listener of listeners)
+ listener();
+ },
+
+ onProgressChange: function gDiscoverView_onProgressChange() { },
+ onStatusChange: function gDiscoverView_onStatusChange() { },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference]),
+
+ getSelectedAddon: function gDiscoverView_getSelectedAddon() null
+};
+
+
+var gCachedAddons = {};
+
+var gSearchView = {
+ node: null,
+ _filter: null,
+ _sorters: null,
+ _loading: null,
+ _listBox: null,
+ _emptyNotice: null,
+ _allResultsLink: null,
+ _lastQuery: null,
+ _lastRemoteTotal: 0,
+ _pendingSearches: 0,
+
+ initialize: function gSearchView_initialize() {
+ this.node = document.getElementById("search-view");
+ this._filter = document.getElementById("search-filter-radiogroup");
+ this._sorters = document.getElementById("search-sorters");
+ this._sorters.handler = this;
+ this._loading = document.getElementById("search-loading");
+ this._listBox = document.getElementById("search-list");
+ this._emptyNotice = document.getElementById("search-list-empty");
+ this._allResultsLink = document.getElementById("search-allresults-link");
+
+ if (!AddonManager.isInstallEnabled("application/x-xpinstall"))
+ this._filter.hidden = true;
+
+ var self = this;
+ this._listBox.addEventListener("keydown", function listbox_onKeydown(aEvent) {
+ if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
+ var item = self._listBox.selectedItem;
+ if (item)
+ item.showInDetailView();
+ }
+ }, false);
+
+ this._filter.addEventListener("command", function filter_onCommand() self.updateView(), false);
+ },
+
+ shutdown: function gSearchView_shutdown() {
+ if (AddonRepository.isSearching)
+ AddonRepository.cancelSearch();
+ },
+
+ get isSearching() {
+ return this._pendingSearches > 0;
+ },
+
+ show: function gSearchView_show(aQuery, aRequest) {
+ gEventManager.registerInstallListener(this);
+
+ this.showEmptyNotice(false);
+ this.showAllResultsLink(0);
+ this.showLoading(true);
+ this._sorters.showprice = false;
+
+ gHeader.searchQuery = aQuery;
+ aQuery = aQuery.trim().toLocaleLowerCase();
+ if (this._lastQuery == aQuery) {
+ this.updateView();
+ gViewController.notifyViewChanged();
+ return;
+ }
+ this._lastQuery = aQuery;
+
+ if (AddonRepository.isSearching)
+ AddonRepository.cancelSearch();
+
+ while (this._listBox.firstChild.localName == "richlistitem")
+ this._listBox.removeChild(this._listBox.firstChild);
+
+ var self = this;
+ gCachedAddons = {};
+ this._pendingSearches = 2;
+ this._sorters.setSort("relevancescore", false);
+
+ var elements = [];
+
+ function createSearchResults(aObjsList, aIsInstall, aIsRemote) {
+ for (let index in aObjsList) {
+ let obj = aObjsList[index];
+ let score = aObjsList.length - index;
+ if (!aIsRemote && aQuery.length > 0) {
+ score = self.getMatchScore(obj, aQuery);
+ if (score == 0)
+ continue;
+ }
+
+ let item = createItem(obj, aIsInstall, aIsRemote);
+ item.setAttribute("relevancescore", score);
+ if (aIsRemote) {
+ gCachedAddons[obj.id] = obj;
+ if (obj.purchaseURL)
+ self._sorters.showprice = true;
+ }
+
+ elements.push(item);
+ }
+ }
+
+ function finishSearch(createdCount) {
+ if (elements.length > 0) {
+ sortElements(elements, [self._sorters.sortBy], self._sorters.ascending);
+ for (let element of elements)
+ self._listBox.insertBefore(element, self._listBox.lastChild);
+ self.updateListAttributes();
+ }
+
+ self._pendingSearches--;
+ self.updateView();
+
+ if (!self.isSearching)
+ gViewController.notifyViewChanged();
+ }
+
+ getAddonsAndInstalls(null, function show_getAddonsAndInstalls(aAddons, aInstalls) {
+ if (gViewController && aRequest != gViewController.currentViewRequest)
+ return;
+
+ createSearchResults(aAddons, false, false);
+ createSearchResults(aInstalls, true, false);
+ finishSearch();
+ });
+
+ var maxRemoteResults = 0;
+ try {
+ maxRemoteResults = Services.prefs.getIntPref(PREF_MAXRESULTS);
+ } catch(e) {}
+
+ if (maxRemoteResults <= 0) {
+ finishSearch(0);
+ return;
+ }
+
+ AddonRepository.searchAddons(aQuery, maxRemoteResults, {
+ searchFailed: function show_SearchFailed() {
+ if (gViewController && aRequest != gViewController.currentViewRequest)
+ return;
+
+ self._lastRemoteTotal = 0;
+
+ // XXXunf Better handling of AMO search failure. See bug 579502
+ finishSearch(0); // Silently fail
+ },
+
+ searchSucceeded: function show_SearchSucceeded(aAddonsList, aAddonCount, aTotalResults) {
+ if (gViewController && aRequest != gViewController.currentViewRequest)
+ return;
+
+ if (aTotalResults > maxRemoteResults)
+ self._lastRemoteTotal = aTotalResults;
+ else
+ self._lastRemoteTotal = 0;
+
+ var createdCount = createSearchResults(aAddonsList, false, true);
+ finishSearch(createdCount);
+ }
+ });
+ },
+
+ showLoading: function gSearchView_showLoading(aLoading) {
+ this._loading.hidden = !aLoading;
+ this._listBox.hidden = aLoading;
+ },
+
+ updateView: function gSearchView_updateView() {
+ var showLocal = this._filter.value == "local";
+
+ if (!showLocal && !AddonManager.isInstallEnabled("application/x-xpinstall"))
+ showLocal = true;
+
+ this._listBox.setAttribute("local", showLocal);
+ this._listBox.setAttribute("remote", !showLocal);
+
+ this.showLoading(this.isSearching && !showLocal);
+ if (!this.isSearching) {
+ var isEmpty = true;
+ var results = this._listBox.getElementsByTagName("richlistitem");
+ for (let result of results) {
+ var isRemote = (result.getAttribute("remote") == "true");
+ if ((isRemote && !showLocal) || (!isRemote && showLocal)) {
+ isEmpty = false;
+ break;
+ }
+ }
+
+ this.showEmptyNotice(isEmpty);
+ this.showAllResultsLink(this._lastRemoteTotal);
+ }
+
+ gViewController.updateCommands();
+ },
+
+ hide: function gSearchView_hide() {
+ gEventManager.unregisterInstallListener(this);
+ doPendingUninstalls(this._listBox);
+ },
+
+ getMatchScore: function gSearchView_getMatchScore(aObj, aQuery) {
+ var score = 0;
+ score += this.calculateMatchScore(aObj.name, aQuery,
+ SEARCH_SCORE_MULTIPLIER_NAME);
+ score += this.calculateMatchScore(aObj.description, aQuery,
+ SEARCH_SCORE_MULTIPLIER_DESCRIPTION);
+ return score;
+ },
+
+ calculateMatchScore: function gSearchView_calculateMatchScore(aStr, aQuery, aMultiplier) {
+ var score = 0;
+ if (!aStr || aQuery.length == 0)
+ return score;
+
+ aStr = aStr.trim().toLocaleLowerCase();
+ var haystack = aStr.split(/\s+/);
+ var needles = aQuery.split(/\s+/);
+
+ for (let needle of needles) {
+ for (let hay of haystack) {
+ if (hay == needle) {
+ // matching whole words is best
+ score += SEARCH_SCORE_MATCH_WHOLEWORD;
+ } else {
+ let i = hay.indexOf(needle);
+ if (i == 0) // matching on word boundries is also good
+ score += SEARCH_SCORE_MATCH_WORDBOUNDRY;
+ else if (i > 0) // substring matches not so good
+ score += SEARCH_SCORE_MATCH_SUBSTRING;
+ }
+ }
+ }
+
+ // give progressively higher score for longer queries, since longer queries
+ // are more likely to be unique and therefore more relevant.
+ if (needles.length > 1 && aStr.indexOf(aQuery) != -1)
+ score += needles.length;
+
+ return score * aMultiplier;
+ },
+
+ showEmptyNotice: function gSearchView_showEmptyNotice(aShow) {
+ this._emptyNotice.hidden = !aShow;
+ this._listBox.hidden = aShow;
+ },
+
+ showAllResultsLink: function gSearchView_showAllResultsLink(aTotalResults) {
+ if (aTotalResults == 0) {
+ this._allResultsLink.hidden = true;
+ return;
+ }
+
+ var linkStr = gStrings.ext.GetStringFromName("showAllSearchResults");
+ linkStr = PluralForm.get(aTotalResults, linkStr);
+ linkStr = linkStr.replace("#1", aTotalResults);
+ this._allResultsLink.setAttribute("value", linkStr);
+
+ this._allResultsLink.setAttribute("href",
+ AddonRepository.getSearchURL(this._lastQuery));
+ this._allResultsLink.hidden = false;
+ },
+
+ updateListAttributes: function gSearchView_updateListAttributes() {
+ var item = this._listBox.querySelector("richlistitem[remote='true'][first]");
+ if (item)
+ item.removeAttribute("first");
+ item = this._listBox.querySelector("richlistitem[remote='true'][last]");
+ if (item)
+ item.removeAttribute("last");
+ var items = this._listBox.querySelectorAll("richlistitem[remote='true']");
+ if (items.length > 0) {
+ items[0].setAttribute("first", true);
+ items[items.length - 1].setAttribute("last", true);
+ }
+
+ item = this._listBox.querySelector("richlistitem:not([remote='true'])[first]");
+ if (item)
+ item.removeAttribute("first");
+ item = this._listBox.querySelector("richlistitem:not([remote='true'])[last]");
+ if (item)
+ item.removeAttribute("last");
+ items = this._listBox.querySelectorAll("richlistitem:not([remote='true'])");
+ if (items.length > 0) {
+ items[0].setAttribute("first", true);
+ items[items.length - 1].setAttribute("last", true);
+ }
+
+ },
+
+ onSortChanged: function gSearchView_onSortChanged(aSortBy, aAscending) {
+ var footer = this._listBox.lastChild;
+ this._listBox.removeChild(footer);
+
+ sortList(this._listBox, aSortBy, aAscending);
+ this.updateListAttributes();
+
+ this._listBox.appendChild(footer);
+ },
+
+ onDownloadCancelled: function gSearchView_onDownloadCancelled(aInstall) {
+ this.removeInstall(aInstall);
+ },
+
+ onInstallCancelled: function gSearchView_onInstallCancelled(aInstall) {
+ this.removeInstall(aInstall);
+ },
+
+ removeInstall: function gSearchView_removeInstall(aInstall) {
+ for (let item of this._listBox.childNodes) {
+ if (item.mInstall == aInstall) {
+ this._listBox.removeChild(item);
+ return;
+ }
+ }
+ },
+
+ getSelectedAddon: function gSearchView_getSelectedAddon() {
+ var item = this._listBox.selectedItem;
+ if (item)
+ return item.mAddon;
+ return null;
+ },
+
+ getListItemForID: function gSearchView_getListItemForID(aId) {
+ var listitem = this._listBox.firstChild;
+ while (listitem) {
+ if (listitem.getAttribute("status") == "installed" && listitem.mAddon.id == aId)
+ return listitem;
+ listitem = listitem.nextSibling;
+ }
+ return null;
+ }
+};
+
+
+var gListView = {
+ node: null,
+ _listBox: null,
+ _emptyNotice: null,
+ _type: null,
+
+ initialize: function gListView_initialize() {
+ this.node = document.getElementById("list-view");
+ this._listBox = document.getElementById("addon-list");
+ this._emptyNotice = document.getElementById("addon-list-empty");
+
+ var self = this;
+ this._listBox.addEventListener("keydown", function listbox_onKeydown(aEvent) {
+ if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
+ var item = self._listBox.selectedItem;
+ if (item)
+ item.showInDetailView();
+ }
+ }, false);
+ },
+
+ show: function gListView_show(aType, aRequest) {
+ if (!(aType in AddonManager.addonTypes))
+ throw Components.Exception("Attempting to show unknown type " + aType, Cr.NS_ERROR_INVALID_ARG);
+
+ this._type = aType;
+ this.node.setAttribute("type", aType);
+ this.showEmptyNotice(false);
+
+ while (this._listBox.itemCount > 0)
+ this._listBox.removeItemAt(0);
+
+ if (aType == "plugin") {
+ navigator.plugins.refresh(false);
+ }
+
+ var self = this;
+ getAddonsAndInstalls(aType, function show_getAddonsAndInstalls(aAddonsList, aInstallsList) {
+ if (gViewController && aRequest != gViewController.currentViewRequest)
+ return;
+
+ var elements = [];
+
+ for (let addonItem of aAddonsList)
+ elements.push(createItem(addonItem));
+
+ for (let installItem of aInstallsList)
+ elements.push(createItem(installItem, true));
+
+ self.showEmptyNotice(elements.length == 0);
+ if (elements.length > 0) {
+ sortElements(elements, ["uiState", "name"], true);
+ for (let element of elements)
+ self._listBox.appendChild(element);
+ }
+
+ gEventManager.registerInstallListener(self);
+ gViewController.updateCommands();
+ gViewController.notifyViewChanged();
+ });
+ },
+
+ hide: function gListView_hide() {
+ gEventManager.unregisterInstallListener(this);
+ doPendingUninstalls(this._listBox);
+ },
+
+ showEmptyNotice: function gListView_showEmptyNotice(aShow) {
+ this._emptyNotice.hidden = !aShow;
+ },
+
+ onSortChanged: function gListView_onSortChanged(aSortBy, aAscending) {
+ sortList(this._listBox, aSortBy, aAscending);
+ },
+
+ onExternalInstall: function gListView_onExternalInstall(aAddon, aExistingAddon, aRequiresRestart) {
+ // The existing list item will take care of upgrade installs
+ if (aExistingAddon)
+ return;
+
+ this.addItem(aAddon);
+ },
+
+ onDownloadStarted: function gListView_onDownloadStarted(aInstall) {
+ this.addItem(aInstall, true);
+ },
+
+ onInstallStarted: function gListView_onInstallStarted(aInstall) {
+ this.addItem(aInstall, true);
+ },
+
+ onDownloadCancelled: function gListView_onDownloadCancelled(aInstall) {
+ this.removeItem(aInstall, true);
+ },
+
+ onInstallCancelled: function gListView_onInstallCancelled(aInstall) {
+ this.removeItem(aInstall, true);
+ },
+
+ onInstallEnded: function gListView_onInstallEnded(aInstall) {
+ // Remove any install entries for upgrades, their status will appear against
+ // the existing item
+ if (aInstall.existingAddon)
+ this.removeItem(aInstall, true);
+ },
+
+ addItem: function gListView_addItem(aObj, aIsInstall) {
+ if (aObj.type != this._type)
+ return;
+
+ if (aIsInstall && aObj.existingAddon)
+ return;
+
+ let prop = aIsInstall ? "mInstall" : "mAddon";
+ for (let item of this._listBox.childNodes) {
+ if (item[prop] == aObj)
+ return;
+ }
+
+ let item = createItem(aObj, aIsInstall);
+ this._listBox.insertBefore(item, this._listBox.firstChild);
+ this.showEmptyNotice(false);
+ },
+
+ removeItem: function gListView_removeItem(aObj, aIsInstall) {
+ let prop = aIsInstall ? "mInstall" : "mAddon";
+
+ for (let item of this._listBox.childNodes) {
+ if (item[prop] == aObj) {
+ this._listBox.removeChild(item);
+ this.showEmptyNotice(this._listBox.itemCount == 0);
+ return;
+ }
+ }
+ },
+
+ getSelectedAddon: function gListView_getSelectedAddon() {
+ var item = this._listBox.selectedItem;
+ if (item)
+ return item.mAddon;
+ return null;
+ },
+
+ getListItemForID: function gListView_getListItemForID(aId) {
+ var listitem = this._listBox.firstChild;
+ while (listitem) {
+ if (listitem.getAttribute("status") == "installed" && listitem.mAddon.id == aId)
+ return listitem;
+ listitem = listitem.nextSibling;
+ }
+ return null;
+ }
+};
+
+
+var gDetailView = {
+ node: null,
+ _addon: null,
+ _loadingTimer: null,
+ _autoUpdate: null,
+
+ initialize: function gDetailView_initialize() {
+ this.node = document.getElementById("detail-view");
+
+ this._autoUpdate = document.getElementById("detail-autoUpdate");
+
+ var self = this;
+ this._autoUpdate.addEventListener("command", function autoUpdate_onCommand() {
+ self._addon.applyBackgroundUpdates = self._autoUpdate.value;
+ }, true);
+ },
+
+ shutdown: function gDetailView_shutdown() {
+ AddonManager.removeManagerListener(this);
+ },
+
+ onUpdateModeChanged: function gDetailView_onUpdateModeChanged() {
+ this.onPropertyChanged(["applyBackgroundUpdates"]);
+ },
+
+ _updateView: function gDetailView_updateView(aAddon, aIsRemote, aScrollToPreferences) {
+ AddonManager.addManagerListener(this);
+ this.clearLoading();
+
+ this._addon = aAddon;
+ gEventManager.registerAddonListener(this, aAddon.id);
+ gEventManager.registerInstallListener(this);
+
+ this.node.setAttribute("type", aAddon.type);
+
+ // If the search category isn't selected then make sure to select the
+ // correct category
+ if (gCategories.selected != "addons://search/")
+ gCategories.select("addons://list/" + aAddon.type);
+
+ document.getElementById("detail-name").textContent = aAddon.name;
+ var icon = aAddon.icon64URL ? aAddon.icon64URL : aAddon.iconURL;
+ document.getElementById("detail-icon").src = icon ? icon : "";
+ document.getElementById("detail-creator").setCreator(aAddon.creator, aAddon.homepageURL);
+ document.getElementById("detail-translators").setTranslators(aAddon.translators, aAddon.type);
+
+ var version = document.getElementById("detail-version");
+ if (shouldShowVersionNumber(aAddon)) {
+ version.hidden = false;
+ version.value = aAddon.version;
+ } else {
+ version.hidden = true;
+ }
+
+ var screenshot = document.getElementById("detail-screenshot");
+ if (aAddon.screenshots && aAddon.screenshots.length > 0) {
+ if (aAddon.screenshots[0].thumbnailURL) {
+ screenshot.src = aAddon.screenshots[0].thumbnailURL;
+ screenshot.width = aAddon.screenshots[0].thumbnailWidth;
+ screenshot.height = aAddon.screenshots[0].thumbnailHeight;
+ } else {
+ screenshot.src = aAddon.screenshots[0].url;
+ screenshot.width = aAddon.screenshots[0].width;
+ screenshot.height = aAddon.screenshots[0].height;
+ }
+ screenshot.setAttribute("loading", "true");
+ screenshot.hidden = false;
+ } else {
+ screenshot.hidden = true;
+ }
+
+ var desc = document.getElementById("detail-desc");
+ desc.textContent = aAddon.description;
+
+ var fullDesc = document.getElementById("detail-fulldesc");
+ if (aAddon.fullDescription) {
+ // The following is part of an awful hack to include the licenses for GMP
+ // plugins without having bug 624602 fixed yet, and intentionally ignores
+ // localisation.
+ if (aAddon.isGMPlugin) {
+ fullDesc.innerHTML = aAddon.fullDescription;
+ } else {
+ fullDesc.textContent = aAddon.fullDescription;
+ }
+
+ fullDesc.hidden = false;
+ } else {
+ fullDesc.hidden = true;
+ }
+
+ var contributions = document.getElementById("detail-contributions");
+ if ("contributionURL" in aAddon && aAddon.contributionURL) {
+ contributions.hidden = false;
+ var amount = document.getElementById("detail-contrib-suggested");
+ if (aAddon.contributionAmount) {
+ amount.value = gStrings.ext.formatStringFromName("contributionAmount2",
+ [aAddon.contributionAmount],
+ 1);
+ amount.hidden = false;
+ } else {
+ amount.hidden = true;
+ }
+ } else {
+ contributions.hidden = true;
+ }
+
+ if ("purchaseURL" in aAddon && aAddon.purchaseURL) {
+ var purchase = document.getElementById("detail-purchase-btn");
+ purchase.label = gStrings.ext.formatStringFromName("cmd.purchaseAddon.label",
+ [aAddon.purchaseDisplayAmount],
+ 1);
+ purchase.accesskey = gStrings.ext.GetStringFromName("cmd.purchaseAddon.accesskey");
+ }
+
+ var updateDateRow = document.getElementById("detail-dateUpdated");
+ if (aAddon.updateDate) {
+ var date = formatDate(aAddon.updateDate);
+ updateDateRow.value = date;
+ } else {
+ updateDateRow.value = null;
+ }
+
+ // TODO if the add-on was downloaded from releases.mozilla.org link to the
+ // AMO profile (bug 590344)
+ if (false) {
+ document.getElementById("detail-repository-row").hidden = false;
+ document.getElementById("detail-homepage-row").hidden = true;
+ var repository = document.getElementById("detail-repository");
+ repository.value = aAddon.homepageURL;
+ repository.href = aAddon.homepageURL;
+ } else if (aAddon.homepageURL) {
+ document.getElementById("detail-repository-row").hidden = true;
+ document.getElementById("detail-homepage-row").hidden = false;
+ var homepage = document.getElementById("detail-homepage");
+ homepage.value = aAddon.homepageURL;
+ homepage.href = aAddon.homepageURL;
+ } else {
+ document.getElementById("detail-repository-row").hidden = true;
+ document.getElementById("detail-homepage-row").hidden = true;
+ }
+
+ var rating = document.getElementById("detail-rating");
+ if (aAddon.averageRating) {
+ rating.averageRating = aAddon.averageRating;
+ rating.hidden = false;
+ } else {
+ rating.hidden = true;
+ }
+
+ var reviews = document.getElementById("detail-reviews");
+ if (aAddon.reviewURL) {
+ var text = gStrings.ext.GetStringFromName("numReviews");
+ text = PluralForm.get(aAddon.reviewCount, text)
+ text = text.replace("#1", aAddon.reviewCount);
+ reviews.value = text;
+ reviews.hidden = false;
+ reviews.href = aAddon.reviewURL;
+ } else {
+ reviews.hidden = true;
+ }
+
+ document.getElementById("detail-rating-row").hidden = !aAddon.averageRating && !aAddon.reviewURL;
+
+ var sizeRow = document.getElementById("detail-size");
+ if (aAddon.size && aIsRemote) {
+ let [size, unit] = DownloadUtils.convertByteUnits(parseInt(aAddon.size));
+ let formatted = gStrings.dl.GetStringFromName("doneSize");
+ formatted = formatted.replace("#1", size).replace("#2", unit);
+ sizeRow.value = formatted;
+ } else {
+ sizeRow.value = null;
+ }
+
+ var downloadsRow = document.getElementById("detail-downloads");
+ if (aAddon.totalDownloads && aIsRemote) {
+ var downloads = aAddon.totalDownloads;
+ downloadsRow.value = downloads;
+ } else {
+ downloadsRow.value = null;
+ }
+
+ var canUpdate = !aIsRemote && hasPermission(aAddon, "upgrade");
+ document.getElementById("detail-updates-row").hidden = !canUpdate;
+
+ if ("applyBackgroundUpdates" in aAddon) {
+ this._autoUpdate.hidden = false;
+ this._autoUpdate.value = aAddon.applyBackgroundUpdates;
+ let hideFindUpdates = AddonManager.shouldAutoUpdate(this._addon);
+ document.getElementById("detail-findUpdates-btn").hidden = hideFindUpdates;
+ } else {
+ this._autoUpdate.hidden = true;
+ document.getElementById("detail-findUpdates-btn").hidden = false;
+ }
+
+ document.getElementById("detail-prefs-btn").hidden = !aIsRemote &&
+ !gViewController.commands.cmd_showItemPreferences.isEnabled(aAddon);
+
+ var gridRows = document.querySelectorAll("#detail-grid rows row");
+ let first = true;
+ for (let gridRow of gridRows) {
+ if (first && window.getComputedStyle(gridRow, null).getPropertyValue("display") != "none") {
+ gridRow.setAttribute("first-row", true);
+ first = false;
+ } else {
+ gridRow.removeAttribute("first-row");
+ }
+ }
+
+ this.fillSettingsRows(aScrollToPreferences, (function updateView_fillSettingsRows() {
+ this.updateState();
+ gViewController.notifyViewChanged();
+ }).bind(this));
+ },
+
+ show: function gDetailView_show(aAddonId, aRequest) {
+ let index = aAddonId.indexOf("/preferences");
+ let scrollToPreferences = false;
+ if (index >= 0) {
+ aAddonId = aAddonId.substring(0, index);
+ scrollToPreferences = true;
+ }
+
+ var self = this;
+ this._loadingTimer = setTimeout(function loadTimeOutTimer() {
+ self.node.setAttribute("loading-extended", true);
+ }, LOADING_MSG_DELAY);
+
+ var view = gViewController.currentViewId;
+
+ AddonManager.getAddonByID(aAddonId, function show_getAddonByID(aAddon) {
+ if (gViewController && aRequest != gViewController.currentViewRequest)
+ return;
+
+ if (aAddon) {
+ self._updateView(aAddon, false, scrollToPreferences);
+ return;
+ }
+
+ // Look for an add-on pending install
+ AddonManager.getAllInstalls(function show_getAllInstalls(aInstalls) {
+ for (let install of aInstalls) {
+ if (install.state == AddonManager.STATE_INSTALLED &&
+ install.addon.id == aAddonId) {
+ self._updateView(install.addon, false);
+ return;
+ }
+ }
+
+ if (aAddonId in gCachedAddons) {
+ self._updateView(gCachedAddons[aAddonId], true);
+ return;
+ }
+
+ // This might happen due to session restore restoring us back to an
+ // add-on that doesn't exist but otherwise shouldn't normally happen.
+ // Either way just revert to the default view.
+ gViewController.replaceView(gViewDefault);
+ });
+ });
+ },
+
+ hide: function gDetailView_hide() {
+ AddonManager.removeManagerListener(this);
+ this.clearLoading();
+ if (this._addon) {
+ if (hasInlineOptions(this._addon)) {
+ Services.obs.notifyObservers(document,
+ AddonManager.OPTIONS_NOTIFICATION_HIDDEN,
+ this._addon.id);
+ }
+
+ gEventManager.unregisterAddonListener(this, this._addon.id);
+ gEventManager.unregisterInstallListener(this);
+ this._addon = null;
+
+ // Flush the preferences to disk so they survive any crash
+ if (this.node.getElementsByTagName("setting").length)
+ Services.prefs.savePrefFile(null);
+ }
+ },
+
+ updateState: function gDetailView_updateState() {
+ gViewController.updateCommands();
+
+ var pending = this._addon.pendingOperations;
+ if (pending != AddonManager.PENDING_NONE) {
+ this.node.removeAttribute("notification");
+
+ var pending = null;
+ const PENDING_OPERATIONS = ["enable", "disable", "install", "uninstall",
+ "upgrade"];
+ for (let op of PENDING_OPERATIONS) {
+ if (isPending(this._addon, op))
+ pending = op;
+ }
+
+ this.node.setAttribute("pending", pending);
+ document.getElementById("detail-pending").textContent = gStrings.ext.formatStringFromName(
+ "details.notification." + pending,
+ [this._addon.name, gStrings.brandShortName], 2
+ );
+ } else {
+ this.node.removeAttribute("pending");
+
+ if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ this.node.setAttribute("notification", "error");
+ document.getElementById("detail-error").textContent = gStrings.ext.formatStringFromName(
+ "details.notification.blocked",
+ [this._addon.name], 1
+ );
+ var errorLink = document.getElementById("detail-error-link");
+ errorLink.value = gStrings.ext.GetStringFromName("details.notification.blocked.link");
+ errorLink.href = this._addon.blocklistURL;
+ errorLink.hidden = false;
+ } else if (!this._addon.isCompatible && (AddonManager.checkCompatibility ||
+ (this._addon.blocklistState != Ci.nsIBlocklistService.STATE_SOFTBLOCKED))) {
+ this.node.setAttribute("notification", "warning");
+ document.getElementById("detail-warning").textContent = gStrings.ext.formatStringFromName(
+ "details.notification.incompatible",
+ [this._addon.name, gStrings.brandShortName, gStrings.appVersion], 3
+ );
+ document.getElementById("detail-warning-link").hidden = true;
+ } else if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
+ this.node.setAttribute("notification", "warning");
+ document.getElementById("detail-warning").textContent = gStrings.ext.formatStringFromName(
+ "details.notification.softblocked",
+ [this._addon.name], 1
+ );
+ var warningLink = document.getElementById("detail-warning-link");
+ warningLink.value = gStrings.ext.GetStringFromName("details.notification.softblocked.link");
+ warningLink.href = this._addon.blocklistURL;
+ warningLink.hidden = false;
+ } else if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) {
+ this.node.setAttribute("notification", "warning");
+ document.getElementById("detail-warning").textContent = gStrings.ext.formatStringFromName(
+ "details.notification.outdated",
+ [this._addon.name], 1
+ );
+ document.getElementById("detail-warning-link").hidden = true;
+ } else if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) {
+ this.node.setAttribute("notification", "error");
+ document.getElementById("detail-error").textContent = gStrings.ext.formatStringFromName(
+ "details.notification.vulnerableUpdatable",
+ [this._addon.name], 1
+ );
+ var errorLink = document.getElementById("detail-error-link");
+ errorLink.value = gStrings.ext.GetStringFromName("details.notification.vulnerableUpdatable.link");
+ errorLink.href = this._addon.blocklistURL;
+ errorLink.hidden = false;
+ } else if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) {
+ this.node.setAttribute("notification", "error");
+ document.getElementById("detail-error").textContent = gStrings.ext.formatStringFromName(
+ "details.notification.vulnerableNoUpdate",
+ [this._addon.name], 1
+ );
+ var errorLink = document.getElementById("detail-error-link");
+ errorLink.value = gStrings.ext.GetStringFromName("details.notification.vulnerableNoUpdate.link");
+ errorLink.href = this._addon.blocklistURL;
+ errorLink.hidden = false;
+ } else if (this._addon.isGMPlugin && !this._addon.isInstalled &&
+ this._addon.isActive) {
+ this.node.setAttribute("notification", "warning");
+ let warning = document.getElementById("detail-warning");
+ warning.textContent =
+ gStrings.ext.formatStringFromName("details.notification.gmpPending",
+ [this._addon.name], 1);
+ } else {
+ this.node.removeAttribute("notification");
+ }
+ }
+
+ let menulist = document.getElementById("detail-state-menulist");
+ let addonType = AddonManager.addonTypes[this._addon.type];
+ if (addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) {
+ let askItem = document.getElementById("detail-ask-to-activate-menuitem");
+ let alwaysItem = document.getElementById("detail-always-activate-menuitem");
+ let neverItem = document.getElementById("detail-never-activate-menuitem");
+ let hasActivatePermission =
+ ["ask_to_activate", "enable", "disable"].some(perm => hasPermission(this._addon, perm));
+
+ if (!this._addon.isActive) {
+ menulist.selectedItem = neverItem;
+ } else if (this._addon.userDisabled == AddonManager.STATE_ASK_TO_ACTIVATE) {
+ menulist.selectedItem = askItem;
+ } else {
+ menulist.selectedItem = alwaysItem;
+ }
+
+ menulist.disabled = !hasActivatePermission;
+ menulist.hidden = false;
+ menulist.classList.add('no-auto-hide');
+ } else {
+ menulist.hidden = true;
+ }
+
+ this.node.setAttribute("active", this._addon.isActive);
+ },
+
+ clearLoading: function gDetailView_clearLoading() {
+ if (this._loadingTimer) {
+ clearTimeout(this._loadingTimer);
+ this._loadingTimer = null;
+ }
+
+ this.node.removeAttribute("loading-extended");
+ },
+
+ emptySettingsRows: function gDetailView_emptySettingsRows() {
+ var lastRow = document.getElementById("detail-downloads");
+ var rows = lastRow.parentNode;
+ while (lastRow.nextSibling)
+ rows.removeChild(rows.lastChild);
+ },
+
+ fillSettingsRows: function gDetailView_fillSettingsRows(aScrollToPreferences, aCallback) {
+ this.emptySettingsRows();
+ if (!hasInlineOptions(this._addon)) {
+ if (aCallback)
+ aCallback();
+ return;
+ }
+
+ // This function removes and returns the text content of aNode without
+ // removing any child elements. Removing the text nodes ensures any XBL
+ // bindings apply properly.
+ function stripTextNodes(aNode) {
+ var text = '';
+ for (var i = 0; i < aNode.childNodes.length; i++) {
+ if (aNode.childNodes[i].nodeType != document.ELEMENT_NODE) {
+ text += aNode.childNodes[i].textContent;
+ aNode.removeChild(aNode.childNodes[i--]);
+ } else {
+ text += stripTextNodes(aNode.childNodes[i]);
+ }
+ }
+ return text;
+ }
+
+ var rows = document.getElementById("detail-downloads").parentNode;
+
+ try {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", this._addon.optionsURL, true);
+ xhr.responseType = "xml";
+ xhr.onload = (function fillSettingsRows_onload() {
+ var xml = xhr.responseXML;
+ var settings = xml.querySelectorAll(":root > setting");
+
+ var firstSetting = null;
+ for (var setting of settings) {
+
+ var desc = stripTextNodes(setting).trim();
+ if (!setting.hasAttribute("desc"))
+ setting.setAttribute("desc", desc);
+
+ var type = setting.getAttribute("type");
+ if (type == "file" || type == "directory")
+ setting.setAttribute("fullpath", "true");
+
+ setting = document.importNode(setting, true);
+ var style = setting.getAttribute("style");
+ if (style) {
+ setting.removeAttribute("style");
+ setting.setAttribute("style", style);
+ }
+
+ rows.appendChild(setting);
+ var visible = window.getComputedStyle(setting, null).getPropertyValue("display") != "none";
+ if (!firstSetting && visible) {
+ setting.setAttribute("first-row", true);
+ firstSetting = setting;
+ }
+ }
+
+ // Ensure the page has loaded and force the XBL bindings to be synchronously applied,
+ // then notify observers.
+ if (gViewController.viewPort.selectedPanel.hasAttribute("loading")) {
+ gDetailView.node.addEventListener("ViewChanged", function viewChangedEventListener() {
+ gDetailView.node.removeEventListener("ViewChanged", viewChangedEventListener, false);
+ if (firstSetting)
+ firstSetting.clientTop;
+ Services.obs.notifyObservers(document,
+ AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
+ gDetailView._addon.id);
+ if (aScrollToPreferences)
+ gDetailView.scrollToPreferencesRows();
+ }, false);
+ } else {
+ if (firstSetting)
+ firstSetting.clientTop;
+ Services.obs.notifyObservers(document,
+ AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
+ this._addon.id);
+ if (aScrollToPreferences)
+ gDetailView.scrollToPreferencesRows();
+ }
+ if (aCallback)
+ aCallback();
+ }).bind(this);
+ xhr.onerror = function fillSettingsRows_onerror(aEvent) {
+ Cu.reportError("Error " + aEvent.target.status +
+ " occurred while receiving " + this._addon.optionsURL);
+ if (aCallback)
+ aCallback();
+ };
+ xhr.send();
+ } catch(e) {
+ Cu.reportError(e);
+ if (aCallback)
+ aCallback();
+ }
+ },
+
+ scrollToPreferencesRows: function gDetailView_scrollToPreferencesRows() {
+ // We find this row, rather than remembering it from above,
+ // in case it has been changed by the observers.
+ let firstRow = gDetailView.node.querySelector('setting[first-row="true"]');
+ if (firstRow) {
+ let top = firstRow.boxObject.y;
+ top -= parseInt(window.getComputedStyle(firstRow, null).getPropertyValue("margin-top"));
+
+ let detailViewBoxObject = gDetailView.node.boxObject;
+ top -= detailViewBoxObject.y;
+
+ detailViewBoxObject.scrollTo(0, top);
+ }
+ },
+
+ getSelectedAddon: function gDetailView_getSelectedAddon() {
+ return this._addon;
+ },
+
+ onEnabling: function gDetailView_onEnabling() {
+ this.updateState();
+ },
+
+ onEnabled: function gDetailView_onEnabled() {
+ this.updateState();
+ this.fillSettingsRows();
+ },
+
+ onDisabling: function gDetailView_onDisabling(aNeedsRestart) {
+ this.updateState();
+ if (!aNeedsRestart && hasInlineOptions(this._addon)) {
+ Services.obs.notifyObservers(document,
+ AddonManager.OPTIONS_NOTIFICATION_HIDDEN,
+ this._addon.id);
+ }
+ },
+
+ onDisabled: function gDetailView_onDisabled() {
+ this.updateState();
+ this.emptySettingsRows();
+ },
+
+ onUninstalling: function gDetailView_onUninstalling() {
+ this.updateState();
+ },
+
+ onUninstalled: function gDetailView_onUninstalled() {
+ gViewController.popState();
+ },
+
+ onOperationCancelled: function gDetailView_onOperationCancelled() {
+ this.updateState();
+ },
+
+ onPropertyChanged: function gDetailView_onPropertyChanged(aProperties) {
+ if (aProperties.indexOf("applyBackgroundUpdates") != -1) {
+ this._autoUpdate.value = this._addon.applyBackgroundUpdates;
+ let hideFindUpdates = AddonManager.shouldAutoUpdate(this._addon);
+ document.getElementById("detail-findUpdates-btn").hidden = hideFindUpdates;
+ }
+
+ if (aProperties.indexOf("appDisabled") != -1 ||
+ aProperties.indexOf("userDisabled") != -1)
+ this.updateState();
+ },
+
+ onExternalInstall: function gDetailView_onExternalInstall(aAddon, aExistingAddon, aNeedsRestart) {
+ // Only care about upgrades for the currently displayed add-on
+ if (!aExistingAddon || aExistingAddon.id != this._addon.id)
+ return;
+
+ if (!aNeedsRestart)
+ this._updateView(aAddon, false);
+ else
+ this.updateState();
+ },
+
+ onInstallCancelled: function gDetailView_onInstallCancelled(aInstall) {
+ if (aInstall.addon.id == this._addon.id)
+ gViewController.popState();
+ }
+};
+
+
+var gUpdatesView = {
+ node: null,
+ _listBox: null,
+ _emptyNotice: null,
+ _sorters: null,
+ _updateSelected: null,
+ _categoryItem: null,
+
+ initialize: function gUpdatesView_initialize() {
+ this.node = document.getElementById("updates-view");
+ this._listBox = document.getElementById("updates-list");
+ this._emptyNotice = document.getElementById("updates-list-empty");
+ this._sorters = document.getElementById("updates-sorters");
+ this._sorters.handler = this;
+
+ this._categoryItem = gCategories.get("addons://updates/available");
+
+ this._updateSelected = document.getElementById("update-selected-btn");
+ this._updateSelected.addEventListener("command", function updateSelected_onCommand() {
+ gUpdatesView.installSelected();
+ }, false);
+
+ this.updateAvailableCount(true);
+
+ AddonManager.addAddonListener(this);
+ AddonManager.addInstallListener(this);
+ },
+
+ shutdown: function gUpdatesView_shutdown() {
+ AddonManager.removeAddonListener(this);
+ AddonManager.removeInstallListener(this);
+ },
+
+ show: function gUpdatesView_show(aType, aRequest) {
+ document.getElementById("empty-availableUpdates-msg").hidden = aType != "available";
+ document.getElementById("empty-recentUpdates-msg").hidden = aType != "recent";
+ this.showEmptyNotice(false);
+
+ while (this._listBox.itemCount > 0)
+ this._listBox.removeItemAt(0);
+
+ this.node.setAttribute("updatetype", aType);
+ if (aType == "recent")
+ this._showRecentUpdates(aRequest);
+ else
+ this._showAvailableUpdates(false, aRequest);
+ },
+
+ hide: function gUpdatesView_hide() {
+ this._updateSelected.hidden = true;
+ this._categoryItem.disabled = this._categoryItem.badgeCount == 0;
+ doPendingUninstalls(this._listBox);
+ },
+
+ _showRecentUpdates: function gUpdatesView_showRecentUpdates(aRequest) {
+ var self = this;
+ AddonManager.getAllAddons(function showRecentUpdates_getAllAddons(aAddonsList) {
+ if (gViewController && aRequest != gViewController.currentViewRequest)
+ return;
+
+ var elements = [];
+ let threshold = Date.now() - UPDATES_RECENT_TIMESPAN;
+ for (let addon of aAddonsList) {
+ if (!addon.updateDate || addon.updateDate.getTime() < threshold)
+ continue;
+
+ elements.push(createItem(addon));
+ }
+
+ self.showEmptyNotice(elements.length == 0);
+ if (elements.length > 0) {
+ sortElements(elements, [self._sorters.sortBy], self._sorters.ascending);
+ for (let element of elements)
+ self._listBox.appendChild(element);
+ }
+
+ gViewController.notifyViewChanged();
+ });
+ },
+
+ _showAvailableUpdates: function gUpdatesView_showAvailableUpdates(aIsRefresh, aRequest) {
+ /* Disable the Update Selected button so it can't get clicked
+ before everything is initialized asynchronously.
+ It will get re-enabled by maybeDisableUpdateSelected(). */
+ this._updateSelected.disabled = true;
+
+ var self = this;
+ AddonManager.getAllInstalls(function showAvailableUpdates_getAllInstalls(aInstallsList) {
+ if (!aIsRefresh && gViewController && aRequest &&
+ aRequest != gViewController.currentViewRequest)
+ return;
+
+ if (aIsRefresh) {
+ self.showEmptyNotice(false);
+ self._updateSelected.hidden = true;
+
+ while (self._listBox.itemCount > 0)
+ self._listBox.removeItemAt(0);
+ }
+
+ var elements = [];
+
+ for (let install of aInstallsList) {
+ if (!self.isManualUpdate(install))
+ continue;
+
+ let item = createItem(install.existingAddon);
+ item.setAttribute("upgrade", true);
+ item.addEventListener("IncludeUpdateChanged", function item_onIncludeUpdateChanged() {
+ self.maybeDisableUpdateSelected();
+ }, false);
+ elements.push(item);
+ }
+
+ self.showEmptyNotice(elements.length == 0);
+ if (elements.length > 0) {
+ self._updateSelected.hidden = false;
+ sortElements(elements, [self._sorters.sortBy], self._sorters.ascending);
+ for (let element of elements)
+ self._listBox.appendChild(element);
+ }
+
+ // ensure badge count is in sync
+ self._categoryItem.badgeCount = self._listBox.itemCount;
+
+ gViewController.notifyViewChanged();
+ });
+ },
+
+ showEmptyNotice: function gUpdatesView_showEmptyNotice(aShow) {
+ this._emptyNotice.hidden = !aShow;
+ },
+
+ isManualUpdate: function gUpdatesView_isManualUpdate(aInstall, aOnlyAvailable) {
+ var isManual = aInstall.existingAddon &&
+ !AddonManager.shouldAutoUpdate(aInstall.existingAddon);
+ if (isManual && aOnlyAvailable)
+ return isInState(aInstall, "available");
+ return isManual;
+ },
+
+ maybeRefresh: function gUpdatesView_maybeRefresh() {
+ if (gViewController.currentViewId == "addons://updates/available")
+ this._showAvailableUpdates(true);
+ this.updateAvailableCount();
+ },
+
+ updateAvailableCount: function gUpdatesView_updateAvailableCount(aInitializing) {
+ if (aInitializing)
+ gPendingInitializations++;
+ var self = this;
+ AddonManager.getAllInstalls(function updateAvailableCount_getAllInstalls(aInstallsList) {
+ var count = aInstallsList.filter(function installListFilter(aInstall) {
+ return self.isManualUpdate(aInstall, true);
+ }).length;
+ self._categoryItem.disabled = gViewController.currentViewId != "addons://updates/available" &&
+ count == 0;
+ self._categoryItem.badgeCount = count;
+ if (aInitializing)
+ notifyInitialized();
+ });
+ },
+
+ maybeDisableUpdateSelected: function gUpdatesView_maybeDisableUpdateSelected() {
+ for (let item of this._listBox.childNodes) {
+ if (item.includeUpdate) {
+ this._updateSelected.disabled = false;
+ return;
+ }
+ }
+ this._updateSelected.disabled = true;
+ },
+
+ installSelected: function gUpdatesView_installSelected() {
+ for (let item of this._listBox.childNodes) {
+ if (item.includeUpdate)
+ item.upgrade();
+ }
+
+ this._updateSelected.disabled = true;
+ },
+
+ getSelectedAddon: function gUpdatesView_getSelectedAddon() {
+ var item = this._listBox.selectedItem;
+ if (item)
+ return item.mAddon;
+ return null;
+ },
+
+ getListItemForID: function gUpdatesView_getListItemForID(aId) {
+ var listitem = this._listBox.firstChild;
+ while (listitem) {
+ if (listitem.mAddon.id == aId)
+ return listitem;
+ listitem = listitem.nextSibling;
+ }
+ return null;
+ },
+
+ onSortChanged: function gUpdatesView_onSortChanged(aSortBy, aAscending) {
+ sortList(this._listBox, aSortBy, aAscending);
+ },
+
+ onNewInstall: function gUpdatesView_onNewInstall(aInstall) {
+ if (!this.isManualUpdate(aInstall))
+ return;
+ this.maybeRefresh();
+ },
+
+ onInstallStarted: function gUpdatesView_onInstallStarted(aInstall) {
+ this.updateAvailableCount();
+ },
+
+ onInstallCancelled: function gUpdatesView_onInstallCancelled(aInstall) {
+ if (!this.isManualUpdate(aInstall))
+ return;
+ this.maybeRefresh();
+ },
+
+ onPropertyChanged: function gUpdatesView_onPropertyChanged(aAddon, aProperties) {
+ if (aProperties.indexOf("applyBackgroundUpdates") != -1)
+ this.updateAvailableCount();
+ }
+};
+
+function debuggingPrefChanged() {
+ gViewController.updateState();
+ gViewController.updateCommands();
+ gViewController.notifyViewChanged();
+}
+
+var gDragDrop = {
+ onDragOver: function gDragDrop_onDragOver(aEvent) {
+ var types = aEvent.dataTransfer.types;
+ if (types.contains("text/uri-list") ||
+ types.contains("text/x-moz-url") ||
+ types.contains("application/x-moz-file"))
+ aEvent.preventDefault();
+ },
+
+ onDrop: function gDragDrop_onDrop(aEvent) {
+ var dataTransfer = aEvent.dataTransfer;
+ var urls = [];
+
+ // Convert every dropped item into a url
+ for (var i = 0; i < dataTransfer.mozItemCount; i++) {
+ var url = dataTransfer.mozGetDataAt("text/uri-list", i);
+ if (url) {
+ urls.push(url);
+ continue;
+ }
+
+ url = dataTransfer.mozGetDataAt("text/x-moz-url", i);
+ if (url) {
+ urls.push(url.split("\n")[0]);
+ continue;
+ }
+
+ var file = dataTransfer.mozGetDataAt("application/x-moz-file", i);
+ if (file) {
+ urls.push(Services.io.newFileURI(file).spec);
+ continue;
+ }
+ }
+
+ var pos = 0;
+ var installs = [];
+
+ function buildNextInstall() {
+ if (pos == urls.length) {
+ if (installs.length > 0) {
+ // Display the normal install confirmation for the installs
+ let webInstaller = Cc["@mozilla.org/addons/web-install-listener;1"].
+ getService(Ci.amIWebInstallListener);
+ webInstaller.onWebInstallRequested(getBrowserElement(),
+ document.documentURIObject,
+ installs, installs.length);
+ }
+ return;
+ }
+
+ AddonManager.getInstallForURL(urls[pos++], function onDrop_getInstallForURL(aInstall) {
+ installs.push(aInstall);
+ buildNextInstall();
+ }, "application/x-xpinstall");
+ }
+
+ buildNextInstall();
+
+ aEvent.preventDefault();
+ }
+};
diff --git a/components/extensions/content/extensions.xml b/components/extensions/content/extensions.xml
new file mode 100644
index 000000000..a3246e220
--- /dev/null
+++ b/components/extensions/content/extensions.xml
@@ -0,0 +1,2084 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+
+<!DOCTYPE page [
+<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/extensions.dtd">
+%extensionsDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://mozapps/locale/extensions/about.dtd">
+%aboutDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+]>
+
+<bindings id="addonBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+
+ <!-- Rating - displays current/average rating, allows setting user rating -->
+ <binding id="rating">
+ <content>
+ <xul:image class="star"
+ onmouseover="document.getBindingParent(this)._hover(1);"
+ onclick="document.getBindingParent(this).userRating = 1;"/>
+ <xul:image class="star"
+ onmouseover="document.getBindingParent(this)._hover(2);"
+ onclick="document.getBindingParent(this).userRating = 2;"/>
+ <xul:image class="star"
+ onmouseover="document.getBindingParent(this)._hover(3);"
+ onclick="document.getBindingParent(this).userRating = 3;"/>
+ <xul:image class="star"
+ onmouseover="document.getBindingParent(this)._hover(4);"
+ onclick="document.getBindingParent(this).userRating = 4;"/>
+ <xul:image class="star"
+ onmouseover="document.getBindingParent(this)._hover(5);"
+ onclick="document.getBindingParent(this).userRating = 5;"/>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this._updateStars();
+ ]]></constructor>
+
+ <property name="stars" readonly="true">
+ <getter><![CDATA[
+ return document.getAnonymousNodes(this);
+ ]]></getter>
+ </property>
+
+ <property name="averageRating">
+ <getter><![CDATA[
+ if (this.hasAttribute("averagerating"))
+ return this.getAttribute("averagerating");
+ return -1;
+ ]]></getter>
+ <setter><![CDATA[
+ this.setAttribute("averagerating", val);
+ if (this.showRating == "average")
+ this._updateStars();
+ ]]></setter>
+ </property>
+
+ <property name="userRating">
+ <getter><![CDATA[
+ if (this.hasAttribute("userrating"))
+ return this.getAttribute("userrating");
+ return -1;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.showRating != "user")
+ return;
+ this.setAttribute("userrating", val);
+ if (this.showRating == "user")
+ this._updateStars();
+ ]]></setter>
+ </property>
+
+ <property name="showRating">
+ <getter><![CDATA[
+ if (this.hasAttribute("showrating"))
+ return this.getAttribute("showrating");
+ return "average";
+ ]]></getter>
+ <setter><![CDATA[
+ if (val != "average" || val != "user")
+ throw Components.Exception("Invalid value", Components.results.NS_ERROR_ILLEGAL_VALUE);
+ this.setAttribute("showrating", val);
+ this._updateStars();
+ ]]></setter>
+ </property>
+
+ <method name="_updateStars">
+ <body><![CDATA[
+ var stars = this.stars;
+ var rating = this[this.showRating + "Rating"];
+ // average ratings can be non-whole numbers, round them so they
+ // match to their closest star
+ rating = Math.round(rating);
+ for (let i = 0; i < stars.length; i++)
+ stars[i].setAttribute("on", rating > i);
+ ]]></body>
+ </method>
+
+ <method name="_hover">
+ <parameter name="aScore"/>
+ <body><![CDATA[
+ if (this.showRating != "user")
+ return;
+ var stars = this.stars;
+ for (let i = 0; i < stars.length; i++)
+ stars[i].setAttribute("on", i <= (aScore -1));
+ ]]></body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="mouseout">
+ this._updateStars();
+ </handler>
+ </handlers>
+ </binding>
+
+ <!-- Download progress - shows graphical progress of download and any
+ related status message. -->
+ <binding id="download-progress">
+ <content>
+ <xul:stack flex="1">
+ <xul:hbox flex="1">
+ <xul:hbox class="start-cap"/>
+ <xul:progressmeter anonid="progress" class="progress" flex="1"
+ min="0" max="100"/>
+ <xul:hbox class="end-cap"/>
+ </xul:hbox>
+ <xul:hbox class="status-container">
+ <xul:spacer flex="1"/>
+ <xul:label anonid="status" class="status"/>
+ <xul:spacer flex="1"/>
+ <xul:button anonid="cancel-btn" class="cancel"
+ tooltiptext="&progress.cancel.tooltip;"
+ oncommand="document.getBindingParent(this).cancel();"/>
+ </xul:hbox>
+ </xul:stack>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ var progress = 0;
+ if (this.hasAttribute("progress"))
+ progress = parseInt(this.getAttribute("progress"));
+ this.progress = progress;
+ ]]></constructor>
+
+ <field name="_progress">
+ document.getAnonymousElementByAttribute(this, "anonid", "progress");
+ </field>
+ <field name="_cancel">
+ document.getAnonymousElementByAttribute(this, "anonid", "cancel-btn");
+ </field>
+ <field name="_status">
+ document.getAnonymousElementByAttribute(this, "anonid", "status");
+ </field>
+
+ <property name="progress">
+ <getter><![CDATA[
+ return this._progress.value;
+ ]]></getter>
+ <setter><![CDATA[
+ this._progress.value = val;
+ if (val == this._progress.max)
+ this.setAttribute("complete", true);
+ else
+ this.removeAttribute("complete");
+ ]]></setter>
+ </property>
+
+ <property name="maxProgress">
+ <getter><![CDATA[
+ return this._progress.max;
+ ]]></getter>
+ <setter><![CDATA[
+ if (val == -1) {
+ this._progress.mode = "undetermined";
+ } else {
+ this._progress.mode = "determined";
+ this._progress.max = val;
+ }
+ this.setAttribute("mode", this._progress.mode);
+ ]]></setter>
+ </property>
+
+ <property name="status">
+ <getter><![CDATA[
+ return this._status.value;
+ ]]></getter>
+ <setter><![CDATA[
+ this._status.value = val;
+ ]]></setter>
+ </property>
+
+ <method name="cancel">
+ <body><![CDATA[
+ this.mInstall.cancel();
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+
+ <!-- Sorters - displays and controls the sort state of a list. -->
+ <binding id="sorters">
+ <content orient="horizontal">
+ <xul:button anonid="name-btn" class="sorter"
+ label="&sort.name.label;" tooltiptext="&sort.name.tooltip;"
+ oncommand="this.parentNode._handleChange('name');"/>
+ <xul:button anonid="date-btn" class="sorter"
+ label="&sort.dateUpdated.label;"
+ tooltiptext="&sort.dateUpdated.tooltip;"
+ oncommand="this.parentNode._handleChange('updateDate');"/>
+ <xul:button anonid="price-btn" class="sorter" hidden="true"
+ label="&sort.price.label;"
+ tooltiptext="&sort.price.tooltip;"
+ oncommand="this.parentNode._handleChange('purchaseAmount');"/>
+ <xul:button anonid="relevance-btn" class="sorter" hidden="true"
+ label="&sort.relevance.label;"
+ tooltiptext="&sort.relevance.tooltip;"
+ oncommand="this.parentNode._handleChange('relevancescore');"/>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ if (!this.hasAttribute("sortby"))
+ this.setAttribute("sortby", "name");
+
+ if (this.getAttribute("showrelevance") == "true")
+ this._btnRelevance.hidden = false;
+
+ if (this.getAttribute("showprice") == "true")
+ this._btnPrice.hidden = false;
+
+ this._refreshState();
+ ]]></constructor>
+
+ <field name="handler">null</field>
+ <field name="_btnName">
+ document.getAnonymousElementByAttribute(this, "anonid", "name-btn");
+ </field>
+ <field name="_btnDate">
+ document.getAnonymousElementByAttribute(this, "anonid", "date-btn");
+ </field>
+ <field name="_btnPrice">
+ document.getAnonymousElementByAttribute(this, "anonid", "price-btn");
+ </field>
+ <field name="_btnRelevance">
+ document.getAnonymousElementByAttribute(this, "anonid", "relevance-btn");
+ </field>
+
+ <property name="sortBy">
+ <getter><![CDATA[
+ return this.getAttribute("sortby");
+ ]]></getter>
+ <setter><![CDATA[
+ if (val != this.sortBy) {
+ this.setAttribute("sortBy", val);
+ this._refreshState();
+ }
+ ]]></setter>
+ </property>
+
+ <property name="ascending">
+ <getter><![CDATA[
+ return (this.getAttribute("ascending") == "true");
+ ]]></getter>
+ <setter><![CDATA[
+ val = !!val;
+ if (val != this.ascending) {
+ this.setAttribute("ascending", val);
+ this._refreshState();
+ }
+ ]]></setter>
+ </property>
+
+ <property name="showrelevance">
+ <getter><![CDATA[
+ return (this.getAttribute("showrelevance") == "true");
+ ]]></getter>
+ <setter><![CDATA[
+ val = !!val;
+ this.setAttribute("showrelevance", val);
+ this._btnRelevance.hidden = !val;
+ ]]></setter>
+ </property>
+
+ <property name="showprice">
+ <getter><![CDATA[
+ return (this.getAttribute("showprice") == "true");
+ ]]></getter>
+ <setter><![CDATA[
+ val = !!val;
+ this.setAttribute("showprice", val);
+ this._btnPrice.hidden = !val;
+ ]]></setter>
+ </property>
+
+ <method name="setSort">
+ <parameter name="aSort"/>
+ <parameter name="aAscending"/>
+ <body><![CDATA[
+ var sortChanged = false;
+ if (aSort != this.sortBy) {
+ this.setAttribute("sortby", aSort);
+ sortChanged = true;
+ }
+
+ aAscending = !!aAscending;
+ if (this.ascending != aAscending) {
+ this.setAttribute("ascending", aAscending);
+ sortChanged = true;
+ }
+
+ if (sortChanged)
+ this._refreshState();
+ ]]></body>
+ </method>
+
+ <method name="_handleChange">
+ <parameter name="aSort"/>
+ <body><![CDATA[
+ const ASCENDING_SORT_FIELDS = ["name", "purchaseAmount"];
+
+ // Toggle ascending if sort by is not changing, otherwise
+ // name sorting defaults to ascending, others to descending
+ if (aSort == this.sortBy)
+ this.ascending = !this.ascending;
+ else
+ this.setSort(aSort, ASCENDING_SORT_FIELDS.indexOf(aSort) >= 0);
+ ]]></body>
+ </method>
+
+ <method name="_refreshState">
+ <body><![CDATA[
+ var sortBy = this.sortBy;
+ var checkState = this.ascending ? 2 : 1;
+
+ if (sortBy == "name") {
+ this._btnName.checkState = checkState;
+ this._btnName.checked = true;
+ } else {
+ this._btnName.checkState = 0;
+ this._btnName.checked = false;
+ }
+
+ if (sortBy == "updateDate") {
+ this._btnDate.checkState = checkState;
+ this._btnDate.checked = true;
+ } else {
+ this._btnDate.checkState = 0;
+ this._btnDate.checked = false;
+ }
+
+ if (sortBy == "purchaseAmount") {
+ this._btnPrice.checkState = checkState;
+ this._btnPrice.checked = true;
+ } else {
+ this._btnPrice.checkState = 0;
+ this._btnPrice.checked = false;
+ }
+
+ if (sortBy == "relevancescore") {
+ this._btnRelevance.checkState = checkState;
+ this._btnRelevance.checked = true;
+ } else {
+ this._btnRelevance.checkState = 0;
+ this._btnRelevance.checked = false;
+ }
+
+ if (this.handler && "onSortChanged" in this.handler)
+ this.handler.onSortChanged(sortBy, this.ascending);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+
+ <!-- Categories list - displays the list of categories on the left pane. -->
+ <binding id="categories-list"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+ <implementation>
+ <!-- This needs to be overridden to allow the fancy animation while not
+ allowing that item to be selected when hiding. -->
+ <method name="_canUserSelect">
+ <parameter name="aItem"/>
+ <body>
+ <![CDATA[
+ if (aItem.hasAttribute("disabled") &&
+ aItem.getAttribute("disabled") == "true")
+ return false;
+ var style = document.defaultView.getComputedStyle(aItem, "");
+ return style.display != "none" && style.visibility == "visible";
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+
+ <!-- Category item - an item in the category list. -->
+ <binding id="category"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content align="center">
+ <xul:image anonid="icon" class="category-icon"/>
+ <xul:label anonid="name" class="category-name" flex="1" xbl:inherits="value=name"/>
+ <xul:label anonid="badge" class="category-badge" xbl:inherits="value=count"/>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ if (!this.hasAttribute("count"))
+ this.setAttribute("count", 0);
+ ]]></constructor>
+
+ <property name="badgeCount">
+ <getter><![CDATA[
+ return this.getAttribute("count");
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.getAttribute("count") == val)
+ return;
+
+ this.setAttribute("count", val);
+ var event = document.createEvent("Events");
+ event.initEvent("CategoryBadgeUpdated", true, true);
+ this.dispatchEvent(event);
+ ]]></setter>
+ </property>
+ </implementation>
+ </binding>
+
+
+ <!-- Creator link - Name of a user/developer, providing a link if relevant. -->
+ <binding id="creator-link">
+ <content>
+ <xul:label anonid="label" value="&addon.createdBy.label;"/>
+ <xul:label anonid="creator-link" class="creator-link text-link"/>
+ <xul:label anonid="creator-name" class="creator-name"/>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ if (this.hasAttribute("nameonly") &&
+ this.getAttribute("nameonly") == "true") {
+ this._label.hidden = true;
+ }
+ ]]></constructor>
+
+ <field name="_label">
+ document.getAnonymousElementByAttribute(this, "anonid", "label");
+ </field>
+ <field name="_creatorLink">
+ document.getAnonymousElementByAttribute(this, "anonid", "creator-link");
+ </field>
+ <field name="_creatorName">
+ document.getAnonymousElementByAttribute(this, "anonid", "creator-name");
+ </field>
+
+ <method name="setCreator">
+ <parameter name="aCreator"/>
+ <parameter name="aHomepageURL"/>
+ <body><![CDATA[
+ if (!aCreator) {
+ this.collapsed = true;
+ return;
+ }
+ this.collapsed = false;
+ var url = aCreator.url || aHomepageURL;
+ var showLink = !!url;
+ if (showLink) {
+ this._creatorLink.value = aCreator.name;
+ this._creatorLink.href = url;
+ } else {
+ this._creatorName.value = aCreator.name;
+ }
+ this._creatorLink.hidden = !showLink;
+ this._creatorName.hidden = showLink;
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+
+ <!-- Translators list - Names of a translators of Language Pack. -->
+ <binding id="translators-list">
+ <content>
+ <xul:label anonid="label" value="&translators.label;" class="sectionTitle"/>
+ <xul:vbox flex="1" anonid="translatorsBox" class="boxIndent"/>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ if (this.hasAttribute("nameonly") &&
+ this.getAttribute("nameonly") == "true") {
+ this._label.hidden = true;
+ }
+ ]]></constructor>
+
+ <field name="_label">
+ document.getAnonymousElementByAttribute(this, "anonid", "label");
+ </field>
+ <field name="_translatorsBox">
+ document.getAnonymousElementByAttribute(this, "anonid", "translatorsBox");
+ </field>
+
+ <method name="setTranslators">
+ <parameter name="aTranslators"/>
+ <parameter name="aType"/>
+ <body><![CDATA[
+ if (aType != "locale" || !aTranslators || aTranslators.length == 0) {
+ this.collapsed = true;
+ return;
+ }
+ this.collapsed = false;
+ while (this._translatorsBox.firstChild) {
+ this._translatorsBox.removeChild(this._translatorsBox.firstChild);
+ }
+ for (let currentItem of aTranslators) {
+ var label = document.createElement("label");
+ label.textContent = currentItem;
+ label.setAttribute("class", "contributor");
+ this._translatorsBox.appendChild(label);
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+
+ <!-- Install status - Displays the status of an install/upgrade. -->
+ <binding id="install-status">
+ <content>
+ <xul:label anonid="message"/>
+ <xul:progressmeter anonid="progress" class="download-progress"/>
+ <xul:button anonid="purchase-remote-btn" hidden="true"
+ class="addon-control"
+ oncommand="document.getBindingParent(this).purchaseRemote();"/>
+ <xul:button anonid="install-remote-btn" hidden="true"
+ class="addon-control install" label="&addon.install.label;"
+ tooltiptext="&addon.install.tooltip;"
+ oncommand="document.getBindingParent(this).installRemote();"/>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ if (this.mInstall)
+ this.initWithInstall(this.mInstall);
+ else if (this.mControl.mAddon.install)
+ this.initWithInstall(this.mControl.mAddon.install);
+ else
+ this.refreshState();
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ if (this.mInstall)
+ this.mInstall.removeListener(this);
+ ]]></destructor>
+
+ <field name="_message">
+ document.getAnonymousElementByAttribute(this, "anonid", "message");
+ </field>
+ <field name="_progress">
+ document.getAnonymousElementByAttribute(this, "anonid", "progress");
+ </field>
+ <field name="_purchaseRemote">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "purchase-remote-btn");
+ </field>
+ <field name="_installRemote">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "install-remote-btn");
+ </field>
+ <field name="_restartNeeded">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "restart-needed");
+ </field>
+ <field name="_undo">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "undo-btn");
+ </field>
+
+ <method name="initWithInstall">
+ <parameter name="aInstall"/>
+ <body><![CDATA[
+ if (this.mInstall) {
+ this.mInstall.removeListener(this);
+ this.mInstall = null;
+ }
+ this.mInstall = aInstall;
+ this._progress.mInstall = aInstall;
+ this.refreshState();
+ this.mInstall.addListener(this);
+ ]]></body>
+ </method>
+
+ <method name="refreshState">
+ <body><![CDATA[
+ var showInstallRemote = false;
+ var showPurchase = false;
+
+ if (this.mInstall) {
+
+ switch (this.mInstall.state) {
+ case AddonManager.STATE_AVAILABLE:
+ if (this.mControl.getAttribute("remote") != "true")
+ break;
+
+ this._progress.hidden = true;
+ showInstallRemote = true;
+ break;
+ case AddonManager.STATE_DOWNLOADING:
+ this.showMessage("installDownloading");
+ break;
+ case AddonManager.STATE_CHECKING:
+ this.showMessage("installVerifying");
+ break;
+ case AddonManager.STATE_DOWNLOADED:
+ this.showMessage("installDownloaded");
+ break;
+ case AddonManager.STATE_DOWNLOAD_FAILED:
+ // XXXunf expose what error occured (bug 553487)
+ this.showMessage("installDownloadFailed", true);
+ break;
+ case AddonManager.STATE_INSTALLING:
+ this.showMessage("installInstalling");
+ break;
+ case AddonManager.STATE_INSTALL_FAILED:
+ // XXXunf expose what error occured (bug 553487)
+ this.showMessage("installFailed", true);
+ break;
+ case AddonManager.STATE_CANCELLED:
+ this.showMessage("installCancelled", true);
+ break;
+ }
+
+ } else if (this.mControl.mAddon.purchaseURL) {
+ this._progress.hidden = true;
+ showPurchase = true;
+ this._purchaseRemote.label =
+ gStrings.ext.formatStringFromName("addon.purchase.label",
+ [this.mControl.mAddon.purchaseDisplayAmount], 1);
+ this._purchaseRemote.tooltiptext =
+ gStrings.ext.GetStringFromName("addon.purchase.tooltip");
+ }
+
+ this._purchaseRemote.hidden = !showPurchase;
+ this._installRemote.hidden = !showInstallRemote;
+
+ if ("refreshInfo" in this.mControl)
+ this.mControl.refreshInfo();
+ ]]></body>
+ </method>
+
+ <method name="showMessage">
+ <parameter name="aMsgId"/>
+ <parameter name="aHideProgress"/>
+ <body><![CDATA[
+ this._message.setAttribute("hidden", !aHideProgress);
+ this._progress.setAttribute("hidden", !!aHideProgress);
+
+ var msg = gStrings.ext.GetStringFromName(aMsgId);
+ if (aHideProgress)
+ this._message.value = msg;
+ else
+ this._progress.status = msg;
+ ]]></body>
+ </method>
+
+ <method name="purchaseRemote">
+ <body><![CDATA[
+ openURL(this.mControl.mAddon.purchaseURL);
+ ]]></body>
+ </method>
+
+ <method name="installRemote">
+ <body><![CDATA[
+ if (this.mControl.getAttribute("remote") != "true")
+ return;
+
+ if (this.mControl.mAddon.eula) {
+ var data = {
+ addon: this.mControl.mAddon,
+ accepted: false
+ };
+ window.openDialog("chrome://mozapps/content/extensions/eula.xul", "_blank",
+ "chrome,dialog,modal,centerscreen,resizable=no", data);
+ if (!data.accepted)
+ return;
+ }
+
+ delete this.mControl.mAddon;
+ this.mControl.mInstall = this.mInstall;
+ this.mControl.setAttribute("status", "installing");
+ this.mInstall.install();
+ ]]></body>
+ </method>
+
+ <method name="undoAction">
+ <body><![CDATA[
+ if (!this.mAddon)
+ return;
+ var pending = this.mAddon.pendingOperations;
+ if (pending & AddonManager.PENDING_ENABLE)
+ this.mAddon.userDisabled = true;
+ else if (pending & AddonManager.PENDING_DISABLE)
+ this.mAddon.userDisabled = false;
+ this.refreshState();
+ ]]></body>
+ </method>
+
+ <method name="onDownloadStarted">
+ <body><![CDATA[
+ this.refreshState();
+ ]]></body>
+ </method>
+
+ <method name="onDownloadEnded">
+ <body><![CDATA[
+ this.refreshState();
+ ]]></body>
+ </method>
+
+ <method name="onDownloadFailed">
+ <body><![CDATA[
+ this.refreshState();
+ ]]></body>
+ </method>
+
+ <method name="onDownloadProgress">
+ <body><![CDATA[
+ this._progress.maxProgress = this.mInstall.maxProgress;
+ this._progress.progress = this.mInstall.progress;
+ ]]></body>
+ </method>
+
+ <method name="onInstallStarted">
+ <body><![CDATA[
+ this._progress.progress = 0;
+ this.refreshState();
+ ]]></body>
+ </method>
+
+ <method name="onInstallEnded">
+ <body><![CDATA[
+ this.refreshState();
+ if ("onInstallCompleted" in this.mControl)
+ this.mControl.onInstallCompleted();
+ ]]></body>
+ </method>
+
+ <method name="onInstallFailed">
+ <body><![CDATA[
+ this.refreshState();
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+
+ <!-- Addon - base - parent binding of any item representing an addon. -->
+ <binding id="addon-base"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <implementation>
+ <method name="hasPermission">
+ <parameter name="aPerm"/>
+ <body><![CDATA[
+ var perm = AddonManager["PERM_CAN_" + aPerm.toUpperCase()];
+ return !!(this.mAddon.permissions & perm);
+ ]]></body>
+ </method>
+
+ <method name="opRequiresRestart">
+ <parameter name="aOperation"/>
+ <body><![CDATA[
+ var operation = AddonManager["OP_NEEDS_RESTART_" + aOperation.toUpperCase()];
+ return !!(this.mAddon.operationsRequiringRestart & operation);
+ ]]></body>
+ </method>
+
+ <method name="typeHasFlag">
+ <parameter name="aFlag"/>
+ <body><![CDATA[
+ let flag = AddonManager["TYPE_" + aFlag];
+ let type = AddonManager.addonTypes[this.mAddon.type];
+
+ return !!(type.flags & flag);
+ ]]></body>
+ </method>
+
+ <method name="isPending">
+ <parameter name="aAction"/>
+ <body><![CDATA[
+ var action = AddonManager["PENDING_" + aAction.toUpperCase()];
+ return !!(this.mAddon.pendingOperations & action);
+ ]]></body>
+ </method>
+
+ <method name="onUninstalled">
+ <body><![CDATA[
+ this.parentNode.removeChild(this);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+
+ <!-- Addon - generic - A normal addon item, or an update to one -->
+ <binding id="addon-generic"
+ extends="chrome://mozapps/content/extensions/extensions.xml#addon-base">
+ <content>
+ <xul:hbox anonid="warning-container"
+ class="warning">
+ <xul:image class="warning-icon"/>
+ <xul:label anonid="warning" flex="1"/>
+ <xul:label anonid="warning-link" class="text-link"/>
+ <xul:button anonid="warning-btn" class="button-link"/>
+ <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </xul:hbox>
+ <xul:hbox anonid="error-container"
+ class="error">
+ <xul:image class="error-icon"/>
+ <xul:label anonid="error" flex="1"/>
+ <xul:label anonid="error-link" class="text-link"/>
+ <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </xul:hbox>
+ <xul:hbox anonid="pending-container"
+ class="pending">
+ <xul:image class="pending-icon"/>
+ <xul:label anonid="pending" flex="1"/>
+ <xul:button anonid="restart-btn" class="button-link"
+ label="&addon.restartNow.label;"
+ oncommand="document.getBindingParent(this).restart();"/>
+ <xul:button anonid="undo-btn" class="button-link"
+ label="&addon.undoAction.label;"
+ tooltipText="&addon.undoAction.tooltip;"
+ oncommand="document.getBindingParent(this).undo();"/>
+ <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </xul:hbox>
+
+ <xul:hbox class="content-container">
+ <xul:vbox class="icon-container">
+ <xul:image anonid="icon" class="icon"/>
+ </xul:vbox>
+ <xul:vbox class="content-inner-container" flex="1">
+ <xul:hbox class="basicinfo-container">
+ <xul:hbox class="name-container">
+ <xul:label anonid="name" class="name" crop="end" flex="1"
+ xbl:inherits="value=name,tooltiptext=name"/>
+ <xul:label anonid="version" class="version"/>
+ <xul:label class="disabled-postfix" value="&addon.disabled.postfix;"/>
+ <xul:label class="update-postfix" value="&addon.update.postfix;"/>
+ <xul:spacer flex="5000"/> <!-- Necessary to make the name crop -->
+ </xul:hbox>
+ <xul:label anonid="date-updated" class="date-updated"
+ unknown="&addon.unknownDate;"/>
+ </xul:hbox>
+ <xul:hbox class="experiment-container">
+ <svg width="6" height="6" viewBox="0 0 6 6" version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ class="experiment-bullet-container">
+ <circle cx="3" cy="3" r="3" class="experiment-bullet"/>
+ </svg>
+ <xul:label anonid="experiment-state" class="experiment-state"/>
+ <xul:label anonid="experiment-time" class="experiment-time"/>
+ </xul:hbox>
+
+ <xul:hbox class="advancedinfo-container" flex="1">
+ <xul:vbox class="description-outer-container" flex="1">
+ <xul:hbox class="description-container">
+ <xul:label anonid="description" class="description" crop="end" flex="1"/>
+ <xul:button anonid="details-btn" class="details button-link"
+ label="&addon.details.label;"
+ tooltiptext="&addon.details.tooltip;"
+ oncommand="document.getBindingParent(this).showInDetailView();"/>
+ <xul:spacer flex="5000"/> <!-- Necessary to make the description crop -->
+ </xul:hbox>
+ <xul:vbox anonid="relnotes-container" class="relnotes-container">
+ <xul:label class="relnotes-header" value="&addon.releaseNotes.label;"/>
+ <xul:label anonid="relnotes-loading" value="&addon.loadingReleaseNotes.label;"/>
+ <xul:label anonid="relnotes-error" hidden="true"
+ value="&addon.errorLoadingReleaseNotes.label;"/>
+ <xul:vbox anonid="relnotes" class="relnotes"/>
+ </xul:vbox>
+ <xul:hbox class="relnotes-toggle-container">
+ <xul:button anonid="relnotes-toggle-btn" class="relnotes-toggle"
+ hidden="true" label="&cmd.showReleaseNotes.label;"
+ tooltiptext="&cmd.showReleaseNotes.tooltip;"
+ showlabel="&cmd.showReleaseNotes.label;"
+ showtooltip="&cmd.showReleaseNotes.tooltip;"
+ hidelabel="&cmd.hideReleaseNotes.label;"
+ hidetooltip="&cmd.hideReleaseNotes.tooltip;"
+ oncommand="document.getBindingParent(this).toggleReleaseNotes();"/>
+ </xul:hbox>
+ </xul:vbox>
+ <xul:vbox class="status-control-wrapper">
+ <xul:hbox class="status-container">
+ <xul:hbox anonid="checking-update" hidden="true">
+ <xul:image class="spinner"/>
+ <xul:label value="&addon.checkingForUpdates.label;"/>
+ </xul:hbox>
+ <xul:vbox anonid="update-available" class="update-available"
+ hidden="true">
+ <xul:checkbox anonid="include-update" class="include-update"
+ label="&addon.includeUpdate.label;" checked="true"
+ oncommand="document.getBindingParent(this).onIncludeUpdateChanged();"/>
+ <xul:hbox class="update-info-container">
+ <xul:label class="update-available-notice"
+ value="&addon.updateAvailable.label;"/>
+ <xul:button anonid="update-btn" class="addon-control update"
+ label="&addon.updateNow.label;"
+ tooltiptext="&addon.updateNow.tooltip;"
+ oncommand="document.getBindingParent(this).upgrade();"/>
+ </xul:hbox>
+ </xul:vbox>
+ <xul:hbox anonid="install-status" class="install-status"
+ hidden="true"/>
+ </xul:hbox>
+ <xul:hbox anonid="control-container" class="control-container">
+ <xul:button anonid="preferences-btn"
+ class="addon-control preferences"
+#ifdef XP_WIN
+ label="&cmd.showPreferencesWin.label;"
+ tooltiptext="&cmd.showPreferencesWin.tooltip;"
+#else
+ label="&cmd.showPreferencesUnix.label;"
+ tooltiptext="&cmd.showPreferencesUnix.tooltip;"
+#endif
+ oncommand="document.getBindingParent(this).showPreferences();"/>
+ <!-- label="&cmd.debugAddon.label;" -->
+ <xul:button anonid="enable-btn" class="addon-control enable"
+ label="&cmd.enableAddon.label;"
+ oncommand="document.getBindingParent(this).userDisabled = false;"/>
+ <xul:button anonid="disable-btn" class="addon-control disable"
+ label="&cmd.disableAddon.label;"
+ oncommand="document.getBindingParent(this).userDisabled = true;"/>
+ <xul:button anonid="remove-btn" class="addon-control remove"
+ label="&cmd.uninstallAddon.label;"
+ oncommand="document.getBindingParent(this).uninstall();"/>
+ <xul:menulist anonid="state-menulist"
+ class="addon-control state"
+ tooltiptext="&cmd.stateMenu.tooltip;">
+ <xul:menupopup>
+ <xul:menuitem anonid="ask-to-activate-menuitem"
+ class="addon-control"
+ label="&cmd.askToActivate.label;"
+ tooltiptext="&cmd.askToActivate.tooltip;"
+ oncommand="document.getBindingParent(this).userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE;"/>
+ <xul:menuitem anonid="always-activate-menuitem"
+ class="addon-control"
+ label="&cmd.alwaysActivate.label;"
+ tooltiptext="&cmd.alwaysActivate.tooltip;"
+ oncommand="document.getBindingParent(this).userDisabled = false;"/>
+ <xul:menuitem anonid="never-activate-menuitem"
+ class="addon-control"
+ label="&cmd.neverActivate.label;"
+ tooltiptext="&cmd.neverActivate.tooltip;"
+ oncommand="document.getBindingParent(this).userDisabled = true;"/>
+ </xul:menupopup>
+ </xul:menulist>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this._installStatus.mControl = this;
+
+ this.setAttribute("contextmenu", "addonitem-popup");
+
+ this._showStatus("none");
+
+ this._initWithAddon(this.mAddon);
+
+ gEventManager.registerAddonListener(this, this.mAddon.id);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ gEventManager.unregisterAddonListener(this, this.mAddon.id);
+ ]]></destructor>
+
+ <field name="_warningContainer">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "warning-container");
+ </field>
+ <field name="_warning">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "warning");
+ </field>
+ <field name="_warningLink">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "warning-link");
+ </field>
+ <field name="_warningBtn">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "warning-btn");
+ </field>
+ <field name="_errorContainer">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "error-container");
+ </field>
+ <field name="_error">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "error");
+ </field>
+ <field name="_errorLink">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "error-link");
+ </field>
+ <field name="_pendingContainer">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "pending-container");
+ </field>
+ <field name="_pending">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "pending");
+ </field>
+ <field name="_infoContainer">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "info-container");
+ </field>
+ <field name="_info">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "info");
+ </field>
+ <field name="_version">
+ document.getAnonymousElementByAttribute(this, "anonid", "version");
+ </field>
+ <field name="_experimentState">
+ document.getAnonymousElementByAttribute(this, "anonid", "experiment-state");
+ </field>
+ <field name="_experimentTime">
+ document.getAnonymousElementByAttribute(this, "anonid", "experiment-time");
+ </field>
+ <field name="_icon">
+ document.getAnonymousElementByAttribute(this, "anonid", "icon");
+ </field>
+ <field name="_dateUpdated">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "date-updated");
+ </field>
+ <field name="_description">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "description");
+ </field>
+ <field name="_stateMenulist">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "state-menulist");
+ </field>
+ <field name="_askToActivateMenuitem">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "ask-to-activate-menuitem");
+ </field>
+ <field name="_alwaysActivateMenuitem">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "always-activate-menuitem");
+ </field>
+ <field name="_neverActivateMenuitem">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "never-activate-menuitem");
+ </field>
+ <field name="_preferencesBtn">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "preferences-btn");
+ </field>
+ <field name="_enableBtn">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "enable-btn");
+ </field>
+ <field name="_disableBtn">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "disable-btn");
+ </field>
+ <field name="_removeBtn">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "remove-btn");
+ </field>
+ <field name="_updateBtn">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "update-btn");
+ </field>
+ <field name="_controlContainer">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "control-container");
+ </field>
+ <field name="_installStatus">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "install-status");
+ </field>
+ <field name="_checkingUpdate">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "checking-update");
+ </field>
+ <field name="_updateAvailable">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "update-available");
+ </field>
+ <field name="_includeUpdate">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "include-update");
+ </field>
+ <field name="_relNotesLoaded">false</field>
+ <field name="_relNotesToggle">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "relnotes-toggle-btn");
+ </field>
+ <field name="_relNotesLoading">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "relnotes-loading");
+ </field>
+ <field name="_relNotesError">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "relnotes-error");
+ </field>
+ <field name="_relNotesContainer">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "relnotes-container");
+ </field>
+ <field name="_relNotes">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "relnotes");
+ </field>
+
+ <property name="userDisabled">
+ <getter><![CDATA[
+ return this.mAddon.userDisabled;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mAddon.userDisabled = val;
+ ]]></setter>
+ </property>
+
+ <property name="includeUpdate">
+ <getter><![CDATA[
+ return this._includeUpdate.checked && !!this.mManualUpdate;
+ ]]></getter>
+ <setter><![CDATA[
+ //XXXunf Eventually, we'll want to persist this for individual
+ // updates - see bug 594619.
+ this._includeUpdate.checked = !!val;
+ ]]></setter>
+ </property>
+
+ <method name="_initWithAddon">
+ <parameter name="aAddon"/>
+ <body><![CDATA[
+ this.mAddon = aAddon;
+
+ this._installStatus.mAddon = this.mAddon;
+ this._updateDates();
+ this._updateState();
+
+ this.setAttribute("name", aAddon.name);
+
+ var iconURL = this.mAddon.iconURL;
+ if (iconURL)
+ this._icon.src = iconURL;
+ else
+ this._icon.src = "";
+
+ if (shouldShowVersionNumber(this.mAddon))
+ this._version.value = this.mAddon.version;
+ else
+ this._version.hidden = true;
+
+ if (this.mAddon.description)
+ this._description.value = this.mAddon.description;
+ else
+ this._description.hidden = true;
+
+ if (!("applyBackgroundUpdates" in this.mAddon) ||
+ (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE ||
+ (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DEFAULT &&
+ !AddonManager.autoUpdateDefault))) {
+ var self = this;
+ AddonManager.getAllInstalls(function(aInstallsList) {
+ // This can return after the binding has been destroyed,
+ // so try to detect that and return early
+ if (!("onNewInstall" in self))
+ return;
+ for (let install of aInstallsList) {
+ if (install.existingAddon &&
+ install.existingAddon.id == self.mAddon.id &&
+ install.state == AddonManager.STATE_AVAILABLE) {
+ self.onNewInstall(install);
+ self.onIncludeUpdateChanged();
+ }
+ }
+ });
+ }
+ ]]></body>
+ </method>
+
+ <method name="_showStatus">
+ <parameter name="aType"/>
+ <body><![CDATA[
+ this._controlContainer.hidden = aType != "none" &&
+ !(aType == "update-available" && !this.hasAttribute("upgrade"));
+
+ this._installStatus.hidden = aType != "progress";
+ if (aType == "progress")
+ this._installStatus.refreshState();
+ this._checkingUpdate.hidden = aType != "checking-update";
+ this._updateAvailable.hidden = aType != "update-available";
+ this._relNotesToggle.hidden = !(this.mManualUpdate ?
+ this.mManualUpdate.releaseNotesURI :
+ this.mAddon.releaseNotesURI);
+ ]]></body>
+ </method>
+
+ <method name="_updateDates">
+ <body><![CDATA[
+ function formatDate(aDate) {
+ return Cc["@mozilla.org/intl/scriptabledateformat;1"]
+ .getService(Ci.nsIScriptableDateFormat)
+ .FormatDate("",
+ Ci.nsIScriptableDateFormat.dateFormatLong,
+ aDate.getFullYear(),
+ aDate.getMonth() + 1,
+ aDate.getDate()
+ );
+ }
+
+ if (this.mAddon.updateDate)
+ this._dateUpdated.value = formatDate(this.mAddon.updateDate);
+ else
+ this._dateUpdated.value = this._dateUpdated.getAttribute("unknown");
+ ]]></body>
+ </method>
+
+ <method name="_updateState">
+ <body><![CDATA[
+ if (this.parentNode.selectedItem == this)
+ gViewController.updateCommands();
+
+ var pending = this.mAddon.pendingOperations;
+ if (pending != AddonManager.PENDING_NONE) {
+ this.removeAttribute("notification");
+
+ var pending = null;
+ const PENDING_OPERATIONS = ["enable", "disable", "install",
+ "uninstall", "upgrade"];
+ for (let op of PENDING_OPERATIONS) {
+ if (this.isPending(op))
+ pending = op;
+ }
+
+ this.setAttribute("pending", pending);
+ this._pending.textContent = gStrings.ext.formatStringFromName(
+ "notification." + pending,
+ [this.mAddon.name, gStrings.brandShortName], 2
+ );
+ } else {
+ this.removeAttribute("pending");
+
+ var isUpgrade = this.hasAttribute("upgrade");
+ var install = this._installStatus.mInstall;
+
+ if (install && install.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+ this.setAttribute("notification", "warning");
+ this._warning.textContent = gStrings.ext.formatStringFromName(
+ "notification.downloadError",
+ [this.mAddon.name], 1
+ );
+ this._warningBtn.label = gStrings.ext.GetStringFromName("notification.downloadError.retry");
+ this._warningBtn.tooltipText = gStrings.ext.GetStringFromName("notification.downloadError.retry.tooltip");
+ this._warningBtn.setAttribute("oncommand", "document.getBindingParent(this).retryInstall();");
+ this._warningBtn.hidden = false;
+ this._warningLink.hidden = true;
+ } else if (install && install.state == AddonManager.STATE_INSTALL_FAILED) {
+ this.setAttribute("notification", "warning");
+ this._warning.textContent = gStrings.ext.formatStringFromName(
+ "notification.installError",
+ [this.mAddon.name], 1
+ );
+ this._warningBtn.label = gStrings.ext.GetStringFromName("notification.installError.retry");
+ this._warningBtn.tooltipText = gStrings.ext.GetStringFromName("notification.downloadError.retry.tooltip");
+ this._warningBtn.setAttribute("oncommand", "document.getBindingParent(this).retryInstall();");
+ this._warningBtn.hidden = false;
+ this._warningLink.hidden = true;
+ } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ this.setAttribute("notification", "error");
+ this._error.textContent = gStrings.ext.formatStringFromName(
+ "notification.blocked",
+ [this.mAddon.name], 1
+ );
+ this._errorLink.value = gStrings.ext.GetStringFromName("notification.blocked.link");
+ this._errorLink.href = this.mAddon.blocklistURL;
+ this._errorLink.hidden = false;
+ } else if ((!isUpgrade && !this.mAddon.isCompatible) && (AddonManager.checkCompatibility
+ || (this.mAddon.blocklistState != Ci.nsIBlocklistService.STATE_SOFTBLOCKED))) {
+ this.setAttribute("notification", "warning");
+ this._warning.textContent = gStrings.ext.formatStringFromName(
+ "notification.incompatible",
+ [this.mAddon.name, gStrings.brandShortName, gStrings.appVersion], 3
+ );
+ this._warningLink.hidden = true;
+ this._warningBtn.hidden = true;
+ } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
+ this.setAttribute("notification", "warning");
+ this._warning.textContent = gStrings.ext.formatStringFromName(
+ "notification.softblocked",
+ [this.mAddon.name], 1
+ );
+ this._warningLink.value = gStrings.ext.GetStringFromName("notification.softblocked.link");
+ this._warningLink.href = this.mAddon.blocklistURL;
+ this._warningLink.hidden = false;
+ this._warningBtn.hidden = true;
+ } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) {
+ this.setAttribute("notification", "warning");
+ this._warning.textContent = gStrings.ext.formatStringFromName(
+ "notification.outdated",
+ [this.mAddon.name], 1
+ );
+ this._warningLink.hidden = true;
+ this._warningBtn.hidden = true;
+ } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) {
+ this.setAttribute("notification", "error");
+ this._error.textContent = gStrings.ext.formatStringFromName(
+ "notification.vulnerableUpdatable",
+ [this.mAddon.name], 1
+ );
+ this._errorLink.value = gStrings.ext.GetStringFromName("notification.vulnerableUpdatable.link");
+ this._errorLink.href = this.mAddon.blocklistURL;
+ this._errorLink.hidden = false;
+ } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) {
+ this.setAttribute("notification", "error");
+ this._error.textContent = gStrings.ext.formatStringFromName(
+ "notification.vulnerableNoUpdate",
+ [this.mAddon.name], 1
+ );
+ this._errorLink.value = gStrings.ext.GetStringFromName("notification.vulnerableNoUpdate.link");
+ this._errorLink.href = this.mAddon.blocklistURL;
+ this._errorLink.hidden = false;
+ } else if (this.mAddon.isGMPlugin && !this.mAddon.isInstalled &&
+ this.mAddon.isActive) {
+ this.setAttribute("notification", "warning");
+ this._warning.textContent =
+ gStrings.ext.formatStringFromName("notification.gmpPending",
+ [this.mAddon.name], 1);
+ } else {
+ this.removeAttribute("notification");
+ }
+ }
+
+ this._preferencesBtn.hidden = (!this.mAddon.optionsURL) ||
+ this.mAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO;
+
+ if (this.typeHasFlag("SUPPORTS_ASK_TO_ACTIVATE")) {
+ this._enableBtn.disabled = true;
+ this._disableBtn.disabled = true;
+ this._askToActivateMenuitem.disabled = !this.hasPermission("ask_to_activate");
+ this._alwaysActivateMenuitem.disabled = !this.hasPermission("enable");
+ this._neverActivateMenuitem.disabled = !this.hasPermission("disable");
+ if (!this.mAddon.isActive) {
+ this._stateMenulist.selectedItem = this._neverActivateMenuitem;
+ } else if (this.mAddon.userDisabled == AddonManager.STATE_ASK_TO_ACTIVATE) {
+ this._stateMenulist.selectedItem = this._askToActivateMenuitem;
+ } else {
+ this._stateMenulist.selectedItem = this._alwaysActivateMenuitem;
+ }
+ let hasActivatePermission =
+ ["ask_to_activate", "enable", "disable"].some(perm => this.hasPermission(perm));
+ this._stateMenulist.disabled = !hasActivatePermission;
+ this._stateMenulist.hidden = false;
+ this._stateMenulist.classList.add('no-auto-hide');
+ } else {
+ this._stateMenulist.hidden = true;
+ if (this.hasPermission("enable")) {
+ this._enableBtn.hidden = false;
+ let tooltip = gViewController.commands["cmd_enableItem"]
+ .getTooltip(this.mAddon);
+ this._enableBtn.setAttribute("tooltiptext", tooltip);
+ } else {
+ this._enableBtn.hidden = true;
+ }
+
+ if (this.hasPermission("disable")) {
+ this._disableBtn.hidden = false;
+ let tooltip = gViewController.commands["cmd_disableItem"]
+ .getTooltip(this.mAddon);
+ this._disableBtn.setAttribute("tooltiptext", tooltip);
+ } else {
+ this._disableBtn.hidden = true;
+ }
+ }
+
+ if (this.hasPermission("uninstall")) {
+ this._removeBtn.hidden = false;
+ let tooltip = gViewController.commands["cmd_uninstallItem"]
+ .getTooltip(this.mAddon);
+ this._removeBtn.setAttribute("tooltiptext", tooltip);
+ } else {
+ this._removeBtn.hidden = true;
+ }
+
+ this.setAttribute("active", this.mAddon.isActive);
+
+ var showProgress = this.mAddon.purchaseURL || (this.mAddon.install &&
+ this.mAddon.install.state != AddonManager.STATE_INSTALLED);
+ this._showStatus(showProgress ? "progress" : "none");
+
+ if (this.mAddon.type == "experiment") {
+ this.removeAttribute("notification");
+ let prefix = "experiment.";
+ let active = this.mAddon.isActive;
+
+ if (!showProgress) {
+ let stateKey = prefix + "state." + (active ? "active" : "complete");
+ this._experimentState.value = gStrings.ext.GetStringFromName(stateKey);
+
+ let now = Date.now();
+ let end = this.endDate;
+ let days = Math.abs(end - now) / (24 * 60 * 60 * 1000);
+
+ let timeKey = prefix + "time.";
+ let timeMessage;
+
+ if (days < 1) {
+ timeKey += (active ? "endsToday" : "endedToday");
+ timeMessage = gStrings.ext.GetStringFromName(timeKey);
+ } else {
+ timeKey += (active ? "daysRemaining" : "daysPassed");
+ days = Math.round(days);
+ let timeString = gStrings.ext.GetStringFromName(timeKey);
+ timeMessage = PluralForm.get(days, timeString)
+ .replace("#1", days);
+ }
+
+ this._experimentTime.value = timeMessage;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_updateUpgradeInfo">
+ <body><![CDATA[
+ // Only update the version string if we're displaying the upgrade info
+ if (this.hasAttribute("upgrade") && shouldShowVersionNumber(this.mAddon))
+ this._version.value = this.mManualUpdate.version;
+ ]]></body>
+ </method>
+
+ <method name="_fetchReleaseNotes">
+ <parameter name="aURI"/>
+ <body><![CDATA[
+ var self = this;
+ if (!aURI || this._relNotesLoaded) {
+ sendToggleEvent();
+ return;
+ }
+
+ var relNotesData = null, transformData = null;
+
+ this._relNotesLoaded = true;
+ this._relNotesLoading.hidden = false;
+ this._relNotesError.hidden = true;
+
+ function sendToggleEvent() {
+ var event = document.createEvent("Events");
+ event.initEvent("RelNotesToggle", true, true);
+ self.dispatchEvent(event);
+ }
+
+ function showRelNotes() {
+ if (!relNotesData || !transformData)
+ return;
+
+ self._relNotesLoading.hidden = true;
+
+ var processor = Components.classes["@mozilla.org/document-transformer;1?type=xslt"]
+ .createInstance(Components.interfaces.nsIXSLTProcessor);
+ processor.flags |= Components.interfaces.nsIXSLTProcessorPrivate.DISABLE_ALL_LOADS;
+
+ processor.importStylesheet(transformData);
+ var fragment = processor.transformToFragment(relNotesData, document);
+ self._relNotes.appendChild(fragment);
+ if (self.hasAttribute("show-relnotes")) {
+ var container = self._relNotesContainer;
+ container.style.height = container.scrollHeight + "px";
+ }
+ sendToggleEvent();
+ }
+
+ function handleError() {
+ dataReq.abort();
+ styleReq.abort();
+ self._relNotesLoading.hidden = true;
+ self._relNotesError.hidden = false;
+ self._relNotesLoaded = false; // allow loading to be re-tried
+ sendToggleEvent();
+ }
+
+ function handleResponse(aEvent) {
+ var req = aEvent.target;
+ var ct = req.getResponseHeader("content-type");
+ if ((!ct || ct.indexOf("text/html") < 0) &&
+ req.responseXML &&
+ req.responseXML.documentElement.namespaceURI != XMLURI_PARSE_ERROR) {
+ if (req == dataReq)
+ relNotesData = req.responseXML;
+ else
+ transformData = req.responseXML;
+ showRelNotes();
+ } else {
+ handleError();
+ }
+ }
+
+ var dataReq = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Components.interfaces.nsIXMLHttpRequest);
+ dataReq.open("GET", aURI.spec, true);
+ dataReq.addEventListener("load", handleResponse, false);
+ dataReq.addEventListener("error", handleError, false);
+ dataReq.send(null);
+
+ var styleReq = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Components.interfaces.nsIXMLHttpRequest);
+ styleReq.open("GET", UPDATES_RELEASENOTES_TRANSFORMFILE, true);
+ styleReq.addEventListener("load", handleResponse, false);
+ styleReq.addEventListener("error", handleError, false);
+ styleReq.send(null);
+ ]]></body>
+ </method>
+
+ <method name="toggleReleaseNotes">
+ <body><![CDATA[
+ if (this.hasAttribute("show-relnotes")) {
+ this._relNotesContainer.style.height = "0px";
+ this.removeAttribute("show-relnotes");
+ this._relNotesToggle.setAttribute(
+ "label",
+ this._relNotesToggle.getAttribute("showlabel")
+ );
+ this._relNotesToggle.setAttribute(
+ "tooltiptext",
+ this._relNotesToggle.getAttribute("showtooltip")
+ );
+ var event = document.createEvent("Events");
+ event.initEvent("RelNotesToggle", true, true);
+ this.dispatchEvent(event);
+ } else {
+ this._relNotesContainer.style.height = this._relNotesContainer.scrollHeight +
+ "px";
+ this.setAttribute("show-relnotes", true);
+ this._relNotesToggle.setAttribute(
+ "label",
+ this._relNotesToggle.getAttribute("hidelabel")
+ );
+ this._relNotesToggle.setAttribute(
+ "tooltiptext",
+ this._relNotesToggle.getAttribute("hidetooltip")
+ );
+ var uri = this.mManualUpdate ?
+ this.mManualUpdate.releaseNotesURI :
+ this.mAddon.releaseNotesURI;
+ this._fetchReleaseNotes(uri);
+ }
+ ]]></body>
+ </method>
+
+ <method name="restart">
+ <body><![CDATA[
+ gViewController.commands["cmd_restartApp"].doCommand();
+ ]]></body>
+ </method>
+
+ <method name="undo">
+ <body><![CDATA[
+ gViewController.commands["cmd_cancelOperation"].doCommand(this.mAddon);
+ ]]></body>
+ </method>
+
+ <method name="uninstall">
+ <body><![CDATA[
+ // If uninstalling does not require a restart and the type doesn't
+ // support undoing of restartless uninstalls, then we fake it by
+ // just disabling it it, and doing the real uninstall later.
+ if (!this.opRequiresRestart("uninstall") &&
+ !this.typeHasFlag("SUPPORTS_UNDO_RESTARTLESS_UNINSTALL")) {
+ this.setAttribute("wasDisabled", this.mAddon.userDisabled);
+
+ // We must set userDisabled to true first, this will call
+ // _updateState which will clear any pending attribute set.
+ this.mAddon.userDisabled = true;
+
+ // This won't update any other add-on manager views (bug 582002)
+ this.setAttribute("pending", "uninstall");
+ } else {
+ this.mAddon.uninstall(true);
+ }
+ ]]></body>
+ </method>
+
+#ifdef MOZ_DEVTOOLS
+ <method name="debug">
+ <body><![CDATA[
+ gViewController.doCommand("cmd_debugItem", this.mAddon);
+ ]]></body>
+ </method>
+#endif
+
+ <method name="showPreferences">
+ <body><![CDATA[
+ gViewController.doCommand("cmd_showItemPreferences", this.mAddon);
+ ]]></body>
+ </method>
+
+ <method name="upgrade">
+ <body><![CDATA[
+ var install = this.mManualUpdate;
+ delete this.mManualUpdate;
+ install.install();
+ ]]></body>
+ </method>
+
+ <method name="retryInstall">
+ <body><![CDATA[
+ var install = this._installStatus.mInstall;
+ if (!install)
+ return;
+ if (install.state != AddonManager.STATE_DOWNLOAD_FAILED &&
+ install.state != AddonManager.STATE_INSTALL_FAILED)
+ return;
+ install.install();
+ ]]></body>
+ </method>
+
+ <method name="showInDetailView">
+ <body><![CDATA[
+ gViewController.loadView("addons://detail/" +
+ encodeURIComponent(this.mAddon.id));
+ ]]></body>
+ </method>
+
+ <method name="onIncludeUpdateChanged">
+ <body><![CDATA[
+ var event = document.createEvent("Events");
+ event.initEvent("IncludeUpdateChanged", true, true);
+ this.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="onEnabling">
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onEnabled">
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onDisabling">
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onDisabled">
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onUninstalling">
+ <parameter name="aRestartRequired"/>
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onOperationCancelled">
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onPropertyChanged">
+ <parameter name="aProperties"/>
+ <body><![CDATA[
+ if (aProperties.indexOf("appDisabled") != -1 ||
+ aProperties.indexOf("userDisabled") != -1)
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onNoUpdateAvailable">
+ <body><![CDATA[
+ this._showStatus("none");
+ ]]></body>
+ </method>
+
+ <method name="onCheckingUpdate">
+ <body><![CDATA[
+ this._showStatus("checking-update");
+ ]]></body>
+ </method>
+
+ <method name="onCompatibilityUpdateAvailable">
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onExternalInstall">
+ <parameter name="aAddon"/>
+ <parameter name="aExistingAddon"/>
+ <parameter name="aNeedsRestart"/>
+ <body><![CDATA[
+ if (aExistingAddon.id != this.mAddon.id)
+ return;
+
+ // If the install completed without needing a restart then switch to
+ // using the new Addon
+ if (!aNeedsRestart)
+ this._initWithAddon(aAddon);
+ else
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onNewInstall">
+ <parameter name="aInstall"/>
+ <body><![CDATA[
+ if (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_ENABLE)
+ return;
+ if (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DEFAULT &&
+ AddonManager.autoUpdateDefault)
+ return;
+
+ this.mManualUpdate = aInstall;
+ this._showStatus("update-available");
+ this._updateUpgradeInfo();
+ ]]></body>
+ </method>
+
+ <method name="onDownloadStarted">
+ <parameter name="aInstall"/>
+ <body><![CDATA[
+ this._updateState();
+ this._showStatus("progress");
+ this._installStatus.initWithInstall(aInstall);
+ ]]></body>
+ </method>
+
+ <method name="onInstallStarted">
+ <parameter name="aInstall"/>
+ <body><![CDATA[
+ this._updateState();
+ this._showStatus("progress");
+ this._installStatus.initWithInstall(aInstall);
+ ]]></body>
+ </method>
+
+ <method name="onInstallEnded">
+ <parameter name="aInstall"/>
+ <parameter name="aAddon"/>
+ <body><![CDATA[
+ // If the install completed without needing a restart then switch to
+ // using the new Addon
+ if (!(aAddon.pendingOperations & AddonManager.PENDING_INSTALL))
+ this._initWithAddon(aAddon);
+ else
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onDownloadFailed">
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onInstallFailed">
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+
+ <method name="onInstallCancelled">
+ <body><![CDATA[
+ this._updateState();
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="click" button="0"><![CDATA[
+ switch (event.detail) {
+ case 1:
+ // Prevent double-click where the UI changes on the first click
+ this._lastClickTarget = event.originalTarget;
+ break;
+ case 2:
+ if (event.originalTarget.localName != 'button' &&
+ !event.originalTarget.classList.contains('text-link') &&
+ event.originalTarget == this._lastClickTarget) {
+ this.showInDetailView();
+ }
+ break;
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+
+ <!-- Addon - uninstalled - An uninstalled addon that can be re-installed. -->
+ <binding id="addon-uninstalled"
+ extends="chrome://mozapps/content/extensions/extensions.xml#addon-base">
+ <content>
+ <xul:hbox class="pending">
+ <xul:image class="pending-icon"/>
+ <xul:label anonid="notice" flex="1"/>
+ <xul:button anonid="restart-btn" class="button-link"
+ label="&addon.restartNow.label;"
+ command="cmd_restartApp"/>
+ <xul:button anonid="undo-btn" class="button-link"
+ label="&addon.undoRemove.label;"
+ tooltiptext="&addon.undoRemove.tooltip;"
+ oncommand="document.getBindingParent(this).cancelUninstall();"/>
+ <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this._notice.textContent = gStrings.ext.formatStringFromName("uninstallNotice",
+ [this.mAddon.name],
+ 1);
+
+ if (!this.opRequiresRestart("uninstall"))
+ this._restartBtn.setAttribute("hidden", true);
+
+ gEventManager.registerAddonListener(this, this.mAddon.id);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ gEventManager.unregisterAddonListener(this, this.mAddon.id);
+ ]]></destructor>
+
+ <field name="_notice" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "notice");
+ </field>
+ <field name="_restartBtn" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "restart-btn");
+ </field>
+
+ <method name="cancelUninstall">
+ <body><![CDATA[
+ // This assumes that disabling does not require a restart when
+ // uninstalling doesn't. Things will still work if not, the add-on
+ // will just still be active until finally getting uninstalled.
+
+ if (this.isPending("uninstall"))
+ this.mAddon.cancelUninstall();
+ else if (this.getAttribute("wasDisabled") != "true")
+ this.mAddon.userDisabled = false;
+
+ this.removeAttribute("pending");
+ ]]></body>
+ </method>
+
+ <method name="onOperationCancelled">
+ <body><![CDATA[
+ if (!this.isPending("uninstall"))
+ this.removeAttribute("pending");
+ ]]></body>
+ </method>
+
+ <method name="onExternalInstall">
+ <parameter name="aAddon"/>
+ <parameter name="aExistingAddon"/>
+ <parameter name="aNeedsRestart"/>
+ <body><![CDATA[
+ if (aExistingAddon.id != this.mAddon.id)
+ return;
+
+ // Make sure any newly installed add-on has the correct disabled state
+ if (this.hasAttribute("wasDisabled"))
+ aAddon.userDisabled = this.getAttribute("wasDisabled") == "true";
+
+ // If the install completed without needing a restart then switch to
+ // using the new Addon
+ if (!aNeedsRestart)
+ this.mAddon = aAddon;
+
+ this.removeAttribute("pending");
+ ]]></body>
+ </method>
+
+ <method name="onInstallStarted">
+ <parameter name="aInstall"/>
+ <body><![CDATA[
+ // Make sure any newly installed add-on has the correct disabled state
+ if (this.hasAttribute("wasDisabled"))
+ aInstall.addon.userDisabled = this.getAttribute("wasDisabled") == "true";
+ ]]></body>
+ </method>
+
+ <method name="onInstallEnded">
+ <parameter name="aInstall"/>
+ <parameter name="aAddon"/>
+ <body><![CDATA[
+ // If the install completed without needing a restart then switch to
+ // using the new Addon
+ if (!(aAddon.pendingOperations & AddonManager.PENDING_INSTALL))
+ this.mAddon = aAddon;
+
+ this.removeAttribute("pending");
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+
+ <!-- Addon - installing - an addon item that is currently being installed -->
+ <binding id="addon-installing"
+ extends="chrome://mozapps/content/extensions/extensions.xml#addon-base">
+ <content>
+ <xul:hbox anonid="warning-container" class="warning">
+ <xul:image class="warning-icon"/>
+ <xul:label anonid="warning" flex="1"/>
+ <xul:button anonid="warning-link" class="button-link"
+ oncommand="document.getBindingParent(this).retryInstall();"/>
+ <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </xul:hbox>
+ <xul:hbox class="content-container">
+ <xul:vbox class="icon-outer-container">
+ <xul:vbox class="icon-container">
+ <xul:image anonid="icon" class="icon"/>
+ </xul:vbox>
+ </xul:vbox>
+ <xul:vbox class="fade name-outer-container" flex="1">
+ <xul:hbox class="name-container">
+ <xul:label anonid="name" class="name" crop="end"/>
+ <xul:label anonid="version" class="version" hidden="true"/>
+ </xul:hbox>
+ </xul:vbox>
+ <xul:vbox class="install-status-container">
+ <xul:hbox anonid="install-status" class="install-status"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this._installStatus.mControl = this;
+ this._installStatus.mInstall = this.mInstall;
+ this.refreshInfo();
+ ]]></constructor>
+
+ <field name="_icon">
+ document.getAnonymousElementByAttribute(this, "anonid", "icon");
+ </field>
+ <field name="_name">
+ document.getAnonymousElementByAttribute(this, "anonid", "name");
+ </field>
+ <field name="_version">
+ document.getAnonymousElementByAttribute(this, "anonid", "version");
+ </field>
+ <field name="_warning">
+ document.getAnonymousElementByAttribute(this, "anonid", "warning");
+ </field>
+ <field name="_warningLink">
+ document.getAnonymousElementByAttribute(this, "anonid", "warning-link");
+ </field>
+ <field name="_installStatus">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "install-status");
+ </field>
+
+ <method name="onInstallCompleted">
+ <body><![CDATA[
+ this.mAddon = this.mInstall.addon;
+ this.setAttribute("name", this.mAddon.name);
+ this.setAttribute("value", this.mAddon.id);
+ this.setAttribute("status", "installed");
+ ]]></body>
+ </method>
+
+ <method name="refreshInfo">
+ <body><![CDATA[
+ this.mAddon = this.mAddon || this.mInstall.addon;
+ if (this.mAddon) {
+ this._icon.src = this.mAddon.iconURL ||
+ (this.mInstall ? this.mInstall.iconURL : "");
+ this._name.value = this.mAddon.name;
+
+ if (this.mAddon.version) {
+ this._version.value = this.mAddon.version;
+ this._version.hidden = false;
+ } else {
+ this._version.hidden = true;
+ }
+
+ } else {
+ this._icon.src = this.mInstall.iconURL;
+ // AddonInstall.name isn't always available - fallback to filename
+ if (this.mInstall.name) {
+ this._name.value = this.mInstall.name;
+ } else if (this.mInstall.sourceURI) {
+ var url = Components.classes["@mozilla.org/network/standard-url;1"]
+ .createInstance(Components.interfaces.nsIStandardURL);
+ url.init(url.URLTYPE_STANDARD, 80, this.mInstall.sourceURI.spec,
+ null, null);
+ url.QueryInterface(Components.interfaces.nsIURL);
+ this._name.value = url.fileName;
+ }
+
+ if (this.mInstall.version) {
+ this._version.value = this.mInstall.version;
+ this._version.hidden = false;
+ } else {
+ this._version.hidden = true;
+ }
+ }
+
+ if (this.mInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+ this.setAttribute("notification", "warning");
+ this._warning.textContent = gStrings.ext.formatStringFromName(
+ "notification.downloadError",
+ [this._name.value], 1
+ );
+ this._warningLink.label = gStrings.ext.GetStringFromName("notification.downloadError.retry");
+ this._warningLink.tooltipText = gStrings.ext.GetStringFromName("notification.downloadError.retry.tooltip");
+ } else if (this.mInstall.state == AddonManager.STATE_INSTALL_FAILED) {
+ this.setAttribute("notification", "warning");
+ this._warning.textContent = gStrings.ext.formatStringFromName(
+ "notification.installError",
+ [this._name.value], 1
+ );
+ this._warningLink.label = gStrings.ext.GetStringFromName("notification.installError.retry");
+ this._warningLink.tooltipText = gStrings.ext.GetStringFromName("notification.downloadError.retry.tooltip");
+ } else {
+ this.removeAttribute("notification");
+ }
+ ]]></body>
+ </method>
+
+ <method name="retryInstall">
+ <body><![CDATA[
+ this.mInstall.install();
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="detail-row">
+ <content>
+ <xul:label class="detail-row-label" xbl:inherits="value=label"/>
+ <xul:label class="detail-row-value" xbl:inherits="value"/>
+ </content>
+
+ <implementation>
+ <property name="value">
+ <getter><![CDATA[
+ return this.getAttribute("value");
+ ]]></getter>
+ <setter><![CDATA[
+ if (!val)
+ this.removeAttribute("value");
+ else
+ this.setAttribute("value", val);
+ ]]></setter>
+ </property>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/components/extensions/content/extensions.xul b/components/extensions/content/extensions.xul
new file mode 100644
index 000000000..70ce55fa2
--- /dev/null
+++ b/components/extensions/content/extensions.xul
@@ -0,0 +1,685 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/content/extensions/extensions.css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/extensions/extensions.css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/extensions/about.css"?>
+
+<!DOCTYPE page [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/extensions.dtd">
+%extensionsDTD;
+]>
+
+<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"
+ id="addons-page" title="&addons.windowTitle;"
+ role="application" windowtype="Addons:Manager"
+ disablefastfind="true">
+
+ <xhtml:link rel="shortcut icon"
+ href="chrome://mozapps/skin/extensions/extensionGeneric-16.png"/>
+
+ <script type="application/javascript"
+ src="chrome://mozapps/content/extensions/extensions.js"/>
+ <script type="application/javascript"
+ src="chrome://global/content/contentAreaUtils.js"/>
+
+ <popupset>
+ <!-- menu for an addon item -->
+ <menupopup id="addonitem-popup">
+ <menuitem id="menuitem_showDetails" command="cmd_showItemDetails"
+ default="true" label="&cmd.showDetails.label;"
+ accesskey="&cmd.showDetails.accesskey;"/>
+ <menuitem id="menuitem_enableItem" command="cmd_enableItem"
+ label="&cmd.enableAddon.label;"
+ accesskey="&cmd.enableAddon.accesskey;"/>
+ <menuitem id="menuitem_disableItem" command="cmd_disableItem"
+ label="&cmd.disableAddon.label;"
+ accesskey="&cmd.disableAddon.accesskey;"/>
+ <menuitem id="menuitem_enableTheme" command="cmd_enableItem"
+ label="&cmd.enableTheme.label;"
+ accesskey="&cmd.enableTheme.accesskey;"/>
+ <menuitem id="menuitem_disableTheme" command="cmd_disableItem"
+ label="&cmd.disableTheme.label;"
+ accesskey="&cmd.disableTheme.accesskey;"/>
+ <menuitem id="menuitem_installItem" command="cmd_installItem"
+ label="&cmd.installAddon.label;"
+ accesskey="&cmd.installAddon.accesskey;"/>
+ <menuitem id="menuitem_uninstallItem" command="cmd_uninstallItem"
+ label="&cmd.uninstallAddon.label;"
+ accesskey="&cmd.uninstallAddon.accesskey;"/>
+#ifdef MOZ_DEVTOOLS
+ <menuitem id="menuitem_debugItem" command="cmd_debugItem"
+ label="&cmd.debugAddon.label;"/>
+#endif
+ <menuseparator id="addonitem-menuseparator" />
+ <menuitem id="menuitem_preferences" command="cmd_showItemPreferences"
+#ifdef XP_WIN
+ label="&cmd.preferencesWin.label;"
+ accesskey="&cmd.preferencesWin.accesskey;"/>
+#else
+ label="&cmd.preferencesUnix.label;"
+ accesskey="&cmd.preferencesUnix.accesskey;"/>
+#endif
+ <menuitem id="menuitem_findUpdates" command="cmd_findItemUpdates"
+ label="&cmd.findUpdates.label;"
+ accesskey="&cmd.findUpdates.accesskey;"/>
+ <menuitem id="menuitem_about" command="cmd_showItemAbout"
+ label="&cmd.about.label;"
+ accesskey="&cmd.about.accesskey;"/>
+ </menupopup>
+ </popupset>
+
+ <!-- global commands - these act on all addons, or affect the addons manager
+ in some other way -->
+ <commandset id="globalCommandSet">
+ <command id="cmd_focusSearch"/>
+ <command id="cmd_findAllUpdates"/>
+ <command id="cmd_restartApp"/>
+ <command id="cmd_goToDiscoverPane"/>
+ <command id="cmd_goToRecentUpdates"/>
+ <command id="cmd_goToAvailableUpdates"/>
+ <command id="cmd_installFromFile"/>
+ <command id="cmd_back"/>
+ <command id="cmd_forward"/>
+ <command id="cmd_enableCheckCompatibility"/>
+ <command id="cmd_enableUpdateSecurity"/>
+ <command id="cmd_toggleAutoUpdateDefault"/>
+ <command id="cmd_resetAddonAutoUpdate"/>
+ <command id="cmd_experimentsLearnMore"/>
+ <command id="cmd_experimentsOpenTelemetryPreferences"/>
+ </commandset>
+
+ <!-- view commands - these act on the selected addon -->
+ <commandset id="viewCommandSet"
+ events="richlistbox-select" commandupdater="true">
+ <command id="cmd_showItemDetails"/>
+ <command id="cmd_findItemUpdates"/>
+ <command id="cmd_showItemPreferences"/>
+ <command id="cmd_showItemAbout"/>
+#ifdef MOZ_DEVTOOLS
+ <command id="cmd_debugItem"/>
+#endif
+ <command id="cmd_enableItem"/>
+ <command id="cmd_disableItem"/>
+ <command id="cmd_installItem"/>
+ <command id="cmd_purchaseItem"/>
+ <command id="cmd_uninstallItem"/>
+ <command id="cmd_cancelUninstallItem"/>
+ <command id="cmd_cancelOperation"/>
+ <command id="cmd_contribute"/>
+ <command id="cmd_askToActivateItem"/>
+ <command id="cmd_alwaysActivateItem"/>
+ <command id="cmd_neverActivateItem"/>
+ </commandset>
+
+ <keyset>
+ <!-- XXXunf Disabled until bug 371900 is fixed. -->
+ <key id="focusSearch" key="&search.commandkey;" modifiers="accel"
+ disabled="true"/>
+ </keyset>
+
+ <!-- main header -->
+ <hbox id="header" align="center">
+ <toolbarbutton id="back-btn" class="nav-button header-button" command="cmd_back"
+ tooltiptext="&cmd.back.tooltip;" hidden="true" disabled="true"/>
+ <toolbarbutton id="forward-btn" class="nav-button header-button" command="cmd_forward"
+ tooltiptext="&cmd.forward.tooltip;" hidden="true" disabled="true"/>
+ <spacer flex="1"/>
+ <hbox id="updates-container" align="center">
+ <image class="spinner"/>
+ <label id="updates-noneFound" hidden="true"
+ value="&updates.noneFound.label;"/>
+ <button id="updates-manualUpdatesFound-btn" class="button-link"
+ hidden="true" label="&updates.manualUpdatesFound.label;"
+ command="cmd_goToAvailableUpdates"/>
+ <label id="updates-progress" hidden="true"
+ value="&updates.updating.label;"/>
+ <label id="updates-installed" hidden="true"
+ value="&updates.installed.label;"/>
+ <label id="updates-downloaded" hidden="true"
+ value="&updates.downloaded.label;"/>
+ <button id="updates-restart-btn" class="button-link" hidden="true"
+ label="&updates.restart.label;"
+ command="cmd_restartApp"/>
+ </hbox>
+ <toolbarbutton id="header-utils-btn" class="header-button" type="menu"
+ tooltiptext="&toolsMenu.tooltip;">
+ <menupopup id="utils-menu">
+ <menuitem id="utils-updateNow"
+ label="&updates.checkForUpdates.label;"
+ accesskey="&updates.checkForUpdates.accesskey;"
+ command="cmd_findAllUpdates"/>
+ <menuitem id="utils-viewUpdates"
+ label="&updates.viewUpdates.label;"
+ accesskey="&updates.viewUpdates.accesskey;"
+ command="cmd_goToRecentUpdates"/>
+ <menuseparator id="utils-installFromFile-separator"/>
+ <menuitem id="utils-installFromFile"
+ label="&installAddonFromFile.label;"
+ accesskey="&installAddonFromFile.accesskey;"
+ command="cmd_installFromFile"/>
+ <menuseparator/>
+ <menuitem id="utils-autoUpdateDefault"
+ label="&updates.updateAddonsAutomatically.label;"
+ accesskey="&updates.updateAddonsAutomatically.accesskey;"
+ type="checkbox" autocheck="false"
+ command="cmd_toggleAutoUpdateDefault"/>
+ <menuitem id="utils-resetAddonUpdatesToAutomatic"
+ label="&updates.resetUpdatesToAutomatic.label;"
+ accesskey="&updates.resetUpdatesToAutomatic.accesskey;"
+ command="cmd_resetAddonAutoUpdate"/>
+ <menuitem id="utils-resetAddonUpdatesToManual"
+ label="&updates.resetUpdatesToManual.label;"
+ accesskey="&updates.resetUpdatesToManual.accesskey;"
+ command="cmd_resetAddonAutoUpdate"/>
+ </menupopup>
+ </toolbarbutton>
+ <textbox id="header-search" type="search" searchbutton="true"
+ searchbuttonlabel="&search.buttonlabel;"
+ placeholder="&search.placeholder;"/>
+ </hbox>
+
+ <hbox id="main" flex="1">
+
+ <!-- category list -->
+ <richlistbox id="categories">
+ <richlistitem id="category-search" value="addons://search/"
+ class="category"
+ name="&view.search.label;" priority="0"
+ tooltiptext="&view.search.label;" disabled="true"/>
+ <richlistitem id="category-discover" value="addons://discover/"
+ class="category"
+ name="&view.discover.label;" priority="1000"
+ tooltiptext="&view.discover.label;"/>
+ <richlistitem id="category-availableUpdates" value="addons://updates/available"
+ class="category"
+ name="&view.availableUpdates.label;" priority="100000"
+ tooltiptext="&view.availableUpdates.label;"
+ disabled="true"/>
+ <richlistitem id="category-recentUpdates" value="addons://updates/recent"
+ class="category"
+ name="&view.recentUpdates.label;" priority="101000"
+ tooltiptext="&view.recentUpdates.label;" disabled="true"/>
+ </richlistbox>
+
+ <box id="view-port-container" class="main-content" flex="1">
+
+ <!-- view port -->
+ <deck id="view-port" flex="1" selectedIndex="0">
+
+ <!-- discover view -->
+ <deck id="discover-view" flex="1" class="view-pane" selectedIndex="0" tabindex="0">
+ <vbox id="discover-loading" align="center" pack="stretch" flex="1" class="alert-container">
+ <spacer class="alert-spacer-before"/>
+ <hbox class="alert loading" align="center">
+ <image/>
+ <label value="&loading.label;"/>
+ </hbox>
+ <spacer class="alert-spacer-after"/>
+ </vbox>
+ <vbox id="discover-error" align="center" pack="stretch" flex="1" class="alert-container">
+ <spacer class="alert-spacer-before"/>
+ <hbox>
+ <spacer class="discover-spacer-before"/>
+ <hbox class="alert" align="center">
+ <image class="discover-logo"/>
+ <vbox flex="1" align="stretch">
+ <label class="discover-title">&discover.title;</label>
+ <description class="discover-description">&discover.description2;</description>
+ <description class="discover-footer">&discover.footer;</description>
+ </vbox>
+ </hbox>
+ <spacer class="discover-spacer-after"/>
+ </hbox>
+ <spacer class="alert-spacer-after"/>
+ </vbox>
+ <browser id="discover-browser" type="content" flex="1"
+ disablehistory="true" homepage="about:blank"/>
+ </deck>
+
+ <!-- search view -->
+ <vbox id="search-view" flex="1" class="view-pane" tabindex="0">
+ <hbox class="view-header global-warning-container" align="center">
+ <!-- global warnings -->
+ <hbox class="global-warning" flex="1">
+ <hbox class="global-warning-safemode" flex="1" align="center"
+ tooltiptext="&warning.safemode.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.safemode.label;"/>
+ </hbox>
+ <hbox class="global-warning-checkcompatibility" flex="1" align="center"
+ tooltiptext="&warning.checkcompatibility.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.checkcompatibility.label;"/>
+ </hbox>
+ <button class="button-link global-warning-checkcompatibility"
+ label="&warning.checkcompatibility.enable.label;"
+ tooltiptext="&warning.checkcompatibility.enable.tooltip;"
+ command="cmd_enableCheckCompatibility"/>
+ <hbox class="global-warning-updatesecurity" flex="1" align="center"
+ tooltiptext="&warning.updatesecurity.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.updatesecurity.label;"/>
+ </hbox>
+ <button class="button-link global-warning-updatesecurity"
+ label="&warning.updatesecurity.enable.label;"
+ tooltiptext="&warning.updatesecurity.enable.tooltip;"
+ command="cmd_enableUpdateSecurity"/>
+ <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </hbox>
+ <spacer flex="1"/>
+ <hbox id="search-sorters" class="sort-controls"
+ showrelevance="true" sortby="relevancescore" ascending="false"/>
+ </hbox>
+ <hbox id="search-filter" align="center">
+ <label id="search-filter-label" value="&search.filter2.label;"/>
+ <radiogroup id="search-filter-radiogroup" orient="horizontal"
+ align="center" persist="value" value="remote">
+ <radio id="search-filter-local" class="search-filter-radio"
+ label="&search.filter2.installed.label;" value="local"
+ tooltiptext="&search.filter2.installed.tooltip;"/>
+ <radio id="search-filter-remote" class="search-filter-radio"
+ label="&search.filter2.available.label;" value="remote"
+ tooltiptext="&search.filter2.available.tooltip;"/>
+ </radiogroup>
+ </hbox>
+ <vbox id="search-loading" class="alert-container"
+ flex="1" hidden="true">
+ <spacer class="alert-spacer-before"/>
+ <hbox class="alert loading" align="center">
+ <image/>
+ <label value="&loading.label;"/>
+ </hbox>
+ <spacer class="alert-spacer-after"/>
+ </vbox>
+ <vbox id="search-list-empty" class="alert-container"
+ flex="1" hidden="true">
+ <spacer class="alert-spacer-before"/>
+ <vbox class="alert">
+ <label value="&listEmpty.search.label;"/>
+ <button class="discover-button"
+ id="discover-button-search"
+ label="&listEmpty.button.label;"
+ command="cmd_goToDiscoverPane"/>
+ </vbox>
+ <spacer class="alert-spacer-after"/>
+ </vbox>
+ <richlistbox id="search-list" class="list" flex="1">
+ <hbox pack="center">
+ <label id="search-allresults-link" class="text-link"/>
+ </hbox>
+ </richlistbox>
+ </vbox>
+
+ <!-- list view -->
+ <vbox id="list-view" flex="1" class="view-pane" align="stretch" tabindex="0">
+ <hbox class="view-header global-warning-container">
+ <!-- global warnings -->
+ <hbox class="global-warning" flex="1">
+ <hbox class="global-warning-safemode" flex="1" align="center"
+ tooltiptext="&warning.safemode.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.safemode.label;"/>
+ </hbox>
+ <hbox class="global-warning-checkcompatibility" flex="1" align="center"
+ tooltiptext="&warning.checkcompatibility.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.checkcompatibility.label;"/>
+ </hbox>
+ <button class="button-link global-warning-checkcompatibility"
+ label="&warning.checkcompatibility.enable.label;"
+ tooltiptext="&warning.checkcompatibility.enable.tooltip;"
+ command="cmd_enableCheckCompatibility"/>
+ <hbox class="global-warning-updatesecurity" flex="1" align="center"
+ tooltiptext="&warning.updatesecurity.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.updatesecurity.label;"/>
+ </hbox>
+ <button class="button-link global-warning-updatesecurity"
+ label="&warning.updatesecurity.enable.label;"
+ tooltiptext="&warning.updatesecurity.enable.tooltip;"
+ command="cmd_enableUpdateSecurity"/>
+ <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </hbox>
+ </hbox>
+ <hbox class="view-header global-info-container experiment-info-container">
+ <hbox class="global-info" flex="1" align="center">
+ <label value="&experiment.info.label;"/>
+ <button id="experiments-learn-more"
+ label="&experiment.info.learnmore;"
+ tooltiptext="&experiment.info.learnmore;"
+ accesskey="&experiment.info.learnmore.accesskey;"
+ command="cmd_experimentsLearnMore"/>
+ <button id="experiments-change-telemetry"
+ label="&experiment.info.changetelemetry;"
+ tooltiptext="&experiment.info.changetelemetry;"
+ accesskey="&experiment.info.changetelemetry.accesskey;"
+ command="cmd_experimentsOpenTelemetryPreferences"/>
+ <spacer flex="5000"/> <!-- Necessary to allow the message to wrap. -->
+ </hbox>
+ </hbox>
+ <vbox id="addon-list-empty" class="alert-container"
+ flex="1" hidden="true">
+ <spacer class="alert-spacer-before"/>
+ <vbox class="alert">
+ <label value="&listEmpty.installed.label;"/>
+ <button class="discover-button"
+ id="discover-button-install"
+ label="&listEmpty.button.label;"
+ command="cmd_goToDiscoverPane"/>
+ </vbox>
+ <spacer class="alert-spacer-after"/>
+ </vbox>
+ <richlistbox id="addon-list" class="list" flex="1"/>
+ </vbox>
+
+ <!-- updates view -->
+ <vbox id="updates-view" flex="1" class="view-pane" tabindex="0">
+ <hbox class="view-header global-warning-container" align="center">
+ <!-- global warnings -->
+ <hbox class="global-warning" flex="1">
+ <hbox class="global-warning-safemode" flex="1" align="center"
+ tooltiptext="&warning.safemode.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.safemode.label;"/>
+ </hbox>
+ <hbox class="global-warning-checkcompatibility" flex="1" align="center"
+ tooltiptext="&warning.checkcompatibility.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.checkcompatibility.label;"/>
+ </hbox>
+ <button class="button-link global-warning-checkcompatibility"
+ label="&warning.checkcompatibility.enable.label;"
+ tooltiptext="&warning.checkcompatibility.enable.tooltip;"
+ command="cmd_enableCheckCompatibility"/>
+ <hbox class="global-warning-updatesecurity" flex="1" align="center"
+ tooltiptext="&warning.updatesecurity.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.updatesecurity.label;"/>
+ </hbox>
+ <button class="button-link global-warning-updatesecurity"
+ label="&warning.updatesecurity.enable.label;"
+ tooltiptext="&warning.updatesecurity.enable.tooltip;"
+ command="cmd_enableUpdateSecurity"/>
+ <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </hbox>
+ <spacer flex="1"/>
+ <hbox id="updates-sorters" class="sort-controls" sortby="updateDate"
+ ascending="false"/>
+ </hbox>
+ <vbox id="updates-list-empty" class="alert-container"
+ flex="1" hidden="true">
+ <spacer class="alert-spacer-before"/>
+ <vbox class="alert">
+ <label id="empty-availableUpdates-msg" value="&listEmpty.availableUpdates.label;"/>
+ <label id="empty-recentUpdates-msg" value="&listEmpty.recentUpdates.label;"/>
+ <button label="&listEmpty.findUpdates.label;"
+ command="cmd_findAllUpdates"/>
+ </vbox>
+ <spacer class="alert-spacer-after"/>
+ </vbox>
+ <hbox id="update-actions" pack="center">
+ <button id="update-selected-btn" hidden="true"
+ label="&updates.updateSelected.label;"
+ tooltiptext="&updates.updateSelected.tooltip;"/>
+ </hbox>
+ <richlistbox id="updates-list" class="list" flex="1"/>
+ </vbox>
+
+ <!-- detail view -->
+ <scrollbox id="detail-view" flex="1" class="view-pane addon-view" orient="vertical" tabindex="0"
+ role="document">
+ <!-- global warnings -->
+ <hbox class="global-warning-container global-warning">
+ <hbox class="global-warning-safemode" flex="1" align="center"
+ tooltiptext="&warning.safemode.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.safemode.label;"/>
+ </hbox>
+ <hbox class="global-warning-checkcompatibility" flex="1" align="center"
+ tooltiptext="&warning.checkcompatibility.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.checkcompatibility.label;"/>
+ </hbox>
+ <button class="button-link global-warning-checkcompatibility"
+ label="&warning.checkcompatibility.enable.label;"
+ tooltiptext="&warning.checkcompatibility.enable.tooltip;"
+ command="cmd_enableCheckCompatibility"/>
+ <hbox class="global-warning-updatesecurity" flex="1" align="center"
+ tooltiptext="&warning.updatesecurity.label;">
+ <image class="warning-icon"/>
+ <label class="global-warning-text" flex="1" crop="end"
+ value="&warning.updatesecurity.label;"/>
+ </hbox>
+ <button class="button-link global-warning-updatesecurity"
+ label="&warning.updatesecurity.enable.label;"
+ tooltiptext="&warning.updatesecurity.enable.tooltip;"
+ command="cmd_enableUpdateSecurity"/>
+ <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </hbox>
+ <hbox flex="1">
+ <spacer flex="1"/>
+ <!-- "loading" splash screen -->
+ <vbox class="alert-container">
+ <spacer class="alert-spacer-before"/>
+ <hbox class="alert loading">
+ <image/>
+ <label value="&loading.label;"/>
+ </hbox>
+ <spacer class="alert-spacer-after"/>
+ </vbox>
+ <!-- actual detail view -->
+ <vbox class="detail-view-container" flex="3" contextmenu="addonitem-popup">
+ <vbox id="detail-notifications">
+ <hbox id="warning-container" align="center" class="warning">
+ <image class="warning-icon"/>
+ <label id="detail-warning" flex="1"/>
+ <label id="detail-warning-link" class="text-link"/>
+ <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </hbox>
+ <hbox id="error-container" align="center" class="error">
+ <image class="error-icon"/>
+ <label id="detail-error" flex="1"/>
+ <label id="detail-error-link" class="text-link"/>
+ <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </hbox>
+ <hbox id="pending-container" align="center" class="pending">
+ <image class="pending-icon"/>
+ <label id="detail-pending" flex="1"/>
+ <button id="detail-restart-btn" class="button-link"
+ label="&addon.restartNow.label;"
+ command="cmd_restartApp"/>
+ <button id="detail-undo-btn" class="button-link"
+ label="&addon.undoAction.label;"
+ tooltipText="&addon.undoAction.tooltip;"
+ command="cmd_cancelOperation"/>
+ <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+ </hbox>
+ </vbox>
+ <hbox align="start">
+ <vbox id="detail-icon-container" align="end">
+ <image id="detail-icon" class="icon"/>
+ </vbox>
+ <vbox flex="1">
+ <vbox id="detail-summary">
+ <hbox id="detail-name-container" class="name-container"
+ align="start">
+ <label id="detail-name" flex="1"/>
+ <label id="detail-version"/>
+ <label class="disabled-postfix" value="&addon.disabled.postfix;"/>
+ <label class="update-postfix" value="&addon.update.postfix;"/>
+ <spacer flex="5000"/> <!-- Necessary to allow the name to wrap -->
+ </hbox>
+ <label id="detail-creator" class="creator"/>
+ <label id="detail-translators" class="translators"/>
+ </vbox>
+ <hbox id="detail-experiment-container">
+ <svg width="8" height="8" viewBox="0 0 8 8" version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ id="detail-experiment-bullet-container">
+ <circle cx="4" cy="4" r="4" id="detail-experiment-bullet"/>
+ </svg>
+ <label id="detail-experiment-state"/>
+ <label id="detail-experiment-time"/>
+ </hbox>
+ <hbox id="detail-desc-container" align="start">
+ <vbox pack="center"> <!-- Necessary to work around bug 394738 -->
+ <image id="detail-screenshot" hidden="true"/>
+ </vbox>
+ <vbox flex="1">
+ <description id="detail-desc"/>
+ <description id="detail-fulldesc"/>
+ </vbox>
+ </hbox>
+ <vbox id="detail-contributions">
+ <description id="detail-contrib-description">
+ &detail.contributions.description;
+ </description>
+ <hbox align="center">
+ <label id="detail-contrib-suggested"/>
+ <spacer flex="1"/>
+ <button id="detail-contrib-btn"
+ label="&cmd.contribute.label;"
+ accesskey="&cmd.contribute.accesskey;"
+ tooltiptext="&cmd.contribute.tooltip;"
+ command="cmd_contribute"/>
+ </hbox>
+ </vbox>
+ <grid id="detail-grid">
+ <columns>
+ <column flex="1"/>
+ <column flex="2"/>
+ </columns>
+ <rows id="detail-rows">
+ <row class="detail-row-complex" id="detail-updates-row">
+ <label class="detail-row-label" value="&detail.updateType;"/>
+ <hbox align="center">
+ <radiogroup id="detail-autoUpdate" orient="horizontal">
+ <!-- The values here need to match the values of
+ AddonManager.AUTOUPDATE_* -->
+ <radio label="&detail.updateDefault.label;"
+ tooltiptext="&detail.updateDefault.tooltip;"
+ value="1"/>
+ <radio label="&detail.updateAutomatic.label;"
+ tooltiptext="&detail.updateAutomatic.tooltip;"
+ value="2"/>
+ <radio label="&detail.updateManual.label;"
+ tooltiptext="&detail.updateManual.tooltip;"
+ value="0"/>
+ </radiogroup>
+ <button id="detail-findUpdates-btn" class="button-link"
+ label="&detail.checkForUpdates.label;"
+ accesskey="&detail.checkForUpdates.accesskey;"
+ tooltiptext="&detail.checkForUpdates.tooltip;"
+ command="cmd_findItemUpdates"/>
+ </hbox>
+ </row>
+ <row class="detail-row" id="detail-dateUpdated" label="&detail.lastupdated.label;"/>
+ <row class="detail-row-complex" id="detail-homepage-row" label="&detail.home;">
+ <label class="detail-row-label" value="&detail.home;"/>
+ <label id="detail-homepage" class="detail-row-value text-link" crop="end"/>
+ </row>
+ <row class="detail-row-complex" id="detail-repository-row" label="&detail.repository;">
+ <label class="detail-row-label" value="&detail.repository;"/>
+ <label id="detail-repository" class="detail-row-value text-link"/>
+ </row>
+ <row class="detail-row" id="detail-size" label="&detail.size;"/>
+ <row class="detail-row-complex" id="detail-rating-row">
+ <label class="detail-row-label" value="&rating2.label;"/>
+ <hbox>
+ <label id="detail-rating" class="meta-value meta-rating"
+ showrating="average"/>
+ <label id="detail-reviews" class="text-link"/>
+ </hbox>
+ </row>
+ <row class="detail-row" id="detail-downloads" label="&detail.numberOfDownloads.label;"/>
+ </rows>
+ </grid>
+ <hbox id="detail-controls">
+ <button id="detail-prefs-btn" class="addon-control preferences"
+#ifdef XP_WIN
+ label="&detail.showPreferencesWin.label;"
+ accesskey="&detail.showPreferencesWin.accesskey;"
+ tooltiptext="&detail.showPreferencesWin.tooltip;"
+#else
+ label="&detail.showPreferencesUnix.label;"
+ accesskey="&detail.showPreferencesUnix.accesskey;"
+ tooltiptext="&detail.showPreferencesUnix.tooltip;"
+#endif
+ command="cmd_showItemPreferences"/>
+ <spacer flex="1"/>
+#ifdef MOZ_DEVTOOLS
+ <button id="detail-debug-btn" class="addon-control debug"
+ label="Debug"
+ command="cmd_debugItem" />
+#endif
+ <button id="detail-enable-btn" class="addon-control enable"
+ label="&cmd.enableAddon.label;"
+ accesskey="&cmd.enableAddon.accesskey;"
+ command="cmd_enableItem"/>
+ <button id="detail-disable-btn" class="addon-control disable"
+ label="&cmd.disableAddon.label;"
+ accesskey="&cmd.disableAddon.accesskey;"
+ command="cmd_disableItem"/>
+ <button id="detail-uninstall-btn" class="addon-control remove"
+ label="&cmd.uninstallAddon.label;"
+ accesskey="&cmd.uninstallAddon.accesskey;"
+ command="cmd_uninstallItem"/>
+ <button id="detail-purchase-btn" class="addon-control purchase"
+ command="cmd_purchaseItem"/>
+ <button id="detail-install-btn" class="addon-control install"
+ label="&cmd.installAddon.label;"
+ accesskey="&cmd.installAddon.accesskey;"
+ command="cmd_installItem"/>
+ <menulist id="detail-state-menulist"
+ crop="none" sizetopopup="always"
+ tooltiptext="&cmd.stateMenu.tooltip;">
+ <menupopup>
+ <menuitem id="detail-ask-to-activate-menuitem"
+ class="addon-control"
+ label="&cmd.askToActivate.label;"
+ tooltiptext="&cmd.askToActivate.tooltip;"
+ command="cmd_askToActivateItem"/>
+ <menuitem id="detail-always-activate-menuitem"
+ class="addon-control"
+ label="&cmd.alwaysActivate.label;"
+ tooltiptext="&cmd.alwaysActivate.tooltip;"
+ command="cmd_alwaysActivateItem"/>
+ <menuitem id="detail-never-activate-menuitem"
+ class="addon-control"
+ label="&cmd.neverActivate.label;"
+ tooltiptext="&cmd.neverActivate.tooltip;"
+ command="cmd_neverActivateItem"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </vbox>
+ </hbox>
+ </vbox>
+ <spacer flex="1"/>
+ </hbox>
+ </scrollbox>
+
+ </deck>
+
+ </box>
+ </hbox>
+
+</page>
diff --git a/components/extensions/content/gmpPrefs.xul b/components/extensions/content/gmpPrefs.xul
new file mode 100644
index 000000000..ea7ee92fa
--- /dev/null
+++ b/components/extensions/content/gmpPrefs.xul
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+
+<!-- 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 is intentionally empty and a dummy to let the GMPProvider
+ have a preferences button in the list view. -->
diff --git a/components/extensions/content/list.js b/components/extensions/content/list.js
new file mode 100644
index 000000000..a31922703
--- /dev/null
+++ b/components/extensions/content/list.js
@@ -0,0 +1,165 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 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 kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const kDialog = "dialog";
+
+/**
+ * This dialog can be initialized from parameters supplied via window.arguments
+ * or it can be used to display blocklist notification and blocklist blocked
+ * installs via nsIDialogParamBlock as is done by nsIExtensionManager.
+ *
+ * When using this dialog with window.arguments it must be opened modally, the
+ * caller can inspect the user action after the dialog closes by inspecting the
+ * value of the |result| parameter on this object which is set to the dlgtype
+ * of the button used to close the dialog.
+ *
+ * window.arguments[0] is an array of strings to display in the tree. If the
+ * array is empty the tree will not be displayed.
+ * window.arguments[1] a JS Object with the following properties:
+ *
+ * title: A title string, to be displayed in the title bar of the dialog.
+ * message1: A message string, displayed above the addon list
+ * message2: A message string, displayed below the addon list
+ * message3: A bolded message string, displayed below the addon list
+ * moreInfoURL: An url for displaying more information
+ * iconClass : Can be one of the following values (default is alert-icon)
+ * alert-icon, error-icon, or question-icon
+ *
+ * If no value is supplied for message1, message2, message3, or moreInfoURL,
+ * the element is not displayed.
+ *
+ * buttons: {
+ * accept: { label: "A Label for the Accept button",
+ * focused: true },
+ * cancel: { label: "A Label for the Cancel button" },
+ * ...
+ * },
+ *
+ * result: The dlgtype of button that was used to dismiss the dialog.
+ */
+
+"use strict";
+
+var gButtons = { };
+
+function init() {
+ var de = document.documentElement;
+ var items = [];
+ if (window.arguments[0] instanceof Components.interfaces.nsIDialogParamBlock) {
+ // This is a warning about a blocklisted item the user is trying to install
+ var args = window.arguments[0];
+ var softblocked = args.GetInt(0) == 1 ? true : false;
+
+ var extensionsBundle = document.getElementById("extensionsBundle");
+ try {
+ var formatter = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+ .getService(Components.interfaces.nsIURLFormatter);
+ var url = formatter.formatURLPref("extensions.blocklist.detailsURL");
+ }
+ catch (e) { }
+
+ var params = {
+ moreInfoURL: url,
+ };
+
+ if (softblocked) {
+ params.title = extensionsBundle.getString("softBlockedInstallTitle");
+ params.message1 = extensionsBundle.getFormattedString("softBlockedInstallMsg",
+ [args.GetString(0)]);
+ var accept = de.getButton("accept");
+ accept.label = extensionsBundle.getString("softBlockedInstallAcceptLabel");
+ accept.accessKey = extensionsBundle.getString("softBlockedInstallAcceptKey");
+ de.getButton("cancel").focus();
+ document.addEventListener("dialogaccept", allowInstall, false);
+ }
+ else {
+ params.title = extensionsBundle.getString("blocklistedInstallTitle2");
+ params.message1 = extensionsBundle.getFormattedString("blocklistedInstallMsg2",
+ [args.GetString(0)]);
+ de.buttons = "accept";
+ de.getButton("accept").focus();
+ }
+ }
+ else {
+ items = window.arguments[0];
+ params = window.arguments[1];
+ }
+
+ var addons = document.getElementById("addonsChildren");
+ if (items.length > 0)
+ document.getElementById("addonsTree").hidden = false;
+
+ // Fill the addons list
+ for (var item of items) {
+ var treeitem = document.createElementNS(kXULNS, "treeitem");
+ var treerow = document.createElementNS(kXULNS, "treerow");
+ var treecell = document.createElementNS(kXULNS, "treecell");
+ treecell.setAttribute("label", item);
+ treerow.appendChild(treecell);
+ treeitem.appendChild(treerow);
+ addons.appendChild(treeitem);
+ }
+
+ // Set the messages
+ var messages = ["message1", "message2", "message3"];
+ for (let messageEntry of messages) {
+ if (messageEntry in params) {
+ var message = document.getElementById(messageEntry);
+ message.hidden = false;
+ message.appendChild(document.createTextNode(params[messageEntry]));
+ }
+ }
+
+ document.getElementById("infoIcon").className =
+ params["iconClass"] ? "spaced " + params["iconClass"] : "spaced alert-icon";
+
+ if ("moreInfoURL" in params && params["moreInfoURL"]) {
+ message = document.getElementById("moreInfo");
+ message.value = extensionsBundle.getString("moreInfoText");
+ message.setAttribute("href", params["moreInfoURL"]);
+ document.getElementById("moreInfoBox").hidden = false;
+ }
+
+ // Set the window title
+ if ("title" in params)
+ document.title = params.title;
+
+ // Set up the buttons
+ if ("buttons" in params) {
+ gButtons = params.buttons;
+ var buttonString = "";
+ for (var buttonType in gButtons)
+ buttonString += "," + buttonType;
+ de.buttons = buttonString.substr(1);
+ for (buttonType in gButtons) {
+ var button = de.getButton(buttonType);
+ button.label = gButtons[buttonType].label;
+ if (gButtons[buttonType].focused)
+ button.focus();
+ document.addEventListener(kDialog + buttonType, handleButtonCommand, true);
+ }
+ }
+}
+
+function shutdown() {
+ for (var buttonType in gButtons)
+ document.removeEventListener(kDialog + buttonType, handleButtonCommand, true);
+}
+
+function allowInstall() {
+ var args = window.arguments[0];
+ args.SetInt(1, 1);
+}
+
+/**
+ * Watch for the user hitting one of the buttons to dismiss the dialog
+ * and report the result back to the caller through the |result| property on
+ * the arguments object.
+ */
+function handleButtonCommand(event) {
+ window.arguments[1].result = event.type.substr(kDialog.length);
+}
diff --git a/components/extensions/content/list.xul b/components/extensions/content/list.xul
new file mode 100644
index 000000000..65efeb6a2
--- /dev/null
+++ b/components/extensions/content/list.xul
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/"?>
+
+<dialog id="addonList" windowtype="Addons:List"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onunload="shutdown();"
+ buttons="accept,cancel" onload="init();">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://mozapps/content/extensions/list.js"/>
+
+ <stringbundle id="extensionsBundle"
+ src="chrome://mozapps/locale/extensions/extensions.properties"/>
+ <stringbundle id="brandBundle"
+ src="chrome://branding/locale/brand.properties"/>
+
+ <hbox align="start">
+ <vbox>
+ <image id="infoIcon"/>
+ </vbox>
+ <vbox class="spaced" style="min-width: 20em; max-width: 40em">
+ <label id="message1" class="spaced" hidden="true"/>
+ <separator class="thin"/>
+ <tree id="addonsTree" rows="6" hidecolumnpicker="true" hidden="true" class="spaced">
+ <treecols style="max-width: 25em;">
+ <treecol flex="1" id="nameColumn" hideheader="true"/>
+ </treecols>
+ <treechildren id="addonsChildren"/>
+ </tree>
+ <label id="message2" class="spaced" hidden="true"/>
+ <label class="bold spaced" id="message3" hidden="true"/>
+ <hbox id="moreInfoBox" hidden="true">
+ <label id="moreInfo" class="text-link spaced"/>
+ <spacer flex="1"/>
+ </hbox>
+ </vbox>
+ </hbox>
+</dialog>
diff --git a/components/extensions/content/newaddon.js b/components/extensions/content/newaddon.js
new file mode 100644
index 000000000..aab556a62
--- /dev/null
+++ b/components/extensions/content/newaddon.js
@@ -0,0 +1,129 @@
+/* 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 Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+var gAddon = null;
+
+// If the user enables the add-on through some other UI close this window
+var EnableListener = {
+ onEnabling: function(aAddon) {
+ if (aAddon.id == gAddon.id)
+ window.close();
+ }
+}
+AddonManager.addAddonListener(EnableListener);
+
+function initialize() {
+ // About URIs don't implement nsIURL so we have to find the query string
+ // manually
+ let spec = document.location.href;
+ let pos = spec.indexOf("?");
+ let query = "";
+ if (pos >= 0)
+ query = spec.substring(pos + 1);
+
+ // Just assume the query is "id=<id>"
+ let id = query.substring(3);
+ if (!id) {
+ window.location = "about:blank";
+ return;
+ }
+
+ let bundle = Services.strings.createBundle("chrome://mozapps/locale/extensions/newaddon.properties");
+
+ AddonManager.getAddonByID(id, function(aAddon) {
+ // If the add-on doesn't exist or it is already enabled or it has already
+ // been seen or it cannot be enabled then this UI is useless, just close it.
+ // This shouldn't normally happen unless session restore restores the tab.
+ if (!aAddon || !aAddon.userDisabled ||
+ !(aAddon.permissions & AddonManager.PERM_CAN_ENABLE)) {
+ window.close();
+ return;
+ }
+
+ gAddon = aAddon;
+
+ document.getElementById("addon-info").setAttribute("type", aAddon.type);
+
+ let icon = document.getElementById("icon");
+ if (aAddon.icon64URL)
+ icon.src = aAddon.icon64URL;
+ else if (aAddon.iconURL)
+ icon.src = aAddon.iconURL;
+
+ let name = bundle.formatStringFromName("name", [aAddon.name, aAddon.version],
+ 2);
+ document.getElementById("name").value = name;
+
+ if (aAddon.creator) {
+ let creator = bundle.formatStringFromName("author", [aAddon.creator], 1);
+ document.getElementById("author").value = creator;
+ } else {
+ document.getElementById("author").hidden = true;
+ }
+
+ let uri = "getResourceURI" in aAddon ? aAddon.getResourceURI() : null;
+ let locationLabel = document.getElementById("location");
+ if (uri instanceof Ci.nsIFileURL) {
+ let location = bundle.formatStringFromName("location", [uri.file.path], 1);
+ locationLabel.value = location;
+ locationLabel.setAttribute("tooltiptext", location);
+ } else {
+ document.getElementById("location").hidden = true;
+ }
+
+ var event = document.createEvent("Events");
+ event.initEvent("AddonDisplayed", true, true);
+ document.dispatchEvent(event);
+ });
+}
+
+function unload() {
+ AddonManager.removeAddonListener(EnableListener);
+}
+
+function continueClicked() {
+ AddonManager.removeAddonListener(EnableListener);
+
+ if (document.getElementById("allow").checked) {
+ gAddon.userDisabled = false;
+
+ if (gAddon.pendingOperations & AddonManager.PENDING_ENABLE) {
+ document.getElementById("allow").disabled = true;
+ document.getElementById("buttonDeck").selectedPanel = document.getElementById("restartPanel");
+ return;
+ }
+ }
+
+ window.close();
+}
+
+function restartClicked() {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested",
+ "restart");
+ if (cancelQuit.data)
+ return; // somebody canceled our quit request
+
+ window.close();
+
+ let appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"].
+ getService(Components.interfaces.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+}
+
+function cancelClicked() {
+ gAddon.userDisabled = true;
+ AddonManager.addAddonListener(EnableListener);
+
+ document.getElementById("allow").disabled = false;
+ document.getElementById("buttonDeck").selectedPanel = document.getElementById("continuePanel");
+}
diff --git a/components/extensions/content/newaddon.xul b/components/extensions/content/newaddon.xul
new file mode 100644
index 000000000..0806f2799
--- /dev/null
+++ b/components/extensions/content/newaddon.xul
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/extensions/newaddon.css"?>
+
+<!DOCTYPE page [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % newaddonDTD SYSTEM "chrome://mozapps/locale/extensions/newaddon.dtd">
+%newaddonDTD;
+]>
+
+<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml" title="&title;"
+ disablefastfind="true" id="addon-page" onload="initialize()"
+ onunload="unload()" role="application" align="stretch" pack="stretch">
+
+ <xhtml:link rel="shortcut icon" style="display: none"
+ href="chrome://mozapps/skin/extensions/extensionGeneric-16.png"/>
+
+ <script type="application/javascript"
+ src="chrome://mozapps/content/extensions/newaddon.js"/>
+
+ <scrollbox id="addon-scrollbox" align="center">
+ <spacer id="spacer-start"/>
+
+ <vbox id="addon-container" class="main-content">
+ <description>&intro;</description>
+
+ <hbox id="addon-info">
+ <image id="icon"/>
+ <vbox flex="1">
+ <label id="name"/>
+ <label id="author"/>
+ <label id="location" crop="end"/>
+ </vbox>
+ </hbox>
+
+ <hbox id="warning">
+ <image id="warning-icon"/>
+ <description flex="1">&warning;</description>
+ </hbox>
+
+ <checkbox id="allow" label="&allow;"/>
+ <description id="later">&later;</description>
+
+ <deck id="buttonDeck">
+ <hbox id="continuePanel">
+ <button id="continue-button" label="&continue;"
+ oncommand="continueClicked()"/>
+ </hbox>
+ <hbox id="restartPanel">
+ <spacer id="restartSpacer"/>
+ <description id="restartMessage" flex="1">&restartMessage;</description>
+ <button id="restart-button" label="&restartButton;" oncommand="restartClicked()"/>
+ <button id="cancel-button" label="&cancelButton;" oncommand="cancelClicked()"/>
+ </hbox>
+ </deck>
+ </vbox>
+
+ <spacer id="spacer-end"/>
+ </scrollbox>
+</page>
diff --git a/components/extensions/content/pluginPrefs.xul b/components/extensions/content/pluginPrefs.xul
new file mode 100644
index 000000000..c3fdbfa5b
--- /dev/null
+++ b/components/extensions/content/pluginPrefs.xul
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE window SYSTEM "chrome://pluginproblem/locale/pluginproblem.dtd">
+
+<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <setting type="control" title="&plugin.file;">
+ <label class="text-list" id="pluginLibraries"/>
+ </setting>
+ <setting type="control" title="&plugin.mimeTypes;">
+ <label class="text-list" id="pluginMimeTypes"/>
+ </setting>
+ <setting type="bool" pref="dom.ipc.plugins.flash.disable-protected-mode"
+ inverted="true" title="&plugin.flashProtectedMode.label;"
+ id="pluginEnableProtectedMode"
+ learnmore="https://support.mozilla.org/kb/flash-protected-mode-settings" />
+</vbox>
diff --git a/components/extensions/content/selectAddons.css b/components/extensions/content/selectAddons.css
new file mode 100644
index 000000000..636757721
--- /dev/null
+++ b/components/extensions/content/selectAddons.css
@@ -0,0 +1,22 @@
+/* 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/. */
+
+#select .addon {
+ -moz-binding: url("chrome://mozapps/content/extensions/selectAddons.xml#addon-select");
+}
+
+#confirm .addon {
+ -moz-binding: url("chrome://mozapps/content/extensions/selectAddons.xml#addon-confirm");
+}
+
+#select-scrollbox,
+#confirm-scrollbox {
+ overflow-y: auto;
+ -moz-box-orient: vertical;
+}
+
+.addon:not([optionalupdate]) .addon-action-update,
+.addon[optionalupdate] .addon-action-message {
+ display: none;
+}
diff --git a/components/extensions/content/selectAddons.js b/components/extensions/content/selectAddons.js
new file mode 100644
index 000000000..01b9030ee
--- /dev/null
+++ b/components/extensions/content/selectAddons.js
@@ -0,0 +1,347 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+var gView = null;
+
+function showView(aView) {
+ gView = aView;
+
+ gView.show();
+
+ // If the view's show method immediately showed a different view then don't
+ // do anything else
+ if (gView != aView)
+ return;
+
+ let viewNode = document.getElementById(gView.nodeID);
+ viewNode.parentNode.selectedPanel = viewNode;
+
+ // For testing dispatch an event when the view changes
+ var event = document.createEvent("Events");
+ event.initEvent("ViewChanged", true, true);
+ viewNode.dispatchEvent(event);
+}
+
+function showButtons(aCancel, aBack, aNext, aDone) {
+ document.getElementById("cancel").hidden = !aCancel;
+ document.getElementById("back").hidden = !aBack;
+ document.getElementById("next").hidden = !aNext;
+ document.getElementById("done").hidden = !aDone;
+}
+
+function isAddonDistroInstalled(aID) {
+ let branch = Services.prefs.getBranch("extensions.installedDistroAddon.");
+ if (!branch.prefHasUserValue(aID))
+ return false;
+
+ return branch.getBoolPref(aID);
+}
+
+function orderForScope(aScope) {
+ return aScope == AddonManager.SCOPE_PROFILE ? 1 : 0;
+}
+
+var gAddons = {};
+
+var gChecking = {
+ nodeID: "checking",
+
+ _progress: null,
+ _addonCount: 0,
+ _completeCount: 0,
+
+ show: function gChecking_show() {
+ showButtons(true, false, false, false);
+ this._progress = document.getElementById("checking-progress");
+
+ AddonManager.getAllAddons(aAddons => {
+ if (aAddons.length == 0) {
+ window.close();
+ return;
+ }
+
+ aAddons = aAddons.filter(function gChecking_filterAddons(aAddon) {
+ if (aAddon.type == "plugin" || aAddon.type == "service")
+ return false;
+
+ if (aAddon.type == "theme") {
+ // Don't show application shipped themes
+ if (aAddon.scope == AddonManager.SCOPE_APPLICATION)
+ return false;
+ // Don't show already disabled themes
+ if (aAddon.userDisabled)
+ return false;
+ }
+
+ return true;
+ });
+
+ this._addonCount = aAddons.length;
+ this._progress.value = 0;
+ this._progress.max = aAddons.length;
+ this._progress.mode = "determined";
+
+ AddonRepository.repopulateCache().then(() => {
+ for (let addonItem of aAddons) {
+ // Ignore disabled themes
+ if (addonItem.type != "theme" || !addonItem.userDisabled) {
+ gAddons[addonItem.id] = {
+ addon: addonItem,
+ install: null,
+ wasActive: addonItem.isActive
+ }
+ }
+
+ addonItem.findUpdates(this, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ }
+ });
+ });
+ },
+
+ onUpdateAvailable: function gChecking_onUpdateAvailable(aAddon, aInstall) {
+ // If the add-on can be upgraded then remember the new version
+ if (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE)
+ gAddons[aAddon.id].install = aInstall;
+ },
+
+ onUpdateFinished: function gChecking_onUpdateFinished(aAddon, aError) {
+ this._completeCount++;
+ this._progress.value = this._completeCount;
+
+ if (this._completeCount < this._addonCount)
+ return;
+
+ // Tycho: var addons = [gAddons[id] for (id in gAddons)];
+ var addons = [];
+ for (let id in gAddons) {
+ addons.push(gAddons[id])
+ }
+
+ addons.sort(function sortAddons(a, b) {
+ let orderA = orderForScope(a.addon.scope);
+ let orderB = orderForScope(b.addon.scope);
+
+ if (orderA != orderB)
+ return orderA - orderB;
+
+ return String.localeCompare(a.addon.name, b.addon.name);
+ });
+
+ let rows = document.getElementById("select-rows");
+ let lastAddon = null;
+ for (let entry of addons) {
+ if (lastAddon &&
+ orderForScope(entry.addon.scope) != orderForScope(lastAddon.scope)) {
+ let separator = document.createElement("separator");
+ rows.appendChild(separator);
+ }
+
+ let row = document.createElement("row");
+ row.setAttribute("id", entry.addon.id);
+ row.setAttribute("class", "addon");
+ rows.appendChild(row);
+ row.setAddon(entry.addon, entry.install, entry.wasActive,
+ isAddonDistroInstalled(entry.addon.id));
+
+ if (entry.install)
+ entry.install.addListener(gUpdate);
+
+ lastAddon = entry.addon;
+ }
+
+ showView(gSelect);
+ }
+};
+
+var gSelect = {
+ nodeID: "select",
+
+ show: function gSelect_show() {
+ this.updateButtons();
+ },
+
+ updateButtons: function gSelect_updateButtons() {
+ for (let row = document.getElementById("select-rows").firstChild;
+ row; row = row.nextSibling) {
+ if (row.localName == "separator")
+ continue;
+
+ if (row.action) {
+ showButtons(false, false, true, false);
+ return;
+ }
+ }
+
+ showButtons(false, false, false, true);
+ },
+
+ next: function gSelect_next() {
+ showView(gConfirm);
+ },
+
+ done: function gSelect_done() {
+ window.close();
+ }
+};
+
+var gConfirm = {
+ nodeID: "confirm",
+
+ show: function gConfirm_show() {
+ showButtons(false, true, false, true);
+
+ let box = document.getElementById("confirm-scrollbox").firstChild;
+ while (box) {
+ box.hidden = true;
+ while (box.lastChild != box.firstChild)
+ box.removeChild(box.lastChild);
+ box = box.nextSibling;
+ }
+
+ for (let row = document.getElementById("select-rows").firstChild;
+ row; row = row.nextSibling) {
+ if (row.localName == "separator")
+ continue;
+
+ let action = row.action;
+ if (!action)
+ continue;
+
+ let list = document.getElementById(action + "-list");
+
+ list.hidden = false;
+ let item = document.createElement("hbox");
+ item.setAttribute("id", row._addon.id);
+ item.setAttribute("class", "addon");
+ item.setAttribute("type", row._addon.type);
+ item.setAttribute("name", row._addon.name);
+ if (action == "update" || action == "enable")
+ item.setAttribute("active", "true");
+ list.appendChild(item);
+
+ if (action == "update")
+ showButtons(false, true, true, false);
+ }
+ },
+
+ back: function gConfirm_back() {
+ showView(gSelect);
+ },
+
+ next: function gConfirm_next() {
+ showView(gUpdate);
+ },
+
+ done: function gConfirm_done() {
+ for (let row = document.getElementById("select-rows").firstChild;
+ row; row = row.nextSibling) {
+ if (row.localName != "separator")
+ row.apply();
+ }
+
+ window.close();
+ }
+}
+
+var gUpdate = {
+ nodeID: "update",
+
+ _progress: null,
+ _waitingCount: 0,
+ _completeCount: 0,
+ _errorCount: 0,
+
+ show: function gUpdate_show() {
+ showButtons(true, false, false, false);
+
+ this._progress = document.getElementById("update-progress");
+
+ for (let row = document.getElementById("select-rows").firstChild;
+ row; row = row.nextSibling) {
+ if (row.localName != "separator")
+ row.apply();
+ }
+
+ this._progress.mode = "determined";
+ this._progress.max = this._waitingCount;
+ this._progress.value = this._completeCount;
+ },
+
+ checkComplete: function gUpdate_checkComplete() {
+ this._progress.value = this._completeCount;
+ if (this._completeCount < this._waitingCount)
+ return;
+
+ if (this._errorCount > 0) {
+ showView(gErrors);
+ return;
+ }
+
+ window.close();
+ },
+
+ onDownloadStarted: function gUpdate_onDownloadStarted(aInstall) {
+ this._waitingCount++;
+ },
+
+ onDownloadFailed: function gUpdate_onDownloadFailed(aInstall) {
+ this._errorCount++;
+ this._completeCount++;
+ this.checkComplete();
+ },
+
+ onInstallFailed: function gUpdate_onInstallFailed(aInstall) {
+ this._errorCount++;
+ this._completeCount++;
+ this.checkComplete();
+ },
+
+ onInstallEnded: function gUpdate_onInstallEnded(aInstall) {
+ this._completeCount++;
+ this.checkComplete();
+ }
+};
+
+var gErrors = {
+ nodeID: "errors",
+
+ show: function gErrors_show() {
+ showButtons(false, false, false, true);
+ },
+
+ done: function gErrors_done() {
+ window.close();
+ }
+};
+
+window.addEventListener("load", function loadEventListener() {
+ showView(gChecking); }, false);
+
+// When closing the window cancel any pending or in-progress installs
+window.addEventListener("unload", function unloadEventListener() {
+ for (let id in gAddons) {
+ let entry = gAddons[id];
+ if (!entry.install)
+ return;
+
+ aEntry.install.removeListener(gUpdate);
+
+ if (entry.install.state != AddonManager.STATE_INSTALLED &&
+ entry.install.state != AddonManager.STATE_DOWNLOAD_FAILED &&
+ entry.install.state != AddonManager.STATE_INSTALL_FAILED) {
+ entry.install.cancel();
+ }
+ }
+}, false);
diff --git a/components/extensions/content/selectAddons.xml b/components/extensions/content/selectAddons.xml
new file mode 100644
index 000000000..dbfc0d400
--- /dev/null
+++ b/components/extensions/content/selectAddons.xml
@@ -0,0 +1,235 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE window [
+<!ENTITY % updateDTD SYSTEM "chrome://mozapps/locale/extensions/selectAddons.dtd">
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%updateDTD;
+%brandDTD;
+]>
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="addon-select">
+ <content>
+ <xul:hbox class="select-keep select-cell">
+ <xul:checkbox class="addon-keep-checkbox" anonid="keep"
+ xbl:inherits="tooltiptext=name"
+ oncommand="document.getBindingParent(this).keepChanged();"/>
+ </xul:hbox>
+ <xul:hbox class="select-icon select-cell">
+ <xul:image class="addon-icon" xbl:inherits="type"/>
+ </xul:hbox>
+ <xul:hbox class="select-name select-cell">
+ <xul:label class="addon-name" crop="end" style="&select.name.style;"
+ xbl:inherits="xbl:text=name"/>
+ </xul:hbox>
+ <xul:hbox class="select-action select-cell">
+ <xul:label class="addon-action-message" style="&select.action.style;"
+ xbl:inherits="xbl:text=action"/>
+ <xul:checkbox anonid="update" checked="true" class="addon-action-update"
+ oncommand="document.getBindingParent(this).updateChanged();"
+ style="&select.action.style;" xbl:inherits="label=action"/>
+ </xul:hbox>
+ <xul:hbox class="select-source select-cell">
+ <xul:label class="addon-source" xbl:inherits="xbl:text=source"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <field name="_addon"/>
+ <field name="_disabled"/>
+ <field name="_install"/>
+ <field name="_wasActive"/>
+ <field name="_keep">document.getAnonymousElementByAttribute(this, "anonid", "keep");</field>
+ <field name="_update">document.getAnonymousElementByAttribute(this, "anonid", "update");</field>
+ <field name="_strings">document.getElementById("strings");</field>
+
+ <property name="action" readonly="true">
+ <getter><![CDATA[
+ if (!this._keep.checked) {
+ if (this._wasActive)
+ return "disable";
+ else
+ return null;
+ }
+
+ if (this._addon.appDisabled && !this._install)
+ return "incompatible";
+
+ if (this._install && (AddonManager.shouldAutoUpdate(this._addon) || this._update.checked))
+ return "update";
+
+ if (this._addon.permissions & AddonManager.PERM_CAN_ENABLE)
+ return "enable";
+
+ return null;
+ ]]></getter>
+ </property>
+
+ <method name="setAddon">
+ <parameter name="aAddon"/>
+ <parameter name="aInstall"/>
+ <parameter name="aWasActive"/>
+ <parameter name="aDistroInstalled"/>
+ <body><![CDATA[
+ this._addon = aAddon;
+ this._install = aInstall;
+ this._wasActive = aWasActive;
+
+ this.setAttribute("name", aAddon.name);
+ this.setAttribute("type", aAddon.type);
+
+ // User and bundled add-ons default to staying enabled,
+ // others default to disabled.
+ switch (aAddon.scope) {
+ case AddonManager.SCOPE_PROFILE:
+ this._keep.checked = !aAddon.userDisabled;
+ if (aDistroInstalled)
+ this.setAttribute("source", this._strings.getString("source.bundled"));
+ else
+ this.setAttribute("source", this._strings.getString("source.profile"));
+ break;
+ default:
+ this._keep.checked = false;
+ this.setAttribute("source", this._strings.getString("source.other"));
+ }
+
+ this.updateAction();
+ ]]></body>
+ </method>
+
+ <method name="setActionMessage">
+ <body><![CDATA[
+ if (!this._keep.checked) {
+ // If the user no longer wants this add-on then it is either being
+ // disabled or nothing is changing. Don't complicate matters by
+ // talking about updates for this case
+
+ if (this._wasActive)
+ this.setAttribute("action", this._strings.getString("action.disabled"));
+ else
+ this.setAttribute("action", "");
+
+ this.removeAttribute("optionalupdate");
+ return;
+ }
+
+ if (this._addon.appDisabled && !this._install) {
+ // If the add-on is incompatible and there is no update available
+ // then it will remain disabled
+
+ this.setAttribute("action", this._strings.getString("action.incompatible"));
+
+ this.removeAttribute("optionalupdate");
+ return;
+ }
+
+ if (this._install) {
+ if (!AddonManager.shouldAutoUpdate(this._addon)) {
+ this.setAttribute("optionalupdate", "true");
+
+ // If manual updating for the add-on then display the right
+ // text depending on whether the update is required to make
+ // the add-on work or not
+ if (this._addon.appDisabled)
+ this.setAttribute("action", this._strings.getString("action.neededupdate"));
+ else
+ this.setAttribute("action", this._strings.getString("action.unneededupdate"));
+ return;
+ }
+
+ this.removeAttribute("optionalupdate");
+
+ // If the update is needed to make the add-on compatible then
+ // say so otherwise just say nothing about it
+ if (this._addon.appDisabled) {
+ this.setAttribute("action", this._strings.getString("action.autoupdate"));
+ return;
+ }
+ }
+ else {
+ this.removeAttribute("optionalupdate");
+ }
+
+ // If the add-on didn't used to be active and it now is (via a
+ // compatibility update) or it can be enabled then the action is to
+ // enable the add-on
+ if (!this._wasActive && (this._addon.isActive || this._addon.permissions & AddonManager.PERM_CAN_ENABLE)) {
+ this.setAttribute("action", this._strings.getString("action.enabled"));
+ return;
+ }
+
+ // In all other cases the add-on is simply remaining enabled
+ this.setAttribute("action", "");
+ ]]></body>
+ </method>
+
+ <method name="updateAction">
+ <body><![CDATA[
+ this.setActionMessage();
+ let installingUpdate = this._install &&
+ (AddonManager.shouldAutoUpdate(this._addon) ||
+ this._update.checked);
+
+ if (this._keep.checked && (!this._addon.appDisabled || installingUpdate))
+ this.setAttribute("active", "true");
+ else
+ this.removeAttribute("active");
+
+ gSelect.updateButtons();
+ ]]></body>
+ </method>
+
+ <method name="updateChanged">
+ <body><![CDATA[
+ this.updateAction();
+ ]]></body>
+ </method>
+
+ <method name="keepChanged">
+ <body><![CDATA[
+ this.updateAction();
+ ]]></body>
+ </method>
+
+ <method name="keep">
+ <body><![CDATA[
+ this._keep.checked = true;
+ this.keepChanged();
+ ]]></body>
+ </method>
+
+ <method name="disable">
+ <body><![CDATA[
+ this._keep.checked = false;
+ this.keepChanged();
+ ]]></body>
+ </method>
+
+ <method name="apply">
+ <body><![CDATA[
+ this._addon.userDisabled = !this._keep.checked;
+
+ if (!this._install || !this._keep.checked)
+ return;
+
+ if (AddonManager.shouldAutoUpdate(this._addon) || this._update.checked)
+ this._install.install();
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="addon-confirm">
+ <content>
+ <xul:image class="addon-icon" xbl:inherits="type"/>
+ <xul:label class="addon-name" xbl:inherits="xbl:text=name"/>
+ </content>
+ </binding>
+</bindings>
diff --git a/components/extensions/content/selectAddons.xul b/components/extensions/content/selectAddons.xul
new file mode 100644
index 000000000..0fa292ecf
--- /dev/null
+++ b/components/extensions/content/selectAddons.xul
@@ -0,0 +1,124 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/content/extensions/selectAddons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/extensions/selectAddons.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % updateDTD SYSTEM "chrome://mozapps/locale/extensions/selectAddons.dtd">
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%updateDTD;
+%brandDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ style="&upgrade.style;" id="select-window">
+
+ <script type="application/javascript" src="chrome://mozapps/content/extensions/selectAddons.js"/>
+ <stringbundle id="strings" src="chrome://mozapps/locale/extensions/selectAddons.properties"/>
+
+ <deck id="view-deck" flex="1" align="stretch" pack="stretch">
+ <vbox id="checking" align="stretch">
+ <vbox flex="1">
+ <label id="checking-heading" class="heading">&checking.heading;</label>
+ </vbox>
+ <progressmeter id="checking-progress" class="progress" mode="undetermined"/>
+ <vbox flex="1">
+ <label id="checking-progress-label" class="progress-label">&checking.progress.label;</label>
+ </vbox>
+ </vbox>
+
+ <vbox id="select" align="stretch">
+ <label id="select-heading" class="heading">&select.heading;</label>
+
+ <description id="select-description">&select.description;</description>
+
+ <vbox id="select-list" align="stretch" flex="1">
+ <hbox id="select-header">
+ <hbox class="select-keep select-cell" style="&select.keep.style;">
+ <label value="&select.keep;"/>
+ </hbox>
+ <hbox class="select-icon select-cell"/>
+ <hbox id="heading-name" class="select-name select-cell" style="&select.name.style;">
+ <label value="&select.name;"/>
+ </hbox>
+ <hbox id="heading-action" class="select-action select-cell" style="&select.action.style;">
+ <label value="&select.action;"/>
+ </hbox>
+ <hbox class="select-source select-cell" flex="1">
+ <label value="&select.source;"/>
+ </hbox>
+ </hbox>
+
+ <scrollbox id="select-scrollbox" flex="1">
+ <grid id="select-grid" flex="1">
+ <columns>
+ <column style="&select.keep.style;"/>
+ <column/>
+ <column id="column-name"/>
+ <column id="column-action" class="select-action"/>
+ <column class="select-source" flex="1"/>
+ </columns>
+
+ <rows id="select-rows"/>
+ </grid>
+ </scrollbox>
+ </vbox>
+ </vbox>
+
+ <vbox id="confirm" align="stretch">
+ <label id="confirm-heading" class="heading">&confirm.heading;</label>
+
+ <description id="confirm-description">&confirm.description;</description>
+
+ <scrollbox id="confirm-scrollbox" flex="1">
+ <vbox id="disable-list" class="action-list" hidden="true">
+ <label class="action-header">&action.disable.heading;</label>
+ </vbox>
+
+ <vbox id="incompatible-list" class="action-list" hidden="true">
+ <label class="action-header">&action.incompatible.heading;</label>
+ </vbox>
+
+ <vbox id="update-list" class="action-list" hidden="true">
+ <label class="action-header">&action.update.heading;</label>
+ </vbox>
+
+ <vbox id="enable-list" class="action-list" hidden="true">
+ <label class="action-header">&action.enable.heading;</label>
+ </vbox>
+ </scrollbox>
+ </vbox>
+
+ <vbox id="update" align="stretch">
+ <vbox flex="1">
+ <label id="update-heading" class="heading">&update.heading;</label>
+ </vbox>
+ <progressmeter id="update-progress" class="progress" mode="undetermined"/>
+ <vbox flex="1">
+ <label id="update-progress-label" class="progress-label">&update.progress.label;</label>
+ </vbox>
+ </vbox>
+
+ <vbox id="errors">
+ <vbox flex="1">
+ <label id="errors-heading" class="heading">&errors.heading;</label>
+ </vbox>
+ <description id="errors-description" value="&errors.description;"/>
+ <spacer flex="1"/>
+ </vbox>
+ </deck>
+
+ <hbox id="footer" align="center">
+ <label id="footer-label" flex="1">&footer.label;</label>
+ <button id="cancel" label="&cancel.label;" oncommand="window.close()"/>
+ <button id="back" label="&back.label;" oncommand="gView.back()" hidden="true"/>
+ <button id="next" label="&next.label;" oncommand="gView.next()" hidden="true"/>
+ <button id="done" label="&done.label;" oncommand="gView.done()" hidden="true"/>
+ </hbox>
+
+</window>
diff --git a/components/extensions/content/setting.xml b/components/extensions/content/setting.xml
new file mode 100644
index 000000000..c4eae1fd3
--- /dev/null
+++ b/components/extensions/content/setting.xml
@@ -0,0 +1,508 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE page [
+<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/extensions.dtd">
+%extensionsDTD;
+]>
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="settings">
+ <content orient="vertical">
+ <xul:label class="settings-title" xbl:inherits="xbl:text=label" flex="1"/>
+ <children/>
+ </content>
+ </binding>
+
+ <binding id="setting-base">
+ <implementation>
+ <constructor><![CDATA[
+ this.preferenceChanged();
+
+ this.addEventListener("keypress", function(event) {
+ event.stopPropagation();
+ }, false);
+
+ if (this.usePref)
+ Services.prefs.addObserver(this.pref, this._observer, true);
+ ]]></constructor>
+
+ <field name="_observer"><![CDATA[({
+ _self: this,
+
+ QueryInterface: function(aIID) {
+ const Ci = Components.interfaces;
+ if (aIID.equals(Ci.nsIObserver) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.Exception("No interface", Components.results.NS_ERROR_NO_INTERFACE);
+ },
+
+ observe: function(aSubject, aTopic, aPrefName) {
+ if (aTopic != "nsPref:changed")
+ return;
+
+ if (this._self.pref == aPrefName)
+ this._self.preferenceChanged();
+ }
+ })]]>
+ </field>
+
+ <method name="fireEvent">
+ <parameter name="eventName"/>
+ <parameter name="funcStr"/>
+ <body>
+ <![CDATA[
+ let body = funcStr || this.getAttribute(eventName);
+ if (!body)
+ return;
+
+ try {
+ let event = document.createEvent("Events");
+ event.initEvent(eventName, true, true);
+ let f = new Function("event", body);
+ f.call(this, event);
+ }
+ catch (e) {
+ Cu.reportError(e);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueFromPreference">
+ <body>
+ <![CDATA[
+ // Should be code to set the from the preference input.value
+ throw Components.Exception("No valueFromPreference implementation",
+ Components.results.NS_ERROR_NOT_IMPLEMENTED);
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueToPreference">
+ <body>
+ <![CDATA[
+ // Should be code to set the input.value from the preference
+ throw Components.Exception("No valueToPreference implementation",
+ Components.results.NS_ERROR_NOT_IMPLEMENTED);
+ ]]>
+ </body>
+ </method>
+
+ <method name="inputChanged">
+ <body>
+ <![CDATA[
+ if (this.usePref && !this._updatingInput) {
+ this.valueToPreference();
+ this.fireEvent("oninputchanged");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="preferenceChanged">
+ <body>
+ <![CDATA[
+ if (this.usePref) {
+ this._updatingInput = true;
+ try {
+ this.valueFromPreference();
+ this.fireEvent("onpreferencechanged");
+ } catch (e) {}
+ this._updatingInput = false;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="usePref" readonly="true" onget="return this.hasAttribute('pref');"/>
+ <property name="pref" readonly="true" onget="return this.getAttribute('pref');"/>
+ <property name="type" readonly="true" onget="return this.getAttribute('type');"/>
+ <property name="value" onget="return this.input.value;" onset="return this.input.value = val;"/>
+
+ <field name="_updatingInput">false</field>
+ <field name="input">document.getAnonymousElementByAttribute(this, "anonid", "input");</field>
+ <field name="settings">
+ this.parentNode.localName == "settings" ? this.parentNode : null;
+ </field>
+ </implementation>
+ </binding>
+
+ <binding id="setting-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
+ <content>
+ <xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/>
+ </xul:hbox>
+ <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/>
+ <xul:label class="preferences-learnmore text-link"
+ onclick="document.getBindingParent(this).openLearnMore()">&setting.learnmore;</xul:label>
+ </xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:checkbox anonid="input" xbl:inherits="disabled,onlabel,offlabel,label=checkboxlabel" oncommand="inputChanged();"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <method name="valueFromPreference">
+ <body>
+ <![CDATA[
+ let val = Services.prefs.getBoolPref(this.pref);
+ this.value = this.inverted ? !val : val;
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueToPreference">
+ <body>
+ <![CDATA[
+ let val = this.value;
+ Services.prefs.setBoolPref(this.pref, this.inverted ? !val : val);
+ ]]>
+ </body>
+ </method>
+
+ <property name="value" onget="return this.input.checked;" onset="return this.input.setChecked(val);"/>
+ <property name="inverted" readonly="true" onget="return this.getAttribute('inverted');"/>
+
+ <method name="openLearnMore">
+ <body>
+ <![CDATA[
+ window.open(this.getAttribute("learnmore"), "_blank");
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="setting-boolint" extends="chrome://mozapps/content/extensions/setting.xml#setting-bool">
+ <implementation>
+ <method name="valueFromPreference">
+ <body>
+ <![CDATA[
+ let val = Services.prefs.getIntPref(this.pref);
+ this.value = (val == this.getAttribute("on"));
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueToPreference">
+ <body>
+ <![CDATA[
+ Services.prefs.setIntPref(this.pref, this.getAttribute(this.value ? "on" : "off"));
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="setting-localized-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-bool">
+ <implementation>
+ <method name="valueFromPreference">
+ <body>
+ <![CDATA[
+ let val = Services.prefs.getComplexValue(this.pref, Components.interfaces.nsIPrefLocalizedString).data;
+ if(this.inverted) val = !val;
+ this.value = (val == "true");
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueToPreference">
+ <body>
+ <![CDATA[
+ let val = this.value;
+ if(this.inverted) val = !val;
+ let pref = Components.classes["@mozilla.org/pref-localizedstring;1"].createInstance(Components.interfaces.nsIPrefLocalizedString);
+ pref.data = this.inverted ? (!val).toString() : val.toString();
+ Services.prefs.setComplexValue(this.pref, Components.interfaces.nsIPrefLocalizedString, pref);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="setting-integer" extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
+ <content>
+ <xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/>
+ </xul:hbox>
+ <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/>
+ </xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:textbox type="number" anonid="input" oninput="inputChanged();" onchange="inputChanged();"
+ xbl:inherits="disabled,emptytext,min,max,increment,hidespinbuttons,wraparound,size"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <method name="valueFromPreference">
+ <body>
+ <![CDATA[
+ let val = Services.prefs.getIntPref(this.pref);
+ this.value = val;
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueToPreference">
+ <body>
+ <![CDATA[
+ Services.prefs.setIntPref(this.pref, this.value);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="setting-control" extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
+ <content>
+ <xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/>
+ </xul:hbox>
+ <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/>
+ </xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <children/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="setting-string" extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
+ <content>
+ <xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/>
+ </xul:hbox>
+ <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/>
+ </xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:textbox anonid="input" flex="1" oninput="inputChanged();"
+ xbl:inherits="disabled,emptytext,type=inputtype,min,max,increment,hidespinbuttons,decimalplaces,wraparound"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <method name="valueFromPreference">
+ <body>
+ <![CDATA[
+ const nsISupportsString = Components.interfaces.nsISupportsString;
+ this.value = Services.prefs.getComplexValue(this.pref, nsISupportsString).data;
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueToPreference">
+ <body>
+ <![CDATA[
+ const nsISupportsString = Components.interfaces.nsISupportsString;
+ let iss = Components.classes["@mozilla.org/supports-string;1"].createInstance(nsISupportsString);
+ iss.data = this.value;
+ Services.prefs.setComplexValue(this.pref, nsISupportsString, iss);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="setting-color" extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
+ <content>
+ <xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/>
+ </xul:hbox>
+ <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/>
+ </xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:colorpicker type="button" anonid="input" xbl:inherits="disabled" onchange="document.getBindingParent(this).inputChanged();"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <method name="valueFromPreference">
+ <body>
+ <![CDATA[
+ // We must wait for the colorpicker's binding to be applied before setting the value
+ if (!this.input.color)
+ this.input.initialize();
+ this.value = Services.prefs.getCharPref(this.pref);
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueToPreference">
+ <body>
+ <![CDATA[
+ Services.prefs.setCharPref(this.pref, this.value);
+ ]]>
+ </body>
+ </method>
+
+ <property name="value" onget="return this.input.color;" onset="return this.input.color = val;"/>
+ </implementation>
+ </binding>
+
+ <binding id="setting-path" extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
+ <content>
+ <xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/>
+ </xul:hbox>
+ <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/>
+ </xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:button type="button" anonid="button" label="&settings.path.button.label;" xbl:inherits="disabled" oncommand="showPicker();"/>
+ <xul:label anonid="input" flex="1" crop="center" xbl:inherits="disabled"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <method name="showPicker">
+ <body>
+ <![CDATA[
+ var filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ filePicker.init(window, this.getAttribute("title"),
+ this.type == "file" ? Ci.nsIFilePicker.modeOpen : Ci.nsIFilePicker.modeGetFolder);
+ if (this.value) {
+ try {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(this.value);
+ filePicker.displayDirectory = this.type == "file" ? file.parent : file;
+ if (this.type == "file") {
+ filePicker.defaultString = file.leafName;
+ }
+ } catch (e) {}
+ }
+ if (filePicker.show() != Ci.nsIFilePicker.returnCancel) {
+ this.value = filePicker.file.path;
+ this.inputChanged();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueFromPreference">
+ <body>
+ <![CDATA[
+ this.value = Services.prefs.getCharPref(this.pref);
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueToPreference">
+ <body>
+ <![CDATA[
+ Services.prefs.setCharPref(this.pref, this.value);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_value"></field>
+
+ <property name="value">
+ <getter>
+ <![CDATA[
+ return this._value;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this._value = val;
+ let label = "";
+ if (val) {
+ try {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(val);
+ label = this.hasAttribute("fullpath") ? file.path : file.leafName;
+ } catch (e) {}
+ }
+ this.input.tooltipText = val;
+ return this.input.value = label;
+ ]]>
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="setting-multi" extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
+ <content>
+ <xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/>
+ </xul:hbox>
+ <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/>
+ </xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <children includes="radiogroup|menulist"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ this.control.addEventListener("command", this.inputChanged.bind(this), false);
+ ]]>
+ </constructor>
+
+ <method name="valueFromPreference">
+ <body>
+ <![CDATA[
+ let val;
+ switch (Services.prefs.getPrefType(this.pref)) {
+ case Ci.nsIPrefBranch.PREF_STRING:
+ val = Services.prefs.getCharPref(this.pref);
+ break;
+ case Ci.nsIPrefBranch.PREF_INT:
+ val = Services.prefs.getIntPref(this.pref);
+ break;
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ val = Services.prefs.getBoolPref(this.pref).toString();
+ break;
+ default:
+ return;
+ }
+
+ if ("itemCount" in this.control) {
+ for (let i = 0; i < this.control.itemCount; i++) {
+ if (this.control.getItemAtIndex(i).value == val) {
+ this.control.selectedIndex = i;
+ break;
+ }
+ }
+ } else {
+ this.control.setAttribute("value", val);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueToPreference">
+ <body>
+ <![CDATA[
+ // We might not have a pref already set, so we guess the type from the value attribute
+ let val = this.control.selectedItem.value;
+ if (val == "true" || val == "false")
+ Services.prefs.setBoolPref(this.pref, val == "true");
+ else if (/^-?\d+$/.test(val))
+ Services.prefs.setIntPref(this.pref, val);
+ else
+ Services.prefs.setCharPref(this.pref, val);
+ ]]>
+ </body>
+ </method>
+
+ <field name="control">this.getElementsByTagName(this.getAttribute("type") == "radio" ? "radiogroup" : "menulist")[0];</field>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/components/extensions/content/update.js b/components/extensions/content/update.js
new file mode 100644
index 000000000..98495b426
--- /dev/null
+++ b/components/extensions/content/update.js
@@ -0,0 +1,691 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// This UI is only opened from the Extension Manager when the app is upgraded.
+
+"use strict";
+
+const PREF_UPDATE_EXTENSIONS_ENABLED = "extensions.update.enabled";
+const PREF_XPINSTALL_ENABLED = "xpinstall.enabled";
+
+// timeout (in milliseconds) to wait for response to the metadata ping
+const METADATA_TIMEOUT = 30000;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate", "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/Log.jsm");
+var logger = null;
+
+var gUpdateWizard = {
+ // When synchronizing app compatibility info this contains all installed
+ // add-ons. When checking for compatible versions this contains only
+ // incompatible add-ons.
+ addons: [],
+ // Contains a Set of IDs for add-on that were disabled by the application update.
+ affectedAddonIDs: null,
+ // The add-ons that we found updates available for
+ addonsToUpdate: [],
+ shouldSuggestAutoChecking: false,
+ shouldAutoCheck: false,
+ xpinstallEnabled: true,
+ xpinstallLocked: false,
+ // cached AddonInstall entries for add-ons we might want to update,
+ // keyed by add-on ID
+ addonInstalls: new Map(),
+ shuttingDown: false,
+ // Count the add-ons disabled by this update, enabled/disabled by
+ // metadata checks, and upgraded.
+ disabled: 0,
+ metadataEnabled: 0,
+ metadataDisabled: 0,
+ upgraded: 0,
+ upgradeFailed: 0,
+ upgradeDeclined: 0,
+
+ init: function gUpdateWizard_init()
+ {
+ logger = Log.repository.getLogger("addons.update-dialog");
+ // XXX could we pass the addons themselves rather than the IDs?
+ this.affectedAddonIDs = new Set(window.arguments[0]);
+
+ try {
+ this.shouldSuggestAutoChecking =
+ !Services.prefs.getBoolPref(PREF_UPDATE_EXTENSIONS_ENABLED);
+ }
+ catch (e) {
+ }
+
+ try {
+ this.xpinstallEnabled = Services.prefs.getBoolPref(PREF_XPINSTALL_ENABLED);
+ this.xpinstallLocked = Services.prefs.prefIsLocked(PREF_XPINSTALL_ENABLED);
+ }
+ catch (e) {
+ }
+
+ if (Services.io.offline)
+ document.documentElement.currentPage = document.getElementById("offline");
+ else
+ document.documentElement.currentPage = document.getElementById("versioninfo");
+ },
+
+ onWizardFinish: function gUpdateWizard_onWizardFinish ()
+ {
+ if (this.shouldSuggestAutoChecking)
+ Services.prefs.setBoolPref(PREF_UPDATE_EXTENSIONS_ENABLED, this.shouldAutoCheck);
+ },
+
+ _setUpButton: function gUpdateWizard_setUpButton(aButtonID, aButtonKey, aDisabled)
+ {
+ var strings = document.getElementById("updateStrings");
+ var button = document.documentElement.getButton(aButtonID);
+ if (aButtonKey) {
+ button.label = strings.getString(aButtonKey);
+ try {
+ button.setAttribute("accesskey", strings.getString(aButtonKey + "Accesskey"));
+ }
+ catch (e) {
+ }
+ }
+ button.disabled = aDisabled;
+ },
+
+ setButtonLabels: function gUpdateWizard_setButtonLabels(aBackButton, aBackButtonIsDisabled,
+ aNextButton, aNextButtonIsDisabled,
+ aCancelButton, aCancelButtonIsDisabled)
+ {
+ this._setUpButton("back", aBackButton, aBackButtonIsDisabled);
+ this._setUpButton("next", aNextButton, aNextButtonIsDisabled);
+ this._setUpButton("cancel", aCancelButton, aCancelButtonIsDisabled);
+ },
+
+ /////////////////////////////////////////////////////////////////////////////
+ // Update Errors
+ errorItems: [],
+
+ checkForErrors: function gUpdateWizard_checkForErrors(aElementIDToShow)
+ {
+ if (this.errorItems.length > 0)
+ document.getElementById(aElementIDToShow).hidden = false;
+ },
+
+ onWizardClose: function gUpdateWizard_onWizardClose(aEvent)
+ {
+ return this.onWizardCancel();
+ },
+
+ onWizardCancel: function gUpdateWizard_onWizardCancel()
+ {
+ gUpdateWizard.shuttingDown = true;
+ // Allow add-ons to continue downloading and installing
+ // in the background, though some may require a later restart
+ // Pages that are waiting for user input go into the background
+ // on cancel
+ if (gMismatchPage.waiting) {
+ logger.info("Dialog closed in mismatch page");
+ if (gUpdateWizard.addonInstalls.size > 0) {
+ // Tycho: gInstallingPage.startInstalls([i for ([, i] of gUpdateWizard.addonInstalls)]);
+ let results = [];
+ for (let [, i] of gUpdateWizard.addonInstalls) {
+ results.push(i);
+ }
+
+ gInstallingPage.startInstalls(results);
+ }
+ return true;
+ }
+
+ // Pages that do asynchronous things will just keep running and check
+ // gUpdateWizard.shuttingDown to trigger background behaviour
+ if (!gInstallingPage.installing) {
+ logger.info("Dialog closed while waiting for updated compatibility information");
+ }
+ else {
+ logger.info("Dialog closed while downloading and installing updates");
+ }
+ return true;
+ }
+};
+
+var gOfflinePage = {
+ onPageAdvanced: function gOfflinePage_onPageAdvanced()
+ {
+ Services.io.offline = false;
+ return true;
+ },
+
+ toggleOffline: function gOfflinePage_toggleOffline()
+ {
+ var nextbtn = document.documentElement.getButton("next");
+ nextbtn.disabled = !nextbtn.disabled;
+ }
+}
+
+// Addon listener to count addons enabled/disabled by metadata checks
+var listener = {
+ onDisabled: function listener_onDisabled(aAddon) {
+ gUpdateWizard.affectedAddonIDs.add(aAddon.id);
+ gUpdateWizard.metadataDisabled++;
+ },
+ onEnabled: function listener_onEnabled(aAddon) {
+ gUpdateWizard.affectedAddonIDs.delete(aAddon.id);
+ gUpdateWizard.metadataEnabled++;
+ }
+};
+
+var gVersionInfoPage = {
+ _completeCount: 0,
+ _totalCount: 0,
+ _versionInfoDone: false,
+ onPageShow: Task.async(function* gVersionInfoPage_onPageShow() {
+ gUpdateWizard.setButtonLabels(null, true,
+ "nextButtonText", true,
+ "cancelButtonText", false);
+
+ gUpdateWizard.disabled = gUpdateWizard.affectedAddonIDs.size;
+
+ // Ensure compatibility overrides are up to date before checking for
+ // individual addon updates.
+ AddonManager.addAddonListener(listener);
+ if (AddonRepository.isMetadataStale()) {
+ // Do the metadata ping, listening for any newly enabled/disabled add-ons.
+ yield AddonRepository.repopulateCache(METADATA_TIMEOUT);
+ if (gUpdateWizard.shuttingDown) {
+ logger.debug("repopulateCache completed after dialog closed");
+ }
+ }
+ // Fetch the add-ons that are still affected by this update.
+ // Tycho: let idlist = [id for (id of gUpdateWizard.affectedAddonIDs)];
+
+ let idlist = [];
+ for (let id of gUpdateWizard.affectedAddonIDs) {
+ idlist.push(id);
+ }
+
+ if (idlist.length < 1) {
+ gVersionInfoPage.onAllUpdatesFinished();
+ return;
+ }
+
+ logger.debug("Fetching affected addons " + idlist.toSource());
+ let fetchedAddons = yield new Promise((resolve, reject) =>
+ AddonManager.getAddonsByIDs(idlist, resolve));
+ // We shouldn't get nulls here, but let's be paranoid...
+ // Tycho: gUpdateWizard.addons = [a for (a of fetchedAddons) if (a)];
+ let results = [];
+ for (let a of fetchedAddons) {
+ if (a) {
+ results.push(a);
+ }
+ }
+
+ gUpdateWizard.addons = results;
+
+ if (gUpdateWizard.addons.length < 1) {
+ gVersionInfoPage.onAllUpdatesFinished();
+ return;
+ }
+
+ gVersionInfoPage._totalCount = gUpdateWizard.addons.length;
+
+ for (let addon of gUpdateWizard.addons) {
+ logger.debug("VersionInfo Finding updates for ${id}", addon);
+ addon.findUpdates(gVersionInfoPage, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ }
+ }),
+
+ onAllUpdatesFinished: function gVersionInfoPage_onAllUpdatesFinished() {
+ AddonManager.removeAddonListener(listener);
+ // Filter out any add-ons that are now enabled.
+ // Tycho:
+ // logger.debug("VersionInfo updates finished: found " +
+ // [addon.id + ":" + addon.appDisabled for (addon of gUpdateWizard.addons)].toSource());
+
+ let logDisabledAddons = [];
+ for (let addon of gUpdateWizard.addons) {
+ if (addon.appDisabled) {
+ logDisabledAddons.push(addon.id + ":" + addon.appDisabled);
+ }
+ }
+ logger.debug("VersionInfo updates finished: found " + logDisabledAddons.toSource());
+
+ let filteredAddons = [];
+ for (let a of gUpdateWizard.addons) {
+ if (a.appDisabled) {
+ logger.debug("Continuing with add-on " + a.id);
+ filteredAddons.push(a);
+ }
+ else if (gUpdateWizard.addonInstalls.has(a.id)) {
+ gUpdateWizard.addonInstalls.get(a.id).cancel();
+ gUpdateWizard.addonInstalls.delete(a.id);
+ }
+ }
+ gUpdateWizard.addons = filteredAddons;
+
+ if (gUpdateWizard.shuttingDown) {
+ // jump directly to updating auto-update add-ons in the background
+ if (gUpdateWizard.addonInstalls.size > 0) {
+ // Tycho: gInstallingPage.startInstalls([i for ([, i] of gUpdateWizard.addonInstalls)]);
+ let results = [];
+ for (let [, i] of gUpdateWizard.addonInstalls) {
+ results.push(i);
+ }
+
+ gInstallingPage.startInstalls(results);
+ }
+ return;
+ }
+
+ if (filteredAddons.length > 0) {
+ if (!gUpdateWizard.xpinstallEnabled && gUpdateWizard.xpinstallLocked) {
+ document.documentElement.currentPage = document.getElementById("adminDisabled");
+ return;
+ }
+ document.documentElement.currentPage = document.getElementById("mismatch");
+ }
+ else {
+ logger.info("VersionInfo: No updates require further action");
+ // VersionInfo compatibility updates resolved all compatibility problems,
+ // close this window and continue starting the application...
+ //XXX Bug 314754 - We need to use setTimeout to close the window due to
+ // the EM using xmlHttpRequest when checking for updates.
+ setTimeout(close, 0);
+ }
+ },
+
+ /////////////////////////////////////////////////////////////////////////////
+ // UpdateListener
+ onUpdateFinished: function gVersionInfoPage_onUpdateFinished(aAddon, status) {
+ ++this._completeCount;
+
+ if (status != AddonManager.UPDATE_STATUS_NO_ERROR) {
+ logger.debug("VersionInfo update " + this._completeCount + " of " + this._totalCount +
+ " failed for " + aAddon.id + ": " + status);
+ gUpdateWizard.errorItems.push(aAddon);
+ }
+ else {
+ logger.debug("VersionInfo update " + this._completeCount + " of " + this._totalCount +
+ " finished for " + aAddon.id);
+ }
+
+ // If we're not in the background, just make a list of add-ons that have
+ // updates available
+ if (!gUpdateWizard.shuttingDown) {
+ // If we're still in the update check window and the add-on is now active
+ // then it won't have been disabled by startup
+ if (aAddon.active) {
+ AddonManagerPrivate.removeStartupChange(AddonManager.STARTUP_CHANGE_DISABLED, aAddon.id);
+ gUpdateWizard.metadataEnabled++;
+ }
+
+ // Update the status text and progress bar
+ var updateStrings = document.getElementById("updateStrings");
+ var statusElt = document.getElementById("versioninfo.status");
+ var statusString = updateStrings.getFormattedString("statusPrefix", [aAddon.name]);
+ statusElt.setAttribute("value", statusString);
+
+ // Update the status text and progress bar
+ var progress = document.getElementById("versioninfo.progress");
+ progress.mode = "normal";
+ progress.value = Math.ceil((this._completeCount / this._totalCount) * 100);
+ }
+
+ if (this._completeCount == this._totalCount)
+ this.onAllUpdatesFinished();
+ },
+
+ onUpdateAvailable: function gVersionInfoPage_onUpdateAvailable(aAddon, aInstall) {
+ logger.debug("VersionInfo got an install for " + aAddon.id + ": " + aAddon.version);
+ gUpdateWizard.addonInstalls.set(aAddon.id, aInstall);
+ },
+};
+
+var gMismatchPage = {
+ waiting: false,
+
+ onPageShow: function gMismatchPage_onPageShow()
+ {
+ gMismatchPage.waiting = true;
+ gUpdateWizard.setButtonLabels(null, true,
+ "mismatchCheckNow", false,
+ "mismatchDontCheck", false);
+ document.documentElement.getButton("next").focus();
+
+ var incompatible = document.getElementById("mismatch.incompatible");
+ for (let addon of gUpdateWizard.addons) {
+ var listitem = document.createElement("listitem");
+ listitem.setAttribute("label", addon.name + " " + addon.version);
+ incompatible.appendChild(listitem);
+ }
+ }
+};
+
+var gUpdatePage = {
+ _totalCount: 0,
+ _completeCount: 0,
+ onPageShow: function gUpdatePage_onPageShow()
+ {
+ gMismatchPage.waiting = false;
+ gUpdateWizard.setButtonLabels(null, true,
+ "nextButtonText", true,
+ "cancelButtonText", false);
+ document.documentElement.getButton("next").focus();
+
+ gUpdateWizard.errorItems = [];
+
+ this._totalCount = gUpdateWizard.addons.length;
+ for (let addon of gUpdateWizard.addons) {
+ logger.debug("UpdatePage requesting update for " + addon.id);
+ // Redundant call to find updates again here when we already got them
+ // in the VersionInfo page: https://bugzilla.mozilla.org/show_bug.cgi?id=960597
+ addon.findUpdates(this, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ }
+ },
+
+ onAllUpdatesFinished: function gUpdatePage_onAllUpdatesFinished() {
+ if (gUpdateWizard.shuttingDown)
+ return;
+
+ var nextPage = document.getElementById("noupdates");
+ if (gUpdateWizard.addonsToUpdate.length > 0)
+ nextPage = document.getElementById("found");
+ document.documentElement.currentPage = nextPage;
+ },
+
+ /////////////////////////////////////////////////////////////////////////////
+ // UpdateListener
+ onUpdateAvailable: function gUpdatePage_onUpdateAvailable(aAddon, aInstall) {
+ logger.debug("UpdatePage got an update for " + aAddon.id + ": " + aAddon.version);
+ gUpdateWizard.addonsToUpdate.push(aInstall);
+ },
+
+ onUpdateFinished: function gUpdatePage_onUpdateFinished(aAddon, status) {
+ if (status != AddonManager.UPDATE_STATUS_NO_ERROR)
+ gUpdateWizard.errorItems.push(aAddon);
+
+ ++this._completeCount;
+
+ if (!gUpdateWizard.shuttingDown) {
+ // Update the status text and progress bar
+ var updateStrings = document.getElementById("updateStrings");
+ var statusElt = document.getElementById("checking.status");
+ var statusString = updateStrings.getFormattedString("statusPrefix", [aAddon.name]);
+ statusElt.setAttribute("value", statusString);
+
+ var progress = document.getElementById("checking.progress");
+ progress.value = Math.ceil((this._completeCount / this._totalCount) * 100);
+ }
+
+ if (this._completeCount == this._totalCount)
+ this.onAllUpdatesFinished()
+ },
+};
+
+var gFoundPage = {
+ onPageShow: function gFoundPage_onPageShow()
+ {
+ gUpdateWizard.setButtonLabels(null, true,
+ "installButtonText", false,
+ null, false);
+
+ var foundUpdates = document.getElementById("found.updates");
+ var itemCount = gUpdateWizard.addonsToUpdate.length;
+ for (let install of gUpdateWizard.addonsToUpdate) {
+ let listItem = foundUpdates.appendItem(install.name + " " + install.version);
+ listItem.setAttribute("type", "checkbox");
+ listItem.setAttribute("checked", "true");
+ listItem.install = install;
+ }
+
+ if (!gUpdateWizard.xpinstallEnabled) {
+ document.getElementById("xpinstallDisabledAlert").hidden = false;
+ document.getElementById("enableXPInstall").focus();
+ document.documentElement.getButton("next").disabled = true;
+ }
+ else {
+ document.documentElement.getButton("next").focus();
+ document.documentElement.getButton("next").disabled = false;
+ }
+ },
+
+ toggleXPInstallEnable: function gFoundPage_toggleXPInstallEnable(aEvent)
+ {
+ var enabled = aEvent.target.checked;
+ gUpdateWizard.xpinstallEnabled = enabled;
+ var pref = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ pref.setBoolPref(PREF_XPINSTALL_ENABLED, enabled);
+ this.updateNextButton();
+ },
+
+ updateNextButton: function gFoundPage_updateNextButton()
+ {
+ if (!gUpdateWizard.xpinstallEnabled) {
+ document.documentElement.getButton("next").disabled = true;
+ return;
+ }
+
+ var oneChecked = false;
+ var foundUpdates = document.getElementById("found.updates");
+ var updates = foundUpdates.getElementsByTagName("listitem");
+ for (let update of updates) {
+ if (!update.checked)
+ continue;
+ oneChecked = true;
+ break;
+ }
+
+ gUpdateWizard.setButtonLabels(null, true,
+ "installButtonText", true,
+ null, false);
+ document.getElementById("found").setAttribute("next", "installing");
+ document.documentElement.getButton("next").disabled = !oneChecked;
+ }
+};
+
+var gInstallingPage = {
+ _installs : [],
+ _errors : [],
+ _strings : null,
+ _currentInstall : -1,
+ _installing : false,
+
+ // Initialize fields we need for installing and tracking progress,
+ // and start iterating through the installations
+ startInstalls: function gInstallingPage_startInstalls(aInstallList) {
+ if (!gUpdateWizard.xpinstallEnabled) {
+ return;
+ }
+
+ // Tycho:
+ // logger.debug("Start installs for "
+ // + [i.existingAddon.id for (i of aInstallList)].toSource());
+
+ let logInstallAddons = [];
+ for (let i of aInstallList) {
+ logInstallAddons.push(i.existingAddon.id);
+ }
+ logger.debug("Start installs for " + logInstallAddons.toSource());
+
+ this._errors = [];
+ this._installs = aInstallList;
+ this._installing = true;
+ this.startNextInstall();
+ },
+
+ onPageShow: function gInstallingPage_onPageShow()
+ {
+ gUpdateWizard.setButtonLabels(null, true,
+ "nextButtonText", true,
+ null, true);
+
+ var foundUpdates = document.getElementById("found.updates");
+ var updates = foundUpdates.getElementsByTagName("listitem");
+ let toInstall = [];
+ for (let update of updates) {
+ if (!update.checked) {
+ logger.info("User chose to cancel update of " + update.label);
+ gUpdateWizard.upgradeDeclined++;
+ update.install.cancel();
+ continue;
+ }
+ toInstall.push(update.install);
+ }
+ this._strings = document.getElementById("updateStrings");
+
+ this.startInstalls(toInstall);
+ },
+
+ startNextInstall: function gInstallingPage_startNextInstall() {
+ if (this._currentInstall >= 0) {
+ this._installs[this._currentInstall].removeListener(this);
+ }
+
+ this._currentInstall++;
+
+ if (this._installs.length == this._currentInstall) {
+ Services.obs.notifyObservers(null, "TEST:all-updates-done", null);
+ this._installing = false;
+ if (gUpdateWizard.shuttingDown) {
+ return;
+ }
+ var nextPage = this._errors.length > 0 ? "installerrors" : "finished";
+ document.getElementById("installing").setAttribute("next", nextPage);
+ document.documentElement.advance();
+ return;
+ }
+
+ let install = this._installs[this._currentInstall];
+
+ if (gUpdateWizard.shuttingDown && !AddonManager.shouldAutoUpdate(install.existingAddon)) {
+ logger.debug("Don't update " + install.existingAddon.id + " in background");
+ gUpdateWizard.upgradeDeclined++;
+ install.cancel();
+ this.startNextInstall();
+ return;
+ }
+ install.addListener(this);
+ install.install();
+ },
+
+ /////////////////////////////////////////////////////////////////////////////
+ // InstallListener
+ onDownloadStarted: function gInstallingPage_onDownloadStarted(aInstall) {
+ if (gUpdateWizard.shuttingDown) {
+ return;
+ }
+ var strings = document.getElementById("updateStrings");
+ var label = strings.getFormattedString("downloadingPrefix", [aInstall.name]);
+ var actionItem = document.getElementById("actionItem");
+ actionItem.value = label;
+ },
+
+ onDownloadProgress: function gInstallingPage_onDownloadProgress(aInstall) {
+ if (gUpdateWizard.shuttingDown) {
+ return;
+ }
+ var downloadProgress = document.getElementById("downloadProgress");
+ downloadProgress.value = Math.ceil(100 * aInstall.progress / aInstall.maxProgress);
+ },
+
+ onDownloadEnded: function gInstallingPage_onDownloadEnded(aInstall) {
+ },
+
+ onDownloadFailed: function gInstallingPage_onDownloadFailed(aInstall) {
+ this._errors.push(aInstall);
+
+ gUpdateWizard.upgradeFailed++;
+ this.startNextInstall();
+ },
+
+ onInstallStarted: function gInstallingPage_onInstallStarted(aInstall) {
+ if (gUpdateWizard.shuttingDown) {
+ return;
+ }
+ var strings = document.getElementById("updateStrings");
+ var label = strings.getFormattedString("installingPrefix", [aInstall.name]);
+ var actionItem = document.getElementById("actionItem");
+ actionItem.value = label;
+ },
+
+ onInstallEnded: function gInstallingPage_onInstallEnded(aInstall, aAddon) {
+ if (!gUpdateWizard.shuttingDown) {
+ // Remember that this add-on was updated during startup
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ aAddon.id);
+ }
+
+ gUpdateWizard.upgraded++;
+ this.startNextInstall();
+ },
+
+ onInstallFailed: function gInstallingPage_onInstallFailed(aInstall) {
+ this._errors.push(aInstall);
+
+ gUpdateWizard.upgradeFailed++;
+ this.startNextInstall();
+ }
+};
+
+var gInstallErrorsPage = {
+ onPageShow: function gInstallErrorsPage_onPageShow()
+ {
+ gUpdateWizard.setButtonLabels(null, true, null, true, null, true);
+ document.documentElement.getButton("finish").focus();
+ },
+};
+
+// Displayed when there are incompatible add-ons and the xpinstall.enabled
+// pref is false and locked.
+var gAdminDisabledPage = {
+ onPageShow: function gAdminDisabledPage_onPageShow()
+ {
+ gUpdateWizard.setButtonLabels(null, true, null, true,
+ "cancelButtonText", true);
+ document.documentElement.getButton("finish").focus();
+ }
+};
+
+// Displayed when selected add-on updates have been installed without error.
+// There can still be add-ons that are not compatible and don't have an update.
+var gFinishedPage = {
+ onPageShow: function gFinishedPage_onPageShow()
+ {
+ gUpdateWizard.setButtonLabels(null, true, null, true, null, true);
+ document.documentElement.getButton("finish").focus();
+
+ if (gUpdateWizard.shouldSuggestAutoChecking) {
+ document.getElementById("finishedCheckDisabled").hidden = false;
+ gUpdateWizard.shouldAutoCheck = true;
+ }
+ else
+ document.getElementById("finishedCheckEnabled").hidden = false;
+
+ document.documentElement.getButton("finish").focus();
+ }
+};
+
+// Displayed when there are incompatible add-ons and there are no available
+// updates.
+var gNoUpdatesPage = {
+ onPageShow: function gNoUpdatesPage_onPageLoad(aEvent)
+ {
+ gUpdateWizard.setButtonLabels(null, true, null, true, null, true);
+ if (gUpdateWizard.shouldSuggestAutoChecking) {
+ document.getElementById("noupdatesCheckDisabled").hidden = false;
+ gUpdateWizard.shouldAutoCheck = true;
+ }
+ else
+ document.getElementById("noupdatesCheckEnabled").hidden = false;
+
+ gUpdateWizard.checkForErrors("updateCheckErrorNotFound");
+ document.documentElement.getButton("finish").focus();
+ }
+};
diff --git a/components/extensions/content/update.xul b/components/extensions/content/update.xul
new file mode 100644
index 000000000..9f5f35196
--- /dev/null
+++ b/components/extensions/content/update.xul
@@ -0,0 +1,180 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: XML; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- -->
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/extensions/update.css" type="text/css"?>
+
+<!DOCTYPE wizard [
+<!ENTITY % updateDTD SYSTEM "chrome://mozapps/locale/extensions/update.dtd">
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%updateDTD;
+%brandDTD;
+]>
+
+<wizard id="updateWizard"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&updateWizard.title;"
+ windowtype="Addons:Compatibility"
+ branded="true"
+ onload="gUpdateWizard.init();"
+ onwizardfinish="gUpdateWizard.onWizardFinish();"
+ onwizardcancel="return gUpdateWizard.onWizardCancel();"
+ onclose="return gUpdateWizard.onWizardClose(event);"
+ buttons="accept,cancel">
+
+ <script type="application/javascript" src="chrome://mozapps/content/extensions/update.js"/>
+
+ <stringbundleset id="updateSet">
+ <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="updateStrings" src="chrome://mozapps/locale/extensions/update.properties"/>
+ </stringbundleset>
+
+ <wizardpage id="dummy" pageid="dummy"/>
+
+ <wizardpage id="offline" pageid="offline" next="versioninfo"
+ label="&offline.title;"
+ onpageadvanced="return gOfflinePage.onPageAdvanced();">
+ <description>&offline.description;</description>
+ <checkbox id="toggleOffline"
+ checked="true"
+ label="&offline.toggleOffline.label;"
+ accesskey="&offline.toggleOffline.accesskey;"
+ oncommand="gOfflinePage.toggleOffline();"/>
+ </wizardpage>
+
+ <wizardpage id="versioninfo" pageid="versioninfo" next="mismatch"
+ label="&versioninfo.wizard.title;"
+ onpageshow="gVersionInfoPage.onPageShow();">
+ <label>&versioninfo.top.label;</label>
+ <separator class="thin"/>
+ <progressmeter id="versioninfo.progress" mode="undetermined"/>
+ <hbox align="center">
+ <image id="versioninfo.throbber" class="throbber"/>
+ <label flex="1" id="versioninfo.status" crop="right">&versioninfo.waiting;</label>
+ </hbox>
+ <separator/>
+ </wizardpage>
+
+ <wizardpage id="mismatch" pageid="mismatch" next="checking"
+ label="&mismatch.win.title;"
+ onpageshow="gMismatchPage.onPageShow();">
+ <label>&mismatch.top.label;</label>
+ <separator class="thin"/>
+ <listbox id="mismatch.incompatible" flex="1"/>
+ <separator class="thin"/>
+ <label>&mismatch.bottom.label;</label>
+ </wizardpage>
+
+ <wizardpage id="checking" pageid="checking" next="noupdates"
+ label="&checking.wizard.title;"
+ onpageshow="gUpdatePage.onPageShow();">
+ <label>&checking.top.label;</label>
+ <separator class="thin"/>
+ <progressmeter id="checking.progress"/>
+ <hbox align="center">
+ <image id="checking.throbber" class="throbber"/>
+ <label id="checking.status" flex="1" crop="right">&checking.status;</label>
+ </hbox>
+ </wizardpage>
+
+ <wizardpage id="noupdates" pageid="noupdates"
+ label="&noupdates.wizard.title;"
+ onpageshow="gNoUpdatesPage.onPageShow();">
+ <description>&noupdates.intro.desc;</description>
+ <separator class="thin"/>
+ <hbox id="updateCheckErrorNotFound" class="alertBox" hidden="true" align="top">
+ <image id="alert"/>
+ <description flex="1">&noupdates.error.desc;</description>
+ </hbox>
+ <separator class="thin"/>
+ <description id="noupdatesCheckEnabled" hidden="true">
+ &noupdates.checkEnabled.desc;
+ </description>
+ <vbox id="noupdatesCheckDisabled" hidden="true">
+ <description>&finished.checkDisabled.desc;</description>
+ <checkbox label="&enableChecking.label;" checked="true"
+ oncommand="gUpdateWizard.shouldAutoCheck = this.checked;"/>
+ </vbox>
+ <separator flex="1"/>
+ <label>&clickFinish.label;</label>
+ <separator class="thin"/>
+ </wizardpage>
+
+ <wizardpage id="found" pageid="found" next="installing"
+ label="&found.wizard.title;"
+ onpageshow="gFoundPage.onPageShow();">
+ <label>&found.top.label;</label>
+ <separator class="thin"/>
+ <listbox id="found.updates" flex="1" seltype="multiple"
+ onclick="gFoundPage.updateNextButton();"/>
+ <separator class="thin"/>
+ <vbox align="left" id="xpinstallDisabledAlert" hidden="true">
+ <description>&found.disabledXPinstall.label;</description>
+ <checkbox label="&found.enableXPInstall.label;"
+ id="enableXPInstall"
+ accesskey="&found.enableXPInstall.accesskey;"
+ oncommand="gFoundPage.toggleXPInstallEnable(event);"/>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="installing" pageid="installing" next="finished"
+ label="&installing.wizard.title;"
+ onpageshow="gInstallingPage.onPageShow();">
+ <label>&installing.top.label;</label>
+ <progressmeter id="downloadProgress"/>
+ <hbox align="center">
+ <image id="installing.throbber" class="throbber"/>
+ <label id="actionItem" flex="1" crop="right"/>
+ </hbox>
+ <separator/>
+ </wizardpage>
+
+ <wizardpage id="installerrors" pageid="installerrors"
+ label="&installerrors.wizard.title;"
+ onpageshow="gInstallErrorsPage.onPageShow();">
+ <hbox align="top" class="alertBox">
+ <description flex="1">&installerrors.intro.label;</description>
+ </hbox>
+ <separator flex="1"/>
+ <label>&clickFinish.label;</label>
+ <separator class="thin"/>
+ </wizardpage>
+
+ <wizardpage id="adminDisabled" pageid="adminDisabled"
+ label="&adminDisabled.wizard.title;"
+ onpageshow="gAdminDisabledPage.onPageShow();">
+ <separator/>
+ <hbox class="alertBox" align="top">
+ <image id="alert"/>
+ <description flex="1">&adminDisabled.warning.label;</description>
+ </hbox>
+ <separator flex="1"/>
+ <label>&clickFinish.label;</label>
+ <separator class="thin"/>
+ </wizardpage>
+
+ <wizardpage id="finished" pageid="finished"
+ label="&finished.wizard.title;"
+ onpageshow="gFinishedPage.onPageShow();">
+
+ <label>&finished.top.label;</label>
+ <separator/>
+ <description id="finishedCheckEnabled" hidden="true">
+ &finished.checkEnabled.desc;
+ </description>
+ <vbox id="finishedCheckDisabled" hidden="true">
+ <description>&finished.checkDisabled.desc;</description>
+ <checkbox label="&enableChecking.label;" checked="true"
+ oncommand="gUpdateWizard.shouldAutoCheck = this.checked;"/>
+ </vbox>
+ <separator flex="1"/>
+ <label>&clickFinish.label;</label>
+ <separator class="thin"/>
+ </wizardpage>
+
+</wizard>
+
diff --git a/components/extensions/content/updateinfo.xsl b/components/extensions/content/updateinfo.xsl
new file mode 100644
index 000000000..5fcccd6d7
--- /dev/null
+++ b/components/extensions/content/updateinfo.xsl
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<xsl:stylesheet version="1.0" xmlns:xhtml="http://www.w3.org/1999/xhtml"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+ <!-- Any elements not otherwise specified will be stripped but the contents
+ will be displayed. All attributes are stripped from copied elements. -->
+
+ <!-- Block these elements and their contents -->
+ <xsl:template match="xhtml:head|xhtml:script|xhtml:style">
+ </xsl:template>
+
+ <!-- Allowable styling elements -->
+ <xsl:template match="xhtml:b|xhtml:i|xhtml:em|xhtml:strong|xhtml:u|xhtml:q|xhtml:sub|xhtml:sup|xhtml:code">
+ <xsl:copy><xsl:apply-templates/></xsl:copy>
+ </xsl:template>
+
+ <!-- Allowable block formatting elements -->
+ <xsl:template match="xhtml:h1|xhtml:h2|xhtml:h3|xhtml:p|xhtml:div|xhtml:blockquote|xhtml:pre">
+ <xsl:copy><xsl:apply-templates/></xsl:copy>
+ </xsl:template>
+
+ <!-- Allowable list formatting elements -->
+ <xsl:template match="xhtml:ul|xhtml:ol|xhtml:li|xhtml:dl|xhtml:dt|xhtml:dd">
+ <xsl:copy><xsl:apply-templates/></xsl:copy>
+ </xsl:template>
+
+ <!-- These elements are copied and their contents dropped -->
+ <xsl:template match="xhtml:br|xhtml:hr">
+ <xsl:copy/>
+ </xsl:template>
+
+ <!-- The root document -->
+ <xsl:template match="/">
+ <xhtml:body><xsl:apply-templates/></xhtml:body>
+ </xsl:template>
+
+</xsl:stylesheet>
diff --git a/components/extensions/content/xpinstallConfirm.css b/components/extensions/content/xpinstallConfirm.css
new file mode 100644
index 000000000..583facfec
--- /dev/null
+++ b/components/extensions/content/xpinstallConfirm.css
@@ -0,0 +1,8 @@
+/* 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/. */
+
+installitem {
+ -moz-binding: url("chrome://mozapps/content/xpinstall/xpinstallItem.xml#installitem");
+ display: -moz-box;
+}
diff --git a/components/extensions/content/xpinstallConfirm.js b/components/extensions/content/xpinstallConfirm.js
new file mode 100644
index 000000000..29be5f5e9
--- /dev/null
+++ b/components/extensions/content/xpinstallConfirm.js
@@ -0,0 +1,192 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var XPInstallConfirm = {};
+
+XPInstallConfirm.init = function()
+{
+ Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+ var _installCountdown;
+ var _installCountdownInterval;
+ var _focused;
+ var _timeout;
+
+ // Default to cancelling the install when the window unloads
+ XPInstallConfirm._installOK = false;
+
+ var bundle = document.getElementById("xpinstallConfirmStrings");
+
+ let args = window.arguments[0].wrappedJSObject;
+
+ // If all installs have already been cancelled in some way then just close
+ // the window
+ if (args.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)) {
+ window.close();
+ return;
+ }
+
+ var _installCountdownLength = 5;
+ try {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var delay_in_milliseconds = prefs.getIntPref("security.dialog_enable_delay");
+ _installCountdownLength = Math.round(delay_in_milliseconds / 500);
+ } catch (ex) { }
+
+ var itemList = document.getElementById("itemList");
+
+ let installMap = new WeakMap();
+ let installListener = {
+ onDownloadCancelled: function(install) {
+ itemList.removeChild(installMap.get(install));
+ if (--numItemsToInstall == 0)
+ window.close();
+ }
+ };
+
+ var numItemsToInstall = args.installs.length;
+ for (let install of args.installs) {
+ var installItem = document.createElement("installitem");
+ itemList.appendChild(installItem);
+
+ installItem.name = install.addon.name;
+ installItem.url = install.sourceURI.spec;
+ var icon = install.iconURL;
+ if (icon)
+ installItem.icon = icon;
+ var type = install.type;
+ if (type)
+ installItem.type = type;
+ if (install.certName) {
+ installItem.cert = bundle.getFormattedString("signed", [install.certName]);
+ }
+ else {
+ installItem.cert = bundle.getString("unverified");
+ }
+ installItem.signed = install.certName ? "true" : "false";
+
+ installMap.set(install, installItem);
+ install.addListener(installListener);
+ }
+
+ var introString = bundle.getString("itemWarnIntroSingle");
+ if (numItemsToInstall > 4)
+ introString = bundle.getFormattedString("itemWarnIntroMultiple", [numItemsToInstall]);
+ var textNode = document.createTextNode(introString);
+ var introNode = document.getElementById("itemWarningIntro");
+ while (introNode.hasChildNodes())
+ introNode.removeChild(introNode.firstChild);
+ introNode.appendChild(textNode);
+
+ var okButton = document.documentElement.getButton("accept");
+ okButton.focus();
+
+ function okButtonCountdown() {
+ _installCountdown -= 1;
+
+ if (_installCountdown < 1) {
+ okButton.label = bundle.getString("installButtonLabel");
+ okButton.disabled = false;
+ clearInterval(_installCountdownInterval);
+ }
+ else
+ okButton.label = bundle.getFormattedString("installButtonDisabledLabel", [_installCountdown]);
+ }
+
+ function myfocus() {
+ // Clear the timeout if it exists so only the last one will be used.
+ if (_timeout)
+ clearTimeout(_timeout);
+
+ // Use setTimeout since the last focus or blur event to fire is the one we
+ // want
+ _timeout = setTimeout(setWidgetsAfterFocus, 0);
+ }
+
+ function setWidgetsAfterFocus() {
+ if (_focused)
+ return;
+
+ _installCountdown = _installCountdownLength;
+ _installCountdownInterval = setInterval(okButtonCountdown, 500);
+ okButton.label = bundle.getFormattedString("installButtonDisabledLabel", [_installCountdown]);
+ _focused = true;
+ }
+
+ function myblur() {
+ // Clear the timeout if it exists so only the last one will be used.
+ if (_timeout)
+ clearTimeout(_timeout);
+
+ // Use setTimeout since the last focus or blur event to fire is the one we
+ // want
+ _timeout = setTimeout(setWidgetsAfterBlur, 0);
+ }
+
+ function setWidgetsAfterBlur() {
+ if (!_focused)
+ return;
+
+ // Set _installCountdown to the inital value set in setWidgetsAfterFocus
+ // plus 1 so when the window is focused there is immediate ui feedback.
+ _installCountdown = _installCountdownLength + 1;
+ okButton.label = bundle.getFormattedString("installButtonDisabledLabel", [_installCountdown]);
+ okButton.disabled = true;
+ clearInterval(_installCountdownInterval);
+ _focused = false;
+ }
+
+ function myUnload() {
+ if (_installCountdownLength > 0) {
+ document.removeEventListener("focus", myfocus, true);
+ document.removeEventListener("blur", myblur, true);
+ }
+ window.removeEventListener("unload", myUnload, false);
+
+ for (let install of args.installs)
+ install.removeListener(installListener);
+
+ // Now perform the desired action - either install the
+ // addons or cancel the installations
+ if (XPInstallConfirm._installOK) {
+ for (let install of args.installs)
+ install.install();
+ }
+ else {
+ for (let install of args.installs) {
+ if (install.state != AddonManager.STATE_CANCELLED)
+ install.cancel();
+ }
+ }
+ }
+
+ window.addEventListener("unload", myUnload, false);
+
+ if (_installCountdownLength > 0) {
+ document.addEventListener("focus", myfocus, true);
+ document.addEventListener("blur", myblur, true);
+
+ okButton.disabled = true;
+ setWidgetsAfterFocus();
+ }
+ else
+ okButton.label = bundle.getString("installButtonLabel");
+}
+
+XPInstallConfirm.onOK = function()
+{
+ // Perform the install or cancel after the window has unloaded
+ XPInstallConfirm._installOK = true;
+ return true;
+}
+
+XPInstallConfirm.onCancel = function()
+{
+ // Perform the install or cancel after the window has unloaded
+ XPInstallConfirm._installOK = false;
+ return true;
+}
diff --git a/components/extensions/content/xpinstallConfirm.xul b/components/extensions/content/xpinstallConfirm.xul
new file mode 100644
index 000000000..f1c29eb73
--- /dev/null
+++ b/components/extensions/content/xpinstallConfirm.xul
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://mozapps/content/xpinstall/xpinstallConfirm.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/xpinstall/xpinstallConfirm.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://mozapps/locale/xpinstall/xpinstallConfirm.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="xpinstallConfirm" title="&dialog.title;" style="&dialog.style;"
+ windowtype="Addons:Install"
+ onload="XPInstallConfirm.init()"
+ ondialogaccept="return XPInstallConfirm.onOK();"
+ ondialogcancel="return XPInstallConfirm.onCancel();">
+
+ <script src="chrome://mozapps/content/xpinstall/xpinstallConfirm.js" type="application/javascript"/>
+
+ <stringbundle id="xpinstallConfirmStrings"
+ src="chrome://mozapps/locale/xpinstall/xpinstallConfirm.properties"/>
+
+ <vbox flex="1" id="dialogContentBox">
+ <hbox id="xpinstallheader" align="start">
+ <image class="alert-icon"/>
+ <vbox flex="1">
+ <description class="warning">&warningPrimary.label;</description>
+ <description>&warningSecondary.label;</description>
+ </vbox>
+ </hbox>
+ <label id="itemWarningIntro"/>
+ <vbox id="itemList" class="listbox" flex="1" style="overflow: auto;"/>
+ </vbox>
+
+</dialog>
diff --git a/components/extensions/content/xpinstallItem.xml b/components/extensions/content/xpinstallItem.xml
new file mode 100644
index 000000000..5146af84f
--- /dev/null
+++ b/components/extensions/content/xpinstallItem.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+
+<!DOCTYPE bindings SYSTEM "chrome://mozapps/locale/xpinstall/xpinstallConfirm.dtd">
+
+<bindings id="xpinstallItemBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="installitem">
+ <resources>
+ <stylesheet src="chrome://mozapps/skin/xpinstall/xpinstallConfirm.css"/>
+ </resources>
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox align="center" pack="center" class="xpinstallIconContainer">
+ <xul:image class="xpinstallItemIcon" xbl:inherits="src=icon"/>
+ </xul:vbox>
+ <xul:vbox flex="1" pack="center">
+ <xul:hbox class="xpinstallItemNameRow" align="center">
+ <xul:label class="xpinstallItemName" xbl:inherits="value=name" crop="right"/>
+ <xul:label class="xpinstallItemSigned" xbl:inherits="value=cert,signed"/>
+ </xul:hbox>
+ <xul:hbox class="xpinstallItemDetailsRow" align="center">
+ <xul:textbox class="xpinstallItemURL" xbl:inherits="value=url" flex="1" readonly="true" crop="right"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ <implementation>
+ <property name="name" onset="this.setAttribute('name', val); return val;"
+ onget="return this.getAttribute('name');"/>
+ <property name="cert" onset="this.setAttribute('cert', val); return val;"
+ onget="return this.getAttribute('cert');"/>
+ <property name="signed" onset="this.setAttribute('signed', val); return val;"
+ onget="return this.getAttribute('signed');"/>
+ <property name="url" onset="this.setAttribute('url', val); return val;"
+ onget="return this.getAttribute('url');"/>
+ <property name="icon" onset="this.setAttribute('icon', val); return val;"
+ onget="return this.getAttribute('icon');"/>
+ <property name="type" onset="this.setAttribute('type', val); return val;"
+ onget="return this.getAttribute('type');"/>
+ </implementation>
+ </binding>
+
+</bindings>
+
diff --git a/components/extensions/extensions.manifest b/components/extensions/extensions.manifest
new file mode 100644
index 000000000..b56152e10
--- /dev/null
+++ b/components/extensions/extensions.manifest
@@ -0,0 +1,16 @@
+component {4399533d-08d1-458c-a87a-235f74451cfa} addonManager.js
+contract @mozilla.org/addons/integration;1 {4399533d-08d1-458c-a87a-235f74451cfa}
+category update-timer addonManager @mozilla.org/addons/integration;1,getService,addon-background-update-timer,extensions.update.interval,86400
+
+component {7beb3ba8-6ec3-41b4-b67c-da89b8518922} amContentHandler.js
+contract @mozilla.org/uriloader/content-handler;1?type=application/x-xpinstall {7beb3ba8-6ec3-41b4-b67c-da89b8518922}
+
+component {0f38e086-89a3-40a5-8ffc-9b694de1d04a} amWebInstallListener.js
+contract @mozilla.org/addons/web-install-listener;1 {0f38e086-89a3-40a5-8ffc-9b694de1d04a}
+
+component {9df8ef2b-94da-45c9-ab9f-132eb55fddf1} amInstallTrigger.js
+contract @mozilla.org/addons/installtrigger;1 {9df8ef2b-94da-45c9-ab9f-132eb55fddf1}
+category JavaScript-global-property InstallTrigger @mozilla.org/addons/installtrigger;1
+
+category addon-provider-module PluginProvider resource://gre/modules/addons/PluginProvider.jsm
+category addon-provider-module GMPProvider resource://gre/modules/addons/GMPProvider.jsm
diff --git a/components/extensions/jar.mn b/components/extensions/jar.mn
new file mode 100644
index 000000000..878be4df1
--- /dev/null
+++ b/components/extensions/jar.mn
@@ -0,0 +1,37 @@
+# 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/.
+
+toolkit.jar:
+% content mozapps %content/mozapps/
+* content/mozapps/extensions/extensions.xul (content/extensions.xul)
+ content/mozapps/extensions/extensions.css (content/extensions.css)
+* content/mozapps/extensions/extensions.js (content/extensions.js)
+* content/mozapps/extensions/extensions.xml (content/extensions.xml)
+ content/mozapps/extensions/updateinfo.xsl (content/updateinfo.xsl)
+ content/mozapps/extensions/about.xul (content/about.xul)
+ content/mozapps/extensions/about.js (content/about.js)
+ content/mozapps/extensions/list.xul (content/list.xul)
+ content/mozapps/extensions/list.js (content/list.js)
+ content/mozapps/extensions/blocklist.xul (content/blocklist.xul)
+ content/mozapps/extensions/blocklist.js (content/blocklist.js)
+ content/mozapps/extensions/blocklist.css (content/blocklist.css)
+ content/mozapps/extensions/blocklist.xml (content/blocklist.xml)
+ content/mozapps/extensions/selectAddons.xul (content/selectAddons.xul)
+ content/mozapps/extensions/selectAddons.xml (content/selectAddons.xml)
+ content/mozapps/extensions/selectAddons.js (content/selectAddons.js)
+ content/mozapps/extensions/selectAddons.css (content/selectAddons.css)
+ content/mozapps/extensions/update.xul (content/update.xul)
+ content/mozapps/extensions/update.js (content/update.js)
+ content/mozapps/extensions/eula.xul (content/eula.xul)
+ content/mozapps/extensions/eula.js (content/eula.js)
+ content/mozapps/extensions/newaddon.xul (content/newaddon.xul)
+ content/mozapps/extensions/newaddon.js (content/newaddon.js)
+ content/mozapps/extensions/setting.xml (content/setting.xml)
+ content/mozapps/extensions/pluginPrefs.xul (content/pluginPrefs.xul)
+ content/mozapps/extensions/gmpPrefs.xul (content/gmpPrefs.xul)
+ content/mozapps/extensions/OpenH264-license.txt (content/OpenH264-license.txt)
+ content/mozapps/xpinstall/xpinstallConfirm.xul (content/xpinstallConfirm.xul)
+ content/mozapps/xpinstall/xpinstallConfirm.js (content/xpinstallConfirm.js)
+ content/mozapps/xpinstall/xpinstallConfirm.css (content/xpinstallConfirm.css)
+ content/mozapps/xpinstall/xpinstallItem.xml (content/xpinstallItem.xml)
diff --git a/components/extensions/locale/about.dtd b/components/extensions/locale/about.dtd
new file mode 100644
index 000000000..4f9098966
--- /dev/null
+++ b/components/extensions/locale/about.dtd
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+
+<!ENTITY creator.label "Created By:">
+<!ENTITY developers.label "Developers:">
+<!ENTITY translators.label "Translators:">
+<!ENTITY contributors.label "Contributors:">
+<!ENTITY homepage.label "Visit Home Page">
diff --git a/components/extensions/locale/blocklist.dtd b/components/extensions/locale/blocklist.dtd
new file mode 100644
index 000000000..a9490db5e
--- /dev/null
+++ b/components/extensions/locale/blocklist.dtd
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<!ENTITY blocklist.title "Add-ons may be causing problems">
+<!ENTITY blocklist.style "width: 45em; height: 30em">
+<!ENTITY blocklist.summary "&brandShortName; has determined that the following add-ons are known to cause issues:">
+<!ENTITY blocklist.softblocked "It is highly recommended, but not required, that you restart with these add-ons disabled.">
+<!ENTITY blocklist.hardblocked "These add-ons have a high risk of causing stability or security problems and have been blocked, but a restart is required to disable them completely.">
+<!ENTITY blocklist.softandhard "Some listed add-ons have a high risk of causing stability or security problems and have been blocked. For the others it is highly recommended, but not required, that you restart with them disabled.">
+<!ENTITY blocklist.moreinfo "More information">
+
+<!ENTITY blocklist.accept.label "Restart &brandShortName;">
+<!ENTITY blocklist.accept.accesskey "R">
+
+<!ENTITY blocklist.blocked.label "Blocked">
+<!ENTITY blocklist.checkbox.label "Disable">
diff --git a/components/extensions/locale/extensions.dtd b/components/extensions/locale/extensions.dtd
new file mode 100644
index 000000000..098896d99
--- /dev/null
+++ b/components/extensions/locale/extensions.dtd
@@ -0,0 +1,230 @@
+<!-- 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/. -->
+
+<!ENTITY addons.windowTitle "Add-ons Manager">
+
+<!ENTITY search.placeholder "Search all add-ons">
+<!ENTITY search.buttonlabel "Search">
+<!-- LOCALIZATION NOTE (search.commandKey):
+ The search command key should match findOnCmd.commandkey from browser.dtd -->
+<!ENTITY search.commandkey "f">
+
+<!ENTITY loading.label "Loading…">
+<!ENTITY listEmpty.installed.label "You don't have any add-ons of this type installed">
+<!ENTITY listEmpty.availableUpdates.label "No updates found">
+<!ENTITY listEmpty.recentUpdates.label "You haven't recently updated any add-ons">
+<!ENTITY listEmpty.findUpdates.label "Check For Updates">
+<!ENTITY listEmpty.search.label "Could not find any matching add-ons">
+<!ENTITY listEmpty.button.label "Learn more about add-ons">
+<!ENTITY installAddonFromFile.label "Install Add-on From File…">
+<!ENTITY installAddonFromFile.accesskey "I">
+<!ENTITY toolsMenu.tooltip "Tools for all add-ons">
+
+<!ENTITY cmd.back.tooltip "Go back one page">
+<!ENTITY cmd.forward.tooltip "Go forward one page">
+
+<!-- global warnings -->
+<!ENTITY warning.safemode.label "All add-ons have been disabled by safe mode.">
+<!ENTITY warning.checkcompatibility.label "Add-on compatibility checking is disabled. You may have incompatible add-ons.">
+<!ENTITY warning.checkcompatibility.enable.label "Enable">
+<!ENTITY warning.checkcompatibility.enable.tooltip "Enable add-on compatibility checking">
+<!ENTITY warning.updatesecurity.label "Add-on update security checking is disabled. You may be compromised by updates.">
+<!ENTITY warning.updatesecurity.enable.label "Enable">
+<!ENTITY warning.updatesecurity.enable.tooltip "Enable add-on update security checking">
+
+<!-- categories / views -->
+<!ENTITY view.search.label "Search">
+<!ENTITY view.discover.label "Get Add-ons">
+<!ENTITY view.recentUpdates.label "Recent Updates">
+<!ENTITY view.availableUpdates.label "Available Updates">
+
+<!-- addon updates -->
+<!ENTITY updates.checkForUpdates.label "Check for Updates">
+<!ENTITY updates.checkForUpdates.accesskey "C">
+<!ENTITY updates.viewUpdates.label "View Recent Updates">
+<!ENTITY updates.viewUpdates.accesskey "V">
+<!-- LOCALIZATION NOTE (updates.updateAddonsAutomatically.label): This menu item
+ is a checkbox that toggles the default global behavior for add-on update
+ checking. -->
+<!ENTITY updates.updateAddonsAutomatically.label "Update Add-ons Automatically">
+<!ENTITY updates.updateAddonsAutomatically.accesskey "A">
+<!-- LOCALIZATION NOTE (updates.resetUpdatesToAutomatic.label, updates.resetUpdatesToManual.label):
+ Specific addons can have custom update checking behaviors ("Manually",
+ "Automatically", "Use default global behavior"). These menu items reset the
+ update checking behavior for all add-ons to the default global behavior
+ (which itself is either "Automatically" or "Manually", controlled by the
+ updates.updateAddonsAutomatically.label menu item). -->
+<!ENTITY updates.resetUpdatesToAutomatic.label "Reset All Add-ons to Update Automatically">
+<!ENTITY updates.resetUpdatesToAutomatic.accesskey "R">
+<!ENTITY updates.resetUpdatesToManual.label "Reset All Add-ons to Update Manually">
+<!ENTITY updates.resetUpdatesToManual.accesskey "R">
+<!ENTITY updates.updating.label "Updating add-ons">
+<!ENTITY updates.installed.label "Your add-ons have been updated.">
+<!ENTITY updates.downloaded.label "Your add-on updates have been downloaded.">
+<!ENTITY updates.restart.label "Restart now to complete installation">
+<!ENTITY updates.noneFound.label "No updates found">
+<!ENTITY updates.manualUpdatesFound.label "View Available Updates">
+<!ENTITY updates.updateSelected.label "Install Updates">
+<!ENTITY updates.updateSelected.tooltip "Install available updates in this list">
+
+<!-- addon actions -->
+<!ENTITY cmd.showDetails.label "Show More Information">
+<!ENTITY cmd.showDetails.accesskey "S">
+<!ENTITY cmd.findUpdates.label "Find Updates">
+<!ENTITY cmd.findUpdates.accesskey "F">
+<!ENTITY cmd.preferencesWin.label "Options">
+<!ENTITY cmd.preferencesWin.accesskey "O">
+<!ENTITY cmd.preferencesUnix.label "Preferences">
+<!ENTITY cmd.preferencesUnix.accesskey "P">
+<!ENTITY cmd.about.label "About">
+<!ENTITY cmd.about.accesskey "A">
+
+<!ENTITY cmd.enableAddon.label "Enable">
+<!ENTITY cmd.enableAddon.accesskey "E">
+<!ENTITY cmd.disableAddon.label "Disable">
+<!ENTITY cmd.disableAddon.accesskey "D">
+<!ENTITY cmd.enableTheme.label "Wear Theme">
+<!ENTITY cmd.enableTheme.accesskey "W">
+<!ENTITY cmd.disableTheme.label "Stop Wearing Theme">
+<!ENTITY cmd.disableTheme.accesskey "W">
+<!ENTITY cmd.askToActivate.label "Ask to Activate">
+<!ENTITY cmd.askToActivate.tooltip "Ask to use this add-on each time">
+<!ENTITY cmd.alwaysActivate.label "Always Activate">
+<!ENTITY cmd.alwaysActivate.tooltip "Always use this add-on">
+<!ENTITY cmd.neverActivate.label "Never Activate">
+<!ENTITY cmd.neverActivate.tooltip "Never use this add-on">
+<!ENTITY cmd.stateMenu.tooltip "Change when this add-on runs">
+<!ENTITY cmd.installAddon.label "Install">
+<!ENTITY cmd.installAddon.accesskey "I">
+<!ENTITY cmd.uninstallAddon.label "Remove">
+<!ENTITY cmd.uninstallAddon.accesskey "R">
+<!ENTITY cmd.debugAddon.label "Debug">
+<!ENTITY cmd.showPreferencesWin.label "Options">
+<!ENTITY cmd.showPreferencesWin.tooltip "Change this add-on's options">
+<!ENTITY cmd.showPreferencesUnix.label "Preferences">
+<!ENTITY cmd.showPreferencesUnix.tooltip "Change this add-on's preferences">
+<!ENTITY cmd.contribute.label "Contribute">
+<!ENTITY cmd.contribute.accesskey "C">
+<!ENTITY cmd.contribute.tooltip "Contribute to the development of this add-on">
+
+<!ENTITY cmd.showReleaseNotes.label "Show Release Notes">
+<!ENTITY cmd.showReleaseNotes.tooltip "Show the release notes for this update">
+<!ENTITY cmd.hideReleaseNotes.label "Hide Release Notes">
+<!ENTITY cmd.hideReleaseNotes.tooltip "Hide the release notes for this update">
+
+<!-- discovery view -->
+<!-- LOCALIZATION NOTE (discover.title,discover.description,discover.footer):
+ Displayed in the center of the Get Add-ons view, see bug 601143 for mockups. -->
+<!ENTITY discover.title "What are Add-ons?">
+<!ENTITY discover.description2 "Add-ons are applications that let you personalize &brandShortName; with
+ extra functionality or style. Try a time-saving sidebar, a weather notifier, or a themed look to make &brandShortName;
+ your own.">
+<!ENTITY discover.footer "When you're connected to the internet, this pane will feature
+ some of the best and most popular add-ons for you to try out.">
+
+<!-- detail view -->
+<!ENTITY detail.version.label "Version">
+<!ENTITY detail.lastupdated.label "Last Updated">
+<!ENTITY detail.creator.label "Developer">
+<!ENTITY detail.homepage.label "Homepage">
+<!ENTITY detail.numberOfDownloads.label "Downloads">
+
+<!ENTITY detail.contributions.description "The developer of this add-on asks that you help support its continued development by making a small contribution.">
+
+<!ENTITY detail.updateType "Automatic Updates">
+<!ENTITY detail.updateDefault.label "Default">
+<!ENTITY detail.updateDefault.tooltip "Automatically install updates only if that's the default">
+<!ENTITY detail.updateAutomatic.label "On">
+<!ENTITY detail.updateAutomatic.tooltip "Automatically install updates">
+<!ENTITY detail.updateManual.label "Off">
+<!ENTITY detail.updateManual.tooltip "Don't automatically install updates">
+<!ENTITY detail.home "Homepage">
+<!ENTITY detail.repository "Add-on Profile">
+<!ENTITY detail.size "Size">
+
+<!ENTITY detail.checkForUpdates.label "Check for Updates">
+<!ENTITY detail.checkForUpdates.accesskey "F">
+<!ENTITY detail.checkForUpdates.tooltip "Check for updates for this add-on">
+<!ENTITY detail.showPreferencesWin.label "Options">
+<!ENTITY detail.showPreferencesWin.accesskey "O">
+<!ENTITY detail.showPreferencesWin.tooltip "Change this add-on's options">
+<!ENTITY detail.showPreferencesUnix.label "Preferences">
+<!ENTITY detail.showPreferencesUnix.accesskey "P">
+<!ENTITY detail.showPreferencesUnix.tooltip "Change this add-on's preferences">
+
+
+<!-- ratings -->
+<!ENTITY rating2.label "Rating">
+
+<!-- download/install progress -->
+<!ENTITY progress.pause.tooltip "Pause">
+<!ENTITY progress.cancel.tooltip "Cancel">
+
+
+<!-- list sorting -->
+<!ENTITY sort.name.label "Name">
+<!ENTITY sort.name.tooltip "Sort by name">
+<!ENTITY sort.dateUpdated.label "Last Updated">
+<!ENTITY sort.dateUpdated.tooltip "Sort by date updated">
+<!ENTITY sort.relevance.label "Best match">
+<!ENTITY sort.relevance.tooltip "Sort by relevance">
+<!ENTITY sort.price.label "Price">
+<!ENTITY sort.price.tooltip "Sort by price">
+
+<!ENTITY search.filter2.label "Search:">
+<!ENTITY search.filter2.installed.label "My Add-ons">
+<!ENTITY search.filter2.installed.tooltip "Show installed add-ons">
+<!ENTITY search.filter2.available.label "Available Add-ons">
+<!ENTITY search.filter2.available.tooltip "Show add-ons available to install">
+
+<!ENTITY addon.homepage "Homepage">
+<!ENTITY addon.details.label "More">
+<!ENTITY addon.details.tooltip "Show more details about this add-on">
+<!ENTITY addon.unknownDate "Unknown">
+<!-- LOCALIZATION NOTE (addon.disabled.postfix): This is used in a normal list
+ to signify that an add-on is disabled, in the form
+ "<Addon name> <1.0> (disabled)" -->
+<!ENTITY addon.disabled.postfix "(disabled)">
+<!-- LOCALIZATION NOTE (addon.update.postfix): This is used in the available
+ updates list to signify that an item is an update, in the form
+ "<Addon name> <1.1> Update". It is fine to use constructs like brackets if
+ necessary -->
+<!ENTITY addon.update.postfix "Update">
+<!ENTITY addon.undoAction.label "Undo">
+<!ENTITY addon.undoAction.tooltip "Undo this action">
+<!ENTITY addon.undoRemove.label "Undo">
+<!ENTITY addon.undoRemove.tooltip "Keep this add-on installed">
+<!ENTITY addon.restartNow.label "Restart now">
+<!ENTITY addon.install.label "Install">
+<!ENTITY addon.install.tooltip "Install this add-on">
+<!ENTITY addon.updateNow.label "Update Now">
+<!ENTITY addon.updateNow.tooltip "Install the update for this add-on">
+<!ENTITY addon.includeUpdate.label "Include in Update">
+<!ENTITY addon.updateAvailable.label "An update is available">
+<!ENTITY addon.checkingForUpdates.label "Checking for updates…">
+<!ENTITY addon.releaseNotes.label "Release Notes:">
+<!ENTITY addon.loadingReleaseNotes.label "Loading…">
+<!ENTITY addon.errorLoadingReleaseNotes.label "Sorry, but there was an error loading the release notes.">
+
+<!ENTITY addon.createdBy.label "By ">
+
+<!ENTITY eula.title "End-User License Agreement">
+<!ENTITY eula.width "560px">
+<!ENTITY eula.height "400px">
+<!ENTITY eula.accept "Accept and Install…">
+
+<!ENTITY settings.path.button.label "Browse…">
+
+<!-- LOCALIZATION NOTE (experiment.info.label): The strings related to
+ experiments are present on the "Experiments" tab of the add-ons manager.
+ This tab won't be displayed unless an Experiment add-on is installed.
+ Install https://people.mozilla.org/~gszorc/dummy-experiment-addon.xpi
+ to cause this tab to appear. -->
+<!ENTITY experiment.info.label "What's this? Telemetry may install and run experiments from time to time.">
+<!ENTITY experiment.info.learnmore "Learn More">
+<!ENTITY experiment.info.learnmore.accesskey "L">
+<!ENTITY experiment.info.changetelemetry "Telemetry Settings">
+<!ENTITY experiment.info.changetelemetry.accesskey "T">
+
+<!ENTITY setting.learnmore "Learn More…">
diff --git a/components/extensions/locale/extensions.properties b/components/extensions/locale/extensions.properties
new file mode 100644
index 000000000..370198f56
--- /dev/null
+++ b/components/extensions/locale/extensions.properties
@@ -0,0 +1,177 @@
+# 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/.
+
+#LOCALIZATION NOTE (aboutWindowTitle) %S is the addon name
+aboutWindowTitle=About %S
+aboutWindowCloseButton=Close
+#LOCALIZATION NOTE (aboutWindowVersionString) %S is the addon version
+aboutWindowVersionString=version %S
+#LOCALIZATION NOTE (aboutAddon) %S is the addon name
+aboutAddon=About %S
+
+#LOCALIZATION NOTE (uninstallNotice) %S is the add-on name
+uninstallNotice=%S has been removed.
+
+#LOCALIZATION NOTE (numReviews): Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of reviews
+numReviews=#1 review;#1 reviews
+
+#LOCALIZATION NOTE (dateUpdated) %S is the date the addon was last updated
+dateUpdated=Updated %S
+
+#LOCALIZATION NOTE (notification.incompatible) %1$S is the add-on name, %2$S is brand name, %3$S is application version
+notification.incompatible=%1$S is incompatible with %2$S %3$S.
+notification.jetsdk=This is a Jetpack/SDK extension which are not supported in %1$S %2$S.
+#LOCALIZATION NOTE (notification.blocked) %1$S is the add-on name
+notification.blocked=%1$S has been disabled due to security or stability issues.
+notification.blocked.link=More Information
+#LOCALIZATION NOTE (notification.softblocked) %1$S is the add-on name
+notification.softblocked=%1$S is known to cause issues.
+notification.softblocked.link=More Information
+#LOCALIZATION NOTE (notification.outdated) %1$S is the add-on name
+notification.outdated=An important update is available for %1$S.
+notification.outdated.link=Update Now
+#LOCALIZATION NOTE (notification.vulnerableUpdatable) %1$S is the add-on name
+notification.vulnerableUpdatable=%1$S is known to be vulnerable and should be updated.
+notification.vulnerableUpdatable.link=Update Now
+#LOCALIZATION NOTE (notification.vulnerableNoUpdate) %1$S is the add-on name
+notification.vulnerableNoUpdate=%1$S is known to be vulnerable. Use with caution.
+notification.vulnerableNoUpdate.link=More Information
+#LOCALIZATION NOTE (notification.enable) %1$S is the add-on name, %2$S is brand name
+notification.enable=%1$S will be enabled after you restart %2$S.
+#LOCALIZATION NOTE (notification.disable) %1$S is the add-on name, %2$S is brand name
+notification.disable=%1$S will be disabled after you restart %2$S.
+#LOCALIZATION NOTE (notification.install) %1$S is the add-on name, %2$S is brand name
+notification.install=%1$S will be installed after you restart %2$S.
+#LOCALIZATION NOTE (notification.uninstall) %1$S is the add-on name, %2$S is brand name
+notification.uninstall=%1$S will be uninstalled after you restart %2$S.
+#LOCALIZATION NOTE (notification.upgrade) %1$S is the add-on name, %2$S is brand name
+notification.upgrade=%1$S will be updated after you restart %2$S.
+#LOCALIZATION NOTE (notification.downloadError) %1$S is the add-on name.
+notification.downloadError=There was an error downloading %1$S.
+notification.downloadError.retry=Try again
+notification.downloadError.retry.tooltip=Try downloading this add-on again
+#LOCALIZATION NOTE (notification.installError) %1$S is the add-on name.
+notification.installError=There was an error installing %1$S.
+notification.installError.retry=Try again
+notification.installError.retry.tooltip=Try downloading and installing this add-on again
+#LOCALIZATION NOTE (notification.gmpPending) %1$S is the add-on name.
+notification.gmpPending=%1$S will be installed shortly.
+
+#LOCALIZATION NOTE (contributionAmount2) %S is the currency amount recommended for contributions
+contributionAmount2=Suggested Contribution: %S
+
+installDownloading=Downloading
+installDownloaded=Downloaded
+installDownloadFailed=Error downloading
+installVerifying=Verifying
+installInstalling=Installing
+installEnablePending=Restart to enable
+installDisablePending=Restart to disable
+installFailed=Error installing
+installCancelled=Install cancelled
+
+#LOCALIZATION NOTE (details.notification.incompatible) %1$S is the add-on name, %2$S is brand name, %3$S is application version
+details.notification.incompatible=%1$S is incompatible with %2$S %3$S.
+#LOCALIZATION NOTE (details.notification.blocked) %1$S is the add-on name
+details.notification.blocked=%1$S has been disabled due to security or stability issues.
+details.notification.blocked.link=More Information
+#LOCALIZATION NOTE (details.notification.softblocked) %1$S is the add-on name
+details.notification.softblocked=%1$S is known to cause issues.
+details.notification.softblocked.link=More Information
+#LOCALIZATION NOTE (details.notification.outdated) %1$S is the add-on name
+details.notification.outdated=An important update is available for %1$S.
+details.notification.outdated.link=Update Now
+#LOCALIZATION NOTE (details.notification.vulnerableUpdatable) %1$S is the add-on name
+details.notification.vulnerableUpdatable=%1$S is known to be vulnerable and should be updated.
+details.notification.vulnerableUpdatable.link=Update Now
+#LOCALIZATION NOTE (details.notification.vulnerableNoUpdate) %1$S is the add-on name
+details.notification.vulnerableNoUpdate=%1$S is known to be vulnerable. Use with caution.
+details.notification.vulnerableNoUpdate.link=More Information
+#LOCALIZATION NOTE (details.notification.enable) %1$S is the add-on name, %2$S is brand name
+details.notification.enable=%1$S will be enabled after you restart %2$S.
+#LOCALIZATION NOTE (details.notification.disable) %1$S is the add-on name, %2$S is brand name
+details.notification.disable=%1$S will be disabled after you restart %2$S.
+#LOCALIZATION NOTE (details.notification.install) %1$S is the add-on name, %2$S is brand name
+details.notification.install=%1$S will be installed after you restart %2$S.
+#LOCALIZATION NOTE (details.notification.uninstall) %1$S is the add-on name, %2$S is brand name
+details.notification.uninstall=%1$S will be uninstalled after you restart %2$S.
+#LOCALIZATION NOTE (details.notification.upgrade) %1$S is the add-on name, %2$S is brand name
+details.notification.upgrade=%1$S will be updated after you restart %2$S.
+#LOCALIZATION NOTE (details.notification.gmpPending) %1$S is the add-on name
+details.notification.gmpPending=%1$S will be installed shortly.
+
+# LOCALIZATION NOTE (details.experiment.time.daysRemaining):
+# Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of days from now that the experiment will remain active (detail view).
+details.experiment.time.daysRemaining=#1 day remaining;#1 days remaining
+#LOCALIZATION NOTE (details.experiment.time.endsToday) The experiment will end in less than a day (detail view).
+details.experiment.time.endsToday=Less than a day remaining
+# LOCALIZATION NOTE (details.experiment.time.daysPassed):
+# Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of days since the experiment ran (detail view).
+details.experiment.time.daysPassed=#1 day ago;#1 days ago
+#LOCALIZATION NOTE (details.experiment.time.endedToday) The experiment ended less than a day ago (detail view).
+details.experiment.time.endedToday=Less than a day ago
+#LOCALIZATION NOTE (details.experiment.state.active) This experiment is active (detail view).
+details.experiment.state.active=Active
+#LOCALIZATION NOTE (details.experiment.state.complete) This experiment is complete (it was previously active) (detail view).
+details.experiment.state.complete=Complete
+
+# LOCALIZATION NOTE (experiment.time.daysRemaining):
+# Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of days from now that the experiment will remain active (list view item).
+experiment.time.daysRemaining=#1 day remaining;#1 days remaining
+#LOCALIZATION NOTE (experiment.time.endsToday) The experiment will end in less than a day (list view item).
+experiment.time.endsToday=Less than a day remaining
+# LOCALIZATION NOTE (experiment.time.daysPassed):
+# Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of days since the experiment ran (list view item).
+experiment.time.daysPassed=#1 day ago;#1 days ago
+#LOCALIZATION NOTE (experiment.time.endedToday) The experiment ended less than a day ago (list view item).
+experiment.time.endedToday=Less than a day ago
+#LOCALIZATION NOTE (experiment.state.active) This experiment is active (list view item).
+experiment.state.active=Active
+#LOCALIZATION NOTE (experiment.state.complete) This experiment is complete (it was previously active) (list view item).
+experiment.state.complete=Complete
+
+installFromFile.dialogTitle=Select add-on to install
+installFromFile.filterName=Add-ons
+
+uninstallAddonTooltip=Uninstall this add-on
+uninstallAddonRestartRequiredTooltip=Uninstall this add-on (restart required)
+enableAddonTooltip=Enable this add-on
+enableAddonRestartRequiredTooltip=Enable this add-on (restart required)
+disableAddonTooltip=Disable this add-on
+disableAddonRestartRequiredTooltip=Disable this add-on (restart required)
+
+#LOCALIZATION NOTE (showAllSearchResults): Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the total number of search results
+showAllSearchResults=See one result;See all #1 results
+
+#LOCALIZATION NOTE (addon.purchase.label) displayed on a button in the list
+# view, %S is the price of the add-on including currency symbol
+addon.purchase.label=Purchase for %S…
+addon.purchase.tooltip=Visit the add-ons gallery to purchase this add-on
+#LOCALIZATION NOTE (cmd.purchaseAddon.label) displayed on a button in the detail
+# view, %S is the price of the add-on including currency symbol
+cmd.purchaseAddon.label=Purchase for %S…
+cmd.purchaseAddon.accesskey=u
+
+#LOCALIZATION NOTE (eulaHeader) %S is name of the add-on asking the user to agree to the EULA
+eulaHeader=%S requires that you accept the following End User License Agreement before installation can proceed:
+
+type.extension.name=Extensions
+type.theme.name=Themes
+type.locale.name=Languages
+type.plugin.name=Plugins
+type.dictionary.name=Dictionaries
+type.service.name=Services
+type.experiment.name=Experiments
diff --git a/components/extensions/locale/newaddon.dtd b/components/extensions/locale/newaddon.dtd
new file mode 100644
index 000000000..1307cebb9
--- /dev/null
+++ b/components/extensions/locale/newaddon.dtd
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+
+<!ENTITY title "Install Add-on">
+<!ENTITY intro "Another program on your computer would like to modify
+ &brandShortName; with the following add-on:">
+<!ENTITY warning "Install add-ons only from authors whom you trust.">
+<!ENTITY allow "Allow this installation">
+<!ENTITY later "You can always change your mind at any time by going
+ to the Add-ons Manager.">
+<!ENTITY continue "Continue">
+<!ENTITY restartMessage "You must restart &brandShortName; to finish installing this add-on.">
+<!ENTITY restartButton "Restart &brandShortName;">
+<!ENTITY cancelButton "Cancel">
diff --git a/components/extensions/locale/newaddon.properties b/components/extensions/locale/newaddon.properties
new file mode 100644
index 000000000..bd5997a26
--- /dev/null
+++ b/components/extensions/locale/newaddon.properties
@@ -0,0 +1,10 @@
+# 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/.
+
+#LOCALIZATION NOTE (name) %1$S is the add-on name, %2$S is the add-on version
+name=%1$S %2$S
+#LOCALIZATION NOTE (author) %S is the author of the add-on
+author=By %S
+#LOCALIZATION NOTE (location) %S is the path the add-on is installed in
+location=Location: %S
diff --git a/components/extensions/locale/selectAddons.dtd b/components/extensions/locale/selectAddons.dtd
new file mode 100644
index 000000000..2f6f1cd57
--- /dev/null
+++ b/components/extensions/locale/selectAddons.dtd
@@ -0,0 +1,49 @@
+<!-- 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/. -->
+
+<!ENTITY upgrade.style "width: 93ch; height: 448px;">
+
+<!ENTITY checking.heading "Checking Your Add-ons">
+<!ENTITY checking.progress.label "Checking your add-ons for compatibility with this version of &brandShortName;.">
+
+<!ENTITY select.heading "Select Your Add-ons">
+<!-- LOCALIZATION NOTE (select.description): The term used for "third parties"
+ here should match the string source.other in selectAddons.properties. -->
+<!ENTITY select.description "Make &brandShortName; even faster by disabling add-ons you no longer use. Add-ons already installed by third parties will be disabled automatically unless you select them below.">
+<!ENTITY select.keep "Keep">
+<!-- LOCALIZATION NOTE (select.keep.style): Should be a width wide enough for
+ the string in select.keep above. -->
+<!ENTITY select.keep.style "width: 6ch;">
+<!ENTITY select.action "Action">
+<!-- LOCALIZATION NOTE (select.action.style): Should be a width wide enough for
+ the action strings in selectAddons.properties or brandShortName. -->
+<!ENTITY select.action.style "width: 35ch;">
+<!ENTITY select.source "Installed By">
+<!ENTITY select.name "Name">
+<!-- LOCALIZATION NOTE (select.name.style): Should be a width small enough so
+ the source column still has enough room for the source strings in
+ selectAddons.properties. -->
+<!ENTITY select.name.style "width: 33ch;">
+
+<!ENTITY confirm.heading "Select Your Add-ons">
+<!-- LOCALIZATION NOTE (confirm.description): The term used for "third parties"
+ here should match the string source.other in selectAddons.properties. -->
+<!ENTITY confirm.description "Make &brandShortName; even faster by disabling add-ons you no longer use. Add-ons already installed by third parties will be disabled automatically unless you select them below.">
+
+<!ENTITY action.disable.heading "The following add-ons will be disabled:">
+<!ENTITY action.incompatible.heading "The following add-ons are disabled, but will be enabled as soon as they are compatible:">
+<!ENTITY action.update.heading "The following add-ons will be updated:">
+<!ENTITY action.enable.heading "The following add-ons will be enabled:">
+
+<!ENTITY update.heading "Updating Your Add-ons">
+<!ENTITY update.progress.label "Downloading and installing updates for your selected add-ons.">
+
+<!ENTITY errors.heading "&brandShortName; could not update some of your add-ons.">
+<!ENTITY errors.description "Installing updates for some of your add-ons failed. &brandShortName; will automatically try to update them again later.">
+
+<!ENTITY footer.label "You can always change your add-ons by going to the Add-ons Manager.">
+<!ENTITY cancel.label "Cancel">
+<!ENTITY back.label "Back">
+<!ENTITY next.label "Next">
+<!ENTITY done.label "Done">
diff --git a/components/extensions/locale/selectAddons.properties b/components/extensions/locale/selectAddons.properties
new file mode 100644
index 000000000..2824758d6
--- /dev/null
+++ b/components/extensions/locale/selectAddons.properties
@@ -0,0 +1,21 @@
+# 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/.
+
+#LOCALIZATION NOTE (source.profile) add-ons installed by the user, this may be
+# translated as "You" or "User" depending on the locale
+source.profile=You
+#LOCALIZATION NOTE (source.bundled) add-ons shipped with the application, and thus
+# treated as installed by the user. This may be
+# translated as "You" or "User" depending on the locale
+source.bundled=You (Bundled)
+#LOCALIZATION NOTE (source.other) add-ons installed by other applications
+# installed on the computer
+source.other=Third Party
+
+action.enabled=Will be enabled
+action.disabled=Will be disabled
+action.autoupdate=Will be updated to be compatible
+action.incompatible=Will be enabled when compatible
+action.neededupdate=Update to make compatible
+action.unneededupdate=Optional update
diff --git a/components/extensions/locale/update.dtd b/components/extensions/locale/update.dtd
new file mode 100644
index 000000000..6c820e088
--- /dev/null
+++ b/components/extensions/locale/update.dtd
@@ -0,0 +1,65 @@
+<!-- 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/. -->
+
+<!ENTITY updateWizard.title "&brandShortName; Update">
+
+<!ENTITY offline.title "&brandShortName; is working offline">
+<!ENTITY offline.description "&brandShortName; needs to go online in order to see if updates
+ are available for your add-ons to make them compatible with this
+ version.">
+<!ENTITY offline.toggleOffline.label "Go online now.">
+<!ENTITY offline.toggleOffline.accesskey "G">
+
+<!ENTITY mismatch.win.title "Incompatible Add-ons">
+<!ENTITY mismatch.top.label "The following add-ons are not compatible with this version of
+ &brandShortName; and have been disabled:">
+<!ENTITY mismatch.bottom.label "&brandShortName; can check if there are compatible versions
+ of these add-ons available.">
+
+<!ENTITY checking.wizard.title "Checking for Compatible Add-ons">
+<!ENTITY checking.top.label "Checking your incompatible add-ons for updates…">
+<!ENTITY checking.status "This may take a few minutes…">
+
+<!ENTITY found.wizard.title "Found Compatible Add-ons">
+<!ENTITY found.top.label "Select the add-ons you would like to install:">
+<!ENTITY found.disabledXPinstall.label "These updates can't be installed because software installation is currently
+ disabled. You can change this setting below.">
+<!ENTITY found.enableXPInstall.label "Allow websites to install software">
+<!ENTITY found.enableXPInstall.accesskey "A">
+
+<!ENTITY installing.wizard.title "Installing Compatible Add-ons">
+<!ENTITY installing.top.label "Downloading and installing updates to your add-ons…">
+
+<!ENTITY noupdates.wizard.title "No Compatible Add-ons Found">
+<!ENTITY noupdates.intro.desc "&brandShortName; was unable to find updates to your
+ incompatible add-ons.">
+<!ENTITY noupdates.error.desc "Some problems were encountered when trying to find updates.">
+<!ENTITY noupdates.checkEnabled.desc "&brandShortName; will check periodically and inform you
+ when compatible updates for these add-ons are found.">
+
+<!ENTITY finished.wizard.title "Compatible Add-ons Installed">
+<!ENTITY finished.top.label "&brandShortName; has installed the updates to your add-ons.">
+<!ENTITY finished.checkDisabled.desc "&brandShortName; can check periodically and inform you
+ when updates for add-ons are found.">
+<!ENTITY finished.checkEnabled.desc "&brandShortName; will check periodically and inform you
+ when updates for add-ons are found.">
+
+<!ENTITY adminDisabled.wizard.title "Unable to Check for Updates">
+<!ENTITY adminDisabled.warning.label "It is not possible to check for updates to incompatible add-ons
+ because software installation for &brandShortName; has been disabled.
+ Please contact your System Administrator for assistance.">
+
+<!ENTITY versioninfo.wizard.title "Checking Compatibility of Add-ons">
+<!ENTITY versioninfo.top.label "Checking your add-ons for compatibility with this
+ version of &brandShortName;.">
+<!ENTITY versioninfo.waiting "This may take a few minutes…">
+
+<!ENTITY installerrors.wizard.title "Problems Installing Updates">
+<!ENTITY installerrors.intro.label "&brandShortName; encountered problems when updating
+ some of your add-ons.">
+
+<!-- general strings used by several of the finish pages -->
+<!ENTITY clickFinish.label "Click Finish to continue starting &brandShortName;.">
+<!ENTITY clickFinish.labelMac "Click Done to continue starting &brandShortName;.">
+<!ENTITY enableChecking.label "Allow &brandShortName; to check for updates.">
diff --git a/components/extensions/locale/update.properties b/components/extensions/locale/update.properties
new file mode 100644
index 000000000..7cf79f9c1
--- /dev/null
+++ b/components/extensions/locale/update.properties
@@ -0,0 +1,21 @@
+# 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/.
+
+mismatchCheckNow=Check Now
+mismatchCheckNowAccesskey=C
+mismatchDontCheck=Don't Check
+mismatchDontCheckAccesskey=D
+installButtonText=Install Now
+installButtonTextAccesskey=I
+nextButtonText=Next >
+nextButtonTextAccesskey=N
+cancelButtonText=Cancel
+cancelButtonTextAccesskey=C
+statusPrefix=Finished checking %S
+downloadingPrefix=Downloading: %S
+installingPrefix=Installing: %S
+closeButton=Close
+installErrors=%S was unable to install updates for the following add-ons:
+checkingErrors=%S was unable to check for updates for the following add-ons:
+installErrorItemFormat=%S (%S)
diff --git a/components/extensions/locale/xpinstallConfirm.dtd b/components/extensions/locale/xpinstallConfirm.dtd
new file mode 100644
index 000000000..6a7d17a16
--- /dev/null
+++ b/components/extensions/locale/xpinstallConfirm.dtd
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<!-- extracted from institems.xul -->
+
+<!ENTITY dialog.title "Software Installation">
+<!ENTITY dialog.style "width: 45em">
+<!ENTITY warningPrimary.label "Install add-ons only from authors whom you trust.">
+<!ENTITY warningSecondary.label "Malicious software can damage your computer or violate your privacy.">
+
+<!ENTITY from.label "from:">
+
diff --git a/components/extensions/locale/xpinstallConfirm.properties b/components/extensions/locale/xpinstallConfirm.properties
new file mode 100644
index 000000000..b538e9e90
--- /dev/null
+++ b/components/extensions/locale/xpinstallConfirm.properties
@@ -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/.
+
+unverified=(Author not verified)
+signed=(%S)
+
+itemWarnIntroMultiple=You have asked to install the following %S items:
+itemWarnIntroSingle=You have asked to install the following item:
+installButtonDisabledLabel=Install (%S)
+installButtonLabel=Install Now
+
+installComplete=Software Installation is complete. You will have to restart %S for changes to take effect.
+installCompleteTitle=Installation Complete
+
+error-203=Error Installing Item
diff --git a/components/extensions/moz.build b/components/extensions/moz.build
new file mode 100644
index 000000000..6b1e2fe93
--- /dev/null
+++ b/components/extensions/moz.build
@@ -0,0 +1,66 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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 is used in multiple places, so is defined here to avoid it getting
+# out of sync.
+DEFINES['MOZ_EXTENSIONS_DB_SCHEMA'] = 16
+
+# Additional debugging info is exposed in debug builds
+if CONFIG['MOZ_EM_DEBUG']:
+ DEFINES['MOZ_EM_DEBUG'] = 1
+
+XPIDL_SOURCES += [
+ 'public/amIAddonManager.idl',
+ 'public/amIAddonPathService.idl',
+ 'public/amIWebInstaller.idl',
+ 'public/amIWebInstallListener.idl',
+]
+
+EXPORTS.mozilla += ['src/AddonPathService.h']
+
+SOURCES += ['src/AddonPathService.cpp']
+
+EXTRA_COMPONENTS += [
+ 'extensions.manifest',
+ 'src/addonManager.js',
+ 'src/amContentHandler.js',
+ 'src/amInstallTrigger.js',
+ 'src/amWebInstallListener.js',
+]
+
+EXTRA_JS_MODULES += [
+ 'src/ChromeManifestParser.jsm',
+ 'src/DeferredSave.jsm',
+ 'src/LightweightThemeConsumer.jsm',
+ 'src/LightweightThemeManager.jsm',
+]
+
+EXTRA_PP_JS_MODULES += [
+ 'src/AddonManager.jsm',
+ 'src/GMPInstallManager.jsm',
+ 'src/GMPUtils.jsm',
+]
+
+EXTRA_JS_MODULES.addons += [
+ 'src/AddonLogging.jsm',
+ 'src/AddonRepository.jsm',
+ 'src/AddonRepository_SQLiteMigrator.jsm',
+ 'src/Content.js',
+ 'src/GMPProvider.jsm',
+ 'src/LightweightThemeImageOptimizer.jsm',
+ 'src/PluginProvider.jsm',
+ 'src/ProductAddonChecker.jsm',
+ 'src/SpellCheckDictionaryBootstrap.js',
+]
+
+EXTRA_PP_JS_MODULES.addons += [
+ 'src/AddonUpdateChecker.jsm',
+ 'src/XPIProvider.jsm',
+ 'src/XPIProviderUtils.js',
+]
+
+XPIDL_MODULE = 'extensions'
+FINAL_LIBRARY = 'xul'
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/components/extensions/public/amIAddonManager.idl b/components/extensions/public/amIAddonManager.idl
new file mode 100644
index 000000000..58a58b62d
--- /dev/null
+++ b/components/extensions/public/amIAddonManager.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+/**
+ * A service to make some AddonManager functionality available to C++ callers.
+ * Javascript callers should still use AddonManager.jsm directly.
+ */
+[scriptable, function, uuid(7b45d82d-7ad5-48d7-9b05-f32eb9818cd4)]
+interface amIAddonManager : nsISupports
+{
+ /**
+ * Synchronously map a URI to the corresponding Addon ID.
+ *
+ * Mappable URIs are limited to in-application resources belonging to the
+ * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
+ * but do not include URIs from meta data, such as the add-on homepage.
+ *
+ * @param aURI
+ * The nsIURI to map
+ * @return
+ * true if the URI has been mapped successfully to an Addon ID
+ */
+ boolean mapURIToAddonID(in nsIURI aURI, out AUTF8String aID);
+};
diff --git a/components/extensions/public/amIAddonPathService.idl b/components/extensions/public/amIAddonPathService.idl
new file mode 100644
index 000000000..9c9197a61
--- /dev/null
+++ b/components/extensions/public/amIAddonPathService.idl
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+/**
+ * This service maps file system paths where add-ons reside to the ID
+ * of the add-on. Paths are added by the add-on manager. They can
+ * looked up by anyone.
+ */
+[scriptable, uuid(fcd9e270-dfb1-11e3-8b68-0800200c9a66)]
+interface amIAddonPathService : nsISupports
+{
+ /**
+ * Given a path to a file, return the ID of the add-on that the file belongs
+ * to. Returns an empty string if there is no add-on there. Note that if an
+ * add-on is located at /a/b/c, then looking up the path /a/b/c/d will return
+ * that add-on.
+ */
+ AString findAddonId(in AString path);
+
+ /**
+ * Call this function to inform the service that the given file system path is
+ * associated with the given add-on ID.
+ */
+ void insertPath(in AString path, in AString addonId);
+
+ /**
+ * Given a URI to a file, return the ID of the add-on that the file belongs
+ * to. Returns an empty string if there is no add-on there.
+ */
+ AString mapURIToAddonId(in nsIURI aURI);
+};
diff --git a/components/extensions/public/amIWebInstallListener.idl b/components/extensions/public/amIWebInstallListener.idl
new file mode 100644
index 000000000..eed108097
--- /dev/null
+++ b/components/extensions/public/amIWebInstallListener.idl
@@ -0,0 +1,134 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMElement;
+interface nsIURI;
+interface nsIVariant;
+
+/**
+ * amIWebInstallInfo is used by the default implementation of
+ * amIWebInstallListener to communicate with the running application and allow
+ * it to warn the user about blocked installs and start the installs running.
+ */
+[scriptable, uuid(fa0b47a3-f819-47ac-bc66-4bd1d7f67b1d)]
+interface amIWebInstallInfo : nsISupports
+{
+ readonly attribute nsIDOMElement browser;
+ readonly attribute nsIURI originatingURI;
+ readonly attribute nsIVariant installs;
+
+ /**
+ * Starts all installs.
+ */
+ void install();
+};
+
+/**
+ * The registered amIWebInstallListener is used to notify about new installs
+ * triggered by websites. The default implementation displays a confirmation
+ * dialog when add-ons are ready to install and uses the observer service to
+ * notify when installations are blocked.
+ */
+[scriptable, uuid(d9240d4b-6b3a-4cad-b402-de6c93337e0c)]
+interface amIWebInstallListener : nsISupports
+{
+ /**
+ * Called when installation by websites is currently disabled.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were blocked
+ * @param aCount
+ * The number of AddonInstalls
+ */
+ void onWebInstallDisabled(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+
+ /**
+ * Called when the website is not allowed to directly prompt the user to
+ * install add-ons.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were blocked
+ * @param aCount
+ * The number of AddonInstalls
+ * @return true if the caller should start the installs
+ */
+ boolean onWebInstallBlocked(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+
+ /**
+ * Called when a website wants to ask the user to install add-ons.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were requested
+ * @param aCount
+ * The number of AddonInstalls
+ * @return true if the caller should start the installs
+ */
+ boolean onWebInstallRequested(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+};
+
+[scriptable, uuid(a80b89ad-bb1a-4c43-9cb7-3ae656556f78)]
+interface amIWebInstallListener2 : nsISupports
+{
+ /**
+ * Called when a non-same-origin resource attempted to initiate an install.
+ * Installs will have already been cancelled and cannot be restarted.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were blocked
+ * @param aCount
+ * The number of AddonInstalls
+ */
+ boolean onWebInstallOriginBlocked(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+};
+
+/**
+ * amIWebInstallPrompt is used, if available, by the default implementation of
+ * amIWebInstallInfo to display a confirmation UI to the user before running
+ * installs.
+ */
+[scriptable, uuid(386906f1-4d18-45bf-bc81-5dcd68e42c3b)]
+interface amIWebInstallPrompt : nsISupports
+{
+ /**
+ * Get a confirmation that the user wants to start the installs.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were requested
+ * @param aCount
+ * The number of AddonInstalls
+ */
+ void confirm(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+};
diff --git a/components/extensions/public/amIWebInstaller.idl b/components/extensions/public/amIWebInstaller.idl
new file mode 100644
index 000000000..6c5ebca67
--- /dev/null
+++ b/components/extensions/public/amIWebInstaller.idl
@@ -0,0 +1,82 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMElement;
+interface nsIVariant;
+interface nsIURI;
+
+/**
+ * A callback function used to notify webpages when a requested install has
+ * ended.
+ *
+ * NOTE: This is *not* the same as InstallListener.
+ */
+[scriptable, function, uuid(bb22f5c0-3ca1-48f6-873c-54e87987700f)]
+interface amIInstallCallback : nsISupports
+{
+ /**
+ * Called when an install completes or fails.
+ *
+ * @param aUrl
+ * The url of the add-on being installed
+ * @param aStatus
+ * 0 if the install was successful or negative if not
+ */
+ void onInstallEnded(in AString aUrl, in int32_t aStatus);
+};
+
+
+/**
+ * This interface is used to allow webpages to start installing add-ons.
+ */
+[scriptable, uuid(658d6c09-15e0-4688-bee8-8551030472a9)]
+interface amIWebInstaller : nsISupports
+{
+ /**
+ * Checks if installation is enabled for a webpage.
+ *
+ * @param aMimetype
+ * The mimetype for the add-on to be installed
+ * @param referer
+ * The URL of the webpage trying to install an add-on
+ * @return true if installation is enabled
+ */
+ boolean isInstallEnabled(in AString aMimetype, in nsIURI aReferer);
+
+ /**
+ * Installs an array of add-ons at the request of a webpage
+ *
+ * @param aMimetype
+ * The mimetype for the add-ons
+ * @param aBrowser
+ * The browser installing the add-ons.
+ * @param aReferer
+ * The URI for the webpage installing the add-ons
+ * @param aUris
+ * The URIs of add-ons to be installed
+ * @param aHashes
+ * The hashes for the add-ons to be installed
+ * @param aNames
+ * The names for the add-ons to be installed
+ * @param aIcons
+ * The icons for the add-ons to be installed
+ * @param aCallback
+ * An optional callback to notify about installation success and
+ * failure
+ * @param aInstallCount
+ * An optional argument including the number of add-ons to install
+ * @return true if the installation was successfully started
+ */
+ boolean installAddonsFromWebpage(in AString aMimetype,
+ in nsIDOMElement aBrowser,
+ in nsIURI aReferer,
+ [array, size_is(aInstallCount)] in wstring aUris,
+ [array, size_is(aInstallCount)] in wstring aHashes,
+ [array, size_is(aInstallCount)] in wstring aNames,
+ [array, size_is(aInstallCount)] in wstring aIcons,
+ [optional] in amIInstallCallback aCallback,
+ [optional] in uint32_t aInstallCount);
+};
diff --git a/components/extensions/src/AddonLogging.jsm b/components/extensions/src/AddonLogging.jsm
new file mode 100644
index 000000000..ffa92c791
--- /dev/null
+++ b/components/extensions/src/AddonLogging.jsm
@@ -0,0 +1,187 @@
+/* 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+const KEY_PROFILEDIR = "ProfD";
+const FILE_EXTENSIONS_LOG = "extensions.log";
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+
+const LOGGER_FILE_PERM = parseInt("666", 8);
+
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = [ "LogManager" ];
+
+var gDebugLogEnabled = false;
+
+function formatLogMessage(aType, aName, aStr, aException) {
+ let message = aType.toUpperCase() + " " + aName + ": " + aStr;
+ if (aException) {
+ if (typeof aException == "number")
+ return message + ": " + Components.Exception("", aException).name;
+
+ message = message + ": " + aException;
+ // instanceOf doesn't work here, let's duck type
+ if (aException.fileName)
+ message = message + " (" + aException.fileName + ":" + aException.lineNumber + ")";
+
+ if (aException.message == "too much recursion")
+ dump(message + "\n" + aException.stack + "\n");
+ }
+ return message;
+}
+
+function getStackDetails(aException) {
+ // Defensively wrap all this to ensure that failing to get the message source
+ // doesn't stop the message from being logged
+ try {
+ if (aException) {
+ if (aException instanceof Ci.nsIException) {
+ return {
+ sourceName: aException.filename,
+ lineNumber: aException.lineNumber
+ };
+ }
+
+ if (typeof aException == "object") {
+ return {
+ sourceName: aException.fileName,
+ lineNumber: aException.lineNumber
+ };
+ }
+ }
+
+ let stackFrame = Components.stack.caller.caller.caller;
+ return {
+ sourceName: stackFrame.filename,
+ lineNumber: stackFrame.lineNumber
+ };
+ }
+ catch (e) {
+ return {
+ sourceName: null,
+ lineNumber: 0
+ };
+ }
+}
+
+function AddonLogger(aName) {
+ this.name = aName;
+}
+
+AddonLogger.prototype = {
+ name: null,
+
+ error: function(aStr, aException) {
+ let message = formatLogMessage("error", this.name, aStr, aException);
+
+ let stack = getStackDetails(aException);
+
+ let consoleMessage = Cc["@mozilla.org/scripterror;1"].
+ createInstance(Ci.nsIScriptError);
+ consoleMessage.init(message, stack.sourceName, null, stack.lineNumber, 0,
+ Ci.nsIScriptError.errorFlag, "component javascript");
+ Services.console.logMessage(consoleMessage);
+
+ // Always dump errors, in case the Console Service isn't listening yet
+ dump("*** " + message + "\n");
+
+ function formatTimestamp(date) {
+ // Format timestamp as: "%Y-%m-%d %H:%M:%S"
+ let year = String(date.getFullYear());
+ let month = String(date.getMonth() + 1).padStart(2, "0");
+ let day = String(date.getDate()).padStart(2, "0");
+ let hours = String(date.getHours()).padStart(2, "0");
+ let minutes = String(date.getMinutes()).padStart(2, "0");
+ let seconds = String(date.getSeconds()).padStart(2, "0");
+
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+ }
+
+ try {
+ var tstamp = new Date();
+ var logfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS_LOG]);
+ var stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ stream.init(logfile, 0x02 | 0x08 | 0x10, LOGGER_FILE_PERM, 0); // write, create, append
+ var writer = Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(Ci.nsIConverterOutputStream);
+ writer.init(stream, "UTF-8", 0, 0x0000);
+ writer.writeString(formatTimestamp(tstamp) + " " +
+ message + " at " + stack.sourceName + ":" +
+ stack.lineNumber + "\n");
+ writer.close();
+ }
+ catch (e) { }
+ },
+
+ warn: function(aStr, aException) {
+ let message = formatLogMessage("warn", this.name, aStr, aException);
+
+ let stack = getStackDetails(aException);
+
+ let consoleMessage = Cc["@mozilla.org/scripterror;1"].
+ createInstance(Ci.nsIScriptError);
+ consoleMessage.init(message, stack.sourceName, null, stack.lineNumber, 0,
+ Ci.nsIScriptError.warningFlag, "component javascript");
+ Services.console.logMessage(consoleMessage);
+
+ if (gDebugLogEnabled)
+ dump("*** " + message + "\n");
+ },
+
+ log: function(aStr, aException) {
+ if (gDebugLogEnabled) {
+ let message = formatLogMessage("log", this.name, aStr, aException);
+ dump("*** " + message + "\n");
+ Services.console.logStringMessage(message);
+ }
+ }
+};
+
+this.LogManager = {
+ getLogger: function(aName, aTarget) {
+ let logger = new AddonLogger(aName);
+
+ if (aTarget) {
+ ["error", "warn", "log"].forEach(function(name) {
+ let fname = name.toUpperCase();
+ delete aTarget[fname];
+ aTarget[fname] = function(aStr, aException) {
+ logger[name](aStr, aException);
+ };
+ });
+ }
+
+ return logger;
+ }
+};
+
+var PrefObserver = {
+ init: function() {
+ Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "xpcom-shutdown") {
+ Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this);
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ }
+ else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) {
+ gDebugLogEnabled = Services.prefs.getBoolPref(PREF_LOGGING_ENABLED, false);
+ }
+ }
+};
+
+PrefObserver.init();
diff --git a/components/extensions/src/AddonManager.jsm b/components/extensions/src/AddonManager.jsm
new file mode 100644
index 000000000..aef5c6acb
--- /dev/null
+++ b/components/extensions/src/AddonManager.jsm
@@ -0,0 +1,2902 @@
+/* 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+// Cannot use Services.appinfo here, or else xpcshell-tests will blow up, as
+// most tests later register different nsIAppInfo implementations, which
+// wouldn't be reflected in Services.appinfo anymore, as the lazy getter
+// underlying it would have been initialized if we used it here.
+if ("@mozilla.org/xre/app-info;1" in Cc) {
+ let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
+ if (runtime.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ // Refuse to run in child processes.
+ throw new Error("You cannot use the AddonManager in child processes!");
+ }
+}
+
+
+const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
+const PREF_DEFAULT_PROVIDERS_ENABLED = "extensions.defaultProviders.enabled";
+const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled";
+const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion";
+const PREF_EM_LAST_PLATFORM_VERSION = "extensions.lastPlatformVersion";
+const PREF_EM_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault";
+const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
+const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
+const PREF_APP_UPDATE_AUTO = "app.update.auto";
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+const UNKNOWN_XPCOM_ABI = "unknownABI";
+
+const UPDATE_REQUEST_VERSION = 2;
+const CATEGORY_UPDATE_PARAMS = "extension-update-params";
+
+const XMLURI_BLOCKLIST = "http://www.mozilla.org/2006/addons-blocklist";
+
+const KEY_PROFILEDIR = "ProfD";
+const KEY_APPDIR = "XCurProcD";
+const FILE_BLOCKLIST = "blocklist.xml";
+
+const BRANCH_REGEXP = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
+const PREF_EM_CHECK_COMPATIBILITY = "extensions.enableCompatibilityChecking";
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+
+const VALID_TYPES_REGEXP = /^[\w\-]+$/;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() {
+ let certUtils = {};
+ Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
+ return certUtils;
+});
+
+
+this.EXPORTED_SYMBOLS = [ "AddonManager", "AddonManagerPrivate" ];
+
+const CATEGORY_PROVIDER_MODULE = "addon-provider-module";
+
+// A list of providers to load by default
+const DEFAULT_PROVIDERS = [
+ "resource://gre/modules/addons/XPIProvider.jsm",
+ "resource://gre/modules/LightweightThemeManager.jsm"
+];
+
+Cu.import("resource://gre/modules/Log.jsm");
+// Configure a logger at the parent 'addons' level to format
+// messages for all the modules under addons.*
+const PARENT_LOGGER_ID = "addons";
+var parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID);
+parentLogger.level = Log.Level.Warn;
+var formatter = new Log.BasicFormatter();
+// Set parent logger (and its children) to append to
+// the Javascript section of the Browser Console
+parentLogger.addAppender(new Log.ConsoleAppender(formatter));
+// Set parent logger (and its children) to
+// also append to standard out
+parentLogger.addAppender(new Log.DumpAppender(formatter));
+
+// Create a new logger (child of 'addons' logger)
+// for use by the Addons Manager
+const LOGGER_ID = "addons.manager";
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+// Provide the ability to enable/disable logging
+// messages at runtime.
+// If the "extensions.logging.enabled" preference is
+// missing or 'false', messages at the WARNING and higher
+// severity should be logged to the JS console and standard error.
+// If "extensions.logging.enabled" is set to 'true', messages
+// at DEBUG and higher should go to JS console and standard error.
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+const UNNAMED_PROVIDER = "<unnamed-provider>";
+function providerName(aProvider) {
+ return aProvider.name || UNNAMED_PROVIDER;
+}
+
+/**
+ * Preference listener which listens for a change in the
+ * "extensions.logging.enabled" preference and changes the logging level of the
+ * parent 'addons' level logger accordingly.
+ */
+var PrefObserver = {
+ init: function() {
+ Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "xpcom-shutdown") {
+ Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this);
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ }
+ else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) {
+ let debugLogEnabled = Services.prefs.getBoolPref(PREF_LOGGING_ENABLED, false);
+ if (debugLogEnabled) {
+ parentLogger.level = Log.Level.Debug;
+ }
+ else {
+ parentLogger.level = Log.Level.Warn;
+ }
+ }
+ }
+};
+
+PrefObserver.init();
+
+/**
+ * Calls a callback method consuming any thrown exception. Any parameters after
+ * the callback parameter will be passed to the callback.
+ *
+ * @param aCallback
+ * The callback method to call
+ */
+function safeCall(aCallback, ...aArgs) {
+ try {
+ aCallback.apply(null, aArgs);
+ }
+ catch (e) {
+ logger.warn("Exception calling callback", e);
+ }
+}
+
+/**
+ * Report an exception thrown by a provider API method.
+ */
+function reportProviderError(aProvider, aMethod, aError) {
+ let method = `provider ${providerName(aProvider)}.${aMethod}`;
+ logger.error("Exception calling " + method, aError);
+}
+
+/**
+ * Calls a method on a provider if it exists and consumes any thrown exception.
+ * Any parameters after the aDefault parameter are passed to the provider's method.
+ *
+ * @param aProvider
+ * The provider to call
+ * @param aMethod
+ * The method name to call
+ * @param aDefault
+ * A default return value if the provider does not implement the named
+ * method or throws an error.
+ * @return the return value from the provider, or aDefault if the provider does not
+ * implement method or throws an error
+ */
+function callProvider(aProvider, aMethod, aDefault, ...aArgs) {
+ if (!(aMethod in aProvider))
+ return aDefault;
+
+ try {
+ return aProvider[aMethod].apply(aProvider, aArgs);
+ }
+ catch (e) {
+ reportProviderError(aProvider, aMethod, e);
+ return aDefault;
+ }
+}
+
+/**
+ * Calls a method on a provider if it exists and consumes any thrown exception.
+ * Parameters after aMethod are passed to aProvider.aMethod().
+ * The last parameter must be a callback function.
+ * If the provider does not implement the method, or the method throws, calls
+ * the callback with 'undefined'.
+ *
+ * @param aProvider
+ * The provider to call
+ * @param aMethod
+ * The method name to call
+ */
+function callProviderAsync(aProvider, aMethod, ...aArgs) {
+ let callback = aArgs[aArgs.length - 1];
+ if (!(aMethod in aProvider)) {
+ callback(undefined);
+ return;
+ }
+ try {
+ return aProvider[aMethod].apply(aProvider, aArgs);
+ }
+ catch (e) {
+ reportProviderError(aProvider, aMethod, e);
+ callback(undefined);
+ return;
+ }
+}
+
+/**
+ * Gets the currently selected locale for display.
+ * @return the selected locale or "en-US" if none is selected
+ */
+function getLocale() {
+ try {
+ if (Services.prefs.getBoolPref(PREF_MATCH_OS_LOCALE))
+ return Services.locale.getLocaleComponentForUserAgent();
+ }
+ catch (e) { }
+
+ try {
+ let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE,
+ Ci.nsIPrefLocalizedString);
+ if (locale)
+ return locale;
+ }
+ catch (e) { }
+
+ try {
+ return Services.prefs.getCharPref(PREF_SELECTED_LOCALE);
+ }
+ catch (e) { }
+
+ return "en-US";
+}
+
+/**
+ * Previously the APIs for installing add-ons from webpages accepted nsIURI
+ * arguments for the installing page. They now take an nsIPrincipal but for now
+ * maintain backwards compatibility by converting an nsIURI to an nsIPrincipal.
+ *
+ * @param aPrincipalOrURI
+ * The argument passed to the API function. Can be null, an nsIURI or
+ * an nsIPrincipal.
+ * @return an nsIPrincipal.
+ */
+function ensurePrincipal(principalOrURI) {
+ if (principalOrURI instanceof Ci.nsIPrincipal)
+ return principalOrURI;
+
+ logger.warn("Deprecated API call, please pass a non-null nsIPrincipal instead of an nsIURI");
+
+ // Previously a null installing URI meant allowing the install regardless.
+ if (!principalOrURI) {
+ return Services.scriptSecurityManager.getSystemPrincipal();
+ }
+
+ if (principalOrURI instanceof Ci.nsIURI) {
+ return Services.scriptSecurityManager.createCodebasePrincipal(principalOrURI, {
+ inBrowser: true
+ });
+ }
+
+ // Just return whatever we have, the API method will log an error about it.
+ return principalOrURI;
+}
+
+/**
+ * A helper class to repeatedly call a listener with each object in an array
+ * optionally checking whether the object has a method in it.
+ *
+ * @param aObjects
+ * The array of objects to iterate through
+ * @param aMethod
+ * An optional method name, if not null any objects without this method
+ * will not be passed to the listener
+ * @param aListener
+ * A listener implementing nextObject and noMoreObjects methods. The
+ * former will be called with the AsyncObjectCaller as the first
+ * parameter and the object as the second. noMoreObjects will be passed
+ * just the AsyncObjectCaller
+ */
+function AsyncObjectCaller(aObjects, aMethod, aListener) {
+ this.objects = [...aObjects];
+ this.method = aMethod;
+ this.listener = aListener;
+
+ this.callNext();
+}
+
+AsyncObjectCaller.prototype = {
+ objects: null,
+ method: null,
+ listener: null,
+
+ /**
+ * Passes the next object to the listener or calls noMoreObjects if there
+ * are none left.
+ */
+ callNext: function() {
+ if (this.objects.length == 0) {
+ this.listener.noMoreObjects(this);
+ return;
+ }
+
+ let object = this.objects.shift();
+ if (!this.method || this.method in object)
+ this.listener.nextObject(this, object);
+ else
+ this.callNext();
+ }
+};
+
+/**
+ * Listens for a browser changing origin and cancels the installs that were
+ * started by it.
+ */
+function BrowserListener(aBrowser, aInstallingPrincipal, aInstalls) {
+ this.browser = aBrowser;
+ this.principal = aInstallingPrincipal;
+ this.installs = aInstalls;
+ this.installCount = aInstalls.length;
+
+ aBrowser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+ Services.obs.addObserver(this, "message-manager-disconnect", true);
+
+ for (let install of this.installs)
+ install.addListener(this);
+
+ this.registered = true;
+}
+
+BrowserListener.prototype = {
+ browser: null,
+ installs: null,
+ installCount: null,
+ registered: false,
+
+ unregister: function() {
+ if (!this.registered)
+ return;
+ this.registered = false;
+
+ Services.obs.removeObserver(this, "message-manager-disconnect");
+ // The browser may have already been detached
+ if (this.browser.removeProgressListener)
+ this.browser.removeProgressListener(this);
+
+ for (let install of this.installs)
+ install.removeListener(this);
+ this.installs = null;
+ },
+
+ cancelInstalls: function() {
+ for (let install of this.installs) {
+ try {
+ install.cancel();
+ }
+ catch (e) {
+ // Some installs may have already failed or been cancelled, ignore these
+ }
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ if (subject != this.browser.messageManager)
+ return;
+
+ // The browser's message manager has closed and so the browser is
+ // going away, cancel all installs
+ this.cancelInstalls();
+ },
+
+ onLocationChange: function(webProgress, request, location) {
+ if (this.browser.contentPrincipal && this.principal.subsumes(this.browser.contentPrincipal))
+ return;
+
+ // The browser has navigated to a new origin so cancel all installs
+ this.cancelInstalls();
+ },
+
+ onDownloadCancelled: function(install) {
+ // Don't need to hear more events from this install
+ install.removeListener(this);
+
+ // Once all installs have ended unregister everything
+ if (--this.installCount == 0)
+ this.unregister();
+ },
+
+ onDownloadFailed: function(install) {
+ this.onDownloadCancelled(install);
+ },
+
+ onInstallFailed: function(install) {
+ this.onDownloadCancelled(install);
+ },
+
+ onInstallEnded: function(install) {
+ this.onDownloadCancelled(install);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
+ Ci.nsIWebProgressListener,
+ Ci.nsIObserver])
+};
+
+/**
+ * This represents an author of an add-on (e.g. creator or developer)
+ *
+ * @param aName
+ * The name of the author
+ * @param aURL
+ * The URL of the author's profile page
+ */
+function AddonAuthor(aName, aURL) {
+ this.name = aName;
+ this.url = aURL;
+}
+
+AddonAuthor.prototype = {
+ name: null,
+ url: null,
+
+ // Returns the author's name, defaulting to the empty string
+ toString: function() {
+ return this.name || "";
+ }
+}
+
+/**
+ * This represents an screenshot for an add-on
+ *
+ * @param aURL
+ * The URL to the full version of the screenshot
+ * @param aWidth
+ * The width in pixels of the screenshot
+ * @param aHeight
+ * The height in pixels of the screenshot
+ * @param aThumbnailURL
+ * The URL to the thumbnail version of the screenshot
+ * @param aThumbnailWidth
+ * The width in pixels of the thumbnail version of the screenshot
+ * @param aThumbnailHeight
+ * The height in pixels of the thumbnail version of the screenshot
+ * @param aCaption
+ * The caption of the screenshot
+ */
+function AddonScreenshot(aURL, aWidth, aHeight, aThumbnailURL,
+ aThumbnailWidth, aThumbnailHeight, aCaption) {
+ this.url = aURL;
+ if (aWidth) this.width = aWidth;
+ if (aHeight) this.height = aHeight;
+ if (aThumbnailURL) this.thumbnailURL = aThumbnailURL;
+ if (aThumbnailWidth) this.thumbnailWidth = aThumbnailWidth;
+ if (aThumbnailHeight) this.thumbnailHeight = aThumbnailHeight;
+ if (aCaption) this.caption = aCaption;
+}
+
+AddonScreenshot.prototype = {
+ url: null,
+ width: null,
+ height: null,
+ thumbnailURL: null,
+ thumbnailWidth: null,
+ thumbnailHeight: null,
+ caption: null,
+
+ // Returns the screenshot URL, defaulting to the empty string
+ toString: function() {
+ return this.url || "";
+ }
+}
+
+
+/**
+ * This represents a compatibility override for an addon.
+ *
+ * @param aType
+ * Overrride type - "compatible" or "incompatible"
+ * @param aMinVersion
+ * Minimum version of the addon to match
+ * @param aMaxVersion
+ * Maximum version of the addon to match
+ * @param aAppID
+ * Application ID used to match appMinVersion and appMaxVersion
+ * @param aAppMinVersion
+ * Minimum version of the application to match
+ * @param aAppMaxVersion
+ * Maximum version of the application to match
+ */
+function AddonCompatibilityOverride(aType, aMinVersion, aMaxVersion, aAppID,
+ aAppMinVersion, aAppMaxVersion) {
+ this.type = aType;
+ this.minVersion = aMinVersion;
+ this.maxVersion = aMaxVersion;
+ this.appID = aAppID;
+ this.appMinVersion = aAppMinVersion;
+ this.appMaxVersion = aAppMaxVersion;
+}
+
+AddonCompatibilityOverride.prototype = {
+ /**
+ * Type of override - "incompatible" or "compatible".
+ * Only "incompatible" is supported for now.
+ */
+ type: null,
+
+ /**
+ * Min version of the addon to match.
+ */
+ minVersion: null,
+
+ /**
+ * Max version of the addon to match.
+ */
+ maxVersion: null,
+
+ /**
+ * Application ID to match.
+ */
+ appID: null,
+
+ /**
+ * Min version of the application to match.
+ */
+ appMinVersion: null,
+
+ /**
+ * Max version of the application to match.
+ */
+ appMaxVersion: null
+};
+
+
+/**
+ * A type of add-on, used by the UI to determine how to display different types
+ * of add-ons.
+ *
+ * @param aID
+ * The add-on type ID
+ * @param aLocaleURI
+ * The URI of a localized properties file to get the displayable name
+ * for the type from
+ * @param aLocaleKey
+ * The key for the string in the properties file or the actual display
+ * name if aLocaleURI is null. Include %ID% to include the type ID in
+ * the key
+ * @param aViewType
+ * The optional type of view to use in the UI
+ * @param aUIPriority
+ * The priority is used by the UI to list the types in order. Lower
+ * values push the type higher in the list.
+ * @param aFlags
+ * An option set of flags that customize the display of the add-on in
+ * the UI.
+ */
+function AddonType(aID, aLocaleURI, aLocaleKey, aViewType, aUIPriority, aFlags) {
+ if (!aID)
+ throw Components.Exception("An AddonType must have an ID", Cr.NS_ERROR_INVALID_ARG);
+
+ if (aViewType && aUIPriority === undefined)
+ throw Components.Exception("An AddonType with a defined view must have a set UI priority",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!aLocaleKey)
+ throw Components.Exception("An AddonType must have a displayable name",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ this.id = aID;
+ this.uiPriority = aUIPriority;
+ this.viewType = aViewType;
+ this.flags = aFlags;
+
+ if (aLocaleURI) {
+ this.__defineGetter__("name", function nameGetter() {
+ delete this.name;
+ let bundle = Services.strings.createBundle(aLocaleURI);
+ this.name = bundle.GetStringFromName(aLocaleKey.replace("%ID%", aID));
+ return this.name;
+ });
+ }
+ else {
+ this.name = aLocaleKey;
+ }
+}
+
+var gStarted = false;
+var gStartupComplete = false;
+var gCheckCompatibility = true;
+var gStrictCompatibility = true;
+var gCheckUpdateSecurityDefault = true;
+var gCheckUpdateSecurity = gCheckUpdateSecurityDefault;
+var gUpdateEnabled = true;
+var gAutoUpdateDefault = true;
+var gShutdownBarrier = null;
+var gRepoShutdownState = "";
+var gShutdownInProgress = false;
+
+/**
+ * This is the real manager, kept here rather than in AddonManager to keep its
+ * contents hidden from API users.
+ */
+var AddonManagerInternal = {
+ managerListeners: [],
+ installListeners: [],
+ addonListeners: [],
+ typeListeners: [],
+ pendingProviders: new Set(),
+ providers: new Set(),
+ providerShutdowns: new Map(),
+ types: {},
+ startupChanges: {},
+
+ validateBlocklist: function() {
+ let appBlocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+
+ // If there is no application shipped blocklist then there is nothing to do
+ if (!appBlocklist.exists())
+ return;
+
+ let profileBlocklist = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
+
+ // If there is no blocklist in the profile then copy the application shipped
+ // one there
+ if (!profileBlocklist.exists()) {
+ try {
+ appBlocklist.copyTo(profileBlocklist.parent, FILE_BLOCKLIST);
+ }
+ catch (e) {
+ logger.warn("Failed to copy the application shipped blocklist to the profile", e);
+ }
+ return;
+ }
+
+ let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ try {
+ let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Ci.nsIConverterInputStream);
+ fileStream.init(appBlocklist, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ cstream.init(fileStream, "UTF-8", 0, 0);
+
+ let data = "";
+ let str = {};
+ let read = 0;
+ do {
+ read = cstream.readString(0xffffffff, str);
+ data += str.value;
+ } while (read != 0);
+
+ let parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ var doc = parser.parseFromString(data, "text/xml");
+ }
+ catch (e) {
+ logger.warn("Application shipped blocklist could not be loaded", e);
+ return;
+ }
+ finally {
+ try {
+ fileStream.close();
+ }
+ catch (e) {
+ logger.warn("Unable to close blocklist file stream", e);
+ }
+ }
+
+ // If the namespace is incorrect then ignore the application shipped
+ // blocklist
+ if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) {
+ logger.warn("Application shipped blocklist has an unexpected namespace (" +
+ doc.documentElement.namespaceURI + ")");
+ return;
+ }
+
+ // If there is no lastupdate information then ignore the application shipped
+ // blocklist
+ if (!doc.documentElement.hasAttribute("lastupdate"))
+ return;
+
+ // If the application shipped blocklist is older than the profile blocklist
+ // then do nothing
+ if (doc.documentElement.getAttribute("lastupdate") <=
+ profileBlocklist.lastModifiedTime)
+ return;
+
+ // Otherwise copy the application shipped blocklist to the profile
+ try {
+ appBlocklist.copyTo(profileBlocklist.parent, FILE_BLOCKLIST);
+ }
+ catch (e) {
+ logger.warn("Failed to copy the application shipped blocklist to the profile", e);
+ }
+ },
+
+ /**
+ * Start up a provider, and register its shutdown hook if it has one
+ */
+ _startProvider(aProvider, aAppChanged, aOldAppVersion, aOldPlatformVersion) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ logger.debug(`Starting provider: ${providerName(aProvider)}`);
+ callProvider(aProvider, "startup", null, aAppChanged, aOldAppVersion, aOldPlatformVersion);
+ if ('shutdown' in aProvider) {
+ let name = providerName(aProvider);
+ let AMProviderShutdown = () => {
+ // If the provider has been unregistered, it will have been removed from
+ // this.providers. If it hasn't been unregistered, then this is a normal
+ // shutdown - and we move it to this.pendingProviders incase we're
+ // running in a test that will start AddonManager again.
+ if (this.providers.has(aProvider)) {
+ this.providers.delete(aProvider);
+ this.pendingProviders.add(aProvider);
+ }
+
+ return new Promise((resolve, reject) => {
+ logger.debug("Calling shutdown blocker for " + name);
+ resolve(aProvider.shutdown());
+ })
+ .catch(err => {
+ logger.warn("Failure during shutdown of " + name, err);
+ });
+ };
+ logger.debug("Registering shutdown blocker for " + name);
+ this.providerShutdowns.set(aProvider, AMProviderShutdown);
+ AddonManager.shutdown.addBlocker(name, AMProviderShutdown);
+ }
+
+ this.pendingProviders.delete(aProvider);
+ this.providers.add(aProvider);
+ logger.debug(`Provider finished startup: ${providerName(aProvider)}`);
+ },
+
+ /**
+ * Initializes the AddonManager, loading any known providers and initializing
+ * them.
+ */
+ startup: function() {
+ try {
+ if (gStarted)
+ return;
+
+ let appChanged = undefined;
+
+ let oldAppVersion = null;
+ try {
+ oldAppVersion = Services.prefs.getCharPref(PREF_EM_LAST_APP_VERSION);
+ appChanged = Services.appinfo.version != oldAppVersion;
+ }
+ catch (e) { }
+
+ let oldPlatformVersion = Services.prefs.getCharPref(PREF_EM_LAST_PLATFORM_VERSION, "");
+
+ if (appChanged !== false) {
+ logger.debug("Application has been upgraded");
+ Services.prefs.setCharPref(PREF_EM_LAST_APP_VERSION,
+ Services.appinfo.version);
+ Services.prefs.setCharPref(PREF_EM_LAST_PLATFORM_VERSION,
+ Services.appinfo.platformVersion);
+ Services.prefs.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION,
+ (appChanged === undefined ? 0 : -1));
+ this.validateBlocklist();
+ }
+
+ gCheckCompatibility = Services.prefs.getBoolPref(PREF_EM_CHECK_COMPATIBILITY,
+ gCheckCompatibility);
+ Services.prefs.addObserver(PREF_EM_CHECK_COMPATIBILITY, this, false);
+
+ gStrictCompatibility = Services.prefs.getBoolPref(PREF_EM_STRICT_COMPATIBILITY,
+ gStrictCompatibility);
+ Services.prefs.addObserver(PREF_EM_STRICT_COMPATIBILITY, this, false);
+
+ let defaultBranch = Services.prefs.getDefaultBranch("");
+ gCheckUpdateSecurityDefault = defaultBranch.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY,
+ gCheckUpdateSecurityDefault);
+
+ gCheckUpdateSecurity = Services.prefs.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY,
+ gCheckUpdateSecurity);
+ Services.prefs.addObserver(PREF_EM_CHECK_UPDATE_SECURITY, this, false);
+
+ gUpdateEnabled = Services.prefs.getBoolPref(PREF_EM_UPDATE_ENABLED, gUpdateEnabled);
+ Services.prefs.addObserver(PREF_EM_UPDATE_ENABLED, this, false);
+
+ gAutoUpdateDefault = Services.prefs.getBoolPref(PREF_EM_AUTOUPDATE_DEFAULT,
+ gAutoUpdateDefault);
+ Services.prefs.addObserver(PREF_EM_AUTOUPDATE_DEFAULT, this, false);
+
+ let defaultProvidersEnabled = Services.prefs.getBoolPref(PREF_DEFAULT_PROVIDERS_ENABLED, true);
+
+ // Ensure all default providers have had a chance to register themselves
+ if (defaultProvidersEnabled) {
+ for (let url of DEFAULT_PROVIDERS) {
+ try {
+ let scope = {};
+ Components.utils.import(url, scope);
+ // Sanity check - make sure the provider exports a symbol that
+ // has a 'startup' method
+ let syms = Object.keys(scope);
+ if ((syms.length < 1) ||
+ (typeof scope[syms[0]].startup != "function")) {
+ logger.warn("Provider " + url + " has no startup()");
+ }
+ logger.debug("Loaded provider scope for " + url + ": " + Object.keys(scope).toSource());
+ }
+ catch (e) {
+ logger.error("Exception loading default provider \"" + url + "\"", e);
+ }
+ };
+ }
+
+ // Load any providers registered in the category manager
+ let catman = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ let entries = catman.enumerateCategory(CATEGORY_PROVIDER_MODULE);
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data;
+ let url = catman.getCategoryEntry(CATEGORY_PROVIDER_MODULE, entry);
+
+ try {
+ Components.utils.import(url, {});
+ logger.debug(`Loaded provider scope for ${url}`);
+ }
+ catch (e) {
+ logger.error("Exception loading provider " + entry + " from category \"" +
+ url + "\"", e);
+ }
+ }
+
+ // Register our shutdown handler with the AsyncShutdown manager
+ gShutdownBarrier = new AsyncShutdown.Barrier("AddonManager: Waiting for providers to shut down.");
+ AsyncShutdown.profileBeforeChange.addBlocker("AddonManager: shutting down.",
+ this.shutdownManager.bind(this),
+ {fetchState: this.shutdownState.bind(this)});
+
+ // Once we start calling providers we must allow all normal methods to work.
+ gStarted = true;
+
+ for (let provider of this.pendingProviders) {
+ this._startProvider(provider, appChanged, oldAppVersion, oldPlatformVersion);
+ }
+
+ // If this is a new profile just pretend that there were no changes
+ if (appChanged === undefined) {
+ for (let type in this.startupChanges)
+ delete this.startupChanges[type];
+ }
+
+ gStartupComplete = true;
+ }
+ catch (e) {
+ logger.error("startup failed", e);
+ }
+
+ logger.debug("Completed startup sequence");
+ this.callManagerListeners("onStartup");
+ },
+
+ /**
+ * Registers a new AddonProvider.
+ *
+ * @param aProvider
+ * The provider to register
+ * @param aTypes
+ * An optional array of add-on types
+ */
+ registerProvider: function(aProvider, aTypes) {
+ if (!aProvider || typeof aProvider != "object")
+ throw Components.Exception("aProvider must be specified",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (aTypes && !Array.isArray(aTypes))
+ throw Components.Exception("aTypes must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ this.pendingProviders.add(aProvider);
+
+ if (aTypes) {
+ aTypes.forEach(function(aType) {
+ if (!(aType.id in this.types)) {
+ if (!VALID_TYPES_REGEXP.test(aType.id)) {
+ logger.warn("Ignoring invalid type " + aType.id);
+ return;
+ }
+
+ this.types[aType.id] = {
+ type: aType,
+ providers: [aProvider]
+ };
+
+ let typeListeners = this.typeListeners.slice(0);
+ for (let listener of typeListeners) {
+ safeCall(function listenerSafeCall() {
+ listener.onTypeAdded(aType);
+ });
+ }
+ }
+ else {
+ this.types[aType.id].providers.push(aProvider);
+ }
+ }, this);
+ }
+
+ // If we're registering after startup call this provider's startup.
+ if (gStarted) {
+ this._startProvider(aProvider);
+ }
+ },
+
+ /**
+ * Unregisters an AddonProvider.
+ *
+ * @param aProvider
+ * The provider to unregister
+ * @return Whatever the provider's 'shutdown' method returns (if anything).
+ * For providers that have async shutdown methods returning Promises,
+ * the caller should wait for that Promise to resolve.
+ */
+ unregisterProvider: function(aProvider) {
+ if (!aProvider || typeof aProvider != "object")
+ throw Components.Exception("aProvider must be specified",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ this.providers.delete(aProvider);
+ // The test harness will unregister XPIProvider *after* shutdown, which is
+ // after the provider will have been moved from providers to
+ // pendingProviders.
+ this.pendingProviders.delete(aProvider);
+
+ for (let type in this.types) {
+ this.types[type].providers = this.types[type].providers.filter(function filterProvider(p) p != aProvider);
+ if (this.types[type].providers.length == 0) {
+ let oldType = this.types[type].type;
+ delete this.types[type];
+
+ let typeListeners = this.typeListeners.slice(0);
+ for (let listener of typeListeners) {
+ safeCall(function listenerSafeCall() {
+ listener.onTypeRemoved(oldType);
+ });
+ }
+ }
+ }
+
+ // If we're unregistering after startup but before shutting down,
+ // remove the blocker for this provider's shutdown and call it.
+ // If we're already shutting down, just let gShutdownBarrier call it to avoid races.
+ if (gStarted && !gShutdownInProgress) {
+ logger.debug("Unregistering shutdown blocker for " + providerName(aProvider));
+ let shutter = this.providerShutdowns.get(aProvider);
+ if (shutter) {
+ this.providerShutdowns.delete(aProvider);
+ gShutdownBarrier.client.removeBlocker(shutter);
+ return shutter();
+ }
+ }
+ return undefined;
+ },
+
+ /**
+ * Mark a provider as safe to access via AddonManager APIs, before its
+ * startup has completed.
+ *
+ * Normally a provider isn't marked as safe until after its (synchronous)
+ * startup() method has returned. Until a provider has been marked safe,
+ * it won't be used by any of the AddonManager APIs. markProviderSafe()
+ * allows a provider to mark itself as safe during its startup; this can be
+ * useful if the provider wants to perform tasks that block startup, which
+ * happen after its required initialization tasks and therefore when the
+ * provider is in a safe state.
+ *
+ * @param aProvider Provider object to mark safe
+ */
+ markProviderSafe: function(aProvider) {
+ if (!gStarted) {
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+
+ if (!aProvider || typeof aProvider != "object") {
+ throw Components.Exception("aProvider must be specified",
+ Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ if (!this.pendingProviders.has(aProvider)) {
+ return;
+ }
+
+ this.pendingProviders.delete(aProvider);
+ this.providers.add(aProvider);
+ },
+
+ /**
+ * Calls a method on all registered providers if it exists and consumes any
+ * thrown exception. Return values are ignored. Any parameters after the
+ * method parameter are passed to the provider's method.
+ * WARNING: Do not use for asynchronous calls; callProviders() does not
+ * invoke callbacks if provider methods throw synchronous exceptions.
+ *
+ * @param aMethod
+ * The method name to call
+ * @see callProvider
+ */
+ callProviders: function(aMethod, ...aArgs) {
+ if (!aMethod || typeof aMethod != "string")
+ throw Components.Exception("aMethod must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let providers = [...this.providers];
+ for (let provider of providers) {
+ try {
+ if (aMethod in provider)
+ provider[aMethod].apply(provider, aArgs);
+ }
+ catch (e) {
+ reportProviderError(aProvider, aMethod, e);
+ }
+ }
+ },
+
+ /**
+ * Report the current state of asynchronous shutdown
+ */
+ shutdownState() {
+ let state = [];
+ if (gShutdownBarrier) {
+ state.push({
+ name: gShutdownBarrier.client.name,
+ state: gShutdownBarrier.state
+ });
+ }
+ state.push({
+ name: "AddonRepository: async shutdown",
+ state: gRepoShutdownState
+ });
+ return state;
+ },
+
+ /**
+ * Shuts down the addon manager and all registered providers, this must clean
+ * up everything in order for automated tests to fake restarts.
+ * @return Promise{null} that resolves when all providers and dependent modules
+ * have finished shutting down
+ */
+ shutdownManager: Task.async(function* () {
+ logger.debug("shutdown");
+ this.callManagerListeners("onShutdown");
+
+ gRepoShutdownState = "pending";
+ gShutdownInProgress = true;
+ // Clean up listeners
+ Services.prefs.removeObserver(PREF_EM_CHECK_COMPATIBILITY, this);
+ Services.prefs.removeObserver(PREF_EM_STRICT_COMPATIBILITY, this);
+ Services.prefs.removeObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
+ Services.prefs.removeObserver(PREF_EM_UPDATE_ENABLED, this);
+ Services.prefs.removeObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
+
+ let savedError = null;
+ // Only shut down providers if they've been started.
+ if (gStarted) {
+ try {
+ yield gShutdownBarrier.wait();
+ }
+ catch(err) {
+ savedError = err;
+ logger.error("Failure during wait for shutdown barrier", err);
+ }
+ }
+
+ // Shut down AddonRepository after providers (if any).
+ try {
+ gRepoShutdownState = "in progress";
+ yield AddonRepository.shutdown();
+ gRepoShutdownState = "done";
+ }
+ catch(err) {
+ savedError = err;
+ logger.error("Failure during AddonRepository shutdown", err);
+ }
+
+ logger.debug("Async provider shutdown done");
+ this.managerListeners.splice(0, this.managerListeners.length);
+ this.installListeners.splice(0, this.installListeners.length);
+ this.addonListeners.splice(0, this.addonListeners.length);
+ this.typeListeners.splice(0, this.typeListeners.length);
+ this.providerShutdowns.clear();
+ for (let type in this.startupChanges)
+ delete this.startupChanges[type];
+ gStarted = false;
+ gStartupComplete = false;
+ gShutdownBarrier = null;
+ gShutdownInProgress = false;
+ if (savedError) {
+ throw savedError;
+ }
+ }),
+
+ /**
+ * Notified when a preference we're interested in has changed.
+ *
+ * @see nsIObserver
+ */
+ observe: function(aSubject, aTopic, aData) {
+ switch (aData) {
+ case PREF_EM_CHECK_COMPATIBILITY: {
+ let oldValue = gCheckCompatibility;
+ gCheckCompatibility = Services.prefs.getBoolPref(PREF_EM_CHECK_COMPATIBILITY, true);
+
+ this.callManagerListeners("onCompatibilityModeChanged");
+
+ if (gCheckCompatibility != oldValue)
+ this.updateAddonAppDisabledStates();
+
+ break;
+ }
+ case PREF_EM_STRICT_COMPATIBILITY: {
+ let oldValue = gStrictCompatibility;
+ gStrictCompatibility = Services.prefs.getBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+
+ this.callManagerListeners("onCompatibilityModeChanged");
+
+ if (gStrictCompatibility != oldValue)
+ this.updateAddonAppDisabledStates();
+
+ break;
+ }
+ case PREF_EM_CHECK_UPDATE_SECURITY: {
+ let oldValue = gCheckUpdateSecurity;
+ gCheckUpdateSecurity = Services.prefs.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, true);
+
+ this.callManagerListeners("onCheckUpdateSecurityChanged");
+
+ if (gCheckUpdateSecurity != oldValue)
+ this.updateAddonAppDisabledStates();
+
+ break;
+ }
+ case PREF_EM_UPDATE_ENABLED: {
+ let oldValue = gUpdateEnabled;
+ gUpdateEnabled = Services.prefs.getBoolPref(PREF_EM_UPDATE_ENABLED, true);
+
+ this.callManagerListeners("onUpdateModeChanged");
+ break;
+ }
+ case PREF_EM_AUTOUPDATE_DEFAULT: {
+ let oldValue = gAutoUpdateDefault;
+ gAutoUpdateDefault = Services.prefs.getBoolPref(PREF_EM_AUTOUPDATE_DEFAULT, true);
+
+ this.callManagerListeners("onUpdateModeChanged");
+ break;
+ }
+ }
+ },
+
+ /**
+ * Replaces %...% strings in an addon url (update and updateInfo) with
+ * appropriate values.
+ *
+ * @param aAddon
+ * The Addon representing the add-on
+ * @param aUri
+ * The string representation of the URI to escape
+ * @param aAppVersion
+ * The optional application version to use for %APP_VERSION%
+ * @return The appropriately escaped URI.
+ */
+ escapeAddonURI: function(aAddon, aUri, aAppVersion)
+ {
+ if (!aAddon || typeof aAddon != "object")
+ throw Components.Exception("aAddon must be an Addon object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!aUri || typeof aUri != "string")
+ throw Components.Exception("aUri must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (aAppVersion && typeof aAppVersion != "string")
+ throw Components.Exception("aAppVersion must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ var addonStatus = aAddon.userDisabled || aAddon.softDisabled ? "userDisabled"
+ : "userEnabled";
+
+ if (!aAddon.isCompatible)
+ addonStatus += ",incompatible";
+ if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
+ addonStatus += ",blocklisted";
+ if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
+ addonStatus += ",softblocked";
+
+ try {
+ var xpcomABI = Services.appinfo.XPCOMABI;
+ } catch (ex) {
+ xpcomABI = UNKNOWN_XPCOM_ABI;
+ }
+
+ let uri = aUri.replace(/%ITEM_ID%/g, aAddon.id);
+ uri = uri.replace(/%ITEM_VERSION%/g, aAddon.version);
+ uri = uri.replace(/%ITEM_STATUS%/g, addonStatus);
+ uri = uri.replace(/%APP_ID%/g, Services.appinfo.ID);
+ uri = uri.replace(/%APP_VERSION%/g, aAppVersion ? aAppVersion :
+ Services.appinfo.version);
+ uri = uri.replace(/%REQ_VERSION%/g, UPDATE_REQUEST_VERSION);
+ uri = uri.replace(/%APP_OS%/g, Services.appinfo.OS);
+ uri = uri.replace(/%APP_ABI%/g, xpcomABI);
+ uri = uri.replace(/%APP_LOCALE%/g, getLocale());
+ uri = uri.replace(/%CURRENT_APP_VERSION%/g, Services.appinfo.version);
+
+ // Replace custom parameters (names of custom parameters must have at
+ // least 3 characters to prevent lookups for something like %D0%C8)
+ var catMan = null;
+ uri = uri.replace(/%(\w{3,})%/g, function parameterReplace(aMatch, aParam) {
+ if (!catMan) {
+ catMan = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ }
+
+ try {
+ var contractID = catMan.getCategoryEntry(CATEGORY_UPDATE_PARAMS, aParam);
+ var paramHandler = Cc[contractID].getService(Ci.nsIPropertyBag2);
+ return paramHandler.getPropertyAsAString(aParam);
+ }
+ catch(e) {
+ return aMatch;
+ }
+ });
+
+ // escape() does not properly encode + symbols in any embedded FVF strings.
+ return uri.replace(/\+/g, "%2B");
+ },
+
+ /**
+ * Performs a background update check by starting an update for all add-ons
+ * that can be updated.
+ * @return Promise{null} Resolves when the background update check is complete
+ * (the resulting addon installations may still be in progress).
+ */
+ backgroundUpdateCheck: function() {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ let buPromise = Task.spawn(function* backgroundUpdateTask() {
+ logger.debug("Background update check beginning");
+
+ Services.obs.notifyObservers(null, "addons-background-update-start", null);
+
+ if (this.updateEnabled) {
+ let scope = {};
+ Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", scope);
+ scope.LightweightThemeManager.updateCurrentTheme();
+
+ let allAddons = yield new Promise((resolve, reject) => this.getAllAddons(resolve));
+
+ // Repopulate repository cache first, to ensure compatibility overrides
+ // are up to date before checking for addon updates.
+ yield AddonRepository.backgroundUpdateCheck();
+
+ // Keep track of all the async add-on updates happening in parallel
+ let updates = [];
+
+ for (let addon of allAddons) {
+ // Check all add-ons for updates so that any compatibility updates will
+ // be applied
+ updates.push(new Promise((resolve, reject) => {
+ addon.findUpdates({
+ onUpdateAvailable: function(aAddon, aInstall) {
+ // Start installing updates when the add-on can be updated and
+ // background updates should be applied.
+ logger.debug("Found update for add-on ${id}", aAddon);
+ if (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE &&
+ AddonManager.shouldAutoUpdate(aAddon)) {
+ // XXX we really should resolve when this install is done,
+ // not when update-available check completes, no?
+ logger.debug("Starting install of ${id}", aAddon);
+ aInstall.install();
+ }
+ },
+
+ onUpdateFinished: aAddon => { logger.debug("onUpdateFinished for ${id}", aAddon); resolve(); }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ }));
+ }
+ yield Promise.all(updates);
+ }
+
+ logger.debug("Background update check complete");
+ Services.obs.notifyObservers(null,
+ "addons-background-update-complete",
+ null);
+ }.bind(this));
+ // Fork the promise chain so we can log the error and let our caller see it too.
+ buPromise.then(null, e => logger.warn("Error in background update", e));
+ return buPromise;
+ },
+
+ /**
+ * Adds a add-on to the list of detected changes for this startup. If
+ * addStartupChange is called multiple times for the same add-on in the same
+ * startup then only the most recent change will be remembered.
+ *
+ * @param aType
+ * The type of change as a string. Providers can define their own
+ * types of changes or use the existing defined STARTUP_CHANGE_*
+ * constants
+ * @param aID
+ * The ID of the add-on
+ */
+ addStartupChange: function(aType, aID) {
+ if (!aType || typeof aType != "string")
+ throw Components.Exception("aType must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!aID || typeof aID != "string")
+ throw Components.Exception("aID must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (gStartupComplete)
+ return;
+
+ // Ensure that an ID is only listed in one type of change
+ for (let type in this.startupChanges)
+ this.removeStartupChange(type, aID);
+
+ if (!(aType in this.startupChanges))
+ this.startupChanges[aType] = [];
+ this.startupChanges[aType].push(aID);
+ },
+
+ /**
+ * Removes a startup change for an add-on.
+ *
+ * @param aType
+ * The type of change
+ * @param aID
+ * The ID of the add-on
+ */
+ removeStartupChange: function(aType, aID) {
+ if (!aType || typeof aType != "string")
+ throw Components.Exception("aType must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!aID || typeof aID != "string")
+ throw Components.Exception("aID must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (gStartupComplete)
+ return;
+
+ if (!(aType in this.startupChanges))
+ return;
+
+ this.startupChanges[aType] = this.startupChanges[aType].filter(
+ function filterItem(aItem) aItem != aID);
+ },
+
+ /**
+ * Calls all registered AddonManagerListeners with an event. Any parameters
+ * after the method parameter are passed to the listener.
+ *
+ * @param aMethod
+ * The method on the listeners to call
+ */
+ callManagerListeners: function(aMethod, ...aArgs) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!aMethod || typeof aMethod != "string")
+ throw Components.Exception("aMethod must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let managerListeners = this.managerListeners.slice(0);
+ for (let listener of managerListeners) {
+ try {
+ if (aMethod in listener)
+ listener[aMethod].apply(listener, aArgs);
+ }
+ catch (e) {
+ logger.warn("AddonManagerListener threw exception when calling " + aMethod, e);
+ }
+ }
+ },
+
+ /**
+ * Calls all registered InstallListeners with an event. Any parameters after
+ * the extraListeners parameter are passed to the listener.
+ *
+ * @param aMethod
+ * The method on the listeners to call
+ * @param aExtraListeners
+ * An optional array of extra InstallListeners to also call
+ * @return false if any of the listeners returned false, true otherwise
+ */
+ callInstallListeners: function(aMethod,
+ aExtraListeners, ...aArgs) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!aMethod || typeof aMethod != "string")
+ throw Components.Exception("aMethod must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (aExtraListeners && !Array.isArray(aExtraListeners))
+ throw Components.Exception("aExtraListeners must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let result = true;
+ let listeners;
+ if (aExtraListeners)
+ listeners = aExtraListeners.concat(this.installListeners);
+ else
+ listeners = this.installListeners.slice(0);
+
+ for (let listener of listeners) {
+ try {
+ if (aMethod in listener) {
+ if (listener[aMethod].apply(listener, aArgs) === false)
+ result = false;
+ }
+ }
+ catch (e) {
+ logger.warn("InstallListener threw exception when calling " + aMethod, e);
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Calls all registered AddonListeners with an event. Any parameters after
+ * the method parameter are passed to the listener.
+ *
+ * @param aMethod
+ * The method on the listeners to call
+ */
+ callAddonListeners: function(aMethod, ...aArgs) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!aMethod || typeof aMethod != "string")
+ throw Components.Exception("aMethod must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let addonListeners = this.addonListeners.slice(0);
+ for (let listener of addonListeners) {
+ try {
+ if (aMethod in listener)
+ listener[aMethod].apply(listener, aArgs);
+ }
+ catch (e) {
+ logger.warn("AddonListener threw exception when calling " + aMethod, e);
+ }
+ }
+ },
+
+ /**
+ * Notifies all providers that an add-on has been enabled when that type of
+ * add-on only supports a single add-on being enabled at a time. This allows
+ * the providers to disable theirs if necessary.
+ *
+ * @param aID
+ * The ID of the enabled add-on
+ * @param aType
+ * The type of the enabled add-on
+ * @param aPendingRestart
+ * A boolean indicating if the change will only take place the next
+ * time the application is restarted
+ */
+ notifyAddonChanged: function(aID, aType, aPendingRestart) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (aID && typeof aID != "string")
+ throw Components.Exception("aID must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!aType || typeof aType != "string")
+ throw Components.Exception("aType must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ // Temporary hack until bug 520124 lands.
+ // We can get here during synchronous startup, at which point it's
+ // considered unsafe (and therefore disallowed by AddonManager.jsm) to
+ // access providers that haven't been initialized yet. Since this is when
+ // XPIProvider is starting up, XPIProvider can't access itself via APIs
+ // going through AddonManager.jsm. Furthermore, LightweightThemeManager may
+ // not be initialized until after XPIProvider is, and therefore would also
+ // be unaccessible during XPIProvider startup. Thankfully, these are the
+ // only two uses of this API, and we know it's safe to use this API with
+ // both providers; so we have this hack to allow bypassing the normal
+ // safetey guard.
+ // The notifyAddonChanged/addonChanged API will be unneeded and therefore
+ // removed by bug 520124, so this is a temporary quick'n'dirty hack.
+ let providers = [...this.providers, ...this.pendingProviders];
+ for (let provider of providers) {
+ callProvider(provider, "addonChanged", null, aID, aType, aPendingRestart);
+ }
+ },
+
+ /**
+ * Notifies all providers they need to update the appDisabled property for
+ * their add-ons in response to an application change such as a blocklist
+ * update.
+ */
+ updateAddonAppDisabledStates: function() {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ this.callProviders("updateAddonAppDisabledStates");
+ },
+
+ /**
+ * Notifies all providers that the repository has updated its data for
+ * installed add-ons.
+ *
+ * @param aCallback
+ * Function to call when operation is complete.
+ */
+ updateAddonRepositoryData: function(aCallback) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ new AsyncObjectCaller(this.providers, "updateAddonRepositoryData", {
+ nextObject: function(aCaller, aProvider) {
+ callProviderAsync(aProvider, "updateAddonRepositoryData",
+ aCaller.callNext.bind(aCaller));
+ },
+ noMoreObjects: function(aCaller) {
+ safeCall(aCallback);
+ // only tests should care about this
+ Services.obs.notifyObservers(null, "TEST:addon-repository-data-updated", null);
+ }
+ });
+ },
+
+ /**
+ * Asynchronously gets an AddonInstall for a URL.
+ *
+ * @param aUrl
+ * The string represenation of the URL the add-on is located at
+ * @param aCallback
+ * A callback to pass the AddonInstall to
+ * @param aMimetype
+ * The mimetype of the add-on
+ * @param aHash
+ * An optional hash of the add-on
+ * @param aName
+ * An optional placeholder name while the add-on is being downloaded
+ * @param aIcons
+ * Optional placeholder icons while the add-on is being downloaded
+ * @param aVersion
+ * An optional placeholder version while the add-on is being downloaded
+ * @param aLoadGroup
+ * An optional nsILoadGroup to associate any network requests with
+ * @throws if the aUrl, aCallback or aMimetype arguments are not specified
+ */
+ getInstallForURL: function(aUrl, aCallback, aMimetype,
+ aHash, aName, aIcons,
+ aVersion, aBrowser) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!aUrl || typeof aUrl != "string")
+ throw Components.Exception("aURL must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!aMimetype || typeof aMimetype != "string")
+ throw Components.Exception("aMimetype must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (aHash && typeof aHash != "string")
+ throw Components.Exception("aHash must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (aName && typeof aName != "string")
+ throw Components.Exception("aName must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (aIcons) {
+ if (typeof aIcons == "string")
+ aIcons = { "32": aIcons };
+ else if (typeof aIcons != "object")
+ throw Components.Exception("aIcons must be a string, an object or null",
+ Cr.NS_ERROR_INVALID_ARG);
+ } else {
+ aIcons = {};
+ }
+
+ if (aVersion && typeof aVersion != "string")
+ throw Components.Exception("aVersion must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (aBrowser && (!(aBrowser instanceof Ci.nsIDOMElement)))
+ throw Components.Exception("aBrowser must be a nsIDOMElement or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let providers = [...this.providers];
+ for (let provider of providers) {
+ if (callProvider(provider, "supportsMimetype", false, aMimetype)) {
+ callProviderAsync(provider, "getInstallForURL",
+ aUrl, aHash, aName, aIcons, aVersion, aBrowser,
+ function getInstallForURL_safeCall(aInstall) {
+ safeCall(aCallback, aInstall);
+ });
+ return;
+ }
+ }
+ safeCall(aCallback, null);
+ },
+
+ /**
+ * Asynchronously gets an AddonInstall for an nsIFile.
+ *
+ * @param aFile
+ * The nsIFile where the add-on is located
+ * @param aCallback
+ * A callback to pass the AddonInstall to
+ * @param aMimetype
+ * An optional mimetype hint for the add-on
+ * @throws if the aFile or aCallback arguments are not specified
+ */
+ getInstallForFile: function(aFile, aCallback, aMimetype) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!(aFile instanceof Ci.nsIFile))
+ throw Components.Exception("aFile must be a nsIFile",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (aMimetype && typeof aMimetype != "string")
+ throw Components.Exception("aMimetype must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ new AsyncObjectCaller(this.providers, "getInstallForFile", {
+ nextObject: function(aCaller, aProvider) {
+ callProviderAsync(aProvider, "getInstallForFile", aFile,
+ function getInstallForFile_safeCall(aInstall) {
+ if (aInstall)
+ safeCall(aCallback, aInstall);
+ else
+ aCaller.callNext();
+ });
+ },
+
+ noMoreObjects: function(aCaller) {
+ safeCall(aCallback, null);
+ }
+ });
+ },
+
+ /**
+ * Asynchronously gets all current AddonInstalls optionally limiting to a list
+ * of types.
+ *
+ * @param aTypes
+ * An optional array of types to retrieve. Each type is a string name
+ * @param aCallback
+ * A callback which will be passed an array of AddonInstalls
+ * @throws If the aCallback argument is not specified
+ */
+ getInstallsByTypes: function(aTypes, aCallback) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (aTypes && !Array.isArray(aTypes))
+ throw Components.Exception("aTypes must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let installs = [];
+
+ new AsyncObjectCaller(this.providers, "getInstallsByTypes", {
+ nextObject: function(aCaller, aProvider) {
+ callProviderAsync(aProvider, "getInstallsByTypes", aTypes,
+ function getInstallsByTypes_safeCall(aProviderInstalls) {
+ if (aProviderInstalls) {
+ installs = installs.concat(aProviderInstalls);
+ }
+ aCaller.callNext();
+ });
+ },
+
+ noMoreObjects: function(aCaller) {
+ safeCall(aCallback, installs);
+ }
+ });
+ },
+
+ /**
+ * Asynchronously gets all current AddonInstalls.
+ *
+ * @param aCallback
+ * A callback which will be passed an array of AddonInstalls
+ */
+ getAllInstalls: function(aCallback) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ this.getInstallsByTypes(null, aCallback);
+ },
+
+ /**
+ * Synchronously map a URI to the corresponding Addon ID.
+ *
+ * Mappable URIs are limited to in-application resources belonging to the
+ * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
+ * but do not include URIs from meta data, such as the add-on homepage.
+ *
+ * @param aURI
+ * nsIURI to map to an addon id
+ * @return string containing the Addon ID or null
+ * @see amIAddonManager.mapURIToAddonID
+ */
+ mapURIToAddonID: function(aURI) {
+ if (!(aURI instanceof Ci.nsIURI)) {
+ throw Components.Exception("aURI is not a nsIURI",
+ Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Try all providers
+ let providers = [...this.providers];
+ for (let provider of providers) {
+ var id = callProvider(provider, "mapURIToAddonID", null, aURI);
+ if (id !== null) {
+ return id;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Checks whether installation is enabled for a particular mimetype.
+ *
+ * @param aMimetype
+ * The mimetype to check
+ * @return true if installation is enabled for the mimetype
+ */
+ isInstallEnabled: function(aMimetype) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!aMimetype || typeof aMimetype != "string")
+ throw Components.Exception("aMimetype must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let providers = [...this.providers];
+ for (let provider of providers) {
+ if (callProvider(provider, "supportsMimetype", false, aMimetype) &&
+ callProvider(provider, "isInstallEnabled"))
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Checks whether a particular source is allowed to install add-ons of a
+ * given mimetype.
+ *
+ * @param aMimetype
+ * The mimetype of the add-on
+ * @param aInstallingPrincipal
+ * The nsIPrincipal that initiated the install
+ * @return true if the source is allowed to install this mimetype
+ */
+ isInstallAllowed: function(aMimetype, aInstallingPrincipal) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!aMimetype || typeof aMimetype != "string")
+ throw Components.Exception("aMimetype must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!aInstallingPrincipal || !(aInstallingPrincipal instanceof Ci.nsIPrincipal))
+ throw Components.Exception("aInstallingPrincipal must be a nsIPrincipal",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let providers = [...this.providers];
+ for (let provider of providers) {
+ if (callProvider(provider, "supportsMimetype", false, aMimetype) &&
+ callProvider(provider, "isInstallAllowed", null, aInstallingPrincipal))
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Starts installation of an array of AddonInstalls notifying the registered
+ * web install listener of blocked or started installs.
+ *
+ * @param aMimetype
+ * The mimetype of add-ons being installed
+ * @param aBrowser
+ * The optional browser element that started the installs
+ * @param aInstallingPrincipal
+ * The nsIPrincipal that initiated the install
+ * @param aInstalls
+ * The array of AddonInstalls to be installed
+ */
+ installAddonsFromWebpage: function(aMimetype, aBrowser, aInstallingPrincipal, aInstalls) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!aMimetype || typeof aMimetype != "string")
+ throw Components.Exception("aMimetype must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (aBrowser && !(aBrowser instanceof Ci.nsIDOMElement))
+ throw Components.Exception("aSource must be a nsIDOMElement, or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!aInstallingPrincipal || !(aInstallingPrincipal instanceof Ci.nsIPrincipal))
+ throw Components.Exception("aInstallingPrincipal must be a nsIPrincipal",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!Array.isArray(aInstalls))
+ throw Components.Exception("aInstalls must be an array",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!("@mozilla.org/addons/web-install-listener;1" in Cc)) {
+ logger.warn("No web installer available, cancelling all installs");
+ aInstalls.forEach(function(aInstall) {
+ aInstall.cancel();
+ });
+ return;
+ }
+
+ // When a chrome in-content UI has loaded a <browser> inside to host a
+ // website we want to do our security checks on the inner-browser but
+ // notify front-end that install events came from the outer-browser (the
+ // main tab's browser). Check this by seeing if the browser we've been
+ // passed is in a content type docshell and if so get the outer-browser.
+ let topBrowser = aBrowser;
+ let docShell = aBrowser.ownerDocument.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIDocShellTreeItem);
+ if (docShell.itemType == Ci.nsIDocShellTreeItem.typeContent)
+ topBrowser = docShell.chromeEventHandler;
+
+ try {
+ let weblistener = Cc["@mozilla.org/addons/web-install-listener;1"].
+ getService(Ci.amIWebInstallListener);
+
+ if (!this.isInstallEnabled(aMimetype)) {
+ for (let install of aInstalls)
+ install.cancel();
+
+ weblistener.onWebInstallDisabled(topBrowser, aInstallingPrincipal.URI,
+ aInstalls, aInstalls.length);
+ return;
+ }
+ else if (!aBrowser.contentPrincipal || !aInstallingPrincipal.subsumes(aBrowser.contentPrincipal)) {
+ for (let install of aInstalls)
+ install.cancel();
+
+ if (weblistener instanceof Ci.amIWebInstallListener2) {
+ weblistener.onWebInstallOriginBlocked(topBrowser, aInstallingPrincipal.URI,
+ aInstalls, aInstalls.length);
+ }
+ return;
+ }
+
+ // The installs may start now depending on the web install listener,
+ // listen for the browser navigating to a new origin and cancel the
+ // installs in that case.
+ new BrowserListener(aBrowser, aInstallingPrincipal, aInstalls);
+
+ if (!this.isInstallAllowed(aMimetype, aInstallingPrincipal)) {
+ if (weblistener.onWebInstallBlocked(topBrowser, aInstallingPrincipal.URI,
+ aInstalls, aInstalls.length)) {
+ aInstalls.forEach(function(aInstall) {
+ aInstall.install();
+ });
+ }
+ }
+ else if (weblistener.onWebInstallRequested(topBrowser, aInstallingPrincipal.URI,
+ aInstalls, aInstalls.length)) {
+ aInstalls.forEach(function(aInstall) {
+ aInstall.install();
+ });
+ }
+ }
+ catch (e) {
+ // In the event that the weblistener throws during instantiation or when
+ // calling onWebInstallBlocked or onWebInstallRequested all of the
+ // installs should get cancelled.
+ logger.warn("Failure calling web installer", e);
+ aInstalls.forEach(function(aInstall) {
+ aInstall.cancel();
+ });
+ }
+ },
+
+ /**
+ * Adds a new InstallListener if the listener is not already registered.
+ *
+ * @param aListener
+ * The InstallListener to add
+ */
+ addInstallListener: function(aListener) {
+ if (!aListener || typeof aListener != "object")
+ throw Components.Exception("aListener must be a InstallListener object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!this.installListeners.some(function addInstallListener_matchListener(i) {
+ return i == aListener; }))
+ this.installListeners.push(aListener);
+ },
+
+ /**
+ * Removes an InstallListener if the listener is registered.
+ *
+ * @param aListener
+ * The InstallListener to remove
+ */
+ removeInstallListener: function(aListener) {
+ if (!aListener || typeof aListener != "object")
+ throw Components.Exception("aListener must be a InstallListener object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let pos = 0;
+ while (pos < this.installListeners.length) {
+ if (this.installListeners[pos] == aListener)
+ this.installListeners.splice(pos, 1);
+ else
+ pos++;
+ }
+ },
+
+ /**
+ * Asynchronously gets an add-on with a specific ID.
+ *
+ * @param aID
+ * The ID of the add-on to retrieve
+ * @param aCallback
+ * The callback to pass the retrieved add-on to
+ * @throws if the aID or aCallback arguments are not specified
+ */
+ getAddonByID: function(aID, aCallback) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!aID || typeof aID != "string")
+ throw Components.Exception("aID must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ new AsyncObjectCaller(this.providers, "getAddonByID", {
+ nextObject: function(aCaller, aProvider) {
+ callProviderAsync(aProvider, "getAddonByID", aID,
+ function getAddonByID_safeCall(aAddon) {
+ if (aAddon)
+ safeCall(aCallback, aAddon);
+ else
+ aCaller.callNext();
+ });
+ },
+
+ noMoreObjects: function(aCaller) {
+ safeCall(aCallback, null);
+ }
+ });
+ },
+
+ /**
+ * Asynchronously get an add-on with a specific Sync GUID.
+ *
+ * @param aGUID
+ * String GUID of add-on to retrieve
+ * @param aCallback
+ * The callback to pass the retrieved add-on to.
+ * @throws if the aGUID or aCallback arguments are not specified
+ */
+ getAddonBySyncGUID: function(aGUID, aCallback) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!aGUID || typeof aGUID != "string")
+ throw Components.Exception("aGUID must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ new AsyncObjectCaller(this.providers, "getAddonBySyncGUID", {
+ nextObject: function(aCaller, aProvider) {
+ callProviderAsync(aProvider, "getAddonBySyncGUID", aGUID,
+ function getAddonBySyncGUID_safeCall(aAddon) {
+ if (aAddon) {
+ safeCall(aCallback, aAddon);
+ } else {
+ aCaller.callNext();
+ }
+ });
+ },
+
+ noMoreObjects: function(aCaller) {
+ safeCall(aCallback, null);
+ }
+ });
+ },
+
+ /**
+ * Asynchronously gets an array of add-ons.
+ *
+ * @param aIDs
+ * The array of IDs to retrieve
+ * @param aCallback
+ * The callback to pass an array of Addons to
+ * @throws if the aID or aCallback arguments are not specified
+ */
+ getAddonsByIDs: function(aIDs, aCallback) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (!Array.isArray(aIDs))
+ throw Components.Exception("aIDs must be an array",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let addons = [];
+
+ new AsyncObjectCaller(aIDs, null, {
+ nextObject: function(aCaller, aID) {
+ AddonManagerInternal.getAddonByID(aID,
+ function getAddonsByIDs_getAddonByID(aAddon) {
+ addons.push(aAddon);
+ aCaller.callNext();
+ });
+ },
+
+ noMoreObjects: function(aCaller) {
+ safeCall(aCallback, addons);
+ }
+ });
+ },
+
+ /**
+ * Asynchronously gets add-ons of specific types.
+ *
+ * @param aTypes
+ * An optional array of types to retrieve. Each type is a string name
+ * @param aCallback
+ * The callback to pass an array of Addons to.
+ * @throws if the aCallback argument is not specified
+ */
+ getAddonsByTypes: function(aTypes, aCallback) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (aTypes && !Array.isArray(aTypes))
+ throw Components.Exception("aTypes must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let addons = [];
+
+ new AsyncObjectCaller(this.providers, "getAddonsByTypes", {
+ nextObject: function(aCaller, aProvider) {
+ callProviderAsync(aProvider, "getAddonsByTypes", aTypes,
+ function getAddonsByTypes_concatAddons(aProviderAddons) {
+ if (aProviderAddons) {
+ addons = addons.concat(aProviderAddons);
+ }
+ aCaller.callNext();
+ });
+ },
+
+ noMoreObjects: function(aCaller) {
+ safeCall(aCallback, addons);
+ }
+ });
+ },
+
+ /**
+ * Asynchronously gets all installed add-ons.
+ *
+ * @param aCallback
+ * A callback which will be passed an array of Addons
+ */
+ getAllAddons: function(aCallback) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ this.getAddonsByTypes(null, aCallback);
+ },
+
+ /**
+ * Asynchronously gets add-ons that have operations waiting for an application
+ * restart to complete.
+ *
+ * @param aTypes
+ * An optional array of types to retrieve. Each type is a string name
+ * @param aCallback
+ * The callback to pass the array of Addons to
+ * @throws if the aCallback argument is not specified
+ */
+ getAddonsWithOperationsByTypes: function(aTypes, aCallback) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ if (aTypes && !Array.isArray(aTypes))
+ throw Components.Exception("aTypes must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let addons = [];
+
+ new AsyncObjectCaller(this.providers, "getAddonsWithOperationsByTypes", {
+ nextObject: function(aCaller, aProvider) {
+ callProviderAsync(aProvider, "getAddonsWithOperationsByTypes", aTypes,
+ function getAddonsWithOperationsByTypes_concatAddons
+ (aProviderAddons) {
+ if (aProviderAddons) {
+ addons = addons.concat(aProviderAddons);
+ }
+ aCaller.callNext();
+ });
+ },
+
+ noMoreObjects: function(caller) {
+ safeCall(aCallback, addons);
+ }
+ });
+ },
+
+ /**
+ * Adds a new AddonManagerListener if the listener is not already registered.
+ *
+ * @param aListener
+ * The listener to add
+ */
+ addManagerListener: function(aListener) {
+ if (!aListener || typeof aListener != "object")
+ throw Components.Exception("aListener must be an AddonManagerListener object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!this.managerListeners.some(function addManagerListener_matchListener(i) {
+ return i == aListener; }))
+ this.managerListeners.push(aListener);
+ },
+
+ /**
+ * Removes an AddonManagerListener if the listener is registered.
+ *
+ * @param aListener
+ * The listener to remove
+ */
+ removeManagerListener: function(aListener) {
+ if (!aListener || typeof aListener != "object")
+ throw Components.Exception("aListener must be an AddonManagerListener object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let pos = 0;
+ while (pos < this.managerListeners.length) {
+ if (this.managerListeners[pos] == aListener)
+ this.managerListeners.splice(pos, 1);
+ else
+ pos++;
+ }
+ },
+
+ /**
+ * Adds a new AddonListener if the listener is not already registered.
+ *
+ * @param aListener
+ * The AddonListener to add
+ */
+ addAddonListener: function(aListener) {
+ if (!aListener || typeof aListener != "object")
+ throw Components.Exception("aListener must be an AddonListener object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!this.addonListeners.some(function addAddonListener_matchListener(i) {
+ return i == aListener; }))
+ this.addonListeners.push(aListener);
+ },
+
+ /**
+ * Removes an AddonListener if the listener is registered.
+ *
+ * @param aListener
+ * The AddonListener to remove
+ */
+ removeAddonListener: function(aListener) {
+ if (!aListener || typeof aListener != "object")
+ throw Components.Exception("aListener must be an AddonListener object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let pos = 0;
+ while (pos < this.addonListeners.length) {
+ if (this.addonListeners[pos] == aListener)
+ this.addonListeners.splice(pos, 1);
+ else
+ pos++;
+ }
+ },
+
+ /**
+ * Adds a new TypeListener if the listener is not already registered.
+ *
+ * @param aListener
+ * The TypeListener to add
+ */
+ addTypeListener: function(aListener) {
+ if (!aListener || typeof aListener != "object")
+ throw Components.Exception("aListener must be a TypeListener object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!this.typeListeners.some(function addTypeListener_matchListener(i) {
+ return i == aListener; }))
+ this.typeListeners.push(aListener);
+ },
+
+ /**
+ * Removes an TypeListener if the listener is registered.
+ *
+ * @param aListener
+ * The TypeListener to remove
+ */
+ removeTypeListener: function(aListener) {
+ if (!aListener || typeof aListener != "object")
+ throw Components.Exception("aListener must be a TypeListener object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ let pos = 0;
+ while (pos < this.typeListeners.length) {
+ if (this.typeListeners[pos] == aListener)
+ this.typeListeners.splice(pos, 1);
+ else
+ pos++;
+ }
+ },
+
+ get addonTypes() {
+ // A read-only wrapper around the types dictionary
+ return new Proxy(this.types, {
+ defineProperty(target, property, descriptor) {
+ // Not allowed to define properties
+ return false;
+ },
+
+ deleteProperty(target, property) {
+ // Not allowed to delete properties
+ return false;
+ },
+
+ get(target, property, receiver) {
+ if (!target.hasOwnProperty(property))
+ return undefined;
+
+ return target[property].type;
+ },
+
+ getOwnPropertyDescriptor(target, property) {
+ if (!target.hasOwnProperty(property))
+ return undefined;
+
+ return {
+ value: target[property].type,
+ writable: false,
+ // Claim configurability to maintain the proxy invariants.
+ configurable: true,
+ enumerable: true
+ }
+ },
+
+ preventExtensions(target) {
+ // Not allowed to prevent adding new properties
+ return false;
+ },
+
+ set(target, property, value, receiver) {
+ // Not allowed to set properties
+ return false;
+ },
+
+ setPrototypeOf(target, prototype) {
+ // Not allowed to change prototype
+ return false;
+ }
+ });
+ },
+
+ get autoUpdateDefault() {
+ return gAutoUpdateDefault;
+ },
+
+ set autoUpdateDefault(aValue) {
+ aValue = !!aValue;
+ if (aValue != gAutoUpdateDefault)
+ Services.prefs.setBoolPref(PREF_EM_AUTOUPDATE_DEFAULT, aValue);
+ return aValue;
+ },
+
+ get checkCompatibility() {
+ return gCheckCompatibility;
+ },
+
+ set checkCompatibility(aValue) {
+ aValue = !!aValue;
+ if (aValue != gCheckCompatibility) {
+ if (!aValue)
+ Services.prefs.setBoolPref(PREF_EM_CHECK_COMPATIBILITY, false);
+ else
+ Services.prefs.clearUserPref(PREF_EM_CHECK_COMPATIBILITY);
+ }
+ return aValue;
+ },
+
+ get strictCompatibility() {
+ return gStrictCompatibility;
+ },
+
+ set strictCompatibility(aValue) {
+ aValue = !!aValue;
+ if (aValue != gStrictCompatibility)
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, aValue);
+ return aValue;
+ },
+
+ get checkUpdateSecurityDefault() {
+ return gCheckUpdateSecurityDefault;
+ },
+
+ get checkUpdateSecurity() {
+ return gCheckUpdateSecurity;
+ },
+
+ set checkUpdateSecurity(aValue) {
+ aValue = !!aValue;
+ if (aValue != gCheckUpdateSecurity) {
+ if (aValue != gCheckUpdateSecurityDefault)
+ Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, aValue);
+ else
+ Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY);
+ }
+ return aValue;
+ },
+
+ get updateEnabled() {
+ return gUpdateEnabled;
+ },
+
+ set updateEnabled(aValue) {
+ aValue = !!aValue;
+ if (aValue != gUpdateEnabled)
+ Services.prefs.setBoolPref(PREF_EM_UPDATE_ENABLED, aValue);
+ return aValue;
+ },
+};
+
+/**
+ * Should not be used outside of core Mozilla code. This is a private API for
+ * the startup and platform integration code to use. Refer to the methods on
+ * AddonManagerInternal for documentation however note that these methods are
+ * subject to change at any time.
+ */
+this.AddonManagerPrivate = {
+ startup: function() {
+ AddonManagerInternal.startup();
+ },
+
+ registerProvider: function(aProvider, aTypes) {
+ AddonManagerInternal.registerProvider(aProvider, aTypes);
+ },
+
+ unregisterProvider: function(aProvider) {
+ AddonManagerInternal.unregisterProvider(aProvider);
+ },
+
+ markProviderSafe: function(aProvider) {
+ AddonManagerInternal.markProviderSafe(aProvider);
+ },
+
+ backgroundUpdateCheck: function() {
+ return AddonManagerInternal.backgroundUpdateCheck();
+ },
+
+ backgroundUpdateTimerHandler() {
+ // Don't call through to the real update check if no checks are enabled.
+ if (!AddonManagerInternal.updateEnabled) {
+ logger.info("Skipping background update check");
+ return;
+ }
+ // Don't return the promise here, since the caller doesn't care.
+ AddonManagerInternal.backgroundUpdateCheck();
+ },
+
+ addStartupChange: function(aType, aID) {
+ AddonManagerInternal.addStartupChange(aType, aID);
+ },
+
+ removeStartupChange: function(aType, aID) {
+ AddonManagerInternal.removeStartupChange(aType, aID);
+ },
+
+ notifyAddonChanged: function(aID, aType, aPendingRestart) {
+ AddonManagerInternal.notifyAddonChanged(aID, aType, aPendingRestart);
+ },
+
+ updateAddonAppDisabledStates: function() {
+ AddonManagerInternal.updateAddonAppDisabledStates();
+ },
+
+ updateAddonRepositoryData: function(aCallback) {
+ AddonManagerInternal.updateAddonRepositoryData(aCallback);
+ },
+
+ callInstallListeners: function(...aArgs) {
+ return AddonManagerInternal.callInstallListeners.apply(AddonManagerInternal,
+ aArgs);
+ },
+
+ callAddonListeners: function(...aArgs) {
+ AddonManagerInternal.callAddonListeners.apply(AddonManagerInternal, aArgs);
+ },
+
+ AddonAuthor: AddonAuthor,
+
+ AddonScreenshot: AddonScreenshot,
+
+ AddonCompatibilityOverride: AddonCompatibilityOverride,
+
+ AddonType: AddonType,
+
+ /**
+ * Helper to call update listeners when no update is available.
+ *
+ * This can be used as an implementation for Addon.findUpdates() when
+ * no update mechanism is available.
+ */
+ callNoUpdateListeners: function(addon, listener, reason, appVersion, platformVersion) {
+ if ("onNoCompatibilityUpdateAvailable" in listener) {
+ safeCall(listener.onNoCompatibilityUpdateAvailable.bind(listener), addon);
+ }
+ if ("onNoUpdateAvailable" in listener) {
+ safeCall(listener.onNoUpdateAvailable.bind(listener), addon);
+ }
+ if ("onUpdateFinished" in listener) {
+ safeCall(listener.onUpdateFinished.bind(listener), addon);
+ }
+ },
+};
+
+/**
+ * This is the public API that UI and developers should be calling. All methods
+ * just forward to AddonManagerInternal.
+ */
+this.AddonManager = {
+ // Constants for the AddonInstall.state property
+ // The install is available for download.
+ STATE_AVAILABLE: 0,
+ // The install is being downloaded.
+ STATE_DOWNLOADING: 1,
+ // The install is checking for compatibility information.
+ STATE_CHECKING: 2,
+ // The install is downloaded and ready to install.
+ STATE_DOWNLOADED: 3,
+ // The download failed.
+ STATE_DOWNLOAD_FAILED: 4,
+ // The add-on is being installed.
+ STATE_INSTALLING: 5,
+ // The add-on has been installed.
+ STATE_INSTALLED: 6,
+ // The install failed.
+ STATE_INSTALL_FAILED: 7,
+ // The install has been cancelled.
+ STATE_CANCELLED: 8,
+
+ // Constants representing different types of errors while downloading an
+ // add-on.
+ // The download failed due to network problems.
+ ERROR_NETWORK_FAILURE: -1,
+ // The downloaded file did not match the provided hash.
+ ERROR_INCORRECT_HASH: -2,
+ // The downloaded file seems to be corrupted in some way.
+ ERROR_CORRUPT_FILE: -3,
+ // An error occured trying to write to the filesystem.
+ ERROR_FILE_ACCESS: -4,
+ // The downloaded file seems to be Jetpack.
+ ERROR_JETPACKSDK_FILE: -8,
+ // The downloaded file seems to be WebExtension.
+ ERROR_WEBEXT_FILE: -9,
+
+ // These must be kept in sync with AddonUpdateChecker.
+ // No error was encountered.
+ UPDATE_STATUS_NO_ERROR: 0,
+ // The update check timed out
+ UPDATE_STATUS_TIMEOUT: -1,
+ // There was an error while downloading the update information.
+ UPDATE_STATUS_DOWNLOAD_ERROR: -2,
+ // The update information was malformed in some way.
+ UPDATE_STATUS_PARSE_ERROR: -3,
+ // The update information was not in any known format.
+ UPDATE_STATUS_UNKNOWN_FORMAT: -4,
+ // The update information was not correctly signed or there was an SSL error.
+ UPDATE_STATUS_SECURITY_ERROR: -5,
+ // The update was cancelled.
+ UPDATE_STATUS_CANCELLED: -6,
+
+ // Constants to indicate why an update check is being performed
+ // Update check has been requested by the user.
+ UPDATE_WHEN_USER_REQUESTED: 1,
+ // Update check is necessary to see if the Addon is compatibile with a new
+ // version of the application.
+ UPDATE_WHEN_NEW_APP_DETECTED: 2,
+ // Update check is necessary because a new application has been installed.
+ UPDATE_WHEN_NEW_APP_INSTALLED: 3,
+ // Update check is a regular background update check.
+ UPDATE_WHEN_PERIODIC_UPDATE: 16,
+ // Update check is needed to check an Addon that is being installed.
+ UPDATE_WHEN_ADDON_INSTALLED: 17,
+
+ // Constants for operations in Addon.pendingOperations
+ // Indicates that the Addon has no pending operations.
+ PENDING_NONE: 0,
+ // Indicates that the Addon will be enabled after the application restarts.
+ PENDING_ENABLE: 1,
+ // Indicates that the Addon will be disabled after the application restarts.
+ PENDING_DISABLE: 2,
+ // Indicates that the Addon will be uninstalled after the application restarts.
+ PENDING_UNINSTALL: 4,
+ // Indicates that the Addon will be installed after the application restarts.
+ PENDING_INSTALL: 8,
+ PENDING_UPGRADE: 16,
+
+ // Constants for operations in Addon.operationsRequiringRestart
+ // Indicates that restart isn't required for any operation.
+ OP_NEEDS_RESTART_NONE: 0,
+ // Indicates that restart is required for enabling the addon.
+ OP_NEEDS_RESTART_ENABLE: 1,
+ // Indicates that restart is required for disabling the addon.
+ OP_NEEDS_RESTART_DISABLE: 2,
+ // Indicates that restart is required for uninstalling the addon.
+ OP_NEEDS_RESTART_UNINSTALL: 4,
+ // Indicates that restart is required for installing the addon.
+ OP_NEEDS_RESTART_INSTALL: 8,
+
+ // Constants for permissions in Addon.permissions.
+ // Indicates that the Addon can be uninstalled.
+ PERM_CAN_UNINSTALL: 1,
+ // Indicates that the Addon can be enabled by the user.
+ PERM_CAN_ENABLE: 2,
+ // Indicates that the Addon can be disabled by the user.
+ PERM_CAN_DISABLE: 4,
+ // Indicates that the Addon can be upgraded.
+ PERM_CAN_UPGRADE: 8,
+ // Indicates that the Addon can be set to be optionally enabled
+ // on a case-by-case basis.
+ PERM_CAN_ASK_TO_ACTIVATE: 16,
+
+ // General descriptions of where items are installed.
+ // Installed in this profile.
+ SCOPE_PROFILE: 1,
+ // Installed for all of this user's profiles.
+ SCOPE_USER: 2,
+ // Installed and owned by the application.
+ SCOPE_APPLICATION: 4,
+ // Installed for all users of the computer.
+ SCOPE_SYSTEM: 8,
+ // The combination of all scopes.
+ SCOPE_ALL: 15,
+
+ // Add-on type is expected to be displayed in the UI in a list.
+ VIEW_TYPE_LIST: "list",
+
+ // Constants describing how add-on types behave.
+
+ // If no add-ons of a type are installed, then the category for that add-on
+ // type should be hidden in the UI.
+ TYPE_UI_HIDE_EMPTY: 16,
+ // Indicates that this add-on type supports the ask-to-activate state.
+ // That is, add-ons of this type can be set to be optionally enabled
+ // on a case-by-case basis.
+ TYPE_SUPPORTS_ASK_TO_ACTIVATE: 32,
+ // The add-on type natively supports undo for restartless uninstalls.
+ // If this flag is not specified, the UI is expected to handle this via
+ // disabling the add-on, and performing the actual uninstall at a later time.
+ TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL: 64,
+
+ // Constants for Addon.applyBackgroundUpdates.
+ // Indicates that the Addon should not update automatically.
+ AUTOUPDATE_DISABLE: 0,
+ // Indicates that the Addon should update automatically only if
+ // that's the global default.
+ AUTOUPDATE_DEFAULT: 1,
+ // Indicates that the Addon should update automatically.
+ AUTOUPDATE_ENABLE: 2,
+
+ // Constants for how Addon options should be shown.
+ // Options will be opened in a new window
+ OPTIONS_TYPE_DIALOG: 1,
+ // Options will be displayed within the AM detail view
+ OPTIONS_TYPE_INLINE: 2,
+ // Options will be displayed in a new tab, if possible
+ OPTIONS_TYPE_TAB: 3,
+ // Same as OPTIONS_TYPE_INLINE, but no Preferences button will be shown.
+ // Used to indicate that only non-interactive information will be shown.
+ OPTIONS_TYPE_INLINE_INFO: 4,
+
+ // Constants for displayed or hidden options notifications
+ // Options notification will be displayed
+ OPTIONS_NOTIFICATION_DISPLAYED: "addon-options-displayed",
+ // Options notification will be hidden
+ OPTIONS_NOTIFICATION_HIDDEN: "addon-options-hidden",
+
+ // Constants for getStartupChanges, addStartupChange and removeStartupChange
+ // Add-ons that were detected as installed during startup. Doesn't include
+ // add-ons that were pending installation the last time the application ran.
+ STARTUP_CHANGE_INSTALLED: "installed",
+ // Add-ons that were detected as changed during startup. This includes an
+ // add-on moving to a different location, changing version or just having
+ // been detected as possibly changed.
+ STARTUP_CHANGE_CHANGED: "changed",
+ // Add-ons that were detected as uninstalled during startup. Doesn't include
+ // add-ons that were pending uninstallation the last time the application ran.
+ STARTUP_CHANGE_UNINSTALLED: "uninstalled",
+ // Add-ons that were detected as disabled during startup, normally because of
+ // an application change making an add-on incompatible. Doesn't include
+ // add-ons that were pending being disabled the last time the application ran.
+ STARTUP_CHANGE_DISABLED: "disabled",
+ // Add-ons that were detected as enabled during startup, normally because of
+ // an application change making an add-on compatible. Doesn't include
+ // add-ons that were pending being enabled the last time the application ran.
+ STARTUP_CHANGE_ENABLED: "enabled",
+
+ // Constants for the Addon.userDisabled property
+ // Indicates that the userDisabled state of this add-on is currently
+ // ask-to-activate. That is, it can be conditionally enabled on a
+ // case-by-case basis.
+ STATE_ASK_TO_ACTIVATE: "askToActivate",
+
+#ifdef MOZ_EM_DEBUG
+ get __AddonManagerInternal__() {
+ return AddonManagerInternal;
+ },
+#endif
+
+ get isReady() {
+ return gStartupComplete && !gShutdownInProgress;
+ },
+
+ getInstallForURL: function(aUrl, aCallback, aMimetype, aHash, aName, aIcons,
+ aVersion, aBrowser) {
+ AddonManagerInternal.getInstallForURL(aUrl, aCallback, aMimetype, aHash,
+ aName, aIcons, aVersion, aBrowser);
+ },
+
+ getInstallForFile: function(aFile, aCallback, aMimetype) {
+ AddonManagerInternal.getInstallForFile(aFile, aCallback, aMimetype);
+ },
+
+ /**
+ * Gets an array of add-on IDs that changed during the most recent startup.
+ *
+ * @param aType
+ * The type of startup change to get
+ * @return An array of add-on IDs
+ */
+ getStartupChanges: function(aType) {
+ if (!(aType in AddonManagerInternal.startupChanges))
+ return [];
+ return AddonManagerInternal.startupChanges[aType].slice(0);
+ },
+
+ getAddonByID: function(aID, aCallback) {
+ AddonManagerInternal.getAddonByID(aID, aCallback);
+ },
+
+ getAddonBySyncGUID: function(aGUID, aCallback) {
+ AddonManagerInternal.getAddonBySyncGUID(aGUID, aCallback);
+ },
+
+ getAddonsByIDs: function(aIDs, aCallback) {
+ AddonManagerInternal.getAddonsByIDs(aIDs, aCallback);
+ },
+
+ getAddonsWithOperationsByTypes: function(aTypes, aCallback) {
+ AddonManagerInternal.getAddonsWithOperationsByTypes(aTypes, aCallback);
+ },
+
+ getAddonsByTypes: function(aTypes, aCallback) {
+ AddonManagerInternal.getAddonsByTypes(aTypes, aCallback);
+ },
+
+ getAllAddons: function(aCallback) {
+ AddonManagerInternal.getAllAddons(aCallback);
+ },
+
+ getInstallsByTypes: function(aTypes, aCallback) {
+ AddonManagerInternal.getInstallsByTypes(aTypes, aCallback);
+ },
+
+ getAllInstalls: function(aCallback) {
+ AddonManagerInternal.getAllInstalls(aCallback);
+ },
+
+ mapURIToAddonID: function(aURI) {
+ return AddonManagerInternal.mapURIToAddonID(aURI);
+ },
+
+ isInstallEnabled: function(aType) {
+ return AddonManagerInternal.isInstallEnabled(aType);
+ },
+
+ isInstallAllowed: function(aType, aInstallingPrincipal) {
+ return AddonManagerInternal.isInstallAllowed(aType, ensurePrincipal(aInstallingPrincipal));
+ },
+
+ installAddonsFromWebpage: function(aType, aBrowser, aInstallingPrincipal, aInstalls) {
+ AddonManagerInternal.installAddonsFromWebpage(aType, aBrowser,
+ ensurePrincipal(aInstallingPrincipal),
+ aInstalls);
+ },
+
+ addManagerListener: function(aListener) {
+ AddonManagerInternal.addManagerListener(aListener);
+ },
+
+ removeManagerListener: function(aListener) {
+ AddonManagerInternal.removeManagerListener(aListener);
+ },
+
+ addInstallListener: function(aListener) {
+ AddonManagerInternal.addInstallListener(aListener);
+ },
+
+ removeInstallListener: function(aListener) {
+ AddonManagerInternal.removeInstallListener(aListener);
+ },
+
+ addAddonListener: function(aListener) {
+ AddonManagerInternal.addAddonListener(aListener);
+ },
+
+ removeAddonListener: function(aListener) {
+ AddonManagerInternal.removeAddonListener(aListener);
+ },
+
+ addTypeListener: function(aListener) {
+ AddonManagerInternal.addTypeListener(aListener);
+ },
+
+ removeTypeListener: function(aListener) {
+ AddonManagerInternal.removeTypeListener(aListener);
+ },
+
+ get addonTypes() {
+ return AddonManagerInternal.addonTypes;
+ },
+
+ /**
+ * Determines whether an Addon should auto-update or not.
+ *
+ * @param aAddon
+ * The Addon representing the add-on
+ * @return true if the addon should auto-update, false otherwise.
+ */
+ shouldAutoUpdate: function(aAddon) {
+ if (!aAddon || typeof aAddon != "object")
+ throw Components.Exception("aAddon must be specified",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!("applyBackgroundUpdates" in aAddon))
+ return false;
+ if (aAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_ENABLE)
+ return true;
+ if (aAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE)
+ return false;
+ return this.autoUpdateDefault;
+ },
+
+ get checkCompatibility() {
+ return AddonManagerInternal.checkCompatibility;
+ },
+
+ set checkCompatibility(aValue) {
+ AddonManagerInternal.checkCompatibility = aValue;
+ },
+
+ get strictCompatibility() {
+ return AddonManagerInternal.strictCompatibility;
+ },
+
+ set strictCompatibility(aValue) {
+ AddonManagerInternal.strictCompatibility = aValue;
+ },
+
+ get checkUpdateSecurityDefault() {
+ return AddonManagerInternal.checkUpdateSecurityDefault;
+ },
+
+ get checkUpdateSecurity() {
+ return AddonManagerInternal.checkUpdateSecurity;
+ },
+
+ set checkUpdateSecurity(aValue) {
+ AddonManagerInternal.checkUpdateSecurity = aValue;
+ },
+
+ get updateEnabled() {
+ return AddonManagerInternal.updateEnabled;
+ },
+
+ set updateEnabled(aValue) {
+ AddonManagerInternal.updateEnabled = aValue;
+ },
+
+ get autoUpdateDefault() {
+ return AddonManagerInternal.autoUpdateDefault;
+ },
+
+ set autoUpdateDefault(aValue) {
+ AddonManagerInternal.autoUpdateDefault = aValue;
+ },
+
+ escapeAddonURI: function(aAddon, aUri, aAppVersion) {
+ return AddonManagerInternal.escapeAddonURI(aAddon, aUri, aAppVersion);
+ },
+
+ get shutdown() {
+ return gShutdownBarrier.client;
+ },
+};
+
+// load the timestamps module into AddonManagerInternal
+Object.freeze(AddonManagerInternal);
+Object.freeze(AddonManagerPrivate);
+Object.freeze(AddonManager);
diff --git a/components/extensions/src/AddonPathService.cpp b/components/extensions/src/AddonPathService.cpp
new file mode 100644
index 000000000..ddfdbe817
--- /dev/null
+++ b/components/extensions/src/AddonPathService.cpp
@@ -0,0 +1,243 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#include "AddonPathService.h"
+
+#include "amIAddonManager.h"
+#include "nsIURI.h"
+#include "nsXULAppAPI.h"
+#include "jsapi.h"
+#include "nsServiceManagerUtils.h"
+#include "nsLiteralString.h"
+#include "nsThreadUtils.h"
+#include "nsIIOService.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIResProtocolHandler.h"
+#include "nsIChromeRegistry.h"
+#include "nsIJARURI.h"
+#include "nsJSUtils.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/AddonPathService.h"
+#include "mozilla/Omnijar.h"
+
+#include <algorithm>
+
+namespace mozilla {
+
+struct PathEntryComparator
+{
+ typedef AddonPathService::PathEntry PathEntry;
+
+ bool Equals(const PathEntry& entry1, const PathEntry& entry2) const
+ {
+ return entry1.mPath == entry2.mPath;
+ }
+
+ bool LessThan(const PathEntry& entry1, const PathEntry& entry2) const
+ {
+ return entry1.mPath < entry2.mPath;
+ }
+};
+
+AddonPathService::AddonPathService()
+{
+}
+
+AddonPathService::~AddonPathService()
+{
+ sInstance = nullptr;
+}
+
+NS_IMPL_ISUPPORTS(AddonPathService, amIAddonPathService)
+
+AddonPathService *AddonPathService::sInstance;
+
+/* static */ AddonPathService*
+AddonPathService::GetInstance()
+{
+ if (!sInstance) {
+ sInstance = new AddonPathService();
+ }
+ NS_ADDREF(sInstance);
+ return sInstance;
+}
+
+static JSAddonId*
+ConvertAddonId(const nsAString& addonIdString)
+{
+ AutoSafeJSContext cx;
+ JS::RootedValue strv(cx);
+ if (!mozilla::dom::ToJSValue(cx, addonIdString, &strv)) {
+ return nullptr;
+ }
+ JS::RootedString str(cx, strv.toString());
+ return JS::NewAddonId(cx, str);
+}
+
+JSAddonId*
+AddonPathService::Find(const nsAString& path)
+{
+ // Use binary search to find the nearest entry that is <= |path|.
+ PathEntryComparator comparator;
+ unsigned index = mPaths.IndexOfFirstElementGt(PathEntry(path, nullptr), comparator);
+ if (index == 0) {
+ return nullptr;
+ }
+ const PathEntry& entry = mPaths[index - 1];
+
+ // Return the entry's addon if its path is a prefix of |path|.
+ if (StringBeginsWith(path, entry.mPath)) {
+ return entry.mAddonId;
+ }
+ return nullptr;
+}
+
+NS_IMETHODIMP
+AddonPathService::FindAddonId(const nsAString& path, nsAString& addonIdString)
+{
+ if (JSAddonId* id = Find(path)) {
+ JSFlatString* flat = JS_ASSERT_STRING_IS_FLAT(JS::StringOfAddonId(id));
+ AssignJSFlatString(addonIdString, flat);
+ }
+ return NS_OK;
+}
+
+/* static */ JSAddonId*
+AddonPathService::FindAddonId(const nsAString& path)
+{
+ // If no service has been created, then we're not going to find anything.
+ if (!sInstance) {
+ return nullptr;
+ }
+
+ return sInstance->Find(path);
+}
+
+NS_IMETHODIMP
+AddonPathService::InsertPath(const nsAString& path, const nsAString& addonIdString)
+{
+ JSAddonId* addonId = ConvertAddonId(addonIdString);
+
+ // Add the new path in sorted order.
+ PathEntryComparator comparator;
+ mPaths.InsertElementSorted(PathEntry(path, addonId), comparator);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AddonPathService::MapURIToAddonId(nsIURI* aURI, nsAString& addonIdString)
+{
+ if (JSAddonId* id = MapURIToAddonID(aURI)) {
+ JSFlatString* flat = JS_ASSERT_STRING_IS_FLAT(JS::StringOfAddonId(id));
+ AssignJSFlatString(addonIdString, flat);
+ }
+ return NS_OK;
+}
+
+static nsresult
+ResolveURI(nsIURI* aURI, nsAString& out)
+{
+ bool equals;
+ nsresult rv;
+ nsCOMPtr<nsIURI> uri;
+ nsAutoCString spec;
+
+ // Resolve resource:// URIs. At the end of this if/else block, we
+ // have both spec and uri variables identifying the same URI.
+ if (NS_SUCCEEDED(aURI->SchemeIs("resource", &equals)) && equals) {
+ nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+
+ nsCOMPtr<nsIProtocolHandler> ph;
+ rv = ioService->GetProtocolHandler("resource", getter_AddRefs(ph));
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+
+ nsCOMPtr<nsIResProtocolHandler> irph(do_QueryInterface(ph, &rv));
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+
+ rv = irph->ResolveURI(aURI, spec);
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+
+ rv = ioService->NewURI(spec, nullptr, nullptr, getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+ } else if (NS_SUCCEEDED(aURI->SchemeIs("chrome", &equals)) && equals) {
+ nsCOMPtr<nsIChromeRegistry> chromeReg =
+ mozilla::services::GetChromeRegistryService();
+ if (NS_WARN_IF(!chromeReg))
+ return NS_ERROR_UNEXPECTED;
+
+ rv = chromeReg->ConvertChromeURL(aURI, getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+ } else {
+ uri = aURI;
+ }
+
+ if (NS_SUCCEEDED(uri->SchemeIs("jar", &equals)) && equals) {
+ nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+
+ nsCOMPtr<nsIURI> jarFileURI;
+ rv = jarURI->GetJARFile(getter_AddRefs(jarFileURI));
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+
+ return ResolveURI(jarFileURI, out);
+ }
+
+ if (NS_SUCCEEDED(uri->SchemeIs("file", &equals)) && equals) {
+ nsCOMPtr<nsIFileURL> baseFileURL = do_QueryInterface(uri, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+
+ nsCOMPtr<nsIFile> file;
+ rv = baseFileURL->GetFile(getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv)))
+ return rv;
+
+ return file->GetPath(out);
+ }
+ return NS_ERROR_FAILURE;
+}
+
+JSAddonId*
+MapURIToAddonID(nsIURI* aURI)
+{
+ if (!NS_IsMainThread() || !XRE_IsParentProcess()) {
+ return nullptr;
+ }
+
+ nsAutoString filePath;
+ nsresult rv = ResolveURI(aURI, filePath);
+ if (NS_FAILED(rv))
+ return nullptr;
+
+ nsCOMPtr<nsIFile> greJar = Omnijar::GetPath(Omnijar::GRE);
+ nsCOMPtr<nsIFile> appJar = Omnijar::GetPath(Omnijar::APP);
+ if (greJar && appJar) {
+ nsAutoString greJarString, appJarString;
+ if (NS_FAILED(greJar->GetPath(greJarString)) || NS_FAILED(appJar->GetPath(appJarString)))
+ return nullptr;
+
+ // If |aURI| is part of either Omnijar, then it can't be part of an
+ // add-on. This catches pretty much all URLs for Firefox content.
+ if (filePath.Equals(greJarString) || filePath.Equals(appJarString))
+ return nullptr;
+ }
+
+ // If it's not part of Firefox, we resort to binary searching through the
+ // add-on paths.
+ return AddonPathService::FindAddonId(filePath);
+}
+
+}
diff --git a/components/extensions/src/AddonPathService.h b/components/extensions/src/AddonPathService.h
new file mode 100644
index 000000000..f739b018f
--- /dev/null
+++ b/components/extensions/src/AddonPathService.h
@@ -0,0 +1,55 @@
+//* -*- Mode: C++; tab-width: 8; 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/. */
+
+#ifndef AddonPathService_h
+#define AddonPathService_h
+
+#include "amIAddonPathService.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsIURI;
+class JSAddonId;
+
+namespace mozilla {
+
+JSAddonId*
+MapURIToAddonID(nsIURI* aURI);
+
+class AddonPathService final : public amIAddonPathService
+{
+public:
+ AddonPathService();
+
+ static AddonPathService* GetInstance();
+
+ JSAddonId* Find(const nsAString& path);
+ static JSAddonId* FindAddonId(const nsAString& path);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_AMIADDONPATHSERVICE
+
+ struct PathEntry
+ {
+ nsString mPath;
+ JSAddonId* mAddonId;
+
+ PathEntry(const nsAString& aPath, JSAddonId* aAddonId)
+ : mPath(aPath), mAddonId(aAddonId)
+ {}
+ };
+
+private:
+ virtual ~AddonPathService();
+
+ // Paths are stored sorted in order of their mPath.
+ nsTArray<PathEntry> mPaths;
+
+ static AddonPathService* sInstance;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/components/extensions/src/AddonRepository.jsm b/components/extensions/src/AddonRepository.jsm
new file mode 100644
index 000000000..41fb5c06d
--- /dev/null
+++ b/components/extensions/src/AddonRepository.jsm
@@ -0,0 +1,2005 @@
+/* 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
+ "resource://gre/modules/DeferredSave.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator",
+ "resource://gre/modules/addons/AddonRepository_SQLiteMigrator.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+
+this.EXPORTED_SYMBOLS = [ "AddonRepository" ];
+
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+const PREF_GETADDONS_CACHE_TYPES = "extensions.getAddons.cache.types";
+const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled"
+const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons";
+const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
+const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url";
+const PREF_GETADDONS_BROWSERECOMMENDED = "extensions.getAddons.recommended.browseURL";
+const PREF_GETADDONS_GETRECOMMENDED = "extensions.getAddons.recommended.url";
+const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL";
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const PREF_GETADDONS_DB_SCHEMA = "extensions.getAddons.databaseSchema"
+
+const PREF_METADATA_LASTUPDATE = "extensions.getAddons.cache.lastUpdate";
+const PREF_METADATA_UPDATETHRESHOLD_SEC = "extensions.getAddons.cache.updateThreshold";
+const DEFAULT_METADATA_UPDATETHRESHOLD_SEC = 172800; // two days
+
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
+
+const API_VERSION = "1.5";
+const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary";
+
+const KEY_PROFILEDIR = "ProfD";
+const FILE_DATABASE = "addons.json";
+const DB_SCHEMA = 5;
+const DB_MIN_JSON_SCHEMA = 5;
+const DB_BATCH_TIMEOUT_MS = 50;
+
+const BLANK_DB = function() {
+ return {
+ addons: new Map(),
+ schema: DB_SCHEMA
+ };
+}
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.repository";
+
+// Create a new logger for use by the Addons Repository
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+// A map between XML keys to AddonSearchResult keys for string values
+// that require no extra parsing from XML
+const STRING_KEY_MAP = {
+ name: "name",
+ version: "version",
+ homepage: "homepageURL",
+ support: "supportURL"
+};
+
+// A map between XML keys to AddonSearchResult keys for string values
+// that require parsing from HTML
+const HTML_KEY_MAP = {
+ summary: "description",
+ description: "fullDescription",
+ developer_comments: "developerComments",
+ eula: "eula"
+};
+
+// A map between XML keys to AddonSearchResult keys for integer values
+// that require no extra parsing from XML
+const INTEGER_KEY_MAP = {
+ total_downloads: "totalDownloads",
+ weekly_downloads: "weeklyDownloads",
+ daily_users: "dailyUsers"
+};
+
+// Wrap the XHR factory so that tests can override with a mock
+var XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
+ "nsIXMLHttpRequest");
+
+function convertHTMLToPlainText(html) {
+ if (!html)
+ return html;
+ var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"].
+ createInstance(Ci.nsIFormatConverter);
+
+ var input = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ input.data = html.replace(/\n/g, "<br>");
+
+ var output = {};
+ converter.convert("text/html", input, input.data.length, "text/unicode",
+ output, {});
+
+ if (output.value instanceof Ci.nsISupportsString)
+ return output.value.data.replace(/\r\n/g, "\n");
+ return html;
+}
+
+function getAddonsToCache(aIds, aCallback) {
+ try {
+ var types = Services.prefs.getCharPref(PREF_GETADDONS_CACHE_TYPES);
+ }
+ catch (e) { }
+ if (!types)
+ types = DEFAULT_CACHE_TYPES;
+
+ types = types.split(",");
+
+ AddonManager.getAddonsByIDs(aIds, function getAddonsToCache_getAddonsByIDs(aAddons) {
+ let enabledIds = [];
+ for (var i = 0; i < aIds.length; i++) {
+ var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]);
+ try {
+ if (!Services.prefs.getBoolPref(preference))
+ continue;
+ } catch(e) {
+ // If the preference doesn't exist caching is enabled by default
+ }
+
+ // The add-ons manager may not know about this ID yet if it is a pending
+ // install. In that case we'll just cache it regardless
+ if (aAddons[i] && (types.indexOf(aAddons[i].type) == -1))
+ continue;
+
+ enabledIds.push(aIds[i]);
+ }
+
+ aCallback(enabledIds);
+ });
+}
+
+function AddonSearchResult(aId) {
+ this.id = aId;
+ this.icons = {};
+ this._unsupportedProperties = {};
+}
+
+AddonSearchResult.prototype = {
+ /**
+ * The ID of the add-on
+ */
+ id: null,
+
+ /**
+ * The add-on type (e.g. "extension" or "theme")
+ */
+ type: null,
+
+ /**
+ * The name of the add-on
+ */
+ name: null,
+
+ /**
+ * The version of the add-on
+ */
+ version: null,
+
+ /**
+ * The creator of the add-on
+ */
+ creator: null,
+
+ /**
+ * The developers of the add-on
+ */
+ developers: null,
+
+ /**
+ * A short description of the add-on
+ */
+ description: null,
+
+ /**
+ * The full description of the add-on
+ */
+ fullDescription: null,
+
+ /**
+ * The developer comments for the add-on. This includes any information
+ * that may be helpful to end users that isn't necessarily applicable to
+ * the add-on description (e.g. known major bugs)
+ */
+ developerComments: null,
+
+ /**
+ * The end-user licensing agreement (EULA) of the add-on
+ */
+ eula: null,
+
+ /**
+ * The url of the add-on's icon
+ */
+ get iconURL() {
+ return this.icons && this.icons[32];
+ },
+
+ /**
+ * The URLs of the add-on's icons, as an object with icon size as key
+ */
+ icons: null,
+
+ /**
+ * An array of screenshot urls for the add-on
+ */
+ screenshots: null,
+
+ /**
+ * The homepage for the add-on
+ */
+ homepageURL: null,
+
+ /**
+ * The homepage for the add-on
+ */
+ learnmoreURL: null,
+
+ /**
+ * The support URL for the add-on
+ */
+ supportURL: null,
+
+ /**
+ * The contribution url of the add-on
+ */
+ contributionURL: null,
+
+ /**
+ * The suggested contribution amount
+ */
+ contributionAmount: null,
+
+ /**
+ * The URL to visit in order to purchase the add-on
+ */
+ purchaseURL: null,
+
+ /**
+ * The numerical cost of the add-on in some currency, for sorting purposes
+ * only
+ */
+ purchaseAmount: null,
+
+ /**
+ * The display cost of the add-on, for display purposes only
+ */
+ purchaseDisplayAmount: null,
+
+ /**
+ * The rating of the add-on, 0-5
+ */
+ averageRating: null,
+
+ /**
+ * The number of reviews for this add-on
+ */
+ reviewCount: null,
+
+ /**
+ * The URL to the list of reviews for this add-on
+ */
+ reviewURL: null,
+
+ /**
+ * The total number of times the add-on was downloaded
+ */
+ totalDownloads: null,
+
+ /**
+ * The number of times the add-on was downloaded the current week
+ */
+ weeklyDownloads: null,
+
+ /**
+ * The number of daily users for the add-on
+ */
+ dailyUsers: null,
+
+ /**
+ * AddonInstall object generated from the add-on XPI url
+ */
+ install: null,
+
+ /**
+ * nsIURI storing where this add-on was installed from
+ */
+ sourceURI: null,
+
+ /**
+ * The status of the add-on in the repository (e.g. 4 = "Public")
+ */
+ repositoryStatus: null,
+
+ /**
+ * The size of the add-on's files in bytes. For an add-on that have not yet
+ * been downloaded this may be an estimated value.
+ */
+ size: null,
+
+ /**
+ * The Date that the add-on was most recently updated
+ */
+ updateDate: null,
+
+ /**
+ * True or false depending on whether the add-on is compatible with the
+ * current version of the application
+ */
+ isCompatible: true,
+
+ /**
+ * True or false depending on whether the add-on is compatible with the
+ * current platform
+ */
+ isPlatformCompatible: true,
+
+ /**
+ * Array of AddonCompatibilityOverride objects, that describe overrides for
+ * compatibility with an application versions.
+ **/
+ compatibilityOverrides: null,
+
+ /**
+ * True if the add-on has a secure means of updating
+ */
+ providesUpdatesSecurely: true,
+
+ /**
+ * The current blocklist state of the add-on
+ */
+ blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+
+ /**
+ * True if this add-on cannot be used in the application based on version
+ * compatibility, dependencies and blocklisting
+ */
+ appDisabled: false,
+
+ /**
+ * True if the user wants this add-on to be disabled
+ */
+ userDisabled: false,
+
+ /**
+ * Indicates what scope the add-on is installed in, per profile, user,
+ * system or application
+ */
+ scope: AddonManager.SCOPE_PROFILE,
+
+ /**
+ * True if the add-on is currently functional
+ */
+ isActive: true,
+
+ /**
+ * A bitfield holding all of the current operations that are waiting to be
+ * performed for this add-on
+ */
+ pendingOperations: AddonManager.PENDING_NONE,
+
+ /**
+ * A bitfield holding all the the operations that can be performed on
+ * this add-on
+ */
+ permissions: 0,
+
+ /**
+ * Tests whether this add-on is known to be compatible with a
+ * particular application and platform version.
+ *
+ * @param appVersion
+ * An application version to test against
+ * @param platformVersion
+ * A platform version to test against
+ * @return Boolean representing if the add-on is compatible
+ */
+ isCompatibleWith: function ASR_isCompatibleWith(aAppVerison, aPlatformVersion) {
+ return true;
+ },
+
+ /**
+ * Starts an update check for this add-on. This will perform
+ * asynchronously and deliver results to the given listener.
+ *
+ * @param aListener
+ * An UpdateListener for the update process
+ * @param aReason
+ * A reason code for performing the update
+ * @param aAppVersion
+ * An application version to check for updates for
+ * @param aPlatformVersion
+ * A platform version to check for updates for
+ */
+ findUpdates: function ASR_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
+ if ("onNoCompatibilityUpdateAvailable" in aListener)
+ aListener.onNoCompatibilityUpdateAvailable(this);
+ if ("onNoUpdateAvailable" in aListener)
+ aListener.onNoUpdateAvailable(this);
+ if ("onUpdateFinished" in aListener)
+ aListener.onUpdateFinished(this);
+ },
+
+ toJSON: function() {
+ let json = {};
+
+ for (let [property, value] of Iterator(this)) {
+ if (property.startsWith("_") ||
+ typeof(value) === "function")
+ continue;
+
+ try {
+ switch (property) {
+ case "sourceURI":
+ json.sourceURI = value ? value.spec : "";
+ break;
+
+ case "updateDate":
+ json.updateDate = value ? value.getTime() : "";
+ break;
+
+ default:
+ json[property] = value;
+ }
+ } catch (ex) {
+ logger.warn("Error writing property value for " + property);
+ }
+ }
+
+ for (let [property, value] of Iterator(this._unsupportedProperties)) {
+ if (!property.startsWith("_"))
+ json[property] = value;
+ }
+
+ return json;
+ }
+}
+
+/**
+ * The add-on repository is a source of add-ons that can be installed. It can
+ * be searched in three ways. The first takes a list of IDs and returns a
+ * list of the corresponding add-ons. The second returns a list of add-ons that
+ * come highly recommended. This list should change frequently. The third is to
+ * search for specific search terms entered by the user. Searches are
+ * asynchronous and results should be passed to the provided callback object
+ * when complete. The results passed to the callback should only include add-ons
+ * that are compatible with the current application and are not already
+ * installed.
+ */
+this.AddonRepository = {
+ /**
+ * Whether caching is currently enabled
+ */
+ get cacheEnabled() {
+ // Act as though caching is disabled if there was an unrecoverable error
+ // openning the database.
+ if (!AddonDatabase.databaseOk) {
+ logger.warn("Cache is disabled because database is not OK");
+ return false;
+ }
+
+ let preference = PREF_GETADDONS_CACHE_ENABLED;
+ return Services.prefs.getBoolPref(preference, false);
+ },
+
+ // A cache of the add-ons stored in the database
+ _addons: null,
+
+ // Whether a search is currently in progress
+ _searching: false,
+
+ // XHR associated with the current request
+ _request: null,
+
+ /*
+ * Addon search results callback object that contains two functions
+ *
+ * searchSucceeded - Called when a search has suceeded.
+ *
+ * @param aAddons
+ * An array of the add-on results. In the case of searching for
+ * specific terms the ordering of results may be determined by
+ * the search provider.
+ * @param aAddonCount
+ * The length of aAddons
+ * @param aTotalResults
+ * The total results actually available in the repository
+ *
+ *
+ * searchFailed - Called when an error occurred when performing a search.
+ */
+ _callback: null,
+
+ // Maximum number of results to return
+ _maxResults: null,
+
+ /**
+ * Shut down AddonRepository
+ * return: promise{integer} resolves with the result of flushing
+ * the AddonRepository database
+ */
+ shutdown: function AddonRepo_shutdown() {
+ this.cancelSearch();
+
+ this._addons = null;
+ return AddonDatabase.shutdown(false);
+ },
+
+ metadataAge: function() {
+ let now = Math.round(Date.now() / 1000);
+ let lastUpdate = Services.prefs.getIntPref(PREF_METADATA_LASTUPDATE, 0);
+
+ // Handle clock jumps
+ if (now < lastUpdate) {
+ return now;
+ }
+ return now - lastUpdate;
+ },
+
+ isMetadataStale: function AddonRepo_isMetadataStale() {
+ let threshold = Services.prefs.getIntPref(PREF_METADATA_UPDATETHRESHOLD_SEC,
+ DEFAULT_METADATA_UPDATETHRESHOLD_SEC);
+ return (this.metadataAge() > threshold);
+ },
+
+ /**
+ * Asynchronously get a cached add-on by id. The add-on (or null if the
+ * add-on is not found) is passed to the specified callback. If caching is
+ * disabled, null is passed to the specified callback.
+ *
+ * @param aId
+ * The id of the add-on to get
+ * @param aCallback
+ * The callback to pass the result back to
+ */
+ getCachedAddonByID: Task.async(function* (aId, aCallback) {
+ if (!aId || !this.cacheEnabled) {
+ aCallback(null);
+ return;
+ }
+
+ function getAddon(aAddons) {
+ aCallback(aAddons.get(aId) || null);
+ }
+
+ if (this._addons == null) {
+ AddonDatabase.retrieveStoredData().then(aAddons => {
+ this._addons = aAddons;
+ getAddon(aAddons);
+ });
+
+ return;
+ }
+
+ getAddon(this._addons);
+ }),
+
+ /**
+ * Asynchronously repopulate cache so it only contains the add-ons
+ * corresponding to the specified ids. If caching is disabled,
+ * the cache is completely removed.
+ *
+ * @param aTimeout
+ * (Optional) timeout in milliseconds to abandon the XHR request
+ * if we have not received a response from the server.
+ * @return Promise{null}
+ * Resolves when the metadata ping is complete
+ */
+ repopulateCache: function(aTimeout) {
+ return this._repopulateCacheInternal(false, aTimeout);
+ },
+
+ /*
+ * Clear and delete the AddonRepository database
+ * @return Promise{null} resolves when the database is deleted
+ */
+ _clearCache: function () {
+ this._addons = null;
+ return AddonDatabase.delete().then(() =>
+ new Promise((resolve, reject) =>
+ AddonManagerPrivate.updateAddonRepositoryData(resolve))
+ );
+ },
+
+ _repopulateCacheInternal: Task.async(function* (aSendPerformance, aTimeout) {
+ let allAddons = yield new Promise((resolve, reject) =>
+ AddonManager.getAllAddons(resolve));
+
+ // Completely remove cache if caching is not enabled
+ if (!this.cacheEnabled) {
+ logger.debug("Clearing cache because it is disabled");
+ return this._clearCache();
+ }
+
+ // Tycho: let ids = [a.id for (a of allAddons)];
+ let ids = [];
+ for (let a of allAddons) {
+ ids.push(a.id);
+ }
+
+ logger.debug("Repopulate add-on cache with " + ids.toSource());
+
+ let self = this;
+ let addonsToCache = yield new Promise((resolve, reject) =>
+ getAddonsToCache(ids, resolve));
+
+ // Completely remove cache if there are no add-ons to cache
+ if (addonsToCache.length == 0) {
+ logger.debug("Clearing cache because 0 add-ons were requested");
+ return this._clearCache();
+ }
+
+ yield new Promise((resolve, reject) =>
+ self._beginGetAddons(addonsToCache, {
+ searchSucceeded: function repopulateCacheInternal_searchSucceeded(aAddons) {
+ self._addons = new Map();
+ for (let addon of aAddons) {
+ self._addons.set(addon.id, addon);
+ }
+ AddonDatabase.repopulate(aAddons, resolve);
+ },
+ searchFailed: function repopulateCacheInternal_searchFailed() {
+ logger.warn("Search failed when repopulating cache");
+ resolve();
+ }
+ }, aSendPerformance, aTimeout));
+
+ // Always call AddonManager updateAddonRepositoryData after we refill the cache
+ yield new Promise((resolve, reject) =>
+ AddonManagerPrivate.updateAddonRepositoryData(resolve));
+ }),
+
+ /**
+ * Asynchronously add add-ons to the cache corresponding to the specified
+ * ids. If caching is disabled, the cache is unchanged and the callback is
+ * immediately called if it is defined.
+ *
+ * @param aIds
+ * The array of add-on ids to add to the cache
+ * @param aCallback
+ * The optional callback to call once complete
+ */
+ cacheAddons: function AddonRepo_cacheAddons(aIds, aCallback) {
+ logger.debug("cacheAddons: enabled " + this.cacheEnabled + " IDs " + aIds.toSource());
+ if (!this.cacheEnabled) {
+ if (aCallback)
+ aCallback();
+ return;
+ }
+
+ let self = this;
+ getAddonsToCache(aIds, function cacheAddons_getAddonsToCache(aAddons) {
+ // If there are no add-ons to cache, act as if caching is disabled
+ if (aAddons.length == 0) {
+ if (aCallback)
+ aCallback();
+ return;
+ }
+
+ self.getAddonsByIDs(aAddons, {
+ searchSucceeded: function cacheAddons_searchSucceeded(aAddons) {
+ for (let addon of aAddons) {
+ self._addons.set(addon.id, addon);
+ }
+ AddonDatabase.insertAddons(aAddons, aCallback);
+ },
+ searchFailed: function cacheAddons_searchFailed() {
+ logger.warn("Search failed when adding add-ons to cache");
+ if (aCallback)
+ aCallback();
+ }
+ });
+ });
+ },
+
+ /**
+ * The homepage for visiting this repository. If the corresponding preference
+ * is not defined, defaults to about:blank.
+ */
+ get homepageURL() {
+ let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {});
+ return (url != null) ? url : "about:blank";
+ },
+
+ /**
+ * Returns whether this instance is currently performing a search. New
+ * searches will not be performed while this is the case.
+ */
+ get isSearching() {
+ return this._searching;
+ },
+
+ /**
+ * The url that can be visited to see recommended add-ons in this repository.
+ * If the corresponding preference is not defined, defaults to about:blank.
+ */
+ getRecommendedURL: function AddonRepo_getRecommendedURL() {
+ let url = this._formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED, {});
+ return (url != null) ? url : "about:blank";
+ },
+
+ /**
+ * Retrieves the url that can be visited to see search results for the given
+ * terms. If the corresponding preference is not defined, defaults to
+ * about:blank.
+ *
+ * @param aSearchTerms
+ * Search terms used to search the repository
+ */
+ getSearchURL: function AddonRepo_getSearchURL(aSearchTerms) {
+ let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, {
+ TERMS : encodeURIComponent(aSearchTerms)
+ });
+ return (url != null) ? url : "about:blank";
+ },
+
+ /**
+ * Cancels the search in progress. If there is no search in progress this
+ * does nothing.
+ */
+ cancelSearch: function AddonRepo_cancelSearch() {
+ this._searching = false;
+ if (this._request) {
+ this._request.abort();
+ this._request = null;
+ }
+ this._callback = null;
+ },
+
+ /**
+ * Begins a search for add-ons in this repository by ID. Results will be
+ * passed to the given callback.
+ *
+ * @param aIDs
+ * The array of ids to search for
+ * @param aCallback
+ * The callback to pass results to
+ */
+ getAddonsByIDs: function AddonRepo_getAddonsByIDs(aIDs, aCallback) {
+ return this._beginGetAddons(aIDs, aCallback, false);
+ },
+
+ /**
+ * Begins a search of add-ons, potentially sending performance data.
+ *
+ * @param aIDs
+ * Array of ids to search for.
+ * @param aCallback
+ * Function to pass results to.
+ * @param aSendPerformance
+ * Boolean indicating whether to send performance data with the
+ * request.
+ * @param aTimeout
+ * (Optional) timeout in milliseconds to abandon the XHR request
+ * if we have not received a response from the server.
+ */
+ _beginGetAddons: function(aIDs, aCallback, aSendPerformance, aTimeout) {
+ let ids = aIDs.slice(0);
+
+ let params = {
+ API_VERSION : API_VERSION,
+ IDS : ids.map(encodeURIComponent).join(',')
+ };
+
+ let pref = PREF_GETADDONS_BYIDS;
+
+ if (aSendPerformance) {
+ let type = Services.prefs.getPrefType(PREF_GETADDONS_BYIDS_PERFORMANCE);
+ if (type == Services.prefs.PREF_STRING) {
+ pref = PREF_GETADDONS_BYIDS_PERFORMANCE;
+
+ let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"].
+ getService(Ci.nsIAppStartup).
+ getStartupInfo();
+
+ params.TIME_MAIN = "";
+ params.TIME_FIRST_PAINT = "";
+ params.TIME_SESSION_RESTORED = "";
+ if (startupInfo.process) {
+ if (startupInfo.main) {
+ params.TIME_MAIN = startupInfo.main - startupInfo.process;
+ }
+ if (startupInfo.firstPaint) {
+ params.TIME_FIRST_PAINT = startupInfo.firstPaint -
+ startupInfo.process;
+ }
+ if (startupInfo.sessionRestored) {
+ params.TIME_SESSION_RESTORED = startupInfo.sessionRestored -
+ startupInfo.process;
+ }
+ }
+ }
+ }
+
+ let url = this._formatURLPref(pref, params);
+
+ let self = this;
+ function handleResults(aElements, aTotalResults, aCompatData) {
+ // Don't use this._parseAddons() so that, for example,
+ // incompatible add-ons are not filtered out
+ let results = [];
+ for (let i = 0; i < aElements.length && results.length < self._maxResults; i++) {
+ let result = self._parseAddon(aElements[i], null, aCompatData);
+ if (result == null)
+ continue;
+
+ // Ignore add-on if it wasn't actually requested
+ let idIndex = ids.indexOf(result.addon.id);
+ if (idIndex == -1)
+ continue;
+
+ // Ignore add-on if the add-on manager doesn't know about its type:
+ if (!(result.addon.type in AddonManager.addonTypes)) {
+ continue;
+ }
+
+ results.push(result);
+ // Ignore this add-on from now on
+ ids.splice(idIndex, 1);
+ }
+
+ // Include any compatibility overrides for addons not hosted by the
+ // remote repository.
+ for each (let addonCompat in aCompatData) {
+ if (addonCompat.hosted)
+ continue;
+
+ let addon = new AddonSearchResult(addonCompat.id);
+ // Compatibility overrides can only be for extensions.
+ addon.type = "extension";
+ addon.compatibilityOverrides = addonCompat.compatRanges;
+ let result = {
+ addon: addon,
+ xpiURL: null,
+ xpiHash: null
+ };
+ results.push(result);
+ }
+
+ // aTotalResults irrelevant
+ self._reportSuccess(results, -1);
+ }
+
+ this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout);
+ },
+
+ /**
+ * Performs the daily background update check.
+ *
+ * This API both searches for the add-on IDs specified and sends performance
+ * data. It is meant to be called as part of the daily update ping. It should
+ * not be used for any other purpose. Use repopulateCache instead.
+ *
+ * @return Promise{null} Resolves when the metadata update is complete.
+ */
+ backgroundUpdateCheck: function () {
+ return this._repopulateCacheInternal(true);
+ },
+
+ /**
+ * Begins a search for recommended add-ons in this repository. Results will
+ * be passed to the given callback.
+ *
+ * @param aMaxResults
+ * The maximum number of results to return
+ * @param aCallback
+ * The callback to pass results to
+ */
+ retrieveRecommendedAddons: function AddonRepo_retrieveRecommendedAddons(aMaxResults, aCallback) {
+ let url = this._formatURLPref(PREF_GETADDONS_GETRECOMMENDED, {
+ API_VERSION : API_VERSION,
+
+ // Get twice as many results to account for potential filtering
+ MAX_RESULTS : 2 * aMaxResults
+ });
+
+ let self = this;
+ function handleResults(aElements, aTotalResults) {
+ self._getLocalAddonIds(function retrieveRecommendedAddons_getLocalAddonIds(aLocalAddonIds) {
+ // aTotalResults irrelevant
+ self._parseAddons(aElements, -1, aLocalAddonIds);
+ });
+ }
+
+ this._beginSearch(url, aMaxResults, aCallback, handleResults);
+ },
+
+ /**
+ * Begins a search for add-ons in this repository. Results will be passed to
+ * the given callback.
+ *
+ * @param aSearchTerms
+ * The terms to search for
+ * @param aMaxResults
+ * The maximum number of results to return
+ * @param aCallback
+ * The callback to pass results to
+ */
+ searchAddons: function AddonRepo_searchAddons(aSearchTerms, aMaxResults, aCallback) {
+ let compatMode = "normal";
+ if (!AddonManager.checkCompatibility)
+ compatMode = "ignore";
+ else if (AddonManager.strictCompatibility)
+ compatMode = "strict";
+
+ let substitutions = {
+ API_VERSION : API_VERSION,
+ TERMS : encodeURIComponent(aSearchTerms),
+ // Get twice as many results to account for potential filtering
+ MAX_RESULTS : 2 * aMaxResults,
+ COMPATIBILITY_MODE : compatMode,
+ };
+
+ let url = this._formatURLPref(PREF_GETADDONS_GETSEARCHRESULTS, substitutions);
+
+ let self = this;
+ function handleResults(aElements, aTotalResults) {
+ self._getLocalAddonIds(function searchAddons_getLocalAddonIds(aLocalAddonIds) {
+ self._parseAddons(aElements, aTotalResults, aLocalAddonIds);
+ });
+ }
+
+ this._beginSearch(url, aMaxResults, aCallback, handleResults);
+ },
+
+ // Posts results to the callback
+ _reportSuccess: function AddonRepo_reportSuccess(aResults, aTotalResults) {
+ this._searching = false;
+ this._request = null;
+ // The callback may want to trigger a new search so clear references early
+ // Tycho: let addons = [result.addon for each(result in aResults)];
+ let addons = [];
+ for each(let result in aResults) {
+ addons.push(result.addon);
+ }
+
+ let callback = this._callback;
+ this._callback = null;
+ callback.searchSucceeded(addons, addons.length, aTotalResults);
+ },
+
+ // Notifies the callback of a failure
+ _reportFailure: function AddonRepo_reportFailure() {
+ this._searching = false;
+ this._request = null;
+ // The callback may want to trigger a new search so clear references early
+ let callback = this._callback;
+ this._callback = null;
+ callback.searchFailed();
+ },
+
+ // Get descendant by unique tag name. Returns null if not unique tag name.
+ _getUniqueDescendant: function AddonRepo_getUniqueDescendant(aElement, aTagName) {
+ let elementsList = aElement.getElementsByTagName(aTagName);
+ return (elementsList.length == 1) ? elementsList[0] : null;
+ },
+
+ // Get direct descendant by unique tag name.
+ // Returns null if not unique tag name.
+ _getUniqueDirectDescendant: function AddonRepo_getUniqueDirectDescendant(aElement, aTagName) {
+ let elementsList = Array.filter(aElement.children,
+ function arrayFiltering(aChild) aChild.tagName == aTagName);
+ return (elementsList.length == 1) ? elementsList[0] : null;
+ },
+
+ // Parse out trimmed text content. Returns null if text content empty.
+ _getTextContent: function AddonRepo_getTextContent(aElement) {
+ let textContent = aElement.textContent.trim();
+ return (textContent.length > 0) ? textContent : null;
+ },
+
+ // Parse out trimmed text content of a descendant with the specified tag name
+ // Returns null if the parsing unsuccessful.
+ _getDescendantTextContent: function AddonRepo_getDescendantTextContent(aElement, aTagName) {
+ let descendant = this._getUniqueDescendant(aElement, aTagName);
+ return (descendant != null) ? this._getTextContent(descendant) : null;
+ },
+
+ // Parse out trimmed text content of a direct descendant with the specified
+ // tag name.
+ // Returns null if the parsing unsuccessful.
+ _getDirectDescendantTextContent: function AddonRepo_getDirectDescendantTextContent(aElement, aTagName) {
+ let descendant = this._getUniqueDirectDescendant(aElement, aTagName);
+ return (descendant != null) ? this._getTextContent(descendant) : null;
+ },
+
+ /*
+ * Creates an AddonSearchResult by parsing an <addon> element
+ *
+ * @param aElement
+ * The <addon> element to parse
+ * @param aSkip
+ * Object containing ids and sourceURIs of add-ons to skip.
+ * @param aCompatData
+ * Array of parsed addon_compatibility elements to accosiate with the
+ * resulting AddonSearchResult. Optional.
+ * @return Result object containing the parsed AddonSearchResult, xpiURL and
+ * xpiHash if the parsing was successful. Otherwise returns null.
+ */
+ _parseAddon: function AddonRepo_parseAddon(aElement, aSkip, aCompatData) {
+ let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : [];
+ let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : [];
+
+ let guid = this._getDescendantTextContent(aElement, "guid");
+ if (guid == null || skipIDs.indexOf(guid) != -1)
+ return null;
+
+ let addon = new AddonSearchResult(guid);
+ let result = {
+ addon: addon,
+ xpiURL: null,
+ xpiHash: null
+ };
+
+ if (aCompatData && guid in aCompatData)
+ addon.compatibilityOverrides = aCompatData[guid].compatRanges;
+
+ let self = this;
+ for (let node = aElement.firstChild; node; node = node.nextSibling) {
+ if (!(node instanceof Ci.nsIDOMElement))
+ continue;
+
+ let localName = node.localName;
+
+ // Handle case where the wanted string value is located in text content
+ // but only if the content is not empty
+ if (localName in STRING_KEY_MAP) {
+ addon[STRING_KEY_MAP[localName]] = this._getTextContent(node) || addon[STRING_KEY_MAP[localName]];
+ continue;
+ }
+
+ // Handle case where the wanted string value is html located in text content
+ if (localName in HTML_KEY_MAP) {
+ addon[HTML_KEY_MAP[localName]] = convertHTMLToPlainText(this._getTextContent(node));
+ continue;
+ }
+
+ // Handle case where the wanted integer value is located in text content
+ if (localName in INTEGER_KEY_MAP) {
+ let value = parseInt(this._getTextContent(node));
+ if (value >= 0)
+ addon[INTEGER_KEY_MAP[localName]] = value;
+ continue;
+ }
+
+ // Handle cases that aren't as simple as grabbing the text content
+ switch (localName) {
+ case "type":
+ // Map AMO's type id to corresponding string
+ // https://github.com/mozilla/olympia/blob/master/apps/constants/base.py#L127
+ // These definitions need to be updated whenever AMO adds a new type.
+ let id = parseInt(node.getAttribute("id"));
+ switch (id) {
+ case 1:
+ addon.type = "extension";
+ break;
+ case 2:
+ addon.type = "theme";
+ break;
+ case 3:
+ addon.type = "dictionary";
+ break;
+ case 4:
+ addon.type = "search";
+ break;
+ case 5:
+ case 6:
+ addon.type = "locale";
+ break;
+ case 7:
+ addon.type = "plugin";
+ break;
+ case 8:
+ addon.type = "api";
+ break;
+ case 9:
+ addon.type = "lightweight-theme";
+ break;
+ case 11:
+ addon.type = "webapp";
+ break;
+ default:
+ logger.info("Unknown type id " + id + " found when parsing response for GUID " + guid);
+ }
+ break;
+ case "authors":
+ let authorNodes = node.getElementsByTagName("author");
+ for (let authorNode of authorNodes) {
+ let name = self._getDescendantTextContent(authorNode, "name");
+ let link = self._getDescendantTextContent(authorNode, "link");
+ if (name == null || link == null)
+ continue;
+
+ let author = new AddonManagerPrivate.AddonAuthor(name, link);
+ if (addon.creator == null)
+ addon.creator = author;
+ else {
+ if (addon.developers == null)
+ addon.developers = [];
+
+ addon.developers.push(author);
+ }
+ }
+ break;
+ case "previews":
+ let previewNodes = node.getElementsByTagName("preview");
+ for (let previewNode of previewNodes) {
+ let full = self._getUniqueDescendant(previewNode, "full");
+ if (full == null)
+ continue;
+
+ let fullURL = self._getTextContent(full);
+ let fullWidth = full.getAttribute("width");
+ let fullHeight = full.getAttribute("height");
+
+ let thumbnailURL, thumbnailWidth, thumbnailHeight;
+ let thumbnail = self._getUniqueDescendant(previewNode, "thumbnail");
+ if (thumbnail) {
+ thumbnailURL = self._getTextContent(thumbnail);
+ thumbnailWidth = thumbnail.getAttribute("width");
+ thumbnailHeight = thumbnail.getAttribute("height");
+ }
+ let caption = self._getDescendantTextContent(previewNode, "caption");
+ let screenshot = new AddonManagerPrivate.AddonScreenshot(fullURL, fullWidth, fullHeight,
+ thumbnailURL, thumbnailWidth,
+ thumbnailHeight, caption);
+
+ if (addon.screenshots == null)
+ addon.screenshots = [];
+
+ if (previewNode.getAttribute("primary") == 1)
+ addon.screenshots.unshift(screenshot);
+ else
+ addon.screenshots.push(screenshot);
+ }
+ break;
+ case "learnmore":
+ addon.learnmoreURL = this._getTextContent(node);
+ addon.homepageURL = addon.homepageURL || addon.learnmoreURL;
+ break;
+ case "contribution_data":
+ let meetDevelopers = this._getDescendantTextContent(node, "meet_developers");
+ let suggestedAmount = this._getDescendantTextContent(node, "suggested_amount");
+ if (meetDevelopers != null) {
+ addon.contributionURL = meetDevelopers;
+ addon.contributionAmount = suggestedAmount;
+ }
+ break
+ case "payment_data":
+ let link = this._getDescendantTextContent(node, "link");
+ let amountTag = this._getUniqueDescendant(node, "amount");
+ let amount = parseFloat(amountTag.getAttribute("amount"));
+ let displayAmount = this._getTextContent(amountTag);
+ if (link != null && amount != null && displayAmount != null) {
+ addon.purchaseURL = link;
+ addon.purchaseAmount = amount;
+ addon.purchaseDisplayAmount = displayAmount;
+ }
+ break
+ case "rating":
+ let averageRating = parseInt(this._getTextContent(node));
+ if (averageRating >= 0)
+ addon.averageRating = Math.min(5, averageRating);
+ break;
+ case "reviews":
+ let url = this._getTextContent(node);
+ let num = parseInt(node.getAttribute("num"));
+ if (url != null && num >= 0) {
+ addon.reviewURL = url;
+ addon.reviewCount = num;
+ }
+ break;
+ case "status":
+ let repositoryStatus = parseInt(node.getAttribute("id"));
+ if (!isNaN(repositoryStatus))
+ addon.repositoryStatus = repositoryStatus;
+ break;
+ case "all_compatible_os":
+ let nodes = node.getElementsByTagName("os");
+ addon.isPlatformCompatible = Array.some(nodes, function parseAddon_platformCompatFilter(aNode) {
+ let text = aNode.textContent.toLowerCase().trim();
+ return text == "all" || text == Services.appinfo.OS.toLowerCase();
+ });
+ break;
+ case "install":
+ // No os attribute means the xpi is compatible with any os
+ if (node.hasAttribute("os")) {
+ let os = node.getAttribute("os").trim().toLowerCase();
+ // If the os is not ALL and not the current OS then ignore this xpi
+ if (os != "all" && os != Services.appinfo.OS.toLowerCase())
+ break;
+ }
+
+ let xpiURL = this._getTextContent(node);
+ if (xpiURL == null)
+ break;
+
+ if (skipSourceURIs.indexOf(xpiURL) != -1)
+ return null;
+
+ result.xpiURL = xpiURL;
+ addon.sourceURI = NetUtil.newURI(xpiURL);
+
+ let size = parseInt(node.getAttribute("size"));
+ addon.size = (size >= 0) ? size : null;
+
+ let xpiHash = node.getAttribute("hash");
+ if (xpiHash != null)
+ xpiHash = xpiHash.trim();
+ result.xpiHash = xpiHash ? xpiHash : null;
+ break;
+ case "last_updated":
+ let epoch = parseInt(node.getAttribute("epoch"));
+ if (!isNaN(epoch))
+ addon.updateDate = new Date(1000 * epoch);
+ break;
+ case "icon":
+ addon.icons[node.getAttribute("size")] = this._getTextContent(node);
+ break;
+ }
+ }
+
+ return result;
+ },
+
+ _parseAddons: function AddonRepo_parseAddons(aElements, aTotalResults, aSkip) {
+ let self = this;
+ let results = [];
+
+ function isSameApplication(aAppNode) {
+ if (self._getTextContent(aAppNode) == Services.appinfo.ID) {
+ return true;
+ }
+ return false;
+ }
+
+ for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) {
+ let element = aElements[i];
+
+ let tags = this._getUniqueDescendant(element, "compatible_applications");
+ if (tags == null)
+ continue;
+
+ let applications = tags.getElementsByTagName("appID");
+ let compatible = Array.some(applications, function parseAddons_applicationsCompatFilter(aAppNode) {
+ if (!isSameApplication(aAppNode))
+ return false;
+
+ let parent = aAppNode.parentNode;
+ let minVersion = self._getDescendantTextContent(parent, "min_version");
+ let maxVersion = self._getDescendantTextContent(parent, "max_version");
+ if (minVersion == null || maxVersion == null)
+ return false;
+
+ let currentVersion = Services.appinfo.version;
+ return (Services.vc.compare(minVersion, currentVersion) <= 0 &&
+ ((!AddonManager.strictCompatibility) ||
+ Services.vc.compare(currentVersion, maxVersion) <= 0));
+ });
+
+ // Ignore add-ons not compatible with this Application
+ if (!compatible) {
+ if (AddonManager.checkCompatibility)
+ continue;
+
+ if (!Array.some(applications, isSameApplication))
+ continue;
+ }
+
+ // Add-on meets all requirements, so parse out data.
+ // Don't pass in compatiblity override data, because that's only returned
+ // in GUID searches, which don't use _parseAddons().
+ let result = this._parseAddon(element, aSkip);
+ if (result == null)
+ continue;
+
+ // Ignore add-on missing a required attribute
+ let requiredAttributes = ["id", "name", "version", "type", "creator"];
+ if (requiredAttributes.some(function parseAddons_attributeFilter(aAttribute) !result.addon[aAttribute]))
+ continue;
+
+ // Ignore add-on with a type AddonManager doesn't understand:
+ if (!(result.addon.type in AddonManager.addonTypes))
+ continue;
+
+ // Add only if the add-on is compatible with the platform
+ if (!result.addon.isPlatformCompatible)
+ continue;
+
+ // Add only if there was an xpi compatible with this OS or there was a
+ // way to purchase the add-on
+ if (!result.xpiURL && !result.addon.purchaseURL)
+ continue;
+
+ result.addon.isCompatible = compatible;
+
+ results.push(result);
+ // Ignore this add-on from now on by adding it to the skip array
+ aSkip.ids.push(result.addon.id);
+ }
+
+ // Immediately report success if no AddonInstall instances to create
+ let pendingResults = results.length;
+ if (pendingResults == 0) {
+ this._reportSuccess(results, aTotalResults);
+ return;
+ }
+
+ // Create an AddonInstall for each result
+ results.forEach(function(aResult) {
+ let addon = aResult.addon;
+ let callback = function addonInstallCallback(aInstall) {
+ addon.install = aInstall;
+ pendingResults--;
+ if (pendingResults == 0)
+ self._reportSuccess(results, aTotalResults);
+ }
+
+ if (aResult.xpiURL) {
+ AddonManager.getInstallForURL(aResult.xpiURL, callback,
+ "application/x-xpinstall", aResult.xpiHash,
+ addon.name, addon.icons, addon.version);
+ }
+ else {
+ callback(null);
+ }
+ });
+ },
+
+ // Parses addon_compatibility nodes, that describe compatibility overrides.
+ _parseAddonCompatElement: function AddonRepo_parseAddonCompatElement(aResultObj, aElement) {
+ let guid = this._getDescendantTextContent(aElement, "guid");
+ if (!guid) {
+ logger.debug("Compatibility override is missing guid.");
+ return;
+ }
+
+ let compat = {id: guid};
+ compat.hosted = aElement.getAttribute("hosted") != "false";
+
+ function findMatchingAppRange(aNodes) {
+ let toolkitAppRange = null;
+ for (let node of aNodes) {
+ let appID = this._getDescendantTextContent(node, "appID");
+ if (appID != Services.appinfo.ID && appID != TOOLKIT_ID)
+ continue;
+
+ let minVersion = this._getDescendantTextContent(node, "min_version");
+ let maxVersion = this._getDescendantTextContent(node, "max_version");
+ if (minVersion == null || maxVersion == null)
+ continue;
+
+ let appRange = { appID: appID,
+ appMinVersion: minVersion,
+ appMaxVersion: maxVersion };
+
+ // Only use Toolkit app ranges if no ranges match the application ID.
+ if (appID == TOOLKIT_ID)
+ toolkitAppRange = appRange;
+ else
+ return appRange;
+ }
+ return toolkitAppRange;
+ }
+
+ function parseRangeNode(aNode) {
+ let type = aNode.getAttribute("type");
+ // Only "incompatible" (blacklisting) is supported for now.
+ if (type != "incompatible") {
+ logger.debug("Compatibility override of unsupported type found.");
+ return null;
+ }
+
+ let override = new AddonManagerPrivate.AddonCompatibilityOverride(type);
+
+ override.minVersion = this._getDirectDescendantTextContent(aNode, "min_version");
+ override.maxVersion = this._getDirectDescendantTextContent(aNode, "max_version");
+
+ if (!override.minVersion) {
+ logger.debug("Compatibility override is missing min_version.");
+ return null;
+ }
+ if (!override.maxVersion) {
+ logger.debug("Compatibility override is missing max_version.");
+ return null;
+ }
+
+ let appRanges = aNode.querySelectorAll("compatible_applications > application");
+ let appRange = findMatchingAppRange.bind(this)(appRanges);
+ if (!appRange) {
+ logger.debug("Compatibility override is missing a valid application range.");
+ return null;
+ }
+
+ override.appID = appRange.appID;
+ override.appMinVersion = appRange.appMinVersion;
+ override.appMaxVersion = appRange.appMaxVersion;
+
+ return override;
+ }
+
+ let rangeNodes = aElement.querySelectorAll("version_ranges > version_range");
+ compat.compatRanges = Array.map(rangeNodes, parseRangeNode.bind(this))
+ .filter(function compatRangesFilter(aItem) !!aItem);
+ if (compat.compatRanges.length == 0)
+ return;
+
+ aResultObj[compat.id] = compat;
+ },
+
+ // Parses addon_compatibility elements.
+ _parseAddonCompatData: function AddonRepo_parseAddonCompatData(aElements) {
+ let compatData = {};
+ Array.forEach(aElements, this._parseAddonCompatElement.bind(this, compatData));
+ return compatData;
+ },
+
+ // Begins a new search if one isn't currently executing
+ _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) {
+ if (this._searching || aURI == null || aMaxResults <= 0) {
+ logger.warn("AddonRepository search failed: searching " + this._searching + " aURI " + aURI +
+ " aMaxResults " + aMaxResults);
+ aCallback.searchFailed();
+ return;
+ }
+
+ this._searching = true;
+ this._callback = aCallback;
+ this._maxResults = aMaxResults;
+
+ logger.debug("Requesting " + aURI);
+
+ this._request = new XHRequest();
+ this._request.mozBackgroundRequest = true;
+ this._request.open("GET", aURI, true);
+ this._request.overrideMimeType("text/xml");
+ if (aTimeout) {
+ this._request.timeout = aTimeout;
+ }
+
+ this._request.addEventListener("error", aEvent => this._reportFailure(), false);
+ this._request.addEventListener("timeout", aEvent => this._reportFailure(), false);
+ this._request.addEventListener("load", aEvent => {
+ logger.debug("Got metadata search load event");
+ let request = aEvent.target;
+ let responseXML = request.responseXML;
+
+ if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
+ (request.status != 200 && request.status != 0)) {
+ this._reportFailure();
+ return;
+ }
+
+ let documentElement = responseXML.documentElement;
+ let elements = documentElement.getElementsByTagName("addon");
+ let totalResults = elements.length;
+ let parsedTotalResults = parseInt(documentElement.getAttribute("total_results"));
+ // Parsed value of total results only makes sense if >= elements.length
+ if (parsedTotalResults >= totalResults)
+ totalResults = parsedTotalResults;
+
+ let compatElements = documentElement.getElementsByTagName("addon_compatibility");
+ let compatData = this._parseAddonCompatData(compatElements);
+
+ aHandleResults(elements, totalResults, compatData);
+ }, false);
+ this._request.send(null);
+ },
+
+ // Gets the id's of local add-ons, and the sourceURI's of local installs,
+ // passing the results to aCallback
+ _getLocalAddonIds: function AddonRepo_getLocalAddonIds(aCallback) {
+ let self = this;
+ let localAddonIds = {ids: null, sourceURIs: null};
+
+ AddonManager.getAllAddons(function getLocalAddonIds_getAllAddons(aAddons) {
+ // Tycho: localAddonIds.ids = [a.id for each (a in aAddons)];
+ localAddonIds.ids = [];
+
+ for each(let a in aAddons) {
+ localAddonIds.ids.push(a.id);
+ }
+
+ if (localAddonIds.sourceURIs)
+ aCallback(localAddonIds);
+ });
+
+ AddonManager.getAllInstalls(function getLocalAddonIds_getAllInstalls(aInstalls) {
+ localAddonIds.sourceURIs = [];
+ aInstalls.forEach(function(aInstall) {
+ if (aInstall.state != AddonManager.STATE_AVAILABLE)
+ localAddonIds.sourceURIs.push(aInstall.sourceURI.spec);
+ });
+
+ if (localAddonIds.ids)
+ aCallback(localAddonIds);
+ });
+ },
+
+ // Create url from preference, returning null if preference does not exist
+ _formatURLPref: function AddonRepo_formatURLPref(aPreference, aSubstitutions) {
+ let url = Services.prefs.getCharPref(aPreference, "");
+ if (!url) {
+ logger.warn("_formatURLPref: Couldn't get pref: " + aPreference);
+ return null;
+ }
+
+ url = url.replace(/%([A-Z_]+)%/g, function urlSubstitution(aMatch, aKey) {
+ return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch;
+ });
+
+ return Services.urlFormatter.formatURL(url);
+ },
+
+ // Find a AddonCompatibilityOverride that matches a given aAddonVersion and
+ // application/platform version.
+ findMatchingCompatOverride: function AddonRepo_findMatchingCompatOverride(aAddonVersion,
+ aCompatOverrides,
+ aAppVersion,
+ aPlatformVersion) {
+ for (let override of aCompatOverrides) {
+
+ let appVersion = null;
+ if (override.appID == TOOLKIT_ID)
+ appVersion = aPlatformVersion || Services.appinfo.platformVersion;
+ else
+ appVersion = aAppVersion || Services.appinfo.version;
+
+ if (Services.vc.compare(override.minVersion, aAddonVersion) <= 0 &&
+ Services.vc.compare(aAddonVersion, override.maxVersion) <= 0 &&
+ Services.vc.compare(override.appMinVersion, appVersion) <= 0 &&
+ Services.vc.compare(appVersion, override.appMaxVersion) <= 0) {
+ return override;
+ }
+ }
+ return null;
+ },
+
+ flush: function() {
+ return AddonDatabase.flush();
+ }
+};
+
+var AddonDatabase = {
+ // false if there was an unrecoverable error opening the database
+ databaseOk: true,
+
+ connectionPromise: null,
+ // the in-memory database
+ DB: BLANK_DB(),
+
+ /**
+ * A getter to retrieve the path to the DB
+ */
+ get jsonFile() {
+ return OS.Path.join(OS.Constants.Path.profileDir, FILE_DATABASE);
+ },
+
+ /**
+ * Asynchronously opens a new connection to the database file.
+ *
+ * @return {Promise} a promise that resolves to the database.
+ */
+ openConnection: function() {
+ if (!this.connectionPromise) {
+ this.connectionPromise = Task.spawn(function*() {
+ this.DB = BLANK_DB();
+
+ let inputDB, schema;
+
+ try {
+ let data = yield OS.File.read(this.jsonFile, { encoding: "utf-8"})
+ inputDB = JSON.parse(data);
+
+ if (!inputDB.hasOwnProperty("addons") ||
+ !Array.isArray(inputDB.addons)) {
+ throw new Error("No addons array.");
+ }
+
+ if (!inputDB.hasOwnProperty("schema")) {
+ throw new Error("No schema specified.");
+ }
+
+ schema = parseInt(inputDB.schema, 10);
+
+ if (!Number.isInteger(schema) ||
+ schema < DB_MIN_JSON_SCHEMA) {
+ throw new Error("Invalid schema value.");
+ }
+ } catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ logger.debug("No " + FILE_DATABASE + " found.");
+
+ // Create a blank addons.json file
+ this._saveDBToDisk();
+
+ let dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA, 0);
+
+ if (dbSchema < DB_MIN_JSON_SCHEMA) {
+ let results = yield new Promise((resolve, reject) => {
+ AddonRepository_SQLiteMigrator.migrate(resolve);
+ });
+
+ if (results.length) {
+ yield this._insertAddons(results);
+ }
+
+ Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
+ }
+
+ return this.DB;
+ } catch (e) {
+ logger.error("Malformed " + FILE_DATABASE + ": " + e);
+ this.databaseOk = false;
+
+ return this.DB;
+ }
+
+ Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
+
+ // We use _insertAddon manually instead of calling
+ // insertAddons to avoid the write to disk which would
+ // be a waste since this is the data that was just read.
+ for (let addon of inputDB.addons) {
+ this._insertAddon(addon);
+ }
+
+ return this.DB;
+ }.bind(this));
+ }
+
+ return this.connectionPromise;
+ },
+
+ /**
+ * A lazy getter for the database connection.
+ */
+ get connection() {
+ return this.openConnection();
+ },
+
+ /**
+ * Asynchronously shuts down the database connection and releases all
+ * cached objects
+ *
+ * @param aCallback
+ * An optional callback to call once complete
+ * @param aSkipFlush
+ * An optional boolean to skip flushing data to disk. Useful
+ * when the database is going to be deleted afterwards.
+ */
+ shutdown: function AD_shutdown(aSkipFlush) {
+ this.databaseOk = true;
+
+ if (!this.connectionPromise) {
+ return Promise.resolve();
+ }
+
+ this.connectionPromise = null;
+
+ if (aSkipFlush) {
+ return Promise.resolve();
+ } else {
+ return this.Writer.flush();
+ }
+ },
+
+ /**
+ * Asynchronously deletes the database, shutting down the connection
+ * first if initialized
+ *
+ * @param aCallback
+ * An optional callback to call once complete
+ * @return Promise{null} resolves when the database has been deleted
+ */
+ delete: function AD_delete(aCallback) {
+ this.DB = BLANK_DB();
+
+ this._deleting = this.Writer.flush()
+ .then(null, () => {})
+ // shutdown(true) never rejects
+ .then(() => this.shutdown(true))
+ .then(() => OS.File.remove(this.jsonFile, {}))
+ .then(null, error => logger.error("Unable to delete Addon Repository file " +
+ this.jsonFile, error))
+ .then(() => this._deleting = null)
+ .then(aCallback);
+ return this._deleting;
+ },
+
+ toJSON: function AD_toJSON() {
+ let json = {
+ schema: this.DB.schema,
+ addons: []
+ }
+
+ for (let [, value] of this.DB.addons)
+ json.addons.push(value);
+
+ return json;
+ },
+
+ /*
+ * This is a deferred task writer that is used
+ * to batch operations done within 50ms of each
+ * other and thus generating only one write to disk
+ */
+ get Writer() {
+ delete this.Writer;
+ this.Writer = new DeferredSave(
+ this.jsonFile,
+ () => { return JSON.stringify(this); },
+ DB_BATCH_TIMEOUT_MS
+ );
+ return this.Writer;
+ },
+
+ /**
+ * Flush any pending I/O on the addons.json file
+ * @return: Promise{null}
+ * Resolves when the pending I/O (writing out or deleting
+ * addons.json) completes
+ */
+ flush: function() {
+ if (this._deleting) {
+ return this._deleting;
+ }
+ return this.Writer.flush();
+ },
+
+ /**
+ * Asynchronously retrieve all add-ons from the database
+ * @return: Promise{Map}
+ * Resolves when the add-ons are retrieved from the database
+ */
+ retrieveStoredData: function (){
+ return this.openConnection().then(db => db.addons);
+ },
+
+ /**
+ * Asynchronously repopulates the database so it only contains the
+ * specified add-ons
+ *
+ * @param aAddons
+ * The array of add-ons to repopulate the database with
+ * @param aCallback
+ * An optional callback to call once complete
+ */
+ repopulate: function AD_repopulate(aAddons, aCallback) {
+ this.DB.addons.clear();
+ this.insertAddons(aAddons, function repopulate_insertAddons() {
+ let now = Math.round(Date.now() / 1000);
+ logger.debug("Cache repopulated, setting " + PREF_METADATA_LASTUPDATE + " to " + now);
+ Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, now);
+ if (aCallback)
+ aCallback();
+ });
+ },
+
+ /**
+ * Asynchronously inserts an array of add-ons into the database
+ *
+ * @param aAddons
+ * The array of add-ons to insert
+ * @param aCallback
+ * An optional callback to call once complete
+ */
+ insertAddons: Task.async(function* (aAddons, aCallback) {
+ yield this.openConnection();
+ yield this._insertAddons(aAddons, aCallback);
+ }),
+
+ _insertAddons: Task.async(function* (aAddons, aCallback) {
+ for (let addon of aAddons) {
+ this._insertAddon(addon);
+ }
+
+ yield this._saveDBToDisk();
+ aCallback && aCallback();
+ }),
+
+ /**
+ * Inserts an individual add-on into the database. If the add-on already
+ * exists in the database (by id), then the specified add-on will not be
+ * inserted.
+ *
+ * @param aAddon
+ * The add-on to insert into the database
+ * @param aCallback
+ * The callback to call once complete
+ */
+ _insertAddon: function AD__insertAddon(aAddon) {
+ let newAddon = this._parseAddon(aAddon);
+ if (!newAddon ||
+ !newAddon.id ||
+ this.DB.addons.has(newAddon.id))
+ return;
+
+ this.DB.addons.set(newAddon.id, newAddon);
+ },
+
+ /*
+ * Creates an AddonSearchResult by parsing an object structure
+ * retrieved from the DB JSON representation.
+ *
+ * @param aObj
+ * The object to parse
+ * @return Returns an AddonSearchResult object.
+ */
+ _parseAddon: function (aObj) {
+ if (aObj instanceof AddonSearchResult)
+ return aObj;
+
+ let id = aObj.id;
+ if (!aObj.id)
+ return null;
+
+ let addon = new AddonSearchResult(id);
+
+ for (let [expectedProperty,] of Iterator(AddonSearchResult.prototype)) {
+ if (!(expectedProperty in aObj) ||
+ typeof(aObj[expectedProperty]) === "function")
+ continue;
+
+ let value = aObj[expectedProperty];
+
+ try {
+ switch (expectedProperty) {
+ case "sourceURI":
+ addon.sourceURI = value ? NetUtil.newURI(value) : null;
+ break;
+
+ case "creator":
+ addon.creator = value
+ ? this._makeDeveloper(value)
+ : null;
+ break;
+
+ case "updateDate":
+ addon.updateDate = value ? new Date(value) : null;
+ break;
+
+ case "developers":
+ if (!addon.developers) addon.developers = [];
+ for (let developer of value) {
+ addon.developers.push(this._makeDeveloper(developer));
+ }
+ break;
+
+ case "screenshots":
+ if (!addon.screenshots) addon.screenshots = [];
+ for (let screenshot of value) {
+ addon.screenshots.push(this._makeScreenshot(screenshot));
+ }
+ break;
+
+ case "compatibilityOverrides":
+ if (!addon.compatibilityOverrides) addon.compatibilityOverrides = [];
+ for (let override of value) {
+ addon.compatibilityOverrides.push(
+ this._makeCompatOverride(override)
+ );
+ }
+ break;
+
+ case "icons":
+ if (!addon.icons) addon.icons = {};
+ for (let [size, url] of Iterator(aObj.icons)) {
+ addon.icons[size] = url;
+ }
+ break;
+
+ case "iconURL":
+ break;
+
+ default:
+ addon[expectedProperty] = value;
+ }
+ } catch (ex) {
+ logger.warn("Error in parsing property value for " + expectedProperty + " | " + ex);
+ }
+
+ // delete property from obj to indicate we've already
+ // handled it. The remaining public properties will
+ // be stored separately and just passed through to
+ // be written back to the DB.
+ delete aObj[expectedProperty];
+ }
+
+ // Copy remaining properties to a separate object
+ // to prevent accidental access on downgraded versions.
+ // The properties will be merged in the same object
+ // prior to being written back through toJSON.
+ for (let remainingProperty of Object.keys(aObj)) {
+ switch (typeof(aObj[remainingProperty])) {
+ case "boolean":
+ case "number":
+ case "string":
+ case "object":
+ // these types are accepted
+ break;
+ default:
+ continue;
+ }
+
+ if (!remainingProperty.startsWith("_"))
+ addon._unsupportedProperties[remainingProperty] =
+ aObj[remainingProperty];
+ }
+
+ return addon;
+ },
+
+ /**
+ * Write the in-memory DB to disk, after waiting for
+ * the DB_BATCH_TIMEOUT_MS timeout.
+ *
+ * @return Promise A promise that resolves after the
+ * write to disk has completed.
+ */
+ _saveDBToDisk: function() {
+ return this.Writer.saveChanges().then(
+ null,
+ e => logger.error("SaveDBToDisk failed", e));
+ },
+
+ /**
+ * Make a developer object from a vanilla
+ * JS object from the JSON database
+ *
+ * @param aObj
+ * The JS object to use
+ * @return The created developer
+ */
+ _makeDeveloper: function (aObj) {
+ let name = aObj.name;
+ let url = aObj.url;
+ return new AddonManagerPrivate.AddonAuthor(name, url);
+ },
+
+ /**
+ * Make a screenshot object from a vanilla
+ * JS object from the JSON database
+ *
+ * @param aObj
+ * The JS object to use
+ * @return The created screenshot
+ */
+ _makeScreenshot: function (aObj) {
+ let url = aObj.url;
+ let width = aObj.width;
+ let height = aObj.height;
+ let thumbnailURL = aObj.thumbnailURL;
+ let thumbnailWidth = aObj.thumbnailWidth;
+ let thumbnailHeight = aObj.thumbnailHeight;
+ let caption = aObj.caption;
+ return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
+ thumbnailWidth, thumbnailHeight, caption);
+ },
+
+ /**
+ * Make a CompatibilityOverride from a vanilla
+ * JS object from the JSON database
+ *
+ * @param aObj
+ * The JS object to use
+ * @return The created CompatibilityOverride
+ */
+ _makeCompatOverride: function (aObj) {
+ let type = aObj.type;
+ let minVersion = aObj.minVersion;
+ let maxVersion = aObj.maxVersion;
+ let appID = aObj.appID;
+ let appMinVersion = aObj.appMinVersion;
+ let appMaxVersion = aObj.appMaxVersion;
+ return new AddonManagerPrivate.AddonCompatibilityOverride(type,
+ minVersion,
+ maxVersion,
+ appID,
+ appMinVersion,
+ appMaxVersion);
+ },
+};
diff --git a/components/extensions/src/AddonRepository_SQLiteMigrator.jsm b/components/extensions/src/AddonRepository_SQLiteMigrator.jsm
new file mode 100644
index 000000000..66147b9aa
--- /dev/null
+++ b/components/extensions/src/AddonRepository_SQLiteMigrator.jsm
@@ -0,0 +1,522 @@
+/* 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+const KEY_PROFILEDIR = "ProfD";
+const FILE_DATABASE = "addons.sqlite";
+const LAST_DB_SCHEMA = 4;
+
+// Add-on properties present in the columns of the database
+const PROP_SINGLE = ["id", "type", "name", "version", "creator", "description",
+ "fullDescription", "developerComments", "eula",
+ "homepageURL", "supportURL", "contributionURL",
+ "contributionAmount", "averageRating", "reviewCount",
+ "reviewURL", "totalDownloads", "weeklyDownloads",
+ "dailyUsers", "sourceURI", "repositoryStatus", "size",
+ "updateDate"];
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.repository.sqlmigrator";
+
+// Create a new logger for use by the Addons Repository SQL Migrator
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"];
+
+
+this.AddonRepository_SQLiteMigrator = {
+
+ /**
+ * Migrates data from a previous SQLite version of the
+ * database to the JSON version.
+ *
+ * @param structFunctions an object that contains functions
+ * to create the various objects used
+ * in the new JSON format
+ * @param aCallback A callback to be called when migration
+ * finishes, with the results in an array
+ * @returns bool True if a migration will happen (DB was
+ * found and succesfully opened)
+ */
+ migrate: function(aCallback) {
+ if (!this._openConnection()) {
+ this._closeConnection();
+ aCallback([]);
+ return false;
+ }
+
+ logger.debug("Importing addon repository from previous " + FILE_DATABASE + " storage.");
+
+ this._retrieveStoredData((results) => {
+ this._closeConnection();
+ // Tycho: let resultArray = [addon for ([,addon] of Iterator(results))];
+ let resultArray = [];
+ for (let [,addon] of Iterator(results)) {
+ resultArray.push(addon);
+ }
+ logger.debug(resultArray.length + " addons imported.")
+ aCallback(resultArray);
+ });
+
+ return true;
+ },
+
+ /**
+ * Synchronously opens a new connection to the database file.
+ *
+ * @return bool Whether the DB was opened successfully.
+ */
+ _openConnection: function AD_openConnection() {
+ delete this.connection;
+
+ let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
+ if (!dbfile.exists())
+ return false;
+
+ try {
+ this.connection = Services.storage.openUnsharedDatabase(dbfile);
+ } catch (e) {
+ return false;
+ }
+
+ this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
+
+ // Any errors in here should rollback
+ try {
+ this.connection.beginTransaction();
+
+ switch (this.connection.schemaVersion) {
+ case 0:
+ return false;
+
+ case 1:
+ logger.debug("Upgrading database schema to version 2");
+ this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN width INTEGER");
+ this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN height INTEGER");
+ this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailWidth INTEGER");
+ this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailHeight INTEGER");
+ case 2:
+ logger.debug("Upgrading database schema to version 3");
+ this.connection.createTable("compatibility_override",
+ "addon_internal_id INTEGER, " +
+ "num INTEGER, " +
+ "type TEXT, " +
+ "minVersion TEXT, " +
+ "maxVersion TEXT, " +
+ "appID TEXT, " +
+ "appMinVersion TEXT, " +
+ "appMaxVersion TEXT, " +
+ "PRIMARY KEY (addon_internal_id, num)");
+ case 3:
+ logger.debug("Upgrading database schema to version 4");
+ this.connection.createTable("icon",
+ "addon_internal_id INTEGER, " +
+ "size INTEGER, " +
+ "url TEXT, " +
+ "PRIMARY KEY (addon_internal_id, size)");
+ this._createIndices();
+ this._createTriggers();
+ this.connection.schemaVersion = LAST_DB_SCHEMA;
+ case LAST_DB_SCHEMA:
+ break;
+ default:
+ return false;
+ }
+ this.connection.commitTransaction();
+ } catch (e) {
+ logger.error("Failed to open " + FILE_DATABASE + ". Data import will not happen.", e);
+ this.logSQLError(this.connection.lastError, this.connection.lastErrorString);
+ this.connection.rollbackTransaction();
+ return false;
+ }
+
+ return true;
+ },
+
+ _closeConnection: function() {
+ for each (let stmt in this.asyncStatementsCache)
+ stmt.finalize();
+ this.asyncStatementsCache = {};
+
+ if (this.connection)
+ this.connection.asyncClose();
+
+ delete this.connection;
+ },
+
+ /**
+ * Asynchronously retrieve all add-ons from the database, and pass it
+ * to the specified callback
+ *
+ * @param aCallback
+ * The callback to pass the add-ons back to
+ */
+ _retrieveStoredData: function AD_retrieveStoredData(aCallback) {
+ let self = this;
+ let addons = {};
+
+ // Retrieve all data from the addon table
+ function getAllAddons() {
+ self.getAsyncStatement("getAllAddons").executeAsync({
+ handleResult: function getAllAddons_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let internal_id = row.getResultByName("internal_id");
+ addons[internal_id] = self._makeAddonFromAsyncRow(row);
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllAddons_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving add-ons from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ getAllDevelopers();
+ }
+ });
+ }
+
+ // Retrieve all data from the developer table
+ function getAllDevelopers() {
+ self.getAsyncStatement("getAllDevelopers").executeAsync({
+ handleResult: function getAllDevelopers_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let addon_internal_id = row.getResultByName("addon_internal_id");
+ if (!(addon_internal_id in addons)) {
+ logger.warn("Found a developer not linked to an add-on in database");
+ continue;
+ }
+
+ let addon = addons[addon_internal_id];
+ if (!addon.developers)
+ addon.developers = [];
+
+ addon.developers.push(self._makeDeveloperFromAsyncRow(row));
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllDevelopers_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving developers from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ getAllScreenshots();
+ }
+ });
+ }
+
+ // Retrieve all data from the screenshot table
+ function getAllScreenshots() {
+ self.getAsyncStatement("getAllScreenshots").executeAsync({
+ handleResult: function getAllScreenshots_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let addon_internal_id = row.getResultByName("addon_internal_id");
+ if (!(addon_internal_id in addons)) {
+ logger.warn("Found a screenshot not linked to an add-on in database");
+ continue;
+ }
+
+ let addon = addons[addon_internal_id];
+ if (!addon.screenshots)
+ addon.screenshots = [];
+ addon.screenshots.push(self._makeScreenshotFromAsyncRow(row));
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllScreenshots_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving screenshots from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ getAllCompatOverrides();
+ }
+ });
+ }
+
+ function getAllCompatOverrides() {
+ self.getAsyncStatement("getAllCompatOverrides").executeAsync({
+ handleResult: function getAllCompatOverrides_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let addon_internal_id = row.getResultByName("addon_internal_id");
+ if (!(addon_internal_id in addons)) {
+ logger.warn("Found a compatibility override not linked to an add-on in database");
+ continue;
+ }
+
+ let addon = addons[addon_internal_id];
+ if (!addon.compatibilityOverrides)
+ addon.compatibilityOverrides = [];
+ addon.compatibilityOverrides.push(self._makeCompatOverrideFromAsyncRow(row));
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllCompatOverrides_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving compatibility overrides from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ getAllIcons();
+ }
+ });
+ }
+
+ function getAllIcons() {
+ self.getAsyncStatement("getAllIcons").executeAsync({
+ handleResult: function getAllIcons_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let addon_internal_id = row.getResultByName("addon_internal_id");
+ if (!(addon_internal_id in addons)) {
+ logger.warn("Found an icon not linked to an add-on in database");
+ continue;
+ }
+
+ let addon = addons[addon_internal_id];
+ let { size, url } = self._makeIconFromAsyncRow(row);
+ addon.icons[size] = url;
+ if (size == 32)
+ addon.iconURL = url;
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllIcons_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving icons from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ let returnedAddons = {};
+ for each (let addon in addons)
+ returnedAddons[addon.id] = addon;
+ aCallback(returnedAddons);
+ }
+ });
+ }
+
+ // Begin asynchronous process
+ getAllAddons();
+ },
+
+ // A cache of statements that are used and need to be finalized on shutdown
+ asyncStatementsCache: {},
+
+ /**
+ * Gets a cached async statement or creates a new statement if it doesn't
+ * already exist.
+ *
+ * @param aKey
+ * A unique key to reference the statement
+ * @return a mozIStorageAsyncStatement for the SQL corresponding to the
+ * unique key
+ */
+ getAsyncStatement: function AD_getAsyncStatement(aKey) {
+ if (aKey in this.asyncStatementsCache)
+ return this.asyncStatementsCache[aKey];
+
+ let sql = this.queries[aKey];
+ try {
+ return this.asyncStatementsCache[aKey] = this.connection.createAsyncStatement(sql);
+ } catch (e) {
+ logger.error("Error creating statement " + aKey + " (" + sql + ")");
+ throw Components.Exception("Error creating statement " + aKey + " (" + sql + "): " + e,
+ e.result);
+ }
+ },
+
+ // The queries used by the database
+ queries: {
+ getAllAddons: "SELECT internal_id, id, type, name, version, " +
+ "creator, creatorURL, description, fullDescription, " +
+ "developerComments, eula, homepageURL, supportURL, " +
+ "contributionURL, contributionAmount, averageRating, " +
+ "reviewCount, reviewURL, totalDownloads, weeklyDownloads, " +
+ "dailyUsers, sourceURI, repositoryStatus, size, updateDate " +
+ "FROM addon",
+
+ getAllDevelopers: "SELECT addon_internal_id, name, url FROM developer " +
+ "ORDER BY addon_internal_id, num",
+
+ getAllScreenshots: "SELECT addon_internal_id, url, width, height, " +
+ "thumbnailURL, thumbnailWidth, thumbnailHeight, caption " +
+ "FROM screenshot ORDER BY addon_internal_id, num",
+
+ getAllCompatOverrides: "SELECT addon_internal_id, type, minVersion, " +
+ "maxVersion, appID, appMinVersion, appMaxVersion " +
+ "FROM compatibility_override " +
+ "ORDER BY addon_internal_id, num",
+
+ getAllIcons: "SELECT addon_internal_id, size, url FROM icon " +
+ "ORDER BY addon_internal_id, size",
+ },
+
+ /**
+ * Make add-on structure from an asynchronous row.
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return The created add-on
+ */
+ _makeAddonFromAsyncRow: function AD__makeAddonFromAsyncRow(aRow) {
+ // This is intentionally not an AddonSearchResult object in order
+ // to allow AddonDatabase._parseAddon to parse it, same as if it
+ // was read from the JSON database.
+
+ let addon = { icons: {} };
+
+ for (let prop of PROP_SINGLE) {
+ addon[prop] = aRow.getResultByName(prop)
+ };
+
+ return addon;
+ },
+
+ /**
+ * Make a developer from an asynchronous row
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return The created developer
+ */
+ _makeDeveloperFromAsyncRow: function AD__makeDeveloperFromAsyncRow(aRow) {
+ let name = aRow.getResultByName("name");
+ let url = aRow.getResultByName("url")
+ return new AddonManagerPrivate.AddonAuthor(name, url);
+ },
+
+ /**
+ * Make a screenshot from an asynchronous row
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return The created screenshot
+ */
+ _makeScreenshotFromAsyncRow: function AD__makeScreenshotFromAsyncRow(aRow) {
+ let url = aRow.getResultByName("url");
+ let width = aRow.getResultByName("width");
+ let height = aRow.getResultByName("height");
+ let thumbnailURL = aRow.getResultByName("thumbnailURL");
+ let thumbnailWidth = aRow.getResultByName("thumbnailWidth");
+ let thumbnailHeight = aRow.getResultByName("thumbnailHeight");
+ let caption = aRow.getResultByName("caption");
+ return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
+ thumbnailWidth, thumbnailHeight, caption);
+ },
+
+ /**
+ * Make a CompatibilityOverride from an asynchronous row
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return The created CompatibilityOverride
+ */
+ _makeCompatOverrideFromAsyncRow: function AD_makeCompatOverrideFromAsyncRow(aRow) {
+ let type = aRow.getResultByName("type");
+ let minVersion = aRow.getResultByName("minVersion");
+ let maxVersion = aRow.getResultByName("maxVersion");
+ let appID = aRow.getResultByName("appID");
+ let appMinVersion = aRow.getResultByName("appMinVersion");
+ let appMaxVersion = aRow.getResultByName("appMaxVersion");
+ return new AddonManagerPrivate.AddonCompatibilityOverride(type,
+ minVersion,
+ maxVersion,
+ appID,
+ appMinVersion,
+ appMaxVersion);
+ },
+
+ /**
+ * Make an icon from an asynchronous row
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return An object containing the size and URL of the icon
+ */
+ _makeIconFromAsyncRow: function AD_makeIconFromAsyncRow(aRow) {
+ let size = aRow.getResultByName("size");
+ let url = aRow.getResultByName("url");
+ return { size: size, url: url };
+ },
+
+ /**
+ * A helper function to log an SQL error.
+ *
+ * @param aError
+ * The storage error code associated with the error
+ * @param aErrorString
+ * An error message
+ */
+ logSQLError: function AD_logSQLError(aError, aErrorString) {
+ logger.error("SQL error " + aError + ": " + aErrorString);
+ },
+
+ /**
+ * A helper function to log any errors that occur during async statements.
+ *
+ * @param aError
+ * A mozIStorageError to log
+ */
+ asyncErrorLogger: function AD_asyncErrorLogger(aError) {
+ logger.error("Async SQL error " + aError.result + ": " + aError.message);
+ },
+
+ /**
+ * Synchronously creates the triggers in the database.
+ */
+ _createTriggers: function AD__createTriggers() {
+ this.connection.executeSimpleSQL("DROP TRIGGER IF EXISTS delete_addon");
+ this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
+ "ON addon BEGIN " +
+ "DELETE FROM developer WHERE addon_internal_id=old.internal_id; " +
+ "DELETE FROM screenshot WHERE addon_internal_id=old.internal_id; " +
+ "DELETE FROM compatibility_override WHERE addon_internal_id=old.internal_id; " +
+ "DELETE FROM icon WHERE addon_internal_id=old.internal_id; " +
+ "END");
+ },
+
+ /**
+ * Synchronously creates the indices in the database.
+ */
+ _createIndices: function AD__createIndices() {
+ this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS developer_idx " +
+ "ON developer (addon_internal_id)");
+ this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS screenshot_idx " +
+ "ON screenshot (addon_internal_id)");
+ this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS compatibility_override_idx " +
+ "ON compatibility_override (addon_internal_id)");
+ this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS icon_idx " +
+ "ON icon (addon_internal_id)");
+ }
+}
diff --git a/components/extensions/src/AddonUpdateChecker.jsm b/components/extensions/src/AddonUpdateChecker.jsm
new file mode 100644
index 000000000..4fce84095
--- /dev/null
+++ b/components/extensions/src/AddonUpdateChecker.jsm
@@ -0,0 +1,955 @@
+/* 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/. */
+
+/**
+ * The AddonUpdateChecker is responsible for retrieving the update information
+ * from an add-on's remote update manifest.
+ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AddonUpdateChecker" ];
+
+const TIMEOUT = 60 * 1000;
+const PREFIX_NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
+const PREFIX_ITEM = "urn:mozilla:item:";
+const PREFIX_EXTENSION = "urn:mozilla:extension:";
+const PREFIX_THEME = "urn:mozilla:theme:";
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+
+const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
+const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
+ "resource://gre/modules/ServiceRequest.jsm");
+
+
+// Shared code for suppressing bad cert dialogs.
+XPCOMUtils.defineLazyGetter(this, "CertUtils", function() {
+ let certUtils = {};
+ Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
+ return certUtils;
+});
+
+var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
+ getService(Ci.nsIRDFService);
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.update-checker";
+
+// Create a new logger for use by the Addons Update Checker
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+/**
+ * A serialisation method for RDF data that produces an identical string
+ * for matching RDF graphs.
+ * The serialisation is not complete, only assertions stemming from a given
+ * resource are included, multiple references to the same resource are not
+ * permitted, and the RDF prolog and epilog are not included.
+ * RDF Blob and Date literals are not supported.
+ */
+function RDFSerializer() {
+ this.cUtils = Cc["@mozilla.org/rdf/container-utils;1"].
+ getService(Ci.nsIRDFContainerUtils);
+ this.resources = [];
+}
+
+RDFSerializer.prototype = {
+ INDENT: " ", // The indent used for pretty-printing
+ resources: null, // Array of the resources that have been found
+
+ /**
+ * Escapes characters from a string that should not appear in XML.
+ *
+ * @param aString
+ * The string to be escaped
+ * @return a string with all characters invalid in XML character data
+ * converted to entity references.
+ */
+ escapeEntities: function(aString) {
+ aString = aString.replace(/&/g, "&amp;");
+ aString = aString.replace(/</g, "&lt;");
+ aString = aString.replace(/>/g, "&gt;");
+ return aString.replace(/"/g, "&quot;");
+ },
+
+ /**
+ * Serializes all the elements of an RDF container.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aContainer
+ * The RDF container to output the child elements of
+ * @param aIndent
+ * The current level of indent for pretty-printing
+ * @return a string containing the serialized elements.
+ */
+ serializeContainerItems: function(aDs, aContainer, aIndent) {
+ var result = "";
+ var items = aContainer.GetElements();
+ while (items.hasMoreElements()) {
+ var item = items.getNext().QueryInterface(Ci.nsIRDFResource);
+ result += aIndent + "<RDF:li>\n"
+ result += this.serializeResource(aDs, item, aIndent + this.INDENT);
+ result += aIndent + "</RDF:li>\n"
+ }
+ return result;
+ },
+
+ /**
+ * Serializes all em:* (see EM_NS) properties of an RDF resource except for
+ * the em:signature property. As this serialization is to be compared against
+ * the manifest signature it cannot contain the em:signature property itself.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aResource
+ * The RDF resource that contains the properties to serialize
+ * @param aIndent
+ * The current level of indent for pretty-printing
+ * @return a string containing the serialized properties.
+ * @throws if the resource contains a property that cannot be serialized
+ */
+ serializeResourceProperties: function(aDs, aResource, aIndent) {
+ var result = "";
+ var items = [];
+ var arcs = aDs.ArcLabelsOut(aResource);
+ while (arcs.hasMoreElements()) {
+ var arc = arcs.getNext().QueryInterface(Ci.nsIRDFResource);
+ if (arc.ValueUTF8.substring(0, PREFIX_NS_EM.length) != PREFIX_NS_EM)
+ continue;
+ var prop = arc.ValueUTF8.substring(PREFIX_NS_EM.length);
+ if (prop == "signature")
+ continue;
+
+ var targets = aDs.GetTargets(aResource, arc, true);
+ while (targets.hasMoreElements()) {
+ var target = targets.getNext();
+ if (target instanceof Ci.nsIRDFResource) {
+ var item = aIndent + "<em:" + prop + ">\n";
+ item += this.serializeResource(aDs, target, aIndent + this.INDENT);
+ item += aIndent + "</em:" + prop + ">\n";
+ items.push(item);
+ }
+ else if (target instanceof Ci.nsIRDFLiteral) {
+ items.push(aIndent + "<em:" + prop + ">" +
+ this.escapeEntities(target.Value) + "</em:" + prop + ">\n");
+ }
+ else if (target instanceof Ci.nsIRDFInt) {
+ items.push(aIndent + "<em:" + prop + " NC:parseType=\"Integer\">" +
+ target.Value + "</em:" + prop + ">\n");
+ }
+ else {
+ throw Components.Exception("Cannot serialize unknown literal type");
+ }
+ }
+ }
+ items.sort();
+ result += items.join("");
+ return result;
+ },
+
+ /**
+ * Recursively serializes an RDF resource and all resources it links to.
+ * This will only output EM_NS properties and will ignore any em:signature
+ * property.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aResource
+ * The RDF resource to serialize
+ * @param aIndent
+ * The current level of indent for pretty-printing. If undefined no
+ * indent will be added
+ * @return a string containing the serialized resource.
+ * @throws if the RDF data contains multiple references to the same resource.
+ */
+ serializeResource: function(aDs, aResource, aIndent) {
+ if (this.resources.indexOf(aResource) != -1 ) {
+ // We cannot output multiple references to the same resource.
+ throw Components.Exception("Cannot serialize multiple references to " + aResource.Value);
+ }
+ if (aIndent === undefined)
+ aIndent = "";
+
+ this.resources.push(aResource);
+ var container = null;
+ var type = "Description";
+ if (this.cUtils.IsSeq(aDs, aResource)) {
+ type = "Seq";
+ container = this.cUtils.MakeSeq(aDs, aResource);
+ }
+ else if (this.cUtils.IsAlt(aDs, aResource)) {
+ type = "Alt";
+ container = this.cUtils.MakeAlt(aDs, aResource);
+ }
+ else if (this.cUtils.IsBag(aDs, aResource)) {
+ type = "Bag";
+ container = this.cUtils.MakeBag(aDs, aResource);
+ }
+
+ var result = aIndent + "<RDF:" + type;
+ if (!gRDF.IsAnonymousResource(aResource))
+ result += " about=\"" + this.escapeEntities(aResource.ValueUTF8) + "\"";
+ result += ">\n";
+
+ if (container)
+ result += this.serializeContainerItems(aDs, container, aIndent + this.INDENT);
+
+ result += this.serializeResourceProperties(aDs, aResource, aIndent + this.INDENT);
+
+ result += aIndent + "</RDF:" + type + ">\n";
+ return result;
+ }
+}
+
+/**
+ * Sanitizes the update URL in an update item, as returned by
+ * parseRDFManifest and parseJSONManifest. Ensures that:
+ *
+ * - The URL is secure, or secured by a strong enough hash.
+ * - The security principal of the update manifest has permission to
+ * load the URL.
+ *
+ * @param aUpdate
+ * The update item to sanitize.
+ * @param aRequest
+ * The XMLHttpRequest used to load the manifest.
+ * @param aHashPattern
+ * The regular expression used to validate the update hash.
+ * @param aHashString
+ * The human-readable string specifying which hash functions
+ * are accepted.
+ */
+function sanitizeUpdateURL(aUpdate, aRequest, aHashPattern, aHashString) {
+ if (aUpdate.updateURL) {
+ let scriptSecurity = Services.scriptSecurityManager;
+ let principal = scriptSecurity.getChannelURIPrincipal(aRequest.channel);
+ try {
+ // This logs an error on failure, so no need to log it a second time
+ scriptSecurity.checkLoadURIStrWithPrincipal(principal, aUpdate.updateURL,
+ scriptSecurity.DISALLOW_SCRIPT);
+ } catch (e) {
+ delete aUpdate.updateURL;
+ return;
+ }
+
+ if (AddonManager.checkUpdateSecurity &&
+ !aUpdate.updateURL.startsWith("https:") &&
+ !aHashPattern.test(aUpdate.updateHash)) {
+ logger.warn(`Update link ${aUpdate.updateURL} is not secure and is not verified ` +
+ `by a strong enough hash (needs to be ${aHashString}).`);
+ delete aUpdate.updateURL;
+ delete aUpdate.updateHash;
+ }
+ }
+}
+
+/**
+ * Parses an RDF style update manifest into an array of update objects.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aRequest
+ * The XMLHttpRequest that has retrieved the update manifest
+ * @param aManifestData
+ * The pre-parsed manifest, as a bare XML DOM document
+ * @return an array of update objects
+ * @throws if the update manifest is invalid in any way
+ */
+function parseRDFManifest(aId, aUpdateKey, aRequest, aManifestData) {
+ if (aManifestData.documentElement.namespaceURI != PREFIX_NS_RDF) {
+ throw Components.Exception("update.rdf: Update manifest had an unrecognised namespace: " +
+ aManifestData.documentElement.namespaceURI);
+ }
+
+ function EM_R(aProp) {
+ return gRDF.GetResource(PREFIX_NS_EM + aProp);
+ }
+
+ function getValue(aLiteral) {
+ if (aLiteral instanceof Ci.nsIRDFLiteral)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFResource)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFInt)
+ return aLiteral.Value;
+ return null;
+ }
+
+ function getProperty(aDs, aSource, aProperty) {
+ return getValue(aDs.GetTarget(aSource, EM_R(aProperty), true));
+ }
+
+ function getBooleanProperty(aDs, aSource, aProperty) {
+ let propValue = aDs.GetTarget(aSource, EM_R(aProperty), true);
+ if (!propValue)
+ return undefined;
+ return getValue(propValue) == "true";
+ }
+
+ function getRequiredProperty(aDs, aSource, aProperty) {
+ let value = getProperty(aDs, aSource, aProperty);
+ if (!value)
+ throw Components.Exception("update.rdf: Update manifest is missing a required " + aProperty + " property.");
+ return value;
+ }
+
+ let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
+ createInstance(Ci.nsIRDFXMLParser);
+ let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
+ createInstance(Ci.nsIRDFDataSource);
+ rdfParser.parseString(ds, aRequest.channel.URI, aRequest.responseText);
+
+ // Differentiating between add-on types is deprecated
+ let extensionRes = gRDF.GetResource(PREFIX_EXTENSION + aId);
+ let themeRes = gRDF.GetResource(PREFIX_THEME + aId);
+ let itemRes = gRDF.GetResource(PREFIX_ITEM + aId);
+ let addonRes;
+ if (ds.ArcLabelsOut(extensionRes).hasMoreElements())
+ addonRes = extensionRes;
+ else if (ds.ArcLabelsOut(themeRes).hasMoreElements())
+ addonRes = themeRes;
+ else
+ addonRes = itemRes;
+
+ // If we have an update key then the update manifest must be signed
+ if (aUpdateKey) {
+ let signature = getProperty(ds, addonRes, "signature");
+ if (!signature)
+ throw Components.Exception("update.rdf: Update manifest for " + aId + " does not contain a required signature");
+ let serializer = new RDFSerializer();
+ let updateString = null;
+
+ try {
+ updateString = serializer.serializeResource(ds, addonRes);
+ }
+ catch (e) {
+ throw Components.Exception("update.rdf: Failed to generate signed string for " + aId + ". Serializer threw " + e,
+ e.result);
+ }
+
+ let result = false;
+
+ try {
+ let verifier = Cc["@mozilla.org/security/datasignatureverifier;1"].
+ getService(Ci.nsIDataSignatureVerifier);
+ result = verifier.verifyData(updateString, signature, aUpdateKey);
+ }
+ catch (e) {
+ throw Components.Exception("update.rdf: The signature or updateKey for " + aId + " is malformed." +
+ "Verifier threw " + e, e.result);
+ }
+
+ if (!result)
+ throw Components.Exception("The signature for " + aId + " was not created by the add-on's updateKey");
+ }
+
+ let updates = ds.GetTarget(addonRes, EM_R("updates"), true);
+
+ // A missing updates property doesn't count as a failure, just as no avialable
+ // update information
+ if (!updates) {
+ logger.warn("update.rdf: Update manifest for " + aId + " did not contain an updates property");
+ return [];
+ }
+
+ if (!(updates instanceof Ci.nsIRDFResource))
+ throw Components.Exception("Missing updates property for " + addonRes.Value);
+
+ let cu = Cc["@mozilla.org/rdf/container-utils;1"].
+ getService(Ci.nsIRDFContainerUtils);
+ if (!cu.IsContainer(ds, updates))
+ throw Components.Exception("update.rdf: Updates property was not an RDF container");
+
+ let results = [];
+ let ctr = Cc["@mozilla.org/rdf/container;1"].
+ createInstance(Ci.nsIRDFContainer);
+ ctr.Init(ds, updates);
+ let items = ctr.GetElements();
+ while (items.hasMoreElements()) {
+ let item = items.getNext().QueryInterface(Ci.nsIRDFResource);
+ let version = getProperty(ds, item, "version");
+ if (!version) {
+ logger.warn("update.rdf: Update manifest is missing a required version property.");
+ continue;
+ }
+
+ logger.debug("update.rdf: Found an update entry for " + aId + " version " + version);
+
+ let targetApps = ds.GetTargets(item, EM_R("targetApplication"), true);
+ while (targetApps.hasMoreElements()) {
+ let targetApp = targetApps.getNext().QueryInterface(Ci.nsIRDFResource);
+
+ let appEntry = {};
+ try {
+ appEntry.id = getRequiredProperty(ds, targetApp, "id");
+ appEntry.minVersion = getRequiredProperty(ds, targetApp, "minVersion");
+ appEntry.maxVersion = getRequiredProperty(ds, targetApp, "maxVersion");
+ }
+ catch (e) {
+ logger.warn(e);
+ continue;
+ }
+
+ let result = {
+ id: aId,
+ version: version,
+ multiprocessCompatible: getBooleanProperty(ds, item, "multiprocessCompatible"),
+ updateURL: getProperty(ds, targetApp, "updateLink"),
+ updateHash: getProperty(ds, targetApp, "updateHash"),
+ updateInfoURL: getProperty(ds, targetApp, "updateInfoURL"),
+ strictCompatibility: !!getBooleanProperty(ds, targetApp, "strictCompatibility"),
+ targetApplications: [appEntry]
+ };
+
+ // The JSON update protocol requires an SHA-2 hash. RDF still
+ // supports SHA-1, for compatibility reasons.
+ sanitizeUpdateURL(result, aRequest, /^sha/, "sha1 or stronger");
+
+ results.push(result);
+ }
+ }
+ return results;
+}
+
+/**
+ * Parses an JSON update manifest into an array of update objects.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aRequest
+ * The XMLHttpRequest that has retrieved the update manifest
+ * @param aManifestData
+ * The pre-parsed manifest, as a JSON object tree
+ * @return an array of update objects
+ * @throws if the update manifest is invalid in any way
+ */
+function parseJSONManifest(aId, aUpdateKey, aRequest, aManifestData) {
+ if (aUpdateKey)
+ throw Components.Exception("update.json: Update keys are not supported for JSON update manifests");
+
+ let TYPE_CHECK = {
+ "array": val => Array.isArray(val),
+ "object": val => val && typeof val == "object" && !Array.isArray(val),
+ };
+
+ function getProperty(aObj, aProperty, aType, aDefault = undefined) {
+ if (!(aProperty in aObj))
+ return aDefault;
+
+ let value = aObj[aProperty];
+
+ let matchesType = aType in TYPE_CHECK ? TYPE_CHECK[aType](value) : typeof value == aType;
+ if (!matchesType)
+ throw Components.Exception(`update.json: Update manifest property '${aProperty}' has incorrect type (expected ${aType})`);
+
+ return value;
+ }
+
+ function getRequiredProperty(aObj, aProperty, aType) {
+ let value = getProperty(aObj, aProperty, aType);
+ if (value === undefined)
+ throw Components.Exception(`update.json: Update manifest is missing a required ${aProperty} property.`);
+ return value;
+ }
+
+ let manifest = aManifestData;
+
+ if (!TYPE_CHECK["object"](manifest))
+ throw Components.Exception("update.json: Root element of update manifest must be a JSON object literal");
+
+ // The set of add-ons this manifest has updates for
+ let addons = getRequiredProperty(manifest, "addons", "object");
+
+ // The entry for this particular add-on
+ let addon = getProperty(addons, aId, "object");
+
+ // A missing entry doesn't count as a failure, just as no avialable update
+ // information
+ if (!addon) {
+ logger.warn("update.json: Update manifest did not contain an entry for " + aId);
+ return [];
+ }
+
+ let appID = Services.appinfo.ID;
+ let platformVersion = Services.appinfo.platformVersion;
+
+ // The list of available updates
+ let updates = getProperty(addon, "updates", "array", []);
+
+ let results = [];
+
+ for (let update of updates) {
+ let version = getRequiredProperty(update, "version", "string");
+ logger.debug(`update.json: Found an update entry for ${aId} version ${version}`);
+
+ let applications = getRequiredProperty(update, "applications", "object");
+
+ let app;
+ let appEntry;
+
+ if (appID in applications) {
+ logger.debug("update.json: Native targetApplication");
+ app = getProperty(applications, appID, "object");
+
+ appEntry = {
+ id: appID,
+ minVersion: getRequiredProperty(app, "min_version", "string"),
+ maxVersion: getRequiredProperty(app, "max_version", "string"),
+ }
+ }
+ else if (TOOLKIT_ID in applications) {
+ logger.debug("update.json: Toolkit targetApplication");
+ app = getProperty(applications, TOOLKIT_ID, "object");
+
+ appEntry = {
+ id: TOOLKIT_ID,
+ minVersion: getRequiredProperty(app, "min_version", "string"),
+ maxVersion: getRequiredProperty(app, "max_version", "string"),
+ }
+ }
+ else if ("gecko" in applications) {
+ logger.debug("update.json: Mozilla Compatiblity Mode");
+ app = getProperty(applications, "gecko", "object");
+
+ appEntry = {
+#ifdef MOZ_PHOENIX
+ id: FIREFOX_ID,
+ minVersion: getProperty(app, "strict_min_version", "string",
+ Services.prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION)),
+#else
+ id: TOOLKIT_ID,
+ minVersion: platformVersion,
+#endif
+ maxVersion: '*',
+ };
+ }
+ else {
+ continue;
+ }
+
+ let result = {
+ id: aId,
+ version: version,
+ multiprocessCompatible: getProperty(update, "multiprocess_compatible", "boolean", false),
+ updateURL: getProperty(update, "update_link", "string"),
+ updateHash: getProperty(update, "update_hash", "string"),
+ updateInfoURL: getProperty(update, "update_info_url", "string"),
+ strictCompatibility: getProperty(app, "strict_compatibility", "boolean", false),
+ targetApplications: [appEntry],
+ };
+
+ // The JSON update protocol requires an SHA-2 hash. RDF still
+ // supports SHA-1, for compatibility reasons.
+ sanitizeUpdateURL(result, aRequest, /^sha(256|512):/, "sha256 or sha512");
+
+ results.push(result);
+ }
+ return results;
+}
+
+/**
+ * Starts downloading an update manifest and then passes it to an appropriate
+ * parser to convert to an array of update objects
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aUrl
+ * The URL of the update manifest
+ * @param aObserver
+ * An observer to pass results to
+ */
+function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
+ this.id = aId;
+ this.updateKey = aUpdateKey;
+ this.observer = aObserver;
+ this.url = aUrl;
+
+ let requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS, true);
+
+ logger.debug("Requesting " + aUrl);
+ try {
+ this.request = new ServiceRequest();
+ this.request.open("GET", this.url, true);
+ this.request.channel.notificationCallbacks = new CertUtils.BadCertHandler(!requireBuiltIn);
+ this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to cache.
+ this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ this.request.overrideMimeType("text/plain");
+ this.request.setRequestHeader("Moz-XPI-Update", "1", true);
+ this.request.timeout = TIMEOUT;
+ this.request.addEventListener("load", () => this.onLoad(), false);
+ this.request.addEventListener("error", () => this.onError(), false);
+ this.request.addEventListener("timeout", () => this.onTimeout(), false);
+ this.request.send(null);
+ }
+ catch (e) {
+ logger.error("Failed to request update manifest", e);
+ }
+}
+
+UpdateParser.prototype = {
+ id: null,
+ updateKey: null,
+ observer: null,
+ request: null,
+ url: null,
+
+ /**
+ * Called when the manifest has been successfully loaded.
+ */
+ onLoad: function() {
+ let request = this.request;
+ this.request = null;
+ this._doneAt = new Error("place holder");
+
+ let requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS, true);
+
+ try {
+ CertUtils.checkCert(request.channel, !requireBuiltIn);
+ }
+ catch (e) {
+ logger.warn("Request failed: " + this.url + " - " + e);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ if (!Components.isSuccessCode(request.status)) {
+ logger.warn("Request failed: " + this.url + " - " + request.status);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ let channel = request.channel;
+ if (channel instanceof Ci.nsIHttpChannel && !channel.requestSucceeded) {
+ logger.warn("Request failed: " + this.url + " - " + channel.responseStatus +
+ ": " + channel.responseStatusText);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ // Detect the manifest type by first attempting to parse it as
+ // JSON, and falling back to parsing it as XML if that fails.
+ let parser;
+ try {
+ try {
+ let json = JSON.parse(request.responseText);
+
+ parser = () => parseJSONManifest(this.id, this.updateKey, request, json);
+ } catch (e) {
+ if (!(e instanceof SyntaxError))
+ throw e;
+ let domParser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
+ let xml = domParser.parseFromString(request.responseText, "text/xml");
+
+ if (xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR)
+ throw new Error("Update manifest was not valid XML or JSON");
+
+ parser = () => parseRDFManifest(this.id, this.updateKey, request, xml);
+ }
+ } catch (e) {
+ logger.warn("onUpdateCheckComplete failed to determine manifest type");
+ this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT);
+ return;
+ }
+
+ let results;
+ try {
+ results = parser();
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckComplete failed to parse update manifest", e);
+ this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
+ return;
+ }
+
+ if ("onUpdateCheckComplete" in this.observer) {
+ try {
+ this.observer.onUpdateCheckComplete(results);
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckComplete notification failed", e);
+ }
+ }
+ else {
+ logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker"));
+ }
+ },
+
+ /**
+ * Called when the request times out
+ */
+ onTimeout: function() {
+ this.request = null;
+ this._doneAt = new Error("Timed out");
+ logger.warn("Request for " + this.url + " timed out");
+ this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
+ },
+
+ /**
+ * Called when the manifest failed to load.
+ */
+ onError: function() {
+ if (!Components.isSuccessCode(this.request.status)) {
+ logger.warn("Request failed: " + this.url + " - " + this.request.status);
+ }
+ else if (this.request.channel instanceof Ci.nsIHttpChannel) {
+ try {
+ if (this.request.channel.requestSucceeded) {
+ logger.warn("Request failed: " + this.url + " - " +
+ this.request.channel.responseStatus + ": " +
+ this.request.channel.responseStatusText);
+ }
+ }
+ catch (e) {
+ logger.warn("HTTP Request failed for an unknown reason");
+ }
+ }
+ else {
+ logger.warn("Request failed for an unknown reason");
+ }
+
+ this.request = null;
+ this._doneAt = new Error("UP_onError");
+
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ },
+
+ /**
+ * Helper method to notify the observer that an error occured.
+ */
+ notifyError: function(aStatus) {
+ if ("onUpdateCheckError" in this.observer) {
+ try {
+ this.observer.onUpdateCheckError(aStatus);
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckError notification failed", e);
+ }
+ }
+ },
+
+ /**
+ * Called to cancel an in-progress update check.
+ */
+ cancel: function() {
+ if (!this.request) {
+ logger.error("Trying to cancel already-complete request", this._doneAt);
+ return;
+ }
+ this.request.abort();
+ this.request = null;
+ this._doneAt = new Error("UP_cancel");
+ this.notifyError(AddonUpdateChecker.ERROR_CANCELLED);
+ }
+};
+
+/**
+ * Tests if an update matches a version of the application or platform
+ *
+ * @param aUpdate
+ * The available update
+ * @param aAppVersion
+ * The application version to use
+ * @param aPlatformVersion
+ * The platform version to use
+ * @param aIgnoreMaxVersion
+ * Ignore maxVersion when testing if an update matches. Optional.
+ * @param aIgnoreStrictCompat
+ * Ignore strictCompatibility when testing if an update matches. Optional.
+ * @param aCompatOverrides
+ * AddonCompatibilityOverride objects to match against. Optional.
+ * @return true if the update is compatible with the application/platform
+ */
+function matchesVersions(aUpdate, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides) {
+ if (aCompatOverrides) {
+ let override = AddonRepository.findMatchingCompatOverride(aUpdate.version,
+ aCompatOverrides,
+ aAppVersion,
+ aPlatformVersion);
+ if (override && override.type == "incompatible")
+ return false;
+ }
+
+ if (aUpdate.strictCompatibility && !aIgnoreStrictCompat)
+ aIgnoreMaxVersion = false;
+
+ let result = false;
+ for (let app of aUpdate.targetApplications) {
+ if (app.id == Services.appinfo.ID) {
+ return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) &&
+ (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0));
+ }
+ if (app.id == TOOLKIT_ID) {
+ result = (Services.vc.compare(aPlatformVersion, app.minVersion) >= 0) &&
+ (aIgnoreMaxVersion || (Services.vc.compare(aPlatformVersion, app.maxVersion) <= 0));
+ }
+ }
+ return result;
+}
+
+this.AddonUpdateChecker = {
+ // These must be kept in sync with AddonManager
+ // The update check timed out
+ ERROR_TIMEOUT: -1,
+ // There was an error while downloading the update information.
+ ERROR_DOWNLOAD_ERROR: -2,
+ // The update information was malformed in some way.
+ ERROR_PARSE_ERROR: -3,
+ // The update information was not in any known format.
+ ERROR_UNKNOWN_FORMAT: -4,
+ // The update information was not correctly signed or there was an SSL error.
+ ERROR_SECURITY_ERROR: -5,
+ // The update was cancelled
+ ERROR_CANCELLED: -6,
+
+ /**
+ * Retrieves the best matching compatibility update for the application from
+ * a list of available update objects.
+ *
+ * @param aUpdates
+ * An array of update objects
+ * @param aVersion
+ * The version of the add-on to get new compatibility information for
+ * @param aIgnoreCompatibility
+ * An optional parameter to get the first compatibility update that
+ * is compatible with any version of the application or toolkit
+ * @param aAppVersion
+ * The version of the application or null to use the current version
+ * @param aPlatformVersion
+ * The version of the platform or null to use the current version
+ * @param aIgnoreMaxVersion
+ * Ignore maxVersion when testing if an update matches. Optional.
+ * @param aIgnoreStrictCompat
+ * Ignore strictCompatibility when testing if an update matches. Optional.
+ * @return an update object if one matches or null if not
+ */
+ getCompatibilityUpdate: function(aUpdates, aVersion, aIgnoreCompatibility,
+ aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat) {
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ for (let update of aUpdates) {
+ if (Services.vc.compare(update.version, aVersion) == 0) {
+ if (aIgnoreCompatibility) {
+ for (let targetApp of update.targetApplications) {
+ let id = targetApp.id;
+ if (id == Services.appinfo.ID || id == TOOLKIT_ID)
+ return update;
+ }
+ }
+ else if (matchesVersions(update, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat)) {
+ return update;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the newest available update from a list of update objects.
+ *
+ * @param aUpdates
+ * An array of update objects
+ * @param aAppVersion
+ * The version of the application or null to use the current version
+ * @param aPlatformVersion
+ * The version of the platform or null to use the current version
+ * @param aIgnoreMaxVersion
+ * When determining compatible updates, ignore maxVersion. Optional.
+ * @param aIgnoreStrictCompat
+ * When determining compatible updates, ignore strictCompatibility. Optional.
+ * @param aCompatOverrides
+ * Array of AddonCompatibilityOverride to take into account. Optional.
+ * @return an update object if one matches or null if not
+ */
+ getNewestCompatibleUpdate: function(aUpdates, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides) {
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ let blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsIBlocklistService);
+
+ let newest = null;
+ for (let update of aUpdates) {
+ if (!update.updateURL)
+ continue;
+ let state = blocklist.getAddonBlocklistState(update, aAppVersion, aPlatformVersion);
+ if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
+ continue;
+ if ((newest == null || (Services.vc.compare(newest.version, update.version) < 0)) &&
+ matchesVersions(update, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides)) {
+ newest = update;
+ }
+ }
+ return newest;
+ },
+
+ /**
+ * Starts an update check.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aUrl
+ * The URL of the add-on's update manifest
+ * @param aObserver
+ * An observer to notify of results
+ * @return UpdateParser so that the caller can use UpdateParser.cancel() to shut
+ * down in-progress update requests
+ */
+ checkForUpdates: function(aId, aUpdateKey, aUrl, aObserver) {
+ // Define an array of internally used IDs to NOT send to AUS.
+ let internalIDS = [
+ '{972ce4c6-7e08-4474-a285-3208198ce6fd}', // Global Default Theme
+ 'modern@themes.mozilla.org', // Modern Theme for Borealis/Suite-based Applications
+ 'xplatform@interlink.projects.binaryoutcast.com', // Pref-set default theme for Interlink
+ '{e2fda1a4-762b-4020-b5ad-a41df1933103}', // Lightning/Calendar Extension
+ '{a62ef8ec-5fdc-40c2-873c-223b8a6925cc}' // Provider for Google Calendar (gdata) Extension
+ ];
+
+ // If the ID is not in the array then go ahead and query AUS
+ if (internalIDS.indexOf(aId) == -1) {
+ return new UpdateParser(aId, aUpdateKey, aUrl, aObserver);
+ }
+ }
+};
diff --git a/components/extensions/src/ChromeManifestParser.jsm b/components/extensions/src/ChromeManifestParser.jsm
new file mode 100644
index 000000000..63f1db785
--- /dev/null
+++ b/components/extensions/src/ChromeManifestParser.jsm
@@ -0,0 +1,157 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["ChromeManifestParser"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const MSG_JAR_FLUSH = "AddonJarFlush";
+
+
+/**
+ * Sends local and remote notifications to flush a JAR file cache entry
+ *
+ * @param aJarFile
+ * The ZIP/XPI/JAR file as a nsIFile
+ */
+function flushJarCache(aJarFile) {
+ Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null);
+ Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageBroadcaster)
+ .broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
+}
+
+
+/**
+ * Parses chrome manifest files.
+ */
+this.ChromeManifestParser = {
+
+ /**
+ * Reads and parses a chrome manifest file located at a specified URI, and all
+ * secondary manifests it references.
+ *
+ * @param aURI
+ * A nsIURI pointing to a chrome manifest.
+ * Typically a file: or jar: URI.
+ * @return Array of objects describing each manifest instruction, in the form:
+ * { type: instruction-type, baseURI: string-uri, args: [arguments] }
+ **/
+ parseSync: function(aURI) {
+ function parseLine(aLine) {
+ let line = aLine.trim();
+ if (line.length == 0 || line.charAt(0) == '#')
+ return;
+ let tokens = line.split(/\s+/);
+ let type = tokens.shift();
+ if (type == "manifest") {
+ let uri = NetUtil.newURI(tokens.shift(), null, aURI);
+ data = data.concat(this.parseSync(uri));
+ } else {
+ data.push({type: type, baseURI: baseURI, args: tokens});
+ }
+ }
+
+ let contents = "";
+ try {
+ if (aURI.scheme == "jar")
+ contents = this._readFromJar(aURI);
+ else
+ contents = this._readFromFile(aURI);
+ } catch (e) {
+ // Silently fail.
+ }
+
+ if (!contents)
+ return [];
+
+ let baseURI = NetUtil.newURI(".", null, aURI).spec;
+
+ let data = [];
+ let lines = contents.split("\n");
+ lines.forEach(parseLine.bind(this));
+ return data;
+ },
+
+ _readFromJar: function(aURI) {
+ let data = "";
+ let entries = [];
+ let readers = [];
+
+ try {
+ // Deconstrict URI, which can be nested jar: URIs.
+ let uri = aURI.clone();
+ while (uri instanceof Ci.nsIJARURI) {
+ entries.push(uri.JAREntry);
+ uri = uri.JARFile;
+ }
+
+ // Open the base jar.
+ let reader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ reader.open(uri.QueryInterface(Ci.nsIFileURL).file);
+ readers.push(reader);
+
+ // Open the nested jars.
+ for (let i = entries.length - 1; i > 0; i--) {
+ let innerReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ innerReader.openInner(reader, entries[i]);
+ readers.push(innerReader);
+ reader = innerReader;
+ }
+
+ // First entry is the actual file we want to read.
+ let zis = reader.getInputStream(entries[0]);
+ data = NetUtil.readInputStreamToString(zis, zis.available());
+ }
+ finally {
+ // Close readers in reverse order.
+ for (let i = readers.length - 1; i >= 0; i--) {
+ readers[i].close();
+ flushJarCache(readers[i].file);
+ }
+ }
+
+ return data;
+ },
+
+ _readFromFile: function(aURI) {
+ let file = aURI.QueryInterface(Ci.nsIFileURL).file;
+ if (!file.exists() || !file.isFile())
+ return "";
+
+ let data = "";
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ try {
+ fis.init(file, -1, -1, false);
+ data = NetUtil.readInputStreamToString(fis, fis.available());
+ } finally {
+ fis.close();
+ }
+ return data;
+ },
+
+ /**
+ * Detects if there were any instructions of a specified type in a given
+ * chrome manifest.
+ *
+ * @param aManifest
+ * Manifest data, as returned by ChromeManifestParser.parseSync().
+ * @param aType
+ * Instruction type to filter by.
+ * @return True if any matching instructions were found in the manifest.
+ */
+ hasType: function(aManifest, aType) {
+ return aManifest.some(entry => entry.type == aType);
+ }
+};
diff --git a/components/extensions/src/Content.js b/components/extensions/src/Content.js
new file mode 100644
index 000000000..9f366ba32
--- /dev/null
+++ b/components/extensions/src/Content.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+/* globals addMessageListener*/
+
+"use strict";
+
+(function() {
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+var {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+var nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
+ "initWithPath");
+
+const MSG_JAR_FLUSH = "AddonJarFlush";
+const MSG_MESSAGE_MANAGER_CACHES_FLUSH = "AddonMessageManagerCachesFlush";
+
+
+try {
+ if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ // Propagate JAR cache flush notifications across process boundaries.
+ addMessageListener(MSG_JAR_FLUSH, function(message) {
+ let file = new nsIFile(message.data);
+ Services.obs.notifyObservers(file, "flush-cache-entry", null);
+ });
+ // Propagate message manager caches flush notifications across processes.
+ addMessageListener(MSG_MESSAGE_MANAGER_CACHES_FLUSH, function() {
+ Services.obs.notifyObservers(null, "message-manager-flush-caches", null);
+ });
+ }
+} catch (e) {
+ Cu.reportError(e);
+}
+
+})();
diff --git a/components/extensions/src/DeferredSave.jsm b/components/extensions/src/DeferredSave.jsm
new file mode 100644
index 000000000..f1537fe4b
--- /dev/null
+++ b/components/extensions/src/DeferredSave.jsm
@@ -0,0 +1,270 @@
+/* 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";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/osfile.jsm");
+/* globals OS*/
+Cu.import("resource://gre/modules/Promise.jsm");
+
+// Make it possible to mock out timers for testing
+var MakeTimer = () => Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+this.EXPORTED_SYMBOLS = ["DeferredSave"];
+
+// If delay parameter is not provided, default is 50 milliseconds.
+const DEFAULT_SAVE_DELAY_MS = 50;
+
+Cu.import("resource://gre/modules/Log.jsm");
+// Configure a logger at the parent 'DeferredSave' level to format
+// messages for all the modules under DeferredSave.*
+const DEFERREDSAVE_PARENT_LOGGER_ID = "DeferredSave";
+var parentLogger = Log.repository.getLogger(DEFERREDSAVE_PARENT_LOGGER_ID);
+parentLogger.level = Log.Level.Warn;
+var formatter = new Log.BasicFormatter();
+// Set parent logger (and its children) to append to
+// the Javascript section of the Browser Console
+parentLogger.addAppender(new Log.ConsoleAppender(formatter));
+// Set parent logger (and its children) to
+// also append to standard out
+parentLogger.addAppender(new Log.DumpAppender(formatter));
+
+// Provide the ability to enable/disable logging
+// messages at runtime.
+// If the "extensions.logging.enabled" preference is
+// missing or 'false', messages at the WARNING and higher
+// severity should be logged to the JS console and standard error.
+// If "extensions.logging.enabled" is set to 'true', messages
+// at DEBUG and higher should go to JS console and standard error.
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+/**
+* Preference listener which listens for a change in the
+* "extensions.logging.enabled" preference and changes the logging level of the
+* parent 'addons' level logger accordingly.
+*/
+var PrefObserver = {
+ init: function() {
+ Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "xpcom-shutdown") {
+ Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this);
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ }
+ else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) {
+ let debugLogEnabled = Services.prefs.getBoolPref(PREF_LOGGING_ENABLED, false);
+ if (debugLogEnabled) {
+ parentLogger.level = Log.Level.Debug;
+ }
+ else {
+ parentLogger.level = Log.Level.Warn;
+ }
+ }
+ }
+};
+
+PrefObserver.init();
+
+/**
+ * A module to manage deferred, asynchronous writing of data files
+ * to disk. Writing is deferred by waiting for a specified delay after
+ * a request to save the data, before beginning to write. If more than
+ * one save request is received during the delay, all requests are
+ * fulfilled by a single write.
+ *
+ * @constructor
+ * @param aPath
+ * String representing the full path of the file where the data
+ * is to be written.
+ * @param aDataProvider
+ * Callback function that takes no argument and returns the data to
+ * be written. If aDataProvider returns an ArrayBufferView, the
+ * bytes it contains are written to the file as is.
+ * If aDataProvider returns a String the data are UTF-8 encoded
+ * and then written to the file.
+ * @param [optional] aDelay
+ * The delay in milliseconds between the first saveChanges() call
+ * that marks the data as needing to be saved, and when the DeferredSave
+ * begins writing the data to disk. Default 50 milliseconds.
+ */
+this.DeferredSave = function(aPath, aDataProvider, aDelay) {
+ // Create a new logger (child of 'DeferredSave' logger)
+ // for use by this particular instance of DeferredSave object
+ let leafName = OS.Path.basename(aPath);
+ let logger_id = DEFERREDSAVE_PARENT_LOGGER_ID + "." + leafName;
+ this.logger = Log.repository.getLogger(logger_id);
+
+ // @type {Deferred|null}, null when no data needs to be written
+ // @resolves with the result of OS.File.writeAtomic when all writes complete
+ // @rejects with the error from OS.File.writeAtomic if the write fails,
+ // or with the error from aDataProvider() if that throws.
+ this._pending = null;
+
+ // @type {Promise}, completes when the in-progress write (if any) completes,
+ // kept as a resolved promise at other times to simplify logic.
+ // Because _deferredSave() always uses _writing.then() to execute
+ // its next action, we don't need a special case for whether a write
+ // is in progress - if the previous write is complete (and the _writing
+ // promise is already resolved/rejected), _writing.then() starts
+ // the next action immediately.
+ //
+ // @resolves with the result of OS.File.writeAtomic
+ // @rejects with the error from OS.File.writeAtomic
+ this._writing = Promise.resolve(0);
+
+ // Are we currently waiting for a write to complete
+ this.writeInProgress = false;
+
+ this._path = aPath;
+ this._dataProvider = aDataProvider;
+
+ this._timer = null;
+
+ // Some counters for telemetry
+ // The total number of times the file was written
+ this.totalSaves = 0;
+
+ // The number of times the data became dirty while
+ // another save was in progress
+ this.overlappedSaves = 0;
+
+ // Error returned by the most recent write (if any)
+ this._lastError = null;
+
+ if (aDelay && (aDelay > 0))
+ this._delay = aDelay;
+ else
+ this._delay = DEFAULT_SAVE_DELAY_MS;
+}
+
+this.DeferredSave.prototype = {
+ get dirty() {
+ return this._pending || this.writeInProgress;
+ },
+
+ get lastError() {
+ return this._lastError;
+ },
+
+ // Start the pending timer if data is dirty
+ _startTimer: function() {
+ if (!this._pending) {
+ return;
+ }
+
+ this.logger.debug("Starting timer");
+ if (!this._timer)
+ this._timer = MakeTimer();
+ this._timer.initWithCallback(() => this._deferredSave(),
+ this._delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /**
+ * Mark the current stored data dirty, and schedule a flush to disk
+ * @return A Promise<integer> that will be resolved after the data is written to disk;
+ * the promise is resolved with the number of bytes written.
+ */
+ saveChanges: function() {
+ this.logger.debug("Save changes");
+ if (!this._pending) {
+ if (this.writeInProgress) {
+ this.logger.debug("Data changed while write in progress");
+ this.overlappedSaves++;
+ }
+ this._pending = Promise.defer();
+ // Wait until the most recent write completes or fails (if it hasn't already)
+ // and then restart our timer
+ this._writing.then(count => this._startTimer(), error => this._startTimer());
+ }
+ return this._pending.promise;
+ },
+
+ _deferredSave: function() {
+ let pending = this._pending;
+ this._pending = null;
+ let writing = this._writing;
+ this._writing = pending.promise;
+
+ // In either the success or the exception handling case, we don't need to handle
+ // the error from _writing here; it's already being handled in another then()
+ let toSave = null;
+ try {
+ toSave = this._dataProvider();
+ }
+ catch (e) {
+ this.logger.error("Deferred save dataProvider failed", e);
+ writing.then(null, error => {})
+ .then(count => {
+ pending.reject(e);
+ });
+ return;
+ }
+
+ writing.then(null, error => { return 0; })
+ .then(count => {
+ this.logger.debug("Starting write");
+ this.totalSaves++;
+ this.writeInProgress = true;
+
+ OS.File.writeAtomic(this._path, toSave, {tmpPath: this._path + ".tmp"})
+ .then(
+ result => {
+ this._lastError = null;
+ this.writeInProgress = false;
+ this.logger.debug("Write succeeded");
+ pending.resolve(result);
+ },
+ error => {
+ this._lastError = error;
+ this.writeInProgress = false;
+ this.logger.warn("Write failed", error);
+ pending.reject(error);
+ });
+ });
+ },
+
+ /**
+ * Immediately save the dirty data to disk, skipping
+ * the delay of normal operation. Note that the write
+ * still happens asynchronously in the worker
+ * thread from OS.File.
+ *
+ * There are four possible situations:
+ * 1) Nothing to flush
+ * 2) Data is not currently being written, in-memory copy is dirty
+ * 3) Data is currently being written, in-memory copy is clean
+ * 4) Data is being written and in-memory copy is dirty
+ *
+ * @return Promise<integer> that will resolve when all in-memory data
+ * has finished being flushed, returning the number of bytes
+ * written. If all in-memory data is clean, completes with the
+ * result of the most recent write.
+ */
+ flush: function() {
+ // If we have pending changes, cancel our timer and set up the write
+ // immediately (_deferredSave queues the write for after the most
+ // recent write completes, if it hasn't already)
+ if (this._pending) {
+ this.logger.debug("Flush called while data is dirty");
+ if (this._timer) {
+ this._timer.cancel();
+ this._timer = null;
+ }
+ this._deferredSave();
+ }
+
+ return this._writing;
+ }
+};
diff --git a/components/extensions/src/GMPInstallManager.jsm b/components/extensions/src/GMPInstallManager.jsm
new file mode 100644
index 000000000..fe4e2de10
--- /dev/null
+++ b/components/extensions/src/GMPInstallManager.jsm
@@ -0,0 +1,917 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} =
+ Components;
+// Chunk size for the incremental downloader
+const DOWNLOAD_CHUNK_BYTES_SIZE = 300000;
+// Incremental downloader interval
+const DOWNLOAD_INTERVAL = 0;
+// 1 day default
+const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/ctypes.jsm");
+Cu.import("resource://gre/modules/GMPUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader",
+ "GMPAddon"];
+
+var gLocale = null;
+
+// Shared code for suppressing bad cert dialogs
+XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() {
+ let temp = { };
+ Cu.import("resource://gre/modules/CertUtils.jsm", temp);
+ return temp;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+/**
+ * Number of milliseconds after which we need to cancel `checkForAddons`.
+ *
+ * Bug 1087674 suggests that the XHR we use in `checkForAddons` may
+ * never terminate in presence of network nuisances (e.g. strange
+ * antivirus behavior). This timeout is a defensive measure to ensure
+ * that we fail cleanly in such case.
+ */
+const CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS = 20000;
+
+function getScopedLogger(prefix) {
+ // `PARENT_LOGGER_ID.` being passed here effectively links this logger
+ // to the parentLogger.
+ return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " ");
+}
+
+// This is copied directly from nsUpdateService.js
+// It is used for calculating the URL string w/ var replacement.
+// TODO: refactor this out somewhere else
+XPCOMUtils.defineLazyGetter(this, "gOSVersion", function aus_gOSVersion() {
+ let osVersion;
+ let sysInfo = Cc["@mozilla.org/system-info;1"].
+ getService(Ci.nsIPropertyBag2);
+ try {
+ osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
+ }
+ catch (e) {
+ LOG("gOSVersion - OS Version unknown: updates are not possible.");
+ }
+
+ if (osVersion) {
+#ifdef XP_WIN
+ const BYTE = ctypes.uint8_t;
+ const WORD = ctypes.uint16_t;
+ const DWORD = ctypes.uint32_t;
+ const WCHAR = ctypes.char16_t;
+ const BOOL = ctypes.int;
+
+ // This structure is described at:
+ // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
+ const SZCSDVERSIONLENGTH = 128;
+ const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW',
+ [
+ {dwOSVersionInfoSize: DWORD},
+ {dwMajorVersion: DWORD},
+ {dwMinorVersion: DWORD},
+ {dwBuildNumber: DWORD},
+ {dwPlatformId: DWORD},
+ {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
+ {wServicePackMajor: WORD},
+ {wServicePackMinor: WORD},
+ {wSuiteMask: WORD},
+ {wProductType: BYTE},
+ {wReserved: BYTE}
+ ]);
+
+ // This structure is described at:
+ // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx
+ const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO',
+ [
+ {wProcessorArchitecture: WORD},
+ {wReserved: WORD},
+ {dwPageSize: DWORD},
+ {lpMinimumApplicationAddress: ctypes.voidptr_t},
+ {lpMaximumApplicationAddress: ctypes.voidptr_t},
+ {dwActiveProcessorMask: DWORD.ptr},
+ {dwNumberOfProcessors: DWORD},
+ {dwProcessorType: DWORD},
+ {dwAllocationGranularity: DWORD},
+ {wProcessorLevel: WORD},
+ {wProcessorRevision: WORD}
+ ]);
+
+ let kernel32 = false;
+ try {
+ kernel32 = ctypes.open("Kernel32");
+ } catch (e) {
+ LOG("gOSVersion - Unable to open kernel32! " + e);
+ osVersion += ".unknown (unknown)";
+ }
+
+ if(kernel32) {
+ try {
+ // Get Service pack info
+ try {
+ let GetVersionEx = kernel32.declare("GetVersionExW",
+ ctypes.default_abi,
+ BOOL,
+ OSVERSIONINFOEXW.ptr);
+ let winVer = OSVERSIONINFOEXW();
+ winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
+
+ if(0 !== GetVersionEx(winVer.address())) {
+ osVersion += "." + winVer.wServicePackMajor
+ + "." + winVer.wServicePackMinor;
+ } else {
+ LOG("gOSVersion - Unknown failure in GetVersionEX (returned 0)");
+ osVersion += ".unknown";
+ }
+ } catch (e) {
+ LOG("gOSVersion - error getting service pack information. Exception: " + e);
+ osVersion += ".unknown";
+ }
+
+ // Get processor architecture
+ let arch = "unknown";
+ try {
+ let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo",
+ ctypes.default_abi,
+ ctypes.void_t,
+ SYSTEM_INFO.ptr);
+ let sysInfo = SYSTEM_INFO();
+ // Default to unknown
+ sysInfo.wProcessorArchitecture = 0xffff;
+
+ GetNativeSystemInfo(sysInfo.address());
+ switch(sysInfo.wProcessorArchitecture) {
+ case 9:
+ arch = "x64";
+ break;
+ case 6:
+ arch = "IA64";
+ break;
+ case 0:
+ arch = "x86";
+ break;
+ }
+ } catch (e) {
+ LOG("gOSVersion - error getting processor architecture. Exception: " + e);
+ } finally {
+ osVersion += " (" + arch + ")";
+ }
+ } finally {
+ kernel32.close();
+ }
+ }
+#endif
+
+ try {
+ osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
+ }
+ catch (e) {
+ // Not all platforms have a secondary widget library, so an error is nothing to worry about.
+ }
+ osVersion = encodeURIComponent(osVersion);
+ }
+ return osVersion;
+});
+
+/**
+ * Provides an easy API for downloading and installing GMP Addons
+ */
+function GMPInstallManager() {
+}
+/**
+ * Temp file name used for downloading
+ */
+GMPInstallManager.prototype = {
+ /**
+ * Obtains a URL with replacement of vars
+ */
+ _getURL: function() {
+ let log = getScopedLogger("GMPInstallManager._getURL");
+ // Use the override URL if it is specified. The override URL is just like
+ // the normal URL but it does not check the cert.
+ let url = GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE);
+ if (url) {
+ log.info("Using override url: " + url);
+ } else {
+ url = GMPPrefs.get(GMPPrefs.KEY_URL);
+ log.info("Using url: " + url);
+ }
+
+ url = UpdateUtils.formatUpdateURL(url);
+ log.info("Using url (with replacement): " + url);
+ return url;
+ },
+ /**
+ * Performs an addon check.
+ * @return a promise which will be resolved or rejected.
+ * The promise is resolved with an array of GMPAddons
+ * The promise is rejected with an object with properties:
+ * target: The XHR request object
+ * status: The HTTP status code
+ * type: Sometimes specifies type of rejection
+ */
+ checkForAddons: function() {
+ let log = getScopedLogger("GMPInstallManager.checkForAddons");
+ if (this._deferred) {
+ log.error("checkForAddons already called");
+ return Promise.reject({type: "alreadycalled"});
+ }
+ this._deferred = Promise.defer();
+ let url = this._getURL();
+
+ this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsISupports);
+ // This is here to let unit test code override XHR
+ if (this._request.wrappedJSObject) {
+ this._request = this._request.wrappedJSObject;
+ }
+ this._request.open("GET", url, true);
+ let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true);
+ this._request.channel.notificationCallbacks =
+ new gCertUtils.BadCertHandler(allowNonBuiltIn);
+ // Prevent the request from reading from the cache.
+ this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to the cache.
+ this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+
+ this._request.overrideMimeType("text/xml");
+ // The Cache-Control header is only interpreted by proxies and the
+ // final destination. It does not help if a resource is already
+ // cached locally.
+ this._request.setRequestHeader("Cache-Control", "no-cache");
+ // HTTP/1.0 servers might not implement Cache-Control and
+ // might only implement Pragma: no-cache
+ this._request.setRequestHeader("Pragma", "no-cache");
+
+ this._request.timeout = CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS;
+ this._request.addEventListener("error", event => this.onFailXML("onErrorXML", event), false);
+ this._request.addEventListener("abort", event => this.onFailXML("onAbortXML", event), false);
+ this._request.addEventListener("timeout", event => this.onFailXML("onTimeoutXML", event), false);
+ this._request.addEventListener("load", event => this.onLoadXML(event), false);
+
+ log.info("sending request to: " + url);
+ this._request.send(null);
+
+ return this._deferred.promise;
+ },
+ /**
+ * Installs the specified addon and calls a callback when done.
+ * @param gmpAddon The GMPAddon object to install
+ * @return a promise which will be resolved or rejected
+ * The promise will resolve with an array of paths that were extracted
+ * The promise will reject with an error object:
+ * target: The XHR request object
+ * status: The HTTP status code
+ * type: A string to represent the type of error
+ * downloaderr, verifyerr or previouserrorencountered
+ */
+ installAddon: function(gmpAddon) {
+ if (this._deferred) {
+ log.error("previous error encountered");
+ return Promise.reject({type: "previouserrorencountered"});
+ }
+ this.gmpDownloader = new GMPDownloader(gmpAddon);
+ return this.gmpDownloader.start();
+ },
+ _getTimeSinceLastCheck: function() {
+ let now = Math.round(Date.now() / 1000);
+ // Default to 0 here because `now - 0` will be returned later if that case
+ // is hit. We want a large value so a check will occur.
+ let lastCheck = GMPPrefs.get(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0);
+ // Handle clock jumps, return now since we want it to represent
+ // a lot of time has passed since the last check.
+ if (now < lastCheck) {
+ return now;
+ }
+ return now - lastCheck;
+ },
+ get _isEMEEnabled() {
+ return GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true);
+ },
+ _isAddonUpdateEnabled: function(aAddon) {
+ return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon) &&
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon);
+ },
+ _updateLastCheck: function() {
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.set(GMPPrefs.KEY_UPDATE_LAST_CHECK, now);
+ },
+ _versionchangeOccurred: function() {
+ let savedBuildID = GMPPrefs.get(GMPPrefs.KEY_BUILDID, null);
+ let buildID = Services.appinfo.platformBuildID;
+ if (savedBuildID == buildID) {
+ return false;
+ }
+ GMPPrefs.set(GMPPrefs.KEY_BUILDID, buildID);
+ return true;
+ },
+ /**
+ * Wrapper for checkForAddons and installAddon.
+ * Will only install if not already installed and will log the results.
+ * This will only install/update the OpenH264 and EME plugins
+ * @return a promise which will be resolved if all addons could be installed
+ * successfully, rejected otherwise.
+ */
+ simpleCheckAndInstall: Task.async(function*() {
+ let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall");
+
+ if (this._versionchangeOccurred()) {
+ log.info("A version change occurred. Ignoring " +
+ "media.gmp-manager.lastCheck to check immediately for " +
+ "new or updated GMPs.");
+ } else {
+ let secondsBetweenChecks =
+ GMPPrefs.get(GMPPrefs.KEY_SECONDS_BETWEEN_CHECKS,
+ DEFAULT_SECONDS_BETWEEN_CHECKS)
+ let secondsSinceLast = this._getTimeSinceLastCheck();
+ log.info("Last check was: " + secondsSinceLast +
+ " seconds ago, minimum seconds: " + secondsBetweenChecks);
+ if (secondsBetweenChecks > secondsSinceLast) {
+ log.info("Will not check for updates.");
+ return {status: "too-frequent-no-check"};
+ }
+ }
+
+ try {
+ let gmpAddons = yield this.checkForAddons();
+ this._updateLastCheck();
+ log.info("Found " + gmpAddons.length + " addons advertised.");
+ let addonsToInstall = gmpAddons.filter(function(gmpAddon) {
+ log.info("Found addon: " + gmpAddon.toString());
+
+ if (!gmpAddon.isValid || GMPUtils.isPluginHidden(gmpAddon) ||
+ gmpAddon.isInstalled) {
+ log.info("Addon invalid, hidden or already installed.");
+ return false;
+ }
+
+ let addonUpdateEnabled = false;
+ if (GMP_PLUGIN_IDS.indexOf(gmpAddon.id) >= 0) {
+ addonUpdateEnabled = this._isAddonUpdateEnabled(gmpAddon.id);
+ if (!addonUpdateEnabled) {
+ log.info("Auto-update is off for " + gmpAddon.id +
+ ", skipping check.");
+ }
+ } else {
+ // Currently, we only support installs of OpenH264 and EME plugins.
+ log.info("Auto-update is off for unknown plugin '" + gmpAddon.id +
+ "', skipping check.");
+ }
+
+ return addonUpdateEnabled;
+ }, this);
+
+ if (!addonsToInstall.length) {
+ log.info("No new addons to install, returning");
+ return {status: "nothing-new-to-install"};
+ }
+
+ let installResults = [];
+ let failureEncountered = false;
+ for (let addon of addonsToInstall) {
+ try {
+ yield this.installAddon(addon);
+ installResults.push({
+ id: addon.id,
+ result: "succeeded",
+ });
+ } catch (e) {
+ failureEncountered = true;
+ installResults.push({
+ id: addon.id,
+ result: "failed",
+ });
+ }
+ }
+ if (failureEncountered) {
+ throw {status: "failed",
+ results: installResults};
+ }
+ return {status: "succeeded",
+ results: installResults};
+ } catch(e) {
+ log.error("Could not check for addons", e);
+ throw e;
+ }
+ }),
+
+ /**
+ * Makes sure everything is cleaned up
+ */
+ uninit: function() {
+ let log = getScopedLogger("GMPInstallManager.uninit");
+ if (this._request) {
+ log.info("Aborting request");
+ this._request.abort();
+ }
+ if (this._deferred) {
+ log.info("Rejecting deferred");
+ this._deferred.reject({type: "uninitialized"});
+ }
+ log.info("Done cleanup");
+ },
+
+ /**
+ * If set to true, specifies to leave the temporary downloaded zip file.
+ * This is useful for tests.
+ */
+ overrideLeaveDownloadedZip: false,
+
+ /**
+ * The XMLHttpRequest succeeded and the document was loaded.
+ * @param event The nsIDOMEvent for the load
+ */
+ onLoadXML: function(event) {
+ let log = getScopedLogger("GMPInstallManager.onLoadXML");
+ try {
+ log.info("request completed downloading document");
+ let certs = null;
+ if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
+ GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) {
+ certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH);
+ }
+
+ let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN,
+ true);
+ log.info("allowNonBuiltIn: " + allowNonBuiltIn);
+
+ gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs);
+
+ this.parseResponseXML();
+ } catch (ex) {
+ log.error("could not load xml: " + ex);
+ this._deferred.reject({
+ target: event.target,
+ status: this._getChannelStatus(event.target),
+ message: "" + ex,
+ });
+ delete this._deferred;
+ }
+ },
+
+ /**
+ * Returns the status code for the XMLHttpRequest
+ */
+ _getChannelStatus: function(request) {
+ let log = getScopedLogger("GMPInstallManager._getChannelStatus");
+ let status = null;
+ try {
+ status = request.status;
+ log.info("request.status is: " + request.status);
+ }
+ catch (e) {
+ }
+
+ if (status == null) {
+ status = request.channel.QueryInterface(Ci.nsIRequest).status;
+ }
+ return status;
+ },
+
+ /**
+ * There was an error of some kind during the XMLHttpRequest. This
+ * error may have been caused by external factors (e.g. network
+ * issues) or internally (by a timeout).
+ *
+ * @param event The nsIDOMEvent for the error
+ */
+ onFailXML: function(failure, event) {
+ let log = getScopedLogger("GMPInstallManager.onFailXML " + failure);
+ let request = event.target;
+ let status = this._getChannelStatus(request);
+ let message = "request.status: " + status + " (" + event.type + ")";
+ log.warn(message);
+ this._deferred.reject({
+ target: request,
+ status: status,
+ message: message
+ });
+ delete this._deferred;
+ },
+
+ /**
+ * Returns an array of GMPAddon objects discovered by the update check.
+ * Or returns an empty array if there were any problems with parsing.
+ * If there's an error, it will be logged if logging is enabled.
+ */
+ parseResponseXML: function() {
+ try {
+ let log = getScopedLogger("GMPInstallManager.parseResponseXML");
+ let updatesElement = this._request.responseXML.documentElement;
+ if (!updatesElement) {
+ let message = "empty updates document";
+ log.warn(message);
+ this._deferred.reject({
+ target: this._request,
+ message: message
+ });
+ delete this._deferred;
+ return;
+ }
+
+ if (updatesElement.nodeName != "updates") {
+ let message = "got node name: " + updatesElement.nodeName +
+ ", expected: updates";
+ log.warn(message);
+ this._deferred.reject({
+ target: this._request,
+ message: message
+ });
+ delete this._deferred;
+ return;
+ }
+
+ const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE;
+ let gmpResults = [];
+ for (let i = 0; i < updatesElement.childNodes.length; ++i) {
+ let updatesChildElement = updatesElement.childNodes.item(i);
+ if (updatesChildElement.nodeType != ELEMENT_NODE) {
+ continue;
+ }
+ if (updatesChildElement.localName == "addons") {
+ gmpResults = GMPAddon.parseGMPAddonsNode(updatesChildElement);
+ }
+ }
+ this._deferred.resolve(gmpResults);
+ delete this._deferred;
+ } catch (e) {
+ this._deferred.reject({
+ target: this._request,
+ message: e
+ });
+ delete this._deferred;
+ }
+ },
+};
+
+/**
+ * Used to construct a single GMP addon
+ * GMPAddon objects are returns from GMPInstallManager.checkForAddons
+ * GMPAddon objects can also be used in calls to GMPInstallManager.installAddon
+ *
+ * @param gmpAddon The AUS response XML's DOM element `addon`
+ */
+function GMPAddon(gmpAddon) {
+ let log = getScopedLogger("GMPAddon.constructor");
+ gmpAddon.QueryInterface(Ci.nsIDOMElement);
+ ["id", "URL", "hashFunction",
+ "hashValue", "version", "size"].forEach(name => {
+ if (gmpAddon.hasAttribute(name)) {
+ this[name] = gmpAddon.getAttribute(name);
+ }
+ });
+ this.size = Number(this.size) || undefined;
+ log.info ("Created new addon: " + this.toString());
+}
+/**
+ * Parses an XML GMP addons node from AUS into an array
+ * @param addonsElement An nsIDOMElement compatible node with XML from AUS
+ * @return An array of GMPAddon results
+ */
+GMPAddon.parseGMPAddonsNode = function(addonsElement) {
+ let log = getScopedLogger("GMPAddon.parseGMPAddonsNode");
+ let gmpResults = [];
+ if (addonsElement.localName !== "addons") {
+ return;
+ }
+
+ addonsElement.QueryInterface(Ci.nsIDOMElement);
+ let addonCount = addonsElement.childNodes.length;
+ for (let i = 0; i < addonCount; ++i) {
+ let addonElement = addonsElement.childNodes.item(i);
+ if (addonElement.localName !== "addon") {
+ continue;
+ }
+ addonElement.QueryInterface(Ci.nsIDOMElement);
+ try {
+ gmpResults.push(new GMPAddon(addonElement));
+ } catch (e) {
+ log.warn("invalid addon: " + e);
+ continue;
+ }
+ }
+ return gmpResults;
+};
+GMPAddon.prototype = {
+ /**
+ * Returns a string representation of the addon
+ */
+ toString: function() {
+ return this.id + " (" +
+ "isValid: " + this.isValid +
+ ", isInstalled: " + this.isInstalled +
+ ", hashFunction: " + this.hashFunction+
+ ", hashValue: " + this.hashValue +
+ (this.size !== undefined ? ", size: " + this.size : "" ) +
+ ")";
+ },
+ /**
+ * If all the fields aren't specified don't consider this addon valid
+ * @return true if the addon is parsed and valid
+ */
+ get isValid() {
+ return this.id && this.URL && this.version &&
+ this.hashFunction && !!this.hashValue;
+ },
+ get isInstalled() {
+ return this.version &&
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) === this.version;
+ },
+ get isEME() {
+ return this.id == "gmp-widevinecdm" || this.id.indexOf("gmp-eme-") == 0;
+ },
+};
+/**
+ * Constructs a GMPExtractor object which is used to extract a GMP zip
+ * into the specified location. (Which typically leties per platform)
+ * @param zipPath The path on disk of the zip file to extract
+ */
+function GMPExtractor(zipPath, installToDirPath) {
+ this.zipPath = zipPath;
+ this.installToDirPath = installToDirPath;
+}
+GMPExtractor.prototype = {
+ /**
+ * Obtains a list of all the entries in a zipfile in the format of *.*.
+ * This also includes files inside directories.
+ *
+ * @param zipReader the nsIZipReader to check
+ * @return An array of string name entries which can be used
+ * in nsIZipReader.extract
+ */
+ _getZipEntries: function(zipReader) {
+ let entries = [];
+ let enumerator = zipReader.findEntries("*.*");
+ while (enumerator.hasMore()) {
+ entries.push(enumerator.getNext());
+ }
+ return entries;
+ },
+ /**
+ * Installs the this.zipPath contents into the directory used to store GMP
+ * addons for the current platform.
+ *
+ * @return a promise which will be resolved or rejected
+ * See GMPInstallManager.installAddon for resolve/rejected info
+ */
+ install: function() {
+ try {
+ let log = getScopedLogger("GMPExtractor.install");
+ this._deferred = Promise.defer();
+ log.info("Installing " + this.zipPath + "...");
+ // Get the input zip file
+ let zipFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+ zipFile.initWithPath(this.zipPath);
+
+ // Initialize a zipReader and obtain the entries
+ var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ zipReader.open(zipFile)
+ let entries = this._getZipEntries(zipReader);
+ let extractedPaths = [];
+
+ // Extract each of the entries
+ entries.forEach(entry => {
+ // We don't need these types of files
+ if (entry.includes("__MACOSX")) {
+ return;
+ }
+ let outFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ outFile.initWithPath(this.installToDirPath);
+ outFile.appendRelativePath(entry);
+
+ // Make sure the directory hierarchy exists
+ if(!outFile.parent.exists()) {
+ outFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ }
+ zipReader.extract(entry, outFile);
+ extractedPaths.push(outFile.path);
+ log.info(entry + " was successfully extracted to: " +
+ outFile.path);
+ });
+ zipReader.close();
+ if (!GMPInstallManager.overrideLeaveDownloadedZip) {
+ zipFile.remove(false);
+ }
+
+ log.info(this.zipPath + " was installed successfully");
+ this._deferred.resolve(extractedPaths);
+ } catch (e) {
+ if (zipReader) {
+ zipReader.close();
+ }
+ this._deferred.reject({
+ target: this,
+ status: e,
+ type: "exception"
+ });
+ }
+ return this._deferred.promise;
+ }
+};
+
+
+/**
+ * Constructs an object which downloads and initiates an install of
+ * the specified GMPAddon object.
+ * @param gmpAddon The addon to install.
+ */
+function GMPDownloader(gmpAddon)
+{
+ this._gmpAddon = gmpAddon;
+}
+/**
+ * Computes the file hash of fileToHash with the specified hash function
+ * @param hashFunctionName A hash function name such as sha512
+ * @param fileToHash An nsIFile to hash
+ * @return a promise which resolve to a digest in binary hex format
+ */
+GMPDownloader.computeHash = function(hashFunctionName, fileToHash) {
+ let log = getScopedLogger("GMPDownloader.computeHash");
+ let digest;
+ let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fileStream.init(fileToHash, FileUtils.MODE_RDONLY,
+ FileUtils.PERMS_FILE, 0);
+ try {
+ let hash = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ let hashFunction =
+ Ci.nsICryptoHash[hashFunctionName.toUpperCase()];
+ if (!hashFunction) {
+ log.error("could not get hash function");
+ return Promise.reject();
+ }
+ hash.init(hashFunction);
+ hash.updateFromStream(fileStream, -1);
+ digest = binaryToHex(hash.finish(false));
+ } catch (e) {
+ log.warn("failed to compute hash: " + e);
+ digest = "";
+ }
+ fileStream.close();
+ return Promise.resolve(digest);
+},
+GMPDownloader.prototype = {
+ /**
+ * Starts the download process for an addon.
+ * @return a promise which will be resolved or rejected
+ * See GMPInstallManager.installAddon for resolve/rejected info
+ */
+ start: function() {
+ let log = getScopedLogger("GMPDownloader.start");
+ this._deferred = Promise.defer();
+ if (!this._gmpAddon.isValid) {
+ log.info("gmpAddon is not valid, will not continue");
+ return Promise.reject({
+ target: this,
+ status: status,
+ type: "downloaderr"
+ });
+ }
+
+ let uri = Services.io.newURI(this._gmpAddon.URL, null, null);
+ this._request = Cc["@mozilla.org/network/incremental-download;1"].
+ createInstance(Ci.nsIIncrementalDownload);
+ let gmpFile = FileUtils.getFile("TmpD", [this._gmpAddon.id + ".zip"]);
+ if (gmpFile.exists()) {
+ gmpFile.remove(false);
+ }
+
+ log.info("downloading from " + uri.spec + " to " + gmpFile.path);
+ this._request.init(uri, gmpFile, DOWNLOAD_CHUNK_BYTES_SIZE,
+ DOWNLOAD_INTERVAL);
+ this._request.start(this, null);
+ return this._deferred.promise;
+ },
+ // For nsIRequestObserver
+ onStartRequest: function(request, context) {
+ },
+ // For nsIRequestObserver
+ // Called when the GMP addon zip file is downloaded
+ onStopRequest: function(request, context, status) {
+ let log = getScopedLogger("GMPDownloader.onStopRequest");
+ log.info("onStopRequest called");
+ if (!Components.isSuccessCode(status)) {
+ log.info("status failed: " + status);
+ this._deferred.reject({
+ target: this,
+ status: status,
+ type: "downloaderr"
+ });
+ return;
+ }
+
+ let promise = this._verifyDownload();
+ promise.then(() => {
+ log.info("GMP file is ready to unzip");
+ let destination = this._request.destination;
+
+ let zipPath = destination.path;
+ let gmpAddon = this._gmpAddon;
+ let installToDirPath = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+ let path = OS.Path.join(OS.Constants.Path.profileDir,
+ gmpAddon.id,
+ gmpAddon.version);
+ installToDirPath.initWithPath(path);
+ log.info("install to directory path: " + installToDirPath.path);
+ let gmpInstaller = new GMPExtractor(zipPath, installToDirPath.path);
+ let installPromise = gmpInstaller.install();
+ installPromise.then(extractedPaths => {
+ // Success, set the prefs
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id);
+ // Setting the version pref signals installation completion to consumers,
+ // if you need to set other prefs etc. do it before this.
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version,
+ gmpAddon.id);
+ this._deferred.resolve(extractedPaths);
+ }, err => {
+ this._deferred.reject(err);
+ });
+ }, err => {
+ log.warn("verifyDownload check failed");
+ this._deferred.reject({
+ target: this,
+ status: 200,
+ type: "verifyerr"
+ });
+ });
+ },
+ /**
+ * Verifies that the downloaded zip file's hash matches the GMPAddon hash.
+ * @return a promise which resolves if the download verifies
+ */
+ _verifyDownload: function() {
+ let verifyDownloadDeferred = Promise.defer();
+ let log = getScopedLogger("GMPDownloader._verifyDownload");
+ log.info("_verifyDownload called");
+ if (!this._request) {
+ return Promise.reject();
+ }
+
+ let destination = this._request.destination;
+ log.info("for path: " + destination.path);
+
+ // Ensure that the file size matches the expected file size.
+ if (this._gmpAddon.size !== undefined &&
+ destination.fileSize != this._gmpAddon.size) {
+ log.warn("Downloader:_verifyDownload downloaded size " +
+ destination.fileSize + " != expected size " +
+ this._gmpAddon.size + ".");
+ return Promise.reject();
+ }
+
+ let promise = GMPDownloader.computeHash(this._gmpAddon.hashFunction, destination);
+ promise.then(digest => {
+ let expectedDigest = this._gmpAddon.hashValue.toLowerCase();
+ if (digest !== expectedDigest) {
+ log.warn("hashes do not match! Got: `" +
+ digest + "`, expected: `" + expectedDigest + "`");
+ this._deferred.reject();
+ return;
+ }
+
+ log.info("hashes match!");
+ verifyDownloadDeferred.resolve();
+ }, err => {
+ verifyDownloadDeferred.reject();
+ });
+ return verifyDownloadDeferred.promise;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver])
+};
+
+/**
+ * Convert a string containing binary values to hex.
+ */
+function binaryToHex(input) {
+ let result = "";
+ for (let i = 0; i < input.length; ++i) {
+ let hex = input.charCodeAt(i).toString(16);
+ if (hex.length == 1)
+ hex = "0" + hex;
+ result += hex;
+ }
+ return result;
+}
diff --git a/components/extensions/src/GMPProvider.jsm b/components/extensions/src/GMPProvider.jsm
new file mode 100644
index 000000000..c89427101
--- /dev/null
+++ b/components/extensions/src/GMPProvider.jsm
@@ -0,0 +1,605 @@
+/* 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 Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/GMPUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(
+ this, "GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(
+ this, "setTimeout", "resource://gre/modules/Timer.jsm");
+
+const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
+const STRING_TYPE_NAME = "type.%ID%.name";
+
+const SEC_IN_A_DAY = 24 * 60 * 60;
+// How long to wait after a user enabled EME before attempting to download CDMs.
+const GMP_CHECK_DELAY = 10 * 1000; // milliseconds
+
+const NS_GRE_DIR = "GreD";
+const CLEARKEY_PLUGIN_ID = "gmp-clearkey";
+const CLEARKEY_VERSION = "0.1";
+
+const GMP_LICENSE_INFO = "gmp_license_info";
+
+const GMP_PLUGINS = [
+ {
+ id: OPEN_H264_ID,
+ name: "openH264_name",
+ description: "openH264_description2",
+ // The following licenseURL is part of an awful hack to include the OpenH264
+ // license without having bug 624602 fixed yet, and intentionally ignores
+ // localisation.
+ licenseURL: "chrome://mozapps/content/extensions/OpenH264-license.txt",
+ homepageURL: "http://www.openh264.org/",
+ optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul"
+ },
+ {
+ id: WIDEVINE_ID,
+ name: "widevine_name",
+ // Describe the purpose of both CDMs in the same way.
+ description: "widevine_description2",
+ licenseURL: "https://www.google.com/policies/privacy/",
+ homepageURL: "https://www.widevine.com/",
+ optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul",
+ isEME: true
+ }];
+XPCOMUtils.defineConstant(this, "GMP_PLUGINS", GMP_PLUGINS);
+
+XPCOMUtils.defineLazyGetter(this, "pluginsBundle",
+ () => Services.strings.createBundle("chrome://global/locale/plugins.properties"));
+XPCOMUtils.defineLazyGetter(this, "gmpService",
+ () => Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(Ci.mozIGeckoMediaPluginChromeService));
+
+var messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+
+var gLogger;
+var gLogAppenderDump = null;
+
+function configureLogging() {
+ if (!gLogger) {
+ gLogger = Log.repository.getLogger("Toolkit.GMP");
+ gLogger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
+ }
+ gLogger.level = GMPPrefs.get(GMPPrefs.KEY_LOGGING_LEVEL, Log.Level.Warn);
+
+ let logDumping = GMPPrefs.get(GMPPrefs.KEY_LOGGING_DUMP, false);
+ if (logDumping != !!gLogAppenderDump) {
+ if (logDumping) {
+ gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter());
+ gLogger.addAppender(gLogAppenderDump);
+ } else {
+ gLogger.removeAppender(gLogAppenderDump);
+ gLogAppenderDump = null;
+ }
+ }
+}
+
+
+
+/**
+ * The GMPWrapper provides the info for the various GMP plugins to public
+ * callers through the API.
+ */
+function GMPWrapper(aPluginInfo) {
+ this._plugin = aPluginInfo;
+ this._log =
+ Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP",
+ "GMPWrapper(" +
+ this._plugin.id + ") ");
+ Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED,
+ this._plugin.id),
+ this.onPrefEnabledChanged, this);
+ Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION,
+ this._plugin.id),
+ this.onPrefVersionChanged, this);
+ if (this._plugin.isEME) {
+ Preferences.observe(GMPPrefs.KEY_EME_ENABLED,
+ this.onPrefEMEGlobalEnabledChanged, this);
+ messageManager.addMessageListener("EMEVideo:ContentMediaKeysRequest", this);
+ }
+}
+
+GMPWrapper.prototype = {
+ // An active task that checks for plugin updates and installs them.
+ _updateTask: null,
+ _gmpPath: null,
+ _isUpdateCheckPending: false,
+
+ optionsType: AddonManager.OPTIONS_TYPE_INLINE,
+ get optionsURL() { return this._plugin.optionsURL; },
+
+ set gmpPath(aPath) { this._gmpPath = aPath; },
+ get gmpPath() {
+ if (!this._gmpPath && this.isInstalled) {
+ this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir,
+ this._plugin.id,
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION,
+ null, this._plugin.id));
+ }
+ return this._gmpPath;
+ },
+
+ get id() { return this._plugin.id; },
+ get type() { return "plugin"; },
+ get isGMPlugin() { return true; },
+ get name() { return this._plugin.name; },
+ get creator() { return null; },
+ get homepageURL() { return this._plugin.homepageURL; },
+
+ get description() { return this._plugin.description; },
+ get fullDescription() { return this._plugin.fullDescription; },
+
+ get version() { return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, null,
+ this._plugin.id); },
+
+ get isActive() { return !this.appDisabled && !this.userDisabled; },
+ get appDisabled() {
+ if (this._plugin.isEME && !GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) {
+ // If "media.eme.enabled" is false, all EME plugins are disabled.
+ return true;
+ }
+ return false;
+ },
+
+ get userDisabled() {
+ return !GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, this._plugin.id);
+ },
+ set userDisabled(aVal) { GMPPrefs.set(GMPPrefs.KEY_PLUGIN_ENABLED,
+ aVal === false,
+ this._plugin.id); },
+
+ get blocklistState() { return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; },
+ get size() { return 0; },
+ get scope() { return AddonManager.SCOPE_APPLICATION; },
+ get pendingOperations() { return AddonManager.PENDING_NONE; },
+
+ get operationsRequiringRestart() { return AddonManager.OP_NEEDS_RESTART_NONE },
+
+ get permissions() {
+ let permissions = 0;
+ if (!this.appDisabled) {
+ permissions |= AddonManager.PERM_CAN_UPGRADE;
+ permissions |= this.userDisabled ? AddonManager.PERM_CAN_ENABLE :
+ AddonManager.PERM_CAN_DISABLE;
+ }
+ return permissions;
+ },
+
+ get updateDate() {
+ let time = Number(GMPPrefs.get(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, null,
+ this._plugin.id));
+ if (time !== NaN && this.isInstalled) {
+ return new Date(time * 1000)
+ }
+ return null;
+ },
+
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ get foreignInstall() {
+ return false;
+ },
+
+ isCompatibleWith: function(aAppVersion, aPlatformVersion) {
+ return true;
+ },
+
+ get applyBackgroundUpdates() {
+ if (!GMPPrefs.isSet(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id)) {
+ return AddonManager.AUTOUPDATE_DEFAULT;
+ }
+
+ return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id) ?
+ AddonManager.AUTOUPDATE_ENABLE : AddonManager.AUTOUPDATE_DISABLE;
+ },
+
+ set applyBackgroundUpdates(aVal) {
+ if (aVal == AddonManager.AUTOUPDATE_DEFAULT) {
+ GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id);
+ } else if (aVal == AddonManager.AUTOUPDATE_ENABLE) {
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id);
+ } else if (aVal == AddonManager.AUTOUPDATE_DISABLE) {
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, this._plugin.id);
+ }
+ },
+
+ findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
+ this._log.trace("findUpdates() - " + this._plugin.id + " - reason=" +
+ aReason);
+
+ AddonManagerPrivate.callNoUpdateListeners(this, aListener);
+
+ if (aReason === AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
+ if (!AddonManager.shouldAutoUpdate(this)) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - no autoupdate");
+ return Promise.resolve(false);
+ }
+
+ let secSinceLastCheck =
+ Date.now() / 1000 - Preferences.get(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0);
+ if (secSinceLastCheck <= SEC_IN_A_DAY) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - last check was less then a day ago");
+ return Promise.resolve(false);
+ }
+ } else if (aReason !== AddonManager.UPDATE_WHEN_USER_REQUESTED) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - the given reason to update is not supported");
+ return Promise.resolve(false);
+ }
+
+ if (this._updateTask !== null) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - update task already running");
+ return this._updateTask;
+ }
+
+ this._updateTask = Task.spawn(function* GMPProvider_updateTask() {
+ this._log.trace("findUpdates() - updateTask");
+ try {
+ let installManager = new GMPInstallManager();
+ let gmpAddons = yield installManager.checkForAddons();
+ let update = gmpAddons.find(function(aAddon) {
+ return aAddon.id === this._plugin.id;
+ }, this);
+ if (update && update.isValid && !update.isInstalled) {
+ this._log.trace("findUpdates() - found update for " +
+ this._plugin.id + ", installing");
+ yield installManager.installAddon(update);
+ } else {
+ this._log.trace("findUpdates() - no updates for " + this._plugin.id);
+ }
+ this._log.info("findUpdates() - updateTask succeeded for " +
+ this._plugin.id);
+ } catch (e) {
+ this._log.error("findUpdates() - updateTask for " + this._plugin.id +
+ " threw", e);
+ throw e;
+ } finally {
+ this._updateTask = null;
+ return true;
+ }
+ }.bind(this));
+
+ return this._updateTask;
+ },
+
+ get pluginMimeTypes() { return []; },
+ get pluginLibraries() {
+ if (this.isInstalled) {
+ let path = this.version;
+ return [path];
+ }
+ return [];
+ },
+ get pluginFullpath() {
+ if (this.isInstalled) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir,
+ this._plugin.id,
+ this.version);
+ return [path];
+ }
+ return [];
+ },
+
+ get isInstalled() {
+ return this.version && this.version.length > 0;
+ },
+
+ _handleEnabledChanged: function() {
+ AddonManagerPrivate.callAddonListeners(this.isActive ?
+ "onEnabling" : "onDisabling",
+ this, false);
+ if (this._gmpPath) {
+ if (this.isActive) {
+ this._log.info("onPrefEnabledChanged() - adding gmp directory " +
+ this._gmpPath);
+ gmpService.addPluginDirectory(this._gmpPath);
+ } else {
+ this._log.info("onPrefEnabledChanged() - removing gmp directory " +
+ this._gmpPath);
+ gmpService.removePluginDirectory(this._gmpPath);
+ }
+ }
+ AddonManagerPrivate.callAddonListeners(this.isActive ?
+ "onEnabled" : "onDisabled",
+ this);
+ },
+
+ onPrefEMEGlobalEnabledChanged: function() {
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this,
+ ["appDisabled"]);
+ if (this.appDisabled) {
+ this.uninstallPlugin();
+ } else {
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this,
+ null, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", this, false);
+ AddonManagerPrivate.callAddonListeners("onInstalled", this);
+ this.checkForUpdates(GMP_CHECK_DELAY);
+ }
+ if (!this.userDisabled) {
+ this._handleEnabledChanged();
+ }
+ },
+
+ checkForUpdates: function(delay) {
+ if (this._isUpdateCheckPending) {
+ return;
+ }
+ this._isUpdateCheckPending = true;
+ GMPPrefs.reset(GMPPrefs.KEY_UPDATE_LAST_CHECK, null);
+ // Delay this in case the user changes his mind and doesn't want to
+ // enable EME after all.
+ setTimeout(() => {
+ if (!this.appDisabled) {
+ let gmpInstallManager = new GMPInstallManager();
+ // We don't really care about the results, if someone is interested
+ // they can check the log.
+ gmpInstallManager.simpleCheckAndInstall().then(null, () => {});
+ }
+ this._isUpdateCheckPending = false;
+ }, delay);
+ },
+
+ receiveMessage: function({target: browser, data: data}) {
+ this._log.trace("receiveMessage() data=" + data);
+ let parsedData;
+ try {
+ parsedData = JSON.parse(data);
+ } catch(ex) {
+ this._log.error("Malformed EME video message with data: " + data);
+ return;
+ }
+ let {status: status, keySystem: keySystem} = parsedData;
+ if (status == "cdm-not-installed" || status == "cdm-insufficient-version") {
+ this.checkForUpdates(0);
+ }
+ },
+
+ onPrefEnabledChanged: function() {
+ if (!this._plugin.isEME || !this.appDisabled) {
+ this._handleEnabledChanged();
+ }
+ },
+
+ onPrefVersionChanged: function() {
+ AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
+ if (this._gmpPath) {
+ this._log.info("onPrefVersionChanged() - unregistering gmp directory " +
+ this._gmpPath);
+ gmpService.removeAndDeletePluginDirectory(this._gmpPath, true /* can defer */);
+ }
+ AddonManagerPrivate.callAddonListeners("onUninstalled", this);
+
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this,
+ null, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", this, false);
+ this._gmpPath = null;
+ if (this.isInstalled) {
+ this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir,
+ this._plugin.id,
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION,
+ null, this._plugin.id));
+ }
+ if (this._gmpPath && this.isActive) {
+ this._log.info("onPrefVersionChanged() - registering gmp directory " +
+ this._gmpPath);
+ gmpService.addPluginDirectory(this._gmpPath);
+ }
+ AddonManagerPrivate.callAddonListeners("onInstalled", this);
+ },
+
+ uninstallPlugin: function() {
+ AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
+ if (this.gmpPath) {
+ this._log.info("uninstallPlugin() - unregistering gmp directory " +
+ this.gmpPath);
+ gmpService.removeAndDeletePluginDirectory(this.gmpPath);
+ }
+ GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_VERSION, this.id);
+ AddonManagerPrivate.callAddonListeners("onUninstalled", this);
+ },
+
+ shutdown: function() {
+ Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED,
+ this._plugin.id),
+ this.onPrefEnabledChanged, this);
+ Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION,
+ this._plugin.id),
+ this.onPrefVersionChanged, this);
+ if (this._plugin.isEME) {
+ Preferences.ignore(GMPPrefs.KEY_EME_ENABLED,
+ this.onPrefEMEGlobalEnabledChanged, this);
+ messageManager.removeMessageListener("EMEVideo:ContentMediaKeysRequest", this);
+ }
+ return this._updateTask;
+ },
+};
+
+var GMPProvider = {
+ get name() { return "GMPProvider"; },
+
+ _plugins: null,
+
+ startup: function() {
+ configureLogging();
+ this._log = Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP",
+ "GMPProvider.");
+ this.buildPluginList();
+ this.ensureProperCDMInstallState();
+
+ Preferences.observe(GMPPrefs.KEY_LOG_BASE, configureLogging);
+
+ for (let [id, plugin] of this._plugins) {
+ let wrapper = plugin.wrapper;
+ let gmpPath = wrapper.gmpPath;
+ let isEnabled = wrapper.isActive;
+ this._log.trace("startup - enabled=" + isEnabled + ", gmpPath=" +
+ gmpPath);
+
+ if (gmpPath && isEnabled) {
+ this._log.info("startup - adding gmp directory " + gmpPath);
+ try {
+ gmpService.addPluginDirectory(gmpPath);
+ } catch (e if e.name == 'NS_ERROR_NOT_AVAILABLE') {
+ this._log.warn("startup - adding gmp directory failed with " +
+ e.name + " - sandboxing not available?", e);
+ }
+ }
+ }
+
+ if (Preferences.get(GMPPrefs.KEY_EME_ENABLED, false)) {
+ try {
+ let greDir = Services.dirsvc.get(NS_GRE_DIR,
+ Ci.nsILocalFile);
+ let clearkeyPath = OS.Path.join(greDir.path,
+ CLEARKEY_PLUGIN_ID,
+ CLEARKEY_VERSION);
+ this._log.info("startup - adding clearkey CDM directory " +
+ clearkeyPath);
+ gmpService.addPluginDirectory(clearkeyPath);
+ } catch (e) {
+ this._log.warn("startup - adding clearkey CDM failed", e);
+ }
+ }
+ },
+
+ shutdown: function() {
+ this._log.trace("shutdown");
+ Preferences.ignore(GMPPrefs.KEY_LOG_BASE, configureLogging);
+
+ let shutdownTask = Task.spawn(function* GMPProvider_shutdownTask() {
+ this._log.trace("shutdown - shutdownTask");
+ let shutdownSucceeded = true;
+
+ for (let plugin of this._plugins.values()) {
+ try {
+ yield plugin.wrapper.shutdown();
+ } catch (e) {
+ shutdownSucceeded = false;
+ }
+ }
+
+ this._plugins = null;
+
+ if (!shutdownSucceeded) {
+ throw new Error("Shutdown failed");
+ }
+ }.bind(this));
+
+ return shutdownTask;
+ },
+
+ getAddonByID: function(aId, aCallback) {
+ if (!this.isEnabled) {
+ aCallback(null);
+ return;
+ }
+
+ let plugin = this._plugins.get(aId);
+ if (plugin && !GMPUtils.isPluginHidden(plugin)) {
+ aCallback(plugin.wrapper);
+ } else {
+ aCallback(null);
+ }
+ },
+
+ getAddonsByTypes: function(aTypes, aCallback) {
+ if (!this.isEnabled ||
+ (aTypes && aTypes.indexOf("plugin") < 0)) {
+ aCallback([]);
+ return;
+ }
+
+ // Tycho:
+ // let results = [p.wrapper for ([id, p] of this._plugins)
+ // if (!GMPUtils.isPluginHidden(p))];
+ let results = [];
+ for (let [id, p] of this._plugins) {
+ if (!GMPUtils.isPluginHidden(p)) {
+ results.push(p.wrapper);
+ }
+ }
+
+ aCallback(results);
+ },
+
+ get isEnabled() {
+ return GMPPrefs.get(GMPPrefs.KEY_PROVIDER_ENABLED, false);
+ },
+
+ generateFullDescription: function(aLicenseURL, aLicenseInfo) {
+ return "<xhtml:a href=\"" + aLicenseURL + "\" target=\"_blank\">" +
+ aLicenseInfo + "</xhtml:a>."
+ },
+
+ buildPluginList: function() {
+ let licenseInfo = pluginsBundle.GetStringFromName(GMP_LICENSE_INFO);
+
+ this._plugins = new Map();
+ for (let aPlugin of GMP_PLUGINS) {
+ let plugin = {
+ id: aPlugin.id,
+ name: pluginsBundle.GetStringFromName(aPlugin.name),
+ description: pluginsBundle.GetStringFromName(aPlugin.description),
+ homepageURL: aPlugin.homepageURL,
+ optionsURL: aPlugin.optionsURL,
+ wrapper: null,
+ isEME: aPlugin.isEME,
+ };
+ if (aPlugin.licenseURL) {
+ plugin.fullDescription =
+ this.generateFullDescription(aPlugin.licenseURL, licenseInfo);
+ }
+ plugin.wrapper = new GMPWrapper(plugin);
+ this._plugins.set(plugin.id, plugin);
+ }
+ },
+
+ ensureProperCDMInstallState: function() {
+ if (!GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) {
+ for (let [id, plugin] of this._plugins) {
+ if (plugin.isEME && plugin.wrapper.isInstalled) {
+ gmpService.addPluginDirectory(plugin.wrapper.gmpPath);
+ plugin.wrapper.uninstallPlugin();
+ }
+ }
+ }
+ },
+};
+
+AddonManagerPrivate.registerProvider(GMPProvider, [
+ new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 6000,
+ AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE)
+]);
diff --git a/components/extensions/src/GMPUtils.jsm b/components/extensions/src/GMPUtils.jsm
new file mode 100644
index 000000000..593fc3c8d
--- /dev/null
+++ b/components/extensions/src/GMPUtils.jsm
@@ -0,0 +1,187 @@
+/* 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";
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} =
+ Components;
+
+this.EXPORTED_SYMBOLS = [ "GMP_PLUGIN_IDS",
+ "GMPPrefs",
+ "GMPUtils",
+ "OPEN_H264_ID",
+ "WIDEVINE_ID" ];
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// GMP IDs
+const OPEN_H264_ID = "gmp-gmpopenh264";
+const WIDEVINE_ID = "gmp-widevinecdm";
+const GMP_PLUGIN_IDS = [ OPEN_H264_ID, WIDEVINE_ID ];
+
+var GMPPluginUnsupportedReason = {
+ NOT_WINDOWS: 1,
+ WINDOWS_VERSION: 2,
+};
+
+var GMPPluginHiddenReason = {
+ UNSUPPORTED: 1,
+ EME_DISABLED: 2,
+};
+
+this.GMPUtils = {
+ /**
+ * Checks whether or not a given plugin is hidden. Hidden plugins are neither
+ * downloaded nor displayed in the addons manager.
+ * @param aPlugin
+ * The plugin to check.
+ */
+ isPluginHidden: function(aPlugin) {
+ if (!aPlugin.isEME) {
+ return false;
+ }
+
+ if (!this._isPluginSupported(aPlugin) ||
+ !this._isPluginVisible(aPlugin)) {
+ return true;
+ }
+
+ if (!GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Checks whether or not a given plugin is supported by the current OS.
+ * @param aPlugin
+ * The plugin to check.
+ */
+ _isPluginSupported: function(aPlugin) {
+ if (this._isPluginForceSupported(aPlugin)) {
+ return true;
+ }
+ if (aPlugin.id == WIDEVINE_ID) {
+
+#if defined(XP_WIN) || defined(XP_LINUX)
+ // The Widevine plugin is available for Windows versions Vista and later,
+ // Mac OSX, and Linux.
+ return true;
+#else
+ return false;
+#endif
+ }
+
+ return true;
+ },
+
+ /**
+ * Checks whether or not a given plugin is visible in the addons manager
+ * UI and the "enable DRM" notification box. This can be used to test
+ * plugins that aren't yet turned on in the mozconfig.
+ * @param aPlugin
+ * The plugin to check.
+ */
+ _isPluginVisible: function(aPlugin) {
+ return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VISIBLE, false, aPlugin.id);
+ },
+
+ /**
+ * Checks whether or not a given plugin is forced-supported. This is used
+ * in automated tests to override the checks that prevent GMPs running on an
+ * unsupported platform.
+ * @param aPlugin
+ * The plugin to check.
+ */
+ _isPluginForceSupported: function(aPlugin) {
+ return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, false, aPlugin.id);
+ },
+};
+
+/**
+ * Manages preferences for GMP addons
+ */
+this.GMPPrefs = {
+ KEY_EME_ENABLED: "media.eme.enabled",
+ KEY_PLUGIN_ENABLED: "media.{0}.enabled",
+ KEY_PLUGIN_LAST_UPDATE: "media.{0}.lastUpdate",
+ KEY_PLUGIN_VERSION: "media.{0}.version",
+ KEY_PLUGIN_AUTOUPDATE: "media.{0}.autoupdate",
+ KEY_PLUGIN_VISIBLE: "media.{0}.visible",
+ KEY_PLUGIN_ABI: "media.{0}.abi",
+ KEY_PLUGIN_FORCE_SUPPORTED: "media.{0}.forceSupported",
+ KEY_URL: "media.gmp-manager.url",
+ KEY_URL_OVERRIDE: "media.gmp-manager.url.override",
+ KEY_CERT_CHECKATTRS: "media.gmp-manager.cert.checkAttributes",
+ KEY_CERT_REQUIREBUILTIN: "media.gmp-manager.cert.requireBuiltIn",
+ KEY_UPDATE_LAST_CHECK: "media.gmp-manager.lastCheck",
+ KEY_SECONDS_BETWEEN_CHECKS: "media.gmp-manager.secondsBetweenChecks",
+ KEY_UPDATE_ENABLED: "media.gmp-manager.updateEnabled",
+ KEY_APP_DISTRIBUTION: "distribution.id",
+ KEY_APP_DISTRIBUTION_VERSION: "distribution.version",
+ KEY_BUILDID: "media.gmp-manager.buildID",
+ KEY_CERTS_BRANCH: "media.gmp-manager.certs.",
+ KEY_PROVIDER_ENABLED: "media.gmp-provider.enabled",
+ KEY_LOG_BASE: "media.gmp.log.",
+ KEY_LOGGING_LEVEL: "media.gmp.log.level",
+ KEY_LOGGING_DUMP: "media.gmp.log.dump",
+
+ /**
+ * Obtains the specified preference in relation to the specified plugin.
+ * @param aKey The preference key value to use.
+ * @param aDefaultValue The default value if no preference exists.
+ * @param aPlugin The plugin to scope the preference to.
+ * @return The obtained preference value, or the defaultValue if none exists.
+ */
+ get: function(aKey, aDefaultValue, aPlugin) {
+ if (aKey === this.KEY_APP_DISTRIBUTION ||
+ aKey === this.KEY_APP_DISTRIBUTION_VERSION) {
+ return Services.prefs.getDefaultBranch(null).getCharPref(aKey, "default");
+ }
+ return Preferences.get(this.getPrefKey(aKey, aPlugin), aDefaultValue);
+ },
+
+ /**
+ * Sets the specified preference in relation to the specified plugin.
+ * @param aKey The preference key value to use.
+ * @param aVal The value to set.
+ * @param aPlugin The plugin to scope the preference to.
+ */
+ set: function(aKey, aVal, aPlugin) {
+ Preferences.set(this.getPrefKey(aKey, aPlugin), aVal);
+ },
+
+ /**
+ * Checks whether or not the specified preference is set in relation to the
+ * specified plugin.
+ * @param aKey The preference key value to use.
+ * @param aPlugin The plugin to scope the preference to.
+ * @return true if the preference is set, false otherwise.
+ */
+ isSet: function(aKey, aPlugin) {
+ return Preferences.isSet(this.getPrefKey(aKey, aPlugin));
+ },
+
+ /**
+ * Resets the specified preference in relation to the specified plugin to its
+ * default.
+ * @param aKey The preference key value to use.
+ * @param aPlugin The plugin to scope the preference to.
+ */
+ reset: function(aKey, aPlugin) {
+ Preferences.reset(this.getPrefKey(aKey, aPlugin));
+ },
+
+ /**
+ * Scopes the specified preference key to the specified plugin.
+ * @param aKey The preference key value to use.
+ * @param aPlugin The plugin to scope the preference to.
+ * @return A preference key scoped to the specified plugin.
+ */
+ getPrefKey: function(aKey, aPlugin) {
+ return aKey.replace("{0}", aPlugin || "");
+ },
+};
diff --git a/components/extensions/src/LightweightThemeConsumer.jsm b/components/extensions/src/LightweightThemeConsumer.jsm
new file mode 100644
index 000000000..9419fdcf2
--- /dev/null
+++ b/components/extensions/src/LightweightThemeConsumer.jsm
@@ -0,0 +1,164 @@
+/* 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.EXPORTED_SYMBOLS = ["LightweightThemeConsumer"];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeImageOptimizer",
+ "resource://gre/modules/addons/LightweightThemeImageOptimizer.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+this.LightweightThemeConsumer =
+ function LightweightThemeConsumer(aDocument) {
+ this._doc = aDocument;
+ this._win = aDocument.defaultView;
+ this._footerId = aDocument.documentElement.getAttribute("lightweightthemesfooter");
+
+/* XXX: If we want to disable LWTs for PB mode, this would be needed.
+ * Perhaps make this pref-controlled in the future if people want it?
+ if (PrivateBrowsingUtils.isWindowPrivate(this._win) &&
+ !PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ return;
+ } */
+
+ let screen = this._win.screen;
+ this._lastScreenWidth = screen.width;
+ this._lastScreenHeight = screen.height;
+
+ Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
+
+ var temp = {};
+ Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
+ this._update(temp.LightweightThemeManager.currentThemeForDisplay);
+ this._win.addEventListener("resize", this);
+}
+
+LightweightThemeConsumer.prototype = {
+ _lastData: null,
+ _lastScreenWidth: null,
+ _lastScreenHeight: null,
+ // Whether the active lightweight theme should be shown on the window.
+ _enabled: true,
+ // Whether a lightweight theme is enabled.
+ _active: false,
+
+ enable: function() {
+ this._enabled = true;
+ this._update(this._lastData);
+ },
+
+ disable: function() {
+ // Dance to keep the data, but reset the applied styles:
+ let lastData = this._lastData
+ this._update(null);
+ this._enabled = false;
+ this._lastData = lastData;
+ },
+
+ getData: function() {
+ return this._enabled ? Cu.cloneInto(this._lastData, this._win) : null;
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ if (aTopic != "lightweight-theme-styling-update")
+ return;
+
+ this._update(JSON.parse(aData));
+ },
+
+ handleEvent: function (aEvent) {
+ let {width, height} = this._win.screen;
+
+ if (this._lastScreenWidth != width || this._lastScreenHeight != height) {
+ this._lastScreenWidth = width;
+ this._lastScreenHeight = height;
+ if (!this._active)
+ return;
+ this._update(this._lastData);
+ Services.obs.notifyObservers(this._win, "lightweight-theme-optimized",
+ JSON.stringify(this._lastData));
+ }
+ },
+
+ destroy: function () {
+/* XXX: If we want to disable LWTs for PB mode, this would be needed.
+ if (!PrivateBrowsingUtils.isWindowPrivate(this._win) ||
+ PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ Services.obs.removeObserver(this, "lightweight-theme-styling-update");
+
+ this._win.removeEventListener("resize", this);
+ } */
+
+ Services.obs.removeObserver(this, "lightweight-theme-styling-update");
+ this._win.removeEventListener("resize", this);
+
+ this._win = this._doc = null;
+ },
+
+ _update: function (aData) {
+ if (!aData) {
+ aData = { headerURL: "", footerURL: "", textcolor: "", accentcolor: "" };
+ this._lastData = aData;
+ } else {
+ this._lastData = aData;
+ aData = LightweightThemeImageOptimizer.optimize(aData, this._win.screen);
+ }
+ if (!this._enabled)
+ return;
+
+ let root = this._doc.documentElement;
+ let active = !!aData.headerURL;
+ let stateChanging = (active != this._active);
+
+ // We need to clear these either way: either because the theme is being removed,
+ // or because we are applying a new theme and the data might be bogus CSS,
+ // so if we don't reset first, it'll keep the old value.
+ root.style.removeProperty("color");
+ root.style.removeProperty("background-color");
+ if (active) {
+ root.style.color = aData.textcolor || "black";
+ root.style.backgroundColor = aData.accentcolor || "white";
+ let [r, g, b] = _parseRGB(this._doc.defaultView.getComputedStyle(root, "").color);
+ let luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
+ root.setAttribute("lwthemetextcolor", luminance <= 110 ? "dark" : "bright");
+ root.setAttribute("lwtheme", "true");
+ } else {
+ root.removeAttribute("lwthemetextcolor");
+ root.removeAttribute("lwtheme");
+ }
+
+ this._active = active;
+
+ _setImage(root, active, aData.headerURL);
+ if (this._footerId) {
+ let footer = this._doc.getElementById(this._footerId);
+ footer.style.backgroundColor = active ? aData.accentcolor || "white" : "";
+ _setImage(footer, active, aData.footerURL);
+ if (active && aData.footerURL)
+ footer.setAttribute("lwthemefooter", "true");
+ else
+ footer.removeAttribute("lwthemefooter");
+ }
+
+ Services.obs.notifyObservers(this._win, "lightweight-theme-window-updated",
+ JSON.stringify(aData));
+ }
+}
+
+function _setImage(aElement, aActive, aURL) {
+ aElement.style.backgroundImage =
+ (aActive && aURL) ? 'url("' + aURL.replace(/"/g, '\\"') + '")' : "";
+}
+
+function _parseRGB(aColorString) {
+ var rgb = aColorString.match(/^rgba?\((\d+), (\d+), (\d+)/);
+ rgb.shift();
+ return rgb.map(x => parseInt(x));
+}
diff --git a/components/extensions/src/LightweightThemeImageOptimizer.jsm b/components/extensions/src/LightweightThemeImageOptimizer.jsm
new file mode 100644
index 000000000..a9201c3da
--- /dev/null
+++ b/components/extensions/src/LightweightThemeImageOptimizer.jsm
@@ -0,0 +1,199 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["LightweightThemeImageOptimizer"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+const ORIGIN_TOP_RIGHT = 1;
+const ORIGIN_BOTTOM_LEFT = 2;
+
+this.LightweightThemeImageOptimizer = {
+ optimize: function LWTIO_optimize(aThemeData, aScreen) {
+ let data = Utils.createCopy(aThemeData);
+ if (!data.headerURL) {
+ return data;
+ }
+
+ data.headerURL = ImageCropper.getCroppedImageURL(
+ data.headerURL, aScreen, ORIGIN_TOP_RIGHT);
+
+ if (data.footerURL) {
+ data.footerURL = ImageCropper.getCroppedImageURL(
+ data.footerURL, aScreen, ORIGIN_BOTTOM_LEFT);
+ }
+
+ return data;
+ },
+
+ purge: function LWTIO_purge() {
+ let dir = FileUtils.getDir("ProfD", ["lwtheme"]);
+ dir.followLinks = false;
+ try {
+ dir.remove(true);
+ } catch (e) {}
+ }
+};
+
+Object.freeze(LightweightThemeImageOptimizer);
+
+var ImageCropper = {
+ _inProgress: {},
+
+ getCroppedImageURL:
+ function ImageCropper_getCroppedImageURL(aImageURL, aScreen, aOrigin) {
+ // We can crop local files, only.
+ if (!aImageURL.startsWith("file://")) {
+ return aImageURL;
+ }
+
+ try {
+ if (Services.prefs.getBoolPref("lightweightThemes.animation.enabled")) {
+ //Don't crop if animated
+ return aImageURL;
+ }
+ } catch(e) {
+ // Continue of pref doesn't exist.
+ }
+
+ // Generate the cropped image's file name using its
+ // base name and the current screen size.
+ let uri = Services.io.newURI(aImageURL, null, null);
+ let file = uri.QueryInterface(Ci.nsIFileURL).file;
+
+ // Make sure the source file exists.
+ if (!file.exists()) {
+ return aImageURL;
+ }
+
+ let fileName = file.leafName + "-" + aScreen.width + "x" + aScreen.height;
+ let croppedFile = FileUtils.getFile("ProfD", ["lwtheme", fileName]);
+
+ // If we have a local file that is not in progress, return it.
+ if (croppedFile.exists() && !(croppedFile.path in this._inProgress)) {
+ let fileURI = Services.io.newFileURI(croppedFile);
+
+ // Copy the query part to avoid wrong caching.
+ fileURI.QueryInterface(Ci.nsIURL).query = uri.query;
+ return fileURI.spec;
+ }
+
+ // Crop the given image in the background.
+ this._crop(uri, croppedFile, aScreen, aOrigin);
+
+ // Return the original image while we're waiting for the cropped version
+ // to be written to disk.
+ return aImageURL;
+ },
+
+ _crop: function ImageCropper_crop(aURI, aTargetFile, aScreen, aOrigin) {
+ let inProgress = this._inProgress;
+ inProgress[aTargetFile.path] = true;
+
+ function resetInProgress() {
+ delete inProgress[aTargetFile.path];
+ }
+
+ ImageFile.read(aURI, function crop_readImageFile(aInputStream, aContentType) {
+ if (aInputStream && aContentType) {
+ let image = ImageTools.decode(aInputStream, aContentType);
+ if (image && image.width && image.height) {
+ let stream = ImageTools.encode(image, aScreen, aOrigin, aContentType);
+ if (stream) {
+ ImageFile.write(aTargetFile, stream, resetInProgress);
+ return;
+ }
+ }
+ }
+
+ resetInProgress();
+ });
+ }
+};
+
+var ImageFile = {
+ read: function(aURI, aCallback) {
+ this._netUtil.asyncFetch({
+ uri: aURI,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
+ }, function(aInputStream, aStatus, aRequest) {
+ if (Components.isSuccessCode(aStatus) && aRequest instanceof Ci.nsIChannel) {
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ aCallback(aInputStream, channel.contentType);
+ } else {
+ aCallback();
+ }
+ });
+ },
+
+ write: function ImageFile_write(aFile, aInputStream, aCallback) {
+ let fos = FileUtils.openSafeFileOutputStream(aFile);
+ this._netUtil.asyncCopy(aInputStream, fos, function write_asyncCopy(aResult) {
+ FileUtils.closeSafeFileOutputStream(fos);
+
+ // Remove the file if writing was not successful.
+ if (!Components.isSuccessCode(aResult)) {
+ try {
+ aFile.remove(false);
+ } catch (e) {}
+ }
+
+ aCallback();
+ });
+ }
+};
+
+XPCOMUtils.defineLazyModuleGetter(ImageFile, "_netUtil",
+ "resource://gre/modules/NetUtil.jsm", "NetUtil");
+
+var ImageTools = {
+ decode: function ImageTools_decode(aInputStream, aContentType) {
+ let outParam = {value: null};
+
+ try {
+ this._imgTools.decodeImageData(aInputStream, aContentType, outParam);
+ } catch (e) {}
+
+ return outParam.value;
+ },
+
+ encode: function ImageTools_encode(aImage, aScreen, aOrigin, aContentType) {
+ let stream;
+ let width = Math.min(aImage.width, aScreen.width);
+ let height = Math.min(aImage.height, aScreen.height);
+ let x = aOrigin == ORIGIN_TOP_RIGHT ? aImage.width - width : 0;
+
+ try {
+ stream = this._imgTools.encodeCroppedImage(aImage, aContentType, x, 0,
+ width, height);
+ } catch (e) {}
+
+ return stream;
+ }
+};
+
+XPCOMUtils.defineLazyServiceGetter(ImageTools, "_imgTools",
+ "@mozilla.org/image/tools;1", "imgITools");
+
+var Utils = {
+ createCopy: function Utils_createCopy(aData) {
+ let copy = {};
+ for (let [k, v] in Iterator(aData)) {
+ copy[k] = v;
+ }
+ return copy;
+ }
+};
diff --git a/components/extensions/src/LightweightThemeManager.jsm b/components/extensions/src/LightweightThemeManager.jsm
new file mode 100644
index 000000000..a4cbf3833
--- /dev/null
+++ b/components/extensions/src/LightweightThemeManager.jsm
@@ -0,0 +1,801 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["LightweightThemeManager"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const ID_SUFFIX = "@personas.mozilla.org";
+const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect";
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
+const ADDON_TYPE = "theme";
+
+const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
+
+const STRING_TYPE_NAME = "type.%ID%.name";
+
+const DEFAULT_MAX_USED_THEMES_COUNT = 30;
+
+const MAX_PREVIEW_SECONDS = 30;
+
+const MANDATORY = ["id", "name", "headerURL"];
+const OPTIONAL = ["footerURL", "textcolor", "accentcolor", "iconURL",
+ "previewURL", "author", "description", "homepageURL",
+ "updateURL", "version"];
+
+const PERSIST_ENABLED = true;
+const PERSIST_BYPASS_CACHE = false;
+const PERSIST_FILES = {
+ headerURL: "lightweighttheme-header",
+ footerURL: "lightweighttheme-footer"
+};
+
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeImageOptimizer",
+ "resource://gre/modules/addons/LightweightThemeImageOptimizer.jsm");
+
+this.__defineGetter__("_prefs", function prefsGetter() {
+ delete this._prefs;
+ return this._prefs = Services.prefs.getBranch("lightweightThemes.");
+});
+
+this.__defineGetter__("_maxUsedThemes", function maxUsedThemesGetter() {
+ delete this._maxUsedThemes;
+ this._maxUsedThemes = _prefs.getIntPref("maxUsedThemes", DEFAULT_MAX_USED_THEMES_COUNT);
+ return this._maxUsedThemes;
+});
+
+this.__defineSetter__("_maxUsedThemes", function maxUsedThemesSetter(aVal) {
+ delete this._maxUsedThemes;
+ return this._maxUsedThemes = aVal;
+});
+
+// Holds the ID of the theme being enabled or disabled while sending out the
+// events so cached AddonWrapper instances can return correct values for
+// permissions and pendingOperations
+var _themeIDBeingEnabled = null;
+var _themeIDBeingDisabled = null;
+
+this.LightweightThemeManager = {
+ get name() "LightweightThemeManager",
+
+ get usedThemes () {
+ try {
+ return JSON.parse(_prefs.getComplexValue("usedThemes",
+ Ci.nsISupportsString).data);
+ } catch (e) {
+ return [];
+ }
+ },
+
+ get currentTheme () {
+ try {
+ if (_prefs.getBoolPref("isThemeSelected"))
+ var data = this.usedThemes[0];
+ } catch (e) {}
+
+ return data || null;
+ },
+
+ get currentThemeForDisplay () {
+ var data = this.currentTheme;
+
+ if (data && PERSIST_ENABLED) {
+ for (let key in PERSIST_FILES) {
+ try {
+ if (data[key] && _prefs.getBoolPref("persisted." + key))
+ data[key] = _getLocalImageURI(PERSIST_FILES[key]).spec
+ + "?" + data.id + ";" + _version(data);
+ } catch (e) {}
+ }
+ }
+
+ return data;
+ },
+
+ set currentTheme (aData) {
+ return _setCurrentTheme(aData, false);
+ },
+
+ setLocalTheme: function(aData) {
+ _setCurrentTheme(aData, true);
+ },
+
+ getUsedTheme: function(aId) {
+ var usedThemes = this.usedThemes;
+ for (let usedTheme of usedThemes) {
+ if (usedTheme.id == aId)
+ return usedTheme;
+ }
+ return null;
+ },
+
+ forgetUsedTheme: function(aId) {
+ let theme = this.getUsedTheme(aId);
+ if (!theme)
+ return;
+
+ let wrapper = new AddonWrapper(theme);
+ AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false);
+
+ var currentTheme = this.currentTheme;
+ if (currentTheme && currentTheme.id == aId) {
+ this.themeChanged(null);
+ AddonManagerPrivate.notifyAddonChanged(null, ADDON_TYPE, false);
+ }
+
+ _updateUsedThemes(_usedThemesExceptId(aId));
+ AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
+ },
+
+ previewTheme: function(aData) {
+ let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ cancel.data = false;
+ Services.obs.notifyObservers(cancel, "lightweight-theme-preview-requested",
+ JSON.stringify(aData));
+ if (cancel.data)
+ return;
+
+ if (_previewTimer)
+ _previewTimer.cancel();
+ else
+ _previewTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ _previewTimer.initWithCallback(_previewTimerCallback,
+ MAX_PREVIEW_SECONDS * 1000,
+ _previewTimer.TYPE_ONE_SHOT);
+
+ _notifyWindows(aData);
+ },
+
+ resetPreview: function() {
+ if (_previewTimer) {
+ _previewTimer.cancel();
+ _previewTimer = null;
+ _notifyWindows(this.currentThemeForDisplay);
+ }
+ },
+
+ parseTheme: function(aString, aBaseURI) {
+ try {
+ return _sanitizeTheme(JSON.parse(aString), aBaseURI, false);
+ } catch (e) {
+ return null;
+ }
+ },
+
+ updateCurrentTheme: function() {
+ try {
+ if (!_prefs.getBoolPref("update.enabled"))
+ return;
+ } catch (e) {
+ return;
+ }
+
+ var theme = this.currentTheme;
+ if (!theme || !theme.updateURL)
+ return;
+
+ var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+
+ req.mozBackgroundRequest = true;
+ req.overrideMimeType("text/plain");
+ req.open("GET", theme.updateURL, true);
+ // Prevent the request from reading from the cache.
+ req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to the cache.
+ req.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+
+ var self = this;
+ req.addEventListener("load", function loadEventListener() {
+ if (req.status != 200)
+ return;
+
+ let newData = self.parseTheme(req.responseText, theme.updateURL);
+ if (!newData ||
+ newData.id != theme.id ||
+ _version(newData) == _version(theme))
+ return;
+
+ var currentTheme = self.currentTheme;
+ if (currentTheme && currentTheme.id == theme.id)
+ self.currentTheme = newData;
+ }, false);
+
+ req.send(null);
+ },
+
+ /**
+ * Switches to a new lightweight theme.
+ *
+ * @param aData
+ * The lightweight theme to switch to
+ */
+ themeChanged: function(aData) {
+ if (_previewTimer) {
+ _previewTimer.cancel();
+ _previewTimer = null;
+ }
+
+ if (aData) {
+ let usedThemes = _usedThemesExceptId(aData.id);
+ usedThemes.unshift(aData);
+ _updateUsedThemes(usedThemes);
+ if (PERSIST_ENABLED) {
+ LightweightThemeImageOptimizer.purge();
+ _persistImages(aData, function themeChanged_persistImages() {
+ _notifyWindows(this.currentThemeForDisplay);
+ }.bind(this));
+ }
+ }
+
+ _prefs.setBoolPref("isThemeSelected", aData != null);
+ _notifyWindows(aData);
+ Services.obs.notifyObservers(null, "lightweight-theme-changed", null);
+ },
+
+ /**
+ * Starts the Addons provider and enables the new lightweight theme if
+ * necessary.
+ */
+ startup: function() {
+ if (Services.prefs.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) {
+ let id = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
+ if (id)
+ this.themeChanged(this.getUsedTheme(id));
+ else
+ this.themeChanged(null);
+ Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT);
+ }
+
+ _prefs.addObserver("", _prefObserver, false);
+ },
+
+ /**
+ * Shuts down the provider.
+ */
+ shutdown: function() {
+ _prefs.removeObserver("", _prefObserver);
+ },
+
+ /**
+ * Called when a new add-on has been enabled when only one add-on of that type
+ * can be enabled.
+ *
+ * @param aId
+ * The ID of the newly enabled add-on
+ * @param aType
+ * The type of the newly enabled add-on
+ * @param aPendingRestart
+ * true if the newly enabled add-on will only become enabled after a
+ * restart
+ */
+ addonChanged: function(aId, aType, aPendingRestart) {
+ if (aType != ADDON_TYPE)
+ return;
+
+ let id = _getInternalID(aId);
+ let current = this.currentTheme;
+
+ try {
+ let next = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
+ if (id == next && aPendingRestart)
+ return;
+
+ Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT);
+ if (next) {
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled",
+ new AddonWrapper(this.getUsedTheme(next)));
+ }
+ else {
+ if (id == current.id) {
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled",
+ new AddonWrapper(current));
+ return;
+ }
+ }
+ }
+ catch (e) {
+ }
+
+ if (current) {
+ if (current.id == id)
+ return;
+ _themeIDBeingDisabled = current.id;
+ let wrapper = new AddonWrapper(current);
+ if (aPendingRestart) {
+ Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, "");
+ AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, true);
+ }
+ else {
+ AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false);
+ this.themeChanged(null);
+ AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
+ }
+ _themeIDBeingDisabled = null;
+ }
+
+ if (id) {
+ let theme = this.getUsedTheme(id);
+ _themeIDBeingEnabled = id;
+ let wrapper = new AddonWrapper(theme);
+ if (aPendingRestart) {
+ AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, true);
+ Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, id);
+
+ // Flush the preferences to disk so they survive any crash
+ Services.prefs.savePrefFile(null);
+ }
+ else {
+ AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false);
+ this.themeChanged(theme);
+ AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
+ }
+ _themeIDBeingEnabled = null;
+ }
+ },
+
+ /**
+ * Called to get an Addon with a particular ID.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ * @param aCallback
+ * A callback to pass the Addon to
+ */
+ getAddonByID: function(aId, aCallback) {
+ let id = _getInternalID(aId);
+ if (!id) {
+ aCallback(null);
+ return;
+ }
+
+ let theme = this.getUsedTheme(id);
+ if (!theme) {
+ aCallback(null);
+ return;
+ }
+
+ aCallback(new AddonWrapper(theme));
+ },
+
+ /**
+ * Called to get Addons of a particular type.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types.
+ * @param aCallback
+ * A callback to pass an array of Addons to
+ */
+ getAddonsByTypes: function(aTypes, aCallback) {
+ if (aTypes && aTypes.indexOf(ADDON_TYPE) == -1) {
+ aCallback([]);
+ return;
+ }
+
+ // Tycho: aCallback([new AddonWrapper(a) for each (a in this.usedThemes)]);
+ let result = [];
+ for each(let a in this.usedThemes) {
+ result.push(new AddonWrapper(a));
+ }
+
+ aCallback(result);
+ },
+};
+
+/**
+ * The AddonWrapper wraps lightweight theme to provide the data visible to
+ * consumers of the AddonManager API.
+ */
+function AddonWrapper(aTheme) {
+ this.__defineGetter__("id", function AddonWrapper_idGetter() aTheme.id + ID_SUFFIX);
+ this.__defineGetter__("type", function AddonWrapper_typeGetter() ADDON_TYPE);
+ this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() {
+ let current = LightweightThemeManager.currentTheme;
+ if (current)
+ return aTheme.id == current.id;
+ return false;
+ });
+
+ this.__defineGetter__("name", function AddonWrapper_nameGetter() aTheme.name);
+ this.__defineGetter__("version", function AddonWrapper_versionGetter() {
+ return "version" in aTheme ? aTheme.version : "";
+ });
+
+ ["description", "homepageURL", "iconURL"].forEach(function(prop) {
+ this.__defineGetter__(prop, function AddonWrapper_optionalPropGetter() {
+ return prop in aTheme ? aTheme[prop] : null;
+ });
+ }, this);
+
+ ["installDate", "updateDate"].forEach(function(prop) {
+ this.__defineGetter__(prop, function AddonWrapper_datePropGetter() {
+ return prop in aTheme ? new Date(aTheme[prop]) : null;
+ });
+ }, this);
+
+ this.__defineGetter__("creator", function AddonWrapper_creatorGetter() {
+ return new AddonManagerPrivate.AddonAuthor(aTheme.author);
+ });
+
+ this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() {
+ let url = aTheme.previewURL;
+ return [new AddonManagerPrivate.AddonScreenshot(url)];
+ });
+
+ this.__defineGetter__("pendingOperations",
+ function AddonWrapper_pendingOperationsGetter() {
+ let pending = AddonManager.PENDING_NONE;
+ if (this.isActive == this.userDisabled)
+ pending |= this.isActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE;
+ return pending;
+ });
+
+ this.__defineGetter__("operationsRequiringRestart",
+ function AddonWrapper_operationsRequiringRestartGetter() {
+ // If a non-default theme is in use then a restart will be required to
+ // enable lightweight themes unless dynamic theme switching is enabled
+ if (Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN)) {
+ try {
+ if (Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED))
+ return AddonManager.OP_NEEDS_RESTART_NONE;
+ }
+ catch (e) {
+ }
+ return AddonManager.OP_NEEDS_RESTART_ENABLE;
+ }
+
+ return AddonManager.OP_NEEDS_RESTART_NONE;
+ });
+
+ this.__defineGetter__("size", function AddonWrapper_sizeGetter() {
+ // The size changes depending on whether the theme is in use or not, this is
+ // probably not worth exposing.
+ return null;
+ });
+
+ this.__defineGetter__("permissions", function AddonWrapper_permissionsGetter() {
+ let permissions = AddonManager.PERM_CAN_UNINSTALL;
+ if (this.userDisabled)
+ permissions |= AddonManager.PERM_CAN_ENABLE;
+ else
+ permissions |= AddonManager.PERM_CAN_DISABLE;
+ return permissions;
+ });
+
+ this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() {
+ if (_themeIDBeingEnabled == aTheme.id)
+ return false;
+ if (_themeIDBeingDisabled == aTheme.id)
+ return true;
+
+ try {
+ let toSelect = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
+ return aTheme.id != toSelect;
+ }
+ catch (e) {
+ let current = LightweightThemeManager.currentTheme;
+ return !current || current.id != aTheme.id;
+ }
+ });
+
+ this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) {
+ if (val == this.userDisabled)
+ return val;
+
+ if (val)
+ LightweightThemeManager.currentTheme = null;
+ else
+ LightweightThemeManager.currentTheme = aTheme;
+
+ return val;
+ });
+
+ this.uninstall = function AddonWrapper_uninstall() {
+ LightweightThemeManager.forgetUsedTheme(aTheme.id);
+ };
+
+ this.cancelUninstall = function AddonWrapper_cancelUninstall() {
+ throw new Error("Theme is not marked to be uninstalled");
+ };
+
+ this.findUpdates = function AddonWrapper_findUpdates(listener, reason, appVersion, platformVersion) {
+ AddonManagerPrivate.callNoUpdateListeners(this, listener, reason, appVersion, platformVersion);
+ };
+}
+
+AddonWrapper.prototype = {
+ // Lightweight themes are never disabled by the application
+ get appDisabled() {
+ return false;
+ },
+
+ // Lightweight themes are always compatible
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get scope() {
+ return AddonManager.SCOPE_PROFILE;
+ },
+
+ get foreignInstall() {
+ return false;
+ },
+
+ // Lightweight themes are always compatible
+ isCompatibleWith: function(appVersion, platformVersion) {
+ return true;
+ },
+
+ // Lightweight themes are always securely updated
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ // Lightweight themes are never blocklisted
+ get blocklistState() {
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ }
+};
+
+/**
+ * Converts the ID used by the public AddonManager API to an lightweight theme
+ * ID.
+ *
+ * @param id
+ * The ID to be converted
+ *
+ * @return the lightweight theme ID or null if the ID was not for a lightweight
+ * theme.
+ */
+function _getInternalID(id) {
+ if (!id)
+ return null;
+ let len = id.length - ID_SUFFIX.length;
+ if (len > 0 && id.substring(len) == ID_SUFFIX)
+ return id.substring(0, len);
+ return null;
+}
+
+function _setCurrentTheme(aData, aLocal) {
+ aData = _sanitizeTheme(aData, null, aLocal);
+
+ let needsRestart = (ADDON_TYPE == "theme") &&
+ Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN);
+
+ let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ cancel.data = false;
+ Services.obs.notifyObservers(cancel, "lightweight-theme-change-requested",
+ JSON.stringify(aData));
+
+ if (aData) {
+ let theme = LightweightThemeManager.getUsedTheme(aData.id);
+ let isInstall = !theme || theme.version != aData.version;
+ if (isInstall) {
+ aData.updateDate = Date.now();
+ if (theme && "installDate" in theme)
+ aData.installDate = theme.installDate;
+ else
+ aData.installDate = aData.updateDate;
+
+ var oldWrapper = theme ? new AddonWrapper(theme) : null;
+ var wrapper = new AddonWrapper(aData);
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null,
+ wrapper, oldWrapper, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false);
+ }
+
+ let current = LightweightThemeManager.currentTheme;
+ let usedThemes = _usedThemesExceptId(aData.id);
+ if (current && current.id != aData.id)
+ usedThemes.splice(1, 0, aData);
+ else
+ usedThemes.unshift(aData);
+ _updateUsedThemes(usedThemes);
+
+ if (isInstall)
+ AddonManagerPrivate.callAddonListeners("onInstalled", wrapper);
+ }
+
+ if (cancel.data)
+ return null;
+
+ AddonManagerPrivate.notifyAddonChanged(aData ? aData.id + ID_SUFFIX : null,
+ ADDON_TYPE, needsRestart);
+
+ return LightweightThemeManager.currentTheme;
+}
+
+function _sanitizeTheme(aData, aBaseURI, aLocal) {
+ if (!aData || typeof aData != "object")
+ return null;
+
+ var resourceProtocols = ["http", "https", "resource"];
+ if (aLocal)
+ resourceProtocols.push("file");
+ var resourceProtocolExp = new RegExp("^(" + resourceProtocols.join("|") + "):");
+
+ function sanitizeProperty(prop) {
+ if (!(prop in aData))
+ return null;
+ if (typeof aData[prop] != "string")
+ return null;
+ let val = aData[prop].trim();
+ if (!val)
+ return null;
+
+ if (!/URL$/.test(prop))
+ return val;
+
+ try {
+ val = _makeURI(val, aBaseURI ? _makeURI(aBaseURI) : null).spec;
+ if ((prop == "updateURL" ? /^https:/ : resourceProtocolExp).test(val))
+ return val;
+ return null;
+ }
+ catch (e) {
+ return null;
+ }
+ }
+
+ let result = {};
+ for (let mandatoryProperty of MANDATORY) {
+ let val = sanitizeProperty(mandatoryProperty);
+ if (!val)
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ result[mandatoryProperty] = val;
+ }
+
+ for (let optionalProperty of OPTIONAL) {
+ let val = sanitizeProperty(optionalProperty);
+ if (!val)
+ continue;
+ result[optionalProperty] = val;
+ }
+
+ return result;
+}
+
+function _usedThemesExceptId(aId)
+ LightweightThemeManager.usedThemes.filter(
+ function usedThemesExceptId_filterID(t) "id" in t && t.id != aId);
+
+function _version(aThemeData)
+ aThemeData.version || "";
+
+function _makeURI(aURL, aBaseURI)
+ Services.io.newURI(aURL, null, aBaseURI);
+
+function _updateUsedThemes(aList) {
+ // Send uninstall events for all themes that need to be removed.
+ while (aList.length > _maxUsedThemes) {
+ let wrapper = new AddonWrapper(aList[aList.length - 1]);
+ AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false);
+ aList.pop();
+ AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
+ }
+
+ var str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = JSON.stringify(aList);
+ _prefs.setComplexValue("usedThemes", Ci.nsISupportsString, str);
+
+ Services.obs.notifyObservers(null, "lightweight-theme-list-changed", null);
+}
+
+function _notifyWindows(aThemeData) {
+ Services.obs.notifyObservers(null, "lightweight-theme-styling-update",
+ JSON.stringify(aThemeData));
+}
+
+var _previewTimer;
+var _previewTimerCallback = {
+ notify: function() {
+ LightweightThemeManager.resetPreview();
+ }
+};
+
+/**
+ * Called when any of the lightweightThemes preferences are changed.
+ */
+function _prefObserver(aSubject, aTopic, aData) {
+ switch (aData) {
+ case "maxUsedThemes":
+ _maxUsedThemes = _prefs.getIntPref(aData, DEFAULT_MAX_USED_THEMES_COUNT);
+
+ // Update the theme list to remove any themes over the number we keep
+ _updateUsedThemes(LightweightThemeManager.usedThemes);
+ break;
+ }
+}
+
+function _persistImages(aData, aCallback) {
+ function onSuccess(key) function () {
+ let current = LightweightThemeManager.currentTheme;
+ if (current && current.id == aData.id) {
+ _prefs.setBoolPref("persisted." + key, true);
+ }
+ if (--numFilesToPersist == 0 && aCallback) {
+ aCallback();
+ }
+ };
+
+ let numFilesToPersist = 0;
+ for (let key in PERSIST_FILES) {
+ _prefs.setBoolPref("persisted." + key, false);
+ if (aData[key]) {
+ numFilesToPersist++;
+ _persistImage(aData[key], PERSIST_FILES[key], onSuccess(key));
+ }
+ }
+}
+
+function _getLocalImageURI(localFileName) {
+ var localFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ localFile.append(localFileName);
+ return Services.io.newFileURI(localFile);
+}
+
+function _persistImage(sourceURL, localFileName, successCallback) {
+ if (/^(file|resource):/.test(sourceURL))
+ return;
+
+ var targetURI = _getLocalImageURI(localFileName);
+ var sourceURI = _makeURI(sourceURL);
+
+ var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+
+ persist.persistFlags =
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION |
+ (PERSIST_BYPASS_CACHE ?
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE :
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FROM_CACHE);
+
+ persist.progressListener = new _persistProgressListener(successCallback);
+
+ persist.saveURI(sourceURI, null,
+ null, Ci.nsIHttpChannel.REFERRER_POLICY_NO_REFERRER_WHEN_DOWNGRADE,
+ null, null, targetURI, null);
+}
+
+function _persistProgressListener(successCallback) {
+ this.onLocationChange = function persistProgressListener_onLocationChange() {};
+ this.onProgressChange = function persistProgressListener_onProgressChange() {};
+ this.onStatusChange = function persistProgressListener_onStatusChange() {};
+ this.onSecurityChange = function persistProgressListener_onSecurityChange() {};
+ this.onStateChange = function persistProgressListener_onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aRequest &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ try {
+ if (aRequest.QueryInterface(Ci.nsIHttpChannel).requestSucceeded) {
+ // success
+ successCallback();
+ return;
+ }
+ } catch (e) { }
+ // failure
+ }
+ };
+}
+
+AddonManagerPrivate.registerProvider(LightweightThemeManager, [
+ new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 5000)
+]);
diff --git a/components/extensions/src/PluginProvider.jsm b/components/extensions/src/PluginProvider.jsm
new file mode 100644
index 000000000..cb07dcb12
--- /dev/null
+++ b/components/extensions/src/PluginProvider.jsm
@@ -0,0 +1,595 @@
+/* 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [];
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
+const STRING_TYPE_NAME = "type.%ID%.name";
+const LIST_UPDATED_TOPIC = "plugins-list-updated";
+const FLASH_MIME_TYPE = "application/x-shockwave-flash";
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.plugins";
+
+// Create a new logger for use by the Addons Plugin Provider
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+function getIDHashForString(aStr) {
+ // return the two-digit hexadecimal code for a byte
+ function toHexString(charCode)
+ ("0" + charCode.toString(16)).slice(-2);
+
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.init(Ci.nsICryptoHash.MD5);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+ stringStream.data = aStr ? aStr : "null";
+ hasher.updateFromStream(stringStream, -1);
+
+ // convert the binary hash data to a hex string.
+ let binary = hasher.finish(false);
+
+ // Tycho: let hash = [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase();
+ let hash = [];
+
+ for (let i in binary) {
+ hash.push(toHexString(binary.charCodeAt(i)));
+ }
+
+ hash = hash.join("").toLowerCase();
+
+ return "{" + hash.substr(0, 8) + "-" +
+ hash.substr(8, 4) + "-" +
+ hash.substr(12, 4) + "-" +
+ hash.substr(16, 4) + "-" +
+ hash.substr(20) + "}";
+}
+
+var PluginProvider = {
+ get name() "PluginProvider",
+
+ // A dictionary mapping IDs to names and descriptions
+ plugins: null,
+
+ startup: function PL_startup() {
+ Services.obs.addObserver(this, LIST_UPDATED_TOPIC, false);
+ Services.obs.addObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, false);
+ },
+
+ /**
+ * Called when the application is shutting down. Only necessary for tests
+ * to be able to simulate a shutdown.
+ */
+ shutdown: function PL_shutdown() {
+ this.plugins = null;
+ Services.obs.removeObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED);
+ Services.obs.removeObserver(this, LIST_UPDATED_TOPIC);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case AddonManager.OPTIONS_NOTIFICATION_DISPLAYED:
+ this.getAddonByID(aData, function PL_displayPluginInfo(plugin) {
+ if (!plugin)
+ return;
+
+ let libLabel = aSubject.getElementById("pluginLibraries");
+ libLabel.textContent = plugin.pluginLibraries.join(", ");
+
+ let typeLabel = aSubject.getElementById("pluginMimeTypes"), types = [];
+ for (let type of plugin.pluginMimeTypes) {
+ let extras = [type.description.trim(), type.suffixes].
+ filter(function(x) x).join(": ");
+ types.push(type.type + (extras ? " (" + extras + ")" : ""));
+ }
+ typeLabel.textContent = types.join(",\n");
+ let showProtectedModePref = canDisableFlashProtectedMode(plugin);
+ aSubject.getElementById("pluginEnableProtectedMode")
+ .setAttribute("collapsed", showProtectedModePref ? "" : "true");
+ });
+ break;
+ case LIST_UPDATED_TOPIC:
+ if (this.plugins)
+ this.updatePluginList();
+ break;
+ }
+ },
+
+ /**
+ * Creates a PluginWrapper for a plugin object.
+ */
+ buildWrapper: function PL_buildWrapper(aPlugin) {
+ return new PluginWrapper(aPlugin.id,
+ aPlugin.name,
+ aPlugin.description,
+ aPlugin.tags);
+ },
+
+ /**
+ * Called to get an Addon with a particular ID.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ * @param aCallback
+ * A callback to pass the Addon to
+ */
+ getAddonByID: function PL_getAddon(aId, aCallback) {
+ if (!this.plugins)
+ this.buildPluginList();
+
+ if (aId in this.plugins)
+ aCallback(this.buildWrapper(this.plugins[aId]));
+ else
+ aCallback(null);
+ },
+
+ /**
+ * Called to get Addons of a particular type.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types.
+ * @param callback
+ * A callback to pass an array of Addons to
+ */
+ getAddonsByTypes: function PL_getAddonsByTypes(aTypes, aCallback) {
+ if (aTypes && aTypes.indexOf("plugin") < 0) {
+ aCallback([]);
+ return;
+ }
+
+ if (!this.plugins)
+ this.buildPluginList();
+
+ let results = [];
+
+ for (let id in this.plugins) {
+ this.getAddonByID(id, function(aAddon) {
+ results.push(aAddon);
+ });
+ }
+
+ aCallback(results);
+ },
+
+ /**
+ * Called to get Addons that have pending operations.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types
+ * @param aCallback
+ * A callback to pass an array of Addons to
+ */
+ getAddonsWithOperationsByTypes: function PL_getAddonsWithOperationsByTypes(aTypes, aCallback) {
+ aCallback([]);
+ },
+
+ /**
+ * Called to get the current AddonInstalls, optionally restricting by type.
+ *
+ * @param aTypes
+ * An array of types or null to get all types
+ * @param aCallback
+ * A callback to pass the array of AddonInstalls to
+ */
+ getInstallsByTypes: function PL_getInstallsByTypes(aTypes, aCallback) {
+ aCallback([]);
+ },
+
+ /**
+ * Builds a list of the current plugins reported by the plugin host
+ *
+ * @return a dictionary of plugins indexed by our generated ID
+ */
+ getPluginList: function PL_getPluginList() {
+ let tags = Cc["@mozilla.org/plugin/host;1"].
+ getService(Ci.nsIPluginHost).
+ getPluginTags({});
+
+ let list = {};
+ let seenPlugins = {};
+ for (let tag of tags) {
+ if (!(tag.name in seenPlugins))
+ seenPlugins[tag.name] = {};
+ if (!(tag.description in seenPlugins[tag.name])) {
+ let plugin = {
+ id: getIDHashForString(tag.name + tag.description),
+ // XXX Flash name substitution like in browser-plugins.js, aboutPermissions.js, permissions.js
+ name: tag.name == "Shockwave Flash" ? "Adobe Flash" : tag.name,
+ description: tag.description,
+ tags: [tag]
+ };
+
+ seenPlugins[tag.name][tag.description] = plugin;
+ list[plugin.id] = plugin;
+ }
+ else {
+ seenPlugins[tag.name][tag.description].tags.push(tag);
+ }
+ }
+
+ return list;
+ },
+
+ /**
+ * Builds the list of known plugins from the plugin host
+ */
+ buildPluginList: function PL_buildPluginList() {
+ this.plugins = this.getPluginList();
+ },
+
+ /**
+ * Updates the plugins from the plugin host by comparing the current plugins
+ * to the last known list sending out any necessary API notifications for
+ * changes.
+ */
+ updatePluginList: function PL_updatePluginList() {
+ let newList = this.getPluginList();
+
+ // Tycho:
+ // let lostPlugins = [this.buildWrapper(this.plugins[id])
+ // for each (id in Object.keys(this.plugins)) if (!(id in newList))];
+
+ // let newPlugins = [this.buildWrapper(newList[id])
+ // for each (id in Object.keys(newList)) if (!(id in this.plugins))];
+
+ // let matchedIDs = [id for each (id in Object.keys(newList)) if (id in this.plugins)];
+
+ let lostPlugins = [];
+ let newPlugins = [];
+ let matchedIDs = [];
+
+ // lostPlugins
+ for each(let id in Object.keys(this.plugins)) {
+ if (!(id in newList)) {
+ lostPlugins.push(this.buildWrapper(this.plugins[id]));
+ }
+ }
+
+ // newPlugins and matchedIDs
+ for each(let id in Object.keys(newList)) {
+ if (!(id in this.plugins)) {
+ newPlugins.push(this.buildWrapper(newList[id]));
+ }
+
+ if (id in this.plugins) {
+ matchedIDs.push(id);
+ }
+ }
+
+
+ // The plugin host generates new tags for every plugin after a scan and
+ // if the plugin's filename has changed then the disabled state won't have
+ // been carried across, send out notifications for anything that has
+ // changed (see bug 830267).
+ let changedWrappers = [];
+ for (let id of matchedIDs) {
+ let oldWrapper = this.buildWrapper(this.plugins[id]);
+ let newWrapper = this.buildWrapper(newList[id]);
+
+ if (newWrapper.isActive != oldWrapper.isActive) {
+ AddonManagerPrivate.callAddonListeners(newWrapper.isActive ?
+ "onEnabling" : "onDisabling",
+ newWrapper, false);
+ changedWrappers.push(newWrapper);
+ }
+ }
+
+ // Notify about new installs
+ for (let plugin of newPlugins) {
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null,
+ plugin, null, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", plugin, false);
+ }
+
+ // Notify for any plugins that have vanished.
+ for (let plugin of lostPlugins)
+ AddonManagerPrivate.callAddonListeners("onUninstalling", plugin, false);
+
+ this.plugins = newList;
+
+ // Signal that new installs are complete
+ for (let plugin of newPlugins)
+ AddonManagerPrivate.callAddonListeners("onInstalled", plugin);
+
+ // Signal that enables/disables are complete
+ for (let wrapper of changedWrappers) {
+ AddonManagerPrivate.callAddonListeners(wrapper.isActive ?
+ "onEnabled" : "onDisabled",
+ wrapper);
+ }
+
+ // Signal that uninstalls are complete
+ for (let plugin of lostPlugins)
+ AddonManagerPrivate.callAddonListeners("onUninstalled", plugin);
+ }
+};
+
+function isFlashPlugin(aPlugin) {
+ for (let type of aPlugin.pluginMimeTypes) {
+ if (type.type == FLASH_MIME_TYPE) {
+ return true;
+ }
+ }
+ return false;
+}
+// Protected mode is win32-only, not win64
+function canDisableFlashProtectedMode(aPlugin) {
+ return isFlashPlugin(aPlugin) && Services.appinfo.XPCOMABI == "x86-msvc";
+}
+
+/**
+ * The PluginWrapper wraps a set of nsIPluginTags to provide the data visible to
+ * public callers through the API.
+ */
+function PluginWrapper(aId, aName, aDescription, aTags) {
+ let safedesc = aDescription.replace(/<\/?[a-z][^>]*>/gi, " ");
+ let homepageURL = null;
+ if (/<A\s+HREF=[^>]*>/i.test(aDescription))
+ homepageURL = /<A\s+HREF=["']?([^>"'\s]*)/i.exec(aDescription)[1];
+
+ this.__defineGetter__("id", function() aId);
+ this.__defineGetter__("type", function() "plugin");
+ this.__defineGetter__("name", function() aName);
+ this.__defineGetter__("creator", function() null);
+ this.__defineGetter__("description", function() safedesc);
+ this.__defineGetter__("version", function() aTags[0].version);
+ this.__defineGetter__("homepageURL", function() homepageURL);
+
+ this.__defineGetter__("isActive", function() !aTags[0].blocklisted && !aTags[0].disabled);
+ this.__defineGetter__("appDisabled", function() aTags[0].blocklisted);
+
+ this.__defineGetter__("userDisabled", function() {
+ if (aTags[0].disabled)
+ return true;
+
+ if ((Services.prefs.getBoolPref("plugins.click_to_play") && aTags[0].clicktoplay) ||
+ this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE ||
+ this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE)
+ return AddonManager.STATE_ASK_TO_ACTIVATE;
+
+ return false;
+ });
+
+ this.__defineSetter__("userDisabled", function(aVal) {
+ let previousVal = this.userDisabled;
+ if (aVal === previousVal)
+ return aVal;
+
+ for (let tag of aTags) {
+ if (aVal === true)
+ tag.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+ else if (aVal === false)
+ tag.enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+ else if (aVal == AddonManager.STATE_ASK_TO_ACTIVATE)
+ tag.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+ }
+
+ // If 'userDisabled' was 'true' and we're going to a state that's not
+ // that, we're enabling, so call those listeners.
+ if (previousVal === true && aVal !== true) {
+ AddonManagerPrivate.callAddonListeners("onEnabling", this, false);
+ AddonManagerPrivate.callAddonListeners("onEnabled", this);
+ }
+
+ // If 'userDisabled' was not 'true' and we're going to a state where
+ // it is, we're disabling, so call those listeners.
+ if (previousVal !== true && aVal === true) {
+ AddonManagerPrivate.callAddonListeners("onDisabling", this, false);
+ AddonManagerPrivate.callAddonListeners("onDisabled", this);
+ }
+
+ // If the 'userDisabled' value involved AddonManager.STATE_ASK_TO_ACTIVATE,
+ // call the onPropertyChanged listeners.
+ if (previousVal == AddonManager.STATE_ASK_TO_ACTIVATE ||
+ aVal == AddonManager.STATE_ASK_TO_ACTIVATE) {
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["userDisabled"]);
+ }
+
+ return aVal;
+ });
+
+
+ this.__defineGetter__("blocklistState", function() {
+ let bs = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsIBlocklistService);
+ return bs.getPluginBlocklistState(aTags[0]);
+ });
+
+ this.__defineGetter__("blocklistURL", function() {
+ let bs = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsIBlocklistService);
+ return bs.getPluginBlocklistURL(aTags[0]);
+ });
+
+ this.__defineGetter__("size", function() {
+ function getDirectorySize(aFile) {
+ let size = 0;
+ let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entry;
+ while ((entry = entries.nextFile)) {
+ if (entry.isSymlink() || !entry.isDirectory())
+ size += entry.fileSize;
+ else
+ size += getDirectorySize(entry);
+ }
+ entries.close();
+ return size;
+ }
+
+ let size = 0;
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ for (let tag of aTags) {
+ file.initWithPath(tag.fullpath);
+ if (file.isDirectory())
+ size += getDirectorySize(file);
+ else
+ size += file.fileSize;
+ }
+ return size;
+ });
+
+ this.__defineGetter__("pluginLibraries", function() {
+ let libs = [];
+ for (let tag of aTags)
+ libs.push(tag.filename);
+ return libs;
+ });
+
+ this.__defineGetter__("pluginFullpath", function() {
+ let paths = [];
+ for (let tag of aTags)
+ paths.push(tag.fullpath);
+ return paths;
+ })
+
+ this.__defineGetter__("pluginMimeTypes", function() {
+ let types = [];
+ for (let tag of aTags) {
+ let mimeTypes = tag.getMimeTypes({});
+ let mimeDescriptions = tag.getMimeDescriptions({});
+ let extensions = tag.getExtensions({});
+ for (let i = 0; i < mimeTypes.length; i++) {
+ let type = {};
+ type.type = mimeTypes[i];
+ type.description = mimeDescriptions[i];
+ type.suffixes = extensions[i];
+
+ types.push(type);
+ }
+ }
+ return types;
+ });
+
+ this.__defineGetter__("installDate", function() {
+ let date = 0;
+ for (let tag of aTags) {
+ date = Math.max(date, tag.lastModifiedTime);
+ }
+ return new Date(date);
+ });
+
+ this.__defineGetter__("scope", function() {
+ let path = aTags[0].fullpath;
+ // Plugins inside the application directory are in the application scope
+ let dir = Services.dirsvc.get("APlugns", Ci.nsIFile);
+ if (path.startsWith(dir.path))
+ return AddonManager.SCOPE_APPLICATION;
+
+ // Plugins inside the profile directory are in the profile scope
+ dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ if (path.startsWith(dir.path))
+ return AddonManager.SCOPE_PROFILE;
+
+ // Plugins anywhere else in the user's home are in the user scope,
+ // but not all platforms have a home directory.
+ try {
+ dir = Services.dirsvc.get("Home", Ci.nsIFile);
+ if (path.startsWith(dir.path))
+ return AddonManager.SCOPE_USER;
+ } catch (e if (e.result && e.result == Components.results.NS_ERROR_FAILURE)) {
+ // Do nothing: missing "Home".
+ }
+
+ // Any other locations are system scope
+ return AddonManager.SCOPE_SYSTEM;
+ });
+
+ this.__defineGetter__("pendingOperations", function() {
+ return AddonManager.PENDING_NONE;
+ });
+
+ this.__defineGetter__("operationsRequiringRestart", function() {
+ return AddonManager.OP_NEEDS_RESTART_NONE;
+ });
+
+ this.__defineGetter__("permissions", function() {
+ let permissions = 0;
+ if (aTags[0].isEnabledStateLocked) {
+ return permissions;
+ }
+ if (!this.appDisabled) {
+
+ if (this.userDisabled !== true)
+ permissions |= AddonManager.PERM_CAN_DISABLE;
+
+ let blocklistState = this.blocklistState;
+ let isCTPBlocklisted =
+ (blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE ||
+ blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
+
+ if (this.userDisabled !== AddonManager.STATE_ASK_TO_ACTIVATE &&
+ (Services.prefs.getBoolPref("plugins.click_to_play") ||
+ isCTPBlocklisted)) {
+ permissions |= AddonManager.PERM_CAN_ASK_TO_ACTIVATE;
+ }
+
+ if (this.userDisabled !== false && !isCTPBlocklisted) {
+ permissions |= AddonManager.PERM_CAN_ENABLE;
+ }
+ }
+ return permissions;
+ });
+
+ this.__defineGetter__("optionsType", function() {
+ if (canDisableFlashProtectedMode(this)) {
+ return AddonManager.OPTIONS_TYPE_INLINE;
+ }
+ return AddonManager.OPTIONS_TYPE_INLINE_INFO;
+ });
+}
+
+PluginWrapper.prototype = {
+ optionsURL: "chrome://mozapps/content/extensions/pluginPrefs.xul",
+
+ get updateDate() {
+ return this.installDate;
+ },
+
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ get foreignInstall() {
+ return true;
+ },
+
+ isCompatibleWith: function(aAppVerison, aPlatformVersion) {
+ return true;
+ },
+
+ findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
+ if ("onNoCompatibilityUpdateAvailable" in aListener)
+ aListener.onNoCompatibilityUpdateAvailable(this);
+ if ("onNoUpdateAvailable" in aListener)
+ aListener.onNoUpdateAvailable(this);
+ if ("onUpdateFinished" in aListener)
+ aListener.onUpdateFinished(this);
+ }
+};
+
+AddonManagerPrivate.registerProvider(PluginProvider, [
+ new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 6000,
+ AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE)
+]);
diff --git a/components/extensions/src/ProductAddonChecker.jsm b/components/extensions/src/ProductAddonChecker.jsm
new file mode 100644
index 000000000..c6324da0a
--- /dev/null
+++ b/components/extensions/src/ProductAddonChecker.jsm
@@ -0,0 +1,464 @@
+/* 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";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const LOCAL_EME_SOURCES = [{
+ "id": "gmp-gmpopenh264",
+ "src": "chrome://global/content/gmp-sources/openh264.json"
+}, {
+ "id": "gmp-widevinecdm",
+ "src": "chrome://global/content/gmp-sources/widevinecdm.json"
+}];
+
+this.EXPORTED_SYMBOLS = [ "ProductAddonChecker" ];
+
+Cu.importGlobalProperties(["XMLHttpRequest"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/CertUtils.jsm");
+/* globals checkCert, BadCertHandler*/
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+/* globals GMPPrefs */
+XPCOMUtils.defineLazyModuleGetter(this, "GMPPrefs",
+ "resource://gre/modules/GMPUtils.jsm");
+
+/* globals OS */
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
+ "resource://gre/modules/ServiceRequest.jsm");
+
+// This exists so that tests can override the XHR behaviour for downloading
+// the addon update XML file.
+var CreateXHR = function() {
+ return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsISupports);
+}
+
+var logger = Log.repository.getLogger("addons.productaddons");
+
+/**
+ * Number of milliseconds after which we need to cancel `downloadXML`.
+ *
+ * Bug 1087674 suggests that the XHR we use in `downloadXML` may
+ * never terminate in presence of network nuisances (e.g. strange
+ * antivirus behavior). This timeout is a defensive measure to ensure
+ * that we fail cleanly in such case.
+ */
+const TIMEOUT_DELAY_MS = 20000;
+// Chunk size for the incremental downloader
+const DOWNLOAD_CHUNK_BYTES_SIZE = 300000;
+// Incremental downloader interval
+const DOWNLOAD_INTERVAL = 0;
+// How much of a file to read into memory at a time for hashing
+const HASH_CHUNK_SIZE = 8192;
+
+/**
+ * Gets the status of an XMLHttpRequest either directly or from its underlying
+ * channel.
+ *
+ * @param request
+ * The XMLHttpRequest.
+ * @return an integer status value.
+ */
+function getRequestStatus(request) {
+ let status = null;
+ try {
+ status = request.status;
+ }
+ catch (e) {
+ }
+
+ if (status != null) {
+ return status;
+ }
+
+ return request.channel.QueryInterface(Ci.nsIRequest).status;
+}
+
+/**
+ * Downloads an XML document from a URL optionally testing the SSL certificate
+ * for certain attributes.
+ *
+ * @param url
+ * The url to download from.
+ * @param allowNonBuiltIn
+ * Whether to trust SSL certificates without a built-in CA issuer.
+ * @param allowedCerts
+ * The list of certificate attributes to match the SSL certificate
+ * against or null to skip checks.
+ * @return a promise that resolves to the DOM document downloaded or rejects
+ * with a JS exception in case of error.
+ */
+function downloadXML(url, allowNonBuiltIn = false, allowedCerts = null) {
+ return new Promise((resolve, reject) => {
+ let request = CreateXHR();
+ // This is here to let unit test code override XHR
+ if (request.wrappedJSObject) {
+ request = request.wrappedJSObject;
+ }
+ request.open("GET", url, true);
+ request.channel.notificationCallbacks = new BadCertHandler(allowNonBuiltIn);
+ // Prevent the request from reading from the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ // Use conservative TLS settings. See bug 1325501.
+ // TODO move to ServiceRequest.
+ if (request.channel instanceof Ci.nsIHttpChannelInternal) {
+ request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
+ }
+ request.timeout = TIMEOUT_DELAY_MS;
+
+ request.overrideMimeType("text/xml");
+ // The Cache-Control header is only interpreted by proxies and the
+ // final destination. It does not help if a resource is already
+ // cached locally.
+ request.setRequestHeader("Cache-Control", "no-cache");
+ // HTTP/1.0 servers might not implement Cache-Control and
+ // might only implement Pragma: no-cache
+ request.setRequestHeader("Pragma", "no-cache");
+
+ let fail = (event) => {
+ let request = event.target;
+ let status = getRequestStatus(request);
+ let message = "Failed downloading XML, status: " + status + ", reason: " + event.type;
+ logger.warn(message);
+ let ex = new Error(message);
+ ex.status = status;
+ reject(ex);
+ };
+
+ let success = (event) => {
+ logger.info("Completed downloading document");
+ let request = event.target;
+
+ try {
+ checkCert(request.channel, allowNonBuiltIn, allowedCerts);
+ } catch (ex) {
+ logger.error("Request failed certificate checks: " + ex);
+ ex.status = getRequestStatus(request);
+ reject(ex);
+ return;
+ }
+
+ resolve(request.responseXML);
+ };
+
+ request.addEventListener("error", fail, false);
+ request.addEventListener("abort", fail, false);
+ request.addEventListener("timeout", fail, false);
+ request.addEventListener("load", success, false);
+
+ logger.info("sending request to: " + url);
+ request.send(null);
+ });
+}
+
+function downloadJSON(uri) {
+ logger.info("fetching config from: " + uri);
+ return new Promise((resolve, reject) => {
+ let xmlHttp = new ServiceRequest({mozAnon: true});
+
+ xmlHttp.onload = function(aResponse) {
+ resolve(JSON.parse(this.responseText));
+ };
+
+ xmlHttp.onerror = function(e) {
+ reject("Fetching " + uri + " results in error code: " + e.target.status);
+ };
+
+ xmlHttp.open("GET", uri);
+ xmlHttp.overrideMimeType("application/json");
+ xmlHttp.send();
+ });
+}
+
+
+/**
+ * Parses a list of add-ons from a DOM document.
+ *
+ * @param document
+ * The DOM document to parse.
+ * @return null if there is no <addons> element otherwise an object containing
+ * an array of the addons listed and a field notifying whether the
+ * fallback was used.
+ */
+function parseXML(document) {
+ // Check that the root element is correct
+ if (document.documentElement.localName != "updates") {
+ throw new Error("got node name: " + document.documentElement.localName +
+ ", expected: updates");
+ }
+
+ // Check if there are any addons elements in the updates element
+ let addons = document.querySelector("updates:root > addons");
+ if (!addons) {
+ return null;
+ }
+
+ let results = [];
+ let addonList = document.querySelectorAll("updates:root > addons > addon");
+ for (let addonElement of addonList) {
+ let addon = {};
+
+ for (let name of ["id", "URL", "hashFunction", "hashValue", "version", "size"]) {
+ if (addonElement.hasAttribute(name)) {
+ addon[name] = addonElement.getAttribute(name);
+ }
+ }
+ addon.size = Number(addon.size) || undefined;
+
+ results.push(addon);
+ }
+
+ return {
+ usedFallback: false,
+ gmpAddons: results
+ };
+}
+
+/**
+ * If downloading from the network fails (AUS server is down),
+ * load the sources from local build configuration.
+ */
+function downloadLocalConfig() {
+
+ if (!GMPPrefs.get(GMPPrefs.KEY_UPDATE_ENABLED, true)) {
+ logger.info("Updates are disabled via media.gmp-manager.updateEnabled");
+ return Promise.resolve({usedFallback: true, gmpAddons: []});
+ }
+
+ return Promise.all(LOCAL_EME_SOURCES.map(conf => {
+ return downloadJSON(conf.src).then(addons => {
+
+ let platforms = addons.vendors[conf.id].platforms;
+ let target = Services.appinfo.OS + "_" + UpdateUtils.ABI;
+ let details = null;
+
+ while (!details) {
+ if (!(target in platforms)) {
+ // There was no matching platform so return false, this addon
+ // will be filtered from the results below
+ logger.info("no details found for: " + target);
+ return false;
+ }
+ // Field either has the details of the binary or is an alias
+ // to another build target key that does
+ if (platforms[target].alias) {
+ target = platforms[target].alias;
+ } else {
+ details = platforms[target];
+ }
+ }
+
+ logger.info("found plugin: " + conf.id);
+ return {
+ "id": conf.id,
+ "URL": details.fileUrl,
+ "hashFunction": addons.hashFunction,
+ "hashValue": details.hashValue,
+ "version": addons.vendors[conf.id].version,
+ "size": details.filesize
+ };
+ });
+ })).then(addons => {
+
+ // Some filters may not match this platform so
+ // filter those out
+ addons = addons.filter(x => x !== false);
+
+ return {
+ usedFallback: true,
+ gmpAddons: addons
+ };
+ });
+}
+
+/**
+ * Downloads file from a URL using XHR.
+ *
+ * @param url
+ * The url to download from.
+ * @return a promise that resolves to the path of a temporary file or rejects
+ * with a JS exception in case of error.
+ */
+function downloadFile(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.onload = function(response) {
+ logger.info("downloadXHR File download. status=" + xhr.status);
+ if (xhr.status != 200 && xhr.status != 206) {
+ reject(Components.Exception("File download failed", xhr.status));
+ return;
+ }
+ Task.spawn(function* () {
+ let f = yield OS.File.openUnique(OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon"));
+ let path = f.path;
+ logger.info(`Downloaded file will be saved to ${path}`);
+ yield f.file.close();
+ yield OS.File.writeAtomic(path, new Uint8Array(xhr.response));
+ return path;
+ }).then(resolve, reject);
+ };
+
+ let fail = (event) => {
+ let request = event.target;
+ let status = getRequestStatus(request);
+ let message = "Failed downloading via XHR, status: " + status + ", reason: " + event.type;
+ logger.warn(message);
+ let ex = new Error(message);
+ ex.status = status;
+ reject(ex);
+ };
+ xhr.addEventListener("error", fail);
+ xhr.addEventListener("abort", fail);
+
+ xhr.responseType = "arraybuffer";
+ try {
+ xhr.open("GET", url);
+ // Use conservative TLS settings. See bug 1325501.
+ // TODO move to ServiceRequest.
+ if (xhr.channel instanceof Ci.nsIHttpChannelInternal) {
+ xhr.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
+ }
+ xhr.send(null);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+}
+
+/**
+ * Convert a string containing binary values to hex.
+ */
+function binaryToHex(input) {
+ let result = "";
+ for (let i = 0; i < input.length; ++i) {
+ let hex = input.charCodeAt(i).toString(16);
+ if (hex.length == 1) {
+ hex = "0" + hex;
+ }
+ result += hex;
+ }
+ return result;
+}
+
+/**
+ * Calculates the hash of a file.
+ *
+ * @param hashFunction
+ * The type of hash function to use, must be supported by nsICryptoHash.
+ * @param path
+ * The path of the file to hash.
+ * @return a promise that resolves to hash of the file or rejects with a JS
+ * exception in case of error.
+ */
+var computeHash = Task.async(function*(hashFunction, path) {
+ let file = yield OS.File.open(path, { existing: true, read: true });
+ try {
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.initWithString(hashFunction);
+
+ let bytes;
+ do {
+ bytes = yield file.read(HASH_CHUNK_SIZE);
+ hasher.update(bytes, bytes.length);
+ } while (bytes.length == HASH_CHUNK_SIZE);
+
+ return binaryToHex(hasher.finish(false));
+ }
+ finally {
+ yield file.close();
+ }
+});
+
+/**
+ * Verifies that a downloaded file matches what was expected.
+ *
+ * @param properties
+ * The properties to check, `size` and `hashFunction` with `hashValue`
+ * are supported. Any properties missing won't be checked.
+ * @param path
+ * The path of the file to check.
+ * @return a promise that resolves if the file matched or rejects with a JS
+ * exception in case of error.
+ */
+var verifyFile = Task.async(function*(properties, path) {
+ if (properties.size !== undefined) {
+ let stat = yield OS.File.stat(path);
+ if (stat.size != properties.size) {
+ throw new Error("Downloaded file was " + stat.size + " bytes but expected " + properties.size + " bytes.");
+ }
+ }
+
+ if (properties.hashFunction !== undefined) {
+ let expectedDigest = properties.hashValue.toLowerCase();
+ let digest = yield computeHash(properties.hashFunction, path);
+ if (digest != expectedDigest) {
+ throw new Error("Hash was `" + digest + "` but expected `" + expectedDigest + "`.");
+ }
+ }
+});
+
+const ProductAddonChecker = {
+ /**
+ * Downloads a list of add-ons from a URL optionally testing the SSL
+ * certificate for certain attributes.
+ *
+ * @param url
+ * The url to download from.
+ * @param allowNonBuiltIn
+ * Whether to trust SSL certificates without a built-in CA issuer.
+ * @param allowedCerts
+ * The list of certificate attributes to match the SSL certificate
+ * against or null to skip checks.
+ * @return a promise that resolves to an object containing the list of add-ons
+ * and whether the local fallback was used, or rejects with a JS
+ * exception in case of error.
+ */
+ getProductAddonList: function(url, allowNonBuiltIn = false, allowedCerts = null) {
+ if (!GMPPrefs.get(GMPPrefs.KEY_UPDATE_ENABLED, true)) {
+ logger.info("Updates are disabled via media.gmp-manager.updateEnabled");
+ return Promise.resolve({usedFallback: true, gmpAddons: []});
+ }
+
+ return downloadXML(url, allowNonBuiltIn, allowedCerts)
+ .then(parseXML)
+ .catch(downloadLocalConfig);
+ },
+
+ /**
+ * Downloads an add-on to a local file and checks that it matches the expected
+ * file. The caller is responsible for deleting the temporary file returned.
+ *
+ * @param addon
+ * The addon to download.
+ * @return a promise that resolves to the temporary file downloaded or rejects
+ * with a JS exception in case of error.
+ */
+ downloadAddon: Task.async(function*(addon) {
+ let path = yield downloadFile(addon.URL);
+ try {
+ yield verifyFile(addon, path);
+ return path;
+ }
+ catch (e) {
+ yield OS.File.remove(path);
+ throw e;
+ }
+ })
+}
diff --git a/components/extensions/src/SpellCheckDictionaryBootstrap.js b/components/extensions/src/SpellCheckDictionaryBootstrap.js
new file mode 100644
index 000000000..f4f557fc2
--- /dev/null
+++ b/components/extensions/src/SpellCheckDictionaryBootstrap.js
@@ -0,0 +1,17 @@
+/* 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 hunspell, dir;
+
+function startup(data) {
+ hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Components.interfaces.mozISpellCheckingEngine);
+ dir = data.installPath.clone();
+ dir.append("dictionaries");
+ hunspell.addDirectory(dir);
+}
+
+function shutdown() {
+ hunspell.removeDirectory(dir);
+}
diff --git a/components/extensions/src/XPIProvider.jsm b/components/extensions/src/XPIProvider.jsm
new file mode 100644
index 000000000..01f2abc69
--- /dev/null
+++ b/components/extensions/src/XPIProvider.jsm
@@ -0,0 +1,7787 @@
+/* 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ["XPIProvider"];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
+ "resource://gre/modules/ChromeManifestParser.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
+ "resource://gre/modules/LightweightThemeManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils",
+ "resource://gre/modules/ZipUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
+ "resource://gre/modules/PermissionsUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+#ifdef MOZ_DEVTOOLS
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess",
+ "resource://devtools/client/framework/ToolboxProcess.jsm");
+#endif
+XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
+ "resource://gre/modules/Console.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "Blocklist",
+ "@mozilla.org/extensions/blocklist;1",
+ Ci.nsIBlocklistService);
+XPCOMUtils.defineLazyServiceGetter(this,
+ "ChromeRegistry",
+ "@mozilla.org/chrome/chrome-registry;1",
+ "nsIChromeRegistry");
+XPCOMUtils.defineLazyServiceGetter(this,
+ "ResProtocolHandler",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsIResProtocolHandler");
+XPCOMUtils.defineLazyServiceGetter(this,
+ "AddonPathService",
+ "@mozilla.org/addon-path-service;1",
+ "amIAddonPathService");
+
+const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
+ "initWithPath");
+
+const PREF_DB_SCHEMA = "extensions.databaseSchema";
+const PREF_INSTALL_CACHE = "extensions.installCache";
+const PREF_XPI_STATE = "extensions.xpiState";
+const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons";
+const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
+const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending";
+const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin";
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+const PREF_EM_UPDATE_URL = "extensions.update.url";
+const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons";
+const PREF_EM_EXTENSION_FORMAT = "extensions.";
+const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes";
+const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes";
+const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI";
+const PREF_XPI_ENABLED = "xpinstall.enabled";
+const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required";
+const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest";
+const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest";
+const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall.";
+const PREF_XPI_UNPACK = "extensions.alwaysUnpack";
+const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
+const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin";
+const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons";
+const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
+const PREF_SHOWN_SELECTION_UI = "extensions.shownSelectionUI";
+const PREF_INTERPOSITION_ENABLED = "extensions.interposition.enabled";
+
+const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion";
+const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
+
+const URI_EXTENSION_SELECT_DIALOG = "chrome://mozapps/content/extensions/selectAddons.xul";
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
+
+const STRING_TYPE_NAME = "type.%ID%.name";
+
+const DIR_EXTENSIONS = "extensions";
+const DIR_STAGE = "staged";
+const DIR_XPI_STAGE = "staged-xpis";
+const DIR_TRASH = "trash";
+
+const FILE_DATABASE = "extensions.json";
+const FILE_OLD_CACHE = "extensions.cache";
+const FILE_INSTALL_MANIFEST = "install.rdf";
+#ifndef MOZ_JETPACK
+const FILE_JETPACK_MANIFEST_1 = "harness-options.json";
+const FILE_JETPACK_MANIFEST_2 = "package.json";
+#endif
+const FILE_WEBEXT_MANIFEST = "manifest.json";
+const FILE_XPI_ADDONS_LIST = "extensions.ini";
+
+const KEY_PROFILEDIR = "ProfD";
+const KEY_APPDIR = "XCurProcD";
+const KEY_TEMPDIR = "TmpD";
+const KEY_APP_DISTRIBUTION = "XREAppDist";
+
+const KEY_APP_PROFILE = "app-profile";
+const KEY_APP_GLOBAL = "app-global";
+const KEY_APP_SYSTEM_LOCAL = "app-system-local";
+const KEY_APP_SYSTEM_SHARE = "app-system-share";
+const KEY_APP_SYSTEM_USER = "app-system-user";
+
+const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
+const XPI_PERMISSION = "install";
+
+const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest";
+const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+
+// The value for this is in Makefile.in
+#expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__;
+XPCOMUtils.defineConstant(this, "DB_SCHEMA", DB_SCHEMA);
+#ifdef MOZ_DEVTOOLS
+const NOTIFICATION_TOOLBOXPROCESS_LOADED = "ToolboxProcessLoaded";
+#endif
+
+// Properties that exist in the install manifest
+const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL",
+ "updateKey", "optionsURL", "optionsType", "aboutURL",
+ "iconURL", "icon64URL"];
+const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
+const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"];
+const PROP_TARGETAPP = ["id", "minVersion", "maxVersion"];
+
+// Properties that should be migrated where possible from an old database. These
+// shouldn't include properties that can be read directly from install.rdf files
+// or calculated
+const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled",
+ "sourceURI", "applyBackgroundUpdates",
+ "releaseNotesURI", "foreignInstall", "syncGUID"];
+// Properties to cache and reload when an addon installation is pending
+const PENDING_INSTALL_METADATA =
+ ["syncGUID", "targetApplications", "userDisabled", "softDisabled",
+ "existingAddonID", "sourceURI", "releaseNotesURI", "installDate",
+ "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"];
+
+// Note: When adding/changing/removing items here, remember to change the
+// DB schema version to ensure changes are picked up ASAP.
+const STATIC_BLOCKLIST_PATTERNS = [
+ { creator: "Mozilla Corp.",
+ level: Blocklist.STATE_BLOCKED,
+ blockID: "i162" },
+ { creator: "Mozilla.org",
+ level: Blocklist.STATE_BLOCKED,
+ blockID: "i162" }
+];
+
+
+const BOOTSTRAP_REASONS = {
+ APP_STARTUP : 1,
+ APP_SHUTDOWN : 2,
+ ADDON_ENABLE : 3,
+ ADDON_DISABLE : 4,
+ ADDON_INSTALL : 5,
+ ADDON_UNINSTALL : 6,
+ ADDON_UPGRADE : 7,
+ ADDON_DOWNGRADE : 8
+};
+
+// Map new string type identifiers to old style nsIUpdateItem types
+const TYPES = {
+ extension: 2,
+ theme: 4,
+ locale: 8,
+ multipackage: 32,
+ dictionary: 64,
+ experiment: 128,
+};
+
+const RESTARTLESS_TYPES = new Set([
+ "dictionary",
+ "experiment",
+ "locale",
+]);
+
+// Keep track of where we are in startup.
+// event happened during XPIDatabase.startup()
+const XPI_STARTING = "XPIStarting";
+// event happened after startup() but before the final-ui-startup event
+const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup";
+// event happened after final-ui-startup
+const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
+
+const COMPATIBLE_BY_DEFAULT_TYPES = {
+ extension: true,
+ dictionary: true
+};
+
+const MSG_JAR_FLUSH = "AddonJarFlush";
+const MSG_MESSAGE_MANAGER_CACHES_FLUSH = "AddonMessageManagerCachesFlush";
+
+var gGlobalScope = this;
+
+/**
+ * Valid IDs fit this pattern.
+ */
+var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.xpi";
+
+// Create a new logger for use by all objects in this Addons XPI Provider module
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+const LAZY_OBJECTS = ["XPIDatabase"];
+
+var gLazyObjectsLoaded = false;
+
+function loadLazyObjects() {
+ let scope = {};
+ scope.AddonInternal = AddonInternal;
+ scope.XPIProvider = XPIProvider;
+ scope.XPIStates = XPIStates;
+ Services.scriptloader.loadSubScript("resource://gre/modules/addons/XPIProviderUtils.js",
+ scope);
+
+ for (let name of LAZY_OBJECTS) {
+ delete gGlobalScope[name];
+ gGlobalScope[name] = scope[name];
+ }
+ gLazyObjectsLoaded = true;
+ return scope;
+}
+
+for (let name of LAZY_OBJECTS) {
+ Object.defineProperty(gGlobalScope, name, {
+ get: function lazyObjectGetter() {
+ let objs = loadLazyObjects();
+ return objs[name];
+ },
+ configurable: true
+ });
+}
+
+
+function findMatchingStaticBlocklistItem(aAddon) {
+ for (let item of STATIC_BLOCKLIST_PATTERNS) {
+ if ("creator" in item && typeof item.creator == "string") {
+ if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) ||
+ (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) {
+ return item;
+ }
+ }
+ }
+ return null;
+}
+
+
+/**
+ * Sets permissions on a file
+ *
+ * @param aFile
+ * The file or directory to operate on.
+ * @param aPermissions
+ * The permisions to set
+ */
+function setFilePermissions(aFile, aPermissions) {
+ try {
+ aFile.permissions = aPermissions;
+ }
+ catch (e) {
+ logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " +
+ aFile.path, e);
+ }
+}
+
+/**
+ * A safe way to install a file or the contents of a directory to a new
+ * directory. The file or directory is moved or copied recursively and if
+ * anything fails an attempt is made to rollback the entire operation. The
+ * operation may also be rolled back to its original state after it has
+ * completed by calling the rollback method.
+ *
+ * Operations can be chained. Calling move or copy multiple times will remember
+ * the whole set and if one fails all of the operations will be rolled back.
+ */
+function SafeInstallOperation() {
+ this._installedFiles = [];
+ this._createdDirs = [];
+}
+
+SafeInstallOperation.prototype = {
+ _installedFiles: null,
+ _createdDirs: null,
+
+ _installFile: function SIO_installFile(aFile, aTargetDirectory, aCopy) {
+ let oldFile = aCopy ? null : aFile.clone();
+ let newFile = aFile.clone();
+ try {
+ if (aCopy)
+ newFile.copyTo(aTargetDirectory, null);
+ else
+ newFile.moveTo(aTargetDirectory, null);
+ }
+ catch (e) {
+ logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path +
+ " to " + aTargetDirectory.path, e);
+ throw e;
+ }
+ this._installedFiles.push({ oldFile: oldFile, newFile: newFile });
+ },
+
+ _installDirectory: function SIO_installDirectory(aDirectory, aTargetDirectory, aCopy) {
+ let newDir = aTargetDirectory.clone();
+ newDir.append(aDirectory.leafName);
+ try {
+ newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ catch (e) {
+ logger.error("Failed to create directory " + newDir.path, e);
+ throw e;
+ }
+ this._createdDirs.push(newDir);
+
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238), and to remove
+ // normal files before their resource forks on OSX (see bug 733436).
+ let entries = getDirectoryEntries(aDirectory, true);
+ entries.forEach(function(aEntry) {
+ try {
+ this._installDirEntry(aEntry, newDir, aCopy);
+ }
+ catch (e) {
+ logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " +
+ aEntry.path, e);
+ throw e;
+ }
+ }, this);
+
+ // If this is only a copy operation then there is nothing else to do
+ if (aCopy)
+ return;
+
+ // The directory should be empty by this point. If it isn't this will throw
+ // and all of the operations will be rolled back
+ try {
+ setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY);
+ aDirectory.remove(false);
+ }
+ catch (e) {
+ logger.error("Failed to remove directory " + aDirectory.path, e);
+ throw e;
+ }
+
+ // Note we put the directory move in after all the file moves so the
+ // directory is recreated before all the files are moved back
+ this._installedFiles.push({ oldFile: aDirectory, newFile: newDir });
+ },
+
+ _installDirEntry: function SIO_installDirEntry(aDirEntry, aTargetDirectory, aCopy) {
+ let isDir = null;
+
+ try {
+ isDir = aDirEntry.isDirectory();
+ }
+ catch (e) {
+ // If the file has already gone away then don't worry about it, this can
+ // happen on OSX where the resource fork is automatically moved with the
+ // data fork for the file. See bug 733436.
+ if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
+ return;
+
+ logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
+ " to " + aTargetDirectory.path);
+ throw e;
+ }
+
+ try {
+ if (isDir)
+ this._installDirectory(aDirEntry, aTargetDirectory, aCopy);
+ else
+ this._installFile(aDirEntry, aTargetDirectory, aCopy);
+ }
+ catch (e) {
+ logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
+ " to " + aTargetDirectory.path);
+ throw e;
+ }
+ },
+
+ /**
+ * Moves a file or directory into a new directory. If an error occurs then all
+ * files that have been moved will be moved back to their original location.
+ *
+ * @param aFile
+ * The file or directory to be moved.
+ * @param aTargetDirectory
+ * The directory to move into, this is expected to be an empty
+ * directory.
+ */
+ moveUnder: function SIO_move(aFile, aTargetDirectory) {
+ try {
+ this._installDirEntry(aFile, aTargetDirectory, false);
+ }
+ catch (e) {
+ this.rollback();
+ throw e;
+ }
+ },
+
+ /**
+ * Renames a file to a new location. If an error occurs then all
+ * files that have been moved will be moved back to their original location.
+ *
+ * @param aOldLocation
+ * The old location of the file.
+ * @param aNewLocation
+ * The new location of the file.
+ */
+ moveTo: function(aOldLocation, aNewLocation) {
+ try {
+ let oldFile = aOldLocation.clone(), newFile = aNewLocation.clone();
+ oldFile.moveTo(newFile.parent, newFile.leafName);
+ this._installedFiles.push({ oldFile: oldFile, newFile: newFile, isMoveTo: true});
+ }
+ catch(e) {
+ this.rollback();
+ throw e;
+ }
+ },
+
+ /**
+ * Copies a file or directory into a new directory. If an error occurs then
+ * all new files that have been created will be removed.
+ *
+ * @param aFile
+ * The file or directory to be copied.
+ * @param aTargetDirectory
+ * The directory to copy into, this is expected to be an empty
+ * directory.
+ */
+ copy: function SIO_copy(aFile, aTargetDirectory) {
+ try {
+ this._installDirEntry(aFile, aTargetDirectory, true);
+ }
+ catch (e) {
+ this.rollback();
+ throw e;
+ }
+ },
+
+ /**
+ * Rolls back all the moves that this operation performed. If an exception
+ * occurs here then both old and new directories are left in an indeterminate
+ * state
+ */
+ rollback: function SIO_rollback() {
+ while (this._installedFiles.length > 0) {
+ let move = this._installedFiles.pop();
+ if (move.isMoveTo) {
+ move.newFile.moveTo(oldDir.parent, oldDir.leafName);
+ }
+ else if (move.newFile.isDirectory()) {
+ let oldDir = move.oldFile.parent.clone();
+ oldDir.append(move.oldFile.leafName);
+ oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ else if (!move.oldFile) {
+ // No old file means this was a copied file
+ move.newFile.remove(true);
+ }
+ else {
+ move.newFile.moveTo(move.oldFile.parent, null);
+ }
+ }
+
+ while (this._createdDirs.length > 0)
+ recursiveRemove(this._createdDirs.pop());
+ }
+};
+
+/**
+ * Gets the currently selected locale for display.
+ * @return the selected locale or "en-US" if none is selected
+ */
+function getLocale() {
+ if (Preferences.get(PREF_MATCH_OS_LOCALE, false))
+ return Services.locale.getLocaleComponentForUserAgent();
+ try {
+ let locale = Preferences.get(PREF_SELECTED_LOCALE, null, Ci.nsIPrefLocalizedString);
+ if (locale)
+ return locale;
+ }
+ catch (e) {}
+ return Preferences.get(PREF_SELECTED_LOCALE, "en-US");
+}
+
+/**
+ * Selects the closest matching locale from a list of locales.
+ *
+ * @param aLocales
+ * An array of locales
+ * @return the best match for the currently selected locale
+ */
+function findClosestLocale(aLocales) {
+ let appLocale = getLocale();
+
+ // Holds the best matching localized resource
+ var bestmatch = null;
+ // The number of locale parts it matched with
+ var bestmatchcount = 0;
+ // The number of locale parts in the match
+ var bestpartcount = 0;
+
+ var matchLocales = [appLocale.toLowerCase()];
+ /* If the current locale is English then it will find a match if there is
+ a valid match for en-US so no point searching that locale too. */
+ if (matchLocales[0].substring(0, 3) != "en-")
+ matchLocales.push("en-us");
+
+ for each (var locale in matchLocales) {
+ var lparts = locale.split("-");
+ for each (var localized in aLocales) {
+ for each (let found in localized.locales) {
+ found = found.toLowerCase();
+ // Exact match is returned immediately
+ if (locale == found)
+ return localized;
+
+ var fparts = found.split("-");
+ /* If we have found a possible match and this one isn't any longer
+ then we dont need to check further. */
+ if (bestmatch && fparts.length < bestmatchcount)
+ continue;
+
+ // Count the number of parts that match
+ var maxmatchcount = Math.min(fparts.length, lparts.length);
+ var matchcount = 0;
+ while (matchcount < maxmatchcount &&
+ fparts[matchcount] == lparts[matchcount])
+ matchcount++;
+
+ /* If we matched more than the last best match or matched the same and
+ this locale is less specific than the last best match. */
+ if (matchcount > bestmatchcount ||
+ (matchcount == bestmatchcount && fparts.length < bestpartcount)) {
+ bestmatch = localized;
+ bestmatchcount = matchcount;
+ bestpartcount = fparts.length;
+ }
+ }
+ }
+ // If we found a valid match for this locale return it
+ if (bestmatch)
+ return bestmatch;
+ }
+ return null;
+}
+
+/**
+ * Sets the userDisabled and softDisabled properties of an add-on based on what
+ * values those properties had for a previous instance of the add-on. The
+ * previous instance may be a previous install or in the case of an application
+ * version change the same add-on.
+ *
+ * NOTE: this may modify aNewAddon in place; callers should save the database if
+ * necessary
+ *
+ * @param aOldAddon
+ * The previous instance of the add-on
+ * @param aNewAddon
+ * The new instance of the add-on
+ * @param aAppVersion
+ * The optional application version to use when checking the blocklist
+ * or undefined to use the current application
+ * @param aPlatformVersion
+ * The optional platform version to use when checking the blocklist or
+ * undefined to use the current platform
+ */
+function applyBlocklistChanges(aOldAddon, aNewAddon, aOldAppVersion,
+ aOldPlatformVersion) {
+ // Copy the properties by default
+ aNewAddon.userDisabled = aOldAddon.userDisabled;
+ aNewAddon.softDisabled = aOldAddon.softDisabled;
+
+ let oldBlocklistState = Blocklist.getAddonBlocklistState(createWrapper(aOldAddon),
+ aOldAppVersion,
+ aOldPlatformVersion);
+ let newBlocklistState = Blocklist.getAddonBlocklistState(createWrapper(aNewAddon));
+
+ // If the blocklist state hasn't changed then the properties don't need to
+ // change
+ if (newBlocklistState == oldBlocklistState)
+ return;
+
+ if (newBlocklistState == Blocklist.STATE_SOFTBLOCKED) {
+ if (aNewAddon.type != "theme") {
+ // The add-on has become softblocked, set softDisabled if it isn't already
+ // userDisabled
+ aNewAddon.softDisabled = !aNewAddon.userDisabled;
+ }
+ else {
+ // Themes just get userDisabled to switch back to the default theme
+ aNewAddon.userDisabled = true;
+ }
+ }
+ else {
+ // If the new add-on is not softblocked then it cannot be softDisabled
+ aNewAddon.softDisabled = false;
+ }
+}
+
+/**
+ * Calculates whether an add-on should be appDisabled or not.
+ *
+ * @param aAddon
+ * The add-on to check
+ * @return true if the add-on should not be appDisabled
+ */
+function isUsableAddon(aAddon) {
+ // Hack to ensure the default theme is always usable
+ if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin)
+ return true;
+
+ if (aAddon.blocklistState == Blocklist.STATE_BLOCKED)
+ return false;
+
+ if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely)
+ return false;
+
+ if (!aAddon.isPlatformCompatible)
+ return false;
+
+ if (AddonManager.checkCompatibility) {
+ if (!aAddon.isCompatible)
+ return false;
+ }
+ else {
+ if (!aAddon.matchingTargetApplication)
+ return false;
+ }
+
+ return true;
+}
+
+XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1",
+ Ci.nsIRDFService);
+
+function EM_R(aProperty) {
+ return gRDF.GetResource(PREFIX_NS_EM + aProperty);
+}
+
+function createAddonDetails(id, aAddon) {
+ return {
+ id: id || aAddon.id,
+ type: aAddon.type,
+ version: aAddon.version
+ };
+}
+
+/**
+ * Converts an RDF literal, resource or integer into a string.
+ *
+ * @param aLiteral
+ * The RDF object to convert
+ * @return a string if the object could be converted or null
+ */
+function getRDFValue(aLiteral) {
+ if (aLiteral instanceof Ci.nsIRDFLiteral)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFResource)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFInt)
+ return aLiteral.Value;
+ return null;
+}
+
+/**
+ * Gets an RDF property as a string
+ *
+ * @param aDs
+ * The RDF datasource to read the property from
+ * @param aResource
+ * The RDF resource to read the property from
+ * @param aProperty
+ * The property to read
+ * @return a string if the property existed or null
+ */
+function getRDFProperty(aDs, aResource, aProperty) {
+ return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
+}
+
+/**
+ * Reads an AddonInternal object from an RDF stream.
+ *
+ * @param aUri
+ * The URI that the manifest is being read from
+ * @param aStream
+ * An open stream to read the RDF from
+ * @return an AddonInternal object
+ * @throws if the install manifest in the RDF stream is corrupt or could not
+ * be read
+ */
+function loadManifestFromRDF(aUri, aStream) {
+ function getPropertyArray(aDs, aSource, aProperty) {
+ let values = [];
+ let targets = aDs.GetTargets(aSource, EM_R(aProperty), true);
+ while (targets.hasMoreElements())
+ values.push(getRDFValue(targets.getNext()));
+
+ return values;
+ }
+
+ /**
+ * Reads locale properties from either the main install manifest root or
+ * an em:localized section in the install manifest.
+ *
+ * @param aDs
+ * The nsIRDFDatasource to read from
+ * @param aSource
+ * The nsIRDFResource to read the properties from
+ * @param isDefault
+ * True if the locale is to be read from the main install manifest
+ * root
+ * @param aSeenLocales
+ * An array of locale names already seen for this install manifest.
+ * Any locale names seen as a part of this function will be added to
+ * this array
+ * @return an object containing the locale properties
+ */
+ function readLocale(aDs, aSource, isDefault, aSeenLocales) {
+ let locale = { };
+ if (!isDefault) {
+ locale.locales = [];
+ let targets = ds.GetTargets(aSource, EM_R("locale"), true);
+ while (targets.hasMoreElements()) {
+ let localeName = getRDFValue(targets.getNext());
+ if (!localeName) {
+ logger.warn("Ignoring empty locale in localized properties");
+ continue;
+ }
+ if (aSeenLocales.indexOf(localeName) != -1) {
+ logger.warn("Ignoring duplicate locale in localized properties");
+ continue;
+ }
+ aSeenLocales.push(localeName);
+ locale.locales.push(localeName);
+ }
+
+ if (locale.locales.length == 0) {
+ logger.warn("Ignoring localized properties with no listed locales");
+ return null;
+ }
+ }
+
+ PROP_LOCALE_SINGLE.forEach(function(aProp) {
+ locale[aProp] = getRDFProperty(aDs, aSource, aProp);
+ });
+
+ PROP_LOCALE_MULTI.forEach(function(aProp) {
+ // Don't store empty arrays
+ let props = getPropertyArray(aDs, aSource,
+ aProp.substring(0, aProp.length - 1));
+ if (props.length > 0)
+ locale[aProp] = props;
+ });
+
+ return locale;
+ }
+
+ let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
+ createInstance(Ci.nsIRDFXMLParser)
+ let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
+ createInstance(Ci.nsIRDFDataSource);
+ let listener = rdfParser.parseAsync(ds, aUri);
+ let channel = Cc["@mozilla.org/network/input-stream-channel;1"].
+ createInstance(Ci.nsIInputStreamChannel);
+ channel.setURI(aUri);
+ channel.contentStream = aStream;
+ channel.QueryInterface(Ci.nsIChannel);
+ channel.contentType = "text/xml";
+
+ listener.onStartRequest(channel, null);
+
+ try {
+ let pos = 0;
+ let count = aStream.available();
+ while (count > 0) {
+ listener.onDataAvailable(channel, null, aStream, pos, count);
+ pos += count;
+ count = aStream.available();
+ }
+ listener.onStopRequest(channel, null, Components.results.NS_OK);
+ }
+ catch (e) {
+ listener.onStopRequest(channel, null, e.result);
+ throw e;
+ }
+
+ let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT);
+ let addon = new AddonInternal();
+ PROP_METADATA.forEach(function(aProp) {
+ addon[aProp] = getRDFProperty(ds, root, aProp);
+ });
+ addon.unpack = getRDFProperty(ds, root, "unpack") == "true";
+
+ if (!addon.type) {
+ addon.type = addon.internalName ? "theme" : "extension";
+ }
+ else {
+ let type = addon.type;
+ addon.type = null;
+ for (let name in TYPES) {
+ if (TYPES[name] == type) {
+ addon.type = name;
+ break;
+ }
+ }
+ }
+
+ if (!(addon.type in TYPES))
+ throw new Error("Install manifest specifies unknown type: " + addon.type);
+
+ if (addon.type != "multipackage") {
+ if (!addon.id)
+ throw new Error("No ID in install manifest");
+ if (!gIDTest.test(addon.id))
+ throw new Error("Illegal add-on ID " + addon.id);
+ if (!addon.version)
+ throw new Error("No version in install manifest");
+ }
+
+ addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
+ getRDFProperty(ds, root, "strictCompatibility") == "true";
+
+ // Only read these properties for extensions.
+ if (addon.type == "extension") {
+ addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
+ addon.multiprocessCompatible = getRDFProperty(ds, root, "multiprocessCompatible") == "true";
+ if (addon.optionsType &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_TAB &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) {
+ throw new Error("Install manifest specifies unknown type: " + addon.optionsType);
+ }
+ }
+ else {
+ // Some add-on types are always restartless.
+ if (RESTARTLESS_TYPES.has(addon.type)) {
+ addon.bootstrap = true;
+ }
+
+ // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For
+ // all other types they are silently ignored
+ addon.optionsURL = null;
+ addon.optionsType = null;
+ addon.aboutURL = null;
+
+ if (addon.type == "theme") {
+ if (!addon.internalName)
+ throw new Error("Themes must include an internalName property");
+ addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true";
+ }
+ }
+
+ addon.defaultLocale = readLocale(ds, root, true);
+
+ let seenLocales = [];
+ addon.locales = [];
+ let targets = ds.GetTargets(root, EM_R("localized"), true);
+ while (targets.hasMoreElements()) {
+ let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
+ let locale = readLocale(ds, target, false, seenLocales);
+ if (locale)
+ addon.locales.push(locale);
+ }
+
+ let seenApplications = [];
+ addon.targetApplications = [];
+ targets = ds.GetTargets(root, EM_R("targetApplication"), true);
+ while (targets.hasMoreElements()) {
+ let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
+ let targetAppInfo = {};
+ PROP_TARGETAPP.forEach(function(aProp) {
+ targetAppInfo[aProp] = getRDFProperty(ds, target, aProp);
+ });
+ if (!targetAppInfo.id || !targetAppInfo.minVersion ||
+ !targetAppInfo.maxVersion) {
+ logger.warn("Ignoring invalid targetApplication entry in install manifest");
+ continue;
+ }
+ if (seenApplications.indexOf(targetAppInfo.id) != -1) {
+ logger.warn("Ignoring duplicate targetApplication entry for " + targetAppInfo.id +
+ " in install manifest");
+ continue;
+ }
+ seenApplications.push(targetAppInfo.id);
+ addon.targetApplications.push(targetAppInfo);
+ }
+
+ // Note that we don't need to check for duplicate targetPlatform entries since
+ // the RDF service coalesces them for us.
+ let targetPlatforms = getPropertyArray(ds, root, "targetPlatform");
+ addon.targetPlatforms = [];
+ targetPlatforms.forEach(function(aPlatform) {
+ let platform = {
+ os: null,
+ abi: null
+ };
+
+ let pos = aPlatform.indexOf("_");
+ if (pos != -1) {
+ platform.os = aPlatform.substring(0, pos);
+ platform.abi = aPlatform.substring(pos + 1);
+ }
+ else {
+ platform.os = aPlatform;
+ }
+
+ addon.targetPlatforms.push(platform);
+ });
+
+ // A theme's userDisabled value is true if the theme is not the selected skin
+ // or if there is an active lightweight theme. We ignore whether softblocking
+ // is in effect since it would change the active theme.
+ if (addon.type == "theme") {
+ addon.userDisabled = !!LightweightThemeManager.currentTheme ||
+ addon.internalName != XPIProvider.selectedSkin;
+ }
+ // Experiments are disabled by default. It is up to the Experiments Manager
+ // to enable them (it drives installation).
+ else if (addon.type == "experiment") {
+ addon.userDisabled = true;
+ }
+ else {
+ addon.userDisabled = false;
+ addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED;
+ }
+
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+
+ // Experiments are managed and updated through an external "experiments
+ // manager." So disable some built-in mechanisms.
+ if (addon.type == "experiment") {
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+ addon.updateURL = null;
+ addon.updateKey = null;
+
+ addon.targetApplications = [];
+ addon.targetPlatforms = [];
+ }
+
+ // Load the storage service before NSS (nsIRandomGenerator),
+ // to avoid a SQLite initialization error (bug 717904).
+ let storage = Services.storage;
+
+ // Generate random GUID used for Sync.
+ // This was lifted from util.js:makeGUID() from services-sync.
+ let guid = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID().toString();
+ addon.syncGUID = guid;
+
+ return addon;
+}
+
+/**
+ * Loads an AddonInternal object from an add-on extracted in a directory.
+ *
+ * @param aDir
+ * The nsIFile directory holding the add-on
+ * @return an AddonInternal object
+ * @throws if the directory does not contain a valid install manifest
+ */
+function loadManifestFromDir(aDir) {
+ function getFileSize(aFile) {
+ if (aFile.isSymlink())
+ return 0;
+
+ if (!aFile.isDirectory())
+ return aFile.fileSize;
+
+ let size = 0;
+ let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entry;
+ while ((entry = entries.nextFile))
+ size += getFileSize(entry);
+ entries.close();
+ return size;
+ }
+
+ let file = aDir.clone();
+ file.append(FILE_INSTALL_MANIFEST);
+ if (!file.exists() || !file.isFile())
+ throw new Error("Directory " + aDir.path + " does not contain a valid " +
+ "install manifest");
+
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(file, -1, -1, false);
+ let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
+ createInstance(Ci.nsIBufferedInputStream);
+ bis.init(fis, 4096);
+
+ try {
+ let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis);
+ addon._sourceBundle = aDir.clone();
+ addon.size = getFileSize(aDir);
+
+ file = aDir.clone();
+ file.append("chrome.manifest");
+ let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file));
+ addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
+ "binary-component");
+
+ addon.appDisabled = !isUsableAddon(addon);
+ return addon;
+ }
+ finally {
+ bis.close();
+ fis.close();
+ }
+}
+
+/**
+ * Loads an AddonInternal object from an nsIZipReader for an add-on.
+ *
+ * @param aZipReader
+ * An open nsIZipReader for the add-on's files
+ * @return an AddonInternal object
+ * @throws if the XPI file does not contain a valid install manifest.
+ * Throws with |webext:true| if a WebExtension manifest was found
+ * to distinguish between WebExtensions and corrupt files.
+ * Throws with |jetpacksdk:true| if a Jetpack files were found
+ * if Jetpack its self isn't built.
+ */
+function loadManifestFromZipReader(aZipReader) {
+ // If WebExtension but not install.rdf throw an error
+ if (aZipReader.hasEntry(FILE_WEBEXT_MANIFEST)) {
+ if (!aZipReader.hasEntry(FILE_INSTALL_MANIFEST)) {
+ throw {
+ name: "UnsupportedExtension",
+ message: Services.appinfo.name + " does not support WebExtensions",
+ webext: true
+ };
+ }
+ }
+
+#ifndef MOZ_JETPACK
+ // If Jetpack is not built throw an error
+ if (aZipReader.hasEntry(FILE_JETPACK_MANIFEST_1) ||
+ aZipReader.hasEntry(FILE_JETPACK_MANIFEST_2)) {
+ throw {
+ name: "UnsupportedExtension",
+ message: Services.appinfo.name + " does not support Jetpack Extensions",
+ jetpacksdk: true
+ };
+ }
+#endif
+
+ // Attempt to open install.rdf else throw normally
+ let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST);
+ // Create a buffered input stream for install.rdf
+ let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
+ createInstance(Ci.nsIBufferedInputStream);
+ bis.init(zis, 4096);
+
+ try {
+ let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
+ let addon = loadManifestFromRDF(uri, bis);
+ addon._sourceBundle = aZipReader.file;
+
+ addon.size = 0;
+ let entries = aZipReader.findEntries(null);
+ while (entries.hasMore())
+ addon.size += aZipReader.getEntry(entries.getNext()).realSize;
+
+ // Binary components can only be loaded from unpacked addons.
+ if (addon.unpack) {
+ uri = buildJarURI(aZipReader.file, "chrome.manifest");
+ let chromeManifest = ChromeManifestParser.parseSync(uri);
+ addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
+ "binary-component");
+ } else {
+ addon.hasBinaryComponents = false;
+ }
+
+ addon.appDisabled = !isUsableAddon(addon);
+ return addon;
+ }
+ finally {
+ // Close the buffered input stream
+ bis.close();
+ // Close the input stream to install.rdf
+ zis.close();
+ }
+}
+
+/**
+ * Loads an AddonInternal object from an add-on in an XPI file.
+ *
+ * @param aXPIFile
+ * An nsIFile pointing to the add-on's XPI file
+ * @return an AddonInternal object
+ * @throws if the XPI file does not contain a valid install manifest
+ */
+function loadManifestFromZipFile(aXPIFile) {
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ try {
+ zipReader.open(aXPIFile);
+
+ return loadManifestFromZipReader(zipReader);
+ }
+ finally {
+ zipReader.close();
+ }
+}
+
+function loadManifestFromFile(aFile) {
+ if (aFile.isFile())
+ return loadManifestFromZipFile(aFile);
+ else
+ return loadManifestFromDir(aFile);
+}
+
+/**
+ * Gets an nsIURI for a file within another file, either a directory or an XPI
+ * file. If aFile is a directory then this will return a file: URI, if it is an
+ * XPI file then it will return a jar: URI.
+ *
+ * @param aFile
+ * The file containing the resources, must be either a directory or an
+ * XPI file
+ * @param aPath
+ * The path to find the resource at, "/" separated. If aPath is empty
+ * then the uri to the root of the contained files will be returned
+ * @return an nsIURI pointing at the resource
+ */
+function getURIForResourceInFile(aFile, aPath) {
+ if (aFile.isDirectory()) {
+ let resource = aFile.clone();
+ if (aPath) {
+ aPath.split("/").forEach(function(aPart) {
+ resource.append(aPart);
+ });
+ }
+ return NetUtil.newURI(resource);
+ }
+
+ return buildJarURI(aFile, aPath);
+}
+
+/**
+ * Creates a jar: URI for a file inside a ZIP file.
+ *
+ * @param aJarfile
+ * The ZIP file as an nsIFile
+ * @param aPath
+ * The path inside the ZIP file
+ * @return an nsIURI for the file
+ */
+function buildJarURI(aJarfile, aPath) {
+ let uri = Services.io.newFileURI(aJarfile);
+ uri = "jar:" + uri.spec + "!/" + aPath;
+ return NetUtil.newURI(uri);
+}
+
+/**
+ * Sends local and remote notifications to flush a JAR file cache entry
+ *
+ * @param aJarFile
+ * The ZIP/XPI/JAR file as a nsIFile
+ */
+function flushJarCache(aJarFile) {
+ Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null);
+ Services.mm.broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
+}
+
+function flushChromeCaches() {
+ // Init this, so it will get the notification.
+ Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+ // Flush message manager cached scripts
+ Services.obs.notifyObservers(null, "message-manager-flush-caches", null);
+ // Also dispatch this event to child processes
+ Services.mm.broadcastAsyncMessage(MSG_MESSAGE_MANAGER_CACHES_FLUSH, null);
+}
+
+/**
+ * Creates and returns a new unique temporary file. The caller should delete
+ * the file when it is no longer needed.
+ *
+ * @return an nsIFile that points to a randomly named, initially empty file in
+ * the OS temporary files directory
+ */
+function getTemporaryFile() {
+ let file = FileUtils.getDir(KEY_TEMPDIR, []);
+ let random = Math.random().toString(36).replace(/0./, '').substr(-3);
+ file.append("tmp-" + random + ".xpi");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ return file;
+}
+
+/**
+ * Verifies that a zip file's contents are all signed by the same principal.
+ * Directory entries and anything in the META-INF directory are not checked.
+ *
+ * @param aZip
+ * A nsIZipReader to check
+ * @param aCertificate
+ * The nsIX509Cert to compare against
+ * @return true if all the contents that should be signed were signed by the
+ * principal
+ */
+function verifyZipSigning(aZip, aCertificate) {
+ var count = 0;
+ var entries = aZip.findEntries(null);
+ while (entries.hasMore()) {
+ var entry = entries.getNext();
+ // Nothing in META-INF is in the manifest.
+ if (entry.substr(0, 9) == "META-INF/")
+ continue;
+ // Directory entries aren't in the manifest.
+ if (entry.substr(-1) == "/")
+ continue;
+ count++;
+ var entryCertificate = aZip.getSigningCert(entry);
+ if (!entryCertificate || !aCertificate.equals(entryCertificate)) {
+ return false;
+ }
+ }
+ return aZip.manifestEntriesCount == count;
+}
+
+/**
+ * Replaces %...% strings in an addon url (update and updateInfo) with
+ * appropriate values.
+ *
+ * @param aAddon
+ * The AddonInternal representing the add-on
+ * @param aUri
+ * The uri to escape
+ * @param aUpdateType
+ * An optional number representing the type of update, only applicable
+ * when creating a url for retrieving an update manifest
+ * @param aAppVersion
+ * The optional application version to use for %APP_VERSION%
+ * @return the appropriately escaped uri.
+ */
+function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion)
+{
+ let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion);
+
+ // If there is an updateType then replace the UPDATE_TYPE string
+ if (aUpdateType)
+ uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType);
+
+ // If this add-on has compatibility information for either the current
+ // application or toolkit then replace the ITEM_MAXAPPVERSION with the
+ // maxVersion
+ let app = aAddon.matchingTargetApplication;
+ if (app)
+ var maxVersion = app.maxVersion;
+ else
+ maxVersion = "";
+ uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion);
+
+ let compatMode = "normal";
+ if (!AddonManager.checkCompatibility)
+ compatMode = "ignore";
+ else if (AddonManager.strictCompatibility)
+ compatMode = "strict";
+ uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode);
+
+ return uri;
+}
+
+function removeAsync(aFile) {
+ return Task.spawn(function () {
+ let info = null;
+ try {
+ info = yield OS.File.stat(aFile.path);
+ if (info.isDir)
+ yield OS.File.removeDir(aFile.path);
+ else
+ yield OS.File.remove(aFile.path);
+ }
+ catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ // The file has already gone away
+ return;
+ }
+ });
+}
+
+/**
+ * Recursively removes a directory or file fixing permissions when necessary.
+ *
+ * @param aFile
+ * The nsIFile to remove
+ */
+function recursiveRemove(aFile) {
+ let isDir = null;
+
+ try {
+ isDir = aFile.isDirectory();
+ }
+ catch (e) {
+ // If the file has already gone away then don't worry about it, this can
+ // happen on OSX where the resource fork is automatically moved with the
+ // data fork for the file. See bug 733436.
+ if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
+ return;
+ if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND)
+ return;
+
+ throw e;
+ }
+
+ setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY
+ : FileUtils.PERMS_FILE);
+
+ try {
+ aFile.remove(true);
+ return;
+ }
+ catch (e) {
+ if (!aFile.isDirectory()) {
+ logger.error("Failed to remove file " + aFile.path, e);
+ throw e;
+ }
+ }
+
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238), and to remove
+ // normal files before their resource forks on OSX (see bug 733436).
+ let entries = getDirectoryEntries(aFile, true);
+ entries.forEach(recursiveRemove);
+
+ try {
+ aFile.remove(true);
+ }
+ catch (e) {
+ logger.error("Failed to remove empty directory " + aFile.path, e);
+ throw e;
+ }
+}
+
+/**
+ * Returns the timestamp and leaf file name of the most recently modified
+ * entry in a directory,
+ * or simply the file's own timestamp if it is not a directory.
+ * Also returns the total number of items (directories and files) visited in the scan
+ *
+ * @param aFile
+ * A non-null nsIFile object
+ * @return [File Name, Epoch time, items visited], as described above.
+ */
+function recursiveLastModifiedTime(aFile) {
+ try {
+ let modTime = aFile.lastModifiedTime;
+ let fileName = aFile.leafName;
+ if (aFile.isFile())
+ return [fileName, modTime, 1];
+
+ if (aFile.isDirectory()) {
+ let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entry;
+ let totalItems = 1;
+ while ((entry = entries.nextFile)) {
+ let [subName, subTime, items] = recursiveLastModifiedTime(entry);
+ totalItems += items;
+ if (subTime > modTime) {
+ modTime = subTime;
+ fileName = subName;
+ }
+ }
+ entries.close();
+ return [fileName, modTime, totalItems];
+ }
+ }
+ catch (e) {
+ logger.warn("Problem getting last modified time for " + aFile.path, e);
+ }
+
+ // If the file is something else, just ignore it.
+ return ["", 0, 0];
+}
+
+/**
+ * Gets a snapshot of directory entries.
+ *
+ * @param aDir
+ * Directory to look at
+ * @param aSortEntries
+ * True to sort entries by filename
+ * @return An array of nsIFile, or an empty array if aDir is not a readable directory
+ */
+function getDirectoryEntries(aDir, aSortEntries) {
+ let dirEnum;
+ try {
+ dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entries = [];
+ while (dirEnum.hasMoreElements())
+ entries.push(dirEnum.nextFile);
+
+ if (aSortEntries) {
+ entries.sort(function sortDirEntries(a, b) {
+ return a.path > b.path ? -1 : 1;
+ });
+ }
+
+ return entries
+ }
+ catch (e) {
+ logger.warn("Can't iterate directory " + aDir.path, e);
+ return [];
+ }
+ finally {
+ if (dirEnum) {
+ dirEnum.close();
+ }
+ }
+}
+
+/**
+ * Wraps a function in an exception handler to protect against exceptions inside callbacks
+ * @param aFunction function(args...)
+ * @return function(args...), a function that takes the same arguments as aFunction
+ * and returns the same result unless aFunction throws, in which case it logs
+ * a warning and returns undefined.
+ */
+function makeSafe(aFunction) {
+ return function(...aArgs) {
+ try {
+ return aFunction(...aArgs);
+ }
+ catch(ex) {
+ logger.warn("XPIProvider callback failed", ex);
+ }
+ return undefined;
+ }
+}
+
+/**
+ * The on-disk state of an individual XPI, created from an Object
+ * as stored in the 'extensions.xpiState' pref.
+ */
+function XPIState(saved) {
+ for (let [short, long] of XPIState.prototype.fields) {
+ if (short in saved) {
+ this[long] = saved[short];
+ }
+ }
+}
+
+XPIState.prototype = {
+ fields: [['d', 'descriptor'],
+ ['e', 'enabled'],
+ ['v', 'version'],
+ ['st', 'scanTime'],
+ ['mt', 'manifestTime']],
+ /**
+ * Return the last modified time, based on enabled/disabled
+ */
+ get mtime() {
+ if (!this.enabled && ('manifestTime' in this) && this.manifestTime > this.scanTime) {
+ return this.manifestTime;
+ }
+ return this.scanTime;
+ },
+
+ toJSON() {
+ let json = {};
+ for (let [short, long] of XPIState.prototype.fields) {
+ if (long in this) {
+ json[short] = this[long];
+ }
+ }
+ return json;
+ },
+
+ /**
+ * Update the last modified time for an add-on on disk.
+ * @param aFile: nsIFile path of the add-on.
+ * @param aId: The add-on ID.
+ * @return True if the time stamp has changed.
+ */
+ getModTime(aFile, aId) {
+ let changed = false;
+ let scanStarted = Cu.now();
+ // For an unknown or enabled add-on, we do a full recursive scan.
+ if (!('scanTime' in this) || this.enabled) {
+ logger.debug('getModTime: Recursive scan of ' + aId);
+ let [modFile, modTime, items] = recursiveLastModifiedTime(aFile);
+ XPIProvider._mostRecentlyModifiedFile[aId] = modFile;
+ if (modTime != this.scanTime) {
+ this.scanTime = modTime;
+ changed = true;
+ }
+ }
+ // if the add-on is disabled, modified time is the install.rdf time, if any.
+ // If {path}/install.rdf doesn't exist, we assume this is a packed .xpi and use
+ // the time stamp of {path}
+ try {
+ // Get the install.rdf update time, if any.
+ // XXX This will eventually also need to check for package.json or whatever
+ // the new manifest is named.
+ let maniFile = aFile.clone();
+ maniFile.append(FILE_INSTALL_MANIFEST);
+ if (!(aId in XPIProvider._mostRecentlyModifiedFile)) {
+ XPIProvider._mostRecentlyModifiedFile[aId] = maniFile.leafName;
+ }
+ let maniTime = maniFile.lastModifiedTime;
+ if (maniTime != this.manifestTime) {
+ this.manifestTime = maniTime;
+ changed = true;
+ }
+ } catch (e) {
+ // No manifest
+ delete this.manifestTime;
+ try {
+ let dtime = aFile.lastModifiedTime;
+ if (dtime != this.scanTime) {
+ changed = true;
+ this.scanTime = dtime;
+ }
+ } catch (e) {
+ logger.warn("Can't get modified time of ${file}: ${e}", {file: aFile.path, e: e});
+ changed = true;
+ this.scanTime = 0;
+ }
+ }
+ return changed;
+ },
+
+ /**
+ * Update the XPIState to match an XPIDatabase entry; if 'enabled' is changed to true,
+ * update the last-modified time. This should probably be made async, but for now we
+ * don't want to maintain parallel sync and async versions of the scan.
+ * Caller is responsible for doing XPIStates.save() if necessary.
+ * @param aDBAddon The DBAddonInternal for this add-on.
+ * @param aUpdated The add-on was updated, so we must record new modified time.
+ */
+ syncWithDB(aDBAddon, aUpdated = false) {
+ logger.debug("Updating XPIState for " + JSON.stringify(aDBAddon));
+ // If the add-on changes from disabled to enabled, we should re-check the modified time.
+ // If this is a newly found add-on, it won't have an 'enabled' field but we
+ // did a full recursive scan in that case, so we don't need to do it again.
+ // We don't use aDBAddon.active here because it's not updated until after restart.
+ let mustGetMod = (aDBAddon.visible && !aDBAddon.disabled && !this.enabled);
+ this.enabled = (aDBAddon.visible && !aDBAddon.disabled);
+ this.version = aDBAddon.version;
+ // XXX Eventually also copy bootstrap, etc.
+ if (aUpdated || mustGetMod) {
+ this.getModTime(new nsIFile(this.descriptor), aDBAddon.id);
+ if (this.scanTime != aDBAddon.updateDate) {
+ aDBAddon.updateDate = this.scanTime;
+ XPIDatabase.saveChanges();
+ }
+ }
+ },
+};
+
+// Constructor for an ES6 Map that knows how to convert itself into a
+// regular object for toJSON().
+function SerializableMap() {
+ let m = new Map();
+ m.toJSON = function() {
+ let out = {}
+ for (let [key, val] of m) {
+ out[key] = val;
+ }
+ return out;
+ };
+ return m;
+}
+
+/**
+ * Keeps track of the state of XPI add-ons on the file system.
+ */
+this.XPIStates = {
+ // Map(location name -> Map(add-on ID -> XPIState))
+ db: null,
+
+ get size() {
+ if (!this.db) {
+ return 0;
+ }
+ let count = 0;
+ for (let location of this.db.values()) {
+ count += location.size;
+ }
+ return count;
+ },
+
+ /**
+ * Load extension state data from preferences.
+ */
+ loadExtensionState() {
+ let state = {};
+
+ // Clear out old directory state cache.
+ Preferences.reset(PREF_INSTALL_CACHE);
+
+ let cache = Preferences.get(PREF_XPI_STATE, "{}");
+ try {
+ state = JSON.parse(cache);
+ } catch (e) {
+ logger.warn("Error parsing extensions.xpiState ${state}: ${error}",
+ {state: cache, error: e});
+ }
+ logger.debug("Loaded add-on state from prefs: ${}", state);
+ return state;
+ },
+
+ /**
+ * Walk through all install locations, highest priority first,
+ * comparing the on-disk state of extensions to what is stored in prefs.
+ * @return true if anything has changed.
+ */
+ getInstallState() {
+ let oldState = this.loadExtensionState();
+ let changed = false;
+ this.db = new SerializableMap();
+
+ for (let location of XPIProvider.installLocations) {
+ // The list of add-on like file/directory names in the install location.
+ let addons = location.addonLocations;
+ // The results of scanning this location.
+ let foundAddons = new SerializableMap();
+
+ // What our old state thinks should be in this location.
+ let locState = {};
+ if (location.name in oldState) {
+ locState = oldState[location.name];
+ // We've seen this location.
+ delete oldState[location.name];
+ }
+
+ for (let file of addons) {
+ let id = location.getIDForLocation(file);
+
+ if (!(id in locState)) {
+ logger.debug("New add-on ${id} in ${location}", {id: id, location: location.name});
+ let xpiState = new XPIState({d: file.persistentDescriptor});
+ changed = xpiState.getModTime(file, id) || changed;
+ foundAddons.set(id, xpiState);
+ } else {
+ let xpiState = new XPIState(locState[id]);
+ // We found this add-on in the file system
+ delete locState[id];
+
+ changed = xpiState.getModTime(file, id) || changed;
+
+ if (file.persistentDescriptor != xpiState.descriptor) {
+ xpiState.descriptor = file.persistentDescriptor;
+ changed = true;
+ }
+ if (changed) {
+ logger.debug("Changed add-on ${id} in ${location}", {id: id, location: location.name});
+ }
+ foundAddons.set(id, xpiState);
+ }
+ }
+
+ // Anything left behind in oldState was removed from the file system.
+ for (let id in locState) {
+ changed = true;
+ break;
+ }
+ // If we found anything, add this location to our database.
+ if (foundAddons.size != 0) {
+ this.db.set(location.name, foundAddons);
+ }
+ }
+
+ // If there's anything left in oldState, an install location that held add-ons
+ // was removed from the browser configuration.
+ for (let location in oldState) {
+ changed = true;
+ break;
+ }
+
+ logger.debug("getInstallState changed: ${rv}, state: ${state}",
+ {rv: changed, state: this.db});
+ return changed;
+ },
+
+ /**
+ * Get the Map of XPI states for a particular location.
+ * @param aLocation The name of the install location.
+ * @return Map (id -> XPIState) or null if there are no add-ons in the location.
+ */
+ getLocation(aLocation) {
+ return this.db.get(aLocation);
+ },
+
+ /**
+ * Get the XPI state for a specific add-on in a location.
+ * If the state is not in our cache, return null.
+ * @param aLocation The name of the location where the add-on is installed.
+ * @param aId The add-on ID
+ * @return The XPIState entry for the add-on, or null.
+ */
+ getAddon(aLocation, aId) {
+ let location = this.db.get(aLocation);
+ if (!location) {
+ return null;
+ }
+ return location.get(aId);
+ },
+
+ /**
+ * Find the highest priority location of an add-on by ID and return the
+ * location and the XPIState.
+ * @param aId The add-on ID
+ * @return [locationName, XPIState] if the add-on is found, [undefined, undefined]
+ * if the add-on is not found.
+ */
+ findAddon(aId) {
+ // Fortunately the Map iterator returns in order of insertion, which is
+ // also our highest -> lowest priority order.
+ for (let [name, location] of this.db) {
+ if (location.has(aId)) {
+ return [name, location.get(aId)];
+ }
+ }
+ return [undefined, undefined];
+ },
+
+ /**
+ * Add a new XPIState for an add-on and synchronize it with the DBAddonInternal.
+ * @param aAddon DBAddonInternal for the new add-on.
+ */
+ addAddon(aAddon) {
+ let location = this.db.get(aAddon.location);
+ if (!location) {
+ // First add-on in this location.
+ location = new SerializableMap();
+ this.db.set(aAddon.location, location);
+ }
+ logger.debug("XPIStates adding add-on ${id} in ${location}: ${descriptor}", aAddon);
+ let xpiState = new XPIState({d: aAddon.descriptor});
+ location.set(aAddon.id, xpiState);
+ xpiState.syncWithDB(aAddon, true);
+ },
+
+ /**
+ * Save the current state of installed add-ons.
+ * XXX this *totally* should be a .json file using DeferredSave...
+ */
+ save() {
+ let cache = JSON.stringify(this.db);
+ Services.prefs.setCharPref(PREF_XPI_STATE, cache);
+ },
+
+ /**
+ * Remove the XPIState for an add-on and save the new state.
+ * @param aLocation The name of the add-on location.
+ * @param aId The ID of the add-on.
+ */
+ removeAddon(aLocation, aId) {
+ logger.debug("Removing XPIState for " + aLocation + ":" + aId);
+ let location = this.db.get(aLocation);
+ if (!location) {
+ return;
+ }
+ location.delete(aId);
+ if (location.size == 0) {
+ this.db.delete(aLocation);
+ }
+ this.save();
+ },
+};
+
+this.XPIProvider = {
+ get name() "XPIProvider",
+
+ // An array of known install locations
+ installLocations: null,
+ // A dictionary of known install locations by name
+ installLocationsByName: null,
+ // An array of currently active AddonInstalls
+ installs: null,
+ // The default skin for the application
+ defaultSkin: "classic/1.0",
+ // The current skin used by the application
+ currentSkin: null,
+ // The selected skin to be used by the application when it is restarted. This
+ // will be the same as currentSkin when it is the skin to be used when the
+ // application is restarted
+ selectedSkin: null,
+ // The value of the minCompatibleAppVersion preference
+ minCompatibleAppVersion: null,
+ // The value of the minCompatiblePlatformVersion preference
+ minCompatiblePlatformVersion: null,
+ // A dictionary of the file descriptors for bootstrappable add-ons by ID
+ bootstrappedAddons: {},
+ // A dictionary of JS scopes of loaded bootstrappable add-ons by ID
+ bootstrapScopes: {},
+ // True if the platform could have activated extensions
+ extensionsActive: false,
+ // True if all of the add-ons found during startup were installed in the
+ // application install location
+ allAppGlobal: true,
+ // A string listing the enabled add-ons for annotating crash reports
+ enabledAddons: null,
+ // Keep track of startup phases.
+ runPhase: XPI_STARTING,
+ // Keep track of the newest file in each add-on, in case we want to
+ // report it.
+ _mostRecentlyModifiedFile: {},
+ // Experiments are disabled by default. Track ones that are locally enabled.
+ _enabledExperiments: null,
+ // A Map from an add-on install to its ID
+ _addonFileMap: new Map(),
+#ifdef MOZ_DEVTOOLS
+ // Flag to know if ToolboxProcess.jsm has already been loaded by someone or not
+ _toolboxProcessLoaded: false,
+#endif
+ // Have we started shutting down bootstrap add-ons?
+ _closing: false,
+
+ // Keep track of in-progress operations that support cancel()
+ _inProgress: [],
+
+ doing: function XPI_doing(aCancellable) {
+ this._inProgress.push(aCancellable);
+ },
+
+ done: function XPI_done(aCancellable) {
+ let i = this._inProgress.indexOf(aCancellable);
+ if (i != -1) {
+ this._inProgress.splice(i, 1);
+ return true;
+ }
+ return false;
+ },
+
+ cancelAll: function XPI_cancelAll() {
+ // Cancelling one may alter _inProgress, so don't use a simple iterator
+ while (this._inProgress.length > 0) {
+ let c = this._inProgress.shift();
+ try {
+ c.cancel();
+ }
+ catch (e) {
+ logger.warn("Cancel failed", e);
+ }
+ }
+ },
+
+ /**
+ * Adds or updates a URI mapping for an Addon.id.
+ *
+ * Mappings should not be removed at any point. This is so that the mappings
+ * will be still valid after an add-on gets disabled or uninstalled, as
+ * consumers may still have URIs of (leaked) resources they want to map.
+ */
+ _addURIMapping: function XPI__addURIMapping(aID, aFile) {
+ logger.info("Mapping " + aID + " to " + aFile.path);
+ this._addonFileMap.set(aID, aFile.path);
+
+ AddonPathService.insertPath(aFile.path, aID);
+ },
+
+ /**
+ * Resolve a URI back to physical file.
+ *
+ * Of course, this works only for URIs pointing to local resources.
+ *
+ * @param aURI
+ * URI to resolve
+ * @return
+ * resolved nsIFileURL
+ */
+ _resolveURIToFile: function XPI__resolveURIToFile(aURI) {
+ switch (aURI.scheme) {
+ case "jar":
+ case "file":
+ if (aURI instanceof Ci.nsIJARURI) {
+ return this._resolveURIToFile(aURI.JARFile);
+ }
+ return aURI;
+
+ case "chrome":
+ aURI = ChromeRegistry.convertChromeURL(aURI);
+ return this._resolveURIToFile(aURI);
+
+ case "resource":
+ aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null,
+ null);
+ return this._resolveURIToFile(aURI);
+
+ case "view-source":
+ aURI = Services.io.newURI(aURI.path, null, null);
+ return this._resolveURIToFile(aURI);
+
+ case "about":
+ if (aURI.spec == "about:blank") {
+ // Do not attempt to map about:blank
+ return null;
+ }
+
+ let chan;
+ try {
+ chan = NetUtil.newChannel({
+ uri: aURI,
+ loadUsingSystemPrincipal: true
+ });
+ }
+ catch (ex) {
+ return null;
+ }
+ // Avoid looping
+ if (chan.URI.equals(aURI)) {
+ return null;
+ }
+ // We want to clone the channel URI to avoid accidentially keeping
+ // unnecessary references to the channel or implementation details
+ // around.
+ return this._resolveURIToFile(chan.URI.clone());
+
+ default:
+ return null;
+ }
+ },
+
+ /**
+ * Starts the XPI provider initializes the install locations and prefs.
+ *
+ * @param aAppChanged
+ * A tri-state value. Undefined means the current profile was created
+ * for this session, true means the profile already existed but was
+ * last used with an application with a different version number,
+ * false means that the profile was last used by this version of the
+ * application.
+ * @param aOldAppVersion
+ * The version of the application last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @param aOldPlatformVersion
+ * The version of the platform last run with this profile or null
+ * if it is a new profile or the version is unknown
+ */
+ startup: function XPI_startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) {
+ function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) {
+ try {
+ var dir = FileUtils.getDir(aKey, aPaths);
+ }
+ catch (e) {
+ // Some directories aren't defined on some platforms, ignore them
+ logger.debug("Skipping unavailable install location " + aName);
+ return;
+ }
+
+ try {
+ var location = new DirectoryInstallLocation(aName, dir, aScope, aLocked);
+ }
+ catch (e) {
+ logger.warn("Failed to add directory install location " + aName, e);
+ return;
+ }
+
+ XPIProvider.installLocations.push(location);
+ XPIProvider.installLocationsByName[location.name] = location;
+ }
+
+ function addRegistryInstallLocation(aName, aRootkey, aScope) {
+ try {
+ var location = new WinRegInstallLocation(aName, aRootkey, aScope);
+ }
+ catch (e) {
+ logger.warn("Failed to add registry install location " + aName, e);
+ return;
+ }
+
+ XPIProvider.installLocations.push(location);
+ XPIProvider.installLocationsByName[location.name] = location;
+ }
+
+ try {
+ logger.debug("startup");
+ this.runPhase = XPI_STARTING;
+ this.installs = [];
+ this.installLocations = [];
+ this.installLocationsByName = {};
+ // Hook for tests to detect when saving database at shutdown time fails
+ this._shutdownError = null;
+ // Clear the set of enabled experiments (experiments disabled by default).
+ this._enabledExperiments = new Set();
+
+ let hasRegistry = ("nsIWindowsRegKey" in Ci);
+
+ let enabledScopes = Preferences.get(PREF_EM_ENABLED_SCOPES,
+ AddonManager.SCOPE_ALL);
+
+ // These must be in order of priority, highest to lowest,
+ // for processFileChanges etc. to work
+ // The profile location is always enabled
+ addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR,
+ [DIR_EXTENSIONS],
+ AddonManager.SCOPE_PROFILE, false);
+
+ if (enabledScopes & AddonManager.SCOPE_USER) {
+ addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt",
+ [Services.appinfo.ID],
+ AddonManager.SCOPE_USER, true);
+ if (hasRegistry) {
+ addRegistryInstallLocation("winreg-app-user",
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ AddonManager.SCOPE_USER);
+ }
+ }
+
+ if (enabledScopes & AddonManager.SCOPE_APPLICATION) {
+ addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_APPDIR,
+ [DIR_EXTENSIONS],
+ AddonManager.SCOPE_APPLICATION, true);
+ }
+
+ if (enabledScopes & AddonManager.SCOPE_SYSTEM) {
+ addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD",
+ [Services.appinfo.ID],
+ AddonManager.SCOPE_SYSTEM, true);
+ addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD",
+ [Services.appinfo.ID],
+ AddonManager.SCOPE_SYSTEM, true);
+ if (hasRegistry) {
+ addRegistryInstallLocation("winreg-app-global",
+ Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ AddonManager.SCOPE_SYSTEM);
+ }
+ }
+
+ let defaultPrefs = new Preferences({ defaultBranch: true });
+ this.defaultSkin = defaultPrefs.get(PREF_GENERAL_SKINS_SELECTEDSKIN,
+ "classic/1.0");
+ this.currentSkin = Preferences.get(PREF_GENERAL_SKINS_SELECTEDSKIN,
+ this.defaultSkin);
+ this.selectedSkin = this.currentSkin;
+ this.applyThemeChange();
+
+ this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION,
+ null);
+ this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
+ null);
+ this.enabledAddons = "";
+
+ Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false);
+ Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false);
+ Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false);
+
+#ifdef MOZ_DEVTOOLS
+ if (Cu.isModuleLoaded("resource://devtools/client/framework/ToolboxProcess.jsm")) {
+ // If BrowserToolboxProcess is already loaded, set the boolean to true
+ // and do whatever is needed
+ this._toolboxProcessLoaded = true;
+ BrowserToolboxProcess.on("connectionchange",
+ this.onDebugConnectionChange.bind(this));
+ }
+ else {
+ // Else, wait for it to load
+ Services.obs.addObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false);
+ }
+#endif
+
+ let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion,
+ aOldPlatformVersion);
+
+ // Changes to installed extensions may have changed which theme is selected
+ this.applyThemeChange();
+
+ AddonManagerPrivate.markProviderSafe(this);
+
+ if (aAppChanged === undefined) {
+ // For new profiles we will never need to show the add-on selection UI
+ Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true);
+ }
+ else if (aAppChanged && !this.allAppGlobal &&
+ Preferences.get(PREF_EM_SHOW_MISMATCH_UI, true)) {
+ if (!Preferences.get(PREF_SHOWN_SELECTION_UI, false)) {
+ // Flip a flag to indicate that we interrupted startup with an interactive prompt
+ Services.startup.interrupted = true;
+ // This *must* be modal as it has to block startup.
+ var features = "chrome,centerscreen,dialog,titlebar,modal";
+ Services.ww.openWindow(null, URI_EXTENSION_SELECT_DIALOG, "", features, null);
+ Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true);
+ // Ensure any changes to the add-ons list are flushed to disk
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
+ !XPIDatabase.writeAddonsList());
+ }
+ else {
+ let addonsToUpdate = this.shouldForceUpdateCheck(aAppChanged);
+ if (addonsToUpdate) {
+ this.showUpgradeUI(addonsToUpdate);
+ flushCaches = true;
+ }
+ }
+ }
+
+ if (flushCaches) {
+ Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+ // UI displayed early in startup (like the compatibility UI) may have
+ // caused us to cache parts of the skin or locale in memory. These must
+ // be flushed to allow extension provided skins and locales to take full
+ // effect
+ Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null);
+ Services.obs.notifyObservers(null, "chrome-flush-caches", null);
+ }
+
+ this.enabledAddons = Preferences.get(PREF_EM_ENABLED_ADDONS, "");
+
+ try {
+ for (let id in this.bootstrappedAddons) {
+ try {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = this.bootstrappedAddons[id].descriptor;
+ let reason = BOOTSTRAP_REASONS.APP_STARTUP;
+ // Eventually set INSTALLED reason when a bootstrap addon
+ // is dropped in profile folder and automatically installed
+ if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
+ .indexOf(id) !== -1)
+ reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
+ this.callBootstrapMethod(createAddonDetails(id, this.bootstrappedAddons[id]),
+ file, "startup", reason);
+ }
+ catch (e) {
+ logger.error("Failed to load bootstrap addon " + id + " from " +
+ this.bootstrappedAddons[id].descriptor, e);
+ }
+ }
+ }
+ catch (e) {
+ logger.error("bootstrap startup failed", e);
+ }
+
+ // Let these shutdown a little earlier when they still have access to most
+ // of XPCOM
+ Services.obs.addObserver({
+ observe: function shutdownObserver(aSubject, aTopic, aData) {
+ XPIProvider._closing = true;
+ for (let id in XPIProvider.bootstrappedAddons) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = XPIProvider.bootstrappedAddons[id].descriptor;
+ let addon = createAddonDetails(id, XPIProvider.bootstrappedAddons[id]);
+ XPIProvider.callBootstrapMethod(addon, file, "shutdown",
+ BOOTSTRAP_REASONS.APP_SHUTDOWN);
+ }
+ Services.obs.removeObserver(this, "quit-application-granted");
+ }
+ }, "quit-application-granted", false);
+
+ this.extensionsActive = true;
+ this.runPhase = XPI_BEFORE_UI_STARTUP;
+ }
+ catch (e) {
+ logger.error("startup failed", e);
+ }
+ },
+
+ /**
+ * Shuts down the database and releases all references.
+ * Return: Promise{integer} resolves / rejects with the result of
+ * flushing the XPI Database if it was loaded,
+ * 0 otherwise.
+ */
+ shutdown: function XPI_shutdown() {
+ logger.debug("shutdown");
+
+ // Stop anything we were doing asynchronously
+ this.cancelAll();
+
+ this.bootstrappedAddons = {};
+ this.bootstrapScopes = {};
+ this.enabledAddons = null;
+ this.allAppGlobal = true;
+
+ // If there are pending operations then we must update the list of active
+ // add-ons
+ if (Preferences.get(PREF_PENDING_OPERATIONS, false)) {
+ XPIDatabase.updateActiveAddons();
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
+ !XPIDatabase.writeAddonsList());
+ }
+
+ this.installs = null;
+ this.installLocations = null;
+ this.installLocationsByName = null;
+
+ // This is needed to allow xpcshell tests to simulate a restart
+ this.extensionsActive = false;
+ this._addonFileMap.clear();
+
+ if (gLazyObjectsLoaded) {
+ let done = XPIDatabase.shutdown();
+ done.then(
+ ret => {
+ logger.debug("Notifying XPI shutdown observers");
+ Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
+ },
+ err => {
+ logger.debug("Notifying XPI shutdown observers");
+ this._shutdownError = err;
+ Services.obs.notifyObservers(null, "xpi-provider-shutdown", err);
+ }
+ );
+ return done;
+ }
+ else {
+ logger.debug("Notifying XPI shutdown observers");
+ Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
+ }
+ },
+
+ /**
+ * Applies any pending theme change to the preferences.
+ */
+ applyThemeChange: function XPI_applyThemeChange() {
+ if (!Preferences.get(PREF_DSS_SWITCHPENDING, false))
+ return;
+
+ // Tell the Chrome Registry which Skin to select
+ try {
+ this.selectedSkin = Preferences.get(PREF_DSS_SKIN_TO_SELECT);
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
+ this.selectedSkin);
+ Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
+ logger.debug("Changed skin to " + this.selectedSkin);
+ this.currentSkin = this.selectedSkin;
+ }
+ catch (e) {
+ logger.error("Error applying theme change", e);
+ }
+ Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
+ },
+
+ /**
+ * If the application has been upgraded and there are add-ons outside the
+ * application directory then we may need to synchronize compatibility
+ * information but only if the mismatch UI isn't disabled.
+ *
+ * @returns False if no update check is needed, otherwise an array of add-on
+ * IDs to check for updates. Array may be empty if no add-ons can be/need
+ * to be updated, but the metadata check needs to be performed.
+ */
+ shouldForceUpdateCheck: function XPI_shouldForceUpdateCheck(aAppChanged) {
+ let startupChanges = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+ logger.debug("shouldForceUpdateCheck startupChanges: " + startupChanges.toSource());
+
+ let forceUpdate = [];
+ if (startupChanges.length > 0) {
+ let addons = XPIDatabase.getAddons();
+ for (let addon of addons) {
+ if ((startupChanges.indexOf(addon.id) != -1) &&
+ (addon.permissions() & AddonManager.PERM_CAN_UPGRADE)) {
+ logger.debug("shouldForceUpdateCheck: can upgrade disabled add-on " + addon.id);
+ forceUpdate.push(addon.id);
+ }
+ }
+ }
+
+ if (AddonRepository.isMetadataStale()) {
+ logger.debug("shouldForceUpdateCheck: metadata is stale");
+ return forceUpdate;
+ }
+ if (forceUpdate.length > 0) {
+ return forceUpdate;
+ }
+
+ return false;
+ },
+
+ /**
+ * Shows the "Compatibility Updates" UI.
+ *
+ * @param aAddonIDs
+ * Array opf addon IDs that were disabled by the application update, and
+ * should therefore be checked for updates.
+ */
+ showUpgradeUI: function XPI_showUpgradeUI(aAddonIDs) {
+ logger.debug("XPI_showUpgradeUI: " + aAddonIDs.toSource());
+ // Flip a flag to indicate that we interrupted startup with an interactive prompt
+ Services.startup.interrupted = true;
+
+ var variant = Cc["@mozilla.org/variant;1"].
+ createInstance(Ci.nsIWritableVariant);
+ variant.setFromVariant(aAddonIDs);
+
+ // This *must* be modal as it has to block startup.
+ var features = "chrome,centerscreen,dialog,titlebar,modal";
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
+
+ // Ensure any changes to the add-ons list are flushed to disk
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
+ !XPIDatabase.writeAddonsList());
+ },
+
+ /**
+ * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref).
+ */
+ persistBootstrappedAddons: function XPI_persistBootstrappedAddons() {
+ // Experiments are disabled upon app load, so don't persist references.
+ let filtered = {};
+ for (let id in this.bootstrappedAddons) {
+ let entry = this.bootstrappedAddons[id];
+ if (entry.type == "experiment") {
+ continue;
+ }
+
+ filtered[id] = entry;
+ }
+
+ Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
+ JSON.stringify(filtered));
+ },
+
+ /**
+ * Adds a list of currently active add-ons to the next crash report.
+ */
+ addAddonsToCrashReporter: function XPI_addAddonsToCrashReporter() {
+ if (!("nsICrashReporter" in Ci) ||
+ !(Services.appinfo instanceof Ci.nsICrashReporter))
+ return;
+
+ // In safe mode no add-ons are loaded so we should not include them in the
+ // crash report
+ if (Services.appinfo.inSafeMode)
+ return;
+
+ let data = this.enabledAddons;
+ for (let id in this.bootstrappedAddons) {
+ data += (data ? "," : "") + encodeURIComponent(id) + ":" +
+ encodeURIComponent(this.bootstrappedAddons[id].version);
+ }
+
+ try {
+ Services.appinfo.annotateCrashReport("Add-ons", data);
+ }
+ catch (e) { }
+ },
+
+ /**
+ * Check the staging directories of install locations for any add-ons to be
+ * installed or add-ons to be uninstalled.
+ *
+ * @param aManifests
+ * A dictionary to add detected install manifests to for the purpose
+ * of passing through updated compatibility information
+ * @return true if an add-on was installed or uninstalled
+ */
+ processPendingFileChanges: function XPI_processPendingFileChanges(aManifests) {
+ let changed = false;
+ this.installLocations.forEach(function(aLocation) {
+ aManifests[aLocation.name] = {};
+ // We can't install or uninstall anything in locked locations
+ if (aLocation.locked)
+ return;
+
+ let stagedXPIDir = aLocation.getXPIStagingDir();
+ let stagingDir = aLocation.getStagingDir();
+
+ if (stagedXPIDir.exists() && stagedXPIDir.isDirectory()) {
+ let entries = stagedXPIDir.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (entries.hasMoreElements()) {
+ let stageDirEntry = entries.nextFile;
+
+ if (!stageDirEntry.isDirectory()) {
+ logger.warn("Ignoring file in XPI staging directory: " + stageDirEntry.path);
+ continue;
+ }
+
+ // Find the last added XPI file in the directory
+ let stagedXPI = null;
+ var xpiEntries = stageDirEntry.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (xpiEntries.hasMoreElements()) {
+ let file = xpiEntries.nextFile;
+ if (file.isDirectory())
+ continue;
+
+ let extension = file.leafName;
+ extension = extension.substring(extension.length - 4);
+
+ if (extension != ".xpi" && extension != ".jar")
+ continue;
+
+ stagedXPI = file;
+ }
+ xpiEntries.close();
+
+ if (!stagedXPI)
+ continue;
+
+ let addon = null;
+ try {
+ addon = loadManifestFromZipFile(stagedXPI);
+ }
+ catch (e) {
+ logger.error("Unable to read add-on manifest from " + stagedXPI.path, e);
+ continue;
+ }
+
+ logger.debug("Migrating staged install of " + addon.id + " in " + aLocation.name);
+
+ if (addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) {
+ let targetDir = stagingDir.clone();
+ targetDir.append(addon.id);
+ try {
+ targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ catch (e) {
+ logger.error("Failed to create staging directory for add-on " + addon.id, e);
+ continue;
+ }
+
+ try {
+ ZipUtils.extractFiles(stagedXPI, targetDir);
+ }
+ catch (e) {
+ logger.error("Failed to extract staged XPI for add-on " + addon.id + " in " +
+ aLocation.name, e);
+ }
+ }
+ else {
+ try {
+ stagedXPI.moveTo(stagingDir, addon.id + ".xpi");
+ }
+ catch (e) {
+ logger.error("Failed to move staged XPI for add-on " + addon.id + " in " +
+ aLocation.name, e);
+ }
+ }
+ }
+ entries.close();
+ }
+
+ if (stagedXPIDir.exists()) {
+ try {
+ recursiveRemove(stagedXPIDir);
+ }
+ catch (e) {
+ // Non-critical, just saves some perf on startup if we clean this up.
+ logger.debug("Error removing XPI staging dir " + stagedXPIDir.path, e);
+ }
+ }
+
+ try {
+ if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory())
+ return;
+ }
+ catch (e) {
+ logger.warn("Failed to find staging directory", e);
+ return;
+ }
+
+ let seenFiles = [];
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238), and to remove
+ // normal files before their resource forks on OSX (see bug 733436).
+ let stagingDirEntries = getDirectoryEntries(stagingDir, true);
+ for (let stageDirEntry of stagingDirEntries) {
+ let id = stageDirEntry.leafName;
+
+ let isDir;
+ try {
+ isDir = stageDirEntry.isDirectory();
+ }
+ catch (e if e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
+ // If the file has already gone away then don't worry about it, this
+ // can happen on OSX where the resource fork is automatically moved
+ // with the data fork for the file. See bug 733436.
+ continue;
+ }
+
+ if (!isDir) {
+ if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
+ id = id.substring(0, id.length - 4);
+ }
+ else {
+ if (id.substring(id.length - 5).toLowerCase() != ".json") {
+ logger.warn("Ignoring file: " + stageDirEntry.path);
+ seenFiles.push(stageDirEntry.leafName);
+ }
+ continue;
+ }
+ }
+
+ // Check that the directory's name is a valid ID.
+ if (!gIDTest.test(id)) {
+ logger.warn("Ignoring directory whose name is not a valid add-on ID: " +
+ stageDirEntry.path);
+ seenFiles.push(stageDirEntry.leafName);
+ continue;
+ }
+
+ changed = true;
+
+ if (isDir) {
+ // Check if the directory contains an install manifest.
+ let manifest = stageDirEntry.clone();
+ manifest.append(FILE_INSTALL_MANIFEST);
+
+ // If the install manifest doesn't exist uninstall this add-on in this
+ // install location.
+ if (!manifest.exists()) {
+ logger.debug("Processing uninstall of " + id + " in " + aLocation.name);
+
+ try {
+ let addonFile = aLocation.getLocationForID(id);
+ let addonToUninstall = loadManifestFromFile(addonFile, aLocation);
+ if (addonToUninstall.bootstrap) {
+ this.callBootstrapMethod(addonToUninstall, addonToUninstall._sourceBundle,
+ "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+ }
+ }
+ catch (e) {
+ // If called on startup this may fail due to staged folder still existing.
+ }
+
+ try {
+ aLocation.uninstallAddon(id);
+ seenFiles.push(stageDirEntry.leafName);
+ }
+ catch (e) {
+ logger.error("Failed to uninstall add-on " + id + " in " + aLocation.name, e);
+ }
+ // The file check later will spot the removal and cleanup the database
+ continue;
+ }
+ }
+
+ aManifests[aLocation.name][id] = null;
+ let existingAddonID = id;
+
+ let jsonfile = stagingDir.clone();
+ jsonfile.append(id + ".json");
+
+ try {
+ aManifests[aLocation.name][id] = loadManifestFromFile(stageDirEntry);
+ }
+ catch (e) {
+ logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e);
+ // This add-on can't be installed so just remove it now
+ seenFiles.push(stageDirEntry.leafName);
+ seenFiles.push(jsonfile.leafName);
+ continue;
+ }
+
+ // Check for a cached metadata for this add-on, it may contain updated
+ // compatibility information
+ if (jsonfile.exists()) {
+ logger.debug("Found updated metadata for " + id + " in " + aLocation.name);
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ let json = Cc["@mozilla.org/dom/json;1"].
+ createInstance(Ci.nsIJSON);
+
+ try {
+ fis.init(jsonfile, -1, 0, 0);
+ let metadata = json.decodeFromStream(fis, jsonfile.fileSize);
+ aManifests[aLocation.name][id].importMetadata(metadata);
+ }
+ catch (e) {
+ // If some data can't be recovered from the cached metadata then it
+ // is unlikely to be a problem big enough to justify throwing away
+ // the install, just log and error and continue
+ logger.error("Unable to read metadata from " + jsonfile.path, e);
+ }
+ finally {
+ fis.close();
+ }
+ }
+ seenFiles.push(jsonfile.leafName);
+
+ existingAddonID = aManifests[aLocation.name][id].existingAddonID || id;
+
+ var oldBootstrap = null;
+ logger.debug("Processing install of " + id + " in " + aLocation.name);
+ if (existingAddonID in this.bootstrappedAddons) {
+ try {
+ var existingAddon = aLocation.getLocationForID(existingAddonID);
+ if (this.bootstrappedAddons[existingAddonID].descriptor ==
+ existingAddon.persistentDescriptor) {
+ oldBootstrap = this.bootstrappedAddons[existingAddonID];
+
+ // We'll be replacing a currently active bootstrapped add-on so
+ // call its uninstall method
+ let newVersion = aManifests[aLocation.name][id].version;
+ let oldVersion = oldBootstrap.version;
+ let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
+ BOOTSTRAP_REASONS.ADDON_UPGRADE :
+ BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+
+ this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
+ existingAddon, "uninstall", uninstallReason,
+ { newVersion: newVersion });
+ this.unloadBootstrapScope(existingAddonID);
+ flushChromeCaches();
+ }
+ }
+ catch (e) {
+ }
+ }
+
+ try {
+ var addonInstallLocation = aLocation.installAddon(id, stageDirEntry,
+ existingAddonID);
+ if (aManifests[aLocation.name][id])
+ aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation;
+ }
+ catch (e) {
+ logger.error("Failed to install staged add-on " + id + " in " + aLocation.name,
+ e);
+ // Re-create the staged install
+ AddonInstall.createStagedInstall(aLocation, stageDirEntry,
+ aManifests[aLocation.name][id]);
+ // Make sure not to delete the cached manifest json file
+ seenFiles.pop();
+
+ delete aManifests[aLocation.name][id];
+
+ if (oldBootstrap) {
+ // Re-install the old add-on
+ this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
+ existingAddon, "install",
+ BOOTSTRAP_REASONS.ADDON_INSTALL);
+ }
+ continue;
+ }
+ }
+
+ try {
+ aLocation.cleanStagingDir(seenFiles);
+ }
+ catch (e) {
+ // Non-critical, just saves some perf on startup if we clean this up.
+ logger.debug("Error cleaning staging dir " + stagingDir.path, e);
+ }
+ }, this);
+ return changed;
+ },
+
+ /**
+ * Installs any add-ons located in the extensions directory of the
+ * application's distribution specific directory into the profile unless a
+ * newer version already exists or the user has previously uninstalled the
+ * distributed add-on.
+ *
+ * @param aManifests
+ * A dictionary to add new install manifests to to save having to
+ * reload them later
+ * @return true if any new add-ons were installed
+ */
+ installDistributionAddons: function XPI_installDistributionAddons(aManifests) {
+ let distroDir;
+ try {
+ distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]);
+ }
+ catch (e) {
+ return false;
+ }
+
+ if (!distroDir.exists())
+ return false;
+
+ if (!distroDir.isDirectory())
+ return false;
+
+ let changed = false;
+ let profileLocation = this.installLocationsByName[KEY_APP_PROFILE];
+
+ let entries = distroDir.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entry;
+ while ((entry = entries.nextFile)) {
+
+ let id = entry.leafName;
+
+ if (entry.isFile()) {
+ if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
+ id = id.substring(0, id.length - 4);
+ }
+ else {
+ logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path);
+ continue;
+ }
+ }
+ else if (!entry.isDirectory()) {
+ logger.debug("Ignoring distribution add-on that isn't a file or directory: " +
+ entry.path);
+ continue;
+ }
+
+ if (!gIDTest.test(id)) {
+ logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " +
+ entry.path);
+ continue;
+ }
+
+ let addon;
+ try {
+ addon = loadManifestFromFile(entry);
+ }
+ catch (e) {
+ logger.warn("File entry " + entry.path + " contains an invalid add-on", e);
+ continue;
+ }
+
+ if (addon.id != id) {
+ logger.warn("File entry " + entry.path + " contains an add-on with an " +
+ "incorrect ID")
+ continue;
+ }
+
+ let existingEntry = null;
+ try {
+ existingEntry = profileLocation.getLocationForID(id);
+ }
+ catch (e) {
+ }
+
+ if (existingEntry) {
+ let existingAddon;
+ try {
+ existingAddon = loadManifestFromFile(existingEntry);
+
+ if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
+ continue;
+ }
+ catch (e) {
+ // Bad add-on in the profile so just proceed and install over the top
+ logger.warn("Profile contains an add-on with a bad or missing install " +
+ "manifest at " + existingEntry.path + ", overwriting", e);
+ }
+ }
+ else if (Preferences.get(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
+ continue;
+ }
+
+ // Install the add-on
+ try {
+ profileLocation.installAddon(id, entry, null, true);
+ logger.debug("Installed distribution add-on " + id);
+
+ Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true)
+
+ // aManifests may contain a copy of a newly installed add-on's manifest
+ // and we'll have overwritten that so instead cache our install manifest
+ // which will later be put into the database in processFileChanges
+ if (!(KEY_APP_PROFILE in aManifests))
+ aManifests[KEY_APP_PROFILE] = {};
+ aManifests[KEY_APP_PROFILE][id] = addon;
+ changed = true;
+ }
+ catch (e) {
+ logger.error("Failed to install distribution add-on " + entry.path, e);
+ }
+ }
+
+ entries.close();
+
+ return changed;
+ },
+
+ /**
+ * Compares the add-ons that are currently installed to those that were
+ * known to be installed when the application last ran and applies any
+ * changes found to the database. Also sends "startupcache-invalidate" signal to
+ * observerservice if it detects that data may have changed.
+ * Always called after XPIProviderUtils.js and extensions.json have been loaded.
+ *
+ * @param aManifests
+ * A dictionary of cached AddonInstalls for add-ons that have been
+ * installed
+ * @param aUpdateCompatibility
+ * true to update add-ons appDisabled property when the application
+ * version has changed
+ * @param aOldAppVersion
+ * The version of the application last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @param aOldPlatformVersion
+ * The version of the platform last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @return a boolean indicating if a change requiring flushing the caches was
+ * detected
+ */
+ processFileChanges: function XPI_processFileChanges(aManifests,
+ aUpdateCompatibility,
+ aOldAppVersion,
+ aOldPlatformVersion) {
+ let visibleAddons = {};
+ let oldBootstrappedAddons = this.bootstrappedAddons;
+ this.bootstrappedAddons = {};
+
+ /**
+ * Updates an add-on's metadata and determines if a restart of the
+ * application is necessary. This is called when either the add-on's
+ * install directory path or last modified time has changed.
+ *
+ * @param aInstallLocation
+ * The install location containing the add-on
+ * @param aOldAddon
+ * The AddonInternal as it appeared the last time the application
+ * ran
+ * @param aAddonState
+ * The new state of the add-on
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function updateMetadata(aInstallLocation, aOldAddon, aAddonState) {
+ logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
+
+ // Check if there is an updated install manifest for this add-on
+ let newAddon = aManifests[aInstallLocation.name][aOldAddon.id];
+
+ try {
+ // If not load it
+ if (!newAddon) {
+ let file = aInstallLocation.getLocationForID(aOldAddon.id);
+ newAddon = loadManifestFromFile(file);
+ applyBlocklistChanges(aOldAddon, newAddon);
+
+ // Carry over any pendingUninstall state to add-ons modified directly
+ // in the profile. This is important when the attempt to remove the
+ // add-on in processPendingFileChanges failed and caused an mtime
+ // change to the add-ons files.
+ newAddon.pendingUninstall = aOldAddon.pendingUninstall;
+ }
+
+ // The ID in the manifest that was loaded must match the ID of the old
+ // add-on.
+ if (newAddon.id != aOldAddon.id)
+ throw new Error("Incorrect id in install manifest for existing add-on " + aOldAddon.id);
+ }
+ catch (e) {
+ logger.warn("updateMetadata: Add-on " + aOldAddon.id + " is invalid", e);
+ XPIDatabase.removeAddonMetadata(aOldAddon);
+ XPIStates.removeAddon(aOldAddon.location, aOldAddon.id);
+ if (!aInstallLocation.locked)
+ aInstallLocation.uninstallAddon(aOldAddon.id);
+ else
+ logger.warn("Could not uninstall invalid item from locked install location");
+ // If this was an active add-on then we must force a restart
+ if (aOldAddon.active)
+ return true;
+
+ return false;
+ }
+
+ // Set the additional properties on the new AddonInternal
+ newAddon._installLocation = aInstallLocation;
+ newAddon.updateDate = aAddonState.mtime;
+ newAddon.visible = !(newAddon.id in visibleAddons);
+
+ // Update the database
+ let newDBAddon = XPIDatabase.updateAddonMetadata(aOldAddon, newAddon,
+ aAddonState.descriptor);
+ if (newDBAddon.visible) {
+ visibleAddons[newDBAddon.id] = newDBAddon;
+ // Remember add-ons that were changed during startup
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ newDBAddon.id);
+
+ // If this was the active theme and it is now disabled then enable the
+ // default theme
+ if (aOldAddon.active && newDBAddon.disabled)
+ XPIProvider.enableDefaultTheme();
+
+ // If the new add-on is bootstrapped and active then call its install method
+ if (newDBAddon.active && newDBAddon.bootstrap) {
+ // Startup cache must be flushed before calling the bootstrap script
+ flushChromeCaches();
+
+ let installReason = Services.vc.compare(aOldAddon.version, newDBAddon.version) < 0 ?
+ BOOTSTRAP_REASONS.ADDON_UPGRADE :
+ BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = aAddonState.descriptor;
+ XPIProvider.callBootstrapMethod(newDBAddon, file, "install",
+ installReason, { oldVersion: aOldAddon.version });
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Updates an add-on's descriptor for when the add-on has moved in the
+ * filesystem but hasn't changed in any other way.
+ *
+ * @param aInstallLocation
+ * The install location containing the add-on
+ * @param aOldAddon
+ * The AddonInternal as it appeared the last time the application
+ * ran
+ * @param aAddonState
+ * The new state of the add-on
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function updateDescriptor(aInstallLocation, aOldAddon, aAddonState) {
+ logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.descriptor);
+
+ aOldAddon.descriptor = aAddonState.descriptor;
+ aOldAddon.visible = !(aOldAddon.id in visibleAddons);
+ XPIDatabase.saveChanges();
+
+ if (aOldAddon.visible) {
+ visibleAddons[aOldAddon.id] = aOldAddon;
+
+ if (aOldAddon.bootstrap && aOldAddon.active) {
+ let bootstrap = oldBootstrappedAddons[aOldAddon.id];
+ bootstrap.descriptor = aAddonState.descriptor;
+ XPIProvider.bootstrappedAddons[aOldAddon.id] = bootstrap;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Called when no change has been detected for an add-on's metadata. The
+ * add-on may have become visible due to other add-ons being removed or
+ * the add-on may need to be updated when the application version has
+ * changed.
+ *
+ * @param aInstallLocation
+ * The install location containing the add-on
+ * @param aOldAddon
+ * The AddonInternal as it appeared the last time the application
+ * ran
+ * @param aAddonState
+ * The new state of the add-on
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function updateVisibilityAndCompatibility(aInstallLocation, aOldAddon,
+ aAddonState) {
+ let changed = false;
+
+ // This add-ons metadata has not changed but it may have become visible
+ if (!(aOldAddon.id in visibleAddons)) {
+ visibleAddons[aOldAddon.id] = aOldAddon;
+
+ if (!aOldAddon.visible) {
+ // Remember add-ons that were changed during startup.
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ aOldAddon.id);
+ XPIDatabase.makeAddonVisible(aOldAddon);
+
+ if (aOldAddon.bootstrap) {
+ // The add-on is bootstrappable so call its install script
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = aAddonState.descriptor;
+ XPIProvider.callBootstrapMethod(aOldAddon, file,
+ "install",
+ BOOTSTRAP_REASONS.ADDON_INSTALL);
+
+ // If it should be active then mark it as active otherwise unload
+ // its scope
+ if (!aOldAddon.disabled) {
+ XPIDatabase.updateAddonActive(aOldAddon, true);
+ }
+ else {
+ XPIProvider.unloadBootstrapScope(newAddon.id);
+ }
+ }
+ else {
+ // Otherwise a restart is necessary
+ changed = true;
+ }
+ }
+ }
+
+ // App version changed, we may need to update the appDisabled property.
+ if (aUpdateCompatibility) {
+ let wasDisabled = aOldAddon.disabled;
+ let wasAppDisabled = aOldAddon.appDisabled;
+ let wasUserDisabled = aOldAddon.userDisabled;
+ let wasSoftDisabled = aOldAddon.softDisabled;
+
+ // This updates the addon's JSON cached data in place
+ applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion,
+ aOldPlatformVersion);
+ aOldAddon.appDisabled = !isUsableAddon(aOldAddon);
+
+ let isDisabled = aOldAddon.disabled;
+
+ // If either property has changed update the database.
+ if (wasAppDisabled != aOldAddon.appDisabled ||
+ wasUserDisabled != aOldAddon.userDisabled ||
+ wasSoftDisabled != aOldAddon.softDisabled) {
+ logger.debug("Add-on " + aOldAddon.id + " changed appDisabled state to " +
+ aOldAddon.appDisabled + ", userDisabled state to " +
+ aOldAddon.userDisabled + " and softDisabled state to " +
+ aOldAddon.softDisabled);
+ XPIDatabase.saveChanges();
+ }
+
+ // If this is a visible add-on and it has changed disabled state then we
+ // may need a restart or to update the bootstrap list.
+ if (aOldAddon.visible && wasDisabled != isDisabled) {
+ // Remember add-ons that became disabled or enabled by the application
+ // change
+ let change = isDisabled ? AddonManager.STARTUP_CHANGE_DISABLED
+ : AddonManager.STARTUP_CHANGE_ENABLED;
+ AddonManagerPrivate.addStartupChange(change, aOldAddon.id);
+ if (aOldAddon.bootstrap) {
+ // Update the add-ons active state
+ XPIDatabase.updateAddonActive(aOldAddon, !isDisabled);
+ }
+ else {
+ changed = true;
+ }
+ }
+ }
+
+ if (aOldAddon.visible && aOldAddon.active && aOldAddon.bootstrap) {
+ XPIProvider.bootstrappedAddons[aOldAddon.id] = {
+ version: aOldAddon.version,
+ type: aOldAddon.type,
+ descriptor: aAddonState.descriptor,
+ multiprocessCompatible: aOldAddon.multiprocessCompatible
+ };
+ }
+
+ return changed;
+ }
+
+ /**
+ * Called when an add-on has been removed.
+ *
+ * @param aOldAddon
+ * The AddonInternal as it appeared the last time the application
+ * ran
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function removeMetadata(aOldAddon) {
+ // This add-on has disappeared
+ logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location);
+ XPIDatabase.removeAddonMetadata(aOldAddon);
+
+ // Remember add-ons that were uninstalled during startup
+ if (aOldAddon.visible) {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED,
+ aOldAddon.id);
+ }
+ else if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
+ .indexOf(aOldAddon.id) != -1) {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ aOldAddon.id);
+ }
+
+ if (aOldAddon.active) {
+ // Enable the default theme if the previously active theme has been
+ // removed
+ if (aOldAddon.type == "theme")
+ XPIProvider.enableDefaultTheme();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Called to add the metadata for an add-on in one of the install locations
+ * to the database. This can be called in three different cases. Either an
+ * add-on has been dropped into the location from outside of Firefox, or
+ * an add-on has been installed through the application, or the database
+ * has been upgraded or become corrupt and add-on data has to be reloaded
+ * into it.
+ *
+ * @param aInstallLocation
+ * The install location containing the add-on
+ * @param aId
+ * The ID of the add-on
+ * @param aAddonState
+ * The new state of the add-on
+ * @param aMigrateData
+ * If during startup the database had to be upgraded this will
+ * contain data that used to be held about this add-on
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function addMetadata(aInstallLocation, aId, aAddonState, aMigrateData) {
+ logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name);
+
+ let newAddon = null;
+ let sameVersion = false;
+ // Check the updated manifests lists for the install location, If there
+ // is no manifest for the add-on ID then newAddon will be undefined
+ if (aInstallLocation.name in aManifests)
+ newAddon = aManifests[aInstallLocation.name][aId];
+
+ // If we had staged data for this add-on or we aren't recovering from a
+ // corrupt database and we don't have migration data for this add-on then
+ // this must be a new install.
+ let isNewInstall = (!!newAddon) || (!XPIDatabase.activeBundles && !aMigrateData);
+
+ // If it's a new install and we haven't yet loaded the manifest then it
+ // must be something dropped directly into the install location
+ let isDetectedInstall = isNewInstall && !newAddon;
+
+ // Load the manifest if necessary and sanity check the add-on ID
+ try {
+ if (!newAddon) {
+ // Load the manifest from the add-on.
+ let file = aInstallLocation.getLocationForID(aId);
+ newAddon = loadManifestFromFile(file);
+ }
+ // The add-on in the manifest should match the add-on ID.
+ if (newAddon.id != aId) {
+ throw new Error("Invalid addon ID: expected addon ID " + aId +
+ ", found " + newAddon.id + " in manifest");
+ }
+ }
+ catch (e) {
+ logger.warn("addMetadata: Add-on " + aId + " is invalid", e);
+
+ // Remove the invalid add-on from the install location if the install
+ // location isn't locked, no restart will be necessary
+ if (!aInstallLocation.locked)
+ aInstallLocation.uninstallAddon(aId);
+ else
+ logger.warn("Could not uninstall invalid item from locked install location");
+ return false;
+ }
+
+ // Update the AddonInternal properties.
+ newAddon._installLocation = aInstallLocation;
+ newAddon.visible = !(newAddon.id in visibleAddons);
+ newAddon.installDate = aAddonState.mtime;
+ newAddon.updateDate = aAddonState.mtime;
+ newAddon.foreignInstall = isDetectedInstall;
+
+ if (aMigrateData) {
+ // If there is migration data then apply it.
+ logger.debug("Migrating data from old database");
+
+ DB_MIGRATE_METADATA.forEach(function(aProp) {
+ // A theme's disabled state is determined by the selected theme
+ // preference which is read in loadManifestFromRDF
+ if (aProp == "userDisabled" && newAddon.type == "theme")
+ return;
+
+ if (aProp in aMigrateData)
+ newAddon[aProp] = aMigrateData[aProp];
+ });
+
+ // Force all non-profile add-ons to be foreignInstalls since they can't
+ // have been installed through the API
+ newAddon.foreignInstall |= aInstallLocation.name != KEY_APP_PROFILE;
+
+ // Some properties should only be migrated if the add-on hasn't changed.
+ // The version property isn't a perfect check for this but covers the
+ // vast majority of cases.
+ if (aMigrateData.version == newAddon.version) {
+ logger.debug("Migrating compatibility info");
+ sameVersion = true;
+ if ("targetApplications" in aMigrateData)
+ newAddon.applyCompatibilityUpdate(aMigrateData, true);
+ }
+
+ // Since the DB schema has changed make sure softDisabled is correct
+ applyBlocklistChanges(newAddon, newAddon, aOldAppVersion,
+ aOldPlatformVersion);
+ }
+
+ // The default theme is never a foreign install
+ if (newAddon.type == "theme" && newAddon.internalName == XPIProvider.defaultSkin)
+ newAddon.foreignInstall = false;
+
+ if (isDetectedInstall && newAddon.foreignInstall) {
+ // If the add-on is a foreign install and is in a scope where add-ons
+ // that were dropped in should default to disabled then disable it
+ let disablingScopes = Preferences.get(PREF_EM_AUTO_DISABLED_SCOPES, 0);
+ if (aInstallLocation.scope & disablingScopes) {
+ logger.warn("Disabling foreign installed add-on " + newAddon.id + " in "
+ + aInstallLocation.name);
+ newAddon.userDisabled = true;
+ }
+ }
+
+ // If we have a list of what add-ons should be marked as active then use
+ // it to guess at migration data.
+ if (!isNewInstall && XPIDatabase.activeBundles) {
+ // For themes we know which is active by the current skin setting
+ if (newAddon.type == "theme")
+ newAddon.active = newAddon.internalName == XPIProvider.currentSkin;
+ else
+ newAddon.active = XPIDatabase.activeBundles.indexOf(aAddonState.descriptor) != -1;
+
+ // If the add-on wasn't active and it isn't already disabled in some way
+ // then it was probably either softDisabled or userDisabled
+ if (!newAddon.active && newAddon.visible && !newAddon.disabled) {
+ // If the add-on is softblocked then assume it is softDisabled
+ if (newAddon.blocklistState == Blocklist.STATE_SOFTBLOCKED)
+ newAddon.softDisabled = true;
+ else
+ newAddon.userDisabled = true;
+ }
+ }
+ else {
+ newAddon.active = (newAddon.visible && !newAddon.disabled);
+ }
+
+ let newDBAddon = XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor);
+
+ if (newDBAddon.visible) {
+ // Remember add-ons that were first detected during startup.
+ if (isDetectedInstall) {
+ // If a copy from a higher priority location was removed then this
+ // add-on has changed
+ if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_UNINSTALLED)
+ .indexOf(newDBAddon.id) != -1) {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ newDBAddon.id);
+ }
+ else {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED,
+ newDBAddon.id);
+ }
+ }
+
+ // Note if any visible add-on is not in the application install location
+ if (newDBAddon._installLocation.name != KEY_APP_GLOBAL)
+ XPIProvider.allAppGlobal = false;
+
+ visibleAddons[newDBAddon.id] = newDBAddon;
+
+ let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL;
+ let extraParams = {};
+
+ // Copy add-on details (enabled, bootstrap, version, etc) to XPIState.
+ aAddonState.syncWithDB(newDBAddon);
+
+ // If we're hiding a bootstrapped add-on then call its uninstall method
+ if (newDBAddon.id in oldBootstrappedAddons) {
+ let oldBootstrap = oldBootstrappedAddons[newDBAddon.id];
+ extraParams.oldVersion = oldBootstrap.version;
+ XPIProvider.bootstrappedAddons[newDBAddon.id] = oldBootstrap;
+
+ // If the old version is the same as the new version, or we're
+ // recovering from a corrupt DB, don't call uninstall and install
+ // methods.
+ if (sameVersion || !isNewInstall) {
+ logger.debug("addMetadata: early return, sameVersion " + sameVersion
+ + ", isNewInstall " + isNewInstall);
+ return false;
+ }
+
+ installReason = Services.vc.compare(oldBootstrap.version, newDBAddon.version) < 0 ?
+ BOOTSTRAP_REASONS.ADDON_UPGRADE :
+ BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+
+ let oldAddonFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+ oldAddonFile.persistentDescriptor = oldBootstrap.descriptor;
+
+ XPIProvider.callBootstrapMethod(createAddonDetails(newDBAddon.id, oldBootstrap),
+ oldAddonFile, "uninstall", installReason,
+ { newVersion: newDBAddon.version });
+
+ XPIProvider.unloadBootstrapScope(newDBAddon.id);
+
+ // If the new add-on is bootstrapped then we must flush the caches
+ // before calling the new bootstrap script
+ if (newDBAddon.bootstrap)
+ flushChromeCaches();
+ }
+
+ if (!newDBAddon.bootstrap)
+ return true;
+
+ // Visible bootstrapped add-ons need to have their install method called
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = aAddonState.descriptor;
+ XPIProvider.callBootstrapMethod(newDBAddon, file,
+ "install", installReason, extraParams);
+ if (!newDBAddon.active)
+ XPIProvider.unloadBootstrapScope(newDBAddon.id);
+ }
+
+ return false;
+ }
+
+ let changed = false;
+
+ // Get all the add-ons in the existing DB and Map them into Sets by install location
+ let allDBAddons = new Map();
+ for (let a of XPIDatabase.getAddons()) {
+ let locationSet = allDBAddons.get(a.location);
+ if (!locationSet) {
+ locationSet = new Set();
+ allDBAddons.set(a.location, locationSet);
+ }
+ locationSet.add(a);
+ }
+
+ for (let installLocation of this.installLocations) {
+ // Get all the on-disk XPI states for this location, and keep track of which
+ // ones we see in the database.
+ let states = XPIStates.getLocation(installLocation.name);
+ let seen = new Set();
+ // Iterate through the add-ons installed the last time the application
+ // ran
+ let dbAddons = allDBAddons.get(installLocation.name);
+ if (dbAddons) {
+ // we've processed this location
+ allDBAddons.delete(installLocation.name);
+
+ logger.debug("processFileChanges reconciling DB for location ${l} state ${s} db ${d}",
+ {l: installLocation.name, s: states, d: [for (a of dbAddons) a.id]});
+ for (let aOldAddon of dbAddons) {
+ // If a version of this add-on has been installed in an higher
+ // priority install location then count it as changed
+ if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
+ .indexOf(aOldAddon.id) != -1) {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ aOldAddon.id);
+ }
+
+ // Check if the add-on is still installed
+ let xpiState = states && states.get(aOldAddon.id);
+ if (xpiState) {
+ // in this block, the add-on is in both XPIStates and the DB
+ seen.add(xpiState);
+
+ // The add-on has changed if the modification time has changed, or
+ // we have an updated manifest for it. Also reload the metadata for
+ // add-ons in the application directory when the application version
+ // has changed
+ if (aOldAddon.id in aManifests[installLocation.name] ||
+ aOldAddon.updateDate != xpiState.mtime ||
+ (aUpdateCompatibility && installLocation.name == KEY_APP_GLOBAL)) {
+ changed = updateMetadata(installLocation, aOldAddon, xpiState) ||
+ changed;
+ }
+ else if (aOldAddon.descriptor != xpiState.descriptor) {
+ changed = updateDescriptor(installLocation, aOldAddon, xpiState) ||
+ changed;
+ }
+ else {
+ changed = updateMetadata(installLocation, aOldAddon, xpiState) ||
+ changed;
+
+ changed = updateVisibilityAndCompatibility(installLocation,
+ aOldAddon, xpiState) ||
+ changed;
+ }
+ if (aOldAddon.visible && aOldAddon._installLocation.name != KEY_APP_GLOBAL)
+ XPIProvider.allAppGlobal = false;
+ // Copy add-on details (enabled, bootstrap, version, etc) to XPIState.
+ xpiState.syncWithDB(aOldAddon);
+ }
+ else {
+ // The add-on is in the DB, but not in xpiState (and thus not on disk).
+ changed = removeMetadata(aOldAddon) || changed;
+ }
+ }
+ }
+
+ // Any add-on in our current location that we haven't seen needs to
+ // be added to the database.
+ // Get the migration data for this install location so we can include that as
+ // we add, in case this is a database upgrade or rebuild.
+ let locMigrateData = {};
+ if (XPIDatabase.migrateData && installLocation.name in XPIDatabase.migrateData)
+ locMigrateData = XPIDatabase.migrateData[installLocation.name];
+ if (states) {
+ for (let [id, xpiState] of states) {
+ if (!seen.has(xpiState)) {
+ changed = addMetadata(installLocation, id, xpiState,
+ (locMigrateData[id] || null)) || changed;
+ }
+ }
+ }
+ }
+
+ // Anything left in allDBAddons is a location where the database contains add-ons,
+ // but the browser is no longer configured to use that location. The metadata for those
+ // add-ons must be removed from the database.
+ for (let [locationName, addons] of allDBAddons) {
+ logger.debug("Removing orphaned DB add-on entries from " + locationName);
+ for (let a of addons) {
+ logger.debug("Remove ${location}:${id}", a);
+ changed = removeMetadata(a) || changed;
+ }
+ }
+
+ XPIStates.save();
+ this.persistBootstrappedAddons();
+
+ // Clear out any cached migration data.
+ XPIDatabase.migrateData = null;
+
+ return changed;
+ },
+
+ /**
+ * Imports the xpinstall permissions from preferences into the permissions
+ * manager for the user to change later.
+ */
+ importPermissions: function XPI_importPermissions() {
+ PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH,
+ XPI_PERMISSION);
+ },
+
+ /**
+ * Checks for any changes that have occurred since the last time the
+ * application was launched.
+ *
+ * @param aAppChanged
+ * A tri-state value. Undefined means the current profile was created
+ * for this session, true means the profile already existed but was
+ * last used with an application with a different version number,
+ * false means that the profile was last used by this version of the
+ * application.
+ * @param aOldAppVersion
+ * The version of the application last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @param aOldPlatformVersion
+ * The version of the platform last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @return true if a change requiring a restart was detected
+ */
+ checkForChanges: function XPI_checkForChanges(aAppChanged, aOldAppVersion,
+ aOldPlatformVersion) {
+ logger.debug("checkForChanges");
+
+ // Keep track of whether and why we need to open and update the database at
+ // startup time.
+ let updateReasons = [];
+ if (aAppChanged) {
+ updateReasons.push("appChanged");
+ }
+
+ // Load the list of bootstrapped add-ons first so processFileChanges can
+ // modify it
+ // XXX eventually get rid of bootstrappedAddons
+ try {
+ this.bootstrappedAddons = JSON.parse(Preferences.get(PREF_BOOTSTRAP_ADDONS,
+ "{}"));
+ } catch (e) {
+ logger.warn("Error parsing enabled bootstrapped extensions cache", e);
+ }
+
+ // First install any new add-ons into the locations, if there are any
+ // changes then we must update the database with the information in the
+ // install locations
+ let manifests = {};
+ let updated = this.processPendingFileChanges(manifests);
+ if (updated) {
+ updateReasons.push("pendingFileChanges");
+ }
+
+ // This will be true if the previous session made changes that affect the
+ // active state of add-ons but didn't commit them properly (normally due
+ // to the application crashing)
+ let hasPendingChanges = Preferences.get(PREF_PENDING_OPERATIONS);
+ if (hasPendingChanges) {
+ updateReasons.push("hasPendingChanges");
+ }
+
+ // If the application has changed then check for new distribution add-ons
+ if (aAppChanged !== false &&
+ Preferences.get(PREF_INSTALL_DISTRO_ADDONS, true))
+ {
+ updated = this.installDistributionAddons(manifests);
+ if (updated) {
+ updateReasons.push("installDistributionAddons");
+ }
+ }
+
+ let installChanged = XPIStates.getInstallState();
+ if (installChanged) {
+ updateReasons.push("directoryState");
+ }
+
+ let haveAnyAddons = (XPIStates.size > 0);
+
+ // If the schema appears to have changed then we should update the database
+ if (DB_SCHEMA != Preferences.get(PREF_DB_SCHEMA, 0)) {
+ // If we don't have any add-ons, just update the pref, since we don't need to
+ // write the database
+ if (!haveAnyAddons) {
+ logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA);
+ Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
+ }
+ else {
+ updateReasons.push("schemaChanged");
+ }
+ }
+
+ // If the database doesn't exist and there are add-ons installed then we
+ // must update the database however if there are no add-ons then there is
+ // no need to update the database.
+ let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
+ if (!dbFile.exists() && haveAnyAddons) {
+ updateReasons.push("needNewDatabase");
+ }
+
+ // XXX This will go away when we fold bootstrappedAddons into XPIStates.
+ if (updateReasons.length == 0) {
+ let bootstrapDescriptors = new Set([for (b of Object.keys(this.bootstrappedAddons))
+ this.bootstrappedAddons[b].descriptor]);
+
+ for (let location of XPIStates.db.values()) {
+ for (let state of location.values()) {
+ bootstrapDescriptors.delete(state.descriptor);
+ }
+ }
+
+ if (bootstrapDescriptors.size > 0) {
+ logger.warn("Bootstrap state is invalid (missing add-ons: "
+ + [for (b of bootstrapDescriptors) b] + ")");
+ updateReasons.push("missingBootstrapAddon");
+ }
+ }
+
+ // Catch and log any errors during the main startup
+ try {
+ let extensionListChanged = false;
+ // If the database needs to be updated then open it and then update it
+ // from the filesystem
+ if (updateReasons.length > 0) {
+ XPIDatabase.syncLoadDB(false);
+ try {
+ extensionListChanged = this.processFileChanges(manifests,
+ aAppChanged,
+ aOldAppVersion,
+ aOldPlatformVersion);
+ }
+ catch (e) {
+ logger.error("Failed to process extension changes at startup", e);
+ }
+ }
+
+ if (aAppChanged) {
+ // When upgrading the app and using a custom skin make sure it is still
+ // compatible otherwise switch back the default
+ if (this.currentSkin != this.defaultSkin) {
+ let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin);
+ if (!oldSkin || oldSkin.disabled)
+ this.enableDefaultTheme();
+ }
+
+ // When upgrading remove the old extensions cache to force older
+ // versions to rescan the entire list of extensions
+ try {
+ let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true);
+ if (oldCache.exists())
+ oldCache.remove(true);
+ }
+ catch (e) {
+ logger.warn("Unable to remove old extension cache " + oldCache.path, e);
+ }
+ }
+
+ // If the application crashed before completing any pending operations then
+ // we should perform them now.
+ if (extensionListChanged || hasPendingChanges) {
+ logger.debug("Updating database with changes to installed add-ons");
+ XPIDatabase.updateActiveAddons();
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
+ !XPIDatabase.writeAddonsList());
+ Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
+ JSON.stringify(this.bootstrappedAddons));
+ return true;
+ }
+
+ logger.debug("No changes found");
+ }
+ catch (e) {
+ logger.error("Error during startup file checks", e);
+ }
+
+ // Check that the add-ons list still exists
+ let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
+ true);
+ // the addons list file should exist if and only if we have add-ons installed
+ if (addonsList.exists() != haveAnyAddons) {
+ logger.debug("Add-ons list is invalid, rebuilding");
+ XPIDatabase.writeAddonsList();
+ }
+
+ return false;
+ },
+
+ /**
+ * Called to test whether this provider supports installing a particular
+ * mimetype.
+ *
+ * @param aMimetype
+ * The mimetype to check for
+ * @return true if the mimetype is application/x-xpinstall
+ */
+ supportsMimetype: function XPI_supportsMimetype(aMimetype) {
+ return aMimetype == "application/x-xpinstall";
+ },
+
+ /**
+ * Called to test whether installing XPI add-ons is enabled.
+ *
+ * @return true if installing is enabled
+ */
+ isInstallEnabled: function XPI_isInstallEnabled() {
+ // Default to enabled if the preference does not exist
+ return Preferences.get(PREF_XPI_ENABLED, true);
+ },
+
+ /**
+ * Called to test whether installing XPI add-ons by direct URL requests is
+ * whitelisted.
+ *
+ * @return true if installing by direct requests is whitelisted
+ */
+ isDirectRequestWhitelisted: function XPI_isDirectRequestWhitelisted() {
+ // Default to whitelisted if the preference does not exist.
+ return Preferences.get(PREF_XPI_DIRECT_WHITELISTED, true);
+ },
+
+ /**
+ * Called to test whether installing XPI add-ons from file referrers is
+ * whitelisted.
+ *
+ * @return true if installing from file referrers is whitelisted
+ */
+ isFileRequestWhitelisted: function XPI_isFileRequestWhitelisted() {
+ // Default to whitelisted if the preference does not exist.
+ return Preferences.get(PREF_XPI_FILE_WHITELISTED, true);
+ },
+
+ /**
+ * Called to test whether installing XPI add-ons from a URI is allowed.
+ *
+ * @param aInstallingPrincipal
+ * The nsIPrincipal that initiated the install
+ * @return true if installing is allowed
+ */
+ isInstallAllowed: function XPI_isInstallAllowed(aInstallingPrincipal) {
+ if (!this.isInstallEnabled())
+ return false;
+
+ let uri = aInstallingPrincipal.URI;
+
+ // Direct requests without a referrer are either whitelisted or blocked.
+ if (!uri)
+ return this.isDirectRequestWhitelisted();
+
+ // Local referrers can be whitelisted.
+ if (this.isFileRequestWhitelisted() &&
+ (uri.schemeIs("chrome") || uri.schemeIs("file")))
+ return true;
+
+ this.importPermissions();
+
+ let permission = Services.perms.testPermissionFromPrincipal(aInstallingPrincipal, XPI_PERMISSION);
+ if (permission == Ci.nsIPermissionManager.DENY_ACTION)
+ return false;
+
+ let requireWhitelist = Preferences.get(PREF_XPI_WHITELIST_REQUIRED, true);
+ if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
+ return false;
+
+ let requireSecureOrigin = Preferences.get(PREF_INSTALL_REQUIRESECUREORIGIN, true);
+ let safeSchemes = ["https", "chrome", "file"];
+ if (requireSecureOrigin && safeSchemes.indexOf(uri.scheme) == -1)
+ return false;
+
+ return true;
+ },
+
+ /**
+ * Called to get an AddonInstall to download and install an add-on from a URL.
+ *
+ * @param aUrl
+ * The URL to be installed
+ * @param aHash
+ * A hash for the install
+ * @param aName
+ * A name for the install
+ * @param aIcons
+ * Icon URLs for the install
+ * @param aVersion
+ * A version for the install
+ * @param aBrowser
+ * The browser performing the install
+ * @param aCallback
+ * A callback to pass the AddonInstall to
+ */
+ getInstallForURL: function XPI_getInstallForURL(aUrl, aHash, aName, aIcons,
+ aVersion, aBrowser, aCallback) {
+ AddonInstall.createDownload(function getInstallForURL_createDownload(aInstall) {
+ aCallback(aInstall.wrapper);
+ }, aUrl, aHash, aName, aIcons, aVersion, aBrowser);
+ },
+
+ /**
+ * Called to get an AddonInstall to install an add-on from a local file.
+ *
+ * @param aFile
+ * The file to be installed
+ * @param aCallback
+ * A callback to pass the AddonInstall to
+ */
+ getInstallForFile: function XPI_getInstallForFile(aFile, aCallback) {
+ AddonInstall.createInstall(function getInstallForFile_createInstall(aInstall) {
+ if (aInstall)
+ aCallback(aInstall.wrapper);
+ else
+ aCallback(null);
+ }, aFile);
+ },
+
+ /**
+ * Removes an AddonInstall from the list of active installs.
+ *
+ * @param install
+ * The AddonInstall to remove
+ */
+ removeActiveInstall: function XPI_removeActiveInstall(aInstall) {
+ let where = this.installs.indexOf(aInstall);
+ if (where == -1) {
+ logger.warn("removeActiveInstall: could not find active install for "
+ + aInstall.sourceURI.spec);
+ return;
+ }
+ this.installs.splice(where, 1);
+ },
+
+ /**
+ * Called to get an Addon with a particular ID.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ * @param aCallback
+ * A callback to pass the Addon to
+ */
+ getAddonByID: function XPI_getAddonByID(aId, aCallback) {
+ XPIDatabase.getVisibleAddonForID (aId, function getAddonByID_getVisibleAddonForID(aAddon) {
+ aCallback(createWrapper(aAddon));
+ });
+ },
+
+ /**
+ * Called to get Addons of a particular type.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types.
+ * @param aCallback
+ * A callback to pass an array of Addons to
+ */
+ getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) {
+ XPIDatabase.getVisibleAddons(aTypes, function getAddonsByTypes_getVisibleAddons(aAddons) {
+ // Tycho: aCallback([createWrapper(a) for each (a in aAddons)]);
+ let result = [];
+ for each(let a in aAddons) {
+ result.push(createWrapper(a));
+ }
+ aCallback(result);
+ });
+ },
+
+ /**
+ * Obtain an Addon having the specified Sync GUID.
+ *
+ * @param aGUID
+ * String GUID of add-on to retrieve
+ * @param aCallback
+ * A callback to pass the Addon to. Receives null if not found.
+ */
+ getAddonBySyncGUID: function XPI_getAddonBySyncGUID(aGUID, aCallback) {
+ XPIDatabase.getAddonBySyncGUID(aGUID, function getAddonBySyncGUID_getAddonBySyncGUID(aAddon) {
+ aCallback(createWrapper(aAddon));
+ });
+ },
+
+ /**
+ * Called to get Addons that have pending operations.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types
+ * @param aCallback
+ * A callback to pass an array of Addons to
+ */
+ getAddonsWithOperationsByTypes:
+ function XPI_getAddonsWithOperationsByTypes(aTypes, aCallback) {
+ XPIDatabase.getVisibleAddonsWithPendingOperations(aTypes,
+ function getAddonsWithOpsByTypes_getVisibleAddonsWithPendingOps(aAddons) {
+ // Tycho: let results = [createWrapper(a) for each (a in aAddons)];
+ let results = [];
+ for each(let a in aAddons) {
+ results.push(createWrapper(a));
+ }
+
+ XPIProvider.installs.forEach(function(aInstall) {
+ if (aInstall.state == AddonManager.STATE_INSTALLED &&
+ !(aInstall.addon.inDatabase))
+ results.push(createWrapper(aInstall.addon));
+ });
+ aCallback(results);
+ });
+ },
+
+ /**
+ * Called to get the current AddonInstalls, optionally limiting to a list of
+ * types.
+ *
+ * @param aTypes
+ * An array of types or null to get all types
+ * @param aCallback
+ * A callback to pass the array of AddonInstalls to
+ */
+ getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) {
+ let results = [];
+ this.installs.forEach(function(aInstall) {
+ if (!aTypes || aTypes.indexOf(aInstall.type) >= 0)
+ results.push(aInstall.wrapper);
+ });
+ aCallback(results);
+ },
+
+ /**
+ * Synchronously map a URI to the corresponding Addon ID.
+ *
+ * Mappable URIs are limited to in-application resources belonging to the
+ * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
+ * but do not include URIs from meta data, such as the add-on homepage.
+ *
+ * @param aURI
+ * nsIURI to map or null
+ * @return string containing the Addon ID
+ * @see AddonManager.mapURIToAddonID
+ * @see amIAddonManager.mapURIToAddonID
+ */
+ mapURIToAddonID: function XPI_mapURIToAddonID(aURI) {
+ // Returns `null` instead of empty string if the URI can't be mapped.
+ return AddonPathService.mapURIToAddonId(aURI) || null;
+ },
+
+ /**
+ * Called when a new add-on has been enabled when only one add-on of that type
+ * can be enabled.
+ *
+ * @param aId
+ * The ID of the newly enabled add-on
+ * @param aType
+ * The type of the newly enabled add-on
+ * @param aPendingRestart
+ * true if the newly enabled add-on will only become enabled after a
+ * restart
+ */
+ addonChanged: function XPI_addonChanged(aId, aType, aPendingRestart) {
+ // We only care about themes in this provider
+ if (aType != "theme")
+ return;
+
+ if (!aId) {
+ // Fallback to the default theme when no theme was enabled
+ this.enableDefaultTheme();
+ return;
+ }
+
+ // Look for the previously enabled theme and find the internalName of the
+ // currently selected theme
+ let previousTheme = null;
+ let newSkin = this.defaultSkin;
+ let addons = XPIDatabase.getAddonsByType("theme");
+ addons.forEach(function(aTheme) {
+ if (!aTheme.visible)
+ return;
+ if (aTheme.id == aId)
+ newSkin = aTheme.internalName;
+ else if (aTheme.userDisabled == false && !aTheme.pendingUninstall)
+ previousTheme = aTheme;
+ }, this);
+
+ if (aPendingRestart) {
+ Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true);
+ Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin);
+ }
+ else if (newSkin == this.currentSkin) {
+ try {
+ Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
+ }
+ catch (e) { }
+ try {
+ Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
+ }
+ catch (e) { }
+ }
+ else {
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin);
+ this.currentSkin = newSkin;
+ }
+ this.selectedSkin = newSkin;
+
+ // Flush the preferences to disk so they don't get out of sync with the
+ // database
+ Services.prefs.savePrefFile(null);
+
+ // Mark the previous theme as disabled. This won't cause recursion since
+ // only enabled calls notifyAddonChanged.
+ if (previousTheme)
+ this.updateAddonDisabledState(previousTheme, true);
+ },
+
+ /**
+ * Update the appDisabled property for all add-ons.
+ */
+ updateAddonAppDisabledStates: function XPI_updateAddonAppDisabledStates() {
+ let addons = XPIDatabase.getAddons();
+ addons.forEach(function(aAddon) {
+ this.updateAddonDisabledState(aAddon);
+ }, this);
+ },
+
+ /**
+ * Update the repositoryAddon property for all add-ons.
+ *
+ * @param aCallback
+ * Function to call when operation is complete.
+ */
+ updateAddonRepositoryData: function XPI_updateAddonRepositoryData(aCallback) {
+ let self = this;
+ XPIDatabase.getVisibleAddons(null, function UARD_getVisibleAddonsCallback(aAddons) {
+ let pending = aAddons.length;
+ logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons");
+ if (pending == 0) {
+ aCallback();
+ return;
+ }
+
+ function notifyComplete() {
+ if (--pending == 0)
+ aCallback();
+ }
+
+ for (let addon of aAddons) {
+ AddonRepository.getCachedAddonByID(addon.id,
+ function UARD_getCachedAddonCallback(aRepoAddon) {
+ if (aRepoAddon) {
+ logger.debug("updateAddonRepositoryData got info for " + addon.id);
+ addon._repositoryAddon = aRepoAddon;
+ addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
+ self.updateAddonDisabledState(addon);
+ }
+
+ notifyComplete();
+ });
+ };
+ });
+ },
+
+ /**
+ * When the previously selected theme is removed this method will be called
+ * to enable the default theme.
+ */
+ enableDefaultTheme: function XPI_enableDefaultTheme() {
+ logger.debug("Activating default theme");
+ let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin);
+ if (addon) {
+ if (addon.userDisabled) {
+ this.updateAddonDisabledState(addon, false);
+ }
+ else if (!this.extensionsActive) {
+ // During startup we may end up trying to enable the default theme when
+ // the database thinks it is already enabled (see f.e. bug 638847). In
+ // this case just force the theme preferences to be correct
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
+ addon.internalName);
+ this.currentSkin = this.selectedSkin = addon.internalName;
+ Preferences.reset(PREF_DSS_SKIN_TO_SELECT);
+ Preferences.reset(PREF_DSS_SWITCHPENDING);
+ }
+ else {
+ logger.warn("Attempting to activate an already active default theme");
+ }
+ }
+ else {
+ logger.warn("Unable to activate the default theme");
+ }
+ },
+
+ onDebugConnectionChange: function(aEvent, aWhat, aConnection) {
+ if (aWhat != "opened")
+ return;
+
+ for (let id of Object.keys(this.bootstrapScopes)) {
+ aConnection.setAddonOptions(id, { global: this.bootstrapScopes[id] });
+ }
+ },
+
+ /**
+ * Notified when a preference we're interested in has changed.
+ *
+ * @see nsIObserver
+ */
+ observe: function XPI_observe(aSubject, aTopic, aData) {
+ if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) {
+ if (!aData || aData == XPI_PERMISSION) {
+ this.importPermissions();
+ }
+ return;
+ }
+#ifdef MOZ_DEVTOOLS
+ else if (aTopic == NOTIFICATION_TOOLBOXPROCESS_LOADED) {
+ Services.obs.removeObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false);
+ this._toolboxProcessLoaded = true;
+ BrowserToolboxProcess.on("connectionchange",
+ this.onDebugConnectionChange.bind(this));
+ }
+#endif
+
+ if (aTopic == "nsPref:changed") {
+ switch (aData) {
+ case PREF_EM_MIN_COMPAT_APP_VERSION:
+ case PREF_EM_MIN_COMPAT_PLATFORM_VERSION:
+ this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION,
+ null);
+ this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
+ null);
+ this.updateAddonAppDisabledStates();
+ break;
+ }
+ }
+ },
+
+ /**
+ * Tests whether enabling an add-on will require a restart.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if the operation requires a restart
+ */
+ enableRequiresRestart: function XPI_enableRequiresRestart(aAddon) {
+ // If the platform couldn't have activated extensions then we can make
+ // changes without any restart.
+ if (!this.extensionsActive)
+ return false;
+
+ // If the application is in safe mode then any change can be made without
+ // restarting
+ if (Services.appinfo.inSafeMode)
+ return false;
+
+ // Anything that is active is already enabled
+ if (aAddon.active)
+ return false;
+
+ if (aAddon.type == "theme") {
+ // If dynamic theme switching is enabled then switching themes does not
+ // require a restart
+ if (Preferences.get(PREF_EM_DSS_ENABLED))
+ return false;
+
+ // If the theme is already the theme in use then no restart is necessary.
+ // This covers the case where the default theme is in use but a
+ // lightweight theme is considered active.
+ return aAddon.internalName != this.currentSkin;
+ }
+
+ return !aAddon.bootstrap;
+ },
+
+ /**
+ * Tests whether disabling an add-on will require a restart.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if the operation requires a restart
+ */
+ disableRequiresRestart: function XPI_disableRequiresRestart(aAddon) {
+ // If the platform couldn't have activated up extensions then we can make
+ // changes without any restart.
+ if (!this.extensionsActive)
+ return false;
+
+ // If the application is in safe mode then any change can be made without
+ // restarting
+ if (Services.appinfo.inSafeMode)
+ return false;
+
+ // Anything that isn't active is already disabled
+ if (!aAddon.active)
+ return false;
+
+ if (aAddon.type == "theme") {
+ // If dynamic theme switching is enabled then switching themes does not
+ // require a restart
+ if (Preferences.get(PREF_EM_DSS_ENABLED))
+ return false;
+
+ // Non-default themes always require a restart to disable since it will
+ // be switching from one theme to another or to the default theme and a
+ // lightweight theme.
+ if (aAddon.internalName != this.defaultSkin)
+ return true;
+
+ // The default theme requires a restart to disable if we are in the
+ // process of switching to a different theme. Note that this makes the
+ // disabled flag of operationsRequiringRestart incorrect for the default
+ // theme (it will be false most of the time). Bug 520124 would be required
+ // to fix it. For the UI this isn't a problem since we never try to
+ // disable or uninstall the default theme.
+ return this.selectedSkin != this.currentSkin;
+ }
+
+ return !aAddon.bootstrap;
+ },
+
+ /**
+ * Tests whether installing an add-on will require a restart.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if the operation requires a restart
+ */
+ installRequiresRestart: function XPI_installRequiresRestart(aAddon) {
+ // If the platform couldn't have activated up extensions then we can make
+ // changes without any restart.
+ if (!this.extensionsActive)
+ return false;
+
+ // If the application is in safe mode then any change can be made without
+ // restarting
+ if (Services.appinfo.inSafeMode)
+ return false;
+
+ // Add-ons that are already installed don't require a restart to install.
+ // This wouldn't normally be called for an already installed add-on (except
+ // for forming the operationsRequiringRestart flags) so is really here as
+ // a safety measure.
+ if (aAddon.inDatabase)
+ return false;
+
+ // If we have an AddonInstall for this add-on then we can see if there is
+ // an existing installed add-on with the same ID
+ if ("_install" in aAddon && aAddon._install) {
+ // If there is an existing installed add-on and uninstalling it would
+ // require a restart then installing the update will also require a
+ // restart
+ let existingAddon = aAddon._install.existingAddon;
+ if (existingAddon && this.uninstallRequiresRestart(existingAddon))
+ return true;
+ }
+
+ // If the add-on is not going to be active after installation then it
+ // doesn't require a restart to install.
+ if (aAddon.disabled)
+ return false;
+
+ // Themes will require a restart (even if dynamic switching is enabled due
+ // to some caching issues) and non-bootstrapped add-ons will require a
+ // restart
+ return aAddon.type == "theme" || !aAddon.bootstrap;
+ },
+
+ /**
+ * Tests whether uninstalling an add-on will require a restart.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if the operation requires a restart
+ */
+ uninstallRequiresRestart: function XPI_uninstallRequiresRestart(aAddon) {
+ // If the platform couldn't have activated up extensions then we can make
+ // changes without any restart.
+ if (!this.extensionsActive)
+ return false;
+
+ // If the application is in safe mode then any change can be made without
+ // restarting
+ if (Services.appinfo.inSafeMode)
+ return false;
+
+ // If the add-on can be disabled without a restart then it can also be
+ // uninstalled without a restart
+ return this.disableRequiresRestart(aAddon);
+ },
+
+ /**
+ * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason
+ * values as constants in the scope. This will also add information about the
+ * add-on to the bootstrappedAddons dictionary and notify the crash reporter
+ * that new add-ons have been loaded.
+ *
+ * @param aId
+ * The add-on's ID
+ * @param aFile
+ * The nsIFile for the add-on
+ * @param aVersion
+ * The add-on's version
+ * @param aType
+ * The type for the add-on
+ * @param aMultiprocessCompatible
+ * Boolean indicating whether the add-on is compatible with electrolysis.
+ * @return a JavaScript scope
+ */
+ loadBootstrapScope: function XPI_loadBootstrapScope(aId, aFile, aVersion, aType,
+ aMultiprocessCompatible) {
+ // Mark the add-on as active for the crash reporter before loading
+ this.bootstrappedAddons[aId] = {
+ version: aVersion,
+ type: aType,
+ descriptor: aFile.persistentDescriptor,
+ multiprocessCompatible: aMultiprocessCompatible
+ };
+ this.persistBootstrappedAddons();
+ this.addAddonsToCrashReporter();
+
+ // Locales only contain chrome and can't have bootstrap scripts
+ if (aType == "locale") {
+ this.bootstrapScopes[aId] = null;
+ return;
+ }
+
+ logger.debug("Loading bootstrap scope from " + aFile.path);
+
+ let principal = Cc["@mozilla.org/systemprincipal;1"].
+ createInstance(Ci.nsIPrincipal);
+ if (!aMultiprocessCompatible && Preferences.get(PREF_INTERPOSITION_ENABLED, false)) {
+ let interposition = Cc["@mozilla.org/addons/multiprocess-shims;1"].
+ getService(Ci.nsIAddonInterposition);
+ Cu.setAddonInterposition(aId, interposition);
+ }
+
+ if (!aFile.exists()) {
+ this.bootstrapScopes[aId] =
+ new Cu.Sandbox(principal, { sandboxName: aFile.path,
+ wantGlobalProperties: ["indexedDB"],
+ addonId: aId,
+ metadata: { addonID: aId } });
+ logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path);
+ return;
+ }
+
+ let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec;
+ if (aType == "dictionary")
+ uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js"
+
+ this.bootstrapScopes[aId] =
+ new Cu.Sandbox(principal, { sandboxName: uri,
+ wantGlobalProperties: ["indexedDB"],
+ addonId: aId,
+ metadata: { addonID: aId, URI: uri } });
+
+ let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ createInstance(Ci.mozIJSSubScriptLoader);
+
+ try {
+ // Copy the reason values from the global object into the bootstrap scope.
+ for (let name in BOOTSTRAP_REASONS)
+ this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name];
+
+ // Add other stuff that extensions want.
+ const features = [ "Worker", "ChromeWorker" ];
+
+ for (let feature of features)
+ this.bootstrapScopes[aId][feature] = gGlobalScope[feature];
+
+ // Define a console for the add-on
+ this.bootstrapScopes[aId]["console"] = new ConsoleAPI({ consoleID: "addon/" + aId });
+
+ // As we don't want our caller to control the JS version used for the
+ // bootstrap file, we run loadSubScript within the context of the
+ // sandbox with the latest JS version set explicitly.
+ this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ = uri;
+ Components.utils.evalInSandbox(
+ "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \
+ .createInstance(Components.interfaces.mozIJSSubScriptLoader) \
+ .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5");
+ }
+ catch (e) {
+ logger.warn("Error loading bootstrap.js for " + aId, e);
+ }
+
+#ifdef MOZ_DEVTOOLS
+ // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been
+ // initialized as otherwise, when it will be initialized, all addons'
+ // globals will be added anyways
+ if (this._toolboxProcessLoaded) {
+ BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] });
+ }
+#endif
+ },
+
+ /**
+ * Unloads a bootstrap scope by dropping all references to it and then
+ * updating the list of active add-ons with the crash reporter.
+ *
+ * @param aId
+ * The add-on's ID
+ */
+ unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) {
+ // In case the add-on was not multiprocess-compatible, deregister
+ // any interpositions for it.
+ Cu.setAddonInterposition(aId, null);
+
+ delete this.bootstrapScopes[aId];
+ delete this.bootstrappedAddons[aId];
+ this.persistBootstrappedAddons();
+ this.addAddonsToCrashReporter();
+
+#ifdef MOZ_DEVTOOLS
+ // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been
+ // initialized as otherwise, there won't be any addon globals added to it
+ if (this._toolboxProcessLoaded) {
+ BrowserToolboxProcess.setAddonOptions(aId, { global: null });
+ }
+#endif
+ },
+
+ /**
+ * Calls a bootstrap method for an add-on.
+ *
+ * @param aAddon
+ * An object representing the add-on, with `id`, `type` and `version`
+ * @param aFile
+ * The nsIFile for the add-on
+ * @param aMethod
+ * The name of the bootstrap method to call
+ * @param aReason
+ * The reason flag to pass to the bootstrap's startup method
+ * @param aExtraParams
+ * An object of additional key/value pairs to pass to the method in
+ * the params argument
+ */
+ callBootstrapMethod: function XPI_callBootstrapMethod(aAddon, aFile, aMethod, aReason, aExtraParams) {
+ // Never call any bootstrap methods in safe mode
+ if (Services.appinfo.inSafeMode)
+ return;
+
+ if (!aAddon.id || !aAddon.version || !aAddon.type) {
+ logger.error(new Error("aAddon must include an id, version, and type"));
+ return;
+ }
+
+ let timeStart = new Date();
+ if (aMethod == "startup") {
+ logger.debug("Registering manifest for " + aFile.path);
+ Components.manager.addBootstrappedManifestLocation(aFile);
+ }
+
+ try {
+ // Load the scope if it hasn't already been loaded
+ if (!(aAddon.id in this.bootstrapScopes))
+ this.loadBootstrapScope(aAddon.id, aFile, aAddon.version, aAddon.type,
+ aAddon.multiprocessCompatible || false);
+
+ // Nothing to call for locales
+ if (aAddon.type == "locale")
+ return;
+
+ let method = undefined;
+ try {
+ method = Components.utils.evalInSandbox(`${aMethod};`,
+ this.bootstrapScopes[aAddon.id],
+ "ECMAv5");
+ }
+ catch (e) {
+ // An exception will be caught if the expected method is not defined.
+ // That will be logged below.
+ }
+
+ if (!method) {
+ logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod);
+ return;
+ }
+
+ let params = {
+ id: aAddon.id,
+ version: aAddon.version,
+ installPath: aFile.clone(),
+ resourceURI: getURIForResourceInFile(aFile, "")
+ };
+
+ if (aExtraParams) {
+ for (let key in aExtraParams) {
+ params[key] = aExtraParams[key];
+ }
+ }
+
+ logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " +
+ aAddon.version);
+ try {
+ method(params, aReason);
+ }
+ catch (e) {
+ logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e);
+ }
+ }
+ finally {
+ if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
+ logger.debug("Removing manifest for " + aFile.path);
+ Components.manager.removeBootstrappedManifestLocation(aFile);
+
+ let manifest = getURIForResourceInFile(aFile, "chrome.manifest");
+ for (let line of ChromeManifestParser.parseSync(manifest)) {
+ if (line.type == "resource") {
+ ResProtocolHandler.setSubstitution(line.args[0], null);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the disabled state for an add-on. Its appDisabled property will be
+ * calculated and if the add-on is changed the database will be saved and
+ * appropriate notifications will be sent out to the registered AddonListeners.
+ *
+ * @param aAddon
+ * The DBAddonInternal to update
+ * @param aUserDisabled
+ * Value for the userDisabled property. If undefined the value will
+ * not change
+ * @param aSoftDisabled
+ * Value for the softDisabled property. If undefined the value will
+ * not change. If true this will force userDisabled to be true
+ * @throws if addon is not a DBAddonInternal
+ */
+ updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon,
+ aUserDisabled,
+ aSoftDisabled) {
+ if (!(aAddon.inDatabase))
+ throw new Error("Can only update addon states for installed addons.");
+ if (aUserDisabled !== undefined && aSoftDisabled !== undefined) {
+ throw new Error("Cannot change userDisabled and softDisabled at the " +
+ "same time");
+ }
+
+ if (aUserDisabled === undefined) {
+ aUserDisabled = aAddon.userDisabled;
+ }
+ else if (!aUserDisabled) {
+ // If enabling the add-on then remove softDisabled
+ aSoftDisabled = false;
+ }
+
+ // If not changing softDisabled or the add-on is already userDisabled then
+ // use the existing value for softDisabled
+ if (aSoftDisabled === undefined || aUserDisabled)
+ aSoftDisabled = aAddon.softDisabled;
+
+ let appDisabled = !isUsableAddon(aAddon);
+ // No change means nothing to do here
+ if (aAddon.userDisabled == aUserDisabled &&
+ aAddon.appDisabled == appDisabled &&
+ aAddon.softDisabled == aSoftDisabled)
+ return;
+
+ let wasDisabled = aAddon.disabled;
+ let isDisabled = aUserDisabled || aSoftDisabled || appDisabled;
+
+ // If appDisabled changes but addon.disabled doesn't,
+ // no onDisabling/onEnabling is sent - so send a onPropertyChanged.
+ let appDisabledChanged = aAddon.appDisabled != appDisabled;
+
+ // Update the properties in the database.
+ // We never persist this for experiments because the disabled flags
+ // are controlled by the Experiments Manager.
+ if (aAddon.type != "experiment") {
+ XPIDatabase.setAddonProperties(aAddon, {
+ userDisabled: aUserDisabled,
+ appDisabled: appDisabled,
+ softDisabled: aSoftDisabled
+ });
+ }
+
+ if (appDisabledChanged) {
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged",
+ aAddon,
+ ["appDisabled"]);
+ }
+
+ // If the add-on is not visible or the add-on is not changing state then
+ // there is no need to do anything else
+ if (!aAddon.visible || (wasDisabled == isDisabled))
+ return;
+
+ // Flag that active states in the database need to be updated on shutdown
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+
+ let wrapper = createWrapper(aAddon);
+ // Have we just gone back to the current state?
+ if (isDisabled != aAddon.active) {
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
+ }
+ else {
+ if (isDisabled) {
+ var needsRestart = this.disableRequiresRestart(aAddon);
+ AddonManagerPrivate.callAddonListeners("onDisabling", wrapper,
+ needsRestart);
+ }
+ else {
+ needsRestart = this.enableRequiresRestart(aAddon);
+ AddonManagerPrivate.callAddonListeners("onEnabling", wrapper,
+ needsRestart);
+ }
+
+ if (!needsRestart) {
+ XPIDatabase.updateAddonActive(aAddon, !isDisabled);
+ if (isDisabled) {
+ if (aAddon.bootstrap) {
+ let file = aAddon._installLocation.getLocationForID(aAddon.id);
+ this.callBootstrapMethod(aAddon, file, "shutdown",
+ BOOTSTRAP_REASONS.ADDON_DISABLE);
+ this.unloadBootstrapScope(aAddon.id);
+ }
+ AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
+ }
+ else {
+ if (aAddon.bootstrap) {
+ let file = aAddon._installLocation.getLocationForID(aAddon.id);
+ this.callBootstrapMethod(aAddon, file, "startup",
+ BOOTSTRAP_REASONS.ADDON_ENABLE);
+ }
+ AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
+ }
+ }
+ }
+
+ // Sync with XPIStates.
+ let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
+ if (xpiState) {
+ xpiState.syncWithDB(aAddon);
+ XPIStates.save();
+ } else {
+ // There should always be an xpiState
+ logger.warn("No XPIState for ${id} in ${location}", aAddon);
+ }
+
+ // Notify any other providers that a new theme has been enabled
+ if (aAddon.type == "theme" && !isDisabled)
+ AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart);
+ },
+
+ /**
+ * Uninstalls an add-on, immediately if possible or marks it as pending
+ * uninstall if not.
+ *
+ * @param aAddon
+ * The DBAddonInternal to uninstall
+ * @param aForcePending
+ * Force this addon into the pending uninstall state, even if
+ * it isn't marked as requiring a restart (used e.g. while the
+ * add-on manager is open and offering an "undo" button)
+ * @throws if the addon cannot be uninstalled because it is in an install
+ * location that does not allow it
+ */
+ uninstallAddon: function XPI_uninstallAddon(aAddon, aForcePending) {
+ if (!(aAddon.inDatabase))
+ throw new Error("Cannot uninstall addon " + aAddon.id + " because it is not installed");
+
+ if (aAddon._installLocation.locked)
+ throw new Error("Cannot uninstall addon " + aAddon.id
+ + " from locked install location " + aAddon._installLocation.name);
+
+ // Inactive add-ons don't require a restart to uninstall
+ let requiresRestart = this.uninstallRequiresRestart(aAddon);
+
+ // if makePending is true, we don't actually apply the uninstall,
+ // we just mark the addon as having a pending uninstall
+ let makePending = aForcePending || requiresRestart;
+
+ if (makePending && aAddon.pendingUninstall)
+ throw new Error("Add-on is already marked to be uninstalled");
+
+ if ("_hasResourceCache" in aAddon)
+ aAddon._hasResourceCache = new Map();
+
+ if (aAddon._updateCheck) {
+ logger.debug("Cancel in-progress update check for " + aAddon.id);
+ aAddon._updateCheck.cancel();
+ }
+
+ let wasPending = aAddon.pendingUninstall;
+
+ if (makePending) {
+ // We create an empty directory in the staging directory to indicate
+ // that an uninstall is necessary on next startup.
+ let stage = aAddon._installLocation.getStagingDir();
+ stage.append(aAddon.id);
+ if (!stage.exists())
+ stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ XPIDatabase.setAddonProperties(aAddon, {
+ pendingUninstall: true
+ });
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+ let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
+ if (xpiState) {
+ xpiState.enabled = false;
+ XPIStates.save();
+ } else {
+ logger.warn("Can't find XPI state while uninstalling ${id} from ${location}", aAddon);
+ }
+ }
+
+ // If the add-on is not visible then there is no need to notify listeners.
+ if (!aAddon.visible)
+ return;
+
+ let wrapper = createWrapper(aAddon);
+
+ // If the add-on wasn't already pending uninstall then notify listeners.
+ if (!wasPending) {
+ // Passing makePending as the requiresRestart parameter is a little
+ // strange as in some cases this operation can complete without a restart
+ // so really this is now saying that the uninstall isn't going to happen
+ // immediately but will happen later.
+ AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
+ makePending);
+ }
+
+ // Reveal the highest priority add-on with the same ID
+ function revealAddon(aAddon) {
+ XPIDatabase.makeAddonVisible(aAddon);
+
+ let wrappedAddon = createWrapper(aAddon);
+ AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false);
+
+ if (!aAddon.disabled && !XPIProvider.enableRequiresRestart(aAddon)) {
+ XPIDatabase.updateAddonActive(aAddon, true);
+ }
+
+ if (aAddon.bootstrap) {
+ let file = aAddon._installLocation.getLocationForID(aAddon.id);
+ XPIProvider.callBootstrapMethod(aAddon, file,
+ "install", BOOTSTRAP_REASONS.ADDON_INSTALL);
+
+ if (aAddon.active) {
+ XPIProvider.callBootstrapMethod(aAddon, file,
+ "startup", BOOTSTRAP_REASONS.ADDON_INSTALL);
+ }
+ else {
+ XPIProvider.unloadBootstrapScope(aAddon.id);
+ }
+ }
+
+ // We always send onInstalled even if a restart is required to enable
+ // the revealed add-on
+ AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
+ }
+
+ function findAddonAndReveal(aId) {
+ let [locationName, ] = XPIStates.findAddon(aId);
+ if (locationName) {
+ XPIDatabase.getAddonInLocation(aId, locationName, revealAddon);
+ }
+ }
+
+ if (!makePending) {
+ if (aAddon.bootstrap) {
+ let file = aAddon._installLocation.getLocationForID(aAddon.id);
+ if (aAddon.active) {
+ this.callBootstrapMethod(aAddon, file, "shutdown",
+ BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+ }
+
+ this.callBootstrapMethod(aAddon, file, "uninstall",
+ BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+ this.unloadBootstrapScope(aAddon.id);
+ flushChromeCaches();
+ }
+ aAddon._installLocation.uninstallAddon(aAddon.id);
+ XPIDatabase.removeAddonMetadata(aAddon);
+ XPIStates.removeAddon(aAddon.location, aAddon.id);
+ AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
+
+ findAddonAndReveal(aAddon.id);
+ }
+ else if (aAddon.bootstrap && aAddon.active && !this.disableRequiresRestart(aAddon)) {
+ this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
+ BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+ this.unloadBootstrapScope(aAddon.id);
+ XPIDatabase.updateAddonActive(aAddon, false);
+ }
+
+ // Notify any other providers that a new theme has been enabled
+ if (aAddon.type == "theme" && aAddon.active)
+ AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart);
+ },
+
+ /**
+ * Cancels the pending uninstall of an add-on.
+ *
+ * @param aAddon
+ * The DBAddonInternal to cancel uninstall for
+ */
+ cancelUninstallAddon: function XPI_cancelUninstallAddon(aAddon) {
+ if (!(aAddon.inDatabase))
+ throw new Error("Can only cancel uninstall for installed addons.");
+
+ if (!aAddon.pendingUninstall)
+ throw new Error("Add-on is not marked to be uninstalled");
+
+ aAddon._installLocation.cleanStagingDir([aAddon.id]);
+
+ XPIDatabase.setAddonProperties(aAddon, {
+ pendingUninstall: false
+ });
+
+ if (!aAddon.visible)
+ return;
+
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+
+ // TODO hide hidden add-ons (bug 557710)
+ let wrapper = createWrapper(aAddon);
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
+
+ if (aAddon.bootstrap && !aAddon.disabled && !this.enableRequiresRestart(aAddon)) {
+ this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
+ BOOTSTRAP_REASONS.ADDON_INSTALL);
+ XPIDatabase.updateAddonActive(aAddon, true);
+ }
+
+ // Notify any other providers that this theme is now enabled again.
+ if (aAddon.type == "theme" && aAddon.active)
+ AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
+ }
+};
+
+function getHashStringForCrypto(aCrypto) {
+ // return the two-digit hexadecimal code for a byte
+ let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2);
+
+ // convert the binary hash data to a hex string.
+ let binary = aCrypto.finish(false);
+ let hash = Array.from(binary, c => toHexString(c.charCodeAt(0)))
+ return hash.join("").toLowerCase();
+}
+
+/**
+ * Instantiates an AddonInstall.
+ *
+ * @param aInstallLocation
+ * The install location the add-on will be installed into
+ * @param aUrl
+ * The nsIURL to get the add-on from. If this is an nsIFileURL then
+ * the add-on will not need to be downloaded
+ * @param aHash
+ * An optional hash for the add-on
+ * @param aReleaseNotesURI
+ * An optional nsIURI of release notes for the add-on
+ * @param aExistingAddon
+ * The add-on this install will update if known
+ * @param aBrowser
+ * The browser performing the install
+ * @throws if the url is the url of a local file and the hash does not match
+ * or the add-on does not contain an valid install manifest
+ */
+function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI,
+ aExistingAddon, aBrowser) {
+ this.wrapper = new AddonInstallWrapper(this);
+ this.installLocation = aInstallLocation;
+ this.sourceURI = aUrl;
+ this.releaseNotesURI = aReleaseNotesURI;
+ if (aHash) {
+ let hashSplit = aHash.toLowerCase().split(":");
+ this.originalHash = {
+ algorithm: hashSplit[0],
+ data: hashSplit[1]
+ };
+ }
+ this.hash = this.originalHash;
+ this.browser = aBrowser;
+ this.listeners = [];
+ this.icons = {};
+ this.existingAddon = aExistingAddon;
+ this.error = 0;
+ this.window = aBrowser ? aBrowser.contentWindow : null;
+
+ // Giving each instance of AddonInstall a reference to the logger.
+ this.logger = logger;
+}
+
+AddonInstall.prototype = {
+ installLocation: null,
+ wrapper: null,
+ stream: null,
+ crypto: null,
+ originalHash: null,
+ hash: null,
+ browser: null,
+ badCertHandler: null,
+ listeners: null,
+ restartDownload: false,
+
+ name: null,
+ type: null,
+ version: null,
+ icons: null,
+ releaseNotesURI: null,
+ sourceURI: null,
+ file: null,
+ ownsTempFile: false,
+ certificate: null,
+ certName: null,
+
+ linkedInstalls: null,
+ existingAddon: null,
+ addon: null,
+
+ state: null,
+ error: null,
+ progress: null,
+ maxProgress: null,
+
+ /**
+ * Initialises this install to be a staged install waiting to be applied
+ *
+ * @param aManifest
+ * The cached manifest for the staged install
+ */
+ initStagedInstall: function AI_initStagedInstall(aManifest) {
+ this.name = aManifest.name;
+ this.type = aManifest.type;
+ this.version = aManifest.version;
+ this.icons = aManifest.icons;
+ this.releaseNotesURI = aManifest.releaseNotesURI ?
+ NetUtil.newURI(aManifest.releaseNotesURI) :
+ null
+ this.sourceURI = aManifest.sourceURI ?
+ NetUtil.newURI(aManifest.sourceURI) :
+ null;
+ this.file = null;
+ this.addon = aManifest;
+
+ this.state = AddonManager.STATE_INSTALLED;
+
+ XPIProvider.installs.push(this);
+ },
+
+ /**
+ * Initialises this install to be an install from a local file.
+ *
+ * @param aCallback
+ * The callback to pass the initialised AddonInstall to
+ */
+ initLocalInstall: function AI_initLocalInstall(aCallback) {
+ aCallback = makeSafe(aCallback);
+ this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
+
+ if (!this.file.exists()) {
+ logger.warn("XPI file " + this.file.path + " does not exist");
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_NETWORK_FAILURE;
+ aCallback(this);
+ return;
+ }
+
+ this.state = AddonManager.STATE_DOWNLOADED;
+ this.progress = this.file.fileSize;
+ this.maxProgress = this.file.fileSize;
+
+ if (this.hash) {
+ let crypto = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ try {
+ crypto.initWithString(this.hash.algorithm);
+ }
+ catch (e) {
+ logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_INCORRECT_HASH;
+ aCallback(this);
+ return;
+ }
+
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(this.file, -1, -1, false);
+ crypto.updateFromStream(fis, this.file.fileSize);
+ let calculatedHash = getHashStringForCrypto(crypto);
+ if (calculatedHash != this.hash.data) {
+ logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" +
+ this.hash.data + ")");
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_INCORRECT_HASH;
+ aCallback(this);
+ return;
+ }
+ }
+
+ try {
+ let self = this;
+ this.loadManifest(function initLocalInstall_loadManifest() {
+ XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) {
+ self.existingAddon = aAddon;
+ if (aAddon)
+ applyBlocklistChanges(aAddon, self.addon);
+ self.addon.updateDate = Date.now();
+ self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate;
+
+ if (!self.addon.isCompatible) {
+ // TODO Should we send some event here?
+ self.state = AddonManager.STATE_CHECKING;
+ new UpdateChecker(self.addon, {
+ onUpdateFinished: function updateChecker_onUpdateFinished(aAddon) {
+ self.state = AddonManager.STATE_DOWNLOADED;
+ XPIProvider.installs.push(self);
+ AddonManagerPrivate.callInstallListeners("onNewInstall",
+ self.listeners,
+ self.wrapper);
+
+ aCallback(self);
+ }
+ }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+ }
+ else {
+ XPIProvider.installs.push(self);
+ AddonManagerPrivate.callInstallListeners("onNewInstall",
+ self.listeners,
+ self.wrapper);
+
+ aCallback(self);
+ }
+ });
+ });
+ }
+ catch (e) {
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ if (e.webext) {
+ logger.warn("WebExtension XPI", e);
+ this.error = AddonManager.ERROR_WEBEXT_FILE;
+#ifndef MOZ_JETPACK
+ } else if (e.jetpacksdk) {
+ logger.warn("Jetpack XPI", e);
+ this.error = AddonManager.ERROR_JETPACKSDK_FILE;
+#endif
+ } else {
+ logger.warn("Invalid XPI", e);
+ this.error = AddonManager.ERROR_CORRUPT_FILE;
+ }
+ aCallback(this);
+ return;
+ }
+ },
+
+ /**
+ * Initialises this install to be a download from a remote url.
+ *
+ * @param aCallback
+ * The callback to pass the initialised AddonInstall to
+ * @param aName
+ * An optional name for the add-on
+ * @param aType
+ * An optional type for the add-on
+ * @param aIcons
+ * Optional icons for the add-on
+ * @param aVersion
+ * An optional version for the add-on
+ */
+ initAvailableDownload: function AI_initAvailableDownload(aName, aType, aIcons, aVersion, aCallback) {
+ this.state = AddonManager.STATE_AVAILABLE;
+ this.name = aName;
+ this.type = aType;
+ this.version = aVersion;
+ this.icons = aIcons;
+ this.progress = 0;
+ this.maxProgress = -1;
+
+ XPIProvider.installs.push(this);
+ AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
+ this.wrapper);
+
+ makeSafe(aCallback)(this);
+ },
+
+ /**
+ * Starts installation of this add-on from whatever state it is currently at
+ * if possible.
+ *
+ * @throws if installation cannot proceed from the current state
+ */
+ install: function AI_install() {
+ switch (this.state) {
+ case AddonManager.STATE_AVAILABLE:
+ this.startDownload();
+ break;
+ case AddonManager.STATE_DOWNLOADED:
+ this.startInstall();
+ break;
+ case AddonManager.STATE_DOWNLOAD_FAILED:
+ case AddonManager.STATE_INSTALL_FAILED:
+ case AddonManager.STATE_CANCELLED:
+ this.removeTemporaryFile();
+ this.state = AddonManager.STATE_AVAILABLE;
+ this.error = 0;
+ this.progress = 0;
+ this.maxProgress = -1;
+ this.hash = this.originalHash;
+ XPIProvider.installs.push(this);
+ this.startDownload();
+ break;
+ case AddonManager.STATE_DOWNLOADING:
+ case AddonManager.STATE_CHECKING:
+ case AddonManager.STATE_INSTALLING:
+ // Installation is already running
+ return;
+ default:
+ throw new Error("Cannot start installing from this state");
+ }
+ },
+
+ /**
+ * Cancels installation of this add-on.
+ *
+ * @throws if installation cannot be cancelled from the current state
+ */
+ cancel: function AI_cancel() {
+ switch (this.state) {
+ case AddonManager.STATE_DOWNLOADING:
+ if (this.channel) {
+ logger.debug("Cancelling download of " + this.sourceURI.spec);
+ this.channel.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ break;
+ case AddonManager.STATE_AVAILABLE:
+ case AddonManager.STATE_DOWNLOADED:
+ logger.debug("Cancelling download of " + this.sourceURI.spec);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+ this.listeners, this.wrapper);
+ this.removeTemporaryFile();
+ break;
+ case AddonManager.STATE_INSTALLED:
+ logger.debug("Cancelling install of " + this.addon.id);
+ let xpi = this.installLocation.getStagingDir();
+ xpi.append(this.addon.id + ".xpi");
+ flushJarCache(xpi);
+ this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi",
+ this.addon.id + ".json"]);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+
+ if (this.existingAddon) {
+ delete this.existingAddon.pendingUpgrade;
+ this.existingAddon.pendingUpgrade = null;
+ }
+
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", createWrapper(this.addon));
+
+ AddonManagerPrivate.callInstallListeners("onInstallCancelled",
+ this.listeners, this.wrapper);
+ break;
+ default:
+ throw new Error("Cannot cancel install of " + this.sourceURI.spec +
+ " from this state (" + this.state + ")");
+ }
+ },
+
+ /**
+ * Adds an InstallListener for this instance if the listener is not already
+ * registered.
+ *
+ * @param aListener
+ * The InstallListener to add
+ */
+ addListener: function AI_addListener(aListener) {
+ if (!this.listeners.some(function addListener_matchListener(i) { return i == aListener; }))
+ this.listeners.push(aListener);
+ },
+
+ /**
+ * Removes an InstallListener for this instance if it is registered.
+ *
+ * @param aListener
+ * The InstallListener to remove
+ */
+ removeListener: function AI_removeListener(aListener) {
+ this.listeners = this.listeners.filter(function removeListener_filterListener(i) {
+ return i != aListener;
+ });
+ },
+
+ /**
+ * Removes the temporary file owned by this AddonInstall if there is one.
+ */
+ removeTemporaryFile: function AI_removeTemporaryFile() {
+ // Only proceed if this AddonInstall owns its XPI file
+ if (!this.ownsTempFile) {
+ this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file");
+ return;
+ }
+
+ try {
+ this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " +
+ this.file.path);
+ this.file.remove(true);
+ this.ownsTempFile = false;
+ }
+ catch (e) {
+ this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " +
+ this.sourceURI.spec,
+ e);
+ }
+ },
+
+ /**
+ * Updates the sourceURI and releaseNotesURI values on the Addon being
+ * installed by this AddonInstall instance.
+ */
+ updateAddonURIs: function AI_updateAddonURIs() {
+ this.addon.sourceURI = this.sourceURI.spec;
+ if (this.releaseNotesURI)
+ this.addon.releaseNotesURI = this.releaseNotesURI.spec;
+ },
+
+ /**
+ * Loads add-on manifests from a multi-package XPI file. Each of the
+ * XPI and JAR files contained in the XPI will be extracted. Any that
+ * do not contain valid add-ons will be ignored. The first valid add-on will
+ * be installed by this AddonInstall instance, the rest will have new
+ * AddonInstall instances created for them.
+ *
+ * @param aZipReader
+ * An open nsIZipReader for the multi-package XPI's files. This will
+ * be closed before this method returns.
+ * @param aCallback
+ * A function to call when all of the add-on manifests have been
+ * loaded. Because this loadMultipackageManifests is an internal API
+ * we don't exception-wrap this callback
+ */
+ _loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader,
+ aCallback) {
+ let files = [];
+ let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
+ while (entries.hasMore()) {
+ let entryName = entries.getNext();
+ var target = getTemporaryFile();
+ try {
+ aZipReader.extract(entryName, target);
+ files.push(target);
+ }
+ catch (e) {
+ logger.warn("Failed to extract " + entryName + " from multi-package " +
+ "XPI", e);
+ target.remove(false);
+ }
+ }
+
+ aZipReader.close();
+
+ if (files.length == 0) {
+ throw new Error("Multi-package XPI does not contain any packages " +
+ "to install");
+ }
+
+ let addon = null;
+
+ // Find the first file that has a valid install manifest and use it for
+ // the add-on that this AddonInstall instance will install.
+ while (files.length > 0) {
+ this.removeTemporaryFile();
+ this.file = files.shift();
+ this.ownsTempFile = true;
+ try {
+ addon = loadManifestFromZipFile(this.file);
+ break;
+ }
+ catch (e) {
+ logger.warn(this.file.leafName + " cannot be installed from multi-package " +
+ "XPI", e);
+ }
+ }
+
+ if (!addon) {
+ // No valid add-on was found
+ aCallback();
+ return;
+ }
+
+ this.addon = addon;
+
+ this.updateAddonURIs();
+
+ this.addon._install = this;
+ this.name = this.addon.selectedLocale.name || this.addon.defaultLocale.name;
+ this.type = this.addon.type;
+ this.version = this.addon.version;
+
+ // Setting the iconURL to something inside the XPI locks the XPI and
+ // makes it impossible to delete on Windows.
+ //let newIcon = createWrapper(this.addon).iconURL;
+ //if (newIcon)
+ // this.iconURL = newIcon;
+
+ // Create new AddonInstall instances for every remaining file
+ if (files.length > 0) {
+ this.linkedInstalls = [];
+ let count = 0;
+ let self = this;
+ files.forEach(function(file) {
+ AddonInstall.createInstall(function loadMultipackageManifests_createInstall(aInstall) {
+ // Ignore bad add-ons (createInstall will have logged the error)
+ if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+ // Manually remove the temporary file
+ file.remove(true);
+ }
+ else {
+ // Make the new install own its temporary file
+ aInstall.ownsTempFile = true;
+
+ self.linkedInstalls.push(aInstall)
+
+ aInstall.sourceURI = self.sourceURI;
+ aInstall.releaseNotesURI = self.releaseNotesURI;
+ aInstall.updateAddonURIs();
+ }
+
+ count++;
+ if (count == files.length)
+ aCallback();
+ }, file);
+ }, this);
+ }
+ else {
+ aCallback();
+ }
+ },
+
+ /**
+ * Called after the add-on is a local file and the signature and install
+ * manifest can be read.
+ *
+ * @param aCallback
+ * A function to call when the manifest has been loaded
+ * @throws if the add-on does not contain a valid install manifest or the
+ * XPI is incorrectly signed
+ */
+ loadManifest: function AI_loadManifest(aCallback) {
+ aCallback = makeSafe(aCallback);
+ let self = this;
+ function addRepositoryData(aAddon) {
+ // Try to load from the existing cache first
+ AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
+ if (aRepoAddon) {
+ aAddon._repositoryAddon = aRepoAddon;
+ self.name = self.name || aAddon._repositoryAddon.name;
+ aAddon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
+ aAddon.appDisabled = !isUsableAddon(aAddon);
+ aCallback();
+ return;
+ }
+
+ // It wasn't there so try to re-download it
+ AddonRepository.cacheAddons([aAddon.id], function loadManifest_cacheAddons() {
+ AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
+ aAddon._repositoryAddon = aRepoAddon;
+ self.name = self.name || aAddon._repositoryAddon.name;
+ aAddon.compatibilityOverrides = aRepoAddon ?
+ aRepoAddon.compatibilityOverrides :
+ null;
+ aAddon.appDisabled = !isUsableAddon(aAddon);
+ aCallback();
+ });
+ });
+ });
+ }
+
+ let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ try {
+ zipreader.open(this.file);
+ }
+ catch (e) {
+ zipreader.close();
+ throw e;
+ }
+
+ let x509 = zipreader.getSigningCert(null);
+ if (x509) {
+ logger.debug("Verifying XPI signature");
+ if (verifyZipSigning(zipreader, x509)) {
+ this.certificate = x509;
+ if (this.certificate.commonName.length > 0) {
+ this.certName = this.certificate.commonName;
+ } else {
+ this.certName = this.certificate.organization;
+ }
+ } else {
+ zipreader.close();
+ throw new Error("XPI is incorrectly signed");
+ }
+ }
+
+ try {
+ this.addon = loadManifestFromZipReader(zipreader);
+ }
+ catch (e) {
+ zipreader.close();
+ throw e;
+ }
+
+ if (this.addon.type == "multipackage") {
+ this._loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() {
+ addRepositoryData(self.addon);
+ });
+ return;
+ }
+
+ zipreader.close();
+
+ this.updateAddonURIs();
+
+ this.addon._install = this;
+ this.name = this.addon.selectedLocale.name || this.addon.defaultLocale.name;
+ this.type = this.addon.type;
+ this.version = this.addon.version;
+
+ // Setting the iconURL to something inside the XPI locks the XPI and
+ // makes it impossible to delete on Windows.
+ //let newIcon = createWrapper(this.addon).iconURL;
+ //if (newIcon)
+ // this.iconURL = newIcon;
+
+ addRepositoryData(this.addon);
+ },
+
+ observe: function AI_observe(aSubject, aTopic, aData) {
+ // Network is going offline
+ this.cancel();
+ },
+
+ /**
+ * Starts downloading the add-on's XPI file.
+ */
+ startDownload: function AI_startDownload() {
+ this.state = AddonManager.STATE_DOWNLOADING;
+ if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted",
+ this.listeners, this.wrapper)) {
+ logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+ this.listeners, this.wrapper)
+ return;
+ }
+
+ // If a listener changed our state then do not proceed with the download
+ if (this.state != AddonManager.STATE_DOWNLOADING)
+ return;
+
+ if (this.channel) {
+ // A previous download attempt hasn't finished cleaning up yet, signal
+ // that it should restart when complete
+ logger.debug("Waiting for previous download to complete");
+ this.restartDownload = true;
+ return;
+ }
+
+ this.openChannel();
+ },
+
+ openChannel: function AI_openChannel() {
+ this.restartDownload = false;
+
+ try {
+ this.file = getTemporaryFile();
+ this.ownsTempFile = true;
+ this.stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0);
+ }
+ catch (e) {
+ logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_FILE_ACCESS;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ return;
+ }
+
+ let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
+ createInstance(Ci.nsIStreamListenerTee);
+ listener.init(this, this.stream);
+ try {
+ Components.utils.import("resource://gre/modules/CertUtils.jsm");
+ let requireBuiltIn = Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true);
+ this.badCertHandler = new BadCertHandler(!requireBuiltIn);
+
+ this.channel = NetUtil.newChannel({
+ uri: this.sourceURI,
+ loadUsingSystemPrincipal: true
+ });
+ this.channel.notificationCallbacks = this;
+ if (this.channel instanceof Ci.nsIHttpChannel) {
+ this.channel.setRequestHeader("Moz-XPI-Update", "1", true);
+ if (this.channel instanceof Ci.nsIHttpChannelInternal)
+ this.channel.forceAllowThirdPartyCookie = true;
+ }
+ this.channel.asyncOpen2(listener);
+
+ Services.obs.addObserver(this, "network:offline-about-to-go-offline", false);
+ }
+ catch (e) {
+ logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_NETWORK_FAILURE;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ }
+ },
+
+ /**
+ * Update the crypto hasher with the new data and call the progress listeners.
+ *
+ * @see nsIStreamListener
+ */
+ onDataAvailable: function AI_onDataAvailable(aRequest, aContext, aInputstream,
+ aOffset, aCount) {
+ this.crypto.updateFromStream(aInputstream, aCount);
+ this.progress += aCount;
+ if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress",
+ this.listeners, this.wrapper)) {
+ // TODO cancel the download and make it available again (bug 553024)
+ }
+ },
+
+ /**
+ * Check the redirect response for a hash of the target XPI and verify that
+ * we don't end up on an insecure channel.
+ *
+ * @see nsIChannelEventSink
+ */
+ asyncOnChannelRedirect: function AI_asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) {
+ if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
+ aOldChannel instanceof Ci.nsIHttpChannel) {
+ try {
+ let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
+ let hashSplit = hashStr.toLowerCase().split(":");
+ this.hash = {
+ algorithm: hashSplit[0],
+ data: hashSplit[1]
+ };
+ }
+ catch (e) {
+ }
+ }
+
+ // Verify that we don't end up on an insecure channel if we haven't got a
+ // hash to verify with (see bug 537761 for discussion)
+ if (!this.hash)
+ this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
+ else
+ aCallback.onRedirectVerifyCallback(Cr.NS_OK);
+
+ this.channel = aNewChannel;
+ },
+
+ /**
+ * This is the first chance to get at real headers on the channel.
+ *
+ * @see nsIStreamListener
+ */
+ onStartRequest: function AI_onStartRequest(aRequest, aContext) {
+ this.crypto = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ if (this.hash) {
+ try {
+ this.crypto.initWithString(this.hash.algorithm);
+ }
+ catch (e) {
+ logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_INCORRECT_HASH;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ return;
+ }
+ }
+ else {
+ // We always need something to consume data from the inputstream passed
+ // to onDataAvailable so just create a dummy cryptohasher to do that.
+ this.crypto.initWithString("sha1");
+ }
+
+ this.progress = 0;
+ if (aRequest instanceof Ci.nsIChannel) {
+ try {
+ this.maxProgress = aRequest.contentLength;
+ }
+ catch (e) {
+ }
+ logger.debug("Download started for " + this.sourceURI.spec + " to file " +
+ this.file.path);
+ }
+ },
+
+ /**
+ * The download is complete.
+ *
+ * @see nsIStreamListener
+ */
+ onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) {
+ this.stream.close();
+ this.channel = null;
+ this.badCerthandler = null;
+ Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
+
+ // If the download was cancelled then update the state and send events
+ if (aStatus == Cr.NS_BINDING_ABORTED) {
+ if (this.state == AddonManager.STATE_DOWNLOADING) {
+ logger.debug("Cancelled download of " + this.sourceURI.spec);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+ this.listeners, this.wrapper);
+ // If a listener restarted the download then there is no need to
+ // remove the temporary file
+ if (this.state != AddonManager.STATE_CANCELLED)
+ return;
+ }
+
+ this.removeTemporaryFile();
+ if (this.restartDownload)
+ this.openChannel();
+ return;
+ }
+
+ logger.debug("Download of " + this.sourceURI.spec + " completed.");
+
+ if (Components.isSuccessCode(aStatus)) {
+ if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) {
+ if (!this.hash && (aRequest instanceof Ci.nsIChannel)) {
+ try {
+ checkCert(aRequest,
+ !Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true));
+ }
+ catch (e) {
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
+ return;
+ }
+ }
+
+ // convert the binary hash data to a hex string.
+ let calculatedHash = getHashStringForCrypto(this.crypto);
+ this.crypto = null;
+ if (this.hash && calculatedHash != this.hash.data) {
+ this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
+ "Downloaded file hash (" + calculatedHash +
+ ") did not match provided hash (" + this.hash.data + ")");
+ return;
+ }
+ try {
+ let self = this;
+ this.loadManifest(function onStopRequest_loadManifest() {
+ if (self.addon.isCompatible) {
+ self.downloadCompleted();
+ }
+ else {
+ // TODO Should we send some event here (bug 557716)?
+ self.state = AddonManager.STATE_CHECKING;
+ new UpdateChecker(self.addon, {
+ onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) {
+ self.downloadCompleted();
+ }
+ }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+ }
+ });
+ }
+ catch (e) {
+ if (e.webext) {
+ this.downloadFailed(AddonManager.ERROR_WEBEXT_FILE, e);
+#ifndef MOZ_JETPACK
+ } else if (e.jetpacksdk) {
+ this.downloadFailed(AddonManager.ERROR_JETPACKSDK_FILE, e);
+#endif
+ } else {
+ this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e);
+ }
+ }
+ }
+ else {
+ if (aRequest instanceof Ci.nsIHttpChannel)
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
+ aRequest.responseStatus + " " +
+ aRequest.responseStatusText);
+ else
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
+ }
+ }
+ else {
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
+ }
+ },
+
+ /**
+ * Notify listeners that the download failed.
+ *
+ * @param aReason
+ * Something to log about the failure
+ * @param error
+ * The error code to pass to the listeners
+ */
+ downloadFailed: function AI_downloadFailed(aReason, aError) {
+ logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = aReason;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
+ this.wrapper);
+
+ // If the listener hasn't restarted the download then remove any temporary
+ // file
+ if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+ logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec);
+ this.removeTemporaryFile();
+ }
+ else
+ logger.debug("downloadFailed: listener changed AddonInstall state for " +
+ this.sourceURI.spec + " to " + this.state);
+ },
+
+ /**
+ * Notify listeners that the download completed.
+ */
+ downloadCompleted: function AI_downloadCompleted() {
+ let self = this;
+ XPIDatabase.getVisibleAddonForID(this.addon.id, function downloadCompleted_getVisibleAddonForID(aAddon) {
+ if (aAddon)
+ self.existingAddon = aAddon;
+
+ self.state = AddonManager.STATE_DOWNLOADED;
+ self.addon.updateDate = Date.now();
+
+ if (self.existingAddon) {
+ self.addon.existingAddonID = self.existingAddon.id;
+ self.addon.installDate = self.existingAddon.installDate;
+ applyBlocklistChanges(self.existingAddon, self.addon);
+ }
+ else {
+ self.addon.installDate = self.addon.updateDate;
+ }
+
+ if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
+ self.listeners,
+ self.wrapper)) {
+ // If a listener changed our state then do not proceed with the install
+ if (self.state != AddonManager.STATE_DOWNLOADED)
+ return;
+
+ self.install();
+
+ if (self.linkedInstalls) {
+ self.linkedInstalls.forEach(function(aInstall) {
+ aInstall.install();
+ });
+ }
+ }
+ });
+ },
+
+ // TODO This relies on the assumption that we are always installing into the
+ // highest priority install location so the resulting add-on will be visible
+ // overriding any existing copy in another install location (bug 557710).
+ /**
+ * Installs the add-on into the install location.
+ */
+ startInstall: function AI_startInstall() {
+ this.state = AddonManager.STATE_INSTALLING;
+ if (!AddonManagerPrivate.callInstallListeners("onInstallStarted",
+ this.listeners, this.wrapper)) {
+ this.state = AddonManager.STATE_DOWNLOADED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onInstallCancelled",
+ this.listeners, this.wrapper)
+ return;
+ }
+
+ // Find and cancel any pending installs for the same add-on in the same
+ // install location
+ for (let aInstall of XPIProvider.installs) {
+ if (aInstall.state == AddonManager.STATE_INSTALLED &&
+ aInstall.installLocation == this.installLocation &&
+ aInstall.addon.id == this.addon.id) {
+ logger.debug("Cancelling previous pending install of " + aInstall.addon.id);
+ aInstall.cancel();
+ }
+ }
+
+ let isUpgrade = this.existingAddon &&
+ this.existingAddon._installLocation == this.installLocation;
+ let requiresRestart = XPIProvider.installRequiresRestart(this.addon);
+
+ logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec);
+ AddonManagerPrivate.callAddonListeners("onInstalling",
+ createWrapper(this.addon),
+ requiresRestart);
+
+ let stagingDir = this.installLocation.getStagingDir();
+ let stagedAddon = stagingDir.clone();
+
+ Task.spawn((function() {
+ let installedUnpacked = 0;
+ yield this.installLocation.requestStagingDir();
+
+ // Remove any staged items for this add-on
+ stagedAddon.append(this.addon.id);
+ yield removeAsync(stagedAddon);
+ stagedAddon.leafName = this.addon.id + ".xpi";
+ yield removeAsync(stagedAddon);
+
+ // First stage the file regardless of whether restarting is necessary
+ if (this.addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) {
+ logger.debug("Addon " + this.addon.id + " will be installed as " +
+ "an unpacked directory");
+ stagedAddon.leafName = this.addon.id;
+ yield OS.File.makeDir(stagedAddon.path);
+ yield ZipUtils.extractFilesAsync(this.file, stagedAddon);
+ installedUnpacked = 1;
+ }
+ else {
+ logger.debug("Addon " + this.addon.id + " will be installed as " +
+ "a packed xpi");
+ stagedAddon.leafName = this.addon.id + ".xpi";
+ yield OS.File.copy(this.file.path, stagedAddon.path);
+ }
+
+ if (requiresRestart) {
+ // Point the add-on to its extracted files as the xpi may get deleted
+ this.addon._sourceBundle = stagedAddon;
+
+ // Cache the AddonInternal as it may have updated compatibility info
+ let stagedJSON = stagedAddon.clone();
+ stagedJSON.leafName = this.addon.id + ".json";
+ if (stagedJSON.exists())
+ stagedJSON.remove(true);
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(Ci.nsIConverterOutputStream);
+
+ try {
+ stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
+ 0);
+ converter.init(stream, "UTF-8", 0, 0x0000);
+ converter.writeString(JSON.stringify(this.addon));
+ }
+ finally {
+ converter.close();
+ stream.close();
+ }
+
+ logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
+ this.state = AddonManager.STATE_INSTALLED;
+ if (isUpgrade) {
+ delete this.existingAddon.pendingUpgrade;
+ this.existingAddon.pendingUpgrade = this.addon;
+ }
+ AddonManagerPrivate.callInstallListeners("onInstallEnded",
+ this.listeners, this.wrapper,
+ createWrapper(this.addon));
+ }
+ else {
+ // The install is completed so it should be removed from the active list
+ XPIProvider.removeActiveInstall(this);
+
+ // TODO We can probably reduce the number of DB operations going on here
+ // We probably also want to support rolling back failed upgrades etc.
+ // See bug 553015.
+
+ // Deactivate and remove the old add-on as necessary
+ let reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
+ if (this.existingAddon) {
+ if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0)
+ reason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
+ else
+ reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+
+ if (this.existingAddon.bootstrap) {
+ let file = this.existingAddon._installLocation
+ .getLocationForID(this.existingAddon.id);
+ if (this.existingAddon.active) {
+ XPIProvider.callBootstrapMethod(this.existingAddon, file,
+ "shutdown", reason,
+ { newVersion: this.addon.version });
+ }
+
+ XPIProvider.callBootstrapMethod(this.existingAddon, file,
+ "uninstall", reason,
+ { newVersion: this.addon.version });
+ XPIProvider.unloadBootstrapScope(this.existingAddon.id);
+ flushChromeCaches();
+ }
+
+ if (!isUpgrade && this.existingAddon.active) {
+ XPIDatabase.updateAddonActive(this.existingAddon, false);
+ }
+ }
+
+ // Install the new add-on into its final location
+ let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
+ let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
+ existingAddonID);
+
+ // Update the metadata in the database
+ this.addon._sourceBundle = file;
+ this.addon._installLocation = this.installLocation;
+ this.addon.visible = true;
+
+ if (isUpgrade) {
+ this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
+ file.persistentDescriptor);
+ let state = XPIStates.getAddon(this.installLocation.name, this.addon.id);
+ if (state) {
+ state.syncWithDB(this.addon, true);
+ } else {
+ logger.warn("Unexpected missing XPI state for add-on ${id}", this.addon);
+ }
+ }
+ else {
+ this.addon.active = (this.addon.visible && !this.addon.disabled);
+ this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor);
+ XPIStates.addAddon(this.addon);
+ this.addon.installDate = this.addon.updateDate;
+ XPIDatabase.saveChanges();
+ }
+ XPIStates.save();
+
+ let extraParams = {};
+ if (this.existingAddon) {
+ extraParams.oldVersion = this.existingAddon.version;
+ }
+
+ if (this.addon.bootstrap) {
+ XPIProvider.callBootstrapMethod(this.addon, file, "install",
+ reason, extraParams);
+ }
+
+ AddonManagerPrivate.callAddonListeners("onInstalled",
+ createWrapper(this.addon));
+
+ logger.debug("Install of " + this.sourceURI.spec + " completed.");
+ this.state = AddonManager.STATE_INSTALLED;
+ AddonManagerPrivate.callInstallListeners("onInstallEnded",
+ this.listeners, this.wrapper,
+ createWrapper(this.addon));
+
+ if (this.addon.bootstrap) {
+ if (this.addon.active) {
+ XPIProvider.callBootstrapMethod(this.addon, file, "startup",
+ reason, extraParams);
+ }
+ else {
+ // XXX this makes it dangerous to do some things in onInstallEnded
+ // listeners because important cleanup hasn't been done yet
+ XPIProvider.unloadBootstrapScope(this.addon.id);
+ }
+ }
+ }
+ }).bind(this)).then(null, (e) => {
+ logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec, e);
+ if (stagedAddon.exists())
+ recursiveRemove(stagedAddon);
+ this.state = AddonManager.STATE_INSTALL_FAILED;
+ this.error = AddonManager.ERROR_FILE_ACCESS;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled",
+ createWrapper(this.addon));
+ AddonManagerPrivate.callInstallListeners("onInstallFailed",
+ this.listeners,
+ this.wrapper);
+ }).then(() => {
+ this.removeTemporaryFile();
+ return this.installLocation.releaseStagingDir();
+ });
+ },
+
+ getInterface: function AI_getInterface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ let win = this.window;
+ if (!win && this.browser)
+ win = this.browser.ownerDocument.defaultView;
+
+ let factory = Cc["@mozilla.org/prompter;1"].
+ getService(Ci.nsIPromptFactory);
+ let prompt = factory.getPrompt(win, Ci.nsIAuthPrompt2);
+
+ if (this.browser && this.browser.isRemoteBrowser && prompt instanceof Ci.nsILoginManagerPrompter)
+ prompt.setE10sData(this.browser, null);
+
+ return prompt;
+ }
+ else if (iid.equals(Ci.nsIChannelEventSink)) {
+ return this;
+ }
+
+ return this.badCertHandler.getInterface(iid);
+ }
+}
+
+/**
+ * Creates a new AddonInstall for an already staged install. Used when
+ * installing the staged install failed for some reason.
+ *
+ * @param aDir
+ * The directory holding the staged install
+ * @param aManifest
+ * The cached manifest for the install
+ */
+AddonInstall.createStagedInstall = function AI_createStagedInstall(aInstallLocation, aDir, aManifest) {
+ let url = Services.io.newFileURI(aDir);
+
+ let install = new AddonInstall(aInstallLocation, aDir);
+ install.initStagedInstall(aManifest);
+};
+
+/**
+ * Creates a new AddonInstall to install an add-on from a local file. Installs
+ * always go into the profile install location.
+ *
+ * @param aCallback
+ * The callback to pass the new AddonInstall to
+ * @param aFile
+ * The file to install
+ */
+AddonInstall.createInstall = function AI_createInstall(aCallback, aFile) {
+ let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+ let url = Services.io.newFileURI(aFile);
+
+ try {
+ let install = new AddonInstall(location, url);
+ install.initLocalInstall(aCallback);
+ }
+ catch(e) {
+ logger.error("Error creating install", e);
+ makeSafe(aCallback)(null);
+ }
+};
+
+/**
+ * Creates a new AddonInstall to download and install a URL.
+ *
+ * @param aCallback
+ * The callback to pass the new AddonInstall to
+ * @param aUri
+ * The URI to download
+ * @param aHash
+ * A hash for the add-on
+ * @param aName
+ * A name for the add-on
+ * @param aIcons
+ * An icon URLs for the add-on
+ * @param aVersion
+ * A version for the add-on
+ * @param aBrowser
+ * The browser performing the install
+ */
+AddonInstall.createDownload = function AI_createDownload(aCallback, aUri, aHash, aName, aIcons,
+ aVersion, aBrowser) {
+ let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+ let url = NetUtil.newURI(aUri);
+
+ let install = new AddonInstall(location, url, aHash, null, null, aBrowser);
+ if (url instanceof Ci.nsIFileURL)
+ install.initLocalInstall(aCallback);
+ else
+ install.initAvailableDownload(aName, null, aIcons, aVersion, aCallback);
+};
+
+/**
+ * Creates a new AddonInstall for an update.
+ *
+ * @param aCallback
+ * The callback to pass the new AddonInstall to
+ * @param aAddon
+ * The add-on being updated
+ * @param aUpdate
+ * The metadata about the new version from the update manifest
+ */
+AddonInstall.createUpdate = function AI_createUpdate(aCallback, aAddon, aUpdate) {
+ let url = NetUtil.newURI(aUpdate.updateURL);
+ let releaseNotesURI = null;
+ try {
+ if (aUpdate.updateInfoURL)
+ releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
+ }
+ catch (e) {
+ // If the releaseNotesURI cannot be parsed then just ignore it.
+ }
+
+ let install = new AddonInstall(aAddon._installLocation, url,
+ aUpdate.updateHash, releaseNotesURI, aAddon);
+ if (url instanceof Ci.nsIFileURL) {
+ install.initLocalInstall(aCallback);
+ }
+ else {
+ install.initAvailableDownload(aAddon.selectedLocale.name ?
+ aAddon.selectedLocale.name : aAddon.defaultLocale.name,
+ aAddon.type, aAddon.icons, aUpdate.version, aCallback);
+ }
+};
+
+/**
+ * Creates a wrapper for an AddonInstall that only exposes the public API
+ *
+ * @param install
+ * The AddonInstall to create a wrapper for
+ */
+function AddonInstallWrapper(aInstall) {
+#ifdef MOZ_EM_DEBUG
+ this.__defineGetter__("__AddonInstallInternal__", function AIW_debugGetter() {
+ return aInstall;
+ });
+#endif
+
+ ["name", "type", "version", "icons", "releaseNotesURI", "file", "state", "error",
+ "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AIW_propertyGetter() aInstall[aProp]);
+ }, this);
+
+ this.__defineGetter__("iconURL", function AIW_iconURL() aInstall.icons[32]);
+
+ this.__defineGetter__("existingAddon", function AIW_existingAddonGetter() {
+ return createWrapper(aInstall.existingAddon);
+ });
+ this.__defineGetter__("addon", function AIW_addonGetter() createWrapper(aInstall.addon));
+ this.__defineGetter__("sourceURI", function AIW_sourceURIGetter() aInstall.sourceURI);
+
+ this.__defineGetter__("linkedInstalls", function AIW_linkedInstallsGetter() {
+ if (!aInstall.linkedInstalls)
+ return null;
+ // Tycho: return [i.wrapper for each (i in aInstall.linkedInstalls)];
+ let result = [];
+ for each (let i in aInstall.linkedInstalls) {
+ result.push(i.wrapper);
+ }
+
+ return result;
+ });
+
+ this.install = function AIW_install() {
+ aInstall.install();
+ }
+
+ this.cancel = function AIW_cancel() {
+ aInstall.cancel();
+ }
+
+ this.addListener = function AIW_addListener(listener) {
+ aInstall.addListener(listener);
+ }
+
+ this.removeListener = function AIW_removeListener(listener) {
+ aInstall.removeListener(listener);
+ }
+}
+
+AddonInstallWrapper.prototype = {};
+
+/**
+ * Creates a new update checker.
+ *
+ * @param aAddon
+ * The add-on to check for updates
+ * @param aListener
+ * An UpdateListener to notify of updates
+ * @param aReason
+ * The reason for the update check
+ * @param aAppVersion
+ * An optional application version to check for updates for
+ * @param aPlatformVersion
+ * An optional platform version to check for updates for
+ * @throws if the aListener or aReason arguments are not valid
+ */
+function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) {
+ if (!aListener || !aReason)
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm");
+
+ this.addon = aAddon;
+ aAddon._updateCheck = this;
+ XPIProvider.doing(this);
+ this.listener = aListener;
+ this.appVersion = aAppVersion;
+ this.platformVersion = aPlatformVersion;
+ this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+
+ let updateURL = aAddon.updateURL;
+ if (!updateURL) {
+ updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_URL);
+ }
+
+ const UPDATE_TYPE_COMPATIBILITY = 32;
+ const UPDATE_TYPE_NEWVERSION = 64;
+
+ aReason |= UPDATE_TYPE_COMPATIBILITY;
+ if ("onUpdateAvailable" in this.listener)
+ aReason |= UPDATE_TYPE_NEWVERSION;
+
+ let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion);
+ this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey,
+ url, this);
+}
+
+UpdateChecker.prototype = {
+ addon: null,
+ listener: null,
+ appVersion: null,
+ platformVersion: null,
+ syncCompatibility: null,
+
+ /**
+ * Calls a method on the listener passing any number of arguments and
+ * consuming any exceptions.
+ *
+ * @param aMethod
+ * The method to call on the listener
+ */
+ callListener: function UC_callListener(aMethod, ...aArgs) {
+ if (!(aMethod in this.listener))
+ return;
+
+ try {
+ this.listener[aMethod].apply(this.listener, aArgs);
+ }
+ catch (e) {
+ logger.warn("Exception calling UpdateListener method " + aMethod, e);
+ }
+ },
+
+ /**
+ * Called when AddonUpdateChecker completes the update check
+ *
+ * @param updates
+ * The list of update details for the add-on
+ */
+ onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) {
+ XPIProvider.done(this.addon._updateCheck);
+ this.addon._updateCheck = null;
+ let AUC = AddonUpdateChecker;
+
+ let ignoreMaxVersion = false;
+ let ignoreStrictCompat = false;
+ if (!AddonManager.checkCompatibility) {
+ ignoreMaxVersion = true;
+ ignoreStrictCompat = true;
+ } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES &&
+ !AddonManager.strictCompatibility &&
+ !this.addon.strictCompatibility &&
+ !this.addon.hasBinaryComponents) {
+ ignoreMaxVersion = true;
+ }
+
+ // Always apply any compatibility update for the current version
+ let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
+ this.syncCompatibility,
+ null, null,
+ ignoreMaxVersion,
+ ignoreStrictCompat);
+ // Apply the compatibility update to the database
+ if (compatUpdate)
+ this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility);
+
+ // If the request is for an application or platform version that is
+ // different to the current application or platform version then look for a
+ // compatibility update for those versions.
+ if ((this.appVersion &&
+ Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) ||
+ (this.platformVersion &&
+ Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) {
+ compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
+ false, this.appVersion,
+ this.platformVersion,
+ ignoreMaxVersion,
+ ignoreStrictCompat);
+ }
+
+ if (compatUpdate)
+ this.callListener("onCompatibilityUpdateAvailable", createWrapper(this.addon));
+ else
+ this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
+
+ function sendUpdateAvailableMessages(aSelf, aInstall) {
+ if (aInstall) {
+ aSelf.callListener("onUpdateAvailable", createWrapper(aSelf.addon),
+ aInstall.wrapper);
+ }
+ else {
+ aSelf.callListener("onNoUpdateAvailable", createWrapper(aSelf.addon));
+ }
+ aSelf.callListener("onUpdateFinished", createWrapper(aSelf.addon),
+ AddonManager.UPDATE_STATUS_NO_ERROR);
+ }
+
+ let compatOverrides = AddonManager.strictCompatibility ?
+ null :
+ this.addon.compatibilityOverrides;
+
+ let update = AUC.getNewestCompatibleUpdate(aUpdates,
+ this.appVersion,
+ this.platformVersion,
+ ignoreMaxVersion,
+ ignoreStrictCompat,
+ compatOverrides);
+
+ if (update && Services.vc.compare(this.addon.version, update.version) < 0) {
+ for (let currentInstall of XPIProvider.installs) {
+ // Skip installs that don't match the available update
+ if (currentInstall.existingAddon != this.addon ||
+ currentInstall.version != update.version)
+ continue;
+
+ // If the existing install has not yet started downloading then send an
+ // available update notification. If it is already downloading then
+ // don't send any available update notification
+ if (currentInstall.state == AddonManager.STATE_AVAILABLE) {
+ logger.debug("Found an existing AddonInstall for " + this.addon.id);
+ sendUpdateAvailableMessages(this, currentInstall);
+ }
+ else
+ sendUpdateAvailableMessages(this, null);
+ return;
+ }
+
+ let self = this;
+ AddonInstall.createUpdate(function onUpdateCheckComplete_createUpdate(aInstall) {
+ sendUpdateAvailableMessages(self, aInstall);
+ }, this.addon, update);
+ }
+ else {
+ sendUpdateAvailableMessages(this, null);
+ }
+ },
+
+ /**
+ * Called when AddonUpdateChecker fails the update check
+ *
+ * @param aError
+ * An error status
+ */
+ onUpdateCheckError: function UC_onUpdateCheckError(aError) {
+ XPIProvider.done(this.addon._updateCheck);
+ this.addon._updateCheck = null;
+ this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
+ this.callListener("onNoUpdateAvailable", createWrapper(this.addon));
+ this.callListener("onUpdateFinished", createWrapper(this.addon), aError);
+ },
+
+ /**
+ * Called to cancel an in-progress update check
+ */
+ cancel: function UC_cancel() {
+ let parser = this._parser;
+ if (parser) {
+ this._parser = null;
+ // This will call back to onUpdateCheckError with a CANCELLED error
+ parser.cancel();
+ }
+ }
+};
+
+/**
+ * The AddonInternal is an internal only representation of add-ons. It may
+ * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm)
+ * or an install manifest.
+ */
+function AddonInternal() {
+}
+
+AddonInternal.prototype = {
+ _selectedLocale: null,
+ active: false,
+ visible: false,
+ userDisabled: false,
+ appDisabled: false,
+ softDisabled: false,
+ sourceURI: null,
+ releaseNotesURI: null,
+ foreignInstall: false,
+
+ get selectedLocale() {
+ if (this._selectedLocale)
+ return this._selectedLocale;
+ let locale = findClosestLocale(this.locales);
+ this._selectedLocale = locale ? locale : this.defaultLocale;
+ return this._selectedLocale;
+ },
+
+ get providesUpdatesSecurely() {
+ return !!(this.updateKey || !this.updateURL ||
+ this.updateURL.substring(0, 6) == "https:");
+ },
+
+ get isCompatible() {
+ return this.isCompatibleWith();
+ },
+
+ get disabled() {
+ return (this.userDisabled || this.appDisabled || this.softDisabled);
+ },
+
+ get isPlatformCompatible() {
+ if (this.targetPlatforms.length == 0)
+ return true;
+
+ let matchedOS = false;
+
+ // If any targetPlatform matches the OS and contains an ABI then we will
+ // only match a targetPlatform that contains both the current OS and ABI
+ let needsABI = false;
+
+ // Some platforms do not specify an ABI, test against null in that case.
+ let abi = null;
+ try {
+ abi = Services.appinfo.XPCOMABI;
+ }
+ catch (e) { }
+
+ // Something is causing errors in here
+ try {
+ for (let platform of this.targetPlatforms) {
+ if (platform.os == Services.appinfo.OS) {
+ if (platform.abi) {
+ needsABI = true;
+ if (platform.abi === abi)
+ return true;
+ }
+ else {
+ matchedOS = true;
+ }
+ }
+ }
+ } catch (e) {
+ let message = "Problem with addon " + this.id + " targetPlatforms "
+ + JSON.stringify(this.targetPlatforms);
+ logger.error(message, e);
+ // don't trust this add-on
+ return false;
+ }
+
+ return matchedOS && !needsABI;
+ },
+
+ isCompatibleWith: function AddonInternal_isCompatibleWith(aAppVersion, aPlatformVersion) {
+ // Experiments are installed through an external mechanism that
+ // limits target audience to compatible clients. We trust it knows what
+ // it's doing and skip compatibility checks.
+ //
+ // This decision does forfeit defense in depth. If the experiments system
+ // is ever wrong about targeting an add-on to a specific application
+ // or platform, the client will likely see errors.
+ if (this.type == "experiment") {
+ return true;
+ }
+
+ let app = this.matchingTargetApplication;
+ if (!app)
+ return false;
+
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ let version;
+ if (app.id == Services.appinfo.ID) {
+ version = aAppVersion;
+ }
+ else if (app.id == TOOLKIT_ID) {
+ version = aPlatformVersion;
+ }
+
+ // Only extensions and dictionaries can be compatible by default; themes
+ // and language packs always use strict compatibility checking.
+ if (this.type in COMPATIBLE_BY_DEFAULT_TYPES &&
+ !AddonManager.strictCompatibility && !this.strictCompatibility &&
+ !this.hasBinaryComponents) {
+
+ // The repository can specify compatibility overrides.
+ // Note: For now, only blacklisting is supported by overrides.
+ if (this._repositoryAddon &&
+ this._repositoryAddon.compatibilityOverrides) {
+ let overrides = this._repositoryAddon.compatibilityOverrides;
+ let override = AddonRepository.findMatchingCompatOverride(this.version,
+ overrides);
+ if (override && override.type == "incompatible")
+ return false;
+ }
+
+ // Extremely old extensions should not be compatible by default.
+ let minCompatVersion;
+ if (app.id == Services.appinfo.ID)
+ minCompatVersion = XPIProvider.minCompatibleAppVersion;
+ else if (app.id == TOOLKIT_ID)
+ minCompatVersion = XPIProvider.minCompatiblePlatformVersion;
+
+ if (minCompatVersion &&
+ Services.vc.compare(minCompatVersion, app.maxVersion) > 0)
+ return false;
+
+ return Services.vc.compare(version, app.minVersion) >= 0;
+ }
+
+ return (Services.vc.compare(version, app.minVersion) >= 0) &&
+ (Services.vc.compare(version, app.maxVersion) <= 0)
+ },
+
+ get matchingTargetApplication() {
+ let app = null;
+ for (let targetApp of this.targetApplications) {
+ if (targetApp.id == Services.appinfo.ID)
+ return targetApp;
+ if (targetApp.id == TOOLKIT_ID)
+ app = targetApp;
+ }
+ // Return toolkit ID if toolkit.
+ return app;
+ },
+
+ get blocklistState() {
+ let staticItem = findMatchingStaticBlocklistItem(this);
+ if (staticItem)
+ return staticItem.level;
+
+ return Blocklist.getAddonBlocklistState(createWrapper(this));
+ },
+
+ get blocklistURL() {
+ let staticItem = findMatchingStaticBlocklistItem(this);
+ if (staticItem) {
+ let url = Services.urlFormatter.formatURLPref("extensions.blocklist.itemURL");
+ return url.replace(/%blockID%/g, staticItem.blockID);
+ }
+
+ return Blocklist.getAddonBlocklistURL(createWrapper(this));
+ },
+
+ applyCompatibilityUpdate: function AddonInternal_applyCompatibilityUpdate(aUpdate, aSyncCompatibility) {
+ if (this.strictCompatibility) {
+ return;
+ }
+ this.targetApplications.forEach(function(aTargetApp) {
+ aUpdate.targetApplications.forEach(function(aUpdateTarget) {
+ if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
+ Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
+ aTargetApp.minVersion = aUpdateTarget.minVersion;
+ aTargetApp.maxVersion = aUpdateTarget.maxVersion;
+ }
+ });
+ });
+ if (aUpdate.multiprocessCompatible !== undefined)
+ this.multiprocessCompatible = aUpdate.multiprocessCompatible;
+ this.appDisabled = !isUsableAddon(this);
+ },
+
+ /**
+ * getDataDirectory tries to execute the callback with two arguments:
+ * 1) the path of the data directory within the profile,
+ * 2) any exception generated from trying to build it.
+ */
+ getDataDirectory: function(callback) {
+ let parentPath = OS.Path.join(OS.Constants.Path.profileDir, "extension-data");
+ let dirPath = OS.Path.join(parentPath, this.id);
+
+ Task.spawn(function*() {
+ yield OS.File.makeDir(parentPath, {ignoreExisting: true});
+ yield OS.File.makeDir(dirPath, {ignoreExisting: true});
+ }).then(() => callback(dirPath, null),
+ e => callback(dirPath, e));
+ },
+
+ /**
+ * toJSON is called by JSON.stringify in order to create a filtered version
+ * of this object to be serialized to a JSON file. A new object is returned
+ * with copies of all non-private properties. Functions, getters and setters
+ * are not copied.
+ *
+ * @param aKey
+ * The key that this object is being serialized as in the JSON.
+ * Unused here since this is always the main object serialized
+ *
+ * @return an object containing copies of the properties of this object
+ * ignoring private properties, functions, getters and setters
+ */
+ toJSON: function AddonInternal_toJSON(aKey) {
+ let obj = {};
+ for (let prop in this) {
+ // Ignore private properties
+ if (prop.substring(0, 1) == "_")
+ continue;
+
+ // Ignore getters
+ if (this.__lookupGetter__(prop))
+ continue;
+
+ // Ignore setters
+ if (this.__lookupSetter__(prop))
+ continue;
+
+ // Ignore functions
+ if (typeof this[prop] == "function")
+ continue;
+
+ obj[prop] = this[prop];
+ }
+
+ return obj;
+ },
+
+ /**
+ * When an add-on install is pending its metadata will be cached in a file.
+ * This method reads particular properties of that metadata that may be newer
+ * than that in the install manifest, like compatibility information.
+ *
+ * @param aObj
+ * A JS object containing the cached metadata
+ */
+ importMetadata: function AddonInternal_importMetaData(aObj) {
+ PENDING_INSTALL_METADATA.forEach(function(aProp) {
+ if (!(aProp in aObj))
+ return;
+
+ this[aProp] = aObj[aProp];
+ }, this);
+
+ // Compatibility info may have changed so update appDisabled
+ this.appDisabled = !isUsableAddon(this);
+ },
+
+ permissions: function AddonInternal_permissions() {
+ let permissions = 0;
+
+ // Add-ons that aren't installed cannot be modified in any way
+ if (!(this.inDatabase))
+ return permissions;
+
+ // Experiments can only be uninstalled. An uninstall reflects the user
+ // intent of "disable this experiment." This is partially managed by the
+ // experiments manager.
+ if (this.type == "experiment") {
+ return AddonManager.PERM_CAN_UNINSTALL;
+ }
+
+ if (!this.appDisabled) {
+ if (this.userDisabled || this.softDisabled) {
+ permissions |= AddonManager.PERM_CAN_ENABLE;
+ }
+ else if (this.type != "theme") {
+ permissions |= AddonManager.PERM_CAN_DISABLE;
+ }
+ }
+
+ // Add-ons that are in locked install locations, or are pending uninstall
+ // cannot be upgraded or uninstalled
+ if (!this._installLocation.locked && !this.pendingUninstall) {
+ // Add-ons that are installed by a file link cannot be upgraded
+ if (!this._installLocation.isLinkedAddon(this.id)) {
+ permissions |= AddonManager.PERM_CAN_UPGRADE;
+ }
+
+ permissions |= AddonManager.PERM_CAN_UNINSTALL;
+ }
+
+ return permissions;
+ },
+};
+
+/**
+ * Creates an AddonWrapper for an AddonInternal.
+ *
+ * @param addon
+ * The AddonInternal to wrap
+ * @return an AddonWrapper or null if addon was null
+ */
+function createWrapper(aAddon) {
+ if (!aAddon)
+ return null;
+ if (!aAddon._wrapper) {
+ aAddon._hasResourceCache = new Map();
+ aAddon._wrapper = new AddonWrapper(aAddon);
+ }
+ return aAddon._wrapper;
+}
+
+/**
+ * The AddonWrapper wraps an Addon to provide the data visible to consumers of
+ * the public API.
+ */
+function AddonWrapper(aAddon) {
+#ifdef MOZ_EM_DEBUG
+ this.__defineGetter__("__AddonInternal__", function AW_debugGetter() {
+ return aAddon;
+ });
+#endif
+
+ function chooseValue(aObj, aProp) {
+ let repositoryAddon = aAddon._repositoryAddon;
+ let objValue = aObj[aProp];
+
+ if (repositoryAddon && (aProp in repositoryAddon) &&
+ (objValue === undefined || objValue === null)) {
+ return [repositoryAddon[aProp], true];
+ }
+
+ return [objValue, false];
+ }
+
+ ["id", "syncGUID", "version", "type", "isCompatible", "isPlatformCompatible",
+ "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
+ "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents",
+ "strictCompatibility", "compatibilityOverrides", "updateURL",
+ "getDataDirectory", "multiprocessCompatible", "native"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]);
+ }, this);
+
+ ["fullDescription", "developerComments", "eula", "supportURL",
+ "contributionURL", "contributionAmount", "averageRating", "reviewCount",
+ "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers",
+ "repositoryStatus"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_repoPropertyGetter() {
+ if (aAddon._repositoryAddon)
+ return aAddon._repositoryAddon[aProp];
+
+ return null;
+ });
+ }, this);
+
+ this.__defineGetter__("aboutURL", function AddonWrapper_aboutURLGetter() {
+ return this.isActive ? aAddon["aboutURL"] : null;
+ });
+
+ ["installDate", "updateDate"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_datePropertyGetter() new Date(aAddon[aProp]));
+ }, this);
+
+ ["sourceURI", "releaseNotesURI"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_URIPropertyGetter() {
+ let [target, fromRepo] = chooseValue(aAddon, aProp);
+ if (!target)
+ return null;
+ if (fromRepo)
+ return target;
+ return NetUtil.newURI(target);
+ });
+ }, this);
+
+ this.__defineGetter__("optionsURL", function AddonWrapper_optionsURLGetter() {
+ if (this.isActive && aAddon.optionsURL)
+ return aAddon.optionsURL;
+
+ if (this.isActive && this.hasResource("options.xul"))
+ return this.getResourceURI("options.xul").spec;
+
+ return null;
+ }, this);
+
+ this.__defineGetter__("optionsType", function AddonWrapper_optionsTypeGetter() {
+ if (!this.isActive)
+ return null;
+
+ let hasOptionsXUL = this.hasResource("options.xul");
+ let hasOptionsURL = !!this.optionsURL;
+
+ if (aAddon.optionsType) {
+ switch (parseInt(aAddon.optionsType, 10)) {
+ case AddonManager.OPTIONS_TYPE_DIALOG:
+ case AddonManager.OPTIONS_TYPE_TAB:
+ return hasOptionsURL ? aAddon.optionsType : null;
+ case AddonManager.OPTIONS_TYPE_INLINE:
+ case AddonManager.OPTIONS_TYPE_INLINE_INFO:
+ return (hasOptionsXUL || hasOptionsURL) ? aAddon.optionsType : null;
+ }
+ return null;
+ }
+
+ if (hasOptionsXUL)
+ return AddonManager.OPTIONS_TYPE_INLINE;
+
+ if (hasOptionsURL)
+ return AddonManager.OPTIONS_TYPE_DIALOG;
+
+ return null;
+ }, this);
+
+ this.__defineGetter__("iconURL", function AddonWrapper_iconURLGetter() {
+ return this.icons[32] || undefined;
+ }, this);
+
+ this.__defineGetter__("icon64URL", function AddonWrapper_icon64URLGetter() {
+ return this.icons[64] || undefined;
+ }, this);
+
+ this.__defineGetter__("icons", function AddonWrapper_iconsGetter() {
+ let icons = {};
+ if (aAddon._repositoryAddon) {
+ for (let size in aAddon._repositoryAddon.icons) {
+ icons[size] = aAddon._repositoryAddon.icons[size];
+ }
+ }
+ if (this.isActive && aAddon.iconURL) {
+ icons[32] = aAddon.iconURL;
+ } else if (this.hasResource("icon.png")) {
+ icons[32] = this.getResourceURI("icon.png").spec;
+ }
+ if (this.isActive && aAddon.icon64URL) {
+ icons[64] = aAddon.icon64URL;
+ } else if (this.hasResource("icon64.png")) {
+ icons[64] = this.getResourceURI("icon64.png").spec;
+ }
+ Object.freeze(icons);
+ return icons;
+ }, this);
+
+ PROP_LOCALE_SINGLE.forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_singleLocaleGetter() {
+ // Override XPI creator if repository creator is defined
+ if (aProp == "creator" &&
+ aAddon._repositoryAddon && aAddon._repositoryAddon.creator) {
+ return aAddon._repositoryAddon.creator;
+ }
+
+ let result = null;
+
+ if (aAddon.active) {
+ try {
+ let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + aProp;
+ let value = Preferences.get(pref, null, Ci.nsIPrefLocalizedString);
+ if (value)
+ result = value;
+ }
+ catch (e) {
+ }
+ }
+
+ if (result == null) {
+ if (typeof aAddon.selectedLocale[aProp] == "string" && aAddon.selectedLocale[aProp].length)
+ [result, ] = chooseValue(aAddon.selectedLocale, aProp);
+ else
+ [result, ] = chooseValue(aAddon.defaultLocale, aProp);
+ }
+
+ if (aProp == "creator")
+ return result ? new AddonManagerPrivate.AddonAuthor(result) : null;
+
+ return result;
+ });
+ }, this);
+
+ PROP_LOCALE_MULTI.forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_multiLocaleGetter() {
+ let results = null;
+ let usedRepository = false;
+
+ if (aAddon.active) {
+ let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." +
+ aProp.substring(0, aProp.length - 1);
+ let list = Services.prefs.getChildList(pref, {});
+ if (list.length > 0) {
+ list.sort();
+ results = [];
+ list.forEach(function(aPref) {
+ let value = Preferences.get(aPref, null, Ci.nsIPrefLocalizedString);
+ if (value)
+ results.push(value);
+ });
+ }
+ }
+
+ if (results == null) {
+ if (aAddon.selectedLocale[aProp] instanceof Array && aAddon.selectedLocale[aProp].length)
+ [results, usedRepository] = chooseValue(aAddon.selectedLocale, aProp);
+ else
+ [results, usedRepository] = chooseValue(aAddon.defaultLocale, aProp);
+ }
+
+ if (results && !usedRepository) {
+ results = results.map(function mapResult(aResult) {
+ return new AddonManagerPrivate.AddonAuthor(aResult);
+ });
+ }
+
+ return results;
+ });
+ }, this);
+
+ this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() {
+ let repositoryAddon = aAddon._repositoryAddon;
+ if (repositoryAddon && ("screenshots" in repositoryAddon)) {
+ let repositoryScreenshots = repositoryAddon.screenshots;
+ if (repositoryScreenshots && repositoryScreenshots.length > 0)
+ return repositoryScreenshots;
+ }
+
+ if (aAddon.type == "theme" && this.hasResource("preview.png")) {
+ let url = this.getResourceURI("preview.png").spec;
+ return [new AddonManagerPrivate.AddonScreenshot(url)];
+ }
+
+ return null;
+ });
+
+ this.__defineGetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesGetter() {
+ return aAddon.applyBackgroundUpdates;
+ });
+ this.__defineSetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesSetter(val) {
+ if (this.type == "experiment") {
+ logger.warn("Setting applyBackgroundUpdates on an experiment is not supported.");
+ return;
+ }
+
+ if (val != AddonManager.AUTOUPDATE_DEFAULT &&
+ val != AddonManager.AUTOUPDATE_DISABLE &&
+ val != AddonManager.AUTOUPDATE_ENABLE) {
+ val = val ? AddonManager.AUTOUPDATE_DEFAULT :
+ AddonManager.AUTOUPDATE_DISABLE;
+ }
+
+ if (val == aAddon.applyBackgroundUpdates)
+ return val;
+
+ XPIDatabase.setAddonProperties(aAddon, {
+ applyBackgroundUpdates: val
+ });
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]);
+
+ return val;
+ });
+
+ this.__defineSetter__("syncGUID", function AddonWrapper_syncGUIDGetter(val) {
+ if (aAddon.syncGUID == val)
+ return val;
+
+ if (aAddon.inDatabase)
+ XPIDatabase.setAddonSyncGUID(aAddon, val);
+
+ aAddon.syncGUID = val;
+
+ return val;
+ });
+
+ this.__defineGetter__("install", function AddonWrapper_installGetter() {
+ if (!("_install" in aAddon) || !aAddon._install)
+ return null;
+ return aAddon._install.wrapper;
+ });
+
+ this.__defineGetter__("pendingUpgrade", function AddonWrapper_pendingUpgradeGetter() {
+ return createWrapper(aAddon.pendingUpgrade);
+ });
+
+ this.__defineGetter__("scope", function AddonWrapper_scopeGetter() {
+ if (aAddon._installLocation)
+ return aAddon._installLocation.scope;
+
+ return AddonManager.SCOPE_PROFILE;
+ });
+
+ this.__defineGetter__("pendingOperations", function AddonWrapper_pendingOperationsGetter() {
+ let pending = 0;
+ if (!(aAddon.inDatabase)) {
+ // Add-on is pending install if there is no associated install (shouldn't
+ // happen here) or if the install is in the process of or has successfully
+ // completed the install. If an add-on is pending install then we ignore
+ // any other pending operations.
+ if (!aAddon._install || aAddon._install.state == AddonManager.STATE_INSTALLING ||
+ aAddon._install.state == AddonManager.STATE_INSTALLED)
+ return AddonManager.PENDING_INSTALL;
+ }
+ else if (aAddon.pendingUninstall) {
+ // If an add-on is pending uninstall then we ignore any other pending
+ // operations
+ return AddonManager.PENDING_UNINSTALL;
+ }
+
+ // Extensions have an intentional inconsistency between what the DB says is
+ // enabled and what we say to the ouside world. so we need to cover up that
+ // lie here as well.
+ if (aAddon.type != "experiment") {
+ if (aAddon.active && aAddon.disabled)
+ pending |= AddonManager.PENDING_DISABLE;
+ else if (!aAddon.active && !aAddon.disabled)
+ pending |= AddonManager.PENDING_ENABLE;
+ }
+
+ if (aAddon.pendingUpgrade)
+ pending |= AddonManager.PENDING_UPGRADE;
+
+ return pending;
+ });
+
+ this.__defineGetter__("operationsRequiringRestart", function AddonWrapper_operationsRequiringRestartGetter() {
+ let ops = 0;
+ if (XPIProvider.installRequiresRestart(aAddon))
+ ops |= AddonManager.OP_NEEDS_RESTART_INSTALL;
+ if (XPIProvider.uninstallRequiresRestart(aAddon))
+ ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL;
+ if (XPIProvider.enableRequiresRestart(aAddon))
+ ops |= AddonManager.OP_NEEDS_RESTART_ENABLE;
+ if (XPIProvider.disableRequiresRestart(aAddon))
+ ops |= AddonManager.OP_NEEDS_RESTART_DISABLE;
+
+ return ops;
+ });
+
+ this.__defineGetter__("isDebuggable", function AddonWrapper_isDebuggable() {
+ return this.isActive && aAddon.bootstrap;
+ });
+
+ this.__defineGetter__("permissions", function AddonWrapper_permisionsGetter() {
+ return aAddon.permissions();
+ });
+
+ this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() {
+ if (Services.appinfo.inSafeMode)
+ return false;
+ return aAddon.active;
+ });
+
+ this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() {
+ if (XPIProvider._enabledExperiments.has(aAddon.id)) {
+ return false;
+ }
+
+ return aAddon.softDisabled || aAddon.userDisabled;
+ });
+ this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) {
+ if (val == this.userDisabled) {
+ return val;
+ }
+
+ if (aAddon.type == "experiment") {
+ if (val) {
+ XPIProvider._enabledExperiments.delete(aAddon.id);
+ } else {
+ XPIProvider._enabledExperiments.add(aAddon.id);
+ }
+ }
+
+ if (aAddon.inDatabase) {
+ if (aAddon.type == "theme" && val) {
+ if (aAddon.internalName == XPIProvider.defaultSkin)
+ throw new Error("Cannot disable the default theme");
+ XPIProvider.enableDefaultTheme();
+ }
+ else {
+ XPIProvider.updateAddonDisabledState(aAddon, val);
+ }
+ }
+ else {
+ aAddon.userDisabled = val;
+ // When enabling remove the softDisabled flag
+ if (!val)
+ aAddon.softDisabled = false;
+ }
+
+ return val;
+ });
+
+ this.__defineSetter__("softDisabled", function AddonWrapper_softDisabledSetter(val) {
+ if (val == aAddon.softDisabled)
+ return val;
+
+ if (aAddon.inDatabase) {
+ // When softDisabling a theme just enable the active theme
+ if (aAddon.type == "theme" && val && !aAddon.userDisabled) {
+ if (aAddon.internalName == XPIProvider.defaultSkin)
+ throw new Error("Cannot disable the default theme");
+ XPIProvider.enableDefaultTheme();
+ }
+ else {
+ XPIProvider.updateAddonDisabledState(aAddon, undefined, val);
+ }
+ }
+ else {
+ // Only set softDisabled if not already disabled
+ if (!aAddon.userDisabled)
+ aAddon.softDisabled = val;
+ }
+
+ return val;
+ });
+
+ this.isCompatibleWith = function AddonWrapper_isCompatiblewith(aAppVersion, aPlatformVersion) {
+ return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion);
+ };
+
+ this.uninstall = function AddonWrapper_uninstall(alwaysAllowUndo) {
+ XPIProvider.uninstallAddon(aAddon, alwaysAllowUndo);
+ };
+
+ this.cancelUninstall = function AddonWrapper_cancelUninstall() {
+ XPIProvider.cancelUninstallAddon(aAddon);
+ };
+
+ this.findUpdates = function AddonWrapper_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
+ // Short-circuit updates for experiments because updates are handled
+ // through the Experiments Manager.
+ if (this.type == "experiment") {
+ AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason,
+ aAppVersion, aPlatformVersion);
+ return;
+ }
+
+ new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion);
+ };
+
+ // Returns true if there was an update in progress, false if there was no update to cancel
+ this.cancelUpdate = function AddonWrapper_cancelUpdate() {
+ if (aAddon._updateCheck) {
+ aAddon._updateCheck.cancel();
+ return true;
+ }
+ return false;
+ };
+
+ this.hasResource = function AddonWrapper_hasResource(aPath) {
+ if (aAddon._hasResourceCache.has(aPath))
+ return aAddon._hasResourceCache.get(aPath);
+
+ let bundle = aAddon._sourceBundle.clone();
+
+ // Bundle may not exist any more if the addon has just been uninstalled,
+ // but explicitly first checking .exists() results in unneeded file I/O.
+ try {
+ var isDir = bundle.isDirectory();
+ } catch (e) {
+ aAddon._hasResourceCache.set(aPath, false);
+ return false;
+ }
+
+ if (isDir) {
+ if (aPath) {
+ aPath.split("/").forEach(function(aPart) {
+ bundle.append(aPart);
+ });
+ }
+ let result = bundle.exists();
+ aAddon._hasResourceCache.set(aPath, result);
+ return result;
+ }
+
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ try {
+ zipReader.open(bundle);
+ let result = zipReader.hasEntry(aPath);
+ aAddon._hasResourceCache.set(aPath, result);
+ return result;
+ }
+ catch (e) {
+ aAddon._hasResourceCache.set(aPath, false);
+ return false;
+ }
+ finally {
+ zipReader.close();
+ }
+ },
+
+ /**
+ * Returns a URI to the selected resource or to the add-on bundle if aPath
+ * is null. URIs to the bundle will always be file: URIs. URIs to resources
+ * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is
+ * still an XPI file.
+ *
+ * @param aPath
+ * The path in the add-on to get the URI for or null to get a URI to
+ * the file or directory the add-on is installed as.
+ * @return an nsIURI
+ */
+ this.getResourceURI = function AddonWrapper_getResourceURI(aPath) {
+ if (!aPath)
+ return NetUtil.newURI(aAddon._sourceBundle);
+
+ return getURIForResourceInFile(aAddon._sourceBundle, aPath);
+ }
+}
+
+/**
+ * An object which identifies a directory install location for add-ons. The
+ * location consists of a directory which contains the add-ons installed in the
+ * location.
+ *
+ * Each add-on installed in the location is either a directory containing the
+ * add-on's files or a text file containing an absolute path to the directory
+ * containing the add-ons files. The directory or text file must have the same
+ * name as the add-on's ID.
+ *
+ * There may also a special directory named "staged" which can contain
+ * directories with the same name as an add-on ID. If the directory is empty
+ * then it means the add-on will be uninstalled from this location during the
+ * next startup. If the directory contains the add-on's files then they will be
+ * installed during the next startup.
+ *
+ * @param aName
+ * The string identifier for the install location
+ * @param aDirectory
+ * The nsIFile directory for the install location
+ * @param aScope
+ * The scope of add-ons installed in this location
+ * @param aLocked
+ * true if add-ons cannot be installed, uninstalled or upgraded in the
+ * install location
+ */
+function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) {
+ this._name = aName;
+ this.locked = aLocked;
+ this._directory = aDirectory;
+ this._scope = aScope
+ this._IDToFileMap = {};
+ this._FileToIDMap = {};
+ this._linkedAddons = [];
+ this._stagingDirLock = 0;
+
+ if (!aDirectory.exists())
+ return;
+ if (!aDirectory.isDirectory())
+ throw new Error("Location must be a directory.");
+
+ this._readAddons();
+}
+
+DirectoryInstallLocation.prototype = {
+ _name : "",
+ _directory : null,
+ _IDToFileMap : null, // mapping from add-on ID to nsIFile
+ _FileToIDMap : null, // mapping from add-on path to add-on ID
+
+ /**
+ * Reads a directory linked to in a file.
+ *
+ * @param file
+ * The file containing the directory path
+ * @return An nsIFile object representing the linked directory.
+ */
+ _readDirectoryFromFile: function DirInstallLocation__readDirectoryFromFile(aFile) {
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(aFile, -1, -1, false);
+ let line = { value: "" };
+ if (fis instanceof Ci.nsILineInputStream)
+ fis.readLine(line);
+ fis.close();
+ if (line.value) {
+ let linkedDirectory = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+
+ try {
+ linkedDirectory.initWithPath(line.value);
+ }
+ catch (e) {
+ linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
+ }
+
+ if (!linkedDirectory.exists()) {
+ logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
+ " which does not exist");
+ return null;
+ }
+
+ if (!linkedDirectory.isDirectory()) {
+ logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
+ " which is not a directory");
+ return null;
+ }
+
+ return linkedDirectory;
+ }
+
+ logger.warn("File pointer " + aFile.path + " does not contain a path");
+ return null;
+ },
+
+ /**
+ * Finds all the add-ons installed in this location.
+ */
+ _readAddons: function DirInstallLocation__readAddons() {
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238).
+ let entries = getDirectoryEntries(this._directory);
+ for (let entry of entries) {
+ let id = entry.leafName;
+
+ if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH)
+ continue;
+
+ let directLoad = false;
+ if (entry.isFile() &&
+ id.substring(id.length - 4).toLowerCase() == ".xpi") {
+ directLoad = true;
+ id = id.substring(0, id.length - 4);
+ }
+
+ if (!gIDTest.test(id)) {
+ logger.debug("Ignoring file entry whose name is not a valid add-on ID: " +
+ entry.path);
+ continue;
+ }
+
+ if (entry.isFile() && !directLoad) {
+ let newEntry = this._readDirectoryFromFile(entry);
+ if (!newEntry) {
+ logger.debug("Deleting stale pointer file " + entry.path);
+ try {
+ entry.remove(true);
+ }
+ catch (e) {
+ logger.warn("Failed to remove stale pointer file " + entry.path, e);
+ // Failing to remove the stale pointer file is ignorable
+ }
+ continue;
+ }
+
+ entry = newEntry;
+ this._linkedAddons.push(id);
+ }
+
+ this._IDToFileMap[id] = entry;
+ this._FileToIDMap[entry.path] = id;
+ XPIProvider._addURIMapping(id, entry);
+ }
+ },
+
+ /**
+ * Gets the name of this install location.
+ */
+ get name() {
+ return this._name;
+ },
+
+ /**
+ * Gets the scope of this install location.
+ */
+ get scope() {
+ return this._scope;
+ },
+
+ /**
+ * Gets an array of nsIFiles for add-ons installed in this location.
+ */
+ get addonLocations() {
+ let locations = [];
+ for (let id in this._IDToFileMap) {
+ locations.push(this._IDToFileMap[id].clone());
+ }
+ return locations;
+ },
+
+ /**
+ * Gets the staging directory to put add-ons that are pending install and
+ * uninstall into.
+ *
+ * @return an nsIFile
+ */
+ getStagingDir: function DirInstallLocation_getStagingDir() {
+ let dir = this._directory.clone();
+ dir.append(DIR_STAGE);
+ return dir;
+ },
+
+ requestStagingDir: function() {
+ this._stagingDirLock++;
+
+ if (this._stagingDirPromise)
+ return this._stagingDirPromise;
+
+ OS.File.makeDir(this._directory.path);
+ let stagepath = OS.Path.join(this._directory.path, DIR_STAGE);
+ return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => {
+ if (e instanceof OS.File.Error && e.becauseExists)
+ return;
+ logger.error("Failed to create staging directory", e);
+ throw e;
+ });
+ },
+
+ releaseStagingDir: function() {
+ this._stagingDirLock--;
+
+ if (this._stagingDirLock == 0) {
+ this._stagingDirPromise = null;
+ this.cleanStagingDir();
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Removes the specified files or directories in the staging directory and
+ * then if the staging directory is empty attempts to remove it.
+ *
+ * @param aLeafNames
+ * An array of file or directory to remove from the directory, the
+ * array may be empty
+ */
+ cleanStagingDir: function(aLeafNames = []) {
+ let dir = this.getStagingDir();
+
+ for (let name of aLeafNames) {
+ let file = dir.clone();
+ file.append(name);
+ recursiveRemove(file);
+ }
+
+ if (this._stagingDirLock > 0)
+ return;
+
+ let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ try {
+ if (dirEntries.nextFile)
+ return;
+ }
+ finally {
+ dirEntries.close();
+ }
+
+ try {
+ setFilePermissions(dir, FileUtils.PERMS_DIRECTORY);
+ dir.remove(false);
+ }
+ catch (e) {
+ logger.warn("Failed to remove staging dir", e);
+ // Failing to remove the staging directory is ignorable
+ }
+ },
+
+ /**
+ * Gets the directory used by old versions for staging XPI and JAR files ready
+ * to be installed.
+ *
+ * @return an nsIFile
+ */
+ getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() {
+ let dir = this._directory.clone();
+ dir.append(DIR_XPI_STAGE);
+ return dir;
+ },
+
+ /**
+ * Returns a directory that is normally on the same filesystem as the rest of
+ * the install location and can be used for temporarily storing files during
+ * safe move operations. Calling this method will delete the existing trash
+ * directory and its contents.
+ *
+ * @return an nsIFile
+ */
+ getTrashDir: function DirInstallLocation_getTrashDir() {
+ let trashDir = this._directory.clone();
+ trashDir.append(DIR_TRASH);
+ let trashDirExists = trashDir.exists();
+ try {
+ if (trashDirExists)
+ recursiveRemove(trashDir);
+ trashDirExists = false;
+ } catch (e) {
+ logger.warn("Failed to remove trash directory", e);
+ }
+ if (!trashDirExists)
+ trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ return trashDir;
+ },
+
+ /**
+ * Installs an add-on into the install location.
+ *
+ * @param aId
+ * The ID of the add-on to install
+ * @param aSource
+ * The source nsIFile to install from
+ * @param aExistingAddonID
+ * The ID of an existing add-on to uninstall at the same time
+ * @param aCopy
+ * If false the source files will be moved to the new location,
+ * otherwise they will only be copied
+ * @return an nsIFile indicating where the add-on was installed to
+ */
+ installAddon: function DirInstallLocation_installAddon(aId, aSource,
+ aExistingAddonID,
+ aCopy) {
+ let trashDir = this.getTrashDir();
+
+ let transaction = new SafeInstallOperation();
+
+ let self = this;
+ function moveOldAddon(aId) {
+ let file = self._directory.clone();
+ file.append(aId);
+
+ if (file.exists())
+ transaction.moveUnder(file, trashDir);
+
+ file = self._directory.clone();
+ file.append(aId + ".xpi");
+ if (file.exists()) {
+ flushJarCache(file);
+ transaction.moveUnder(file, trashDir);
+ }
+ }
+
+ // If any of these operations fails the finally block will clean up the
+ // temporary directory
+ try {
+ moveOldAddon(aId);
+ if (aExistingAddonID && aExistingAddonID != aId) {
+ moveOldAddon(aExistingAddonID);
+
+ {
+ // Move the data directories.
+ /* XXX ajvincent We can't use OS.File: installAddon isn't compatible
+ * with Promises, nor is SafeInstallOperation. Bug 945540 has been filed
+ * for porting to OS.File.
+ */
+ let oldDataDir = FileUtils.getDir(
+ KEY_PROFILEDIR, ["extension-data", aExistingAddonID], false, true
+ );
+
+ if (oldDataDir.exists()) {
+ let newDataDir = FileUtils.getDir(
+ KEY_PROFILEDIR, ["extension-data", aId], false, true
+ );
+ if (newDataDir.exists()) {
+ let trashData = trashDir.clone();
+ trashData.append("data-directory");
+ transaction.moveUnder(newDataDir, trashData);
+ }
+
+ transaction.moveTo(oldDataDir, newDataDir);
+ }
+ }
+ }
+
+ if (aCopy) {
+ transaction.copy(aSource, this._directory);
+ }
+ else {
+ if (aSource.isFile())
+ flushJarCache(aSource);
+
+ transaction.moveUnder(aSource, this._directory);
+ }
+ }
+ finally {
+ // It isn't ideal if this cleanup fails but it isn't worth rolling back
+ // the install because of it.
+ try {
+ recursiveRemove(trashDir);
+ }
+ catch (e) {
+ logger.warn("Failed to remove trash directory when installing " + aId, e);
+ }
+ }
+
+ let newFile = this._directory.clone();
+ newFile.append(aSource.leafName);
+ try {
+ newFile.lastModifiedTime = Date.now();
+ } catch (e) {
+ logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
+ }
+ this._FileToIDMap[newFile.path] = aId;
+ this._IDToFileMap[aId] = newFile;
+ XPIProvider._addURIMapping(aId, newFile);
+
+ if (aExistingAddonID && aExistingAddonID != aId &&
+ aExistingAddonID in this._IDToFileMap) {
+ delete this._FileToIDMap[this._IDToFileMap[aExistingAddonID]];
+ delete this._IDToFileMap[aExistingAddonID];
+ }
+
+ return newFile;
+ },
+
+ /**
+ * Uninstalls an add-on from this location.
+ *
+ * @param aId
+ * The ID of the add-on to uninstall
+ * @throws if the ID does not match any of the add-ons installed
+ */
+ uninstallAddon: function DirInstallLocation_uninstallAddon(aId) {
+ let file = this._IDToFileMap[aId];
+ if (!file) {
+ logger.warn("Attempted to remove " + aId + " from " +
+ this._name + " but it was already gone");
+ return;
+ }
+
+ file = this._directory.clone();
+ file.append(aId);
+ if (!file.exists())
+ file.leafName += ".xpi";
+
+ if (!file.exists()) {
+ logger.warn("Attempted to remove " + aId + " from " +
+ this._name + " but it was already gone");
+
+ delete this._FileToIDMap[file.path];
+ delete this._IDToFileMap[aId];
+ return;
+ }
+
+ let trashDir = this.getTrashDir();
+
+ if (file.leafName != aId) {
+ logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId);
+ flushJarCache(file);
+ }
+
+ let transaction = new SafeInstallOperation();
+
+ try {
+ transaction.moveUnder(file, trashDir);
+ }
+ finally {
+ // It isn't ideal if this cleanup fails, but it is probably better than
+ // rolling back the uninstall at this point
+ try {
+ recursiveRemove(trashDir);
+ }
+ catch (e) {
+ logger.warn("Failed to remove trash directory when uninstalling " + aId, e);
+ }
+ }
+
+ delete this._FileToIDMap[file.path];
+ delete this._IDToFileMap[aId];
+ },
+
+ /**
+ * Gets the ID of the add-on installed in the given nsIFile.
+ *
+ * @param aFile
+ * The nsIFile to look in
+ * @return the ID
+ * @throws if the file does not represent an installed add-on
+ */
+ getIDForLocation: function DirInstallLocation_getIDForLocation(aFile) {
+ if (aFile.path in this._FileToIDMap)
+ return this._FileToIDMap[aFile.path];
+ throw new Error("Unknown add-on location " + aFile.path);
+ },
+
+ /**
+ * Gets the directory that the add-on with the given ID is installed in.
+ *
+ * @param aId
+ * The ID of the add-on
+ * @return The nsIFile
+ * @throws if the ID does not match any of the add-ons installed
+ */
+ getLocationForID: function DirInstallLocation_getLocationForID(aId) {
+ if (aId in this._IDToFileMap)
+ return this._IDToFileMap[aId].clone();
+ throw new Error("Unknown add-on ID " + aId);
+ },
+
+ /**
+ * Returns true if the given addon was installed in this location by a text
+ * file pointing to its real path.
+ *
+ * @param aId
+ * The ID of the addon
+ */
+ isLinkedAddon: function DirInstallLocation__isLinkedAddon(aId) {
+ return this._linkedAddons.indexOf(aId) != -1;
+ }
+};
+
+#ifdef XP_WIN
+/**
+ * An object that identifies a registry install location for add-ons. The location
+ * consists of a registry key which contains string values mapping ID to the
+ * path where an add-on is installed
+ *
+ * @param aName
+ * The string identifier of this Install Location.
+ * @param aRootKey
+ * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
+ * @param scope
+ * The scope of add-ons installed in this location
+ */
+function WinRegInstallLocation(aName, aRootKey, aScope) {
+ this.locked = true;
+ this._name = aName;
+ this._rootKey = aRootKey;
+ this._scope = aScope;
+ this._IDToFileMap = {};
+ this._FileToIDMap = {};
+
+ let path = this._appKeyPath + "\\Extensions";
+ let key = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(Ci.nsIWindowsRegKey);
+
+ // Reading the registry may throw an exception, and that's ok. In error
+ // cases, we just leave ourselves in the empty state.
+ try {
+ key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
+ }
+ catch (e) {
+ return;
+ }
+
+ this._readAddons(key);
+ key.close();
+}
+
+WinRegInstallLocation.prototype = {
+ _name : "",
+ _rootKey : null,
+ _scope : null,
+ _IDToFileMap : null, // mapping from ID to nsIFile
+ _FileToIDMap : null, // mapping from path to ID
+
+ /**
+ * Retrieves the path of this Application's data key in the registry.
+ */
+ get _appKeyPath() {
+ let appVendor = Services.appinfo.vendor;
+ let appName = Services.appinfo.name;
+
+ // .xulapp-based apps may intentionally not specify a vendor
+ if (appVendor != "")
+ appVendor += "\\";
+
+ return "SOFTWARE\\" + appVendor + appName;
+ },
+
+ /**
+ * Read the registry and build a mapping between ID and path for each
+ * installed add-on.
+ *
+ * @param key
+ * The key that contains the ID to path mapping
+ */
+ _readAddons: function RegInstallLocation__readAddons(aKey) {
+ let count = aKey.valueCount;
+ for (let i = 0; i < count; ++i) {
+ let id = aKey.getValueName(i);
+
+ let file = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+ file.initWithPath(aKey.readStringValue(id));
+
+ if (!file.exists()) {
+ logger.warn("Ignoring missing add-on in " + file.path);
+ continue;
+ }
+
+ this._IDToFileMap[id] = file;
+ this._FileToIDMap[file.path] = id;
+ XPIProvider._addURIMapping(id, file);
+ }
+ },
+
+ /**
+ * Gets the name of this install location.
+ */
+ get name() {
+ return this._name;
+ },
+
+ /**
+ * Gets the scope of this install location.
+ */
+ get scope() {
+ return this._scope;
+ },
+
+ /**
+ * Gets an array of nsIFiles for add-ons installed in this location.
+ */
+ get addonLocations() {
+ let locations = [];
+ for (let id in this._IDToFileMap) {
+ locations.push(this._IDToFileMap[id].clone());
+ }
+ return locations;
+ },
+
+ /**
+ * Gets the ID of the add-on installed in the given nsIFile.
+ *
+ * @param aFile
+ * The nsIFile to look in
+ * @return the ID
+ * @throws if the file does not represent an installed add-on
+ */
+ getIDForLocation: function RegInstallLocation_getIDForLocation(aFile) {
+ if (aFile.path in this._FileToIDMap)
+ return this._FileToIDMap[aFile.path];
+ throw new Error("Unknown add-on location");
+ },
+
+ /**
+ * Gets the nsIFile that the add-on with the given ID is installed in.
+ *
+ * @param aId
+ * The ID of the add-on
+ * @return the nsIFile
+ */
+ getLocationForID: function RegInstallLocation_getLocationForID(aId) {
+ if (aId in this._IDToFileMap)
+ return this._IDToFileMap[aId].clone();
+ throw new Error("Unknown add-on ID");
+ },
+
+ /**
+ * @see DirectoryInstallLocation
+ */
+ isLinkedAddon: function RegInstallLocation_isLinkedAddon(aId) {
+ return true;
+ }
+};
+#endif
+
+var addonTypes = [
+ new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 4000,
+ AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
+ new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 5000),
+ new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 7000,
+ AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
+ new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 8000,
+ AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
+];
+
+// We only register experiments support if the application supports them.
+// Ideally, we would install an observer to watch the pref. Installing
+// an observer for this pref is not necessary here and may be buggy with
+// regards to registering this XPIProvider twice.
+if (Preferences.get("experiments.supported", false)) {
+ addonTypes.push(
+ new AddonManagerPrivate.AddonType("experiment",
+ URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 11000,
+ AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL));
+}
+
+AddonManagerPrivate.registerProvider(XPIProvider, addonTypes);
diff --git a/components/extensions/src/XPIProviderUtils.js b/components/extensions/src/XPIProviderUtils.js
new file mode 100644
index 000000000..9f3273b1a
--- /dev/null
+++ b/components/extensions/src/XPIProviderUtils.js
@@ -0,0 +1,1430 @@
+/* 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 Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
+ "resource://gre/modules/DeferredSave.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.xpi-utils";
+
+// Create a new logger for use by the Addons XPI Provider Utils
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+const KEY_PROFILEDIR = "ProfD";
+const FILE_DATABASE = "extensions.sqlite";
+const FILE_JSON_DB = "extensions.json";
+const FILE_OLD_DATABASE = "extensions.rdf";
+const FILE_XPI_ADDONS_LIST = "extensions.ini";
+
+// The value for this is in Makefile.in
+#expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__;
+
+// The last version of DB_SCHEMA implemented in SQLITE
+const LAST_SQLITE_DB_SCHEMA = 14;
+const PREF_DB_SCHEMA = "extensions.databaseSchema";
+const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
+const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons";
+const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
+
+// Properties that only exist in the database
+const DB_METADATA = ["syncGUID",
+ "installDate",
+ "updateDate",
+ "size",
+ "sourceURI",
+ "releaseNotesURI",
+ "applyBackgroundUpdates"];
+const DB_BOOL_METADATA = ["visible", "active", "userDisabled", "appDisabled",
+ "pendingUninstall", "bootstrap", "skinnable",
+ "softDisabled", "isForeignInstall",
+ "hasBinaryComponents", "strictCompatibility"];
+
+// Properties to save in JSON file
+const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type",
+ "internalName", "updateURL", "updateKey", "optionsURL",
+ "optionsType", "aboutURL", "iconURL", "icon64URL",
+ "defaultLocale", "visible", "active", "userDisabled",
+ "appDisabled", "pendingUninstall", "descriptor", "installDate",
+ "updateDate", "applyBackgroundUpdates", "bootstrap",
+ "skinnable", "size", "sourceURI", "releaseNotesURI",
+ "softDisabled", "foreignInstall", "hasBinaryComponents",
+ "strictCompatibility", "locales", "targetApplications",
+ "targetPlatforms", "multiprocessCompatible",
+ ];
+
+// Time to wait before async save of XPI JSON database, in milliseconds
+const ASYNC_SAVE_DELAY_MS = 20;
+
+const PREFIX_ITEM_URI = "urn:mozilla:item:";
+const RDFURI_ITEM_ROOT = "urn:mozilla:item:root"
+const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
+
+XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1",
+ Ci.nsIRDFService);
+
+function EM_R(aProperty) {
+ return gRDF.GetResource(PREFIX_NS_EM + aProperty);
+}
+
+/**
+ * Converts an RDF literal, resource or integer into a string.
+ *
+ * @param aLiteral
+ * The RDF object to convert
+ * @return a string if the object could be converted or null
+ */
+function getRDFValue(aLiteral) {
+ if (aLiteral instanceof Ci.nsIRDFLiteral)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFResource)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFInt)
+ return aLiteral.Value;
+ return null;
+}
+
+/**
+ * Gets an RDF property as a string
+ *
+ * @param aDs
+ * The RDF datasource to read the property from
+ * @param aResource
+ * The RDF resource to read the property from
+ * @param aProperty
+ * The property to read
+ * @return a string if the property existed or null
+ */
+function getRDFProperty(aDs, aResource, aProperty) {
+ return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
+}
+
+/**
+ * Asynchronously fill in the _repositoryAddon field for one addon
+ */
+function getRepositoryAddon(aAddon, aCallback) {
+ if (!aAddon) {
+ aCallback(aAddon);
+ return;
+ }
+ function completeAddon(aRepositoryAddon) {
+ aAddon._repositoryAddon = aRepositoryAddon;
+ aAddon.compatibilityOverrides = aRepositoryAddon ?
+ aRepositoryAddon.compatibilityOverrides :
+ null;
+ aCallback(aAddon);
+ }
+ AddonRepository.getCachedAddonByID(aAddon.id, completeAddon);
+}
+
+/**
+ * Wrap an API-supplied function in an exception handler to make it safe to call
+ */
+function makeSafe(aCallback) {
+ return function(...aArgs) {
+ try {
+ aCallback(...aArgs);
+ }
+ catch(ex) {
+ logger.warn("XPI Database callback failed", ex);
+ }
+ }
+}
+
+/**
+ * A helper method to asynchronously call a function on an array
+ * of objects, calling a callback when function(x) has been gathered
+ * for every element of the array.
+ * WARNING: not currently error-safe; if the async function does not call
+ * our internal callback for any of the array elements, asyncMap will not
+ * call the callback parameter.
+ *
+ * @param aObjects
+ * The array of objects to process asynchronously
+ * @param aMethod
+ * Function with signature function(object, function aCallback(f_of_object))
+ * @param aCallback
+ * Function with signature f([aMethod(object)]), called when all values
+ * are available
+ */
+function asyncMap(aObjects, aMethod, aCallback) {
+ var resultsPending = aObjects.length;
+ var results = []
+ if (resultsPending == 0) {
+ aCallback(results);
+ return;
+ }
+
+ function asyncMap_gotValue(aIndex, aValue) {
+ results[aIndex] = aValue;
+ if (--resultsPending == 0) {
+ aCallback(results);
+ }
+ }
+
+ aObjects.map(function asyncMap_each(aObject, aIndex, aArray) {
+ try {
+ aMethod(aObject, function asyncMap_callback(aResult) {
+ asyncMap_gotValue(aIndex, aResult);
+ });
+ }
+ catch (e) {
+ logger.warn("Async map function failed", e);
+ asyncMap_gotValue(aIndex, undefined);
+ }
+ });
+}
+
+/**
+ * A generator to synchronously return result rows from an mozIStorageStatement.
+ *
+ * @param aStatement
+ * The statement to execute
+ */
+function resultRows(aStatement) {
+ try {
+ while (stepStatement(aStatement))
+ yield aStatement.row;
+ }
+ finally {
+ aStatement.reset();
+ }
+}
+
+/**
+ * A helper function to log an SQL error.
+ *
+ * @param aError
+ * The storage error code associated with the error
+ * @param aErrorString
+ * An error message
+ */
+function logSQLError(aError, aErrorString) {
+ logger.error("SQL error " + aError + ": " + aErrorString);
+}
+
+/**
+ * A helper function to log any errors that occur during async statements.
+ *
+ * @param aError
+ * A mozIStorageError to log
+ */
+function asyncErrorLogger(aError) {
+ logSQLError(aError.result, aError.message);
+}
+
+/**
+ * A helper function to step a statement synchronously and log any error that
+ * occurs.
+ *
+ * @param aStatement
+ * A mozIStorageStatement to execute
+ */
+function stepStatement(aStatement) {
+ try {
+ return aStatement.executeStep();
+ }
+ catch (e) {
+ logSQLError(XPIDatabase.connection.lastError,
+ XPIDatabase.connection.lastErrorString);
+ throw e;
+ }
+}
+
+/**
+ * Copies properties from one object to another. If no target object is passed
+ * a new object will be created and returned.
+ *
+ * @param aObject
+ * An object to copy from
+ * @param aProperties
+ * An array of properties to be copied
+ * @param aTarget
+ * An optional target object to copy the properties to
+ * @return the object that the properties were copied onto
+ */
+function copyProperties(aObject, aProperties, aTarget) {
+ if (!aTarget)
+ aTarget = {};
+ aProperties.forEach(function(aProp) {
+ aTarget[aProp] = aObject[aProp];
+ });
+ return aTarget;
+}
+
+/**
+ * Copies properties from a mozIStorageRow to an object. If no target object is
+ * passed a new object will be created and returned.
+ *
+ * @param aRow
+ * A mozIStorageRow to copy from
+ * @param aProperties
+ * An array of properties to be copied
+ * @param aTarget
+ * An optional target object to copy the properties to
+ * @return the object that the properties were copied onto
+ */
+function copyRowProperties(aRow, aProperties, aTarget) {
+ if (!aTarget)
+ aTarget = {};
+ aProperties.forEach(function(aProp) {
+ aTarget[aProp] = aRow.getResultByName(aProp);
+ });
+ return aTarget;
+}
+
+/**
+ * The DBAddonInternal is a special AddonInternal that has been retrieved from
+ * the database. The constructor will initialize the DBAddonInternal with a set
+ * of fields, which could come from either the JSON store or as an
+ * XPIProvider.AddonInternal created from an addon's manifest
+ * @constructor
+ * @param aLoaded
+ * Addon data fields loaded from JSON or the addon manifest.
+ */
+function DBAddonInternal(aLoaded) {
+ copyProperties(aLoaded, PROP_JSON_FIELDS, this);
+
+ if (aLoaded._installLocation) {
+ this._installLocation = aLoaded._installLocation;
+ this.location = aLoaded._installLocation._name;
+ }
+ else if (aLoaded.location) {
+ this._installLocation = XPIProvider.installLocationsByName[this.location];
+ }
+
+ this._key = this.location + ":" + this.id;
+
+ try {
+ this._sourceBundle = this._installLocation.getLocationForID(this.id);
+ }
+ catch (e) {
+ // An exception will be thrown if the add-on appears in the database but
+ // not on disk. In general this should only happen during startup as
+ // this change is being detected.
+ }
+
+ XPCOMUtils.defineLazyGetter(this, "pendingUpgrade",
+ function DBA_pendingUpgradeGetter() {
+ for (let install of XPIProvider.installs) {
+ if (install.state == AddonManager.STATE_INSTALLED &&
+ !(install.addon.inDatabase) &&
+ install.addon.id == this.id &&
+ install.installLocation == this._installLocation) {
+ delete this.pendingUpgrade;
+ return this.pendingUpgrade = install.addon;
+ }
+ };
+ return null;
+ });
+}
+
+function DBAddonInternalPrototype()
+{
+ this.applyCompatibilityUpdate =
+ function(aUpdate, aSyncCompatibility) {
+ this.targetApplications.forEach(function(aTargetApp) {
+ aUpdate.targetApplications.forEach(function(aUpdateTarget) {
+ if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
+ Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
+ aTargetApp.minVersion = aUpdateTarget.minVersion;
+ aTargetApp.maxVersion = aUpdateTarget.maxVersion;
+ XPIDatabase.saveChanges();
+ }
+ });
+ });
+ if (aUpdate.multiprocessCompatible !== undefined &&
+ aUpdate.multiprocessCompatible != this.multiprocessCompatible) {
+ this.multiprocessCompatible = aUpdate.multiprocessCompatible;
+ XPIDatabase.saveChanges();
+ }
+ XPIProvider.updateAddonDisabledState(this);
+ };
+
+ this.toJSON =
+ function() {
+ return copyProperties(this, PROP_JSON_FIELDS);
+ };
+
+ Object.defineProperty(this, "inDatabase",
+ { get: function() { return true; },
+ enumerable: true,
+ configurable: true });
+}
+DBAddonInternalPrototype.prototype = AddonInternal.prototype;
+
+DBAddonInternal.prototype = new DBAddonInternalPrototype();
+
+/**
+ * Internal interface: find an addon from an already loaded addonDB
+ */
+function _findAddon(addonDB, aFilter) {
+ for (let addon of addonDB.values()) {
+ if (aFilter(addon)) {
+ return addon;
+ }
+ }
+ return null;
+}
+
+/**
+ * Internal interface to get a filtered list of addons from a loaded addonDB
+ */
+function _filterDB(addonDB, aFilter) {
+ return [for (addon of addonDB.values()) if (aFilter(addon)) addon];
+}
+
+this.XPIDatabase = {
+ // true if the database connection has been opened
+ initialized: false,
+ // The database file
+ jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true),
+ // Migration data loaded from an old version of the database.
+ migrateData: null,
+ // Active add-on directories loaded from extensions.ini and prefs at startup.
+ activeBundles: null,
+
+ // Saved error object if we fail to read an existing database
+ _loadError: null,
+
+ // Error reported by our most recent attempt to read or write the database, if any
+ get lastError() {
+ if (this._loadError)
+ return this._loadError;
+ if (this._deferredSave)
+ return this._deferredSave.lastError;
+ return null;
+ },
+
+ /**
+ * Mark the current stored data dirty, and schedule a flush to disk
+ */
+ saveChanges: function() {
+ if (!this.initialized) {
+ throw new Error("Attempt to use XPI database when it is not initialized");
+ }
+
+ if (XPIProvider._closing) {
+ // use an Error here so we get a stack trace.
+ let err = new Error("XPI database modified after shutdown began");
+ logger.warn(err);
+ }
+
+ if (!this._deferredSave) {
+ this._deferredSave = new DeferredSave(this.jsonFile.path,
+ () => JSON.stringify(this),
+ ASYNC_SAVE_DELAY_MS);
+ }
+
+ let promise = this._deferredSave.saveChanges();
+ if (!this._schemaVersionSet) {
+ this._schemaVersionSet = true;
+ promise.then(
+ count => {
+ // Update the XPIDB schema version preference the first time we successfully
+ // save the database.
+ logger.debug("XPI Database saved, setting schema version preference to " + DB_SCHEMA);
+ Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
+ // Reading the DB worked once, so we don't need the load error
+ this._loadError = null;
+ },
+ error => {
+ // Need to try setting the schema version again later
+ this._schemaVersionSet = false;
+ logger.warn("Failed to save XPI database", error);
+ // this._deferredSave.lastError has the most recent error so we don't
+ // need this any more
+ this._loadError = null;
+ });
+ }
+ },
+
+ flush: function() {
+ // handle the "in memory only" and "saveChanges never called" cases
+ if (!this._deferredSave) {
+ return Promise.resolve(0);
+ }
+
+ return this._deferredSave.flush();
+ },
+
+ /**
+ * Converts the current internal state of the XPI addon database to
+ * a JSON.stringify()-ready structure
+ */
+ toJSON: function() {
+ if (!this.addonDB) {
+ // We never loaded the database?
+ throw new Error("Attempt to save database without loading it first");
+ }
+
+ let toSave = {
+ schemaVersion: DB_SCHEMA,
+ addons: [...this.addonDB.values()]
+ };
+ return toSave;
+ },
+
+ /**
+ * Pull upgrade information from an existing SQLITE database
+ *
+ * @return false if there is no SQLITE database
+ * true and sets this.migrateData to null if the SQLITE DB exists
+ * but does not contain useful information
+ * true and sets this.migrateData to
+ * {location: {id1:{addon1}, id2:{addon2}}, location2:{...}, ...}
+ * if there is useful information
+ */
+ getMigrateDataFromSQLITE: function XPIDB_getMigrateDataFromSQLITE() {
+ let connection = null;
+ let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
+ // Attempt to open the database
+ try {
+ connection = Services.storage.openUnsharedDatabase(dbfile);
+ }
+ catch (e) {
+ logger.warn("Failed to open sqlite database " + dbfile.path + " for upgrade", e);
+ return null;
+ }
+ logger.debug("Migrating data from sqlite");
+ let migrateData = this.getMigrateDataFromDatabase(connection);
+ connection.close();
+ return migrateData;
+ },
+
+ /**
+ * Synchronously opens and reads the database file, upgrading from old
+ * databases or making a new DB if needed.
+ *
+ * The possibilities, in order of priority, are:
+ * 1) Perfectly good, up to date database
+ * 2) Out of date JSON database needs to be upgraded => upgrade
+ * 3) JSON database exists but is mangled somehow => build new JSON
+ * 4) no JSON DB, but a useable SQLITE db we can upgrade from => upgrade
+ * 5) useless SQLITE DB => build new JSON
+ * 6) useable RDF DB => upgrade
+ * 7) useless RDF DB => build new JSON
+ * 8) Nothing at all => build new JSON
+ * @param aRebuildOnError
+ * A boolean indicating whether add-on information should be loaded
+ * from the install locations if the database needs to be rebuilt.
+ * (if false, caller is XPIProvider.checkForChanges() which will rebuild)
+ */
+ syncLoadDB: function XPIDB_syncLoadDB(aRebuildOnError) {
+ this.migrateData = null;
+ let fstream = null;
+ let data = "";
+ try {
+ logger.debug("Opening XPI database " + this.jsonFile.path);
+ fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Components.interfaces.nsIFileInputStream);
+ fstream.init(this.jsonFile, -1, 0, 0);
+ let cstream = null;
+ try {
+ cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Components.interfaces.nsIConverterInputStream);
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let str = {};
+ let read = 0;
+ do {
+ read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
+ data += str.value;
+ } while (read != 0);
+
+ this.parseDB(data, aRebuildOnError);
+ }
+ catch(e) {
+ logger.error("Failed to load XPI JSON data from profile", e);
+ this.rebuildDatabase(aRebuildOnError);
+ }
+ finally {
+ if (cstream)
+ cstream.close();
+ }
+ }
+ catch (e) {
+ if (e.result === Cr.NS_ERROR_FILE_NOT_FOUND) {
+ this.upgradeDB(aRebuildOnError);
+ }
+ else {
+ this.rebuildUnreadableDB(e, aRebuildOnError);
+ }
+ }
+ finally {
+ if (fstream)
+ fstream.close();
+ }
+ // If an async load was also in progress, resolve that promise with our DB;
+ // otherwise create a resolved promise
+ if (this._dbPromise) {
+ this._dbPromise.resolve(this.addonDB);
+ }
+ else
+ this._dbPromise = Promise.resolve(this.addonDB);
+ },
+
+ /**
+ * Parse loaded data, reconstructing the database if the loaded data is not valid
+ * @param aRebuildOnError
+ * If true, synchronously reconstruct the database from installed add-ons
+ */
+ parseDB: function(aData, aRebuildOnError) {
+ try {
+ // dump("Loaded JSON:\n" + aData + "\n");
+ let inputAddons = JSON.parse(aData);
+ // Now do some sanity checks on our JSON db
+ if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) {
+ // Content of JSON file is bad, need to rebuild from scratch
+ logger.error("bad JSON file contents");
+ this.rebuildDatabase(aRebuildOnError);
+ return;
+ }
+ if (inputAddons.schemaVersion != DB_SCHEMA) {
+ // Handle mismatched JSON schema version. For now, we assume
+ // compatibility for JSON data, though we throw away any fields we
+ // don't know about (bug 902956)
+ logger.debug("JSON schema mismatch: expected " + DB_SCHEMA +
+ ", actual " + inputAddons.schemaVersion);
+ // When we rev the schema of the JSON database, we need to make sure we
+ // force the DB to save so that the DB_SCHEMA value in the JSON file and
+ // the preference are updated.
+ }
+ // If we got here, we probably have good data
+ // Make AddonInternal instances from the loaded data and save them
+ let addonDB = new Map();
+ for (let loadedAddon of inputAddons.addons) {
+ let newAddon = new DBAddonInternal(loadedAddon);
+ addonDB.set(newAddon._key, newAddon);
+ };
+ this.addonDB = addonDB;
+ logger.debug("Successfully read XPI database");
+ this.initialized = true;
+ }
+ catch(e) {
+ // If we catch and log a SyntaxError from the JSON
+ // parser, the xpcshell test harness fails the test for us: bug 870828
+ if (e.name == "SyntaxError") {
+ logger.error("Syntax error parsing saved XPI JSON data");
+ }
+ else {
+ logger.error("Failed to load XPI JSON data from profile", e);
+ }
+ this.rebuildDatabase(aRebuildOnError);
+ }
+ },
+
+ /**
+ * Upgrade database from earlier (sqlite or RDF) version if available
+ */
+ upgradeDB: function(aRebuildOnError) {
+ try {
+ let schemaVersion = Services.prefs.getIntPref(PREF_DB_SCHEMA);
+ if (schemaVersion <= LAST_SQLITE_DB_SCHEMA) {
+ // we should have an older SQLITE database
+ logger.debug("Attempting to upgrade from SQLITE database");
+ this.migrateData = this.getMigrateDataFromSQLITE();
+ }
+ else {
+ // we've upgraded before but the JSON file is gone, fall through
+ // and rebuild from scratch
+ }
+ }
+ catch(e) {
+ // No schema version pref means either a really old upgrade (RDF) or
+ // a new profile
+ this.migrateData = this.getMigrateDataFromRDF();
+ }
+
+ this.rebuildDatabase(aRebuildOnError);
+ },
+
+ /**
+ * Reconstruct when the DB file exists but is unreadable
+ * (for example because read permission is denied)
+ */
+ rebuildUnreadableDB: function(aError, aRebuildOnError) {
+ logger.warn("Extensions database " + this.jsonFile.path +
+ " exists but is not readable; rebuilding", aError);
+ // Remember the error message until we try and write at least once, so
+ // we know at shutdown time that there was a problem
+ this._loadError = aError;
+ this.rebuildDatabase(aRebuildOnError);
+ },
+
+ /**
+ * Open and read the XPI database asynchronously, upgrading if
+ * necessary. If any DB load operation fails, we need to
+ * synchronously rebuild the DB from the installed extensions.
+ *
+ * @return Promise<Map> resolves to the Map of loaded JSON data stored
+ * in this.addonDB; never rejects.
+ */
+ asyncLoadDB: function XPIDB_asyncLoadDB() {
+ // Already started (and possibly finished) loading
+ if (this._dbPromise) {
+ return this._dbPromise;
+ }
+
+ logger.debug("Starting async load of XPI database " + this.jsonFile.path);
+ let readOptions = {
+ outExecutionDuration: 0
+ };
+ return this._dbPromise = OS.File.read(this.jsonFile.path, null, readOptions).then(
+ byteArray => {
+ logger.debug("Async JSON file read took " + readOptions.outExecutionDuration + " MS");
+ if (this._addonDB) {
+ logger.debug("Synchronous load completed while waiting for async load");
+ return this.addonDB;
+ }
+ logger.debug("Finished async read of XPI database, parsing...");
+ let decoder = new TextDecoder();
+ let data = decoder.decode(byteArray);
+ this.parseDB(data, true);
+ return this.addonDB;
+ })
+ .then(null,
+ error => {
+ if (this._addonDB) {
+ logger.debug("Synchronous load completed while waiting for async load");
+ return this.addonDB;
+ }
+ if (error.becauseNoSuchFile) {
+ this.upgradeDB(true);
+ }
+ else {
+ // it's there but unreadable
+ this.rebuildUnreadableDB(error, true);
+ }
+ return this.addonDB;
+ });
+ },
+
+ /**
+ * Rebuild the database from addon install directories. If this.migrateData
+ * is available, uses migrated information for settings on the addons found
+ * during rebuild
+ * @param aRebuildOnError
+ * A boolean indicating whether add-on information should be loaded
+ * from the install locations if the database needs to be rebuilt.
+ * (if false, caller is XPIProvider.checkForChanges() which will rebuild)
+ */
+ rebuildDatabase: function XIPDB_rebuildDatabase(aRebuildOnError) {
+ this.addonDB = new Map();
+ this.initialized = true;
+
+ if (XPIStates.size == 0) {
+ // No extensions installed, so we're done
+ logger.debug("Rebuilding XPI database with no extensions");
+ return;
+ }
+
+ // If there is no migration data then load the list of add-on directories
+ // that were active during the last run
+ if (!this.migrateData)
+ this.activeBundles = this.getActiveBundles();
+
+ if (aRebuildOnError) {
+ logger.warn("Rebuilding add-ons database from installed extensions.");
+ try {
+ XPIProvider.processFileChanges({}, false);
+ }
+ catch (e) {
+ logger.error("Failed to rebuild XPI database from installed extensions", e);
+ }
+ // Make sure to update the active add-ons and add-ons list on shutdown
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+ }
+ },
+
+ /**
+ * Gets the list of file descriptors of active extension directories or XPI
+ * files from the add-ons list. This must be loaded from disk since the
+ * directory service gives no easy way to get both directly. This list doesn't
+ * include themes as preferences already say which theme is currently active
+ *
+ * @return an array of persistent descriptors for the directories
+ */
+ getActiveBundles: function XPIDB_getActiveBundles() {
+ let bundles = [];
+
+ // non-bootstrapped extensions
+ let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
+ true);
+
+ if (!addonsList.exists())
+ // XXX Irving believes this is broken in the case where there is no
+ // extensions.ini but there are bootstrap extensions
+ return null;
+
+ try {
+ let iniFactory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
+ .getService(Ci.nsIINIParserFactory);
+ let parser = iniFactory.createINIParser(addonsList);
+ let keys = parser.getKeys("ExtensionDirs");
+
+ while (keys.hasMore())
+ bundles.push(parser.getString("ExtensionDirs", keys.getNext()));
+ }
+ catch (e) {
+ logger.warn("Failed to parse extensions.ini", e);
+ return null;
+ }
+
+ // Also include the list of active bootstrapped extensions
+ for (let id in XPIProvider.bootstrappedAddons)
+ bundles.push(XPIProvider.bootstrappedAddons[id].descriptor);
+
+ return bundles;
+ },
+
+ /**
+ * Retrieves migration data from the old extensions.rdf database.
+ *
+ * @return an object holding information about what add-ons were previously
+ * userDisabled and any updated compatibility information
+ */
+ getMigrateDataFromRDF: function XPIDB_getMigrateDataFromRDF(aDbWasMissing) {
+
+ // Migrate data from extensions.rdf
+ let rdffile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_DATABASE], true);
+ if (!rdffile.exists())
+ return null;
+
+ logger.debug("Migrating data from " + FILE_OLD_DATABASE);
+ let migrateData = {};
+
+ try {
+ let ds = gRDF.GetDataSourceBlocking(Services.io.newFileURI(rdffile).spec);
+ let root = Cc["@mozilla.org/rdf/container;1"].
+ createInstance(Ci.nsIRDFContainer);
+ root.Init(ds, gRDF.GetResource(RDFURI_ITEM_ROOT));
+ let elements = root.GetElements();
+
+ while (elements.hasMoreElements()) {
+ let source = elements.getNext().QueryInterface(Ci.nsIRDFResource);
+
+ let location = getRDFProperty(ds, source, "installLocation");
+ if (location) {
+ if (!(location in migrateData))
+ migrateData[location] = {};
+ let id = source.ValueUTF8.substring(PREFIX_ITEM_URI.length);
+ migrateData[location][id] = {
+ version: getRDFProperty(ds, source, "version"),
+ userDisabled: false,
+ targetApplications: []
+ }
+
+ let disabled = getRDFProperty(ds, source, "userDisabled");
+ if (disabled == "true" || disabled == "needs-disable")
+ migrateData[location][id].userDisabled = true;
+
+ let targetApps = ds.GetTargets(source, EM_R("targetApplication"),
+ true);
+ while (targetApps.hasMoreElements()) {
+ let targetApp = targetApps.getNext()
+ .QueryInterface(Ci.nsIRDFResource);
+ let appInfo = {
+ id: getRDFProperty(ds, targetApp, "id")
+ };
+
+ let minVersion = getRDFProperty(ds, targetApp, "updatedMinVersion");
+ if (minVersion) {
+ appInfo.minVersion = minVersion;
+ appInfo.maxVersion = getRDFProperty(ds, targetApp, "updatedMaxVersion");
+ }
+ else {
+ appInfo.minVersion = getRDFProperty(ds, targetApp, "minVersion");
+ appInfo.maxVersion = getRDFProperty(ds, targetApp, "maxVersion");
+ }
+ migrateData[location][id].targetApplications.push(appInfo);
+ }
+ }
+ }
+ }
+ catch (e) {
+ logger.warn("Error reading " + FILE_OLD_DATABASE, e);
+ migrateData = null;
+ }
+
+ return migrateData;
+ },
+
+ /**
+ * Retrieves migration data from a database that has an older or newer schema.
+ *
+ * @return an object holding information about what add-ons were previously
+ * userDisabled and any updated compatibility information
+ */
+ getMigrateDataFromDatabase: function XPIDB_getMigrateDataFromDatabase(aConnection) {
+ let migrateData = {};
+
+ // Attempt to migrate data from a different (even future!) version of the
+ // database
+ try {
+ var stmt = aConnection.createStatement("PRAGMA table_info(addon)");
+
+ const REQUIRED = ["internal_id", "id", "location", "userDisabled",
+ "installDate", "version"];
+
+ let reqCount = 0;
+ let props = [];
+ for (let row in resultRows(stmt)) {
+ if (REQUIRED.indexOf(row.name) != -1) {
+ reqCount++;
+ props.push(row.name);
+ }
+ else if (DB_METADATA.indexOf(row.name) != -1) {
+ props.push(row.name);
+ }
+ else if (DB_BOOL_METADATA.indexOf(row.name) != -1) {
+ props.push(row.name);
+ }
+ }
+
+ if (reqCount < REQUIRED.length) {
+ logger.error("Unable to read anything useful from the database");
+ return null;
+ }
+ stmt.finalize();
+
+ stmt = aConnection.createStatement("SELECT " + props.join(",") + " FROM addon");
+ for (let row in resultRows(stmt)) {
+ if (!(row.location in migrateData))
+ migrateData[row.location] = {};
+ let addonData = {
+ targetApplications: []
+ }
+ migrateData[row.location][row.id] = addonData;
+
+ props.forEach(function(aProp) {
+ if (aProp == "isForeignInstall")
+ addonData.foreignInstall = (row[aProp] == 1);
+ if (DB_BOOL_METADATA.indexOf(aProp) != -1)
+ addonData[aProp] = row[aProp] == 1;
+ else
+ addonData[aProp] = row[aProp];
+ })
+ }
+
+ var taStmt = aConnection.createStatement("SELECT id, minVersion, " +
+ "maxVersion FROM " +
+ "targetApplication WHERE " +
+ "addon_internal_id=:internal_id");
+
+ for (let location in migrateData) {
+ for (let id in migrateData[location]) {
+ taStmt.params.internal_id = migrateData[location][id].internal_id;
+ delete migrateData[location][id].internal_id;
+ for (let row in resultRows(taStmt)) {
+ migrateData[location][id].targetApplications.push({
+ id: row.id,
+ minVersion: row.minVersion,
+ maxVersion: row.maxVersion
+ });
+ }
+ }
+ }
+ }
+ catch (e) {
+ // An error here means the schema is too different to read
+ logger.error("Error migrating data", e);
+ return null;
+ }
+ finally {
+ if (taStmt)
+ taStmt.finalize();
+ if (stmt)
+ stmt.finalize();
+ }
+
+ return migrateData;
+ },
+
+ /**
+ * Shuts down the database connection and releases all cached objects.
+ * Return: Promise{integer} resolves / rejects with the result of the DB
+ * flush after the database is flushed and
+ * all cleanup is done
+ */
+ shutdown: function XPIDB_shutdown() {
+ logger.debug("shutdown");
+ if (this.initialized) {
+ // If our last database I/O had an error, try one last time to save.
+ if (this.lastError)
+ this.saveChanges();
+
+ this.initialized = false;
+
+ // Return a promise that any pending writes of the DB are complete and we
+ // are finished cleaning up
+ let flushPromise = this.flush();
+ flushPromise.then(null, error => {
+ logger.error("Flush of XPI database failed", error);
+ // If our last attempt to read or write the DB failed, force a new
+ // extensions.ini to be written to disk on the next startup
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+ })
+ .then(count => {
+ // Clear out the cached addons data loaded from JSON
+ delete this.addonDB;
+ delete this._dbPromise;
+ // same for the deferred save
+ delete this._deferredSave;
+ // re-enable the schema version setter
+ delete this._schemaVersionSet;
+ });
+ return flushPromise;
+ }
+ return Promise.resolve(0);
+ },
+
+ /**
+ * Asynchronously list all addons that match the filter function
+ * @param aFilter
+ * Function that takes an addon instance and returns
+ * true if that addon should be included in the selected array
+ * @param aCallback
+ * Called back with an array of addons matching aFilter
+ * or an empty array if none match
+ */
+ getAddonList: function(aFilter, aCallback) {
+ this.asyncLoadDB().then(
+ addonDB => {
+ let addonList = _filterDB(addonDB, aFilter);
+ asyncMap(addonList, getRepositoryAddon, makeSafe(aCallback));
+ })
+ .then(null,
+ error => {
+ logger.error("getAddonList failed", error);
+ makeSafe(aCallback)([]);
+ });
+ },
+
+ /**
+ * (Possibly asynchronously) get the first addon that matches the filter function
+ * @param aFilter
+ * Function that takes an addon instance and returns
+ * true if that addon should be selected
+ * @param aCallback
+ * Called back with the addon, or null if no matching addon is found
+ */
+ getAddon: function(aFilter, aCallback) {
+ return this.asyncLoadDB().then(
+ addonDB => {
+ getRepositoryAddon(_findAddon(addonDB, aFilter), makeSafe(aCallback));
+ })
+ .then(null,
+ error => {
+ logger.error("getAddon failed", e);
+ makeSafe(aCallback)(null);
+ });
+ },
+
+ /**
+ * Asynchronously gets an add-on with a particular ID in a particular
+ * install location.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ * @param aLocation
+ * The name of the install location
+ * @param aCallback
+ * A callback to pass the DBAddonInternal to
+ */
+ getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) {
+ this.asyncLoadDB().then(
+ addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId),
+ makeSafe(aCallback)));
+ },
+
+ /**
+ * Asynchronously gets the add-on with the specified ID that is visible.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ * @param aCallback
+ * A callback to pass the DBAddonInternal to
+ */
+ getVisibleAddonForID: function XPIDB_getVisibleAddonForID(aId, aCallback) {
+ this.getAddon(aAddon => ((aAddon.id == aId) && aAddon.visible),
+ aCallback);
+ },
+
+ /**
+ * Asynchronously gets the visible add-ons, optionally restricting by type.
+ *
+ * @param aTypes
+ * An array of types to include or null to include all types
+ * @param aCallback
+ * A callback to pass the array of DBAddonInternals to
+ */
+ getVisibleAddons: function XPIDB_getVisibleAddons(aTypes, aCallback) {
+ this.getAddonList(aAddon => (aAddon.visible &&
+ (!aTypes || (aTypes.length == 0) ||
+ (aTypes.indexOf(aAddon.type) > -1))),
+ aCallback);
+ },
+
+ /**
+ * Synchronously gets all add-ons of a particular type.
+ *
+ * @param aType
+ * The type of add-on to retrieve
+ * @return an array of DBAddonInternals
+ */
+ getAddonsByType: function XPIDB_getAddonsByType(aType) {
+ if (!this.addonDB) {
+ // jank-tastic! Must synchronously load DB if the theme switches from
+ // an XPI theme to a lightweight theme before the DB has loaded,
+ // because we're called from sync XPIProvider.addonChanged
+ logger.warn("Synchronous load of XPI database due to getAddonsByType(" + aType + ")");
+ this.syncLoadDB(true);
+ }
+ return _filterDB(this.addonDB, aAddon => (aAddon.type == aType));
+ },
+
+ /**
+ * Synchronously gets an add-on with a particular internalName.
+ *
+ * @param aInternalName
+ * The internalName of the add-on to retrieve
+ * @return a DBAddonInternal
+ */
+ getVisibleAddonForInternalName: function XPIDB_getVisibleAddonForInternalName(aInternalName) {
+ if (!this.addonDB) {
+ // This may be called when the DB hasn't otherwise been loaded
+ logger.warn("Synchronous load of XPI database due to getVisibleAddonForInternalName");
+ this.syncLoadDB(true);
+ }
+
+ return _findAddon(this.addonDB,
+ aAddon => aAddon.visible &&
+ (aAddon.internalName == aInternalName));
+ },
+
+ /**
+ * Asynchronously gets all add-ons with pending operations.
+ *
+ * @param aTypes
+ * The types of add-ons to retrieve or null to get all types
+ * @param aCallback
+ * A callback to pass the array of DBAddonInternal to
+ */
+ getVisibleAddonsWithPendingOperations:
+ function XPIDB_getVisibleAddonsWithPendingOperations(aTypes, aCallback) {
+
+ this.getAddonList(
+ aAddon => (aAddon.visible &&
+ (aAddon.pendingUninstall ||
+ // Logic here is tricky. If we're active but disabled,
+ // we're pending disable; !active && !disabled, we're pending enable
+ (aAddon.active == aAddon.disabled)) &&
+ (!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))),
+ aCallback);
+ },
+
+ /**
+ * Asynchronously get an add-on by its Sync GUID.
+ *
+ * @param aGUID
+ * Sync GUID of add-on to fetch
+ * @param aCallback
+ * A callback to pass the DBAddonInternal record to. Receives null
+ * if no add-on with that GUID is found.
+ *
+ */
+ getAddonBySyncGUID: function XPIDB_getAddonBySyncGUID(aGUID, aCallback) {
+ this.getAddon(aAddon => aAddon.syncGUID == aGUID,
+ aCallback);
+ },
+
+ /**
+ * Synchronously gets all add-ons in the database.
+ * This is only called from the preference observer for the default
+ * compatibility version preference, so we can return an empty list if
+ * we haven't loaded the database yet.
+ *
+ * @return an array of DBAddonInternals
+ */
+ getAddons: function XPIDB_getAddons() {
+ if (!this.addonDB) {
+ return [];
+ }
+ return _filterDB(this.addonDB, aAddon => true);
+ },
+
+ /**
+ * Synchronously adds an AddonInternal's metadata to the database.
+ *
+ * @param aAddon
+ * AddonInternal to add
+ * @param aDescriptor
+ * The file descriptor of the add-on
+ * @return The DBAddonInternal that was added to the database
+ */
+ addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) {
+ if (!this.addonDB) {
+ this.syncLoadDB(false);
+ }
+
+ let newAddon = new DBAddonInternal(aAddon);
+ newAddon.descriptor = aDescriptor;
+ this.addonDB.set(newAddon._key, newAddon);
+ if (newAddon.visible) {
+ this.makeAddonVisible(newAddon);
+ }
+
+ this.saveChanges();
+ return newAddon;
+ },
+
+ /**
+ * Synchronously updates an add-on's metadata in the database. Currently just
+ * removes and recreates.
+ *
+ * @param aOldAddon
+ * The DBAddonInternal to be replaced
+ * @param aNewAddon
+ * The new AddonInternal to add
+ * @param aDescriptor
+ * The file descriptor of the add-on
+ * @return The DBAddonInternal that was added to the database
+ */
+ updateAddonMetadata: function XPIDB_updateAddonMetadata(aOldAddon, aNewAddon,
+ aDescriptor) {
+ this.removeAddonMetadata(aOldAddon);
+ aNewAddon.syncGUID = aOldAddon.syncGUID;
+ aNewAddon.installDate = aOldAddon.installDate;
+ aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates;
+ aNewAddon.foreignInstall = aOldAddon.foreignInstall;
+ aNewAddon.active = (aNewAddon.visible && !aNewAddon.disabled && !aNewAddon.pendingUninstall);
+
+ // addAddonMetadata does a saveChanges()
+ return this.addAddonMetadata(aNewAddon, aDescriptor);
+ },
+
+ /**
+ * Synchronously removes an add-on from the database.
+ *
+ * @param aAddon
+ * The DBAddonInternal being removed
+ */
+ removeAddonMetadata: function XPIDB_removeAddonMetadata(aAddon) {
+ this.addonDB.delete(aAddon._key);
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously marks a DBAddonInternal as visible marking all other
+ * instances with the same ID as not visible.
+ *
+ * @param aAddon
+ * The DBAddonInternal to make visible
+ */
+ makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) {
+ logger.debug("Make addon " + aAddon._key + " visible");
+ for (let [, otherAddon] of this.addonDB) {
+ if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) {
+ logger.debug("Hide addon " + otherAddon._key);
+ otherAddon.visible = false;
+ }
+ }
+ aAddon.visible = true;
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously sets properties for an add-on.
+ *
+ * @param aAddon
+ * The DBAddonInternal being updated
+ * @param aProperties
+ * A dictionary of properties to set
+ */
+ setAddonProperties: function XPIDB_setAddonProperties(aAddon, aProperties) {
+ for (let key in aProperties) {
+ aAddon[key] = aProperties[key];
+ }
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously sets the Sync GUID for an add-on.
+ * Only called when the database is already loaded.
+ *
+ * @param aAddon
+ * The DBAddonInternal being updated
+ * @param aGUID
+ * GUID string to set the value to
+ * @throws if another addon already has the specified GUID
+ */
+ setAddonSyncGUID: function XPIDB_setAddonSyncGUID(aAddon, aGUID) {
+ // Need to make sure no other addon has this GUID
+ function excludeSyncGUID(otherAddon) {
+ return (otherAddon._key != aAddon._key) && (otherAddon.syncGUID == aGUID);
+ }
+ let otherAddon = _findAddon(this.addonDB, excludeSyncGUID);
+ if (otherAddon) {
+ throw new Error("Addon sync GUID conflict for addon " + aAddon._key +
+ ": " + otherAddon._key + " already has GUID " + aGUID);
+ }
+ aAddon.syncGUID = aGUID;
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously updates an add-on's active flag in the database.
+ *
+ * @param aAddon
+ * The DBAddonInternal to update
+ */
+ updateAddonActive: function XPIDB_updateAddonActive(aAddon, aActive) {
+ logger.debug("Updating active state for add-on " + aAddon.id + " to " + aActive);
+
+ aAddon.active = aActive;
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously calculates and updates all the active flags in the database.
+ */
+ updateActiveAddons: function XPIDB_updateActiveAddons() {
+ if (!this.addonDB) {
+ logger.warn("updateActiveAddons called when DB isn't loaded");
+ // force the DB to load
+ this.syncLoadDB(true);
+ }
+ logger.debug("Updating add-on states");
+ for (let [, addon] of this.addonDB) {
+ let newActive = (addon.visible && !addon.disabled && !addon.pendingUninstall);
+ if (newActive != addon.active) {
+ addon.active = newActive;
+ this.saveChanges();
+ }
+ }
+ },
+
+ /**
+ * Writes out the XPI add-ons list for the platform to read.
+ * @return true if the file was successfully updated, false otherwise
+ */
+ writeAddonsList: function XPIDB_writeAddonsList() {
+ if (!this.addonDB) {
+ // force the DB to load
+ this.syncLoadDB(true);
+ }
+ Services.appinfo.invalidateCachesOnRestart();
+
+ let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
+ true);
+ let enabledAddons = [];
+ let text = "[ExtensionDirs]\r\n";
+ let count = 0;
+ let fullCount = 0;
+
+ let activeAddons = _filterDB(
+ this.addonDB,
+ aAddon => aAddon.active && !aAddon.bootstrap && (aAddon.type != "theme"));
+
+ for (let row of activeAddons) {
+ text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
+ enabledAddons.push(encodeURIComponent(row.id) + ":" +
+ encodeURIComponent(row.version));
+ }
+ fullCount += count;
+
+ // The selected skin may come from an inactive theme (the default theme
+ // when a lightweight theme is applied for example)
+ text += "\r\n[ThemeDirs]\r\n";
+
+ let dssEnabled = Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED, false);
+
+ let themes = [];
+ if (dssEnabled) {
+ themes = _filterDB(this.addonDB, aAddon => aAddon.type == "theme");
+ }
+ else {
+ let activeTheme = _findAddon(
+ this.addonDB,
+ aAddon => (aAddon.type == "theme") &&
+ (aAddon.internalName == XPIProvider.selectedSkin));
+ if (activeTheme) {
+ themes.push(activeTheme);
+ }
+ }
+
+ if (themes.length > 0) {
+ count = 0;
+ for (let row of themes) {
+ text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
+ enabledAddons.push(encodeURIComponent(row.id) + ":" +
+ encodeURIComponent(row.version));
+ }
+ fullCount += count;
+ }
+
+ text += "\r\n[MultiprocessIncompatibleExtensions]\r\n";
+
+ count = 0;
+ for (let row of activeAddons) {
+ if (!row.multiprocessCompatible) {
+ text += "Extension" + (count++) + "=" + row.id + "\r\n";
+ }
+ }
+
+ if (fullCount > 0) {
+ logger.debug("Writing add-ons list");
+
+ try {
+ let addonsListTmp = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST + ".tmp"],
+ true);
+ var fos = FileUtils.openFileOutputStream(addonsListTmp);
+ fos.write(text, text.length);
+ fos.close();
+ addonsListTmp.moveTo(addonsListTmp.parent, FILE_XPI_ADDONS_LIST);
+
+ Services.prefs.setCharPref(PREF_EM_ENABLED_ADDONS, enabledAddons.join(","));
+ }
+ catch (e) {
+ logger.error("Failed to write add-ons list to profile directory", e);
+ return false;
+ }
+ }
+ else {
+ if (addonsList.exists()) {
+ logger.debug("Deleting add-ons list");
+ try {
+ addonsList.remove(false);
+ }
+ catch (e) {
+ logger.error("Failed to remove " + addonsList.path, e);
+ return false;
+ }
+ }
+
+ Services.prefs.clearUserPref(PREF_EM_ENABLED_ADDONS);
+ }
+ return true;
+ }
+};
diff --git a/components/extensions/src/addonManager.js b/components/extensions/src/addonManager.js
new file mode 100644
index 000000000..2628ea87b
--- /dev/null
+++ b/components/extensions/src/addonManager.js
@@ -0,0 +1,200 @@
+/* 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 component serves as integration between the platform and AddonManager.
+ * It is responsible for initializing and shutting down the AddonManager as well
+ * as passing new installs from webpages to the AddonManager.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const PREF_EM_UPDATE_INTERVAL = "extensions.update.interval";
+
+// The old XPInstall error codes
+const EXECUTION_ERROR = -203;
+const CANT_READ_ARCHIVE = -207;
+const USER_CANCELLED = -210;
+const DOWNLOAD_ERROR = -228;
+const UNSUPPORTED_TYPE = -244;
+const SUCCESS = 0;
+
+const MSG_INSTALL_ENABLED = "WebInstallerIsInstallEnabled";
+const MSG_INSTALL_ADDONS = "WebInstallerInstallAddonsFromWebpage";
+const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
+
+const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+var gSingleton = null;
+
+var gParentMM = null;
+
+
+function amManager() {
+ Cu.import("resource://gre/modules/AddonManager.jsm");
+
+ let globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ globalMM.loadFrameScript(CHILD_SCRIPT, true);
+ globalMM.addMessageListener(MSG_INSTALL_ADDONS, this);
+
+ gParentMM = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ gParentMM.addMessageListener(MSG_INSTALL_ENABLED, this);
+
+ // Needed so receiveMessage can be called directly by JS callers
+ this.wrappedJSObject = this;
+}
+
+amManager.prototype = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "addons-startup")
+ AddonManagerPrivate.startup();
+ },
+
+ /**
+ * @see amIAddonManager.idl
+ */
+ mapURIToAddonID: function(uri, id) {
+ id.value = AddonManager.mapURIToAddonID(uri);
+ return !!id.value;
+ },
+
+ /**
+ * @see amIWebInstaller.idl
+ */
+ isInstallEnabled: function(aMimetype, aReferer) {
+ return AddonManager.isInstallEnabled(aMimetype);
+ },
+
+ /**
+ * @see amIWebInstaller.idl
+ */
+ installAddonsFromWebpage: function(aMimetype, aBrowser, aInstallingPrincipal,
+ aUris, aHashes, aNames, aIcons, aCallback) {
+ if (aUris.length == 0)
+ return false;
+
+ let retval = true;
+ if (!AddonManager.isInstallAllowed(aMimetype, aInstallingPrincipal)) {
+ aCallback = null;
+ retval = false;
+ }
+
+ let installs = [];
+ function buildNextInstall() {
+ if (aUris.length == 0) {
+ AddonManager.installAddonsFromWebpage(aMimetype, aBrowser, aInstallingPrincipal, installs);
+ return;
+ }
+ let uri = aUris.shift();
+ AddonManager.getInstallForURL(uri, function buildNextInstall_getInstallForURL(aInstall) {
+ function callCallback(aUri, aStatus) {
+ try {
+ aCallback.onInstallEnded(aUri, aStatus);
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ if (aInstall) {
+ installs.push(aInstall);
+ if (aCallback) {
+ aInstall.addListener({
+ onDownloadCancelled: function(aInstall) {
+ callCallback(uri, USER_CANCELLED);
+ },
+
+ onDownloadFailed: function(aInstall) {
+ if (aInstall.error == AddonManager.ERROR_CORRUPT_FILE)
+ callCallback(uri, CANT_READ_ARCHIVE);
+ else
+ callCallback(uri, DOWNLOAD_ERROR);
+ },
+
+ onInstallFailed: function(aInstall) {
+ callCallback(uri, EXECUTION_ERROR);
+ },
+
+ onInstallEnded: function(aInstall, aStatus) {
+ callCallback(uri, SUCCESS);
+ }
+ });
+ }
+ }
+ else if (aCallback) {
+ aCallback.onInstallEnded(uri, UNSUPPORTED_TYPE);
+ }
+ buildNextInstall();
+ }, aMimetype, aHashes.shift(), aNames.shift(), aIcons.shift(), null, aBrowser);
+ }
+ buildNextInstall();
+
+ return retval;
+ },
+
+ notify: function(aTimer) {
+ AddonManagerPrivate.backgroundUpdateTimerHandler();
+ },
+
+ /**
+ * messageManager callback function.
+ *
+ * Listens to requests from child processes for InstallTrigger
+ * activity, and sends back callbacks.
+ */
+ receiveMessage: function(aMessage) {
+ let payload = aMessage.data;
+
+ switch (aMessage.name) {
+ case MSG_INSTALL_ENABLED:
+ return AddonManager.isInstallEnabled(payload.mimetype);
+
+ case MSG_INSTALL_ADDONS: {
+ let callback = null;
+ if (payload.callbackID != -1) {
+ callback = {
+ onInstallEnded: function(url, status) {
+ gParentMM.broadcastAsyncMessage(MSG_INSTALL_CALLBACK, {
+ callbackID: payload.callbackID,
+ url: url,
+ status: status
+ });
+ },
+ };
+ }
+
+ return this.installAddonsFromWebpage(payload.mimetype,
+ aMessage.target, payload.triggeringPrincipal, payload.uris,
+ payload.hashes, payload.names, payload.icons, callback);
+ }
+ }
+ },
+
+ classID: Components.ID("{4399533d-08d1-458c-a87a-235f74451cfa}"),
+ _xpcom_factory: {
+ createInstance: function(aOuter, aIid) {
+ if (aOuter != null)
+ throw Components.Exception("Component does not support aggregation",
+ Cr.NS_ERROR_NO_AGGREGATION);
+
+ if (!gSingleton)
+ gSingleton = new amManager();
+ return gSingleton.QueryInterface(aIid);
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.amIAddonManager,
+ Ci.amIWebInstaller,
+ Ci.nsITimerCallback,
+ Ci.nsIObserver,
+ Ci.nsIMessageListener])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([amManager]);
diff --git a/components/extensions/src/amContentHandler.js b/components/extensions/src/amContentHandler.js
new file mode 100644
index 000000000..8dc4dfecd
--- /dev/null
+++ b/components/extensions/src/amContentHandler.js
@@ -0,0 +1,100 @@
+/* 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+const XPI_CONTENT_TYPE = "application/x-xpinstall";
+const MSG_INSTALL_ADDONS = "WebInstallerInstallAddonsFromWebpage";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function amContentHandler() {
+}
+
+amContentHandler.prototype = {
+ /**
+ * Handles a new request for an application/x-xpinstall file.
+ *
+ * @param aMimetype
+ * The mimetype of the file
+ * @param aContext
+ * The context passed to nsIChannel.asyncOpen
+ * @param aRequest
+ * The nsIRequest dealing with the content
+ */
+ handleContent: function(aMimetype, aContext, aRequest) {
+ if (aMimetype != XPI_CONTENT_TYPE)
+ throw Cr.NS_ERROR_WONT_HANDLE_CONTENT;
+
+ if (!(aRequest instanceof Ci.nsIChannel))
+ throw Cr.NS_ERROR_WONT_HANDLE_CONTENT;
+
+ let uri = aRequest.URI;
+
+ let window = null;
+ let callbacks = aRequest.notificationCallbacks ?
+ aRequest.notificationCallbacks :
+ aRequest.loadGroup.notificationCallbacks;
+ if (callbacks)
+ window = callbacks.getInterface(Ci.nsIDOMWindow);
+
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+
+ let installs = {
+ uris: [uri.spec],
+ hashes: [null],
+ names: [null],
+ icons: [null],
+ mimetype: XPI_CONTENT_TYPE,
+ triggeringPrincipal: aRequest.loadInfo.triggeringPrincipal,
+ callbackID: -1
+ };
+
+ if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ // When running in the main process this might be a frame inside an
+ // in-content UI page, walk up to find the first frame element in a chrome
+ // privileged document
+ let element = window.frameElement;
+ let ssm = Services.scriptSecurityManager;
+ while (element && !ssm.isSystemPrincipal(element.ownerDocument.nodePrincipal))
+ element = element.ownerDocument.defaultView.frameElement;
+
+ if (element) {
+ let listener = Cc["@mozilla.org/addons/integration;1"].
+ getService(Ci.nsIMessageListener);
+ listener.wrappedJSObject.receiveMessage({
+ name: MSG_INSTALL_ADDONS,
+ target: element,
+ data: installs,
+ });
+ return;
+ }
+ }
+
+ // Fall back to sending through the message manager
+ let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+
+ messageManager.sendAsyncMessage(MSG_INSTALL_ADDONS, installs);
+ },
+
+ classID: Components.ID("{7beb3ba8-6ec3-41b4-b67c-da89b8518922}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler]),
+
+ log : function(aMsg) {
+ let msg = "amContentHandler.js: " + (aMsg.join ? aMsg.join("") : aMsg);
+ Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
+ logStringMessage(msg);
+ dump(msg + "\n");
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([amContentHandler]);
diff --git a/components/extensions/src/amInstallTrigger.js b/components/extensions/src/amInstallTrigger.js
new file mode 100644
index 000000000..5fc0e1717
--- /dev/null
+++ b/components/extensions/src/amInstallTrigger.js
@@ -0,0 +1,230 @@
+/* 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";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+const XPINSTALL_MIMETYPE = "application/x-xpinstall";
+
+const MSG_INSTALL_ENABLED = "WebInstallerIsInstallEnabled";
+const MSG_INSTALL_ADDONS = "WebInstallerInstallAddonsFromWebpage";
+const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
+
+
+var log = Log.repository.getLogger("AddonManager.InstallTrigger");
+log.level = Log.Level[Preferences.get("extensions.logging.enabled", false) ? "Warn" : "Trace"];
+
+function CallbackObject(id, callback, urls, mediator) {
+ this.id = id;
+ this.callback = callback;
+ this.urls = new Set(urls);
+ this.callCallback = function(url, status) {
+ try {
+ this.callback(url, status);
+ }
+ catch (e) {
+ log.warn("InstallTrigger callback threw an exception: " + e);
+ }
+
+ this.urls.delete(url);
+ if (this.urls.size == 0)
+ mediator._callbacks.delete(id);
+ };
+}
+
+function RemoteMediator(windowID) {
+ this._windowID = windowID;
+ this._lastCallbackID = 0;
+ this._callbacks = new Map();
+ this.mm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ this.mm.addWeakMessageListener(MSG_INSTALL_CALLBACK, this);
+}
+
+RemoteMediator.prototype = {
+ receiveMessage: function(message) {
+ if (message.name == MSG_INSTALL_CALLBACK) {
+ let payload = message.data;
+ let callbackHandler = this._callbacks.get(payload.callbackID);
+ if (callbackHandler) {
+ callbackHandler.callCallback(payload.url, payload.status);
+ }
+ }
+ },
+
+ enabled: function(url) {
+ let params = {
+ mimetype: XPINSTALL_MIMETYPE
+ };
+ return this.mm.sendSyncMessage(MSG_INSTALL_ENABLED, params)[0];
+ },
+
+ install: function(installs, principal, callback, window) {
+ let callbackID = this._addCallback(callback, installs.uris);
+
+ installs.mimetype = XPINSTALL_MIMETYPE;
+ installs.triggeringPrincipal = principal;
+ installs.callbackID = callbackID;
+
+ if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ // When running in the main process this might be a frame inside an
+ // in-content UI page, walk up to find the first frame element in a chrome
+ // privileged document
+ let element = window.frameElement;
+ let ssm = Services.scriptSecurityManager;
+ while (element && !ssm.isSystemPrincipal(element.ownerDocument.nodePrincipal))
+ element = element.ownerDocument.defaultView.frameElement;
+
+ if (element) {
+ let listener = Cc["@mozilla.org/addons/integration;1"].
+ getService(Ci.nsIMessageListener);
+ return listener.wrappedJSObject.receiveMessage({
+ name: MSG_INSTALL_ADDONS,
+ target: element,
+ data: installs,
+ });
+ }
+ }
+
+ // Fall back to sending through the message manager
+ let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+
+ return messageManager.sendSyncMessage(MSG_INSTALL_ADDONS, installs)[0];
+ },
+
+ _addCallback: function(callback, urls) {
+ if (!callback || typeof callback != "function")
+ return -1;
+
+ let callbackID = this._windowID + "-" + ++this._lastCallbackID;
+ let callbackObject = new CallbackObject(callbackID, callback, urls, this);
+ this._callbacks.set(callbackID, callbackObject);
+ return callbackID;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference])
+};
+
+
+function InstallTrigger() {
+}
+
+InstallTrigger.prototype = {
+ // Here be magic. We've declared ourselves as providing the
+ // nsIDOMGlobalPropertyInitializer interface, and are registered in the
+ // "JavaScript-global-property" category in the XPCOM category manager. This
+ // means that for newly created windows, XPCOM will createinstance this
+ // object, and then call init, passing in the window for which we need to
+ // provide an instance. We then initialize ourselves and return the webidl
+ // version of this object using the webidl-provided _create method, which
+ // XPCOM will then duly expose as a property value on the window. All this
+ // indirection is necessary because webidl does not (yet) support statics
+ // (bug 863952). See bug 926712 for more details about this implementation.
+ init: function(window) {
+ this._window = window;
+ this._principal = window.document.nodePrincipal;
+ this._url = window.document.documentURIObject;
+
+ window.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
+ let utils = window.getInterface(Components.interfaces.nsIDOMWindowUtils);
+ this._mediator = new RemoteMediator(utils.currentInnerWindowID);
+
+ return window.InstallTriggerImpl._create(window, this);
+ },
+
+ enabled: function() {
+ return this._mediator.enabled(this._url.spec);
+ },
+
+ updateEnabled: function() {
+ return this.enabled();
+ },
+
+ install: function(installs, callback) {
+ let installData = {
+ uris: [],
+ hashes: [],
+ names: [],
+ icons: [],
+ };
+
+ for (let name of Object.keys(installs)) {
+ let item = installs[name];
+ if (typeof item === "string") {
+ item = { URL: item };
+ }
+ if (!item.URL) {
+ throw new this._window.DOMError("Error", "Missing URL property for '" + name + "'");
+ }
+
+ let url = this._resolveURL(item.URL);
+ if (!this._checkLoadURIFromScript(url)) {
+ throw new this._window.DOMError("SecurityError", "Insufficient permissions to install: " + url.spec);
+ }
+
+ let iconUrl = null;
+ if (item.IconURL) {
+ iconUrl = this._resolveURL(item.IconURL);
+ if (!this._checkLoadURIFromScript(iconUrl)) {
+ iconUrl = null; // If page can't load the icon, just ignore it
+ }
+ }
+
+ installData.uris.push(url.spec);
+ installData.hashes.push(item.Hash || null);
+ installData.names.push(name);
+ installData.icons.push(iconUrl ? iconUrl.spec : null);
+ }
+
+ return this._mediator.install(installData, this._principal, callback, this._window);
+ },
+
+ startSoftwareUpdate: function(url, flags) {
+ let filename = Services.io.newURI(url, null, null)
+ .QueryInterface(Ci.nsIURL)
+ .filename;
+ let args = {};
+ args[filename] = { "URL": url };
+ return this.install(args);
+ },
+
+ installChrome: function(type, url, skin) {
+ return this.startSoftwareUpdate(url);
+ },
+
+ _resolveURL: function(url) {
+ return Services.io.newURI(url, null, this._url);
+ },
+
+ _checkLoadURIFromScript: function(uri) {
+ let secman = Services.scriptSecurityManager;
+ try {
+ secman.checkLoadURIWithPrincipal(this._principal,
+ uri,
+ secman.DISALLOW_INHERIT_PRINCIPAL);
+ return true;
+ }
+ catch(e) {
+ return false;
+ }
+ },
+
+ classID: Components.ID("{9df8ef2b-94da-45c9-ab9f-132eb55fddf1}"),
+ contractID: "@mozilla.org/addons/installtrigger;1",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer])
+};
+
+
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InstallTrigger]);
diff --git a/components/extensions/src/amWebInstallListener.js b/components/extensions/src/amWebInstallListener.js
new file mode 100644
index 000000000..9b9c53f44
--- /dev/null
+++ b/components/extensions/src/amWebInstallListener.js
@@ -0,0 +1,338 @@
+/* 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 is a default implementation of amIWebInstallListener that should work
+ * for most applications but can be overriden. It notifies the observer service
+ * about blocked installs. For normal installs it pops up an install
+ * confirmation when all the add-ons have been downloaded.
+ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PromptUtils", "resource://gre/modules/SharedPromptUtils.jsm");
+
+const URI_XPINSTALL_DIALOG = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
+
+// Installation can begin from any of these states
+const READY_STATES = [
+ AddonManager.STATE_AVAILABLE,
+ AddonManager.STATE_DOWNLOAD_FAILED,
+ AddonManager.STATE_INSTALL_FAILED,
+ AddonManager.STATE_CANCELLED
+];
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.weblistener";
+
+// Create a new logger for use by the Addons Web Listener
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+function notifyObservers(aTopic, aBrowser, aUri, aInstalls) {
+ let info = {
+ browser: aBrowser,
+ originatingURI: aUri,
+ installs: aInstalls,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo])
+ };
+ Services.obs.notifyObservers(info, aTopic, null);
+}
+
+/**
+ * Creates a new installer to monitor downloads and prompt to install when
+ * ready
+ *
+ * @param aBrowser
+ * The browser that started the installations
+ * @param aUrl
+ * The URL that started the installations
+ * @param aInstalls
+ * An array of AddonInstalls
+ */
+function Installer(aBrowser, aUrl, aInstalls) {
+ this.browser = aBrowser;
+ this.url = aUrl;
+ this.downloads = aInstalls;
+ this.installed = [];
+
+ notifyObservers("addon-install-started", aBrowser, aUrl, aInstalls);
+
+ aInstalls.forEach(function(aInstall) {
+ aInstall.addListener(this);
+
+ // Start downloading if it hasn't already begun
+ if (READY_STATES.indexOf(aInstall.state) != -1)
+ aInstall.install();
+ }, this);
+
+ this.checkAllDownloaded();
+}
+
+Installer.prototype = {
+ browser: null,
+ downloads: null,
+ installed: null,
+ isDownloading: true,
+
+ /**
+ * Checks if all downloads are now complete and if so prompts to install.
+ */
+ checkAllDownloaded: function() {
+ // Prevent re-entrancy caused by the confirmation dialog cancelling unwanted
+ // installs.
+ if (!this.isDownloading)
+ return;
+
+ var failed = [];
+ var installs = [];
+
+ for (let install of this.downloads) {
+ switch (install.state) {
+ case AddonManager.STATE_AVAILABLE:
+ case AddonManager.STATE_DOWNLOADING:
+ // Exit early if any add-ons haven't started downloading yet or are
+ // still downloading
+ return;
+ case AddonManager.STATE_DOWNLOAD_FAILED:
+ failed.push(install);
+ break;
+ case AddonManager.STATE_DOWNLOADED:
+ // App disabled items are not compatible and so fail to install
+ if (install.addon.appDisabled)
+ failed.push(install);
+ else
+ installs.push(install);
+
+ if (install.linkedInstalls) {
+ install.linkedInstalls.forEach(function(aInstall) {
+ aInstall.addListener(this);
+ // App disabled items are not compatible and so fail to install
+ if (aInstall.addon.appDisabled)
+ failed.push(aInstall);
+ else
+ installs.push(aInstall);
+ }, this);
+ }
+ break;
+ case AddonManager.STATE_CANCELLED:
+ // Just ignore cancelled downloads
+ break;
+ default:
+ logger.warn("Download of " + install.sourceURI.spec + " in unexpected state " +
+ install.state);
+ }
+ }
+
+ this.isDownloading = false;
+ this.downloads = installs;
+
+ if (failed.length > 0) {
+ // Stop listening and cancel any installs that are failed because of
+ // compatibility reasons.
+ failed.forEach(function(aInstall) {
+ if (aInstall.state == AddonManager.STATE_DOWNLOADED) {
+ aInstall.removeListener(this);
+ aInstall.cancel();
+ }
+ }, this);
+ notifyObservers("addon-install-failed", this.browser, this.url, failed);
+ }
+
+ // If none of the downloads were successful then exit early
+ if (this.downloads.length == 0)
+ return;
+
+ // Check for a custom installation prompt that may be provided by the
+ // applicaton
+ if ("@mozilla.org/addons/web-install-prompt;1" in Cc) {
+ try {
+ let prompt = Cc["@mozilla.org/addons/web-install-prompt;1"].
+ getService(Ci.amIWebInstallPrompt);
+ prompt.confirm(this.browser, this.url, this.downloads, this.downloads.length);
+ return;
+ }
+ catch (e) {}
+ }
+
+ let args = {};
+ args.url = this.url;
+ args.installs = this.downloads;
+ args.wrappedJSObject = args;
+
+ try {
+ let parentWindow = null;
+ if (this.browser) {
+ parentWindow = this.browser.ownerDocument.defaultView;
+ PromptUtils.fireDialogEvent(parentWindow, "DOMWillOpenModalDialog", this.browser);
+ }
+ Services.ww.openWindow(parentWindow, URI_XPINSTALL_DIALOG,
+ null, "chrome,modal,centerscreen", args);
+ } catch (e) {
+ logger.warn("Exception showing install confirmation dialog", e);
+ this.downloads.forEach(function(aInstall) {
+ aInstall.removeListener(this);
+ // Cancel the installs, as currently there is no way to make them fail
+ // from here.
+ aInstall.cancel();
+ }, this);
+ notifyObservers("addon-install-cancelled", this.browser, this.url,
+ this.downloads);
+ }
+ },
+
+ /**
+ * Checks if all installs are now complete and if so notifies observers.
+ */
+ checkAllInstalled: function() {
+ var failed = [];
+
+ for (let install of this.downloads) {
+ switch(install.state) {
+ case AddonManager.STATE_DOWNLOADED:
+ case AddonManager.STATE_INSTALLING:
+ // Exit early if any add-ons haven't started installing yet or are
+ // still installing
+ return;
+ case AddonManager.STATE_INSTALL_FAILED:
+ failed.push(install);
+ break;
+ }
+ }
+
+ this.downloads = null;
+
+ if (failed.length > 0)
+ notifyObservers("addon-install-failed", this.browser, this.url, failed);
+
+ if (this.installed.length > 0)
+ notifyObservers("addon-install-complete", this.browser, this.url, this.installed);
+ this.installed = null;
+ },
+
+ onDownloadCancelled: function(aInstall) {
+ aInstall.removeListener(this);
+ this.checkAllDownloaded();
+ },
+
+ onDownloadFailed: function(aInstall) {
+ aInstall.removeListener(this);
+ this.checkAllDownloaded();
+ },
+
+ onDownloadEnded: function(aInstall) {
+ this.checkAllDownloaded();
+ return false;
+ },
+
+ onInstallCancelled: function(aInstall) {
+ aInstall.removeListener(this);
+ this.checkAllInstalled();
+ },
+
+ onInstallFailed: function(aInstall) {
+ aInstall.removeListener(this);
+ this.checkAllInstalled();
+ },
+
+ onInstallEnded: function(aInstall) {
+ aInstall.removeListener(this);
+ this.installed.push(aInstall);
+
+ // If installing a theme that is disabled and can be enabled then enable it
+ if (aInstall.addon.type == "theme" &&
+ aInstall.addon.userDisabled == true &&
+ aInstall.addon.appDisabled == false) {
+ aInstall.addon.userDisabled = false;
+ }
+
+ this.checkAllInstalled();
+ }
+};
+
+function extWebInstallListener() {
+}
+
+extWebInstallListener.prototype = {
+ /**
+ * @see amIWebInstallListener.idl
+ */
+ onWebInstallDisabled: function(aBrowser, aUri, aInstalls) {
+ let info = {
+ browser: aBrowser,
+ originatingURI: aUri,
+ installs: aInstalls,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo])
+ };
+ Services.obs.notifyObservers(info, "addon-install-disabled", null);
+ },
+
+ /**
+ * @see amIWebInstallListener.idl
+ */
+ onWebInstallOriginBlocked: function(aBrowser, aUri, aInstalls) {
+ let info = {
+ browser: aBrowser,
+ originatingURI: aUri,
+ installs: aInstalls,
+
+ install: function() {
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo])
+ };
+ Services.obs.notifyObservers(info, "addon-install-origin-blocked", null);
+
+ return false;
+ },
+
+ /**
+ * @see amIWebInstallListener.idl
+ */
+ onWebInstallBlocked: function(aBrowser, aUri, aInstalls) {
+ let info = {
+ browser: aBrowser,
+ originatingURI: aUri,
+ installs: aInstalls,
+
+ install: function() {
+ new Installer(this.browser, this.originatingURI, this.installs);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo])
+ };
+ Services.obs.notifyObservers(info, "addon-install-blocked", null);
+
+ return false;
+ },
+
+ /**
+ * @see amIWebInstallListener.idl
+ */
+ onWebInstallRequested: function(aBrowser, aUri, aInstalls) {
+ new Installer(aBrowser, aUri, aInstalls);
+
+ // We start the installs ourself
+ return false;
+ },
+
+ classDescription: "XPI Install Handler",
+ contractID: "@mozilla.org/addons/web-install-listener;1",
+ classID: Components.ID("{0f38e086-89a3-40a5-8ffc-9b694de1d04a}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallListener,
+ Ci.amIWebInstallListener2])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([extWebInstallListener]);