summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/GMPInstallManager.jsm
diff options
context:
space:
mode:
authorMoonchild <mcwerewolf@wolfbeast.com>2019-01-31 14:54:57 +0100
committerGitHub <noreply@github.com>2019-01-31 14:54:57 +0100
commit6edfc0a2010cfe14c25f7a4fd3acf4ea3d6a9bf9 (patch)
treec7080a6109482793740f0f863a344886324df566 /toolkit/mozapps/extensions/GMPInstallManager.jsm
parentfecab2c542df1e1aed5c9cb9ef45d113533c1dee (diff)
parenta606410ba941d4d093ad02a1dde26eb75a7fd80a (diff)
downloadUXP-6edfc0a2010cfe14c25f7a4fd3acf4ea3d6a9bf9.tar
UXP-6edfc0a2010cfe14c25f7a4fd3acf4ea3d6a9bf9.tar.gz
UXP-6edfc0a2010cfe14c25f7a4fd3acf4ea3d6a9bf9.tar.lz
UXP-6edfc0a2010cfe14c25f7a4fd3acf4ea3d6a9bf9.tar.xz
UXP-6edfc0a2010cfe14c25f7a4fd3acf4ea3d6a9bf9.zip
Merge pull request #954 from MoonchildProductions/TychoEME-work
[TychoAM] Fix GMP* so that EME works in Basilisk
Diffstat (limited to 'toolkit/mozapps/extensions/GMPInstallManager.jsm')
-rw-r--r--toolkit/mozapps/extensions/GMPInstallManager.jsm961
1 files changed, 961 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/GMPInstallManager.jsm b/toolkit/mozapps/extensions/GMPInstallManager.jsm
new file mode 100644
index 000000000..b9ebe5d7e
--- /dev/null
+++ b/toolkit/mozapps/extensions/GMPInstallManager.jsm
@@ -0,0 +1,961 @@
+/* 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, "UpdateChannel",
+ "resource://gre/modules/UpdateChannel.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;
+});
+
+// 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, "gABI", function aus_gABI() {
+ let abi = null;
+ try {
+ abi = Services.appinfo.XPCOMABI;
+ }
+ catch (e) {
+ LOG("gABI - XPCOM ABI unknown: updates are not possible.");
+ }
+#ifdef XP_MACOSX
+ // Mac universal build should report a different ABI than either macppc
+ // or mactel.
+ let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].
+ getService(Ci.nsIMacUtils);
+
+ if (macutils.isUniversalBinary)
+ abi += "-u-" + macutils.architecturesInBinary;
+#ifdef MOZ_SHARK
+ // Disambiguate optimised and shark nightlies
+ abi += "-shark"
+#endif
+#endif
+ return abi;
+});
+
+/**
+ * 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 =
+ url.replace(/%PRODUCT%/g, Services.appinfo.name)
+ .replace(/%VERSION%/g, Services.appinfo.version)
+ .replace(/%BUILD_ID%/g, Services.appinfo.appBuildID)
+ .replace(/%BUILD_TARGET%/g, Services.appinfo.OS + "_" + gABI)
+ .replace(/%OS_VERSION%/g, gOSVersion);
+ if (/%LOCALE%/.test(url)) {
+ // TODO: Get the real local, does it actually matter for GMP plugins?
+ url = url.replace(/%LOCALE%/g, "en-US");
+ }
+ url =
+ url.replace(/%CHANNEL%/g, UpdateChannel.get())
+ .replace(/%PLATFORM_VERSION%/g, Services.appinfo.platformVersion)
+ .replace(/%DISTRIBUTION%/g,
+ GMPPrefs.get(GMPPrefs.KEY_APP_DISTRIBUTION))
+ .replace(/%DISTRIBUTION_VERSION%/g,
+ GMPPrefs.get(GMPPrefs.KEY_APP_DISTRIBUTION_VERSION))
+ .replace(/\+/g, "%2B");
+ 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;
+}