From a606410ba941d4d093ad02a1dde26eb75a7fd80a Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Tue, 29 Jan 2019 10:48:41 -0500 Subject: [TychoAM] Fix GMP* so that EME works in Basilisk Also, keep WebExAM working for the moment --- toolkit/modules/GMPInstallManager.jsm | 523 ----------- toolkit/modules/GMPUtils.jsm | 208 ----- toolkit/modules/moz.build | 2 - toolkit/mozapps/extensions/GMPInstallManager.jsm | 961 +++++++++++++++++++++ toolkit/mozapps/extensions/GMPUtils.jsm | 208 +++++ .../mozapps/extensions/internal/GMPProvider.jsm | 12 + toolkit/mozapps/extensions/moz.build | 4 +- .../mozapps/webextensions/GMPInstallManager.jsm | 523 +++++++++++ toolkit/mozapps/webextensions/moz.build | 2 + 9 files changed, 1709 insertions(+), 734 deletions(-) delete mode 100644 toolkit/modules/GMPInstallManager.jsm delete mode 100644 toolkit/modules/GMPUtils.jsm create mode 100644 toolkit/mozapps/extensions/GMPInstallManager.jsm create mode 100644 toolkit/mozapps/extensions/GMPUtils.jsm create mode 100644 toolkit/mozapps/webextensions/GMPInstallManager.jsm (limited to 'toolkit') diff --git a/toolkit/modules/GMPInstallManager.jsm b/toolkit/modules/GMPInstallManager.jsm deleted file mode 100644 index b5987ca55..000000000 --- a/toolkit/modules/GMPInstallManager.jsm +++ /dev/null @@ -1,523 +0,0 @@ -/* 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; -// 1 day default -const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24; - -var GMPInstallFailureReason = { - GMP_INVALID: 1, - GMP_HIDDEN: 2, - GMP_DISABLED: 3, - GMP_UPDATE_DISABLED: 4, -}; - -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/GMPUtils.jsm"); -Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm"); - -this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader", - "GMPAddon"]; - -// 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"); - -function getScopedLogger(prefix) { - // `PARENT_LOGGER_ID.` being passed here effectively links this logger - // to the parentLogger. - return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " "); -} - -/** - * 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 object with properties: - * gmpAddons: array of GMPAddons - * 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 - */ - 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(); - - let allowNonBuiltIn = true; - let certs = null; - if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE)) { - allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN, true); - if (GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) { - certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH); - } - } - - let addonPromise = ProductAddonChecker - .getProductAddonList(url, allowNonBuiltIn, certs); - - addonPromise.then(res => { - if (!res || !res.gmpAddons) { - this._deferred.resolve({gmpAddons: []}); - } - else { - res.gmpAddons = res.gmpAddons.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: 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); - }, - _isAddonEnabled: function(aAddon) { - return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon); - }, - _isAddonUpdateEnabled: function(aAddon) { - return this._isAddonEnabled(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 {usedFallback, 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) { - 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.indexOf(gmpAddon.id) >= 0) { - 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) { - 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, -}; - -/** - * 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: 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; - }, - /** - * @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.get(GMPPrefs.KEY_PLUGIN_VERSION, false, this.id); - }, -}; -/** - * 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 = []; - - let destDir = Cc["@mozilla.org/file/local;1"]. - createInstance(Ci.nsILocalFile); - destDir.initWithPath(this.installToDirPath); - // Make sure the destination exists - if (!destDir.exists()) { - destDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); - } - - // Extract each of the entries - entries.forEach(entry => { - // We don't need these types of files - if (entry.includes("__MACOSX") || - entry == "_metadata/verified_contents.json" || - entry == "imgs/icon-128x128.png") { - return; - } - let outFile = destDir.clone(); - // Do not extract into directories. Extract all files to the same - // directory. DO NOT use |OS.Path.basename()| here, as in Windows it - // does not work properly with forward slashes (which we must use here). - let outBaseName = entry.slice(entry.lastIndexOf("/") + 1); - outFile.appendRelativePath(outBaseName); - - zipReader.extract(entry, outFile); - extractedPaths.push(outFile.path); - // Ensure files are writable and executable. Otherwise we may be unable to - // execute or uninstall them. - outFile.permissions |= parseInt("0700", 8); - 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; -} - -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"); - let gmpAddon = this._gmpAddon; - - if (!gmpAddon.isValid) { - log.info("gmpAddon is not valid, will not continue"); - return Promise.reject({ - target: this, - status: status, - type: "downloaderr" - }); - } - - return ProductAddonChecker.downloadAddon(gmpAddon).then((zipPath) => { - let path = OS.Path.join(OS.Constants.Path.profileDir, - gmpAddon.id, - gmpAddon.version); - log.info("install to directory path: " + path); - let gmpInstaller = new GMPExtractor(zipPath, path); - let installPromise = gmpInstaller.install(); - return installPromise.then(extractedPaths => { - // Success, set the prefs - let now = Math.round(Date.now() / 1000); - GMPPrefs.set(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. - GMPPrefs.set(GMPPrefs.KEY_PLUGIN_ABI, UpdateUtils.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.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version, - gmpAddon.id); - return extractedPaths; - }); - }); - }, -}; diff --git a/toolkit/modules/GMPUtils.jsm b/toolkit/modules/GMPUtils.jsm deleted file mode 100644 index 9e41a7a61..000000000 --- a/toolkit/modules/GMPUtils.jsm +++ /dev/null @@ -1,208 +0,0 @@ -/* 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"; - -const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = - Components; - -this.EXPORTED_SYMBOLS = [ "EME_ADOBE_ID", - "GMP_PLUGIN_IDS", - "GMPPrefs", - "GMPUtils", - "OPEN_H264_ID", - "WIDEVINE_ID" ]; - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/AppConstants.jsm"); - -// GMP IDs -const OPEN_H264_ID = "gmp-gmpopenh264"; -const EME_ADOBE_ID = "gmp-eme-adobe"; -const WIDEVINE_ID = "gmp-widevinecdm"; -const GMP_PLUGIN_IDS = [ OPEN_H264_ID, EME_ADOBE_ID, WIDEVINE_ID ]; - -var GMPPluginUnsupportedReason = { - NOT_WINDOWS: 1, - WINDOWS_VERSION: 2, -}; - -var GMPPluginHiddenReason = { - UNSUPPORTED: 1, - EME_DISABLED: 2, -}; - -this.GMPUtils = { - /** - * Checks whether or not a given plugin is hidden. Hidden plugins are neither - * downloaded nor displayed in the addons manager. - * @param aPlugin - * The plugin to check. - */ - isPluginHidden: function(aPlugin) { - if (this._is32bitModeMacOS()) { - // GMPs are hidden on MacOS when running in 32 bit mode. - // See bug 1291537. - return true; - } - if (!aPlugin.isEME) { - return false; - } - - if (!this._isPluginSupported(aPlugin) || - !this._isPluginVisible(aPlugin)) { - return true; - } - - if (!GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) { - return true; - } - - return false; - }, - - /** - * Checks whether or not a given plugin is supported by the current OS. - * @param aPlugin - * The plugin to check. - */ - _isPluginSupported: function(aPlugin) { - if (this._isPluginForceSupported(aPlugin)) { - return true; - } - if (aPlugin.id == EME_ADOBE_ID) { - // Windows Vista and later only supported by Adobe EME. - return AppConstants.isPlatformAndVersionAtLeast("win", "6"); - } else if (aPlugin.id == WIDEVINE_ID) { - // The Widevine plugin is available for Windows versions Vista and later, - // Mac OSX, and Linux. - return AppConstants.isPlatformAndVersionAtLeast("win", "6") || - AppConstants.platform == "macosx" || - AppConstants.platform == "linux"; - } - - return true; - }, - - _is32bitModeMacOS: function() { - if (AppConstants.platform != "macosx") { - return false; - } - return Services.appinfo.XPCOMABI.split("-")[0] == "x86"; - }, - - /** - * Checks whether or not a given plugin is visible in the addons manager - * UI and the "enable DRM" notification box. This can be used to test - * plugins that aren't yet turned on in the mozconfig. - * @param aPlugin - * The plugin to check. - */ - _isPluginVisible: function(aPlugin) { - return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VISIBLE, false, aPlugin.id); - }, - - /** - * Checks whether or not a given plugin is forced-supported. This is used - * in automated tests to override the checks that prevent GMPs running on an - * unsupported platform. - * @param aPlugin - * The plugin to check. - */ - _isPluginForceSupported: function(aPlugin) { - return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, false, aPlugin.id); - }, -}; - -/** - * Manages preferences for GMP addons - */ -this.GMPPrefs = { - KEY_EME_ENABLED: "media.eme.enabled", - KEY_PLUGIN_ENABLED: "media.{0}.enabled", - KEY_PLUGIN_LAST_UPDATE: "media.{0}.lastUpdate", - KEY_PLUGIN_VERSION: "media.{0}.version", - KEY_PLUGIN_AUTOUPDATE: "media.{0}.autoupdate", - KEY_PLUGIN_VISIBLE: "media.{0}.visible", - KEY_PLUGIN_ABI: "media.{0}.abi", - KEY_PLUGIN_FORCE_SUPPORTED: "media.{0}.forceSupported", - KEY_URL: "media.gmp-manager.url", - KEY_URL_OVERRIDE: "media.gmp-manager.url.override", - KEY_CERT_CHECKATTRS: "media.gmp-manager.cert.checkAttributes", - KEY_CERT_REQUIREBUILTIN: "media.gmp-manager.cert.requireBuiltIn", - KEY_UPDATE_LAST_CHECK: "media.gmp-manager.lastCheck", - KEY_SECONDS_BETWEEN_CHECKS: "media.gmp-manager.secondsBetweenChecks", - KEY_UPDATE_ENABLED: "media.gmp-manager.updateEnabled", - KEY_APP_DISTRIBUTION: "distribution.id", - KEY_APP_DISTRIBUTION_VERSION: "distribution.version", - KEY_BUILDID: "media.gmp-manager.buildID", - KEY_CERTS_BRANCH: "media.gmp-manager.certs.", - KEY_PROVIDER_ENABLED: "media.gmp-provider.enabled", - KEY_LOG_BASE: "media.gmp.log.", - KEY_LOGGING_LEVEL: "media.gmp.log.level", - KEY_LOGGING_DUMP: "media.gmp.log.dump", - - /** - * Obtains the specified preference in relation to the specified plugin. - * @param aKey The preference key value to use. - * @param aDefaultValue The default value if no preference exists. - * @param aPlugin The plugin to scope the preference to. - * @return The obtained preference value, or the defaultValue if none exists. - */ - get: function(aKey, aDefaultValue, aPlugin) { - if (aKey === this.KEY_APP_DISTRIBUTION || - aKey === this.KEY_APP_DISTRIBUTION_VERSION) { - let prefValue = "default"; - try { - prefValue = Services.prefs.getDefaultBranch(null).getCharPref(aKey); - } catch (e) { - // use default when pref not found - } - return prefValue; - } - return Preferences.get(this.getPrefKey(aKey, aPlugin), aDefaultValue); - }, - - /** - * Sets the specified preference in relation to the specified plugin. - * @param aKey The preference key value to use. - * @param aVal The value to set. - * @param aPlugin The plugin to scope the preference to. - */ - set: function(aKey, aVal, aPlugin) { - Preferences.set(this.getPrefKey(aKey, aPlugin), aVal); - }, - - /** - * Checks whether or not the specified preference is set in relation to the - * specified plugin. - * @param aKey The preference key value to use. - * @param aPlugin The plugin to scope the preference to. - * @return true if the preference is set, false otherwise. - */ - isSet: function(aKey, aPlugin) { - return Preferences.isSet(this.getPrefKey(aKey, aPlugin)); - }, - - /** - * Resets the specified preference in relation to the specified plugin to its - * default. - * @param aKey The preference key value to use. - * @param aPlugin The plugin to scope the preference to. - */ - reset: function(aKey, aPlugin) { - Preferences.reset(this.getPrefKey(aKey, aPlugin)); - }, - - /** - * Scopes the specified preference key to the specified plugin. - * @param aKey The preference key value to use. - * @param aPlugin The plugin to scope the preference to. - * @return A preference key scoped to the specified plugin. - */ - getPrefKey: function(aKey, aPlugin) { - return aKey.replace("{0}", aPlugin || ""); - }, -}; diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index e288d11c8..948d8d2c9 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -46,8 +46,6 @@ EXTRA_JS_MODULES += [ 'FinderIterator.jsm', 'FormLikeFactory.jsm', 'Geometry.jsm', - 'GMPInstallManager.jsm', - 'GMPUtils.jsm', 'Http.jsm', 'InlineSpellChecker.jsm', 'InlineSpellCheckerContent.jsm', 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; +} diff --git a/toolkit/mozapps/extensions/GMPUtils.jsm b/toolkit/mozapps/extensions/GMPUtils.jsm new file mode 100644 index 000000000..9e41a7a61 --- /dev/null +++ b/toolkit/mozapps/extensions/GMPUtils.jsm @@ -0,0 +1,208 @@ +/* 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"; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = + Components; + +this.EXPORTED_SYMBOLS = [ "EME_ADOBE_ID", + "GMP_PLUGIN_IDS", + "GMPPrefs", + "GMPUtils", + "OPEN_H264_ID", + "WIDEVINE_ID" ]; + +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +// GMP IDs +const OPEN_H264_ID = "gmp-gmpopenh264"; +const EME_ADOBE_ID = "gmp-eme-adobe"; +const WIDEVINE_ID = "gmp-widevinecdm"; +const GMP_PLUGIN_IDS = [ OPEN_H264_ID, EME_ADOBE_ID, WIDEVINE_ID ]; + +var GMPPluginUnsupportedReason = { + NOT_WINDOWS: 1, + WINDOWS_VERSION: 2, +}; + +var GMPPluginHiddenReason = { + UNSUPPORTED: 1, + EME_DISABLED: 2, +}; + +this.GMPUtils = { + /** + * Checks whether or not a given plugin is hidden. Hidden plugins are neither + * downloaded nor displayed in the addons manager. + * @param aPlugin + * The plugin to check. + */ + isPluginHidden: function(aPlugin) { + if (this._is32bitModeMacOS()) { + // GMPs are hidden on MacOS when running in 32 bit mode. + // See bug 1291537. + return true; + } + if (!aPlugin.isEME) { + return false; + } + + if (!this._isPluginSupported(aPlugin) || + !this._isPluginVisible(aPlugin)) { + return true; + } + + if (!GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) { + return true; + } + + return false; + }, + + /** + * Checks whether or not a given plugin is supported by the current OS. + * @param aPlugin + * The plugin to check. + */ + _isPluginSupported: function(aPlugin) { + if (this._isPluginForceSupported(aPlugin)) { + return true; + } + if (aPlugin.id == EME_ADOBE_ID) { + // Windows Vista and later only supported by Adobe EME. + return AppConstants.isPlatformAndVersionAtLeast("win", "6"); + } else if (aPlugin.id == WIDEVINE_ID) { + // The Widevine plugin is available for Windows versions Vista and later, + // Mac OSX, and Linux. + return AppConstants.isPlatformAndVersionAtLeast("win", "6") || + AppConstants.platform == "macosx" || + AppConstants.platform == "linux"; + } + + return true; + }, + + _is32bitModeMacOS: function() { + if (AppConstants.platform != "macosx") { + return false; + } + return Services.appinfo.XPCOMABI.split("-")[0] == "x86"; + }, + + /** + * Checks whether or not a given plugin is visible in the addons manager + * UI and the "enable DRM" notification box. This can be used to test + * plugins that aren't yet turned on in the mozconfig. + * @param aPlugin + * The plugin to check. + */ + _isPluginVisible: function(aPlugin) { + return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VISIBLE, false, aPlugin.id); + }, + + /** + * Checks whether or not a given plugin is forced-supported. This is used + * in automated tests to override the checks that prevent GMPs running on an + * unsupported platform. + * @param aPlugin + * The plugin to check. + */ + _isPluginForceSupported: function(aPlugin) { + return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, false, aPlugin.id); + }, +}; + +/** + * Manages preferences for GMP addons + */ +this.GMPPrefs = { + KEY_EME_ENABLED: "media.eme.enabled", + KEY_PLUGIN_ENABLED: "media.{0}.enabled", + KEY_PLUGIN_LAST_UPDATE: "media.{0}.lastUpdate", + KEY_PLUGIN_VERSION: "media.{0}.version", + KEY_PLUGIN_AUTOUPDATE: "media.{0}.autoupdate", + KEY_PLUGIN_VISIBLE: "media.{0}.visible", + KEY_PLUGIN_ABI: "media.{0}.abi", + KEY_PLUGIN_FORCE_SUPPORTED: "media.{0}.forceSupported", + KEY_URL: "media.gmp-manager.url", + KEY_URL_OVERRIDE: "media.gmp-manager.url.override", + KEY_CERT_CHECKATTRS: "media.gmp-manager.cert.checkAttributes", + KEY_CERT_REQUIREBUILTIN: "media.gmp-manager.cert.requireBuiltIn", + KEY_UPDATE_LAST_CHECK: "media.gmp-manager.lastCheck", + KEY_SECONDS_BETWEEN_CHECKS: "media.gmp-manager.secondsBetweenChecks", + KEY_UPDATE_ENABLED: "media.gmp-manager.updateEnabled", + KEY_APP_DISTRIBUTION: "distribution.id", + KEY_APP_DISTRIBUTION_VERSION: "distribution.version", + KEY_BUILDID: "media.gmp-manager.buildID", + KEY_CERTS_BRANCH: "media.gmp-manager.certs.", + KEY_PROVIDER_ENABLED: "media.gmp-provider.enabled", + KEY_LOG_BASE: "media.gmp.log.", + KEY_LOGGING_LEVEL: "media.gmp.log.level", + KEY_LOGGING_DUMP: "media.gmp.log.dump", + + /** + * Obtains the specified preference in relation to the specified plugin. + * @param aKey The preference key value to use. + * @param aDefaultValue The default value if no preference exists. + * @param aPlugin The plugin to scope the preference to. + * @return The obtained preference value, or the defaultValue if none exists. + */ + get: function(aKey, aDefaultValue, aPlugin) { + if (aKey === this.KEY_APP_DISTRIBUTION || + aKey === this.KEY_APP_DISTRIBUTION_VERSION) { + let prefValue = "default"; + try { + prefValue = Services.prefs.getDefaultBranch(null).getCharPref(aKey); + } catch (e) { + // use default when pref not found + } + return prefValue; + } + return Preferences.get(this.getPrefKey(aKey, aPlugin), aDefaultValue); + }, + + /** + * Sets the specified preference in relation to the specified plugin. + * @param aKey The preference key value to use. + * @param aVal The value to set. + * @param aPlugin The plugin to scope the preference to. + */ + set: function(aKey, aVal, aPlugin) { + Preferences.set(this.getPrefKey(aKey, aPlugin), aVal); + }, + + /** + * Checks whether or not the specified preference is set in relation to the + * specified plugin. + * @param aKey The preference key value to use. + * @param aPlugin The plugin to scope the preference to. + * @return true if the preference is set, false otherwise. + */ + isSet: function(aKey, aPlugin) { + return Preferences.isSet(this.getPrefKey(aKey, aPlugin)); + }, + + /** + * Resets the specified preference in relation to the specified plugin to its + * default. + * @param aKey The preference key value to use. + * @param aPlugin The plugin to scope the preference to. + */ + reset: function(aKey, aPlugin) { + Preferences.reset(this.getPrefKey(aKey, aPlugin)); + }, + + /** + * Scopes the specified preference key to the specified plugin. + * @param aKey The preference key value to use. + * @param aPlugin The plugin to scope the preference to. + * @return A preference key scoped to the specified plugin. + */ + getPrefKey: function(aKey, aPlugin) { + return aKey.replace("{0}", aPlugin || ""); + }, +}; diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.jsm b/toolkit/mozapps/extensions/internal/GMPProvider.jsm index 25651f1b8..131db7249 100644 --- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm +++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm @@ -49,6 +49,7 @@ const GMP_PLUGINS = [ homepageURL: "http://www.openh264.org/", optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul" }, +/* { id: EME_ADOBE_ID, name: "eme-adobe_name", @@ -57,6 +58,17 @@ const GMP_PLUGINS = [ homepageURL: "http://help.adobe.com/en_US/primetime/drm/HTML5_CDM", optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul", isEME: true + }, +*/ + { + id: WIDEVINE_ID, + name: "widevine_description", + // Describe the purpose of both CDMs in the same way. + description: "eme-adobe_description", + licenseURL: "https://www.google.com/policies/privacy/", + homepageURL: "https://www.widevine.com/", + optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul", + isEME: true }]; XPCOMUtils.defineConstant(this, "GMP_PLUGINS", GMP_PLUGINS); diff --git a/toolkit/mozapps/extensions/moz.build b/toolkit/mozapps/extensions/moz.build index 3988cc27a..104e8d734 100644 --- a/toolkit/mozapps/extensions/moz.build +++ b/toolkit/mozapps/extensions/moz.build @@ -29,11 +29,13 @@ EXTRA_PP_COMPONENTS += [ EXTRA_JS_MODULES += [ 'ChromeManifestParser.jsm', 'DeferredSave.jsm', + 'GMPUtils.jsm', 'LightweightThemeManager.jsm', ] EXTRA_PP_JS_MODULES += [ - 'AddonManager.jsm' + 'AddonManager.jsm', + 'GMPInstallManager.jsm', ] # Additional debugging info is exposed in debug builds diff --git a/toolkit/mozapps/webextensions/GMPInstallManager.jsm b/toolkit/mozapps/webextensions/GMPInstallManager.jsm new file mode 100644 index 000000000..b5987ca55 --- /dev/null +++ b/toolkit/mozapps/webextensions/GMPInstallManager.jsm @@ -0,0 +1,523 @@ +/* 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; +// 1 day default +const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24; + +var GMPInstallFailureReason = { + GMP_INVALID: 1, + GMP_HIDDEN: 2, + GMP_DISABLED: 3, + GMP_UPDATE_DISABLED: 4, +}; + +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/GMPUtils.jsm"); +Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm"); + +this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader", + "GMPAddon"]; + +// 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"); + +function getScopedLogger(prefix) { + // `PARENT_LOGGER_ID.` being passed here effectively links this logger + // to the parentLogger. + return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " "); +} + +/** + * 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 object with properties: + * gmpAddons: array of GMPAddons + * 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 + */ + 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(); + + let allowNonBuiltIn = true; + let certs = null; + if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE)) { + allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN, true); + if (GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) { + certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH); + } + } + + let addonPromise = ProductAddonChecker + .getProductAddonList(url, allowNonBuiltIn, certs); + + addonPromise.then(res => { + if (!res || !res.gmpAddons) { + this._deferred.resolve({gmpAddons: []}); + } + else { + res.gmpAddons = res.gmpAddons.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: 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); + }, + _isAddonEnabled: function(aAddon) { + return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon); + }, + _isAddonUpdateEnabled: function(aAddon) { + return this._isAddonEnabled(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 {usedFallback, 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) { + 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.indexOf(gmpAddon.id) >= 0) { + 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) { + 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, +}; + +/** + * 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: 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; + }, + /** + * @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.get(GMPPrefs.KEY_PLUGIN_VERSION, false, this.id); + }, +}; +/** + * 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 = []; + + let destDir = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsILocalFile); + destDir.initWithPath(this.installToDirPath); + // Make sure the destination exists + if (!destDir.exists()) { + destDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } + + // Extract each of the entries + entries.forEach(entry => { + // We don't need these types of files + if (entry.includes("__MACOSX") || + entry == "_metadata/verified_contents.json" || + entry == "imgs/icon-128x128.png") { + return; + } + let outFile = destDir.clone(); + // Do not extract into directories. Extract all files to the same + // directory. DO NOT use |OS.Path.basename()| here, as in Windows it + // does not work properly with forward slashes (which we must use here). + let outBaseName = entry.slice(entry.lastIndexOf("/") + 1); + outFile.appendRelativePath(outBaseName); + + zipReader.extract(entry, outFile); + extractedPaths.push(outFile.path); + // Ensure files are writable and executable. Otherwise we may be unable to + // execute or uninstall them. + outFile.permissions |= parseInt("0700", 8); + 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; +} + +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"); + let gmpAddon = this._gmpAddon; + + if (!gmpAddon.isValid) { + log.info("gmpAddon is not valid, will not continue"); + return Promise.reject({ + target: this, + status: status, + type: "downloaderr" + }); + } + + return ProductAddonChecker.downloadAddon(gmpAddon).then((zipPath) => { + let path = OS.Path.join(OS.Constants.Path.profileDir, + gmpAddon.id, + gmpAddon.version); + log.info("install to directory path: " + path); + let gmpInstaller = new GMPExtractor(zipPath, path); + let installPromise = gmpInstaller.install(); + return installPromise.then(extractedPaths => { + // Success, set the prefs + let now = Math.round(Date.now() / 1000); + GMPPrefs.set(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. + GMPPrefs.set(GMPPrefs.KEY_PLUGIN_ABI, UpdateUtils.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.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version, + gmpAddon.id); + return extractedPaths; + }); + }); + }, +}; diff --git a/toolkit/mozapps/webextensions/moz.build b/toolkit/mozapps/webextensions/moz.build index e703125e6..f6e83a355 100644 --- a/toolkit/mozapps/webextensions/moz.build +++ b/toolkit/mozapps/webextensions/moz.build @@ -30,8 +30,10 @@ EXTRA_PP_COMPONENTS += [ EXTRA_JS_MODULES += [ '../extensions/ChromeManifestParser.jsm', '../extensions/DeferredSave.jsm', + '../extensions/GMPUtils.jsm', 'AddonManager.jsm', 'LightweightThemeManager.jsm', + 'GMPInstallManager.jsm', ] JAR_MANIFESTS += ['jar.mn'] -- cgit v1.2.3