Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 'use strict';
7 Cu.import("resource://gre/modules/Services.jsm");
8 let gAppUpdater;
10 let AboutFlyoutPanel = {
11 init: function() {
12 if (this._isInitialized) {
13 Cu.reportError("Attempted to initialize AboutFlyoutPanel more than once");
14 }
16 this._isInitialized = true;
18 let self = this;
19 this._elements = {};
20 [
21 ['versionLabel', 'about-version-label'],
22 ['AboutFlyoutPanel', 'about-flyoutpanel'],
23 ].forEach(function(aElement) {
24 let [name, id] = aElement;
25 XPCOMUtils.defineLazyGetter(self._elements, name, function() {
26 return document.getElementById(id);
27 });
28 });
30 this._topmostElement = this._elements.AboutFlyoutPanel;
32 // Include the build ID if this is an "a#" (nightly or aurora) build
33 let version = Services.appinfo.version;
34 if (/a\d+$/.test(version)) {
35 let buildID = Services.appinfo.appBuildID;
36 let buildDate = buildID.slice(0,4) + "-" + buildID.slice(4,6) +
37 "-" + buildID.slice(6,8);
38 this._elements.versionLabel.textContent +=" (" + buildDate + ")";
39 }
41 window.addEventListener('MozFlyoutPanelShowing', this, false);
42 window.addEventListener('MozFlyoutPanelHiding', this, false);
44 #if MOZ_UPDATE_CHANNEL != release
45 let defaults = Services.prefs.getDefaultBranch("");
46 let channelLabel = document.getElementById("currentChannel");
47 channelLabel.value = defaults.getCharPref("app.update.channel");
48 #endif
49 },
51 onPolicyClick: function(aEvent) {
52 if (aEvent.button != 0) {
53 return;
54 }
55 let url = Services.urlFormatter.formatURLPref("app.privacyURL");
56 BrowserUI.addAndShowTab(url, Browser.selectedTab);
57 },
59 handleEvent: function(aEvent) {
60 switch (aEvent.type) {
61 case 'MozFlyoutPanelShowing':
62 #ifdef MOZ_UPDATER
63 this.appUpdater = new appUpdater();
64 gAppUpdater = this.appUpdater;
65 #endif
66 break;
67 case 'MozFlyoutPanelHiding':
68 #ifdef MOZ_UPDATER
69 onUnload();
70 #endif
71 break;
72 }
73 }
74 };
76 #ifdef MOZ_UPDATER
77 function onUnload(aEvent) {
78 if (!gAppUpdater) {
79 return;
80 }
82 if (gAppUpdater.isChecking)
83 gAppUpdater.checker.stopChecking(Components.interfaces.nsIUpdateChecker.CURRENT_CHECK);
84 // Safe to call even when there isn't a download in progress.
85 gAppUpdater.removeDownloadListener();
86 gAppUpdater = null;
87 AboutFlyoutPanel.appUpdater = null;
88 }
90 function appUpdater()
91 {
92 this.updateDeck = document.getElementById("updateDeck");
94 XPCOMUtils.defineLazyServiceGetter(this, "aus",
95 "@mozilla.org/updates/update-service;1",
96 "nsIApplicationUpdateService");
97 XPCOMUtils.defineLazyServiceGetter(this, "checker",
98 "@mozilla.org/updates/update-checker;1",
99 "nsIUpdateChecker");
100 XPCOMUtils.defineLazyServiceGetter(this, "um",
101 "@mozilla.org/updates/update-manager;1",
102 "nsIUpdateManager");
104 this.bundle = Services.strings.
105 createBundle("chrome://browser/locale/browser.properties");
107 this.updateBtn = document.getElementById("updateButton");
109 // The button label value must be set so its height is correct.
110 this.setupUpdateButton("update.checkInsideButton");
112 let manualURL = Services.urlFormatter.formatURLPref("app.update.url.manual");
113 let manualLink = document.getElementById("manualLink");
114 manualLink.value = manualURL;
115 manualLink.href = manualURL;
116 document.getElementById("failedLink").href = manualURL;
118 if (this.updateDisabledAndLocked) {
119 this.selectPanel("adminDisabled");
120 return;
121 }
123 if (this.isPending || this.isApplied) {
124 this.setupUpdateButton("update.restart." +
125 (this.isMajor ? "upgradeButton" : "updateButton"));
126 return;
127 }
129 if (this.aus.isOtherInstanceHandlingUpdates) {
130 this.selectPanel("otherInstanceHandlingUpdates");
131 return;
132 }
134 if (this.isDownloading) {
135 this.startDownload();
136 return;
137 }
139 if (this.updateEnabled && this.updateAuto) {
140 this.selectPanel("checkingForUpdates");
141 this.isChecking = true;
142 this.checker.checkForUpdates(this.updateCheckListener, true);
143 return;
144 }
145 }
147 appUpdater.prototype =
148 {
149 // true when there is an update check in progress.
150 isChecking: false,
152 // true when there is an update already staged / ready to be applied.
153 get isPending() {
154 if (this.update) {
155 return this.update.state == "pending" ||
156 this.update.state == "pending-service";
157 }
158 return this.um.activeUpdate &&
159 (this.um.activeUpdate.state == "pending" ||
160 this.um.activeUpdate.state == "pending-service");
161 },
163 // true when there is an update already installed in the background.
164 get isApplied() {
165 if (this.update)
166 return this.update.state == "applied" ||
167 this.update.state == "applied-service";
168 return this.um.activeUpdate &&
169 (this.um.activeUpdate.state == "applied" ||
170 this.um.activeUpdate.state == "applied-service");
171 },
173 // true when there is an update download in progress.
174 get isDownloading() {
175 if (this.update)
176 return this.update.state == "downloading";
177 return this.um.activeUpdate &&
178 this.um.activeUpdate.state == "downloading";
179 },
181 // true when the update type is major.
182 get isMajor() {
183 if (this.update)
184 return this.update.type == "major";
185 return this.um.activeUpdate.type == "major";
186 },
188 // true when updating is disabled by an administrator.
189 get updateDisabledAndLocked() {
190 return !this.updateEnabled &&
191 Services.prefs.prefIsLocked("app.update.enabled");
192 },
194 // true when updating is enabled.
195 get updateEnabled() {
196 let updatesEnabled = true;
197 try {
198 updatesEnabled = Services.prefs.getBoolPref("app.update.metro.enabled");
199 }
200 catch (e) { }
201 if (!updatesEnabled) {
202 return false;
203 }
205 try {
206 updatesEnabled = Services.prefs.getBoolPref("app.update.enabled")
207 }
208 catch (e) { }
210 return updatesEnabled;
211 },
213 // true when updating in background is enabled.
214 get backgroundUpdateEnabled() {
215 return this.updateEnabled &&
216 gAppUpdater.aus.canStageUpdates;
217 },
219 // true when updating is automatic.
220 get updateAuto() {
221 try {
222 return Services.prefs.getBoolPref("app.update.auto");
223 }
224 catch (e) { }
225 return true; // Firefox default is true
226 },
228 /**
229 * Sets the deck's selected panel.
230 *
231 * @param aChildID
232 * The id of the deck's child to select.
233 */
234 selectPanel: function(aChildID) {
235 this.updateDeck.selectedPanel = document.getElementById(aChildID);
236 this.updateBtn.disabled = (aChildID != "updateButtonBox");
237 },
239 /**
240 * Sets the update button's label and accesskey.
241 *
242 * @param aKeyPrefix
243 * The prefix for the properties file entry to use for setting the
244 * label and accesskey.
245 */
246 setupUpdateButton: function(aKeyPrefix) {
247 this.updateBtn.label = this.bundle.GetStringFromName(aKeyPrefix + ".label");
248 this.updateBtn.accessKey = this.bundle.GetStringFromName(aKeyPrefix + ".accesskey");
249 if (!document.commandDispatcher.focusedElement ||
250 document.commandDispatcher.focusedElement == this.updateBtn)
251 this.updateBtn.focus();
252 },
254 /**
255 * Handles oncommand for the update button.
256 */
257 buttonOnCommand: function() {
258 if (this.isPending || this.isApplied) {
259 // Notify all windows that an application quit has been requested.
260 let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
261 createInstance(Components.interfaces.nsISupportsPRBool);
262 Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
264 // Something aborted the quit process.
265 if (cancelQuit.data)
266 return;
268 let appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"].
269 getService(Components.interfaces.nsIAppStartup);
271 // If already in safe mode restart in safe mode (bug 327119)
272 if (Services.appinfo.inSafeMode) {
273 appStartup.restartInSafeMode(Components.interfaces.nsIAppStartup.eAttemptQuit);
274 return;
275 }
277 Services.metro.updatePending = true;
278 appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit |
279 Components.interfaces.nsIAppStartup.eRestartTouchEnvironment);
280 return;
281 }
283 // XXX We can't create dialogs in metro, and we currently don't support addons, so
284 // commenting this out for now.
285 /* const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul";
286 // Firefox no longer displays a license for updates and the licenseURL check
287 // is just in case a distibution does.
288 if (this.update && (this.update.billboardURL || this.update.licenseURL ||
289 this.addons.length != 0)) {
290 var ary = null;
291 ary = Components.classes["@mozilla.org/supports-array;1"].
292 createInstance(Components.interfaces.nsISupportsArray);
293 ary.AppendElement(this.update);
294 var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no";
295 Services.ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, "", openFeatures, ary);
296 window.close();
297 return;
298 }*/
300 this.selectPanel("checkingForUpdates");
301 this.isChecking = true;
302 this.checker.checkForUpdates(this.updateCheckListener, true);
303 },
305 /**
306 * Implements nsIUpdateCheckListener. The methods implemented by
307 * nsIUpdateCheckListener are in a different scope from nsIIncrementalDownload
308 * to make it clear which are used by each interface.
309 */
310 updateCheckListener: {
311 /**
312 * See nsIUpdateService.idl
313 */
314 onCheckComplete: function(aRequest, aUpdates, aUpdateCount) {
315 gAppUpdater.isChecking = false;
316 gAppUpdater.update = gAppUpdater.aus.
317 selectUpdate(aUpdates, aUpdates.length);
318 if (!gAppUpdater.update) {
319 gAppUpdater.selectPanel("noUpdatesFound");
320 return;
321 }
323 if (!gAppUpdater.aus.canApplyUpdates) {
324 gAppUpdater.selectPanel("manualUpdate");
325 return;
326 }
328 // Firefox no longer displays a license for updates and the licenseURL
329 // check is just in case a distibution does.
330 if (gAppUpdater.update.billboardURL || gAppUpdater.update.licenseURL) {
331 gAppUpdater.selectPanel("updateButtonBox");
332 gAppUpdater.setupUpdateButton("update.openUpdateUI." +
333 (this.isMajor ? "upgradeButton"
334 : "applyButton"));
335 return;
336 }
338 if (!gAppUpdater.update.appVersion ||
339 Services.vc.compare(gAppUpdater.update.appVersion,
340 Services.appinfo.version) == 0) {
341 gAppUpdater.startDownload();
342 return;
343 }
345 gAppUpdater.checkAddonCompatibility();
346 },
348 /**
349 * See nsIUpdateService.idl
350 */
351 onError: function(aRequest, aUpdate) {
352 // Errors in the update check are treated as no updates found. If the
353 // update check fails repeatedly without a success the user will be
354 // notified with the normal app update user interface so this is safe.
355 gAppUpdater.isChecking = false;
356 gAppUpdater.selectPanel("noUpdatesFound");
357 },
359 /**
360 * See nsISupports.idl
361 */
362 QueryInterface: function(aIID) {
363 if (!aIID.equals(Components.interfaces.nsIUpdateCheckListener) &&
364 !aIID.equals(Components.interfaces.nsISupports))
365 throw Components.results.NS_ERROR_NO_INTERFACE;
366 return this;
367 }
368 },
370 /**
371 * Checks the compatibility of add-ons for the application update.
372 */
373 checkAddonCompatibility: function() {
374 try {
375 var hotfixID = Services.prefs.getCharPref(PREF_EM_HOTFIX_ID);
376 }
377 catch (e) { }
379 var self = this;
380 AddonManager.getAllAddons(function(aAddons) {
381 self.addons = [];
382 self.addonsCheckedCount = 0;
383 aAddons.forEach(function(aAddon) {
384 // Protect against code that overrides the add-ons manager and doesn't
385 // implement the isCompatibleWith or the findUpdates method.
386 if (!("isCompatibleWith" in aAddon) || !("findUpdates" in aAddon)) {
387 let errMsg = "Add-on doesn't implement either the isCompatibleWith " +
388 "or the findUpdates method!";
389 if (aAddon.id)
390 errMsg += " Add-on ID: " + aAddon.id;
391 Components.utils.reportError(errMsg);
392 return;
393 }
395 // If an add-on isn't appDisabled and isn't userDisabled then it is
396 // either active now or the user expects it to be active after the
397 // restart. If that is the case and the add-on is not installed by the
398 // application and is not compatible with the new application version
399 // then the user should be warned that the add-on will become
400 // incompatible. If an addon's type equals plugin it is skipped since
401 // checking plugins compatibility information isn't supported and
402 // getting the scope property of a plugin breaks in some environments
403 // (see bug 566787). The hotfix add-on is also ignored as it shouldn't
404 // block the user from upgrading.
405 try {
406 if (aAddon.type != "plugin" && aAddon.id != hotfixID &&
407 !aAddon.appDisabled && !aAddon.userDisabled &&
408 aAddon.scope != AddonManager.SCOPE_APPLICATION &&
409 aAddon.isCompatible &&
410 !aAddon.isCompatibleWith(self.update.appVersion,
411 self.update.platformVersion))
412 self.addons.push(aAddon);
413 }
414 catch (e) {
415 Components.utils.reportError(e);
416 }
417 });
418 self.addonsTotalCount = self.addons.length;
419 if (self.addonsTotalCount == 0) {
420 self.startDownload();
421 return;
422 }
424 self.checkAddonsForUpdates();
425 });
426 },
428 /**
429 * Checks if there are updates for add-ons that are incompatible with the
430 * application update.
431 */
432 checkAddonsForUpdates: function() {
433 this.addons.forEach(function(aAddon) {
434 aAddon.findUpdates(this, AddonManager.UPDATE_WHEN_NEW_APP_DETECTED,
435 this.update.appVersion,
436 this.update.platformVersion);
437 }, this);
438 },
440 /**
441 * See XPIProvider.jsm
442 */
443 onCompatibilityUpdateAvailable: function(aAddon) {
444 for (var i = 0; i < this.addons.length; ++i) {
445 if (this.addons[i].id == aAddon.id) {
446 this.addons.splice(i, 1);
447 break;
448 }
449 }
450 },
452 /**
453 * See XPIProvider.jsm
454 */
455 onUpdateAvailable: function(aAddon, aInstall) {
456 if (!Services.blocklist.isAddonBlocklisted(aAddon,
457 this.update.appVersion,
458 this.update.platformVersion)) {
459 // Compatibility or new version updates mean the same thing here.
460 this.onCompatibilityUpdateAvailable(aAddon);
461 }
462 },
464 /**
465 * See XPIProvider.jsm
466 */
467 onUpdateFinished: function(aAddon) {
468 ++this.addonsCheckedCount;
470 if (this.addonsCheckedCount < this.addonsTotalCount)
471 return;
473 if (this.addons.length == 0) {
474 // Compatibility updates or new version updates were found for all add-ons
475 this.startDownload();
476 return;
477 }
479 this.selectPanel("updateButtonBox");
480 this.setupUpdateButton("update.openUpdateUI." +
481 (this.isMajor ? "upgradeButton" : "applyButton"));
482 },
484 /**
485 * Starts the download of an update mar.
486 */
487 startDownload: function() {
488 if (!this.update)
489 this.update = this.um.activeUpdate;
490 this.update.QueryInterface(Components.interfaces.nsIWritablePropertyBag);
491 this.update.setProperty("foregroundDownload", "true");
493 this.aus.pauseDownload();
494 let state = this.aus.downloadUpdate(this.update, false);
495 if (state == "failed") {
496 this.selectPanel("downloadFailed");
497 return;
498 }
500 this.setupDownloadingUI();
501 },
503 /**
504 * Switches to the UI responsible for tracking the download.
505 */
506 setupDownloadingUI: function() {
507 this.downloadStatus = document.getElementById("downloadStatus");
508 this.downloadStatus.textContent =
509 DownloadUtils.getTransferTotal(0, this.update.selectedPatch.size);
510 this.selectPanel("downloading");
511 this.aus.addDownloadListener(this);
512 },
514 removeDownloadListener: function() {
515 if (this.aus) {
516 this.aus.removeDownloadListener(this);
517 }
518 },
520 /**
521 * See nsIRequestObserver.idl
522 */
523 onStartRequest: function(aRequest, aContext) {
524 },
526 /**
527 * See nsIRequestObserver.idl
528 */
529 onStopRequest: function(aRequest, aContext, aStatusCode) {
530 switch (aStatusCode) {
531 case Components.results.NS_ERROR_UNEXPECTED:
532 if (this.update.selectedPatch.state == "download-failed" &&
533 (this.update.isCompleteUpdate || this.update.patchCount != 2)) {
534 // Verification error of complete patch, informational text is held in
535 // the update object.
536 this.removeDownloadListener();
537 this.selectPanel("downloadFailed");
538 break;
539 }
540 // Verification failed for a partial patch, complete patch is now
541 // downloading so return early and do NOT remove the download listener!
542 break;
543 case Components.results.NS_BINDING_ABORTED:
544 // Do not remove UI listener since the user may resume downloading again.
545 break;
546 case Components.results.NS_OK:
547 this.removeDownloadListener();
548 if (this.backgroundUpdateEnabled) {
549 this.selectPanel("applying");
550 let update = this.um.activeUpdate;
551 let self = this;
552 Services.obs.addObserver(function updateStaged(aSubject, aTopic, aData) {
553 // Update the UI when the background updater is finished
554 let status = aData;
555 if (status == "applied" || status == "applied-service" ||
556 status == "pending" || status == "pending-service") {
557 // If the update is successfully applied, or if the updater has
558 // fallen back to non-staged updates, show the Restart to Update
559 // button.
560 self.selectPanel("updateButtonBox");
561 self.setupUpdateButton("update.restart." +
562 (self.isMajor ? "upgradeButton" : "updateButton"));
563 } else if (status == "failed") {
564 // Background update has failed, let's show the UI responsible for
565 // prompting the user to update manually.
566 self.selectPanel("downloadFailed");
567 } else if (status == "downloading") {
568 // We've fallen back to downloading the full update because the
569 // partial update failed to get staged in the background.
570 // Therefore we need to keep our observer.
571 self.setupDownloadingUI();
572 return;
573 }
574 Services.obs.removeObserver(updateStaged, "update-staged");
575 }, "update-staged", false);
576 } else {
577 this.selectPanel("updateButtonBox");
578 this.setupUpdateButton("update.restart." +
579 (this.isMajor ? "upgradeButton" : "updateButton"));
580 }
581 break;
582 default:
583 this.removeDownloadListener();
584 this.selectPanel("downloadFailed");
585 break;
586 }
588 },
590 /**
591 * See nsIProgressEventSink.idl
592 */
593 onStatus: function(aRequest, aContext, aStatus, aStatusArg) {
594 },
596 /**
597 * See nsIProgressEventSink.idl
598 */
599 onProgress: function(aRequest, aContext, aProgress, aProgressMax) {
600 this.downloadStatus.textContent =
601 DownloadUtils.getTransferTotal(aProgress, aProgressMax);
602 },
604 /**
605 * See nsISupports.idl
606 */
607 QueryInterface: function(aIID) {
608 if (!aIID.equals(Components.interfaces.nsIProgressEventSink) &&
609 !aIID.equals(Components.interfaces.nsIRequestObserver) &&
610 !aIID.equals(Components.interfaces.nsISupports))
611 throw Components.results.NS_ERROR_NO_INTERFACE;
612 return this;
613 }
614 };
615 #endif