michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * This is a default implementation of amIWebInstallListener that should work michael@0: * for most applications but can be overriden. It notifies the observer service michael@0: * about blocked installs. For normal installs it pops up an install michael@0: * confirmation when all the add-ons have been downloaded. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/AddonManager.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: const URI_XPINSTALL_DIALOG = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; michael@0: michael@0: // Installation can begin from any of these states michael@0: const READY_STATES = [ michael@0: AddonManager.STATE_AVAILABLE, michael@0: AddonManager.STATE_DOWNLOAD_FAILED, michael@0: AddonManager.STATE_INSTALL_FAILED, michael@0: AddonManager.STATE_CANCELLED michael@0: ]; michael@0: michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: const LOGGER_ID = "addons.weblistener"; michael@0: michael@0: // Create a new logger for use by the Addons Web Listener michael@0: // (Requires AddonManager.jsm) michael@0: let logger = Log.repository.getLogger(LOGGER_ID); michael@0: michael@0: function notifyObservers(aTopic, aWindow, aUri, aInstalls) { michael@0: let info = { michael@0: originatingWindow: aWindow, michael@0: originatingURI: aUri, michael@0: installs: aInstalls, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) michael@0: }; michael@0: Services.obs.notifyObservers(info, aTopic, null); michael@0: } michael@0: michael@0: /** michael@0: * Creates a new installer to monitor downloads and prompt to install when michael@0: * ready michael@0: * michael@0: * @param aWindow michael@0: * The window that started the installations michael@0: * @param aUrl michael@0: * The URL that started the installations michael@0: * @param aInstalls michael@0: * An array of AddonInstalls michael@0: */ michael@0: function Installer(aWindow, aUrl, aInstalls) { michael@0: this.window = aWindow; michael@0: this.url = aUrl; michael@0: this.downloads = aInstalls; michael@0: this.installed = []; michael@0: michael@0: notifyObservers("addon-install-started", aWindow, aUrl, aInstalls); michael@0: michael@0: aInstalls.forEach(function(aInstall) { michael@0: aInstall.addListener(this); michael@0: michael@0: // Start downloading if it hasn't already begun michael@0: if (READY_STATES.indexOf(aInstall.state) != -1) michael@0: aInstall.install(); michael@0: }, this); michael@0: michael@0: this.checkAllDownloaded(); michael@0: } michael@0: michael@0: Installer.prototype = { michael@0: window: null, michael@0: downloads: null, michael@0: installed: null, michael@0: isDownloading: true, michael@0: michael@0: /** michael@0: * Checks if all downloads are now complete and if so prompts to install. michael@0: */ michael@0: checkAllDownloaded: function Installer_checkAllDownloaded() { michael@0: // Prevent re-entrancy caused by the confirmation dialog cancelling unwanted michael@0: // installs. michael@0: if (!this.isDownloading) michael@0: return; michael@0: michael@0: var failed = []; michael@0: var installs = []; michael@0: michael@0: for (let install of this.downloads) { michael@0: switch (install.state) { michael@0: case AddonManager.STATE_AVAILABLE: michael@0: case AddonManager.STATE_DOWNLOADING: michael@0: // Exit early if any add-ons haven't started downloading yet or are michael@0: // still downloading michael@0: return; michael@0: case AddonManager.STATE_DOWNLOAD_FAILED: michael@0: failed.push(install); michael@0: break; michael@0: case AddonManager.STATE_DOWNLOADED: michael@0: // App disabled items are not compatible and so fail to install michael@0: if (install.addon.appDisabled) michael@0: failed.push(install); michael@0: else michael@0: installs.push(install); michael@0: michael@0: if (install.linkedInstalls) { michael@0: install.linkedInstalls.forEach(function(aInstall) { michael@0: aInstall.addListener(this); michael@0: // App disabled items are not compatible and so fail to install michael@0: if (aInstall.addon.appDisabled) michael@0: failed.push(aInstall); michael@0: else michael@0: installs.push(aInstall); michael@0: }, this); michael@0: } michael@0: break; michael@0: case AddonManager.STATE_CANCELLED: michael@0: // Just ignore cancelled downloads michael@0: break; michael@0: default: michael@0: logger.warn("Download of " + install.sourceURI.spec + " in unexpected state " + michael@0: install.state); michael@0: } michael@0: } michael@0: michael@0: this.isDownloading = false; michael@0: this.downloads = installs; michael@0: michael@0: if (failed.length > 0) { michael@0: // Stop listening and cancel any installs that are failed because of michael@0: // compatibility reasons. michael@0: failed.forEach(function(aInstall) { michael@0: if (aInstall.state == AddonManager.STATE_DOWNLOADED) { michael@0: aInstall.removeListener(this); michael@0: aInstall.cancel(); michael@0: } michael@0: }, this); michael@0: notifyObservers("addon-install-failed", this.window, this.url, failed); michael@0: } michael@0: michael@0: // If none of the downloads were successful then exit early michael@0: if (this.downloads.length == 0) michael@0: return; michael@0: michael@0: // Check for a custom installation prompt that may be provided by the michael@0: // applicaton michael@0: if ("@mozilla.org/addons/web-install-prompt;1" in Cc) { michael@0: try { michael@0: let prompt = Cc["@mozilla.org/addons/web-install-prompt;1"]. michael@0: getService(Ci.amIWebInstallPrompt); michael@0: prompt.confirm(this.window, this.url, this.downloads, this.downloads.length); michael@0: return; michael@0: } michael@0: catch (e) {} michael@0: } michael@0: michael@0: let args = {}; michael@0: args.url = this.url; michael@0: args.installs = this.downloads; michael@0: args.wrappedJSObject = args; michael@0: michael@0: try { michael@0: Cc["@mozilla.org/base/telemetry;1"]. michael@0: getService(Ci.nsITelemetry). michael@0: getHistogramById("SECURITY_UI"). michael@0: add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL); michael@0: Services.ww.openWindow(this.window, URI_XPINSTALL_DIALOG, michael@0: null, "chrome,modal,centerscreen", args); michael@0: } catch (e) { michael@0: this.downloads.forEach(function(aInstall) { michael@0: aInstall.removeListener(this); michael@0: // Cancel the installs, as currently there is no way to make them fail michael@0: // from here. michael@0: aInstall.cancel(); michael@0: }, this); michael@0: notifyObservers("addon-install-cancelled", this.window, this.url, michael@0: this.downloads); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Checks if all installs are now complete and if so notifies observers. michael@0: */ michael@0: checkAllInstalled: function Installer_checkAllInstalled() { michael@0: var failed = []; michael@0: michael@0: for (let install of this.downloads) { michael@0: switch(install.state) { michael@0: case AddonManager.STATE_DOWNLOADED: michael@0: case AddonManager.STATE_INSTALLING: michael@0: // Exit early if any add-ons haven't started installing yet or are michael@0: // still installing michael@0: return; michael@0: case AddonManager.STATE_INSTALL_FAILED: michael@0: failed.push(install); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: this.downloads = null; michael@0: michael@0: if (failed.length > 0) michael@0: notifyObservers("addon-install-failed", this.window, this.url, failed); michael@0: michael@0: if (this.installed.length > 0) michael@0: notifyObservers("addon-install-complete", this.window, this.url, this.installed); michael@0: this.installed = null; michael@0: }, michael@0: michael@0: onDownloadCancelled: function Installer_onDownloadCancelled(aInstall) { michael@0: aInstall.removeListener(this); michael@0: this.checkAllDownloaded(); michael@0: }, michael@0: michael@0: onDownloadFailed: function Installer_onDownloadFailed(aInstall) { michael@0: aInstall.removeListener(this); michael@0: this.checkAllDownloaded(); michael@0: }, michael@0: michael@0: onDownloadEnded: function Installer_onDownloadEnded(aInstall) { michael@0: this.checkAllDownloaded(); michael@0: return false; michael@0: }, michael@0: michael@0: onInstallCancelled: function Installer_onInstallCancelled(aInstall) { michael@0: aInstall.removeListener(this); michael@0: this.checkAllInstalled(); michael@0: }, michael@0: michael@0: onInstallFailed: function Installer_onInstallFailed(aInstall) { michael@0: aInstall.removeListener(this); michael@0: this.checkAllInstalled(); michael@0: }, michael@0: michael@0: onInstallEnded: function Installer_onInstallEnded(aInstall) { michael@0: aInstall.removeListener(this); michael@0: this.installed.push(aInstall); michael@0: michael@0: // If installing a theme that is disabled and can be enabled then enable it michael@0: if (aInstall.addon.type == "theme" && michael@0: aInstall.addon.userDisabled == true && michael@0: aInstall.addon.appDisabled == false) { michael@0: aInstall.addon.userDisabled = false; michael@0: } michael@0: michael@0: this.checkAllInstalled(); michael@0: } michael@0: }; michael@0: michael@0: function extWebInstallListener() { michael@0: } michael@0: michael@0: extWebInstallListener.prototype = { michael@0: /** michael@0: * @see amIWebInstallListener.idl michael@0: */ michael@0: onWebInstallDisabled: function extWebInstallListener_onWebInstallDisabled(aWindow, aUri, aInstalls) { michael@0: let info = { michael@0: originatingWindow: aWindow, michael@0: originatingURI: aUri, michael@0: installs: aInstalls, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) michael@0: }; michael@0: Services.obs.notifyObservers(info, "addon-install-disabled", null); michael@0: }, michael@0: michael@0: /** michael@0: * @see amIWebInstallListener.idl michael@0: */ michael@0: onWebInstallBlocked: function extWebInstallListener_onWebInstallBlocked(aWindow, aUri, aInstalls) { michael@0: let info = { michael@0: originatingWindow: aWindow, michael@0: originatingURI: aUri, michael@0: installs: aInstalls, michael@0: michael@0: install: function onWebInstallBlocked_install() { michael@0: new Installer(this.originatingWindow, this.originatingURI, this.installs); michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) michael@0: }; michael@0: Services.obs.notifyObservers(info, "addon-install-blocked", null); michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * @see amIWebInstallListener.idl michael@0: */ michael@0: onWebInstallRequested: function extWebInstallListener_onWebInstallRequested(aWindow, aUri, aInstalls) { michael@0: new Installer(aWindow, aUri, aInstalls); michael@0: michael@0: // We start the installs ourself michael@0: return false; michael@0: }, michael@0: michael@0: classDescription: "XPI Install Handler", michael@0: contractID: "@mozilla.org/addons/web-install-listener;1", michael@0: classID: Components.ID("{0f38e086-89a3-40a5-8ffc-9b694de1d04a}"), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallListener]) michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([extWebInstallListener]);