/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; this.EXPORTED_SYMBOLS = []; const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components; // Chunk size for the incremental downloader const DOWNLOAD_CHUNK_BYTES_SIZE = 300000; // Incremental downloader interval const DOWNLOAD_INTERVAL = 0; // 1 day default const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/ctypes.jsm"); Cu.import("resource://gre/modules/GMPUtils.jsm"); this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader", "GMPAddon"]; var gLocale = null; // Shared code for suppressing bad cert dialogs XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() { let temp = { }; Cu.import("resource://gre/modules/CertUtils.jsm", temp); return temp; }); XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", "resource://gre/modules/UpdateUtils.jsm"); /** * Number of milliseconds after which we need to cancel `checkForAddons`. * * Bug 1087674 suggests that the XHR we use in `checkForAddons` may * never terminate in presence of network nuisances (e.g. strange * antivirus behavior). This timeout is a defensive measure to ensure * that we fail cleanly in such case. */ const CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS = 20000; function getScopedLogger(prefix) { // `PARENT_LOGGER_ID.` being passed here effectively links this logger // to the parentLogger. return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " "); } // This is copied directly from nsUpdateService.js // It is used for calculating the URL string w/ var replacement. // TODO: refactor this out somewhere else XPCOMUtils.defineLazyGetter(this, "gOSVersion", function aus_gOSVersion() { let osVersion; let sysInfo = Cc["@mozilla.org/system-info;1"]. getService(Ci.nsIPropertyBag2); try { osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version"); } catch (e) { LOG("gOSVersion - OS Version unknown: updates are not possible."); } if (osVersion) { #ifdef XP_WIN const BYTE = ctypes.uint8_t; const WORD = ctypes.uint16_t; const DWORD = ctypes.uint32_t; const WCHAR = ctypes.char16_t; const BOOL = ctypes.int; // This structure is described at: // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx const SZCSDVERSIONLENGTH = 128; const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', [ {dwOSVersionInfoSize: DWORD}, {dwMajorVersion: DWORD}, {dwMinorVersion: DWORD}, {dwBuildNumber: DWORD}, {dwPlatformId: DWORD}, {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)}, {wServicePackMajor: WORD}, {wServicePackMinor: WORD}, {wSuiteMask: WORD}, {wProductType: BYTE}, {wReserved: BYTE} ]); // This structure is described at: // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO', [ {wProcessorArchitecture: WORD}, {wReserved: WORD}, {dwPageSize: DWORD}, {lpMinimumApplicationAddress: ctypes.voidptr_t}, {lpMaximumApplicationAddress: ctypes.voidptr_t}, {dwActiveProcessorMask: DWORD.ptr}, {dwNumberOfProcessors: DWORD}, {dwProcessorType: DWORD}, {dwAllocationGranularity: DWORD}, {wProcessorLevel: WORD}, {wProcessorRevision: WORD} ]); let kernel32 = false; try { kernel32 = ctypes.open("Kernel32"); } catch (e) { LOG("gOSVersion - Unable to open kernel32! " + e); osVersion += ".unknown (unknown)"; } if(kernel32) { try { // Get Service pack info try { let GetVersionEx = kernel32.declare("GetVersionExW", ctypes.default_abi, BOOL, OSVERSIONINFOEXW.ptr); let winVer = OSVERSIONINFOEXW(); winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size; if(0 !== GetVersionEx(winVer.address())) { osVersion += "." + winVer.wServicePackMajor + "." + winVer.wServicePackMinor; } else { LOG("gOSVersion - Unknown failure in GetVersionEX (returned 0)"); osVersion += ".unknown"; } } catch (e) { LOG("gOSVersion - error getting service pack information. Exception: " + e); osVersion += ".unknown"; } // Get processor architecture let arch = "unknown"; try { let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo", ctypes.default_abi, ctypes.void_t, SYSTEM_INFO.ptr); let sysInfo = SYSTEM_INFO(); // Default to unknown sysInfo.wProcessorArchitecture = 0xffff; GetNativeSystemInfo(sysInfo.address()); switch(sysInfo.wProcessorArchitecture) { case 9: arch = "x64"; break; case 6: arch = "IA64"; break; case 0: arch = "x86"; break; } } catch (e) { LOG("gOSVersion - error getting processor architecture. Exception: " + e); } finally { osVersion += " (" + arch + ")"; } } finally { kernel32.close(); } } #endif try { osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")"; } catch (e) { // Not all platforms have a secondary widget library, so an error is nothing to worry about. } osVersion = encodeURIComponent(osVersion); } return osVersion; }); /** * Provides an easy API for downloading and installing GMP Addons */ function GMPInstallManager() { } /** * Temp file name used for downloading */ GMPInstallManager.prototype = { /** * Obtains a URL with replacement of vars */ _getURL: function() { let log = getScopedLogger("GMPInstallManager._getURL"); // Use the override URL if it is specified. The override URL is just like // the normal URL but it does not check the cert. let url = GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE); if (url) { log.info("Using override url: " + url); } else { url = GMPPrefs.get(GMPPrefs.KEY_URL); log.info("Using url: " + url); } url = UpdateUtils.formatUpdateURL(url); log.info("Using url (with replacement): " + url); return url; }, /** * Performs an addon check. * @return a promise which will be resolved or rejected. * The promise is resolved with an array of GMPAddons * The promise is rejected with an object with properties: * target: The XHR request object * status: The HTTP status code * type: Sometimes specifies type of rejection */ checkForAddons: function() { let log = getScopedLogger("GMPInstallManager.checkForAddons"); if (this._deferred) { log.error("checkForAddons already called"); return Promise.reject({type: "alreadycalled"}); } this._deferred = Promise.defer(); let url = this._getURL(); this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. createInstance(Ci.nsISupports); // This is here to let unit test code override XHR if (this._request.wrappedJSObject) { this._request = this._request.wrappedJSObject; } this._request.open("GET", url, true); let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true); this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(allowNonBuiltIn); // Prevent the request from reading from the cache. this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // Prevent the request from writing to the cache. this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; this._request.overrideMimeType("text/xml"); // The Cache-Control header is only interpreted by proxies and the // final destination. It does not help if a resource is already // cached locally. this._request.setRequestHeader("Cache-Control", "no-cache"); // HTTP/1.0 servers might not implement Cache-Control and // might only implement Pragma: no-cache this._request.setRequestHeader("Pragma", "no-cache"); this._request.timeout = CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS; this._request.addEventListener("error", event => this.onFailXML("onErrorXML", event), false); this._request.addEventListener("abort", event => this.onFailXML("onAbortXML", event), false); this._request.addEventListener("timeout", event => this.onFailXML("onTimeoutXML", event), false); this._request.addEventListener("load", event => this.onLoadXML(event), false); log.info("sending request to: " + url); this._request.send(null); return this._deferred.promise; }, /** * Installs the specified addon and calls a callback when done. * @param gmpAddon The GMPAddon object to install * @return a promise which will be resolved or rejected * The promise will resolve with an array of paths that were extracted * The promise will reject with an error object: * target: The XHR request object * status: The HTTP status code * type: A string to represent the type of error * downloaderr, verifyerr or previouserrorencountered */ installAddon: function(gmpAddon) { if (this._deferred) { log.error("previous error encountered"); return Promise.reject({type: "previouserrorencountered"}); } this.gmpDownloader = new GMPDownloader(gmpAddon); return this.gmpDownloader.start(); }, _getTimeSinceLastCheck: function() { let now = Math.round(Date.now() / 1000); // Default to 0 here because `now - 0` will be returned later if that case // is hit. We want a large value so a check will occur. let lastCheck = GMPPrefs.get(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0); // Handle clock jumps, return now since we want it to represent // a lot of time has passed since the last check. if (now < lastCheck) { return now; } return now - lastCheck; }, get _isEMEEnabled() { return GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true); }, _isAddonUpdateEnabled: function(aAddon) { return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon) && GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon); }, _updateLastCheck: function() { let now = Math.round(Date.now() / 1000); GMPPrefs.set(GMPPrefs.KEY_UPDATE_LAST_CHECK, now); }, _versionchangeOccurred: function() { let savedBuildID = GMPPrefs.get(GMPPrefs.KEY_BUILDID, null); let buildID = Services.appinfo.platformBuildID; if (savedBuildID == buildID) { return false; } GMPPrefs.set(GMPPrefs.KEY_BUILDID, buildID); return true; }, /** * Wrapper for checkForAddons and installAddon. * Will only install if not already installed and will log the results. * This will only install/update the OpenH264 and EME plugins * @return a promise which will be resolved if all addons could be installed * successfully, rejected otherwise. */ simpleCheckAndInstall: Task.async(function*() { let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall"); if (this._versionchangeOccurred()) { log.info("A version change occurred. Ignoring " + "media.gmp-manager.lastCheck to check immediately for " + "new or updated GMPs."); } else { let secondsBetweenChecks = GMPPrefs.get(GMPPrefs.KEY_SECONDS_BETWEEN_CHECKS, DEFAULT_SECONDS_BETWEEN_CHECKS) let secondsSinceLast = this._getTimeSinceLastCheck(); log.info("Last check was: " + secondsSinceLast + " seconds ago, minimum seconds: " + secondsBetweenChecks); if (secondsBetweenChecks > secondsSinceLast) { log.info("Will not check for updates."); return {status: "too-frequent-no-check"}; } } try { let gmpAddons = yield this.checkForAddons(); this._updateLastCheck(); log.info("Found " + gmpAddons.length + " addons advertised."); let addonsToInstall = gmpAddons.filter(function(gmpAddon) { log.info("Found addon: " + gmpAddon.toString()); if (!gmpAddon.isValid || GMPUtils.isPluginHidden(gmpAddon) || gmpAddon.isInstalled) { log.info("Addon invalid, hidden or already installed."); return false; } let addonUpdateEnabled = false; if (GMP_PLUGIN_IDS.indexOf(gmpAddon.id) >= 0) { addonUpdateEnabled = this._isAddonUpdateEnabled(gmpAddon.id); if (!addonUpdateEnabled) { log.info("Auto-update is off for " + gmpAddon.id + ", skipping check."); } } else { // Currently, we only support installs of OpenH264 and EME plugins. log.info("Auto-update is off for unknown plugin '" + gmpAddon.id + "', skipping check."); } return addonUpdateEnabled; }, this); if (!addonsToInstall.length) { log.info("No new addons to install, returning"); return {status: "nothing-new-to-install"}; } let installResults = []; let failureEncountered = false; for (let addon of addonsToInstall) { try { yield this.installAddon(addon); installResults.push({ id: addon.id, result: "succeeded", }); } catch (e) { failureEncountered = true; installResults.push({ id: addon.id, result: "failed", }); } } if (failureEncountered) { throw {status: "failed", results: installResults}; } return {status: "succeeded", results: installResults}; } catch(e) { log.error("Could not check for addons", e); throw e; } }), /** * Makes sure everything is cleaned up */ uninit: function() { let log = getScopedLogger("GMPInstallManager.uninit"); if (this._request) { log.info("Aborting request"); this._request.abort(); } if (this._deferred) { log.info("Rejecting deferred"); this._deferred.reject({type: "uninitialized"}); } log.info("Done cleanup"); }, /** * If set to true, specifies to leave the temporary downloaded zip file. * This is useful for tests. */ overrideLeaveDownloadedZip: false, /** * The XMLHttpRequest succeeded and the document was loaded. * @param event The nsIDOMEvent for the load */ onLoadXML: function(event) { let log = getScopedLogger("GMPInstallManager.onLoadXML"); try { log.info("request completed downloading document"); let certs = null; if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) && GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) { certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH); } let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN, true); log.info("allowNonBuiltIn: " + allowNonBuiltIn); gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs); this.parseResponseXML(); } catch (ex) { log.error("could not load xml: " + ex); this._deferred.reject({ target: event.target, status: this._getChannelStatus(event.target), message: "" + ex, }); delete this._deferred; } }, /** * Returns the status code for the XMLHttpRequest */ _getChannelStatus: function(request) { let log = getScopedLogger("GMPInstallManager._getChannelStatus"); let status = null; try { status = request.status; log.info("request.status is: " + request.status); } catch (e) { } if (status == null) { status = request.channel.QueryInterface(Ci.nsIRequest).status; } return status; }, /** * There was an error of some kind during the XMLHttpRequest. This * error may have been caused by external factors (e.g. network * issues) or internally (by a timeout). * * @param event The nsIDOMEvent for the error */ onFailXML: function(failure, event) { let log = getScopedLogger("GMPInstallManager.onFailXML " + failure); let request = event.target; let status = this._getChannelStatus(request); let message = "request.status: " + status + " (" + event.type + ")"; log.warn(message); this._deferred.reject({ target: request, status: status, message: message }); delete this._deferred; }, /** * Returns an array of GMPAddon objects discovered by the update check. * Or returns an empty array if there were any problems with parsing. * If there's an error, it will be logged if logging is enabled. */ parseResponseXML: function() { try { let log = getScopedLogger("GMPInstallManager.parseResponseXML"); let updatesElement = this._request.responseXML.documentElement; if (!updatesElement) { let message = "empty updates document"; log.warn(message); this._deferred.reject({ target: this._request, message: message }); delete this._deferred; return; } if (updatesElement.nodeName != "updates") { let message = "got node name: " + updatesElement.nodeName + ", expected: updates"; log.warn(message); this._deferred.reject({ target: this._request, message: message }); delete this._deferred; return; } const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; let gmpResults = []; for (let i = 0; i < updatesElement.childNodes.length; ++i) { let updatesChildElement = updatesElement.childNodes.item(i); if (updatesChildElement.nodeType != ELEMENT_NODE) { continue; } if (updatesChildElement.localName == "addons") { gmpResults = GMPAddon.parseGMPAddonsNode(updatesChildElement); } } this._deferred.resolve(gmpResults); delete this._deferred; } catch (e) { this._deferred.reject({ target: this._request, message: e }); delete this._deferred; } }, }; /** * Used to construct a single GMP addon * GMPAddon objects are returns from GMPInstallManager.checkForAddons * GMPAddon objects can also be used in calls to GMPInstallManager.installAddon * * @param gmpAddon The AUS response XML's DOM element `addon` */ function GMPAddon(gmpAddon) { let log = getScopedLogger("GMPAddon.constructor"); gmpAddon.QueryInterface(Ci.nsIDOMElement); ["id", "URL", "hashFunction", "hashValue", "version", "size"].forEach(name => { if (gmpAddon.hasAttribute(name)) { this[name] = gmpAddon.getAttribute(name); } }); this.size = Number(this.size) || undefined; log.info ("Created new addon: " + this.toString()); } /** * Parses an XML GMP addons node from AUS into an array * @param addonsElement An nsIDOMElement compatible node with XML from AUS * @return An array of GMPAddon results */ GMPAddon.parseGMPAddonsNode = function(addonsElement) { let log = getScopedLogger("GMPAddon.parseGMPAddonsNode"); let gmpResults = []; if (addonsElement.localName !== "addons") { return; } addonsElement.QueryInterface(Ci.nsIDOMElement); let addonCount = addonsElement.childNodes.length; for (let i = 0; i < addonCount; ++i) { let addonElement = addonsElement.childNodes.item(i); if (addonElement.localName !== "addon") { continue; } addonElement.QueryInterface(Ci.nsIDOMElement); try { gmpResults.push(new GMPAddon(addonElement)); } catch (e) { log.warn("invalid addon: " + e); continue; } } return gmpResults; }; GMPAddon.prototype = { /** * Returns a string representation of the addon */ toString: function() { return this.id + " (" + "isValid: " + this.isValid + ", isInstalled: " + this.isInstalled + ", hashFunction: " + this.hashFunction+ ", hashValue: " + this.hashValue + (this.size !== undefined ? ", size: " + this.size : "" ) + ")"; }, /** * If all the fields aren't specified don't consider this addon valid * @return true if the addon is parsed and valid */ get isValid() { return this.id && this.URL && this.version && this.hashFunction && !!this.hashValue; }, get isInstalled() { return this.version && GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) === this.version; }, get isEME() { return this.id == "gmp-widevinecdm" || this.id.indexOf("gmp-eme-") == 0; }, }; /** * Constructs a GMPExtractor object which is used to extract a GMP zip * into the specified location. (Which typically leties per platform) * @param zipPath The path on disk of the zip file to extract */ function GMPExtractor(zipPath, installToDirPath) { this.zipPath = zipPath; this.installToDirPath = installToDirPath; } GMPExtractor.prototype = { /** * Obtains a list of all the entries in a zipfile in the format of *.*. * This also includes files inside directories. * * @param zipReader the nsIZipReader to check * @return An array of string name entries which can be used * in nsIZipReader.extract */ _getZipEntries: function(zipReader) { let entries = []; let enumerator = zipReader.findEntries("*.*"); while (enumerator.hasMore()) { entries.push(enumerator.getNext()); } return entries; }, /** * Installs the this.zipPath contents into the directory used to store GMP * addons for the current platform. * * @return a promise which will be resolved or rejected * See GMPInstallManager.installAddon for resolve/rejected info */ install: function() { try { let log = getScopedLogger("GMPExtractor.install"); this._deferred = Promise.defer(); log.info("Installing " + this.zipPath + "..."); // Get the input zip file let zipFile = Cc["@mozilla.org/file/local;1"]. createInstance(Ci.nsIFile); zipFile.initWithPath(this.zipPath); // Initialize a zipReader and obtain the entries var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. createInstance(Ci.nsIZipReader); zipReader.open(zipFile) let entries = this._getZipEntries(zipReader); let extractedPaths = []; // Extract each of the entries entries.forEach(entry => { // We don't need these types of files if (entry.includes("__MACOSX")) { return; } let outFile = Cc["@mozilla.org/file/local;1"]. createInstance(Ci.nsILocalFile); outFile.initWithPath(this.installToDirPath); outFile.appendRelativePath(entry); // Make sure the directory hierarchy exists if(!outFile.parent.exists()) { outFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); } zipReader.extract(entry, outFile); extractedPaths.push(outFile.path); log.info(entry + " was successfully extracted to: " + outFile.path); }); zipReader.close(); if (!GMPInstallManager.overrideLeaveDownloadedZip) { zipFile.remove(false); } log.info(this.zipPath + " was installed successfully"); this._deferred.resolve(extractedPaths); } catch (e) { if (zipReader) { zipReader.close(); } this._deferred.reject({ target: this, status: e, type: "exception" }); } return this._deferred.promise; } }; /** * Constructs an object which downloads and initiates an install of * the specified GMPAddon object. * @param gmpAddon The addon to install. */ function GMPDownloader(gmpAddon) { this._gmpAddon = gmpAddon; } /** * Computes the file hash of fileToHash with the specified hash function * @param hashFunctionName A hash function name such as sha512 * @param fileToHash An nsIFile to hash * @return a promise which resolve to a digest in binary hex format */ GMPDownloader.computeHash = function(hashFunctionName, fileToHash) { let log = getScopedLogger("GMPDownloader.computeHash"); let digest; let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. createInstance(Ci.nsIFileInputStream); fileStream.init(fileToHash, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); try { let hash = Cc["@mozilla.org/security/hash;1"]. createInstance(Ci.nsICryptoHash); let hashFunction = Ci.nsICryptoHash[hashFunctionName.toUpperCase()]; if (!hashFunction) { log.error("could not get hash function"); return Promise.reject(); } hash.init(hashFunction); hash.updateFromStream(fileStream, -1); digest = binaryToHex(hash.finish(false)); } catch (e) { log.warn("failed to compute hash: " + e); digest = ""; } fileStream.close(); return Promise.resolve(digest); }, GMPDownloader.prototype = { /** * Starts the download process for an addon. * @return a promise which will be resolved or rejected * See GMPInstallManager.installAddon for resolve/rejected info */ start: function() { let log = getScopedLogger("GMPDownloader.start"); this._deferred = Promise.defer(); if (!this._gmpAddon.isValid) { log.info("gmpAddon is not valid, will not continue"); return Promise.reject({ target: this, status: status, type: "downloaderr" }); } let uri = Services.io.newURI(this._gmpAddon.URL, null, null); this._request = Cc["@mozilla.org/network/incremental-download;1"]. createInstance(Ci.nsIIncrementalDownload); let gmpFile = FileUtils.getFile("TmpD", [this._gmpAddon.id + ".zip"]); if (gmpFile.exists()) { gmpFile.remove(false); } log.info("downloading from " + uri.spec + " to " + gmpFile.path); this._request.init(uri, gmpFile, DOWNLOAD_CHUNK_BYTES_SIZE, DOWNLOAD_INTERVAL); this._request.start(this, null); return this._deferred.promise; }, // For nsIRequestObserver onStartRequest: function(request, context) { }, // For nsIRequestObserver // Called when the GMP addon zip file is downloaded onStopRequest: function(request, context, status) { let log = getScopedLogger("GMPDownloader.onStopRequest"); log.info("onStopRequest called"); if (!Components.isSuccessCode(status)) { log.info("status failed: " + status); this._deferred.reject({ target: this, status: status, type: "downloaderr" }); return; } let promise = this._verifyDownload(); promise.then(() => { log.info("GMP file is ready to unzip"); let destination = this._request.destination; let zipPath = destination.path; let gmpAddon = this._gmpAddon; let installToDirPath = Cc["@mozilla.org/file/local;1"]. createInstance(Ci.nsIFile); let path = OS.Path.join(OS.Constants.Path.profileDir, gmpAddon.id, gmpAddon.version); installToDirPath.initWithPath(path); log.info("install to directory path: " + installToDirPath.path); let gmpInstaller = new GMPExtractor(zipPath, installToDirPath.path); let installPromise = gmpInstaller.install(); installPromise.then(extractedPaths => { // Success, set the prefs let now = Math.round(Date.now() / 1000); GMPPrefs.set(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id); // Setting the version pref signals installation completion to consumers, // if you need to set other prefs etc. do it before this. GMPPrefs.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version, gmpAddon.id); this._deferred.resolve(extractedPaths); }, err => { this._deferred.reject(err); }); }, err => { log.warn("verifyDownload check failed"); this._deferred.reject({ target: this, status: 200, type: "verifyerr" }); }); }, /** * Verifies that the downloaded zip file's hash matches the GMPAddon hash. * @return a promise which resolves if the download verifies */ _verifyDownload: function() { let verifyDownloadDeferred = Promise.defer(); let log = getScopedLogger("GMPDownloader._verifyDownload"); log.info("_verifyDownload called"); if (!this._request) { return Promise.reject(); } let destination = this._request.destination; log.info("for path: " + destination.path); // Ensure that the file size matches the expected file size. if (this._gmpAddon.size !== undefined && destination.fileSize != this._gmpAddon.size) { log.warn("Downloader:_verifyDownload downloaded size " + destination.fileSize + " != expected size " + this._gmpAddon.size + "."); return Promise.reject(); } let promise = GMPDownloader.computeHash(this._gmpAddon.hashFunction, destination); promise.then(digest => { let expectedDigest = this._gmpAddon.hashValue.toLowerCase(); if (digest !== expectedDigest) { log.warn("hashes do not match! Got: `" + digest + "`, expected: `" + expectedDigest + "`"); this._deferred.reject(); return; } log.info("hashes match!"); verifyDownloadDeferred.resolve(); }, err => { verifyDownloadDeferred.reject(); }); return verifyDownloadDeferred.promise; }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver]) }; /** * Convert a string containing binary values to hex. */ function binaryToHex(input) { let result = ""; for (let i = 0; i < input.length; ++i) { let hex = input.charCodeAt(i).toString(16); if (hex.length == 1) hex = "0" + hex; result += hex; } return result; }