mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 05:41:12 +00:00
f3a8c52c53
Differential Revision: https://phabricator.services.mozilla.com/D150893
809 lines
26 KiB
JavaScript
809 lines
26 KiB
JavaScript
/* 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";
|
|
|
|
// 1 day default
|
|
const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
|
|
|
|
const { PromiseUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/PromiseUtils.jsm"
|
|
);
|
|
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
|
|
const { GMPPrefs, GMPUtils, GMP_PLUGIN_IDS, WIDEVINE_ID } = ChromeUtils.import(
|
|
"resource://gre/modules/GMPUtils.jsm"
|
|
);
|
|
const { ProductAddonChecker } = ChromeUtils.import(
|
|
"resource://gre/modules/addons/ProductAddonChecker.jsm"
|
|
);
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"GMPInstallManager",
|
|
"GMPExtractor",
|
|
"GMPDownloader",
|
|
"GMPAddon",
|
|
];
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
lazy,
|
|
"CertUtils",
|
|
"resource://gre/modules/CertUtils.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
lazy,
|
|
"FileUtils",
|
|
"resource://gre/modules/FileUtils.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
lazy,
|
|
"UpdateUtils",
|
|
"resource://gre/modules/UpdateUtils.jsm"
|
|
);
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
lazy,
|
|
"ServiceRequest",
|
|
"resource://gre/modules/ServiceRequest.jsm"
|
|
);
|
|
|
|
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",
|
|
},
|
|
{
|
|
id: "gmp-widevinecdm",
|
|
src: "chrome://global/content/gmp-sources/widevinecdm.json",
|
|
},
|
|
];
|
|
|
|
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() {
|
|
let log = getScopedLogger("GMPInstallManager.downloadLocalConfig");
|
|
return Promise.all(
|
|
LOCAL_GMP_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,
|
|
};
|
|
});
|
|
})
|
|
).then(addons => {
|
|
// Some filters may not match this platform so
|
|
// filter those out
|
|
addons = addons.filter(x => x !== false);
|
|
|
|
return {
|
|
usedFallback: true,
|
|
addons,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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 = PromiseUtils.defer();
|
|
|
|
// 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 addonPromise = ProductAddonChecker.getProductAddonList(
|
|
url,
|
|
allowNonBuiltIn,
|
|
certs,
|
|
checkContentSignature
|
|
)
|
|
.then(res => {
|
|
if (checkContentSignature) {
|
|
this.recordUpdateXmlTelemetryForContentSignature(true);
|
|
} else {
|
|
this.recordUpdateXmlTelemetryForCertPinning(true);
|
|
}
|
|
return res;
|
|
})
|
|
.catch(err => {
|
|
if (checkContentSignature) {
|
|
this.recordUpdateXmlTelemetryForContentSignature(false, err);
|
|
} else {
|
|
this.recordUpdateXmlTelemetryForCertPinning(false, err);
|
|
}
|
|
return downloadLocalConfig();
|
|
});
|
|
|
|
addonPromise.then(
|
|
res => {
|
|
if (!res || !res.addons) {
|
|
this._deferred.resolve({ addons: [] });
|
|
} else {
|
|
res.addons = res.addons.map(a => new GMPAddon(a));
|
|
this._deferred.resolve(res);
|
|
}
|
|
delete this._deferred;
|
|
},
|
|
ex => {
|
|
this._deferred.reject(ex);
|
|
delete this._deferred;
|
|
}
|
|
);
|
|
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(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 { usedFallback, 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 (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
|
|
*/
|
|
function GMPAddon(addon) {
|
|
let log = getScopedLogger("GMPAddon.constructor");
|
|
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 &&
|
|
GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) ===
|
|
this.version
|
|
);
|
|
},
|
|
get isEME() {
|
|
return this.id == WIDEVINE_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.
|
|
*/
|
|
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 = PromiseUtils.defer();
|
|
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/GMPExtractorWorker.js"
|
|
);
|
|
worker.onmessage = function(msg) {
|
|
let log = getScopedLogger("GMPExtractor");
|
|
worker.terminate();
|
|
if (msg.data.result != "success") {
|
|
log.error("Failed to extract zip file: " + zipURI);
|
|
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.
|
|
*/
|
|
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);
|
|
// 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 => {
|
|
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;
|
|
}
|
|
);
|
|
},
|
|
};
|