/* * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is MozMill Test code. * * The Initial Developer of the Original Code is Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2009 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Henrik Skupin * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * **** END LICENSE BLOCK ***** */ /** * @fileoverview * The SoftwareUpdateAPI adds support for an easy access to the update process. */ // Include required modules var prefs = require("prefs"); var utils = require("utils"); const gTimeoutUpdateCheck = 10000; const gTimeoutUpdateDownload = 360000; const PREF_DISABLED_ADDONS = "extensions.disabledAddons"; // Helper lookup constants for elements of the software update dialog const WIZARD = '/id("updates")'; const WIZARD_BUTTONS = WIZARD + '/anon({"anonid":"Buttons"})'; const WIZARD_DECK = WIZARD + '/anon({"anonid":"Deck"})'; const WIZARD_PAGES = { dummy: 'dummy', checking: 'checking', pluginUpdatesFound: 'pluginupdatesfound', noUpdatesFound: 'noupdatesfound', manualUpdate: 'manualUpdate', incompatibleCheck: 'incompatibleCheck', updatesFoundBasic: 'updatesfoundbasic', updatesFoundBillboard: 'updatesfoundbillboard', license: 'license', incompatibleList: 'incompatibleList', downloading: 'downloading', errors: 'errors', errorPatching: 'errorpatching', finished: 'finished', finishedBackground: 'finishedBackground', installed: 'installed' } // On Mac there is another DOM structure used as on Windows and Linux if (mozmill.isMac) { var WIZARD_BUTTONS_BOX = WIZARD_BUTTONS + '/anon({"flex":"1"})/{"class":"wizard-buttons-btm"}/'; var WIZARD_BUTTON = { back: '{"dlgtype":"back"}', next: '{"dlgtype":"next"}', cancel: '{"dlgtype":"cancel"}', finish: '{"dlgtype":"finish"}', extra1: '{"dlgtype":"extra1"}', extra2: '{"dlgtype":"extra2"}' } } else { var WIZARD_BUTTONS_BOX = WIZARD_BUTTONS + '/anon({"flex":"1"})/{"class":"wizard-buttons-box-2"}/'; var WIZARD_BUTTON = { back: '{"dlgtype":"back"}', next: 'anon({"anonid":"WizardButtonDeck"})/[1]/{"dlgtype":"next"}', cancel: '{"dlgtype":"cancel"}', finish: 'anon({"anonid":"WizardButtonDeck"})/[0]/{"dlgtype":"finish"}', extra1: '{"dlgtype":"extra1"}', extra2: '{"dlgtype":"extra2"}' } } /** * Constructor for software update class */ function softwareUpdate() { this._controller = null; this._wizard = null; this._downloadDuration = -1; this._aus = Cc["@mozilla.org/updates/update-service;1"]. getService(Ci.nsIApplicationUpdateService); this._ums = Cc["@mozilla.org/updates/update-manager;1"]. getService(Ci.nsIUpdateManager); this._vc = Cc["@mozilla.org/xpcom/version-comparator;1"]. getService(Ci.nsIVersionComparator); } /** * Class for software updates */ softwareUpdate.prototype = { /** * Returns the active update * * @returns The currently selected update * @type nsIUpdate */ get activeUpdate() { return this._ums.activeUpdate; }, /** * Check if the user has permissions to run the software update * * @returns Status if the user has the permissions. * @type {boolean} */ get allowed() { return this._aus.canCheckForUpdates && this._aus.canApplyUpdates; }, /** * Returns information of the current build version */ get buildInfo() { return { buildid : utils.appInfo.buildID, disabled_addons : prefs.preferences.getPref(PREF_DISABLED_ADDONS, ''), locale : utils.appInfo.locale, user_agent : utils.appInfo.userAgent, version : utils.appInfo.version }; }, /** * Returns the current update channel */ get channel() { return prefs.preferences.getPref('app.update.channel', ''); }, /** * Get the controller of the associated engine manager dialog * * @returns Controller of the browser window * @type MozMillController */ get controller() { return this._controller; }, /** * Returns the current step of the software update dialog wizard */ get currentPage() { return this._wizard.getNode().getAttribute('currentpageid'); }, /** * Returns true if the offered update is a complete update */ get isCompleteUpdate() { // Throw when isCompleteUpdate is called without an update. This should // never happen except if the test is incorrectly written. if (!this.activeUpdate) throw new Error(arguments.callee.name + ": isCompleteUpdate called " + "when activeUpdate is null!"); var patchCount = this.activeUpdate.patchCount; if ((patchCount < 1) || (patchCount > 2)) { throw new Error("An update must have one or two patches included."); } // XXX: After Firefox 4 has been released and we do not have to test any // beta release anymore uncomment out the following code // if (this.activeUpdate.patchCount == 2) { // var patch0URL = this.activeUpdate.getPatchAt(0).URL; // var patch1URL = this.activeUpdate.getPatchAt(1).URL; // Test that the update snippet created by releng doesn't have the same // url for both patches (bug 514040). // controller.assertJS("subject.patch0URL != subject.patch1URL", // {patch0URL: patch0URL, patch1URL: patch1URL}); // } return (this.activeUpdate.selectedPatch.type == "complete"); }, /** * Returns information of the active update in the queue. */ get patchInfo() { this._controller.assert(function() { return !!this.activeUpdate; }, "An active update is in the queue.", this); return { buildid : this.activeUpdate.buildID, channel : this.channel, is_complete : this.isCompleteUpdate, size : this.activeUpdate.selectedPatch.size, type : this.activeUpdate.type, url : this.activeUpdate.selectedPatch.finalURL || "n/a", download_duration : this._downloadDuration, version : this.activeUpdate.version }; }, /** * Returns the update type (minor or major) * * @returns The update type */ get updateType() { return this.activeUpdate.type; }, /** * Check if updates have been found */ get updatesFound() { return this.currentPage.indexOf("updatesfound") == 0; }, /** * Checks if an update has been applied correctly * * @param {object} updateData * All the data collected during the update process */ assertUpdateApplied : function softwareUpdate_assertUpdateApplied(updateData) { // Get the information from the last update var info = updateData.updates[updateData.updateIndex]; // The upgraded version should be identical with the version given by // the update and we shouldn't have run a downgrade var check = this._vc.compare(info.build_post.version, info.build_pre.version); this._controller.assert(function () { return check >= 0; }, "The version number of the upgraded build is higher or equal."); // The build id should be identical with the one from the update this._controller.assert(function () { return info.build_post.buildid === info.patch.buildid; }, "The build id is equal to the build id of the update."); // If a target build id has been given, check if it matches the updated build info.target_buildid = updateData.targetBuildID; if (info.target_buildid) { this._controller.assert(function () { return info.build_post.buildid === info.target_buildid; }, "Target build id matches id of updated build."); } // An upgrade should not change the builds locale this._controller.assert(function () { return info.build_post.locale === info.build_pre.locale; }, "The locale of the updated build is identical to the original locale."); // Check that no application-wide add-ons have been disabled this._controller.assert(function () { return info.build_post.disabled_addons === info.build_pre.disabled_addons; }, "No application-wide add-ons have been disabled by the update."); }, /** * Close the software update dialog */ closeDialog: function softwareUpdate_closeDialog() { if (this._controller) { this._controller.keypress(null, "VK_ESCAPE", {}); this._controller.sleep(500); this._controller = null; this._wizard = null; } }, /** * Download the update of the given channel and type * @param {string} channel * Update channel to use * @param {boolean} waitForFinish * Sets if the function should wait until the download has been finished * @param {number} timeout * Timeout the download has to stop */ download : function softwareUpdate_download(channel, waitForFinish, timeout) { waitForFinish = waitForFinish ? waitForFinish : true; // Check that the correct channel has been set this._controller.assert(function() { return channel == this.channel; }, "The current update channel is identical to the specified one.", this); // Retrieve the timestamp, so we can measure the duration of the download var startTime = Date.now(); // Click the next button var next = this.getElement({type: "button", subtype: "next"}); this._controller.click(next); // Wait for the download page - if it fails the update was already cached try { this.waitForWizardPage(WIZARD_PAGES.downloading); if (waitForFinish) this.waitforDownloadFinished(timeout); } catch (ex) { this.waitForWizardPage(WIZARD_PAGES.finished); } // Calculate the duration in ms this._downloadDuration = Date.now() - startTime; }, /** * Update the update.status file and set the status to 'failed:6' */ forceFallback : function softwareUpdate_forceFallback() { var dirService = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); var updateDir; var updateStatus; // Check the global update folder first try { updateDir = dirService.get("UpdRootD", Ci.nsIFile); updateDir.append("updates"); updateDir.append("0"); updateStatus = updateDir.clone(); updateStatus.append("update.status"); } catch (ex) { } if (updateStatus == undefined || !updateStatus.exists()) { updateDir = dirService.get("XCurProcD", Ci.nsIFile); updateDir.append("updates"); updateDir.append("0"); updateStatus = updateDir.clone(); updateStatus.append("update.status"); } var foStream = Cc["@mozilla.org/network/file-output-stream;1"]. createInstance(Ci.nsIFileOutputStream); var status = "failed: 6\n"; foStream.init(updateStatus, 0x02 | 0x08 | 0x20, -1, 0); foStream.write(status, status.length); foStream.close(); }, /** * Gets all the needed external DTD urls as an array * * @returns Array of external DTD urls * @type [string] */ getDtds : function softwareUpdate_getDtds() { var dtds = ["chrome://mozapps/locale/update/history.dtd", "chrome://mozapps/locale/update/updates.dtd"] return dtds; }, /** * Retrieve an UI element based on the given spec * * @param {object} spec * Information of the UI element which should be retrieved * type: General type information * subtype: Specific element or property * value: Value of the element or property * @returns Element which has been created * @type {ElemBase} */ getElement : function softwareUpdate_getElement(spec) { var elem = null; switch(spec.type) { /** * subtype: subtype to match * value: value to match */ case "button": elem = new elementslib.Lookup(this._controller.window.document, WIZARD_BUTTONS_BOX + WIZARD_BUTTON[spec.subtype]); break; case "wizard": elem = new elementslib.Lookup(this._controller.window.document, WIZARD); break; case "wizard_page": elem = new elementslib.Lookup(this._controller.window.document, WIZARD_DECK + '/id(' + spec.subtype + ')'); break; case "download_progress": elem = new elementslib.ID(this._controller.window.document, "downloadProgress"); break; default: throw new Error(arguments.callee.name + ": Unknown element type - " + spec.type); } return elem; }, /** * Open software update dialog * * @param {MozMillController} browserController * Mozmill controller of the browser window */ openDialog: function softwareUpdate_openDialog(browserController) { // XXX: After Firefox 4 has been released and we do not have to test any // beta release anymore uncomment out the following code // With version >= 4.0b7pre the update dialog is reachable from within the // about window now. var appVersion = utils.appInfo.version; if (this._vc.compare(appVersion, "4.0b7pre") >= 0) { // XXX: We can't open the about window, otherwise a parallel download of // the update will let us fallback to a complete one all the time // Open the about window and check the update button //var aboutItem = new elementslib.Elem(browserController.menus.helpMenu.aboutName); //browserController.click(aboutItem); // //utils.handleWindow("type", "Browser:About", function(controller) { // // XXX: Bug 599290 - Check for updates has been completely relocated // // into the about window. We can't check the in-about ui yet. // var updateButton = new elementslib.ID(controller.window.document, // "checkForUpdatesButton"); // //controller.click(updateButton); // controller.waitForElement(updateButton, gTimeout); //}); // For now just call the old ui until we have support for the about window. var updatePrompt = Cc["@mozilla.org/updates/update-prompt;1"]. createInstance(Ci.nsIUpdatePrompt); updatePrompt.checkForUpdates(); } else { // For builds <4.0b7pre updateItem = new elementslib.Elem(browserController.menus.helpMenu.checkForUpdates); browserController.click(updateItem); } this.waitForDialogOpen(browserController); }, /** * Wait that check for updates has been finished * @param {number} timeout */ waitForCheckFinished : function softwareUpdate_waitForCheckFinished(timeout) { timeout = timeout ? timeout : gTimeoutUpdateCheck; this._controller.waitFor(function() { return this.currentPage != WIZARD_PAGES.checking; }, "Check for updates has been completed.", timeout, null, this); }, /** * Wait for the software update dialog * * @param {MozMillController} browserController * Mozmill controller of the browser window */ waitForDialogOpen : function softwareUpdate_waitForDialogOpen(browserController) { this._controller = utils.handleWindow("type", "Update:Wizard", undefined, false); this._wizard = this.getElement({type: "wizard"}); this._controller.waitFor(function () { return this.currentPage !== WIZARD_PAGES.dummy; }, "Dummy wizard page has been made invisible - got " + this.currentPage, undefined, undefined, this); this._controller.window.focus(); }, /** * Wait until the download has been finished * * @param {number} timeout * Timeout the download has to stop */ waitforDownloadFinished: function softwareUpdate_waitForDownloadFinished(timeout) { timeout = timeout ? timeout : gTimeoutUpdateDownload; var progress = this.getElement({type: "download_progress"}); this._controller.waitFor(function () { return progress.getNode().value === '100'; }, "Update has been finished downloading.", timeout); this.waitForWizardPage(WIZARD_PAGES.finished); }, /** * Waits for the given page of the update dialog wizard */ waitForWizardPage : function softwareUpdate_waitForWizardPage(step) { this._controller.waitFor(function () { return this.currentPage === step; }, "New wizard page has been selected - got " + this.currentPage + ", expected " + step, undefined, undefined, this); } } // Export of variables exports.WIZARD_PAGES = WIZARD_PAGES; // Export of classes exports.softwareUpdate = softwareUpdate;