1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/extensions/amWebInstallListener.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,314 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/** 1.9 + * This is a default implementation of amIWebInstallListener that should work 1.10 + * for most applications but can be overriden. It notifies the observer service 1.11 + * about blocked installs. For normal installs it pops up an install 1.12 + * confirmation when all the add-ons have been downloaded. 1.13 + */ 1.14 + 1.15 +"use strict"; 1.16 + 1.17 +const Cc = Components.classes; 1.18 +const Ci = Components.interfaces; 1.19 +const Cr = Components.results; 1.20 +const Cu = Components.utils; 1.21 + 1.22 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.23 +Cu.import("resource://gre/modules/AddonManager.jsm"); 1.24 +Cu.import("resource://gre/modules/Services.jsm"); 1.25 + 1.26 +const URI_XPINSTALL_DIALOG = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; 1.27 + 1.28 +// Installation can begin from any of these states 1.29 +const READY_STATES = [ 1.30 + AddonManager.STATE_AVAILABLE, 1.31 + AddonManager.STATE_DOWNLOAD_FAILED, 1.32 + AddonManager.STATE_INSTALL_FAILED, 1.33 + AddonManager.STATE_CANCELLED 1.34 +]; 1.35 + 1.36 +Cu.import("resource://gre/modules/Log.jsm"); 1.37 +const LOGGER_ID = "addons.weblistener"; 1.38 + 1.39 +// Create a new logger for use by the Addons Web Listener 1.40 +// (Requires AddonManager.jsm) 1.41 +let logger = Log.repository.getLogger(LOGGER_ID); 1.42 + 1.43 +function notifyObservers(aTopic, aWindow, aUri, aInstalls) { 1.44 + let info = { 1.45 + originatingWindow: aWindow, 1.46 + originatingURI: aUri, 1.47 + installs: aInstalls, 1.48 + 1.49 + QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) 1.50 + }; 1.51 + Services.obs.notifyObservers(info, aTopic, null); 1.52 +} 1.53 + 1.54 +/** 1.55 + * Creates a new installer to monitor downloads and prompt to install when 1.56 + * ready 1.57 + * 1.58 + * @param aWindow 1.59 + * The window that started the installations 1.60 + * @param aUrl 1.61 + * The URL that started the installations 1.62 + * @param aInstalls 1.63 + * An array of AddonInstalls 1.64 + */ 1.65 +function Installer(aWindow, aUrl, aInstalls) { 1.66 + this.window = aWindow; 1.67 + this.url = aUrl; 1.68 + this.downloads = aInstalls; 1.69 + this.installed = []; 1.70 + 1.71 + notifyObservers("addon-install-started", aWindow, aUrl, aInstalls); 1.72 + 1.73 + aInstalls.forEach(function(aInstall) { 1.74 + aInstall.addListener(this); 1.75 + 1.76 + // Start downloading if it hasn't already begun 1.77 + if (READY_STATES.indexOf(aInstall.state) != -1) 1.78 + aInstall.install(); 1.79 + }, this); 1.80 + 1.81 + this.checkAllDownloaded(); 1.82 +} 1.83 + 1.84 +Installer.prototype = { 1.85 + window: null, 1.86 + downloads: null, 1.87 + installed: null, 1.88 + isDownloading: true, 1.89 + 1.90 + /** 1.91 + * Checks if all downloads are now complete and if so prompts to install. 1.92 + */ 1.93 + checkAllDownloaded: function Installer_checkAllDownloaded() { 1.94 + // Prevent re-entrancy caused by the confirmation dialog cancelling unwanted 1.95 + // installs. 1.96 + if (!this.isDownloading) 1.97 + return; 1.98 + 1.99 + var failed = []; 1.100 + var installs = []; 1.101 + 1.102 + for (let install of this.downloads) { 1.103 + switch (install.state) { 1.104 + case AddonManager.STATE_AVAILABLE: 1.105 + case AddonManager.STATE_DOWNLOADING: 1.106 + // Exit early if any add-ons haven't started downloading yet or are 1.107 + // still downloading 1.108 + return; 1.109 + case AddonManager.STATE_DOWNLOAD_FAILED: 1.110 + failed.push(install); 1.111 + break; 1.112 + case AddonManager.STATE_DOWNLOADED: 1.113 + // App disabled items are not compatible and so fail to install 1.114 + if (install.addon.appDisabled) 1.115 + failed.push(install); 1.116 + else 1.117 + installs.push(install); 1.118 + 1.119 + if (install.linkedInstalls) { 1.120 + install.linkedInstalls.forEach(function(aInstall) { 1.121 + aInstall.addListener(this); 1.122 + // App disabled items are not compatible and so fail to install 1.123 + if (aInstall.addon.appDisabled) 1.124 + failed.push(aInstall); 1.125 + else 1.126 + installs.push(aInstall); 1.127 + }, this); 1.128 + } 1.129 + break; 1.130 + case AddonManager.STATE_CANCELLED: 1.131 + // Just ignore cancelled downloads 1.132 + break; 1.133 + default: 1.134 + logger.warn("Download of " + install.sourceURI.spec + " in unexpected state " + 1.135 + install.state); 1.136 + } 1.137 + } 1.138 + 1.139 + this.isDownloading = false; 1.140 + this.downloads = installs; 1.141 + 1.142 + if (failed.length > 0) { 1.143 + // Stop listening and cancel any installs that are failed because of 1.144 + // compatibility reasons. 1.145 + failed.forEach(function(aInstall) { 1.146 + if (aInstall.state == AddonManager.STATE_DOWNLOADED) { 1.147 + aInstall.removeListener(this); 1.148 + aInstall.cancel(); 1.149 + } 1.150 + }, this); 1.151 + notifyObservers("addon-install-failed", this.window, this.url, failed); 1.152 + } 1.153 + 1.154 + // If none of the downloads were successful then exit early 1.155 + if (this.downloads.length == 0) 1.156 + return; 1.157 + 1.158 + // Check for a custom installation prompt that may be provided by the 1.159 + // applicaton 1.160 + if ("@mozilla.org/addons/web-install-prompt;1" in Cc) { 1.161 + try { 1.162 + let prompt = Cc["@mozilla.org/addons/web-install-prompt;1"]. 1.163 + getService(Ci.amIWebInstallPrompt); 1.164 + prompt.confirm(this.window, this.url, this.downloads, this.downloads.length); 1.165 + return; 1.166 + } 1.167 + catch (e) {} 1.168 + } 1.169 + 1.170 + let args = {}; 1.171 + args.url = this.url; 1.172 + args.installs = this.downloads; 1.173 + args.wrappedJSObject = args; 1.174 + 1.175 + try { 1.176 + Cc["@mozilla.org/base/telemetry;1"]. 1.177 + getService(Ci.nsITelemetry). 1.178 + getHistogramById("SECURITY_UI"). 1.179 + add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL); 1.180 + Services.ww.openWindow(this.window, URI_XPINSTALL_DIALOG, 1.181 + null, "chrome,modal,centerscreen", args); 1.182 + } catch (e) { 1.183 + this.downloads.forEach(function(aInstall) { 1.184 + aInstall.removeListener(this); 1.185 + // Cancel the installs, as currently there is no way to make them fail 1.186 + // from here. 1.187 + aInstall.cancel(); 1.188 + }, this); 1.189 + notifyObservers("addon-install-cancelled", this.window, this.url, 1.190 + this.downloads); 1.191 + } 1.192 + }, 1.193 + 1.194 + /** 1.195 + * Checks if all installs are now complete and if so notifies observers. 1.196 + */ 1.197 + checkAllInstalled: function Installer_checkAllInstalled() { 1.198 + var failed = []; 1.199 + 1.200 + for (let install of this.downloads) { 1.201 + switch(install.state) { 1.202 + case AddonManager.STATE_DOWNLOADED: 1.203 + case AddonManager.STATE_INSTALLING: 1.204 + // Exit early if any add-ons haven't started installing yet or are 1.205 + // still installing 1.206 + return; 1.207 + case AddonManager.STATE_INSTALL_FAILED: 1.208 + failed.push(install); 1.209 + break; 1.210 + } 1.211 + } 1.212 + 1.213 + this.downloads = null; 1.214 + 1.215 + if (failed.length > 0) 1.216 + notifyObservers("addon-install-failed", this.window, this.url, failed); 1.217 + 1.218 + if (this.installed.length > 0) 1.219 + notifyObservers("addon-install-complete", this.window, this.url, this.installed); 1.220 + this.installed = null; 1.221 + }, 1.222 + 1.223 + onDownloadCancelled: function Installer_onDownloadCancelled(aInstall) { 1.224 + aInstall.removeListener(this); 1.225 + this.checkAllDownloaded(); 1.226 + }, 1.227 + 1.228 + onDownloadFailed: function Installer_onDownloadFailed(aInstall) { 1.229 + aInstall.removeListener(this); 1.230 + this.checkAllDownloaded(); 1.231 + }, 1.232 + 1.233 + onDownloadEnded: function Installer_onDownloadEnded(aInstall) { 1.234 + this.checkAllDownloaded(); 1.235 + return false; 1.236 + }, 1.237 + 1.238 + onInstallCancelled: function Installer_onInstallCancelled(aInstall) { 1.239 + aInstall.removeListener(this); 1.240 + this.checkAllInstalled(); 1.241 + }, 1.242 + 1.243 + onInstallFailed: function Installer_onInstallFailed(aInstall) { 1.244 + aInstall.removeListener(this); 1.245 + this.checkAllInstalled(); 1.246 + }, 1.247 + 1.248 + onInstallEnded: function Installer_onInstallEnded(aInstall) { 1.249 + aInstall.removeListener(this); 1.250 + this.installed.push(aInstall); 1.251 + 1.252 + // If installing a theme that is disabled and can be enabled then enable it 1.253 + if (aInstall.addon.type == "theme" && 1.254 + aInstall.addon.userDisabled == true && 1.255 + aInstall.addon.appDisabled == false) { 1.256 + aInstall.addon.userDisabled = false; 1.257 + } 1.258 + 1.259 + this.checkAllInstalled(); 1.260 + } 1.261 +}; 1.262 + 1.263 +function extWebInstallListener() { 1.264 +} 1.265 + 1.266 +extWebInstallListener.prototype = { 1.267 + /** 1.268 + * @see amIWebInstallListener.idl 1.269 + */ 1.270 + onWebInstallDisabled: function extWebInstallListener_onWebInstallDisabled(aWindow, aUri, aInstalls) { 1.271 + let info = { 1.272 + originatingWindow: aWindow, 1.273 + originatingURI: aUri, 1.274 + installs: aInstalls, 1.275 + 1.276 + QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) 1.277 + }; 1.278 + Services.obs.notifyObservers(info, "addon-install-disabled", null); 1.279 + }, 1.280 + 1.281 + /** 1.282 + * @see amIWebInstallListener.idl 1.283 + */ 1.284 + onWebInstallBlocked: function extWebInstallListener_onWebInstallBlocked(aWindow, aUri, aInstalls) { 1.285 + let info = { 1.286 + originatingWindow: aWindow, 1.287 + originatingURI: aUri, 1.288 + installs: aInstalls, 1.289 + 1.290 + install: function onWebInstallBlocked_install() { 1.291 + new Installer(this.originatingWindow, this.originatingURI, this.installs); 1.292 + }, 1.293 + 1.294 + QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) 1.295 + }; 1.296 + Services.obs.notifyObservers(info, "addon-install-blocked", null); 1.297 + 1.298 + return false; 1.299 + }, 1.300 + 1.301 + /** 1.302 + * @see amIWebInstallListener.idl 1.303 + */ 1.304 + onWebInstallRequested: function extWebInstallListener_onWebInstallRequested(aWindow, aUri, aInstalls) { 1.305 + new Installer(aWindow, aUri, aInstalls); 1.306 + 1.307 + // We start the installs ourself 1.308 + return false; 1.309 + }, 1.310 + 1.311 + classDescription: "XPI Install Handler", 1.312 + contractID: "@mozilla.org/addons/web-install-listener;1", 1.313 + classID: Components.ID("{0f38e086-89a3-40a5-8ffc-9b694de1d04a}"), 1.314 + QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallListener]) 1.315 +}; 1.316 + 1.317 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([extWebInstallListener]);