|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 /** |
|
6 * This is a default implementation of amIWebInstallListener that should work |
|
7 * for most applications but can be overriden. It notifies the observer service |
|
8 * about blocked installs. For normal installs it pops up an install |
|
9 * confirmation when all the add-ons have been downloaded. |
|
10 */ |
|
11 |
|
12 "use strict"; |
|
13 |
|
14 const Cc = Components.classes; |
|
15 const Ci = Components.interfaces; |
|
16 const Cr = Components.results; |
|
17 const Cu = Components.utils; |
|
18 |
|
19 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
20 Cu.import("resource://gre/modules/AddonManager.jsm"); |
|
21 Cu.import("resource://gre/modules/Services.jsm"); |
|
22 |
|
23 const URI_XPINSTALL_DIALOG = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; |
|
24 |
|
25 // Installation can begin from any of these states |
|
26 const READY_STATES = [ |
|
27 AddonManager.STATE_AVAILABLE, |
|
28 AddonManager.STATE_DOWNLOAD_FAILED, |
|
29 AddonManager.STATE_INSTALL_FAILED, |
|
30 AddonManager.STATE_CANCELLED |
|
31 ]; |
|
32 |
|
33 Cu.import("resource://gre/modules/Log.jsm"); |
|
34 const LOGGER_ID = "addons.weblistener"; |
|
35 |
|
36 // Create a new logger for use by the Addons Web Listener |
|
37 // (Requires AddonManager.jsm) |
|
38 let logger = Log.repository.getLogger(LOGGER_ID); |
|
39 |
|
40 function notifyObservers(aTopic, aWindow, aUri, aInstalls) { |
|
41 let info = { |
|
42 originatingWindow: aWindow, |
|
43 originatingURI: aUri, |
|
44 installs: aInstalls, |
|
45 |
|
46 QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) |
|
47 }; |
|
48 Services.obs.notifyObservers(info, aTopic, null); |
|
49 } |
|
50 |
|
51 /** |
|
52 * Creates a new installer to monitor downloads and prompt to install when |
|
53 * ready |
|
54 * |
|
55 * @param aWindow |
|
56 * The window that started the installations |
|
57 * @param aUrl |
|
58 * The URL that started the installations |
|
59 * @param aInstalls |
|
60 * An array of AddonInstalls |
|
61 */ |
|
62 function Installer(aWindow, aUrl, aInstalls) { |
|
63 this.window = aWindow; |
|
64 this.url = aUrl; |
|
65 this.downloads = aInstalls; |
|
66 this.installed = []; |
|
67 |
|
68 notifyObservers("addon-install-started", aWindow, aUrl, aInstalls); |
|
69 |
|
70 aInstalls.forEach(function(aInstall) { |
|
71 aInstall.addListener(this); |
|
72 |
|
73 // Start downloading if it hasn't already begun |
|
74 if (READY_STATES.indexOf(aInstall.state) != -1) |
|
75 aInstall.install(); |
|
76 }, this); |
|
77 |
|
78 this.checkAllDownloaded(); |
|
79 } |
|
80 |
|
81 Installer.prototype = { |
|
82 window: null, |
|
83 downloads: null, |
|
84 installed: null, |
|
85 isDownloading: true, |
|
86 |
|
87 /** |
|
88 * Checks if all downloads are now complete and if so prompts to install. |
|
89 */ |
|
90 checkAllDownloaded: function Installer_checkAllDownloaded() { |
|
91 // Prevent re-entrancy caused by the confirmation dialog cancelling unwanted |
|
92 // installs. |
|
93 if (!this.isDownloading) |
|
94 return; |
|
95 |
|
96 var failed = []; |
|
97 var installs = []; |
|
98 |
|
99 for (let install of this.downloads) { |
|
100 switch (install.state) { |
|
101 case AddonManager.STATE_AVAILABLE: |
|
102 case AddonManager.STATE_DOWNLOADING: |
|
103 // Exit early if any add-ons haven't started downloading yet or are |
|
104 // still downloading |
|
105 return; |
|
106 case AddonManager.STATE_DOWNLOAD_FAILED: |
|
107 failed.push(install); |
|
108 break; |
|
109 case AddonManager.STATE_DOWNLOADED: |
|
110 // App disabled items are not compatible and so fail to install |
|
111 if (install.addon.appDisabled) |
|
112 failed.push(install); |
|
113 else |
|
114 installs.push(install); |
|
115 |
|
116 if (install.linkedInstalls) { |
|
117 install.linkedInstalls.forEach(function(aInstall) { |
|
118 aInstall.addListener(this); |
|
119 // App disabled items are not compatible and so fail to install |
|
120 if (aInstall.addon.appDisabled) |
|
121 failed.push(aInstall); |
|
122 else |
|
123 installs.push(aInstall); |
|
124 }, this); |
|
125 } |
|
126 break; |
|
127 case AddonManager.STATE_CANCELLED: |
|
128 // Just ignore cancelled downloads |
|
129 break; |
|
130 default: |
|
131 logger.warn("Download of " + install.sourceURI.spec + " in unexpected state " + |
|
132 install.state); |
|
133 } |
|
134 } |
|
135 |
|
136 this.isDownloading = false; |
|
137 this.downloads = installs; |
|
138 |
|
139 if (failed.length > 0) { |
|
140 // Stop listening and cancel any installs that are failed because of |
|
141 // compatibility reasons. |
|
142 failed.forEach(function(aInstall) { |
|
143 if (aInstall.state == AddonManager.STATE_DOWNLOADED) { |
|
144 aInstall.removeListener(this); |
|
145 aInstall.cancel(); |
|
146 } |
|
147 }, this); |
|
148 notifyObservers("addon-install-failed", this.window, this.url, failed); |
|
149 } |
|
150 |
|
151 // If none of the downloads were successful then exit early |
|
152 if (this.downloads.length == 0) |
|
153 return; |
|
154 |
|
155 // Check for a custom installation prompt that may be provided by the |
|
156 // applicaton |
|
157 if ("@mozilla.org/addons/web-install-prompt;1" in Cc) { |
|
158 try { |
|
159 let prompt = Cc["@mozilla.org/addons/web-install-prompt;1"]. |
|
160 getService(Ci.amIWebInstallPrompt); |
|
161 prompt.confirm(this.window, this.url, this.downloads, this.downloads.length); |
|
162 return; |
|
163 } |
|
164 catch (e) {} |
|
165 } |
|
166 |
|
167 let args = {}; |
|
168 args.url = this.url; |
|
169 args.installs = this.downloads; |
|
170 args.wrappedJSObject = args; |
|
171 |
|
172 try { |
|
173 Cc["@mozilla.org/base/telemetry;1"]. |
|
174 getService(Ci.nsITelemetry). |
|
175 getHistogramById("SECURITY_UI"). |
|
176 add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL); |
|
177 Services.ww.openWindow(this.window, URI_XPINSTALL_DIALOG, |
|
178 null, "chrome,modal,centerscreen", args); |
|
179 } catch (e) { |
|
180 this.downloads.forEach(function(aInstall) { |
|
181 aInstall.removeListener(this); |
|
182 // Cancel the installs, as currently there is no way to make them fail |
|
183 // from here. |
|
184 aInstall.cancel(); |
|
185 }, this); |
|
186 notifyObservers("addon-install-cancelled", this.window, this.url, |
|
187 this.downloads); |
|
188 } |
|
189 }, |
|
190 |
|
191 /** |
|
192 * Checks if all installs are now complete and if so notifies observers. |
|
193 */ |
|
194 checkAllInstalled: function Installer_checkAllInstalled() { |
|
195 var failed = []; |
|
196 |
|
197 for (let install of this.downloads) { |
|
198 switch(install.state) { |
|
199 case AddonManager.STATE_DOWNLOADED: |
|
200 case AddonManager.STATE_INSTALLING: |
|
201 // Exit early if any add-ons haven't started installing yet or are |
|
202 // still installing |
|
203 return; |
|
204 case AddonManager.STATE_INSTALL_FAILED: |
|
205 failed.push(install); |
|
206 break; |
|
207 } |
|
208 } |
|
209 |
|
210 this.downloads = null; |
|
211 |
|
212 if (failed.length > 0) |
|
213 notifyObservers("addon-install-failed", this.window, this.url, failed); |
|
214 |
|
215 if (this.installed.length > 0) |
|
216 notifyObservers("addon-install-complete", this.window, this.url, this.installed); |
|
217 this.installed = null; |
|
218 }, |
|
219 |
|
220 onDownloadCancelled: function Installer_onDownloadCancelled(aInstall) { |
|
221 aInstall.removeListener(this); |
|
222 this.checkAllDownloaded(); |
|
223 }, |
|
224 |
|
225 onDownloadFailed: function Installer_onDownloadFailed(aInstall) { |
|
226 aInstall.removeListener(this); |
|
227 this.checkAllDownloaded(); |
|
228 }, |
|
229 |
|
230 onDownloadEnded: function Installer_onDownloadEnded(aInstall) { |
|
231 this.checkAllDownloaded(); |
|
232 return false; |
|
233 }, |
|
234 |
|
235 onInstallCancelled: function Installer_onInstallCancelled(aInstall) { |
|
236 aInstall.removeListener(this); |
|
237 this.checkAllInstalled(); |
|
238 }, |
|
239 |
|
240 onInstallFailed: function Installer_onInstallFailed(aInstall) { |
|
241 aInstall.removeListener(this); |
|
242 this.checkAllInstalled(); |
|
243 }, |
|
244 |
|
245 onInstallEnded: function Installer_onInstallEnded(aInstall) { |
|
246 aInstall.removeListener(this); |
|
247 this.installed.push(aInstall); |
|
248 |
|
249 // If installing a theme that is disabled and can be enabled then enable it |
|
250 if (aInstall.addon.type == "theme" && |
|
251 aInstall.addon.userDisabled == true && |
|
252 aInstall.addon.appDisabled == false) { |
|
253 aInstall.addon.userDisabled = false; |
|
254 } |
|
255 |
|
256 this.checkAllInstalled(); |
|
257 } |
|
258 }; |
|
259 |
|
260 function extWebInstallListener() { |
|
261 } |
|
262 |
|
263 extWebInstallListener.prototype = { |
|
264 /** |
|
265 * @see amIWebInstallListener.idl |
|
266 */ |
|
267 onWebInstallDisabled: function extWebInstallListener_onWebInstallDisabled(aWindow, aUri, aInstalls) { |
|
268 let info = { |
|
269 originatingWindow: aWindow, |
|
270 originatingURI: aUri, |
|
271 installs: aInstalls, |
|
272 |
|
273 QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) |
|
274 }; |
|
275 Services.obs.notifyObservers(info, "addon-install-disabled", null); |
|
276 }, |
|
277 |
|
278 /** |
|
279 * @see amIWebInstallListener.idl |
|
280 */ |
|
281 onWebInstallBlocked: function extWebInstallListener_onWebInstallBlocked(aWindow, aUri, aInstalls) { |
|
282 let info = { |
|
283 originatingWindow: aWindow, |
|
284 originatingURI: aUri, |
|
285 installs: aInstalls, |
|
286 |
|
287 install: function onWebInstallBlocked_install() { |
|
288 new Installer(this.originatingWindow, this.originatingURI, this.installs); |
|
289 }, |
|
290 |
|
291 QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) |
|
292 }; |
|
293 Services.obs.notifyObservers(info, "addon-install-blocked", null); |
|
294 |
|
295 return false; |
|
296 }, |
|
297 |
|
298 /** |
|
299 * @see amIWebInstallListener.idl |
|
300 */ |
|
301 onWebInstallRequested: function extWebInstallListener_onWebInstallRequested(aWindow, aUri, aInstalls) { |
|
302 new Installer(aWindow, aUri, aInstalls); |
|
303 |
|
304 // We start the installs ourself |
|
305 return false; |
|
306 }, |
|
307 |
|
308 classDescription: "XPI Install Handler", |
|
309 contractID: "@mozilla.org/addons/web-install-listener;1", |
|
310 classID: Components.ID("{0f38e086-89a3-40a5-8ffc-9b694de1d04a}"), |
|
311 QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallListener]) |
|
312 }; |
|
313 |
|
314 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([extWebInstallListener]); |