diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-10 02:51:36 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-10 02:51:36 -0500 |
commit | 37d5300335d81cecbecc99812747a657588c63eb (patch) | |
tree | 765efa3b6a56bb715d9813a8697473e120436278 /toolkit/mozapps/webextensions/content | |
parent | b2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff) | |
parent | 4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff) | |
download | UXP-37d5300335d81cecbecc99812747a657588c63eb.tar UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.gz UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.lz UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.xz UXP-37d5300335d81cecbecc99812747a657588c63eb.zip |
Merge branch 'ext-work'
Diffstat (limited to 'toolkit/mozapps/webextensions/content')
27 files changed, 9491 insertions, 0 deletions
diff --git a/toolkit/mozapps/webextensions/content/OpenH264-license.txt b/toolkit/mozapps/webextensions/content/OpenH264-license.txt new file mode 100644 index 000000000..ad37989b8 --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/about.js b/toolkit/mozapps/webextensions/content/about.js new file mode 100644 index 000000000..4f8fb353e --- /dev/null +++ b/toolkit/mozapps/webextensions/content/about.js @@ -0,0 +1,103 @@ +// -*- 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"; + +/* import-globals-from ../../../content/contentAreaUtils.js */ + +var Cu = Components.utils; +Cu.import("resource://gre/modules/AddonManager.jsm"); + +function init() { + var addon = window.arguments[0]; + var extensionsStrings = document.getElementById("extensionsStrings"); + + document.documentElement.setAttribute("addontype", addon.type); + + var iconURL = AddonManager.getPreferredIconURL(addon, 48, window); + if (iconURL) { + var extensionIcon = document.getElementById("extensionIcon"); + extensionIcon.src = 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/toolkit/mozapps/webextensions/content/about.xul b/toolkit/mozapps/webextensions/content/about.xul new file mode 100644 index 000000000..6effcf37a --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/blocklist.css b/toolkit/mozapps/webextensions/content/blocklist.css new file mode 100644 index 000000000..cb48005a2 --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/blocklist.js b/toolkit/mozapps/webextensions/content/blocklist.js new file mode 100644 index 000000000..6d524e6ee --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/blocklist.xml b/toolkit/mozapps/webextensions/content/blocklist.xml new file mode 100644 index 000000000..74474392f --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/blocklist.xul b/toolkit/mozapps/webextensions/content/blocklist.xul new file mode 100644 index 000000000..240d9e4e1 --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/eula.js b/toolkit/mozapps/webextensions/content/eula.js new file mode 100644 index 000000000..537ee7284 --- /dev/null +++ b/toolkit/mozapps/webextensions/content/eula.js @@ -0,0 +1,25 @@ +// -*- 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"; + +var Cu = Components.utils; +Cu.import("resource://gre/modules/AddonManager.jsm"); + +function Startup() { + var bundle = document.getElementById("extensionsStrings"); + var addon = window.arguments[0].addon; + + document.documentElement.setAttribute("addontype", addon.type); + + var iconURL = AddonManager.getPreferredIconURL(addon, 48, window); + if (iconURL) + document.getElementById("icon").src = iconURL; + + var label = document.createTextNode(bundle.getFormattedString("eulaHeader", [addon.name])); + document.getElementById("heading").appendChild(label); + document.getElementById("eula").value = addon.eula; +} diff --git a/toolkit/mozapps/webextensions/content/eula.xul b/toolkit/mozapps/webextensions/content/eula.xul new file mode 100644 index 000000000..10e657951 --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/extensions.css b/toolkit/mozapps/webextensions/content/extensions.css new file mode 100644 index 000000000..cb5313365 --- /dev/null +++ b/toolkit/mozapps/webextensions/content/extensions.css @@ -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/. */ + +@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"); +} + +.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, +setting[type="boolint"]: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; +} + +#show-disabled-unsigned-extensions .button-text { + margin-inline-start: 3px !important; + margin-inline-end: 2px !important; +} + +#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"]) #plugindeprecation-notice { + 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"] .addon-control.enable, +.view-pane[type="experiment"] .addon-control.disable, +#detail-view[type="experiment"] .alert-container, +#detail-view[type="experiment"] #detail-version, +#detail-view[type="experiment"] #detail-creator, +#detail-view[type="experiment"] #detail-enable-btn, +#detail-view[type="experiment"] #detail-disable-btn { + 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; +} diff --git a/toolkit/mozapps/webextensions/content/extensions.js b/toolkit/mozapps/webextensions/content/extensions.js new file mode 100644 index 000000000..56158d9c6 --- /dev/null +++ b/toolkit/mozapps/webextensions/content/extensions.js @@ -0,0 +1,3915 @@ +/* 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"; + +/* import-globals-from ../../../content/contentAreaUtils.js */ +/* globals XMLStylesheetProcessingInstruction*/ + +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"); + +const CONSTANTS = {}; +Cu.import("resource://gre/modules/addons/AddonConstants.jsm", CONSTANTS); +const SIGNING_REQUIRED = CONSTANTS.REQUIRE_SIGNING ? + true : + Services.prefs.getBoolPref("xpinstall.signatures.required"); + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Preferences", + "resource://gre/modules/Preferences.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Experiments", + "resource:///modules/experiments/Experiments.jsm"); + +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 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() { + return this.bundleSvc.createBundle("chrome://branding/locale/brand.properties"); +}); +XPCOMUtils.defineLazyGetter(gStrings, "ext", function() { + return this.bundleSvc.createBundle("chrome://mozapps/locale/extensions/extensions.properties"); +}); +XPCOMUtils.defineLazyGetter(gStrings, "dl", function() { + return this.bundleSvc.createBundle("chrome://mozapps/locale/downloads/downloads.properties"); +}); + +XPCOMUtils.defineLazyGetter(gStrings, "brandShortName", function() { + return this.brand.GetStringFromName("brandShortName"); +}); +XPCOMUtils.defineLazyGetter(gStrings, "appVersion", function() { + return Services.appinfo.version; +}); + +document.addEventListener("load", initialize, true); +window.addEventListener("unload", shutdown, false); + +class MessageDispatcher { + constructor(target) { + this.listeners = new Map(); + this.target = target; + } + + addMessageListener(name, handler) { + if (!this.listeners.has(name)) { + this.listeners.set(name, new Set()); + } + + this.listeners.get(name).add(handler); + } + + removeMessageListener(name, handler) { + if (this.listeners.has(name)) { + this.listeners.get(name).delete(handler); + } + } + + sendAsyncMessage(name, data) { + for (let handler of this.listeners.get(name) || new Set()) { + Promise.resolve().then(() => { + handler.receiveMessage({ + name, + data, + target: this.target, + }); + }); + } + } +} + +/** + * A mock FrameMessageManager global to allow frame scripts to run in + * non-top-level, non-remote <browser>s as if they were top-level or + * remote. + * + * @param {Element} browser + * A XUL <browser> element. + */ +class FakeFrameMessageManager { + constructor(browser) { + let dispatcher = new MessageDispatcher(browser); + let frameDispatcher = new MessageDispatcher(null); + + this.sendAsyncMessage = frameDispatcher.sendAsyncMessage.bind(frameDispatcher); + this.addMessageListener = dispatcher.addMessageListener.bind(dispatcher); + this.removeMessageListener = dispatcher.removeMessageListener.bind(dispatcher); + + this.frame = { + get content() { + return browser.contentWindow; + }, + + get docShell() { + return browser.docShell; + }, + + addEventListener: browser.addEventListener.bind(browser), + removeEventListener: browser.removeEventListener.bind(browser), + + sendAsyncMessage: dispatcher.sendAsyncMessage.bind(dispatcher), + addMessageListener: frameDispatcher.addMessageListener.bind(frameDispatcher), + removeMessageListener: frameDispatcher.removeMessageListener.bind(frameDispatcher), + } + } + + loadFrameScript(url) { + Services.scriptloader.loadSubScript(url, Object.create(this.frame)); + } +} + +var gPendingInitializations = 1; +Object.defineProperty(this, "gIsInitializing", { + get: () => 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); +} + +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"); +} + +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 isCorrectlySigned(aAddon) { + // Add-ons without an "isCorrectlySigned" property are correctly signed as + // they aren't the correct type for signing. + return aAddon.isCorrectlySigned !== false; +} + +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; +} + +function getExperimentEndDate(aAddon) { + if (!("@mozilla.org/browser/experiments-service;1" in Cc)) { + return 0; + } + + if (!aAddon.isActive) { + return aAddon.endDate; + } + + let experiment = Experiments.instance().getActiveExperiment(); + if (!experiment) { + return 0; + } + + return experiment.endDate; +} + +/** + * 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; + } + 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() { + window.history.back(); + gViewController.updateCommand("cmd_back"); + gViewController.updateCommand("cmd_forward"); + }, + + forward: function() { + window.history.forward(); + gViewController.updateCommand("cmd_back"); + gViewController.updateCommand("cmd_forward"); + }, + + pushState: function(aState) { + window.history.pushState(aState, document.title); + }, + + replaceState: function(aState) { + window.history.replaceState(aState, document.title); + }, + + popState: function() { + 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() { + 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() { + 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(aState) { + this.pos++; + this.states.splice(this.pos, this.states.length); + this.states.push(aState); + }, + + replaceState: function(aState) { + this.states[this.pos] = aState; + }, + + popState: function() { + 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() { + const ADDON_EVENTS = ["onEnabling", "onEnabled", "onDisabling", + "onDisabled", "onUninstalling", "onUninstalled", + "onInstalled", "onOperationCancelled", + "onUpdateAvailable", "onUpdateFinished", + "onCompatibilityUpdateAvailable", + "onPropertyChanged"]; + for (let evt of ADDON_EVENTS) { + let event = evt; + this[event] = (...aArgs) => this.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; + this[event] = (...aArgs) => this.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() { + 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); + + let addonTooltip = document.getElementById("addonitem-tooltip"); + addonTooltip.addEventListener("popupshowing", function() { + let addonItem = addonTooltip.triggerNode; + // The way the test triggers the tooltip the richlistitem is the + // tooltipNode but in normal use it is the anonymous node. This allows + // any case + if (addonItem.localName != "richlistitem") + addonItem = document.getBindingParent(addonItem); + + let tiptext = addonItem.getAttribute("name"); + + if (addonItem.mAddon) { + if (shouldShowVersionNumber(addonItem.mAddon)) { + tiptext += " " + (addonItem.hasAttribute("upgrade") ? addonItem.mManualUpdate.version + : addonItem.mAddon.version); + } + } + else if (shouldShowVersionNumber(addonItem.mInstall)) { + tiptext += " " + addonItem.mInstall.version; + } + + addonTooltip.label = tiptext; + }, false); + }, + + shutdown: function() { + AddonManager.removeManagerListener(this); + AddonManager.removeInstallListener(this); + AddonManager.removeAddonListener(this); + }, + + registerAddonListener: function(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(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(aListener) { + if (this._installListeners.indexOf(aListener) != -1) + return; + this._installListeners.push(aListener); + }, + + unregisterInstallListener: function(aListener) { + var i = this._installListeners.indexOf(aListener); + if (i == -1) + return; + this._installListeners.splice(i, 1); + }, + + delegateAddonEvent: function(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(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() { + 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() { + 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() { + this.refreshGlobalWarning(); + }, + + onCheckUpdateSecurityChanged: function() { + this.refreshGlobalWarning(); + }, + + onUpdateModeChanged: function() { + this.refreshAutoUpdateDefault(); + } +}; + + +var gViewController = { + viewPort: null, + currentViewId: "", + currentViewObj: null, + currentViewRequest: 0, + viewObjects: {}, + viewChangeCallback: null, + initialViewSelected: false, + lastHistoryIndex: -1, + + initialize: function() { + this.viewPort = document.getElementById("view-port"); + this.headeredViews = document.getElementById("headered-views"); + this.headeredViewsDeck = document.getElementById("headered-views-content"); + + this.viewObjects["search"] = gSearchView; + this.viewObjects["discover"] = gDiscoverView; + this.viewObjects["list"] = gListView; + this.viewObjects["detail"] = gDetailView; + this.viewObjects["updates"] = gUpdatesView; + + for (let type in this.viewObjects) { + let view = this.viewObjects[type]; + view.initialize(); + } + + window.controllers.appendController(this); + + window.addEventListener("popstate", function(e) { + gViewController.updateState(e.state); + }, + false); + }, + + shutdown: function() { + if (this.currentViewObj) + this.currentViewObj.hide(); + this.currentViewRequest = 0; + + for (let type in this.viewObjects) { + let view = this.viewObjects[type]; + if ("shutdown" in view) { + try { + view.shutdown(); + } catch (e) { + // this shouldn't be fatal + Cu.reportError(e); + } + } + } + + window.controllers.removeController(this); + }, + + updateState: function(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(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(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(aViewId) { + if (aViewId == this.currentViewId) + return; + + var state = { + view: aViewId, + previousView: null + }; + gHistory.replaceState(state); + this.loadViewInternal(aViewId, null, state); + }, + + loadInitialView: function(aViewId) { + var state = { + view: aViewId, + previousView: null + }; + gHistory.replaceState(state); + + this.loadViewInternal(aViewId, null, state); + this.initialViewSelected = true; + notifyInitialized(); + }, + + get displayedView() { + if (this.viewPort.selectedPanel == this.headeredViews) { + return this.headeredViewsDeck.selectedPanel; + } + return this.viewPort.selectedPanel; + }, + + set displayedView(view) { + let node = view.node; + if (node.parentNode == this.headeredViewsDeck) { + this.headeredViewsDeck.selectedPanel = node; + this.viewPort.selectedPanel = this.headeredViews; + } else { + this.viewPort.selectedPanel = node; + } + }, + + loadViewInternal: function(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.displayedView.removeAttribute("loading"); + } catch (e) { + // this shouldn't be fatal + Cu.reportError(e); + } + } + + gCategories.select(aViewId, aPreviousView); + + this.currentViewId = aViewId; + this.currentViewObj = viewObj; + + this.displayedView = this.currentViewObj; + this.currentViewObj.node.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(aCallback) { + this.viewChangeCallback = aCallback; + gHistory.popState(); + }, + + notifyViewChanged: function() { + this.displayedView.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() { + return gHistory.canGoBack; + }, + doCommand: function() { + gHistory.back(); + } + }, + + cmd_forward: { + isEnabled: function() { + return gHistory.canGoForward; + }, + doCommand: function() { + gHistory.forward(); + } + }, + + cmd_focusSearch: { + isEnabled: () => true, + doCommand: function() { + gHeader.focusSearchBox(); + } + }, + + cmd_restartApp: { + isEnabled: function() { + return true; + }, + doCommand: function() { + 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() { + return true; + }, + doCommand: function() { + AddonManager.checkCompatibility = true; + } + }, + + cmd_enableUpdateSecurity: { + isEnabled: function() { + return true; + }, + doCommand: function() { + AddonManager.checkUpdateSecurity = true; + } + }, + + cmd_toggleAutoUpdateDefault: { + isEnabled: function() { + return true; + }, + doCommand: function() { + 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() { + return true; + }, + doCommand: function() { + AddonManager.getAllAddons(function(aAddonList) { + for (let addon of aAddonList) { + if ("applyBackgroundUpdates" in addon) + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; + } + }); + } + }, + + cmd_goToDiscoverPane: { + isEnabled: function() { + return gDiscoverView.enabled; + }, + doCommand: function() { + gViewController.loadView("addons://discover/"); + } + }, + + cmd_goToRecentUpdates: { + isEnabled: function() { + return true; + }, + doCommand: function() { + gViewController.loadView("addons://updates/recent"); + } + }, + + cmd_goToAvailableUpdates: { + isEnabled: function() { + return true; + }, + doCommand: function() { + gViewController.loadView("addons://updates/available"); + } + }, + + cmd_showItemDetails: { + isEnabled: function(aAddon) { + return !!aAddon && (gViewController.currentViewObj != gDetailView); + }, + doCommand: function(aAddon, aScrollToPreferences) { + gViewController.loadView("addons://detail/" + + encodeURIComponent(aAddon.id) + + (aScrollToPreferences ? "/preferences" : "")); + } + }, + + cmd_findAllUpdates: { + inProgress: false, + isEnabled: function() { + return !this.inProgress; + }, + doCommand: function() { + 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; + + let updateStatus = () => { + if (pendingChecks > 0) + return; + + this.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() { + pendingChecks--; + updateStatus(); + }, + onInstallFailed: function() { + pendingChecks--; + updateStatus(); + }, + onInstallEnded: function(aInstall, aAddon) { + pendingChecks--; + numUpdated++; + if (isPending(aInstall.existingAddon, "upgrade")) + restartNeeded = true; + updateStatus(); + } + }; + + var updateCheckListener = { + onUpdateAvailable: function(aAddon, aInstall) { + gEventManager.delegateAddonEvent("onUpdateAvailable", + [aAddon, aInstall]); + if (AddonManager.shouldAutoUpdate(aAddon)) { + aInstall.addListener(updateInstallListener); + aInstall.install(); + } else { + pendingChecks--; + numManualUpdates++; + updateStatus(); + } + }, + onNoUpdateAvailable: function(aAddon) { + pendingChecks--; + updateStatus(); + }, + onUpdateFinished: function(aAddon, aError) { + gEventManager.delegateAddonEvent("onUpdateFinished", + [aAddon, aError]); + } + }; + + AddonManager.getAddonsByTypes(null, function(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(aAddon) { + if (!aAddon) + return false; + return hasPermission(aAddon, "upgrade"); + }, + doCommand: function(aAddon) { + var listener = { + onUpdateAvailable: function(aAddon, aInstall) { + gEventManager.delegateAddonEvent("onUpdateAvailable", + [aAddon, aInstall]); + if (AddonManager.shouldAutoUpdate(aAddon)) + aInstall.install(); + }, + onNoUpdateAvailable: function(aAddon) { + gEventManager.delegateAddonEvent("onNoUpdateAvailable", + [aAddon]); + } + }; + gEventManager.delegateAddonEvent("onCheckingUpdate", [aAddon]); + aAddon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED); + } + }, + + cmd_showItemPreferences: { + isEnabled: function(aAddon) { + if (!aAddon || + (!aAddon.isActive && !aAddon.isGMPlugin) || + !aAddon.optionsURL) { + return false; + } + if (gViewController.currentViewObj == gDetailView && + (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE || + aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_BROWSER)) { + return false; + } + if (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO) + return false; + return true; + }, + doCommand: function(aAddon) { + if (hasInlineOptions(aAddon)) { + 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(aAddon) { + // XXXunf This may be applicable to install items too. See bug 561260 + return !!aAddon; + }, + doCommand: function(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(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(aAddon) { + aAddon.userDisabled = false; + }, + getTooltip: function(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(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(aAddon) { + aAddon.userDisabled = true; + }, + getTooltip: function(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(aAddon) { + if (!aAddon) + return false; + return aAddon.install && aAddon.install.state == AddonManager.STATE_AVAILABLE; + }, + doCommand: function(aAddon) { + function doInstall() { + gViewController.currentViewObj.getListItemForID(aAddon.id)._installStatus.installRemote(); + } + + if (gViewController.currentViewObj == gDetailView) + gViewController.popState(doInstall); + else + doInstall(); + } + }, + + cmd_purchaseItem: { + isEnabled: function(aAddon) { + if (!aAddon) + return false; + return !!aAddon.purchaseURL; + }, + doCommand: function(aAddon) { + openURL(aAddon.purchaseURL); + } + }, + + cmd_uninstallItem: { + isEnabled: function(aAddon) { + if (!aAddon) + return false; + return hasPermission(aAddon, "uninstall"); + }, + doCommand: function(aAddon) { + if (gViewController.currentViewObj != gDetailView) { + aAddon.uninstall(); + return; + } + + gViewController.popState(function() { + gViewController.currentViewObj.getListItemForID(aAddon.id).uninstall(); + }); + }, + getTooltip: function(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(aAddon) { + if (!aAddon) + return false; + return isPending(aAddon, "uninstall"); + }, + doCommand: function(aAddon) { + aAddon.cancelUninstall(); + } + }, + + cmd_installFromFile: { + isEnabled: function() { + return true; + }, + doCommand: function() { + 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); + } + return; + } + + var file = files.getNext(); + AddonManager.getInstallForFile(file, function(aInstall) { + installs.push(aInstall); + buildNextInstall(); + }); + } + + buildNextInstall(); + } + }, + + cmd_debugAddons: { + isEnabled: function() { + return true; + }, + doCommand: function() { + let mainWindow = getMainWindow(); + if ("switchToTabHavingURI" in mainWindow) { + mainWindow.switchToTabHavingURI("about:debugging#addons", true); + } + }, + }, + + cmd_cancelOperation: { + isEnabled: function(aAddon) { + if (!aAddon) + return false; + return aAddon.pendingOperations != AddonManager.PENDING_NONE; + }, + doCommand: function(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(aAddon) { + if (!aAddon) + return false; + return ("contributionURL" in aAddon && aAddon.contributionURL); + }, + doCommand: function(aAddon) { + openURL(aAddon.contributionURL); + } + }, + + cmd_askToActivateItem: { + isEnabled: function(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(aAddon) { + aAddon.userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE; + } + }, + + cmd_alwaysActivateItem: { + isEnabled: function(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(aAddon) { + aAddon.userDisabled = false; + } + }, + + cmd_neverActivateItem: { + isEnabled: function(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(aAddon) { + aAddon.userDisabled = true; + } + }, + + cmd_experimentsLearnMore: { + isEnabled: function() { + let mainWindow = getMainWindow(); + return mainWindow && "switchToTabHavingURI" in mainWindow; + }, + doCommand: function() { + let url = Services.prefs.getCharPref("toolkit.telemetry.infoURL"); + openOptionsInTab(url); + }, + }, + + cmd_experimentsOpenTelemetryPreferences: { + isEnabled: function() { + return !!getMainWindowWithPreferencesPane(); + }, + doCommand: function() { + let mainWindow = getMainWindowWithPreferencesPane(); + mainWindow.openAdvancedPreferences("dataChoicesTab"); + }, + }, + + cmd_showUnsignedExtensions: { + isEnabled: function() { + return true; + }, + doCommand: function() { + gViewController.loadView("addons://list/extension?unsigned=true"); + }, + }, + + cmd_showAllExtensions: { + isEnabled: function() { + return true; + }, + doCommand: function() { + gViewController.loadView("addons://list/extension"); + }, + }, + }, + + supportsCommand: function(aCommand) { + return (aCommand in this.commands); + }, + + isCommandEnabled: function(aCommand) { + if (!this.supportsCommand(aCommand)) + return false; + var addon = this.currentViewObj.getSelectedAddon(); + return this.commands[aCommand].isEnabled(addon); + }, + + updateCommands: function() { + // 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(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(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() {} +}; + +function hasInlineOptions(aAddon) { + return (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE || + aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_BROWSER || + 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) { + const locale = Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIXULChromeRegistry) + .getSelectedLocale("global", true); + const dtOptions = { year: 'numeric', month: 'long', day: 'numeric' }; + return aDate.toLocaleDateString(locale, dtOptions); +} + + +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 experiments. + if (aAddon.type == "experiment") + 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); + + if (aObj.type == "experiment") { + item.endDate = getExperimentEndDate(aObj); + } + + 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"; + 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(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(aAddonsList) { + addons = aAddonsList.filter(a => !a.hidden); + if (installs != null) + aCallback(addons, installs); + }); + + AddonManager.getInstallsByTypes(types, function(aInstallsList) { + // skip over upgrade installs and non-active installs + installs = aInstallsList.filter(function(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() { + 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; + + this.node.addEventListener("select", () => { + this.maybeHideSearch(); + gViewController.loadView(this.node.selectedItem.value); + }, false); + + this.node.addEventListener("click", (aEvent) => { + var selectedItem = this.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() { + AddonManager.removeTypeListener(this); + }, + + _insertCategory: function(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(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(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; + } + + gPendingInitializations++; + getAddonsAndInstalls(aType.id, (aAddonsList, aInstallsList) => { + var hidden = (aAddonsList.length == 0 && aInstallsList.length == 0); + var item = this.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(aInstall) { + this._maybeShowCategory(aInstall); + }, + + onInstallStarted: function(aInstall) { + this._maybeShowCategory(aInstall); + }, + + onInstallEnded: function(aInstall, aAddon) { + this._maybeShowCategory(aAddon); + }, + + onExternalInstall: function(aAddon, aExistingAddon, aRequiresRestart) { + this._maybeShowCategory(aAddon); + }, + + _maybeShowCategory: aAddon => { + if (aType.id == aAddon.type) { + this.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(aType) { + this._removeCategory(aType.id); + }, + + get selected() { + return this.node.selectedItem ? this.node.selectedItem.value : null; + }, + + select: function(aId, aPreviousView) { + var view = gViewController.parseViewId(aId); + if (view.type == "detail" && aPreviousView) { + aId = aPreviousView; + view = gViewController.parseViewId(aPreviousView); + } + aId = aId.replace(/\?.*/, ""); + + 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; + } + + var item; + if (view.type == "search") + item = this._search; + else + 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(aId) { + var items = document.getElementsByAttribute("value", aId); + if (items.length) + return items[0]; + return null; + }, + + setBadge: function(aId, aCount) { + let item = this.get(aId); + if (item) + item.badgeCount = aCount; + }, + + maybeHideSearch: function() { + var view = gViewController.parseViewId(this.node.selectedItem.value); + this._search.disabled = view.type != "search"; + } +}; + + +var gHeader = { + _search: null, + _dest: "", + + initialize: function() { + this._search = document.getElementById("header-search"); + + this._search.addEventListener("command", function(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(aEvent) { + if (aEvent.target == window) + updateNavButtonVisibility(); + }, false); + + updateNavButtonVisibility(); + }, + + focusSearchBox: function() { + this._search.focus(); + }, + + onKeyPress: function(aEvent) { + if (String.fromCharCode(aEvent.charCode) == "/") { + 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: [], + hideHeader: true, + + initialize: function() { + 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); + + let setURL = (aURL) => { + try { + this.homepageURL = Services.io.newURI(aURL, null, null); + } catch (e) { + this.showError(); + notifyInitialized(); + return; + } + + this._browser.homePage = this.homepageURL.spec; + this._browser.addProgressListener(this); + + if (this.loaded) + this._loadURL(this.homepageURL.spec, false, notifyInitialized); + else + notifyInitialized(); + } + + if (Services.prefs.getBoolPref(PREF_GETADDONS_CACHE_ENABLED) == false) { + setURL(url); + return; + } + + gPendingInitializations++; + AddonManager.getAllAddons(function(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() { + try { + this._browser.removeProgressListener(this); + } + catch (e) { + // Ignore the case when the listener wasn't already registered + } + }, + + show: function(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() { + if (this._browser.currentURI && + this._browser.currentURI.spec == this._browser.homePage) + return false; + return true; + }, + + refresh: function(aParam, aRequest, aState) { + this.show(aParam, aRequest, aState, true); + }, + + hide: function() { }, + + showError: function() { + this.node.selectedPanel = this._error; + }, + + _loadURL: function(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(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(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(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() { }, + onStatusChange: function() { }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + getSelectedAddon: function() { + return 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() { + 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; + + this._listBox.addEventListener("keydown", aEvent => { + if (aEvent.keyCode == aEvent.DOM_VK_RETURN) { + var item = this._listBox.selectedItem; + if (item) + item.showInDetailView(); + } + }, false); + + this._filter.addEventListener("command", () => this.updateView(), false); + }, + + shutdown: function() { + if (AddonRepository.isSearching) + AddonRepository.cancelSearch(); + }, + + get isSearching() { + return this._pendingSearches > 0; + }, + + show: function(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); + + gCachedAddons = {}; + this._pendingSearches = 2; + this._sorters.setSort("relevancescore", false); + + var elements = []; + + let createSearchResults = (aObjsList, aIsInstall, aIsRemote) => { + for (let index in aObjsList) { + let obj = aObjsList[index]; + let score = aObjsList.length - index; + if (!aIsRemote && aQuery.length > 0) { + score = this.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) + this._sorters.showprice = true; + } + + elements.push(item); + } + } + + let finishSearch = (createdCount) => { + if (elements.length > 0) { + sortElements(elements, [this._sorters.sortBy], this._sorters.ascending); + for (let element of elements) + this._listBox.insertBefore(element, this._listBox.lastChild); + this.updateListAttributes(); + } + + this._pendingSearches--; + this.updateView(); + + if (!this.isSearching) + gViewController.notifyViewChanged(); + } + + getAddonsAndInstalls(null, function(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: () => { + if (gViewController && aRequest != gViewController.currentViewRequest) + return; + + this._lastRemoteTotal = 0; + + // XXXunf Better handling of AMO search failure. See bug 579502 + finishSearch(0); // Silently fail + }, + + searchSucceeded: (aAddonsList, aAddonCount, aTotalResults) => { + if (gViewController && aRequest != gViewController.currentViewRequest) + return; + + if (aTotalResults > maxRemoteResults) + this._lastRemoteTotal = aTotalResults; + else + this._lastRemoteTotal = 0; + + var createdCount = createSearchResults(aAddonsList, false, true); + finishSearch(createdCount); + } + }); + }, + + showLoading: function(aLoading) { + this._loading.hidden = !aLoading; + this._listBox.hidden = aLoading; + }, + + updateView: function() { + 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() { + gEventManager.unregisterInstallListener(this); + doPendingUninstalls(this._listBox); + }, + + getMatchScore: function(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(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(aShow) { + this._emptyNotice.hidden = !aShow; + this._listBox.hidden = aShow; + }, + + showAllResultsLink: function(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() { + 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(aSortBy, aAscending) { + var footer = this._listBox.lastChild; + this._listBox.removeChild(footer); + + sortList(this._listBox, aSortBy, aAscending); + this.updateListAttributes(); + + this._listBox.appendChild(footer); + }, + + onDownloadCancelled: function(aInstall) { + this.removeInstall(aInstall); + }, + + onInstallCancelled: function(aInstall) { + this.removeInstall(aInstall); + }, + + removeInstall: function(aInstall) { + for (let item of this._listBox.childNodes) { + if (item.mInstall == aInstall) { + this._listBox.removeChild(item); + return; + } + } + }, + + getSelectedAddon: function() { + var item = this._listBox.selectedItem; + if (item) + return item.mAddon; + return null; + }, + + getListItemForID: function(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() { + this.node = document.getElementById("list-view"); + this._listBox = document.getElementById("addon-list"); + this._emptyNotice = document.getElementById("addon-list-empty"); + + this._listBox.addEventListener("keydown", (aEvent) => { + if (aEvent.keyCode == aEvent.DOM_VK_RETURN) { + var item = this._listBox.selectedItem; + if (item) + item.showInDetailView(); + } + }, false); + + document.getElementById("signing-learn-more").setAttribute("href", + Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"); + + let findSignedAddonsLink = document.getElementById("find-alternative-addons"); + try { + findSignedAddonsLink.setAttribute("href", + Services.urlFormatter.formatURLPref("extensions.getAddons.link.url")); + } catch (e) { + findSignedAddonsLink.classList.remove("text-link"); + } + + try { + document.getElementById("signing-dev-manual-link").setAttribute("href", + Services.prefs.getCharPref("xpinstall.signatures.devInfoURL")); + } catch (e) { + document.getElementById("signing-dev-info").hidden = true; + } + + if (Preferences.get("plugin.load_flash_only", true)) { + document.getElementById("plugindeprecation-learnmore-link") + .setAttribute("href", Services.urlFormatter.formatURLPref("app.support.baseURL") + "npapi"); + } else { + document.getElementById("plugindeprecation-notice").hidden = true; + } + }, + + show: function(aType, aRequest) { + let showOnlyDisabledUnsigned = false; + if (aType.endsWith("?unsigned=true")) { + aType = aType.replace(/\?.*/, ""); + showOnlyDisabledUnsigned = true; + } + + 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); + } + + getAddonsAndInstalls(aType, (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)); + + this.showEmptyNotice(elements.length == 0); + if (elements.length > 0) { + sortElements(elements, ["uiState", "name"], true); + for (let element of elements) + this._listBox.appendChild(element); + } + + this.filterDisabledUnsigned(showOnlyDisabledUnsigned); + + gEventManager.registerInstallListener(this); + gViewController.updateCommands(); + gViewController.notifyViewChanged(); + }); + }, + + hide: function() { + gEventManager.unregisterInstallListener(this); + doPendingUninstalls(this._listBox); + }, + + filterDisabledUnsigned: function(aFilter = true) { + let foundDisabledUnsigned = false; + + if (SIGNING_REQUIRED) { + for (let item of this._listBox.childNodes) { + if (!isCorrectlySigned(item.mAddon)) + foundDisabledUnsigned = true; + else + item.hidden = aFilter; + } + } + + document.getElementById("show-disabled-unsigned-extensions").hidden = + aFilter || !foundDisabledUnsigned; + + document.getElementById("show-all-extensions").hidden = !aFilter; + document.getElementById("disabled-unsigned-addons-info").hidden = !aFilter; + }, + + showEmptyNotice: function(aShow) { + this._emptyNotice.hidden = !aShow; + this._listBox.hidden = aShow; + }, + + onSortChanged: function(aSortBy, aAscending) { + sortList(this._listBox, aSortBy, aAscending); + }, + + onExternalInstall: function(aAddon, aExistingAddon, aRequiresRestart) { + // The existing list item will take care of upgrade installs + if (aExistingAddon) + return; + + if (aAddon.hidden) + return; + + this.addItem(aAddon); + }, + + onDownloadStarted: function(aInstall) { + this.addItem(aInstall, true); + }, + + onInstallStarted: function(aInstall) { + this.addItem(aInstall, true); + }, + + onDownloadCancelled: function(aInstall) { + this.removeItem(aInstall, true); + }, + + onInstallCancelled: function(aInstall) { + this.removeItem(aInstall, true); + }, + + onInstallEnded: function(aInstall) { + // Remove any install entries for upgrades, their status will appear against + // the existing item + if (aInstall.existingAddon) + this.removeItem(aInstall, true); + + if (aInstall.addon.type == "experiment") { + let item = this.getListItemForID(aInstall.addon.id); + if (item) { + item.endDate = getExperimentEndDate(aInstall.addon); + } + } + }, + + addItem: function(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(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() { + var item = this._listBox.selectedItem; + if (item) + return item.mAddon; + return null; + }, + + getListItemForID: function(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() { + this.node = document.getElementById("detail-view"); + + this._autoUpdate = document.getElementById("detail-autoUpdate"); + + this._autoUpdate.addEventListener("command", () => { + this._addon.applyBackgroundUpdates = this._autoUpdate.value; + }, true); + }, + + shutdown: function() { + AddonManager.removeManagerListener(this); + }, + + onUpdateModeChanged: function() { + this.onPropertyChanged(["applyBackgroundUpdates"]); + }, + + _updateView: function(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 = AddonManager.getPreferredIconURL(aAddon, 64, window); + document.getElementById("detail-icon").src = icon ? icon : ""; + document.getElementById("detail-creator").setCreator(aAddon.creator, aAddon.homepageURL); + + var version = document.getElementById("detail-version"); + if (shouldShowVersionNumber(aAddon)) { + version.hidden = false; + version.value = aAddon.version; + } else { + version.hidden = true; + } + + var screenshotbox = document.getElementById("detail-screenshot-box"); + 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"); + screenshotbox.hidden = false; + } else { + screenshotbox.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") && aAddon.id != AddonManager.hotfixID; + 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"); + } + } + + if (this._addon.type == "experiment") { + let prefix = "details.experiment."; + let active = this._addon.isActive; + + let stateKey = prefix + "state." + (active ? "active" : "complete"); + let node = document.getElementById("detail-experiment-state"); + node.value = gStrings.ext.GetStringFromName(stateKey); + + let now = Date.now(); + let end = getExperimentEndDate(this._addon); + 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); + } + + document.getElementById("detail-experiment-time").value = timeMessage; + } + + this.fillSettingsRows(aScrollToPreferences, (function() { + this.updateState(); + gViewController.notifyViewChanged(); + }).bind(this)); + }, + + show: function(aAddonId, aRequest) { + let index = aAddonId.indexOf("/preferences"); + let scrollToPreferences = false; + if (index >= 0) { + aAddonId = aAddonId.substring(0, index); + scrollToPreferences = true; + } + + this._loadingTimer = setTimeout(() => { + this.node.setAttribute("loading-extended", true); + }, LOADING_MSG_DELAY); + + var view = gViewController.currentViewId; + + AddonManager.getAddonByID(aAddonId, (aAddon) => { + if (gViewController && aRequest != gViewController.currentViewRequest) + return; + + if (aAddon) { + this._updateView(aAddon, false, scrollToPreferences); + return; + } + + // Look for an add-on pending install + AddonManager.getAllInstalls(aInstalls => { + for (let install of aInstalls) { + if (install.state == AddonManager.STATE_INSTALLED && + install.addon.id == aAddonId) { + this._updateView(install.addon, false); + return; + } + } + + if (aAddonId in gCachedAddons) { + this._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() { + 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() { + gViewController.updateCommands(); + + var pending = this._addon.pendingOperations; + if (pending != AddonManager.PENDING_NONE) { + this.node.removeAttribute("notification"); + + 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 (!isCorrectlySigned(this._addon) && SIGNING_REQUIRED) { + this.node.setAttribute("notification", "error"); + document.getElementById("detail-error").textContent = gStrings.ext.formatStringFromName( + "details.notification.unsignedAndDisabled", [this._addon.name, gStrings.brandShortName], 2 + ); + let errorLink = document.getElementById("detail-error-link"); + errorLink.value = gStrings.ext.GetStringFromName("details.notification.unsigned.link"); + errorLink.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; + 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 (!isCorrectlySigned(this._addon)) { + this.node.setAttribute("notification", "warning"); + document.getElementById("detail-warning").textContent = gStrings.ext.formatStringFromName( + "details.notification.unsigned", [this._addon.name, gStrings.brandShortName], 2 + ); + var warningLink = document.getElementById("detail-warning-link"); + warningLink.value = gStrings.ext.GetStringFromName("details.notification.unsigned.link"); + warningLink.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; + warningLink.hidden = false; + } 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 + ); + let 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 + ); + let warningLink = document.getElementById("detail-warning-link"); + warningLink.value = gStrings.ext.GetStringFromName("details.notification.outdated.link"); + warningLink.href = this._addon.blocklistURL; + warningLink.hidden = false; + } 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 + ); + let 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 + ); + let 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() { + if (this._loadingTimer) { + clearTimeout(this._loadingTimer); + this._loadingTimer = null; + } + + this.node.removeAttribute("loading-extended"); + }, + + emptySettingsRows: function() { + var lastRow = document.getElementById("detail-downloads"); + var rows = lastRow.parentNode; + while (lastRow.nextSibling) + rows.removeChild(rows.lastChild); + }, + + fillSettingsRows: function(aScrollToPreferences, aCallback) { + this.emptySettingsRows(); + if (!hasInlineOptions(this._addon)) { + if (aCallback) + aCallback(); + return; + } + + // We can't use a promise for this, since some code (especially in tests) + // relies on us finishing before the ViewChanged event bubbles up to its + // listeners, and promises resolve asynchronously. + let whenViewLoaded = callback => { + if (gViewController.displayedView.hasAttribute("loading")) { + gDetailView.node.addEventListener("ViewChanged", function viewChangedEventListener() { + gDetailView.node.removeEventListener("ViewChanged", viewChangedEventListener); + callback(); + }); + } else { + callback(); + } + }; + + let finish = (firstSetting) => { + // Ensure the page has loaded and force the XBL bindings to be synchronously applied, + // then notify observers. + whenViewLoaded(() => { + if (firstSetting) + firstSetting.clientTop; + Services.obs.notifyObservers(document, + AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, + this._addon.id); + if (aScrollToPreferences) + gDetailView.scrollToPreferencesRows(); + }); + } + + // 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 { + if (this._addon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_BROWSER) { + whenViewLoaded(() => { + this.createOptionsBrowser(rows).then(browser => { + // Make sure the browser is unloaded as soon as we change views, + // rather than waiting for the next detail view to load. + document.addEventListener("ViewChanged", function viewChangedEventListener() { + document.removeEventListener("ViewChanged", viewChangedEventListener); + browser.remove(); + }); + + finish(browser); + }); + }); + + if (aCallback) + aCallback(); + } else { + var xhr = new XMLHttpRequest(); + xhr.open("GET", this._addon.optionsURL, true); + xhr.responseType = "xml"; + xhr.onload = (function() { + 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; + } + } + + finish(firstSetting); + + if (aCallback) + aCallback(); + }).bind(this); + xhr.onerror = function(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() { + // 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); + } + }, + + createOptionsBrowser: function(parentNode) { + let browser = document.createElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("disableglobalhistory", "true"); + browser.setAttribute("class", "inline-options-browser"); + + return new Promise((resolve, reject) => { + let messageListener = { + receiveMessage({name, data}) { + if (name === "Extension:BrowserResized") + browser.style.height = `${data.height}px`; + else if (name === "Extension:BrowserContentLoaded") + resolve(browser); + }, + }; + + let onload = () => { + browser.removeEventListener("load", onload, true); + + let mm = new FakeFrameMessageManager(browser); + mm.loadFrameScript("chrome://extensions/content/ext-browser-content.js", + false); + mm.addMessageListener("Extension:BrowserContentLoaded", messageListener); + mm.addMessageListener("Extension:BrowserResized", messageListener); + mm.sendAsyncMessage("Extension:InitBrowser", {fixedWidth: true}); + + browser.setAttribute("src", this._addon.optionsURL); + }; + browser.addEventListener("load", onload, true); + browser.addEventListener("error", reject); + + parentNode.appendChild(browser); + }); + }, + + getSelectedAddon: function() { + return this._addon; + }, + + onEnabling: function() { + this.updateState(); + }, + + onEnabled: function() { + this.updateState(); + this.fillSettingsRows(); + }, + + onDisabling: function(aNeedsRestart) { + this.updateState(); + if (!aNeedsRestart && hasInlineOptions(this._addon)) { + Services.obs.notifyObservers(document, + AddonManager.OPTIONS_NOTIFICATION_HIDDEN, + this._addon.id); + } + }, + + onDisabled: function() { + this.updateState(); + this.emptySettingsRows(); + }, + + onUninstalling: function() { + this.updateState(); + }, + + onUninstalled: function() { + gViewController.popState(); + }, + + onOperationCancelled: function() { + this.updateState(); + }, + + onPropertyChanged: function(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("signedState") != -1 || + aProperties.indexOf("userDisabled") != -1) + this.updateState(); + }, + + onExternalInstall: function(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(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() { + 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() { + gUpdatesView.installSelected(); + }, false); + + this.updateAvailableCount(true); + + AddonManager.addAddonListener(this); + AddonManager.addInstallListener(this); + }, + + shutdown: function() { + AddonManager.removeAddonListener(this); + AddonManager.removeInstallListener(this); + }, + + show: function(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() { + this._updateSelected.hidden = true; + this._categoryItem.disabled = this._categoryItem.badgeCount == 0; + doPendingUninstalls(this._listBox); + }, + + _showRecentUpdates: function(aRequest) { + AddonManager.getAllAddons((aAddonsList) => { + if (gViewController && aRequest != gViewController.currentViewRequest) + return; + + var elements = []; + let threshold = Date.now() - UPDATES_RECENT_TIMESPAN; + for (let addon of aAddonsList) { + if (addon.hidden || !addon.updateDate || addon.updateDate.getTime() < threshold) + continue; + + elements.push(createItem(addon)); + } + + this.showEmptyNotice(elements.length == 0); + if (elements.length > 0) { + sortElements(elements, [this._sorters.sortBy], this._sorters.ascending); + for (let element of elements) + this._listBox.appendChild(element); + } + + gViewController.notifyViewChanged(); + }); + }, + + _showAvailableUpdates: function(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; + + AddonManager.getAllInstalls((aInstallsList) => { + if (!aIsRefresh && gViewController && aRequest && + aRequest != gViewController.currentViewRequest) + return; + + if (aIsRefresh) { + this.showEmptyNotice(false); + this._updateSelected.hidden = true; + + while (this._listBox.childNodes.length > 0) + this._listBox.removeChild(this._listBox.firstChild); + } + + var elements = []; + + for (let install of aInstallsList) { + if (!this.isManualUpdate(install)) + continue; + + let item = createItem(install.existingAddon); + item.setAttribute("upgrade", true); + item.addEventListener("IncludeUpdateChanged", () => { + this.maybeDisableUpdateSelected(); + }, false); + elements.push(item); + } + + this.showEmptyNotice(elements.length == 0); + if (elements.length > 0) { + this._updateSelected.hidden = false; + sortElements(elements, [this._sorters.sortBy], this._sorters.ascending); + for (let element of elements) + this._listBox.appendChild(element); + } + + // ensure badge count is in sync + this._categoryItem.badgeCount = this._listBox.itemCount; + + gViewController.notifyViewChanged(); + }); + }, + + showEmptyNotice: function(aShow) { + this._emptyNotice.hidden = !aShow; + this._listBox.hidden = aShow; + }, + + isManualUpdate: function(aInstall, aOnlyAvailable) { + var isManual = aInstall.existingAddon && + !AddonManager.shouldAutoUpdate(aInstall.existingAddon); + if (isManual && aOnlyAvailable) + return isInState(aInstall, "available"); + return isManual; + }, + + maybeRefresh: function() { + if (gViewController.currentViewId == "addons://updates/available") + this._showAvailableUpdates(true); + this.updateAvailableCount(); + }, + + updateAvailableCount: function(aInitializing) { + if (aInitializing) + gPendingInitializations++; + AddonManager.getAllInstalls((aInstallsList) => { + var count = aInstallsList.filter(aInstall => { + return this.isManualUpdate(aInstall, true); + }).length; + this._categoryItem.disabled = gViewController.currentViewId != "addons://updates/available" && + count == 0; + this._categoryItem.badgeCount = count; + if (aInitializing) + notifyInitialized(); + }); + }, + + maybeDisableUpdateSelected: function() { + for (let item of this._listBox.childNodes) { + if (item.includeUpdate) { + this._updateSelected.disabled = false; + return; + } + } + this._updateSelected.disabled = true; + }, + + installSelected: function() { + for (let item of this._listBox.childNodes) { + if (item.includeUpdate) + item.upgrade(); + } + + this._updateSelected.disabled = true; + }, + + getSelectedAddon: function() { + var item = this._listBox.selectedItem; + if (item) + return item.mAddon; + return null; + }, + + getListItemForID: function(aId) { + var listitem = this._listBox.firstChild; + while (listitem) { + if (listitem.mAddon.id == aId) + return listitem; + listitem = listitem.nextSibling; + } + return null; + }, + + onSortChanged: function(aSortBy, aAscending) { + sortList(this._listBox, aSortBy, aAscending); + }, + + onNewInstall: function(aInstall) { + if (!this.isManualUpdate(aInstall)) + return; + this.maybeRefresh(); + }, + + onInstallStarted: function(aInstall) { + this.updateAvailableCount(); + }, + + onInstallCancelled: function(aInstall) { + if (!this.isManualUpdate(aInstall)) + return; + this.maybeRefresh(); + }, + + onPropertyChanged: function(aAddon, aProperties) { + if (aProperties.indexOf("applyBackgroundUpdates") != -1) + this.updateAvailableCount(); + } +}; + +var gDragDrop = { + onDragOver: function(aEvent) { + var types = aEvent.dataTransfer.types; + if (types.includes("text/uri-list") || + types.includes("text/x-moz-url") || + types.includes("application/x-moz-file")) + aEvent.preventDefault(); + }, + + onDrop: function(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); + } + return; + } + + AddonManager.getInstallForURL(urls[pos++], function(aInstall) { + installs.push(aInstall); + buildNextInstall(); + }, "application/x-xpinstall"); + } + + buildNextInstall(); + + aEvent.preventDefault(); + } +}; diff --git a/toolkit/mozapps/webextensions/content/extensions.xml b/toolkit/mozapps/webextensions/content/extensions.xml new file mode 100644 index 000000000..b49645cf0 --- /dev/null +++ b/toolkit/mozapps/webextensions/content/extensions.xml @@ -0,0 +1,2008 @@ +<?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; +]> + +<!-- import-globals-from extensions.js --> + +<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> + + + <!-- 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="isPending"> + <parameter name="aAction"/> + <body><![CDATA[ + var action = AddonManager["PENDING_" + aAction.toUpperCase()]; + return !!(this.mAddon.pendingOperations & action); + ]]></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="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" hidden="true"/> + <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" hidden="true"/> + <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" align="center"> + <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" + tooltip="addonitem-tooltip" xbl:inherits="value=name"/> + <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: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();"/> + <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> + </content> + + <implementation> + <constructor><![CDATA[ + this._installStatus = document.getAnonymousElementByAttribute(this, "anonid", "install-status"); + 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="_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 = AddonManager.getPreferredIconURL(aAddon, 48, window); + if (iconURL) + this._icon.src = iconURL; + else + this._icon.src = ""; + + 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))) { + AddonManager.getAllInstalls(aInstallsList => { + // This can return after the binding has been destroyed, + // so try to detect that and return early + if (!("onNewInstall" in this)) + return; + for (let install of aInstallsList) { + if (install.existingAddon && + install.existingAddon.id == this.mAddon.id && + install.state == AddonManager.STATE_AVAILABLE) { + this.onNewInstall(install); + this.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) { + const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"] + .getService(Components.interfaces.nsIXULChromeRegistry) + .getSelectedLocale("global", true); + const dtOptions = { year: 'numeric', month: 'long', day: 'numeric' }; + return aDate.toLocaleDateString(locale, dtOptions); + } + + 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"); + + 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 && !isCorrectlySigned(this.mAddon) && SIGNING_REQUIRED) { + this.setAttribute("notification", "error"); + this._error.textContent = gStrings.ext.formatStringFromName( + "notification.unsignedAndDisabled", [this.mAddon.name, gStrings.brandShortName], 2 + ); + this._errorLink.value = gStrings.ext.GetStringFromName("notification.unsigned.link"); + this._errorLink.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; + 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 && !isCorrectlySigned(this.mAddon)) { + this.setAttribute("notification", "warning"); + this._warning.textContent = gStrings.ext.formatStringFromName( + "notification.unsigned", [this.mAddon.name, gStrings.brandShortName], 2 + ); + this._warningLink.value = gStrings.ext.GetStringFromName("notification.unsigned.link"); + this._warningLink.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; + this._warningLink.hidden = false; + } 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.value = gStrings.ext.GetStringFromName("notification.outdated.link"); + this._warningLink.href = this.mAddon.blocklistURL; + this._warningLink.hidden = false; + 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; + + let enableTooltip = gViewController.commands["cmd_enableItem"] + .getTooltip(this.mAddon); + this._enableBtn.setAttribute("tooltiptext", enableTooltip); + if (this.hasPermission("enable")) { + this._enableBtn.hidden = false; + } else { + this._enableBtn.hidden = true; + } + + let disableTooltip = gViewController.commands["cmd_disableItem"] + .getTooltip(this.mAddon); + this._disableBtn.setAttribute("tooltiptext", disableTooltip); + if (this.hasPermission("disable")) { + this._disableBtn.hidden = false; + } else { + this._disableBtn.hidden = true; + } + } + + let uninstallTooltip = gViewController.commands["cmd_uninstallItem"] + .getTooltip(this.mAddon); + this._removeBtn.setAttribute("tooltiptext", uninstallTooltip); + if (this.hasPermission("uninstall")) { + this._removeBtn.hidden = false; + } 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="_fetchReleaseNotes"> + <parameter name="aURI"/> + <body><![CDATA[ + if (!aURI || this._relNotesLoaded) { + sendToggleEvent(); + return; + } + + var relNotesData = null, transformData = null; + + this._relNotesLoaded = true; + this._relNotesLoading.hidden = false; + this._relNotesError.hidden = true; + + let sendToggleEvent = () => { + var event = document.createEvent("Events"); + event.initEvent("RelNotesToggle", true, true); + this.dispatchEvent(event); + } + + let showRelNotes = () => { + if (!relNotesData || !transformData) + return; + + this._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); + this._relNotes.appendChild(fragment); + if (this.hasAttribute("show-relnotes")) { + var container = this._relNotesContainer; + container.style.height = container.scrollHeight + "px"; + } + sendToggleEvent(); + } + + let handleError = () => { + dataReq.abort(); + styleReq.abort(); + this._relNotesLoading.hidden = true; + this._relNotesError.hidden = false; + this._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.responseType = "document"; + 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.responseType = "document"; + 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> + + <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("signedState") != -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"); + ]]></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" tooltip="addonitem-tooltip"/> + </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="_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; + } 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.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/toolkit/mozapps/webextensions/content/extensions.xul b/toolkit/mozapps/webextensions/content/extensions.xul new file mode 100644 index 000000000..70939d024 --- /dev/null +++ b/toolkit/mozapps/webextensions/content/extensions.xul @@ -0,0 +1,715 @@ +<?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"?> + +<!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;"/> + <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> + + <tooltip id="addonitem-tooltip"/> + </popupset> + + <!-- global commands - these act on all addons, or affect the addons manager + in some other way --> + <commandset id="globalCommandSet"> + <!-- XXXsw remove useless oncommand attribute once bug 371900 is fixed --> + <command id="cmd_focusSearch" oncommand=";"/> + <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_debugAddons"/> + <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"/> + <command id="cmd_showUnsignedExtensions"/> + <command id="cmd_showAllExtensions"/> + </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"/> + <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> + <key id="focusSearch" key="&search.commandkey;" modifiers="accel" + command="cmd_focusSearch"/> + </keyset> + <hbox flex="1"> + <vbox> + <hbox id="nav-header" + align="center" + pack="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"/> + </hbox> + <!-- category list --> + <richlistbox id="categories" flex="1"> + <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> + </vbox> + <vbox 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> + + <!-- container for views with the search/tools header --> + <vbox id="headered-views" flex="1"> + <!-- main header --> + <hbox id="header" align="center"> + <button id="show-all-extensions" hidden="true" + label="&showAllExtensions.button.label;" + command="cmd_showAllExtensions"/> + <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> + <button id="show-disabled-unsigned-extensions" hidden="true" + class="warning" + label="&showUnsignedExtensions.button.label;" + command="cmd_showUnsignedExtensions"/> + <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"/> + <menuitem id="utils-debugAddons" + label="&debugAddons.label;" + accesskey="&debugAddons.accesskey;" + command="cmd_debugAddons"/> + <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> + + <deck id="headered-views-content" flex="1" selectedIndex="0"> + <!-- 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"> + <!-- info UI for add-ons that have been disabled for being unsigned --> + <vbox id="disabled-unsigned-addons-info" hidden="true"> + <label id="disabled-unsigned-addons-heading" value="&disabledUnsigned.heading;"/> + <description> + &disabledUnsigned.description.start;<label class="text-link plain" id="find-alternative-addons">&disabledUnsigned.description.findAddonsLink;</label>&disabledUnsigned.description.end; + </description> + <hbox pack="start"><label class="text-link" id="signing-learn-more">&disabledUnsigned.learnMore;</label></hbox> + <description id="signing-dev-info"> + &disabledUnsigned.devInfo.start;<label class="text-link plain" id="signing-dev-manual-link">&disabledUnsigned.devInfo.linkToManual;</label>&disabledUnsigned.devInfo.end; + </description> + </vbox> + <vbox id="plugindeprecation-notice" class="alert-container"> + <hbox class="alert"> + <description>&pluginDeprecation.description;   + <label class="text-link plain" id="plugindeprecation-learnmore-link">&pluginDeprecation.learnMore;</label> + </description> + </hbox> + </vbox> + <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"/> + </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 id="detail-screenshot-box" pack="center" hidden="true"> <!-- Necessary to work around bug 394738 --> + <image id="detail-screenshot"/> + </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"/> + <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> + </vbox> + </deck> + </vbox> + </hbox> +</page> diff --git a/toolkit/mozapps/webextensions/content/gmpPrefs.xul b/toolkit/mozapps/webextensions/content/gmpPrefs.xul new file mode 100644 index 000000000..ea7ee92fa --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/list.js b/toolkit/mozapps/webextensions/content/list.js new file mode 100644 index 000000000..a31922703 --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/list.xul b/toolkit/mozapps/webextensions/content/list.xul new file mode 100644 index 000000000..65efeb6a2 --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/newaddon.js b/toolkit/mozapps/webextensions/content/newaddon.js new file mode 100644 index 000000000..b1ad5631b --- /dev/null +++ b/toolkit/mozapps/webextensions/content/newaddon.js @@ -0,0 +1,137 @@ +/* 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.seen || + !(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; + } + + // Only mark the add-on as seen if the page actually gets focus + if (document.hasFocus()) { + aAddon.markAsSeen(); + } + else { + document.addEventListener("focus", () => aAddon.markAsSeen(), false); + } + + 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/toolkit/mozapps/webextensions/content/newaddon.xul b/toolkit/mozapps/webextensions/content/newaddon.xul new file mode 100644 index 000000000..1d8545249 --- /dev/null +++ b/toolkit/mozapps/webextensions/content/newaddon.xul @@ -0,0 +1,67 @@ +<?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> + <vbox id="restartPanel"> + <description id="restartMessage">&restartMessage;</description> + <hbox id="restartPanelButtons"> + <button id="restart-button" label="&restartButton;" oncommand="restartClicked()"/> + <button id="cancel-button" label="&cancelButton;" oncommand="cancelClicked()"/> + </hbox> + </vbox> + </deck> + </vbox> + + <spacer id="spacer-end"/> + </scrollbox> +</page> diff --git a/toolkit/mozapps/webextensions/content/pluginPrefs.xul b/toolkit/mozapps/webextensions/content/pluginPrefs.xul new file mode 100644 index 000000000..c3fdbfa5b --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/setting.xml b/toolkit/mozapps/webextensions/content/setting.xml new file mode 100644 index 000000000..2b70eb0d0 --- /dev/null +++ b/toolkit/mozapps/webextensions/content/setting.xml @@ -0,0 +1,486 @@ +<?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; +]> + +<!-- import-globals-from extensions.js --> + +<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="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[ + this.value = Preferences.get(this.pref, ""); + ]]> + </body> + </method> + + <method name="valueToPreference"> + <body> + <![CDATA[ + Preferences.set(this.pref, this.value); + ]]> + </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 = Preferences.get(this.pref, ""); + ]]> + </body> + </method> + + <method name="valueToPreference"> + <body> + <![CDATA[ + Preferences.set(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 = Preferences.get(this.pref, "").toString(); + + 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") { + val = val == "true"; + } else if (/^-?\d+$/.test(val)) { + val = parseInt(val, 10); + } + Preferences.set(this.pref, val); + ]]> + </body> + </method> + + <field name="control">this.getElementsByTagName(this.getAttribute("type") == "radio" ? "radiogroup" : "menulist")[0];</field> + </implementation> + </binding> +</bindings> diff --git a/toolkit/mozapps/webextensions/content/update.js b/toolkit/mozapps/webextensions/content/update.js new file mode 100644 index 000000000..80d0fa688 --- /dev/null +++ b/toolkit/mozapps/webextensions/content/update.js @@ -0,0 +1,663 @@ +// -*- 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() + { + 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(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(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(aElementIDToShow) + { + if (this.errorItems.length > 0) + document.getElementById(aElementIDToShow).hidden = false; + }, + + onWizardClose: function(aEvent) + { + return this.onWizardCancel(); + }, + + onWizardCancel: function() + { + 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) { + gInstallingPage.startInstalls( + Array.from(gUpdateWizard.addonInstalls.values())); + } + 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() + { + Services.io.offline = false; + return true; + }, + + toggleOffline: function() + { + var nextbtn = document.documentElement.getButton("next"); + nextbtn.disabled = !nextbtn.disabled; + } +} + +// Addon listener to count addons enabled/disabled by metadata checks +var listener = { + onDisabled: function(aAddon) { + gUpdateWizard.affectedAddonIDs.add(aAddon.id); + gUpdateWizard.metadataDisabled++; + }, + onEnabled: function(aAddon) { + gUpdateWizard.affectedAddonIDs.delete(aAddon.id); + gUpdateWizard.metadataEnabled++; + } +}; + +var gVersionInfoPage = { + _completeCount: 0, + _totalCount: 0, + _versionInfoDone: false, + onPageShow: Task.async(function*() { + 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, + // excluding the hotfix add-on. + let idlist = Array.from(gUpdateWizard.affectedAddonIDs).filter( + a => a.id != AddonManager.hotfixID); + 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... + gUpdateWizard.addons = fetchedAddons.filter(a => a); + 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() { + AddonManager.removeAddonListener(listener); + AddonManagerPrivate.recordSimpleMeasure("appUpdate_disabled", + gUpdateWizard.disabled); + AddonManagerPrivate.recordSimpleMeasure("appUpdate_metadata_enabled", + gUpdateWizard.metadataEnabled); + AddonManagerPrivate.recordSimpleMeasure("appUpdate_metadata_disabled", + gUpdateWizard.metadataDisabled); + // Record 0 for these here in case we exit early; values will be replaced + // later if we actually upgrade any. + AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgraded", 0); + AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgradeFailed", 0); + AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgradeDeclined", 0); + // Filter out any add-ons that are now enabled. + let addonList = gUpdateWizard.addons.map(a => a.id + ":" + a.appDisabled); + logger.debug("VersionInfo updates finished: found " + addonList.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) { + let installs = Array.from(gUpdateWizard.addonInstalls.values()); + gInstallingPage.startInstalls(installs); + } + 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(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(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.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() + { + 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() { + 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(aAddon, aInstall) { + logger.debug("UpdatePage got an update for " + aAddon.id + ": " + aAddon.version); + gUpdateWizard.addonsToUpdate.push(aInstall); + }, + + onUpdateFinished: function(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() + { + 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(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() + { + 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(aInstallList) { + if (!gUpdateWizard.xpinstallEnabled) { + return; + } + + let installs = Array.from(aInstallList).map(a => a.existingAddon.id); + logger.debug("Start installs for " + installs.toSource()); + this._errors = []; + this._installs = aInstallList; + this._installing = true; + this.startNextInstall(); + }, + + onPageShow: function() + { + 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() { + 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); + AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgraded", + gUpdateWizard.upgraded); + AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgradeFailed", + gUpdateWizard.upgradeFailed); + AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgradeDeclined", + gUpdateWizard.upgradeDeclined); + 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(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(aInstall) { + if (gUpdateWizard.shuttingDown) { + return; + } + var downloadProgress = document.getElementById("downloadProgress"); + downloadProgress.value = Math.ceil(100 * aInstall.progress / aInstall.maxProgress); + }, + + onDownloadEnded: function(aInstall) { + }, + + onDownloadFailed: function(aInstall) { + this._errors.push(aInstall); + + gUpdateWizard.upgradeFailed++; + this.startNextInstall(); + }, + + onInstallStarted: function(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(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(aInstall) { + this._errors.push(aInstall); + + gUpdateWizard.upgradeFailed++; + this.startNextInstall(); + } +}; + +var gInstallErrorsPage = { + onPageShow: function() + { + 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() + { + 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() + { + 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(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/toolkit/mozapps/webextensions/content/update.xul b/toolkit/mozapps/webextensions/content/update.xul new file mode 100644 index 000000000..745983814 --- /dev/null +++ b/toolkit/mozapps/webextensions/content/update.xul @@ -0,0 +1,194 @@ +<?xml version="1.0"?> + +# -*- Mode: Java; 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"> + <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"/> +#ifndef XP_MACOSX + <label>&clickFinish.label;</label> +#else + <label>&clickFinish.labelMac;</label> +#endif + <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"/> +#ifndef XP_MACOSX + <label>&clickFinish.label;</label> +#else + <label>&clickFinish.labelMac;</label> +#endif + <separator class="thin"/> + </wizardpage> + + <wizardpage id="adminDisabled" pageid="adminDisabled" + label="&adminDisabled.wizard.title;" + onpageshow="gAdminDisabledPage.onPageShow();"> + <separator/> + <hbox class="alertBox" align="top"> + <description flex="1">&adminDisabled.warning.label;</description> + </hbox> + <separator flex="1"/> +#ifndef XP_MACOSX + <label>&clickFinish.label;</label> +#else + <label>&clickFinish.labelMac;</label> +#endif + <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"/> +#ifndef XP_MACOSX + <label>&clickFinish.label;</label> +#else + <label>&clickFinish.labelMac;</label> +#endif + <separator class="thin"/> + </wizardpage> + +</wizard> + diff --git a/toolkit/mozapps/webextensions/content/updateinfo.xsl b/toolkit/mozapps/webextensions/content/updateinfo.xsl new file mode 100644 index 000000000..5fcccd6d7 --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/xpinstallConfirm.css b/toolkit/mozapps/webextensions/content/xpinstallConfirm.css new file mode 100644 index 000000000..583facfec --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/xpinstallConfirm.js b/toolkit/mozapps/webextensions/content/xpinstallConfirm.js new file mode 100644 index 000000000..5660cdaaf --- /dev/null +++ b/toolkit/mozapps/webextensions/content/xpinstallConfirm.js @@ -0,0 +1,196 @@ +// -*- 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() +{ + Components.classes["@mozilla.org/base/telemetry;1"]. + getService(Components.interfaces.nsITelemetry). + getHistogramById("SECURITY_UI"). + add(Components.interfaces.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH); + // 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/toolkit/mozapps/webextensions/content/xpinstallConfirm.xul b/toolkit/mozapps/webextensions/content/xpinstallConfirm.xul new file mode 100644 index 000000000..f1c29eb73 --- /dev/null +++ b/toolkit/mozapps/webextensions/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/toolkit/mozapps/webextensions/content/xpinstallItem.xml b/toolkit/mozapps/webextensions/content/xpinstallItem.xml new file mode 100644 index 000000000..5146af84f --- /dev/null +++ b/toolkit/mozapps/webextensions/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> + |