summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/GMPInstallManager.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/GMPInstallManager.sys.mjs')
-rw-r--r--toolkit/modules/GMPInstallManager.sys.mjs850
1 files changed, 850 insertions, 0 deletions
diff --git a/toolkit/modules/GMPInstallManager.sys.mjs b/toolkit/modules/GMPInstallManager.sys.mjs
new file mode 100644
index 0000000000..41f57b9a63
--- /dev/null
+++ b/toolkit/modules/GMPInstallManager.sys.mjs
@@ -0,0 +1,850 @@
+/* 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/. */
+
+// 1 day default
+const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+import {
+ GMPPrefs,
+ GMPUtils,
+ GMP_PLUGIN_IDS,
+ WIDEVINE_L1_ID,
+ WIDEVINE_L3_ID,
+} from "resource://gre/modules/GMPUtils.sys.mjs";
+
+import { ProductAddonChecker } from "resource://gre/modules/addons/ProductAddonChecker.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CertUtils: "resource://gre/modules/CertUtils.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+});
+
+function getScopedLogger(prefix) {
+ // `PARENT_LOGGER_ID.` being passed here effectively links this logger
+ // to the parentLogger.
+ return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " ");
+}
+
+const LOCAL_GMP_SOURCES = [
+ {
+ id: "gmp-gmpopenh264",
+ src: "chrome://global/content/gmp-sources/openh264.json",
+ installByDefault: true,
+ },
+ {
+ id: "gmp-widevinecdm",
+ src: "chrome://global/content/gmp-sources/widevinecdm.json",
+ installByDefault: true,
+ },
+ {
+ id: "gmp-widevinecdm-l1",
+ src: "chrome://global/content/gmp-sources/widevinecdm_l1.json",
+ installByDefault: false,
+ },
+];
+
+function downloadJSON(uri) {
+ let log = getScopedLogger("GMPInstallManager.checkForAddons");
+ log.info("fetching config from: " + uri);
+ return new Promise((resolve, reject) => {
+ let xmlHttp = new lazy.ServiceRequest({ mozAnon: true });
+
+ xmlHttp.onload = function (aResponse) {
+ resolve(JSON.parse(this.responseText));
+ };
+
+ xmlHttp.onerror = function (e) {
+ reject("Fetching " + uri + " results in error code: " + e.target.status);
+ };
+
+ xmlHttp.open("GET", uri);
+ xmlHttp.overrideMimeType("application/json");
+ xmlHttp.send();
+ });
+}
+
+/**
+ * If downloading from the network fails (AUS server is down),
+ * load the sources from local build configuration.
+ */
+function downloadLocalConfig(sources) {
+ if (!sources.length) {
+ return Promise.resolve({ addons: [] });
+ }
+
+ let log = getScopedLogger("GMPInstallManager.downloadLocalConfig");
+ return Promise.all(
+ sources.map(conf => {
+ return downloadJSON(conf.src).then(addons => {
+ let platforms = addons.vendors[conf.id].platforms;
+ let target = Services.appinfo.OS + "_" + lazy.UpdateUtils.ABI;
+ let details = null;
+
+ while (!details) {
+ if (!(target in platforms)) {
+ // There was no matching platform so return false, this addon
+ // will be filtered from the results below
+ log.info("no details found for: " + target);
+ return false;
+ }
+ // Field either has the details of the binary or is an alias
+ // to another build target key that does
+ if (platforms[target].alias) {
+ target = platforms[target].alias;
+ } else {
+ details = platforms[target];
+ }
+ }
+
+ log.info("found plugin: " + conf.id);
+ return {
+ id: conf.id,
+ URL: details.fileUrl,
+ hashFunction: addons.hashFunction,
+ hashValue: details.hashValue,
+ version: addons.vendors[conf.id].version,
+ size: details.filesize,
+ usedFallback: true,
+ };
+ });
+ })
+ ).then(addons => {
+ // Some filters may not match this platform so
+ // filter those out
+ return { addons: addons.filter(x => x !== false) };
+ });
+}
+
+/**
+ * Provides an easy API for downloading and installing GMP Addons
+ */
+export function GMPInstallManager() {}
+
+/**
+ * Temp file name used for downloading
+ */
+GMPInstallManager.prototype = {
+ /**
+ * Obtains a URL with replacement of vars
+ */
+ async _getURL() {
+ 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.getString(GMPPrefs.KEY_URL_OVERRIDE, "");
+ if (url) {
+ log.info("Using override url: " + url);
+ } else {
+ url = GMPPrefs.getString(GMPPrefs.KEY_URL);
+ log.info("Using url: " + url);
+ }
+
+ url = await lazy.UpdateUtils.formatUpdateURL(url);
+
+ log.info("Using url (with replacement): " + url);
+ return url;
+ },
+
+ /**
+ * Records telemetry results on if fetching update.xml from Balrog succeeded
+ * when content signature was used to verify the response from Balrog.
+ * @param didGetAddonList
+ * A boolean indicating if an update.xml containing the addon list was
+ * successfully fetched (true) or not (false).
+ * @param err
+ * The error that was thrown (if it exists) for the failure case. This
+ * is expected to have a addonCheckerErr member which provides further
+ * information on why the addon checker failed.
+ */
+ recordUpdateXmlTelemetryForContentSignature(didGetAddonList, err = null) {
+ let log = getScopedLogger(
+ "GMPInstallManager.recordUpdateXmlTelemetryForContentSignature"
+ );
+ try {
+ let updateResultHistogram = Services.telemetry.getHistogramById(
+ "MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
+ );
+
+ // The non-glean telemetry used here will be removed in future and just
+ // the glean data will be gathered.
+ if (didGetAddonList) {
+ updateResultHistogram.add("content_sig_ok");
+ Glean.gmp.updateXmlFetchResult.content_sig_success.add(1);
+ return;
+ }
+ // All remaining cases are failure cases.
+ updateResultHistogram.add("content_sig_fail");
+ if (!err?.addonCheckerErr) {
+ // Unknown error case. If this is happening we should audit error paths
+ // to identify why we're not getting an error, or not getting it
+ // labelled.
+ Glean.gmp.updateXmlFetchResult.content_sig_unknown_error.add(1);
+ return;
+ }
+ const errorToHistogramMap = {
+ [ProductAddonChecker.NETWORK_REQUEST_ERR]:
+ "content_sig_net_request_error",
+ [ProductAddonChecker.NETWORK_TIMEOUT_ERR]: "content_sig_net_timeout",
+ [ProductAddonChecker.ABORT_ERR]: "content_sig_abort",
+ [ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR]:
+ "content_sig_missing_data",
+ [ProductAddonChecker.VERIFICATION_FAILED_ERR]: "content_sig_failed",
+ [ProductAddonChecker.VERIFICATION_INVALID_ERR]: "content_sig_invalid",
+ [ProductAddonChecker.XML_PARSE_ERR]: "content_sig_xml_parse_error",
+ };
+ let metricID =
+ errorToHistogramMap[err.addonCheckerErr] ?? "content_sig_unknown_error";
+ let metric = Glean.gmp.updateXmlFetchResult[metricID];
+ metric.add(1);
+ } catch (e) {
+ // We don't expect this path to be hit, but we don't want telemetry
+ // failures to break GMP updates, so catch any issues here and let the
+ // update machinery continue.
+ log.error(
+ `Failed to record telemetry result of getProductAddonList, got error: ${e}`
+ );
+ }
+ },
+
+ /**
+ * Records telemetry results on if fetching update.xml from Balrog succeeded
+ * when cert pinning was used to verify the response from Balrog. This
+ * should be removed once we're no longer using cert pinning.
+ * @param didGetAddonList
+ * A boolean indicating if an update.xml containing the addon list was
+ * successfully fetched (true) or not (false).
+ * @param err
+ * The error that was thrown (if it exists) for the failure case. This
+ * is expected to have a addonCheckerErr member which provides further
+ * information on why the addon checker failed.
+ */
+ recordUpdateXmlTelemetryForCertPinning(didGetAddonList, err = null) {
+ let log = getScopedLogger(
+ "GMPInstallManager.recordUpdateXmlTelemetryForCertPinning"
+ );
+ try {
+ let updateResultHistogram = Services.telemetry.getHistogramById(
+ "MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
+ );
+
+ // The non-glean telemetry used here will be removed in future and just
+ // the glean data will be gathered.
+ if (didGetAddonList) {
+ updateResultHistogram.add("cert_pinning_ok");
+ Glean.gmp.updateXmlFetchResult.cert_pin_success.add(1);
+ return;
+ }
+ // All remaining cases are failure cases.
+ updateResultHistogram.add("cert_pinning_fail");
+ if (!err?.addonCheckerErr) {
+ // Unknown error case. If this is happening we should audit error paths
+ // to identify why we're not getting an error, or not getting it
+ // labelled.
+ Glean.gmp.updateXmlFetchResult.cert_pin_unknown_error.add(1);
+ return;
+ }
+ const errorToHistogramMap = {
+ [ProductAddonChecker.NETWORK_REQUEST_ERR]: "cert_pin_net_request_error",
+ [ProductAddonChecker.NETWORK_TIMEOUT_ERR]: "cert_pin_net_timeout",
+ [ProductAddonChecker.ABORT_ERR]: "cert_pin_abort",
+ [ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR]:
+ "cert_pin_missing_data",
+ [ProductAddonChecker.VERIFICATION_FAILED_ERR]: "cert_pin_failed",
+ [ProductAddonChecker.VERIFICATION_INVALID_ERR]: "cert_pin_invalid",
+ [ProductAddonChecker.XML_PARSE_ERR]: "cert_pin_xml_parse_error",
+ };
+ let metricID =
+ errorToHistogramMap[err.addonCheckerErr] ?? "cert_pin_unknown_error";
+ let metric = Glean.gmp.updateXmlFetchResult[metricID];
+ metric.add(1);
+ } catch (e) {
+ // We don't expect this path to be hit, but we don't want telemetry
+ // failures to break GMP updates, so catch any issues here and let the
+ // update machinery continue.
+ log.error(
+ `Failed to record telemetry result of getProductAddonList, got error: ${e}`
+ );
+ }
+ },
+
+ /**
+ * Performs an addon check.
+ * @return a promise which will be resolved or rejected.
+ * The promise is resolved with an object with properties:
+ * addons: array of addons
+ * usedFallback: whether the data was collected from online or
+ * from fallback data within the build
+ * 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
+ */
+ async checkForAddons() {
+ let log = getScopedLogger("GMPInstallManager.checkForAddons");
+ if (this._deferred) {
+ log.error("checkForAddons already called");
+ return Promise.reject({ type: "alreadycalled" });
+ }
+
+ if (!GMPPrefs.getBool(GMPPrefs.KEY_UPDATE_ENABLED, true)) {
+ log.info("Updates are disabled via media.gmp-manager.updateEnabled");
+ return { usedFallback: true, addons: [] };
+ }
+
+ this._deferred = Promise.withResolvers();
+ let deferredPromise = this._deferred.promise;
+
+ // Should content signature checking of Balrog replies be used? If so this
+ // will be done instead of the older cert pinning method.
+ let checkContentSignature = GMPPrefs.getBool(
+ GMPPrefs.KEY_CHECK_CONTENT_SIGNATURE,
+ true
+ );
+
+ let allowNonBuiltIn = true;
+ let certs = null;
+ // Only check certificates if we're not using a custom URL, and only if
+ // we're not checking a content signature.
+ if (
+ !Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
+ !checkContentSignature
+ ) {
+ allowNonBuiltIn = !GMPPrefs.getString(
+ GMPPrefs.KEY_CERT_REQUIREBUILTIN,
+ true
+ );
+ if (GMPPrefs.getBool(GMPPrefs.KEY_CERT_CHECKATTRS, true)) {
+ certs = lazy.CertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH);
+ }
+ }
+
+ let url = await this._getURL();
+
+ log.info(
+ `Fetching product addon list url=${url}, allowNonBuiltIn=${allowNonBuiltIn}, certs=${certs}, checkContentSignature=${checkContentSignature}`
+ );
+
+ let success = true;
+ let res;
+ try {
+ res = await ProductAddonChecker.getProductAddonList(
+ url,
+ allowNonBuiltIn,
+ certs,
+ checkContentSignature
+ );
+
+ if (checkContentSignature) {
+ this.recordUpdateXmlTelemetryForContentSignature(true);
+ } else {
+ this.recordUpdateXmlTelemetryForCertPinning(true);
+ }
+ } catch (err) {
+ success = false;
+ if (checkContentSignature) {
+ this.recordUpdateXmlTelemetryForContentSignature(false, err);
+ } else {
+ this.recordUpdateXmlTelemetryForCertPinning(false, err);
+ }
+ }
+
+ try {
+ if (!success) {
+ log.info("Falling back to local config");
+ let fallbackSources = LOCAL_GMP_SOURCES.filter(function (gmpSource) {
+ return gmpSource.installByDefault;
+ });
+ res = await downloadLocalConfig(fallbackSources);
+ }
+ } catch (err) {
+ this._deferred.reject(err);
+ delete this._deferred;
+ return deferredPromise;
+ }
+
+ let addons;
+ if (res && res.addons) {
+ addons = res.addons.map(a => new GMPAddon(a));
+ } else {
+ addons = [];
+ }
+
+ // We need to merge in the addons that are only available via fallback that
+ // the user has requested be forced installed regardless of our update
+ // server configuration.
+ try {
+ let forcedSources = LOCAL_GMP_SOURCES.filter(function (gmpSource) {
+ return GMPPrefs.getBool(
+ GMPPrefs.KEY_PLUGIN_FORCE_INSTALL,
+ false,
+ gmpSource.id
+ );
+ });
+
+ let forcedConfigs = await downloadLocalConfig(
+ forcedSources.filter(function (gmpSource) {
+ return !addons.find(gmpAddon => gmpAddon.id == gmpSource.id);
+ })
+ );
+
+ let forcedAddons = forcedConfigs.addons.map(
+ config => new GMPAddon(config)
+ );
+
+ log.info("Forced " + forcedAddons.length + " addons.");
+ addons = addons.concat(forcedAddons);
+ } catch (err) {
+ log.info("Failed to force addons: " + err);
+ }
+
+ this._deferred.resolve({ addons });
+ delete this._deferred;
+ return deferredPromise;
+ },
+ /**
+ * 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(gmpAddon) {
+ if (this._deferred) {
+ let log = getScopedLogger("GMPInstallManager.installAddon");
+ log.error("previous error encountered");
+ return Promise.reject({ type: "previouserrorencountered" });
+ }
+ this.gmpDownloader = new GMPDownloader(gmpAddon);
+ return this.gmpDownloader.start();
+ },
+ _getTimeSinceLastCheck() {
+ 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.getInt(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.getBool(GMPPrefs.KEY_EME_ENABLED, true);
+ },
+ _isAddonEnabled(aAddon) {
+ return GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon);
+ },
+ _isAddonUpdateEnabled(aAddon) {
+ return (
+ this._isAddonEnabled(aAddon) &&
+ GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon)
+ );
+ },
+ _updateLastCheck() {
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.setInt(GMPPrefs.KEY_UPDATE_LAST_CHECK, now);
+ },
+ _versionchangeOccurred() {
+ let savedBuildID = GMPPrefs.getString(GMPPrefs.KEY_BUILDID, "");
+ let buildID = Services.appinfo.platformBuildID || "";
+ if (savedBuildID == buildID) {
+ return false;
+ }
+ GMPPrefs.setString(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.
+ */
+ async simpleCheckAndInstall() {
+ 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.getInt(
+ 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 { addons } = await this.checkForAddons();
+ this._updateLastCheck();
+ log.info("Found " + addons.length + " addons advertised.");
+ let addonsToInstall = addons.filter(function (gmpAddon) {
+ log.info("Found addon: " + gmpAddon.toString());
+
+ if (!gmpAddon.isValid) {
+ log.info("Addon |" + gmpAddon.id + "| is invalid.");
+ return false;
+ }
+
+ if (GMPUtils.isPluginHidden(gmpAddon)) {
+ log.info("Addon |" + gmpAddon.id + "| has been hidden.");
+ return false;
+ }
+
+ if (gmpAddon.isInstalled) {
+ log.info("Addon |" + gmpAddon.id + "| already installed.");
+ return false;
+ }
+
+ // Do not install from fallback if already installed as it
+ // may be a downgrade
+ if (gmpAddon.usedFallback && gmpAddon.isUpdate) {
+ log.info(
+ "Addon |" +
+ gmpAddon.id +
+ "| not installing updates based " +
+ "on fallback."
+ );
+ return false;
+ }
+
+ let addonUpdateEnabled = false;
+ if (GMP_PLUGIN_IDS.includes(gmpAddon.id)) {
+ if (!this._isAddonEnabled(gmpAddon.id)) {
+ log.info(
+ "GMP |" + gmpAddon.id + "| has been disabled; skipping check."
+ );
+ } else if (!this._isAddonUpdateEnabled(gmpAddon.id)) {
+ log.info(
+ "Auto-update is off for " + gmpAddon.id + ", skipping check."
+ );
+ } else {
+ addonUpdateEnabled = true;
+ }
+ } 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) {
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.setInt(GMPPrefs.KEY_UPDATE_LAST_EMPTY_CHECK, now);
+ 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 {
+ await this.installAddon(addon);
+ installResults.push({
+ id: addon.id,
+ result: "succeeded",
+ });
+ } catch (e) {
+ failureEncountered = true;
+ installResults.push({
+ id: addon.id,
+ result: "failed",
+ });
+ }
+ }
+ if (failureEncountered) {
+ // eslint-disable-next-line no-throw-literal
+ 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() {
+ 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,
+};
+
+/**
+ * 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 addon The ProductAddonChecker `addon` object
+ */
+export function GMPAddon(addon) {
+ let log = getScopedLogger("GMPAddon.constructor");
+ this.usedFallback = false;
+ for (let name of Object.keys(addon)) {
+ this[name] = addon[name];
+ }
+ log.info("Created new addon: " + this.toString());
+}
+
+GMPAddon.prototype = {
+ /**
+ * Returns a string representation of the addon
+ */
+ toString() {
+ 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 &&
+ !!this.hashValue &&
+ GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) ===
+ this.version &&
+ GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_HASHVALUE, "", this.id) ===
+ this.hashValue
+ );
+ },
+ get isEME() {
+ return this.id == WIDEVINE_L1_ID || this.id == WIDEVINE_L3_ID;
+ },
+ get isOpenH264() {
+ return this.id == "gmp-gmpopenh264";
+ },
+ /**
+ * @return true if the addon has been previously installed and this is
+ * a new version, if this is a fresh install return false
+ */
+ get isUpdate() {
+ return (
+ this.version &&
+ GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_VERSION, false, this.id)
+ );
+ },
+};
+
+/**
+ * Constructs a GMPExtractor object which is used to extract a GMP zip
+ * into the specified location.
+ * @param zipPath The path on disk of the zip file to extract
+ * @param relativePath The relative path components inside the profile directory
+ * to extract the zip to.
+ */
+export function GMPExtractor(zipPath, relativeInstallPath) {
+ this.zipPath = zipPath;
+ this.relativeInstallPath = relativeInstallPath;
+}
+
+GMPExtractor.prototype = {
+ /**
+ * 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() {
+ this._deferred = Promise.withResolvers();
+ let deferredPromise = this._deferred;
+ let { zipPath, relativeInstallPath } = this;
+ // Escape the zip path since the worker will use it as a URI
+ let zipFile = new lazy.FileUtils.File(zipPath);
+ let zipURI = Services.io.newFileURI(zipFile).spec;
+ let worker = new ChromeWorker(
+ "resource://gre/modules/GMPExtractor.worker.js"
+ );
+ worker.onmessage = function (msg) {
+ let log = getScopedLogger("GMPExtractor");
+ worker.terminate();
+ if (msg.data.result != "success") {
+ log.error("Failed to extract zip file: " + zipURI);
+ log.error("Exception: " + msg.data.exception);
+ return deferredPromise.reject({
+ target: this,
+ status: msg.data.exception,
+ type: "exception",
+ });
+ }
+ log.info("Successfully extracted zip file: " + zipURI);
+ return deferredPromise.resolve(msg.data.extractedPaths);
+ };
+ worker.postMessage({ zipURI, relativeInstallPath });
+ return this._deferred.promise;
+ },
+};
+
+/**
+ * Constructs an object which downloads and initiates an install of
+ * the specified GMPAddon object.
+ * @param gmpAddon The addon to install.
+ */
+export function GMPDownloader(gmpAddon) {
+ this._gmpAddon = gmpAddon;
+}
+
+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() {
+ let log = getScopedLogger("GMPDownloader");
+ let gmpAddon = this._gmpAddon;
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_INSTALL_START, now, gmpAddon.id);
+
+ if (!gmpAddon.isValid) {
+ log.info("gmpAddon is not valid, will not continue");
+ return Promise.reject({
+ target: this,
+ type: "downloaderr",
+ });
+ }
+ // If the HTTPS-Only Mode is enabled, every insecure request gets upgraded
+ // by default. This upgrade has to be prevented for openh264 downloads since
+ // the server doesn't support https://
+ const downloadOptions = {
+ httpsOnlyNoUpgrade: gmpAddon.isOpenH264,
+ };
+ return ProductAddonChecker.downloadAddon(gmpAddon, downloadOptions).then(
+ zipPath => {
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD, now, gmpAddon.id);
+ log.info(
+ `install to directory path: ${gmpAddon.id}/${gmpAddon.version}`
+ );
+ let gmpInstaller = new GMPExtractor(zipPath, [
+ gmpAddon.id,
+ gmpAddon.version,
+ ]);
+ let installPromise = gmpInstaller.install();
+ return installPromise.then(
+ extractedPaths => {
+ // Success, set the prefs
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id);
+ // Remember our ABI, so that if the profile is migrated to another
+ // platform or from 32 -> 64 bit, we notice and don't try to load the
+ // unexecutable plugin library.
+ let abi = GMPUtils._expectedABI(gmpAddon);
+ log.info("Setting ABI to '" + abi + "' for " + gmpAddon.id);
+ GMPPrefs.setString(GMPPrefs.KEY_PLUGIN_ABI, abi, gmpAddon.id);
+ // We use the combination of the hash and version to ensure we are
+ // up to date.
+ GMPPrefs.setString(
+ GMPPrefs.KEY_PLUGIN_HASHVALUE,
+ gmpAddon.hashValue,
+ gmpAddon.id
+ );
+ // Setting the version pref signals installation completion to consumers,
+ // if you need to set other prefs etc. do it before this.
+ GMPPrefs.setString(
+ GMPPrefs.KEY_PLUGIN_VERSION,
+ gmpAddon.version,
+ gmpAddon.id
+ );
+ return extractedPaths;
+ },
+ reason => {
+ GMPPrefs.setString(
+ GMPPrefs.KEY_PLUGIN_LAST_INSTALL_FAIL_REASON,
+ reason,
+ gmpAddon.id
+ );
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.setInt(
+ GMPPrefs.KEY_PLUGIN_LAST_INSTALL_FAILED,
+ now,
+ gmpAddon.id
+ );
+ throw reason;
+ }
+ );
+ },
+ reason => {
+ GMPPrefs.setString(
+ GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD_FAIL_REASON,
+ reason,
+ gmpAddon.id
+ );
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.setInt(
+ GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD_FAILED,
+ now,
+ gmpAddon.id
+ );
+ throw reason;
+ }
+ );
+ },
+};