summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2022-04-20 11:45:55 -0500
committerMatt A. Tobin <email@mattatobin.com>2022-04-20 11:45:55 -0500
commit87dabd4e0b724bb81db3eaeefa09cfd2c5545a36 (patch)
treef91a4de96d859747a5028abf252e87d3361006cb /components
parent1e640f82104e57d63e268785f75914ca7d3953ce (diff)
downloadaura-central-87dabd4e0b724bb81db3eaeefa09cfd2c5545a36.tar.gz
Issue #1 - Restore Gecko Media Plugins
Diffstat (limited to 'components')
-rw-r--r--components/addons/content/OpenH264-license.txt59
-rw-r--r--components/addons/content/gmpPrefs.xul8
-rw-r--r--components/addons/extensions.manifest1
-rw-r--r--components/addons/jar.mn2
-rw-r--r--components/addons/moz.build4
-rw-r--r--components/addons/src/GMPInstallManager.jsm917
-rw-r--r--components/addons/src/GMPProvider.jsm605
-rw-r--r--components/addons/src/GMPUtils.jsm187
-rw-r--r--components/addons/src/ProductAddonChecker.jsm464
-rw-r--r--components/global/content/gmp-sources/openh264.json57
-rw-r--r--components/global/content/gmp-sources/widevinecdm.json49
-rw-r--r--components/global/content/process-content.js7
-rw-r--r--components/global/jar.mn2
13 files changed, 2362 insertions, 0 deletions
diff --git a/components/addons/content/OpenH264-license.txt b/components/addons/content/OpenH264-license.txt
new file mode 100644
index 000000000..ad37989b8
--- /dev/null
+++ b/components/addons/content/OpenH264-license.txt
@@ -0,0 +1,59 @@
+-------------------------------------------------------
+About The Cisco-Provided Binary of OpenH264 Video Codec
+-------------------------------------------------------
+
+Cisco provides this program under the terms of the BSD license.
+
+Additionally, this binary is licensed under Cisco’s AVC/H.264 Patent Portfolio License from MPEG LA, at no cost to you, provided that the requirements and conditions shown below in the AVC/H.264 Patent Portfolio sections are met.
+
+As with all AVC/H.264 codecs, you may also obtain your own patent license from MPEG LA or from the individual patent owners, or proceed at your own risk. Your rights from Cisco under the BSD license are not affected by this choice.
+
+For more information on the OpenH264 binary licensing, please see the OpenH264 FAQ found at http://www.openh264.org/faq.html#binary
+
+A corresponding source code to this binary program is available under the same BSD terms, which can be found at http://www.openh264.org
+
+-----------
+BSD License
+-----------
+
+Copyright © 2014 Cisco Systems, Inc.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-----------------------------------------
+AVC/H.264 Patent Portfolio License Notice
+-----------------------------------------
+
+The binary form of this Software is distributed by Cisco under the AVC/H.264 Patent Portfolio License from MPEG LA, and is subject to the following requirements, which may or may not be applicable to your use of this software:
+
+THIS PRODUCT IS LICENSED UNDER THE AVC PATENT PORTFOLIO LICENSE FOR THE PERSONAL USE OF A CONSUMER OR OTHER USES IN WHICH IT DOES NOT RECEIVE REMUNERATION TO (i) ENCODE VIDEO IN COMPLIANCE WITH THE AVC STANDARD (“AVC VIDEO”) AND/OR (ii) DECODE AVC VIDEO THAT WAS ENCODED BY A CONSUMER ENGAGED IN A PERSONAL ACTIVITY AND/OR WAS OBTAINED FROM A VIDEO PROVIDER LICENSED TO PROVIDE AVC VIDEO. NO LICENSE IS GRANTED OR SHALL BE IMPLIED FOR ANY OTHER USE. ADDITIONAL INFORMATION MAY BE OBTAINED FROM MPEG LA, L.L.C. SEE HTTP://WWW.MPEGLA.COM
+
+Accordingly, please be advised that content providers and broadcasters using AVC/H.264 in their service may be required to obtain a separate use license from MPEG LA, referred to as "(b) sublicenses" in the SUMMARY OF AVC/H.264 LICENSE TERMS from MPEG LA found at http://www.openh264.org/mpegla
+
+---------------------------------------------
+AVC/H.264 Patent Portfolio License Conditions
+---------------------------------------------
+
+In addition, the Cisco-provided binary of this Software is licensed under Cisco's license from MPEG LA only if the following conditions are met:
+
+1. The Cisco-provided binary is separately downloaded to an end user’s device, and not integrated into or combined with third party software prior to being downloaded to the end user’s device;
+
+2. The end user must have the ability to control (e.g., to enable, disable, or re-enable) the use of the Cisco-provided binary;
+
+3. Third party software, in the location where end users can control the use of the Cisco-provided binary, must display the following text:
+
+ "OpenH264 Video Codec provided by Cisco Systems, Inc."
+
+4. Any third-party software that makes use of the Cisco-provided binary must reproduce all of the above text, as well as this last condition, in the EULA and/or in another location where licensing information is to be presented to the end user.
+
+
+
+ v1.0
diff --git a/components/addons/content/gmpPrefs.xul b/components/addons/content/gmpPrefs.xul
new file mode 100644
index 000000000..ea7ee92fa
--- /dev/null
+++ b/components/addons/content/gmpPrefs.xul
@@ -0,0 +1,8 @@
+<?xml version="1.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/. -->
+
+<!-- This is intentionally empty and a dummy to let the GMPProvider
+ have a preferences button in the list view. -->
diff --git a/components/addons/extensions.manifest b/components/addons/extensions.manifest
index 35d605575..b56152e10 100644
--- a/components/addons/extensions.manifest
+++ b/components/addons/extensions.manifest
@@ -13,3 +13,4 @@ contract @mozilla.org/addons/installtrigger;1 {9df8ef2b-94da-45c9-ab9f-132eb55fd
category JavaScript-global-property InstallTrigger @mozilla.org/addons/installtrigger;1
category addon-provider-module PluginProvider resource://gre/modules/addons/PluginProvider.jsm
+category addon-provider-module GMPProvider resource://gre/modules/addons/GMPProvider.jsm
diff --git a/components/addons/jar.mn b/components/addons/jar.mn
index 0f4bfce26..878be4df1 100644
--- a/components/addons/jar.mn
+++ b/components/addons/jar.mn
@@ -29,6 +29,8 @@ toolkit.jar:
content/mozapps/extensions/newaddon.js (content/newaddon.js)
content/mozapps/extensions/setting.xml (content/setting.xml)
content/mozapps/extensions/pluginPrefs.xul (content/pluginPrefs.xul)
+ content/mozapps/extensions/gmpPrefs.xul (content/gmpPrefs.xul)
+ content/mozapps/extensions/OpenH264-license.txt (content/OpenH264-license.txt)
content/mozapps/xpinstall/xpinstallConfirm.xul (content/xpinstallConfirm.xul)
content/mozapps/xpinstall/xpinstallConfirm.js (content/xpinstallConfirm.js)
content/mozapps/xpinstall/xpinstallConfirm.css (content/xpinstallConfirm.css)
diff --git a/components/addons/moz.build b/components/addons/moz.build
index 22193c1e7..6b1e2fe93 100644
--- a/components/addons/moz.build
+++ b/components/addons/moz.build
@@ -39,6 +39,8 @@ EXTRA_JS_MODULES += [
EXTRA_PP_JS_MODULES += [
'src/AddonManager.jsm',
+ 'src/GMPInstallManager.jsm',
+ 'src/GMPUtils.jsm',
]
EXTRA_JS_MODULES.addons += [
@@ -46,8 +48,10 @@ EXTRA_JS_MODULES.addons += [
'src/AddonRepository.jsm',
'src/AddonRepository_SQLiteMigrator.jsm',
'src/Content.js',
+ 'src/GMPProvider.jsm',
'src/LightweightThemeImageOptimizer.jsm',
'src/PluginProvider.jsm',
+ 'src/ProductAddonChecker.jsm',
'src/SpellCheckDictionaryBootstrap.js',
]
diff --git a/components/addons/src/GMPInstallManager.jsm b/components/addons/src/GMPInstallManager.jsm
new file mode 100644
index 000000000..fe4e2de10
--- /dev/null
+++ b/components/addons/src/GMPInstallManager.jsm
@@ -0,0 +1,917 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} =
+ Components;
+// Chunk size for the incremental downloader
+const DOWNLOAD_CHUNK_BYTES_SIZE = 300000;
+// Incremental downloader interval
+const DOWNLOAD_INTERVAL = 0;
+// 1 day default
+const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/ctypes.jsm");
+Cu.import("resource://gre/modules/GMPUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader",
+ "GMPAddon"];
+
+var gLocale = null;
+
+// Shared code for suppressing bad cert dialogs
+XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() {
+ let temp = { };
+ Cu.import("resource://gre/modules/CertUtils.jsm", temp);
+ return temp;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+/**
+ * Number of milliseconds after which we need to cancel `checkForAddons`.
+ *
+ * Bug 1087674 suggests that the XHR we use in `checkForAddons` may
+ * never terminate in presence of network nuisances (e.g. strange
+ * antivirus behavior). This timeout is a defensive measure to ensure
+ * that we fail cleanly in such case.
+ */
+const CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS = 20000;
+
+function getScopedLogger(prefix) {
+ // `PARENT_LOGGER_ID.` being passed here effectively links this logger
+ // to the parentLogger.
+ return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " ");
+}
+
+// This is copied directly from nsUpdateService.js
+// It is used for calculating the URL string w/ var replacement.
+// TODO: refactor this out somewhere else
+XPCOMUtils.defineLazyGetter(this, "gOSVersion", function aus_gOSVersion() {
+ let osVersion;
+ let sysInfo = Cc["@mozilla.org/system-info;1"].
+ getService(Ci.nsIPropertyBag2);
+ try {
+ osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
+ }
+ catch (e) {
+ LOG("gOSVersion - OS Version unknown: updates are not possible.");
+ }
+
+ if (osVersion) {
+#ifdef XP_WIN
+ const BYTE = ctypes.uint8_t;
+ const WORD = ctypes.uint16_t;
+ const DWORD = ctypes.uint32_t;
+ const WCHAR = ctypes.char16_t;
+ const BOOL = ctypes.int;
+
+ // This structure is described at:
+ // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
+ const SZCSDVERSIONLENGTH = 128;
+ const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW',
+ [
+ {dwOSVersionInfoSize: DWORD},
+ {dwMajorVersion: DWORD},
+ {dwMinorVersion: DWORD},
+ {dwBuildNumber: DWORD},
+ {dwPlatformId: DWORD},
+ {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
+ {wServicePackMajor: WORD},
+ {wServicePackMinor: WORD},
+ {wSuiteMask: WORD},
+ {wProductType: BYTE},
+ {wReserved: BYTE}
+ ]);
+
+ // This structure is described at:
+ // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx
+ const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO',
+ [
+ {wProcessorArchitecture: WORD},
+ {wReserved: WORD},
+ {dwPageSize: DWORD},
+ {lpMinimumApplicationAddress: ctypes.voidptr_t},
+ {lpMaximumApplicationAddress: ctypes.voidptr_t},
+ {dwActiveProcessorMask: DWORD.ptr},
+ {dwNumberOfProcessors: DWORD},
+ {dwProcessorType: DWORD},
+ {dwAllocationGranularity: DWORD},
+ {wProcessorLevel: WORD},
+ {wProcessorRevision: WORD}
+ ]);
+
+ let kernel32 = false;
+ try {
+ kernel32 = ctypes.open("Kernel32");
+ } catch (e) {
+ LOG("gOSVersion - Unable to open kernel32! " + e);
+ osVersion += ".unknown (unknown)";
+ }
+
+ if(kernel32) {
+ try {
+ // Get Service pack info
+ try {
+ let GetVersionEx = kernel32.declare("GetVersionExW",
+ ctypes.default_abi,
+ BOOL,
+ OSVERSIONINFOEXW.ptr);
+ let winVer = OSVERSIONINFOEXW();
+ winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
+
+ if(0 !== GetVersionEx(winVer.address())) {
+ osVersion += "." + winVer.wServicePackMajor
+ + "." + winVer.wServicePackMinor;
+ } else {
+ LOG("gOSVersion - Unknown failure in GetVersionEX (returned 0)");
+ osVersion += ".unknown";
+ }
+ } catch (e) {
+ LOG("gOSVersion - error getting service pack information. Exception: " + e);
+ osVersion += ".unknown";
+ }
+
+ // Get processor architecture
+ let arch = "unknown";
+ try {
+ let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo",
+ ctypes.default_abi,
+ ctypes.void_t,
+ SYSTEM_INFO.ptr);
+ let sysInfo = SYSTEM_INFO();
+ // Default to unknown
+ sysInfo.wProcessorArchitecture = 0xffff;
+
+ GetNativeSystemInfo(sysInfo.address());
+ switch(sysInfo.wProcessorArchitecture) {
+ case 9:
+ arch = "x64";
+ break;
+ case 6:
+ arch = "IA64";
+ break;
+ case 0:
+ arch = "x86";
+ break;
+ }
+ } catch (e) {
+ LOG("gOSVersion - error getting processor architecture. Exception: " + e);
+ } finally {
+ osVersion += " (" + arch + ")";
+ }
+ } finally {
+ kernel32.close();
+ }
+ }
+#endif
+
+ try {
+ osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
+ }
+ catch (e) {
+ // Not all platforms have a secondary widget library, so an error is nothing to worry about.
+ }
+ osVersion = encodeURIComponent(osVersion);
+ }
+ return osVersion;
+});
+
+/**
+ * Provides an easy API for downloading and installing GMP Addons
+ */
+function GMPInstallManager() {
+}
+/**
+ * Temp file name used for downloading
+ */
+GMPInstallManager.prototype = {
+ /**
+ * Obtains a URL with replacement of vars
+ */
+ _getURL: function() {
+ let log = getScopedLogger("GMPInstallManager._getURL");
+ // Use the override URL if it is specified. The override URL is just like
+ // the normal URL but it does not check the cert.
+ let url = GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE);
+ if (url) {
+ log.info("Using override url: " + url);
+ } else {
+ url = GMPPrefs.get(GMPPrefs.KEY_URL);
+ log.info("Using url: " + url);
+ }
+
+ url = UpdateUtils.formatUpdateURL(url);
+ log.info("Using url (with replacement): " + url);
+ return url;
+ },
+ /**
+ * Performs an addon check.
+ * @return a promise which will be resolved or rejected.
+ * The promise is resolved with an array of GMPAddons
+ * The promise is rejected with an object with properties:
+ * target: The XHR request object
+ * status: The HTTP status code
+ * type: Sometimes specifies type of rejection
+ */
+ checkForAddons: function() {
+ let log = getScopedLogger("GMPInstallManager.checkForAddons");
+ if (this._deferred) {
+ log.error("checkForAddons already called");
+ return Promise.reject({type: "alreadycalled"});
+ }
+ this._deferred = Promise.defer();
+ let url = this._getURL();
+
+ this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsISupports);
+ // This is here to let unit test code override XHR
+ if (this._request.wrappedJSObject) {
+ this._request = this._request.wrappedJSObject;
+ }
+ this._request.open("GET", url, true);
+ let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true);
+ this._request.channel.notificationCallbacks =
+ new gCertUtils.BadCertHandler(allowNonBuiltIn);
+ // Prevent the request from reading from the cache.
+ this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to the cache.
+ this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+
+ this._request.overrideMimeType("text/xml");
+ // The Cache-Control header is only interpreted by proxies and the
+ // final destination. It does not help if a resource is already
+ // cached locally.
+ this._request.setRequestHeader("Cache-Control", "no-cache");
+ // HTTP/1.0 servers might not implement Cache-Control and
+ // might only implement Pragma: no-cache
+ this._request.setRequestHeader("Pragma", "no-cache");
+
+ this._request.timeout = CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS;
+ this._request.addEventListener("error", event => this.onFailXML("onErrorXML", event), false);
+ this._request.addEventListener("abort", event => this.onFailXML("onAbortXML", event), false);
+ this._request.addEventListener("timeout", event => this.onFailXML("onTimeoutXML", event), false);
+ this._request.addEventListener("load", event => this.onLoadXML(event), false);
+
+ log.info("sending request to: " + url);
+ this._request.send(null);
+
+ return this._deferred.promise;
+ },
+ /**
+ * Installs the specified addon and calls a callback when done.
+ * @param gmpAddon The GMPAddon object to install
+ * @return a promise which will be resolved or rejected
+ * The promise will resolve with an array of paths that were extracted
+ * The promise will reject with an error object:
+ * target: The XHR request object
+ * status: The HTTP status code
+ * type: A string to represent the type of error
+ * downloaderr, verifyerr or previouserrorencountered
+ */
+ installAddon: function(gmpAddon) {
+ if (this._deferred) {
+ log.error("previous error encountered");
+ return Promise.reject({type: "previouserrorencountered"});
+ }
+ this.gmpDownloader = new GMPDownloader(gmpAddon);
+ return this.gmpDownloader.start();
+ },
+ _getTimeSinceLastCheck: function() {
+ let now = Math.round(Date.now() / 1000);
+ // Default to 0 here because `now - 0` will be returned later if that case
+ // is hit. We want a large value so a check will occur.
+ let lastCheck = GMPPrefs.get(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0);
+ // Handle clock jumps, return now since we want it to represent
+ // a lot of time has passed since the last check.
+ if (now < lastCheck) {
+ return now;
+ }
+ return now - lastCheck;
+ },
+ get _isEMEEnabled() {
+ return GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true);
+ },
+ _isAddonUpdateEnabled: function(aAddon) {
+ return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon) &&
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon);
+ },
+ _updateLastCheck: function() {
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.set(GMPPrefs.KEY_UPDATE_LAST_CHECK, now);
+ },
+ _versionchangeOccurred: function() {
+ let savedBuildID = GMPPrefs.get(GMPPrefs.KEY_BUILDID, null);
+ let buildID = Services.appinfo.platformBuildID;
+ if (savedBuildID == buildID) {
+ return false;
+ }
+ GMPPrefs.set(GMPPrefs.KEY_BUILDID, buildID);
+ return true;
+ },
+ /**
+ * Wrapper for checkForAddons and installAddon.
+ * Will only install if not already installed and will log the results.
+ * This will only install/update the OpenH264 and EME plugins
+ * @return a promise which will be resolved if all addons could be installed
+ * successfully, rejected otherwise.
+ */
+ simpleCheckAndInstall: Task.async(function*() {
+ let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall");
+
+ if (this._versionchangeOccurred()) {
+ log.info("A version change occurred. Ignoring " +
+ "media.gmp-manager.lastCheck to check immediately for " +
+ "new or updated GMPs.");
+ } else {
+ let secondsBetweenChecks =
+ GMPPrefs.get(GMPPrefs.KEY_SECONDS_BETWEEN_CHECKS,
+ DEFAULT_SECONDS_BETWEEN_CHECKS)
+ let secondsSinceLast = this._getTimeSinceLastCheck();
+ log.info("Last check was: " + secondsSinceLast +
+ " seconds ago, minimum seconds: " + secondsBetweenChecks);
+ if (secondsBetweenChecks > secondsSinceLast) {
+ log.info("Will not check for updates.");
+ return {status: "too-frequent-no-check"};
+ }
+ }
+
+ try {
+ let gmpAddons = yield this.checkForAddons();
+ this._updateLastCheck();
+ log.info("Found " + gmpAddons.length + " addons advertised.");
+ let addonsToInstall = gmpAddons.filter(function(gmpAddon) {
+ log.info("Found addon: " + gmpAddon.toString());
+
+ if (!gmpAddon.isValid || GMPUtils.isPluginHidden(gmpAddon) ||
+ gmpAddon.isInstalled) {
+ log.info("Addon invalid, hidden or already installed.");
+ return false;
+ }
+
+ let addonUpdateEnabled = false;
+ if (GMP_PLUGIN_IDS.indexOf(gmpAddon.id) >= 0) {
+ addonUpdateEnabled = this._isAddonUpdateEnabled(gmpAddon.id);
+ if (!addonUpdateEnabled) {
+ log.info("Auto-update is off for " + gmpAddon.id +
+ ", skipping check.");
+ }
+ } else {
+ // Currently, we only support installs of OpenH264 and EME plugins.
+ log.info("Auto-update is off for unknown plugin '" + gmpAddon.id +
+ "', skipping check.");
+ }
+
+ return addonUpdateEnabled;
+ }, this);
+
+ if (!addonsToInstall.length) {
+ log.info("No new addons to install, returning");
+ return {status: "nothing-new-to-install"};
+ }
+
+ let installResults = [];
+ let failureEncountered = false;
+ for (let addon of addonsToInstall) {
+ try {
+ yield this.installAddon(addon);
+ installResults.push({
+ id: addon.id,
+ result: "succeeded",
+ });
+ } catch (e) {
+ failureEncountered = true;
+ installResults.push({
+ id: addon.id,
+ result: "failed",
+ });
+ }
+ }
+ if (failureEncountered) {
+ throw {status: "failed",
+ results: installResults};
+ }
+ return {status: "succeeded",
+ results: installResults};
+ } catch(e) {
+ log.error("Could not check for addons", e);
+ throw e;
+ }
+ }),
+
+ /**
+ * Makes sure everything is cleaned up
+ */
+ uninit: function() {
+ let log = getScopedLogger("GMPInstallManager.uninit");
+ if (this._request) {
+ log.info("Aborting request");
+ this._request.abort();
+ }
+ if (this._deferred) {
+ log.info("Rejecting deferred");
+ this._deferred.reject({type: "uninitialized"});
+ }
+ log.info("Done cleanup");
+ },
+
+ /**
+ * If set to true, specifies to leave the temporary downloaded zip file.
+ * This is useful for tests.
+ */
+ overrideLeaveDownloadedZip: false,
+
+ /**
+ * The XMLHttpRequest succeeded and the document was loaded.
+ * @param event The nsIDOMEvent for the load
+ */
+ onLoadXML: function(event) {
+ let log = getScopedLogger("GMPInstallManager.onLoadXML");
+ try {
+ log.info("request completed downloading document");
+ let certs = null;
+ if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
+ GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) {
+ certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH);
+ }
+
+ let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN,
+ true);
+ log.info("allowNonBuiltIn: " + allowNonBuiltIn);
+
+ gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs);
+
+ this.parseResponseXML();
+ } catch (ex) {
+ log.error("could not load xml: " + ex);
+ this._deferred.reject({
+ target: event.target,
+ status: this._getChannelStatus(event.target),
+ message: "" + ex,
+ });
+ delete this._deferred;
+ }
+ },
+
+ /**
+ * Returns the status code for the XMLHttpRequest
+ */
+ _getChannelStatus: function(request) {
+ let log = getScopedLogger("GMPInstallManager._getChannelStatus");
+ let status = null;
+ try {
+ status = request.status;
+ log.info("request.status is: " + request.status);
+ }
+ catch (e) {
+ }
+
+ if (status == null) {
+ status = request.channel.QueryInterface(Ci.nsIRequest).status;
+ }
+ return status;
+ },
+
+ /**
+ * There was an error of some kind during the XMLHttpRequest. This
+ * error may have been caused by external factors (e.g. network
+ * issues) or internally (by a timeout).
+ *
+ * @param event The nsIDOMEvent for the error
+ */
+ onFailXML: function(failure, event) {
+ let log = getScopedLogger("GMPInstallManager.onFailXML " + failure);
+ let request = event.target;
+ let status = this._getChannelStatus(request);
+ let message = "request.status: " + status + " (" + event.type + ")";
+ log.warn(message);
+ this._deferred.reject({
+ target: request,
+ status: status,
+ message: message
+ });
+ delete this._deferred;
+ },
+
+ /**
+ * Returns an array of GMPAddon objects discovered by the update check.
+ * Or returns an empty array if there were any problems with parsing.
+ * If there's an error, it will be logged if logging is enabled.
+ */
+ parseResponseXML: function() {
+ try {
+ let log = getScopedLogger("GMPInstallManager.parseResponseXML");
+ let updatesElement = this._request.responseXML.documentElement;
+ if (!updatesElement) {
+ let message = "empty updates document";
+ log.warn(message);
+ this._deferred.reject({
+ target: this._request,
+ message: message
+ });
+ delete this._deferred;
+ return;
+ }
+
+ if (updatesElement.nodeName != "updates") {
+ let message = "got node name: " + updatesElement.nodeName +
+ ", expected: updates";
+ log.warn(message);
+ this._deferred.reject({
+ target: this._request,
+ message: message
+ });
+ delete this._deferred;
+ return;
+ }
+
+ const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE;
+ let gmpResults = [];
+ for (let i = 0; i < updatesElement.childNodes.length; ++i) {
+ let updatesChildElement = updatesElement.childNodes.item(i);
+ if (updatesChildElement.nodeType != ELEMENT_NODE) {
+ continue;
+ }
+ if (updatesChildElement.localName == "addons") {
+ gmpResults = GMPAddon.parseGMPAddonsNode(updatesChildElement);
+ }
+ }
+ this._deferred.resolve(gmpResults);
+ delete this._deferred;
+ } catch (e) {
+ this._deferred.reject({
+ target: this._request,
+ message: e
+ });
+ delete this._deferred;
+ }
+ },
+};
+
+/**
+ * Used to construct a single GMP addon
+ * GMPAddon objects are returns from GMPInstallManager.checkForAddons
+ * GMPAddon objects can also be used in calls to GMPInstallManager.installAddon
+ *
+ * @param gmpAddon The AUS response XML's DOM element `addon`
+ */
+function GMPAddon(gmpAddon) {
+ let log = getScopedLogger("GMPAddon.constructor");
+ gmpAddon.QueryInterface(Ci.nsIDOMElement);
+ ["id", "URL", "hashFunction",
+ "hashValue", "version", "size"].forEach(name => {
+ if (gmpAddon.hasAttribute(name)) {
+ this[name] = gmpAddon.getAttribute(name);
+ }
+ });
+ this.size = Number(this.size) || undefined;
+ log.info ("Created new addon: " + this.toString());
+}
+/**
+ * Parses an XML GMP addons node from AUS into an array
+ * @param addonsElement An nsIDOMElement compatible node with XML from AUS
+ * @return An array of GMPAddon results
+ */
+GMPAddon.parseGMPAddonsNode = function(addonsElement) {
+ let log = getScopedLogger("GMPAddon.parseGMPAddonsNode");
+ let gmpResults = [];
+ if (addonsElement.localName !== "addons") {
+ return;
+ }
+
+ addonsElement.QueryInterface(Ci.nsIDOMElement);
+ let addonCount = addonsElement.childNodes.length;
+ for (let i = 0; i < addonCount; ++i) {
+ let addonElement = addonsElement.childNodes.item(i);
+ if (addonElement.localName !== "addon") {
+ continue;
+ }
+ addonElement.QueryInterface(Ci.nsIDOMElement);
+ try {
+ gmpResults.push(new GMPAddon(addonElement));
+ } catch (e) {
+ log.warn("invalid addon: " + e);
+ continue;
+ }
+ }
+ return gmpResults;
+};
+GMPAddon.prototype = {
+ /**
+ * Returns a string representation of the addon
+ */
+ toString: function() {
+ return this.id + " (" +
+ "isValid: " + this.isValid +
+ ", isInstalled: " + this.isInstalled +
+ ", hashFunction: " + this.hashFunction+
+ ", hashValue: " + this.hashValue +
+ (this.size !== undefined ? ", size: " + this.size : "" ) +
+ ")";
+ },
+ /**
+ * If all the fields aren't specified don't consider this addon valid
+ * @return true if the addon is parsed and valid
+ */
+ get isValid() {
+ return this.id && this.URL && this.version &&
+ this.hashFunction && !!this.hashValue;
+ },
+ get isInstalled() {
+ return this.version &&
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) === this.version;
+ },
+ get isEME() {
+ return this.id == "gmp-widevinecdm" || this.id.indexOf("gmp-eme-") == 0;
+ },
+};
+/**
+ * Constructs a GMPExtractor object which is used to extract a GMP zip
+ * into the specified location. (Which typically leties per platform)
+ * @param zipPath The path on disk of the zip file to extract
+ */
+function GMPExtractor(zipPath, installToDirPath) {
+ this.zipPath = zipPath;
+ this.installToDirPath = installToDirPath;
+}
+GMPExtractor.prototype = {
+ /**
+ * Obtains a list of all the entries in a zipfile in the format of *.*.
+ * This also includes files inside directories.
+ *
+ * @param zipReader the nsIZipReader to check
+ * @return An array of string name entries which can be used
+ * in nsIZipReader.extract
+ */
+ _getZipEntries: function(zipReader) {
+ let entries = [];
+ let enumerator = zipReader.findEntries("*.*");
+ while (enumerator.hasMore()) {
+ entries.push(enumerator.getNext());
+ }
+ return entries;
+ },
+ /**
+ * Installs the this.zipPath contents into the directory used to store GMP
+ * addons for the current platform.
+ *
+ * @return a promise which will be resolved or rejected
+ * See GMPInstallManager.installAddon for resolve/rejected info
+ */
+ install: function() {
+ try {
+ let log = getScopedLogger("GMPExtractor.install");
+ this._deferred = Promise.defer();
+ log.info("Installing " + this.zipPath + "...");
+ // Get the input zip file
+ let zipFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+ zipFile.initWithPath(this.zipPath);
+
+ // Initialize a zipReader and obtain the entries
+ var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ zipReader.open(zipFile)
+ let entries = this._getZipEntries(zipReader);
+ let extractedPaths = [];
+
+ // Extract each of the entries
+ entries.forEach(entry => {
+ // We don't need these types of files
+ if (entry.includes("__MACOSX")) {
+ return;
+ }
+ let outFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ outFile.initWithPath(this.installToDirPath);
+ outFile.appendRelativePath(entry);
+
+ // Make sure the directory hierarchy exists
+ if(!outFile.parent.exists()) {
+ outFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ }
+ zipReader.extract(entry, outFile);
+ extractedPaths.push(outFile.path);
+ log.info(entry + " was successfully extracted to: " +
+ outFile.path);
+ });
+ zipReader.close();
+ if (!GMPInstallManager.overrideLeaveDownloadedZip) {
+ zipFile.remove(false);
+ }
+
+ log.info(this.zipPath + " was installed successfully");
+ this._deferred.resolve(extractedPaths);
+ } catch (e) {
+ if (zipReader) {
+ zipReader.close();
+ }
+ this._deferred.reject({
+ target: this,
+ status: e,
+ type: "exception"
+ });
+ }
+ return this._deferred.promise;
+ }
+};
+
+
+/**
+ * Constructs an object which downloads and initiates an install of
+ * the specified GMPAddon object.
+ * @param gmpAddon The addon to install.
+ */
+function GMPDownloader(gmpAddon)
+{
+ this._gmpAddon = gmpAddon;
+}
+/**
+ * Computes the file hash of fileToHash with the specified hash function
+ * @param hashFunctionName A hash function name such as sha512
+ * @param fileToHash An nsIFile to hash
+ * @return a promise which resolve to a digest in binary hex format
+ */
+GMPDownloader.computeHash = function(hashFunctionName, fileToHash) {
+ let log = getScopedLogger("GMPDownloader.computeHash");
+ let digest;
+ let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fileStream.init(fileToHash, FileUtils.MODE_RDONLY,
+ FileUtils.PERMS_FILE, 0);
+ try {
+ let hash = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ let hashFunction =
+ Ci.nsICryptoHash[hashFunctionName.toUpperCase()];
+ if (!hashFunction) {
+ log.error("could not get hash function");
+ return Promise.reject();
+ }
+ hash.init(hashFunction);
+ hash.updateFromStream(fileStream, -1);
+ digest = binaryToHex(hash.finish(false));
+ } catch (e) {
+ log.warn("failed to compute hash: " + e);
+ digest = "";
+ }
+ fileStream.close();
+ return Promise.resolve(digest);
+},
+GMPDownloader.prototype = {
+ /**
+ * Starts the download process for an addon.
+ * @return a promise which will be resolved or rejected
+ * See GMPInstallManager.installAddon for resolve/rejected info
+ */
+ start: function() {
+ let log = getScopedLogger("GMPDownloader.start");
+ this._deferred = Promise.defer();
+ if (!this._gmpAddon.isValid) {
+ log.info("gmpAddon is not valid, will not continue");
+ return Promise.reject({
+ target: this,
+ status: status,
+ type: "downloaderr"
+ });
+ }
+
+ let uri = Services.io.newURI(this._gmpAddon.URL, null, null);
+ this._request = Cc["@mozilla.org/network/incremental-download;1"].
+ createInstance(Ci.nsIIncrementalDownload);
+ let gmpFile = FileUtils.getFile("TmpD", [this._gmpAddon.id + ".zip"]);
+ if (gmpFile.exists()) {
+ gmpFile.remove(false);
+ }
+
+ log.info("downloading from " + uri.spec + " to " + gmpFile.path);
+ this._request.init(uri, gmpFile, DOWNLOAD_CHUNK_BYTES_SIZE,
+ DOWNLOAD_INTERVAL);
+ this._request.start(this, null);
+ return this._deferred.promise;
+ },
+ // For nsIRequestObserver
+ onStartRequest: function(request, context) {
+ },
+ // For nsIRequestObserver
+ // Called when the GMP addon zip file is downloaded
+ onStopRequest: function(request, context, status) {
+ let log = getScopedLogger("GMPDownloader.onStopRequest");
+ log.info("onStopRequest called");
+ if (!Components.isSuccessCode(status)) {
+ log.info("status failed: " + status);
+ this._deferred.reject({
+ target: this,
+ status: status,
+ type: "downloaderr"
+ });
+ return;
+ }
+
+ let promise = this._verifyDownload();
+ promise.then(() => {
+ log.info("GMP file is ready to unzip");
+ let destination = this._request.destination;
+
+ let zipPath = destination.path;
+ let gmpAddon = this._gmpAddon;
+ let installToDirPath = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+ let path = OS.Path.join(OS.Constants.Path.profileDir,
+ gmpAddon.id,
+ gmpAddon.version);
+ installToDirPath.initWithPath(path);
+ log.info("install to directory path: " + installToDirPath.path);
+ let gmpInstaller = new GMPExtractor(zipPath, installToDirPath.path);
+ let installPromise = gmpInstaller.install();
+ installPromise.then(extractedPaths => {
+ // Success, set the prefs
+ let now = Math.round(Date.now() / 1000);
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id);
+ // Setting the version pref signals installation completion to consumers,
+ // if you need to set other prefs etc. do it before this.
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version,
+ gmpAddon.id);
+ this._deferred.resolve(extractedPaths);
+ }, err => {
+ this._deferred.reject(err);
+ });
+ }, err => {
+ log.warn("verifyDownload check failed");
+ this._deferred.reject({
+ target: this,
+ status: 200,
+ type: "verifyerr"
+ });
+ });
+ },
+ /**
+ * Verifies that the downloaded zip file's hash matches the GMPAddon hash.
+ * @return a promise which resolves if the download verifies
+ */
+ _verifyDownload: function() {
+ let verifyDownloadDeferred = Promise.defer();
+ let log = getScopedLogger("GMPDownloader._verifyDownload");
+ log.info("_verifyDownload called");
+ if (!this._request) {
+ return Promise.reject();
+ }
+
+ let destination = this._request.destination;
+ log.info("for path: " + destination.path);
+
+ // Ensure that the file size matches the expected file size.
+ if (this._gmpAddon.size !== undefined &&
+ destination.fileSize != this._gmpAddon.size) {
+ log.warn("Downloader:_verifyDownload downloaded size " +
+ destination.fileSize + " != expected size " +
+ this._gmpAddon.size + ".");
+ return Promise.reject();
+ }
+
+ let promise = GMPDownloader.computeHash(this._gmpAddon.hashFunction, destination);
+ promise.then(digest => {
+ let expectedDigest = this._gmpAddon.hashValue.toLowerCase();
+ if (digest !== expectedDigest) {
+ log.warn("hashes do not match! Got: `" +
+ digest + "`, expected: `" + expectedDigest + "`");
+ this._deferred.reject();
+ return;
+ }
+
+ log.info("hashes match!");
+ verifyDownloadDeferred.resolve();
+ }, err => {
+ verifyDownloadDeferred.reject();
+ });
+ return verifyDownloadDeferred.promise;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver])
+};
+
+/**
+ * Convert a string containing binary values to hex.
+ */
+function binaryToHex(input) {
+ let result = "";
+ for (let i = 0; i < input.length; ++i) {
+ let hex = input.charCodeAt(i).toString(16);
+ if (hex.length == 1)
+ hex = "0" + hex;
+ result += hex;
+ }
+ return result;
+}
diff --git a/components/addons/src/GMPProvider.jsm b/components/addons/src/GMPProvider.jsm
new file mode 100644
index 000000000..c89427101
--- /dev/null
+++ b/components/addons/src/GMPProvider.jsm
@@ -0,0 +1,605 @@
+/* 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";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/GMPUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(
+ this, "GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(
+ this, "setTimeout", "resource://gre/modules/Timer.jsm");
+
+const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
+const STRING_TYPE_NAME = "type.%ID%.name";
+
+const SEC_IN_A_DAY = 24 * 60 * 60;
+// How long to wait after a user enabled EME before attempting to download CDMs.
+const GMP_CHECK_DELAY = 10 * 1000; // milliseconds
+
+const NS_GRE_DIR = "GreD";
+const CLEARKEY_PLUGIN_ID = "gmp-clearkey";
+const CLEARKEY_VERSION = "0.1";
+
+const GMP_LICENSE_INFO = "gmp_license_info";
+
+const GMP_PLUGINS = [
+ {
+ id: OPEN_H264_ID,
+ name: "openH264_name",
+ description: "openH264_description2",
+ // The following licenseURL is part of an awful hack to include the OpenH264
+ // license without having bug 624602 fixed yet, and intentionally ignores
+ // localisation.
+ licenseURL: "chrome://mozapps/content/extensions/OpenH264-license.txt",
+ homepageURL: "http://www.openh264.org/",
+ optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul"
+ },
+ {
+ id: WIDEVINE_ID,
+ name: "widevine_name",
+ // Describe the purpose of both CDMs in the same way.
+ description: "widevine_description2",
+ 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);
+
+XPCOMUtils.defineLazyGetter(this, "pluginsBundle",
+ () => Services.strings.createBundle("chrome://global/locale/plugins.properties"));
+XPCOMUtils.defineLazyGetter(this, "gmpService",
+ () => Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(Ci.mozIGeckoMediaPluginChromeService));
+
+var messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+
+var gLogger;
+var gLogAppenderDump = null;
+
+function configureLogging() {
+ if (!gLogger) {
+ gLogger = Log.repository.getLogger("Toolkit.GMP");
+ gLogger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
+ }
+ gLogger.level = GMPPrefs.get(GMPPrefs.KEY_LOGGING_LEVEL, Log.Level.Warn);
+
+ let logDumping = GMPPrefs.get(GMPPrefs.KEY_LOGGING_DUMP, false);
+ if (logDumping != !!gLogAppenderDump) {
+ if (logDumping) {
+ gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter());
+ gLogger.addAppender(gLogAppenderDump);
+ } else {
+ gLogger.removeAppender(gLogAppenderDump);
+ gLogAppenderDump = null;
+ }
+ }
+}
+
+
+
+/**
+ * The GMPWrapper provides the info for the various GMP plugins to public
+ * callers through the API.
+ */
+function GMPWrapper(aPluginInfo) {
+ this._plugin = aPluginInfo;
+ this._log =
+ Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP",
+ "GMPWrapper(" +
+ this._plugin.id + ") ");
+ Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED,
+ this._plugin.id),
+ this.onPrefEnabledChanged, this);
+ Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION,
+ this._plugin.id),
+ this.onPrefVersionChanged, this);
+ if (this._plugin.isEME) {
+ Preferences.observe(GMPPrefs.KEY_EME_ENABLED,
+ this.onPrefEMEGlobalEnabledChanged, this);
+ messageManager.addMessageListener("EMEVideo:ContentMediaKeysRequest", this);
+ }
+}
+
+GMPWrapper.prototype = {
+ // An active task that checks for plugin updates and installs them.
+ _updateTask: null,
+ _gmpPath: null,
+ _isUpdateCheckPending: false,
+
+ optionsType: AddonManager.OPTIONS_TYPE_INLINE,
+ get optionsURL() { return this._plugin.optionsURL; },
+
+ set gmpPath(aPath) { this._gmpPath = aPath; },
+ get gmpPath() {
+ if (!this._gmpPath && this.isInstalled) {
+ this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir,
+ this._plugin.id,
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION,
+ null, this._plugin.id));
+ }
+ return this._gmpPath;
+ },
+
+ get id() { return this._plugin.id; },
+ get type() { return "plugin"; },
+ get isGMPlugin() { return true; },
+ get name() { return this._plugin.name; },
+ get creator() { return null; },
+ get homepageURL() { return this._plugin.homepageURL; },
+
+ get description() { return this._plugin.description; },
+ get fullDescription() { return this._plugin.fullDescription; },
+
+ get version() { return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, null,
+ this._plugin.id); },
+
+ get isActive() { return !this.appDisabled && !this.userDisabled; },
+ get appDisabled() {
+ if (this._plugin.isEME && !GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) {
+ // If "media.eme.enabled" is false, all EME plugins are disabled.
+ return true;
+ }
+ return false;
+ },
+
+ get userDisabled() {
+ return !GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, this._plugin.id);
+ },
+ set userDisabled(aVal) { GMPPrefs.set(GMPPrefs.KEY_PLUGIN_ENABLED,
+ aVal === false,
+ this._plugin.id); },
+
+ get blocklistState() { return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; },
+ get size() { return 0; },
+ get scope() { return AddonManager.SCOPE_APPLICATION; },
+ get pendingOperations() { return AddonManager.PENDING_NONE; },
+
+ get operationsRequiringRestart() { return AddonManager.OP_NEEDS_RESTART_NONE },
+
+ get permissions() {
+ let permissions = 0;
+ if (!this.appDisabled) {
+ permissions |= AddonManager.PERM_CAN_UPGRADE;
+ permissions |= this.userDisabled ? AddonManager.PERM_CAN_ENABLE :
+ AddonManager.PERM_CAN_DISABLE;
+ }
+ return permissions;
+ },
+
+ get updateDate() {
+ let time = Number(GMPPrefs.get(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, null,
+ this._plugin.id));
+ if (time !== NaN && this.isInstalled) {
+ return new Date(time * 1000)
+ }
+ return null;
+ },
+
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ get foreignInstall() {
+ return false;
+ },
+
+ isCompatibleWith: function(aAppVersion, aPlatformVersion) {
+ return true;
+ },
+
+ get applyBackgroundUpdates() {
+ if (!GMPPrefs.isSet(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id)) {
+ return AddonManager.AUTOUPDATE_DEFAULT;
+ }
+
+ return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id) ?
+ AddonManager.AUTOUPDATE_ENABLE : AddonManager.AUTOUPDATE_DISABLE;
+ },
+
+ set applyBackgroundUpdates(aVal) {
+ if (aVal == AddonManager.AUTOUPDATE_DEFAULT) {
+ GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id);
+ } else if (aVal == AddonManager.AUTOUPDATE_ENABLE) {
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id);
+ } else if (aVal == AddonManager.AUTOUPDATE_DISABLE) {
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, this._plugin.id);
+ }
+ },
+
+ findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
+ this._log.trace("findUpdates() - " + this._plugin.id + " - reason=" +
+ aReason);
+
+ AddonManagerPrivate.callNoUpdateListeners(this, aListener);
+
+ if (aReason === AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
+ if (!AddonManager.shouldAutoUpdate(this)) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - no autoupdate");
+ return Promise.resolve(false);
+ }
+
+ let secSinceLastCheck =
+ Date.now() / 1000 - Preferences.get(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0);
+ if (secSinceLastCheck <= SEC_IN_A_DAY) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - last check was less then a day ago");
+ return Promise.resolve(false);
+ }
+ } else if (aReason !== AddonManager.UPDATE_WHEN_USER_REQUESTED) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - the given reason to update is not supported");
+ return Promise.resolve(false);
+ }
+
+ if (this._updateTask !== null) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - update task already running");
+ return this._updateTask;
+ }
+
+ this._updateTask = Task.spawn(function* GMPProvider_updateTask() {
+ this._log.trace("findUpdates() - updateTask");
+ try {
+ let installManager = new GMPInstallManager();
+ let gmpAddons = yield installManager.checkForAddons();
+ let update = gmpAddons.find(function(aAddon) {
+ return aAddon.id === this._plugin.id;
+ }, this);
+ if (update && update.isValid && !update.isInstalled) {
+ this._log.trace("findUpdates() - found update for " +
+ this._plugin.id + ", installing");
+ yield installManager.installAddon(update);
+ } else {
+ this._log.trace("findUpdates() - no updates for " + this._plugin.id);
+ }
+ this._log.info("findUpdates() - updateTask succeeded for " +
+ this._plugin.id);
+ } catch (e) {
+ this._log.error("findUpdates() - updateTask for " + this._plugin.id +
+ " threw", e);
+ throw e;
+ } finally {
+ this._updateTask = null;
+ return true;
+ }
+ }.bind(this));
+
+ return this._updateTask;
+ },
+
+ get pluginMimeTypes() { return []; },
+ get pluginLibraries() {
+ if (this.isInstalled) {
+ let path = this.version;
+ return [path];
+ }
+ return [];
+ },
+ get pluginFullpath() {
+ if (this.isInstalled) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir,
+ this._plugin.id,
+ this.version);
+ return [path];
+ }
+ return [];
+ },
+
+ get isInstalled() {
+ return this.version && this.version.length > 0;
+ },
+
+ _handleEnabledChanged: function() {
+ AddonManagerPrivate.callAddonListeners(this.isActive ?
+ "onEnabling" : "onDisabling",
+ this, false);
+ if (this._gmpPath) {
+ if (this.isActive) {
+ this._log.info("onPrefEnabledChanged() - adding gmp directory " +
+ this._gmpPath);
+ gmpService.addPluginDirectory(this._gmpPath);
+ } else {
+ this._log.info("onPrefEnabledChanged() - removing gmp directory " +
+ this._gmpPath);
+ gmpService.removePluginDirectory(this._gmpPath);
+ }
+ }
+ AddonManagerPrivate.callAddonListeners(this.isActive ?
+ "onEnabled" : "onDisabled",
+ this);
+ },
+
+ onPrefEMEGlobalEnabledChanged: function() {
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this,
+ ["appDisabled"]);
+ if (this.appDisabled) {
+ this.uninstallPlugin();
+ } else {
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this,
+ null, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", this, false);
+ AddonManagerPrivate.callAddonListeners("onInstalled", this);
+ this.checkForUpdates(GMP_CHECK_DELAY);
+ }
+ if (!this.userDisabled) {
+ this._handleEnabledChanged();
+ }
+ },
+
+ checkForUpdates: function(delay) {
+ if (this._isUpdateCheckPending) {
+ return;
+ }
+ this._isUpdateCheckPending = true;
+ GMPPrefs.reset(GMPPrefs.KEY_UPDATE_LAST_CHECK, null);
+ // Delay this in case the user changes his mind and doesn't want to
+ // enable EME after all.
+ setTimeout(() => {
+ if (!this.appDisabled) {
+ let gmpInstallManager = new GMPInstallManager();
+ // We don't really care about the results, if someone is interested
+ // they can check the log.
+ gmpInstallManager.simpleCheckAndInstall().then(null, () => {});
+ }
+ this._isUpdateCheckPending = false;
+ }, delay);
+ },
+
+ receiveMessage: function({target: browser, data: data}) {
+ this._log.trace("receiveMessage() data=" + data);
+ let parsedData;
+ try {
+ parsedData = JSON.parse(data);
+ } catch(ex) {
+ this._log.error("Malformed EME video message with data: " + data);
+ return;
+ }
+ let {status: status, keySystem: keySystem} = parsedData;
+ if (status == "cdm-not-installed" || status == "cdm-insufficient-version") {
+ this.checkForUpdates(0);
+ }
+ },
+
+ onPrefEnabledChanged: function() {
+ if (!this._plugin.isEME || !this.appDisabled) {
+ this._handleEnabledChanged();
+ }
+ },
+
+ onPrefVersionChanged: function() {
+ AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
+ if (this._gmpPath) {
+ this._log.info("onPrefVersionChanged() - unregistering gmp directory " +
+ this._gmpPath);
+ gmpService.removeAndDeletePluginDirectory(this._gmpPath, true /* can defer */);
+ }
+ AddonManagerPrivate.callAddonListeners("onUninstalled", this);
+
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this,
+ null, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", this, false);
+ this._gmpPath = null;
+ if (this.isInstalled) {
+ this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir,
+ this._plugin.id,
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION,
+ null, this._plugin.id));
+ }
+ if (this._gmpPath && this.isActive) {
+ this._log.info("onPrefVersionChanged() - registering gmp directory " +
+ this._gmpPath);
+ gmpService.addPluginDirectory(this._gmpPath);
+ }
+ AddonManagerPrivate.callAddonListeners("onInstalled", this);
+ },
+
+ uninstallPlugin: function() {
+ AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
+ if (this.gmpPath) {
+ this._log.info("uninstallPlugin() - unregistering gmp directory " +
+ this.gmpPath);
+ gmpService.removeAndDeletePluginDirectory(this.gmpPath);
+ }
+ GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_VERSION, this.id);
+ AddonManagerPrivate.callAddonListeners("onUninstalled", this);
+ },
+
+ shutdown: function() {
+ Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED,
+ this._plugin.id),
+ this.onPrefEnabledChanged, this);
+ Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION,
+ this._plugin.id),
+ this.onPrefVersionChanged, this);
+ if (this._plugin.isEME) {
+ Preferences.ignore(GMPPrefs.KEY_EME_ENABLED,
+ this.onPrefEMEGlobalEnabledChanged, this);
+ messageManager.removeMessageListener("EMEVideo:ContentMediaKeysRequest", this);
+ }
+ return this._updateTask;
+ },
+};
+
+var GMPProvider = {
+ get name() { return "GMPProvider"; },
+
+ _plugins: null,
+
+ startup: function() {
+ configureLogging();
+ this._log = Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP",
+ "GMPProvider.");
+ this.buildPluginList();
+ this.ensureProperCDMInstallState();
+
+ Preferences.observe(GMPPrefs.KEY_LOG_BASE, configureLogging);
+
+ for (let [id, plugin] of this._plugins) {
+ let wrapper = plugin.wrapper;
+ let gmpPath = wrapper.gmpPath;
+ let isEnabled = wrapper.isActive;
+ this._log.trace("startup - enabled=" + isEnabled + ", gmpPath=" +
+ gmpPath);
+
+ if (gmpPath && isEnabled) {
+ this._log.info("startup - adding gmp directory " + gmpPath);
+ try {
+ gmpService.addPluginDirectory(gmpPath);
+ } catch (e if e.name == 'NS_ERROR_NOT_AVAILABLE') {
+ this._log.warn("startup - adding gmp directory failed with " +
+ e.name + " - sandboxing not available?", e);
+ }
+ }
+ }
+
+ if (Preferences.get(GMPPrefs.KEY_EME_ENABLED, false)) {
+ try {
+ let greDir = Services.dirsvc.get(NS_GRE_DIR,
+ Ci.nsILocalFile);
+ let clearkeyPath = OS.Path.join(greDir.path,
+ CLEARKEY_PLUGIN_ID,
+ CLEARKEY_VERSION);
+ this._log.info("startup - adding clearkey CDM directory " +
+ clearkeyPath);
+ gmpService.addPluginDirectory(clearkeyPath);
+ } catch (e) {
+ this._log.warn("startup - adding clearkey CDM failed", e);
+ }
+ }
+ },
+
+ shutdown: function() {
+ this._log.trace("shutdown");
+ Preferences.ignore(GMPPrefs.KEY_LOG_BASE, configureLogging);
+
+ let shutdownTask = Task.spawn(function* GMPProvider_shutdownTask() {
+ this._log.trace("shutdown - shutdownTask");
+ let shutdownSucceeded = true;
+
+ for (let plugin of this._plugins.values()) {
+ try {
+ yield plugin.wrapper.shutdown();
+ } catch (e) {
+ shutdownSucceeded = false;
+ }
+ }
+
+ this._plugins = null;
+
+ if (!shutdownSucceeded) {
+ throw new Error("Shutdown failed");
+ }
+ }.bind(this));
+
+ return shutdownTask;
+ },
+
+ getAddonByID: function(aId, aCallback) {
+ if (!this.isEnabled) {
+ aCallback(null);
+ return;
+ }
+
+ let plugin = this._plugins.get(aId);
+ if (plugin && !GMPUtils.isPluginHidden(plugin)) {
+ aCallback(plugin.wrapper);
+ } else {
+ aCallback(null);
+ }
+ },
+
+ getAddonsByTypes: function(aTypes, aCallback) {
+ if (!this.isEnabled ||
+ (aTypes && aTypes.indexOf("plugin") < 0)) {
+ aCallback([]);
+ return;
+ }
+
+ // Tycho:
+ // let results = [p.wrapper for ([id, p] of this._plugins)
+ // if (!GMPUtils.isPluginHidden(p))];
+ let results = [];
+ for (let [id, p] of this._plugins) {
+ if (!GMPUtils.isPluginHidden(p)) {
+ results.push(p.wrapper);
+ }
+ }
+
+ aCallback(results);
+ },
+
+ get isEnabled() {
+ return GMPPrefs.get(GMPPrefs.KEY_PROVIDER_ENABLED, false);
+ },
+
+ generateFullDescription: function(aLicenseURL, aLicenseInfo) {
+ return "<xhtml:a href=\"" + aLicenseURL + "\" target=\"_blank\">" +
+ aLicenseInfo + "</xhtml:a>."
+ },
+
+ buildPluginList: function() {
+ let licenseInfo = pluginsBundle.GetStringFromName(GMP_LICENSE_INFO);
+
+ this._plugins = new Map();
+ for (let aPlugin of GMP_PLUGINS) {
+ let plugin = {
+ id: aPlugin.id,
+ name: pluginsBundle.GetStringFromName(aPlugin.name),
+ description: pluginsBundle.GetStringFromName(aPlugin.description),
+ homepageURL: aPlugin.homepageURL,
+ optionsURL: aPlugin.optionsURL,
+ wrapper: null,
+ isEME: aPlugin.isEME,
+ };
+ if (aPlugin.licenseURL) {
+ plugin.fullDescription =
+ this.generateFullDescription(aPlugin.licenseURL, licenseInfo);
+ }
+ plugin.wrapper = new GMPWrapper(plugin);
+ this._plugins.set(plugin.id, plugin);
+ }
+ },
+
+ ensureProperCDMInstallState: function() {
+ if (!GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) {
+ for (let [id, plugin] of this._plugins) {
+ if (plugin.isEME && plugin.wrapper.isInstalled) {
+ gmpService.addPluginDirectory(plugin.wrapper.gmpPath);
+ plugin.wrapper.uninstallPlugin();
+ }
+ }
+ }
+ },
+};
+
+AddonManagerPrivate.registerProvider(GMPProvider, [
+ new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 6000,
+ AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE)
+]);
diff --git a/components/addons/src/GMPUtils.jsm b/components/addons/src/GMPUtils.jsm
new file mode 100644
index 000000000..593fc3c8d
--- /dev/null
+++ b/components/addons/src/GMPUtils.jsm
@@ -0,0 +1,187 @@
+/* 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 = [ "GMP_PLUGIN_IDS",
+ "GMPPrefs",
+ "GMPUtils",
+ "OPEN_H264_ID",
+ "WIDEVINE_ID" ];
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// GMP IDs
+const OPEN_H264_ID = "gmp-gmpopenh264";
+const WIDEVINE_ID = "gmp-widevinecdm";
+const GMP_PLUGIN_IDS = [ OPEN_H264_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 (!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 == WIDEVINE_ID) {
+
+#if defined(XP_WIN) || defined(XP_LINUX)
+ // The Widevine plugin is available for Windows versions Vista and later,
+ // Mac OSX, and Linux.
+ return true;
+#else
+ return false;
+#endif
+ }
+
+ return true;
+ },
+
+ /**
+ * 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) {
+ return Services.prefs.getDefaultBranch(null).getCharPref(aKey, "default");
+ }
+ 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/components/addons/src/ProductAddonChecker.jsm b/components/addons/src/ProductAddonChecker.jsm
new file mode 100644
index 000000000..c6324da0a
--- /dev/null
+++ b/components/addons/src/ProductAddonChecker.jsm
@@ -0,0 +1,464 @@
+/* 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, utils: Cu } = Components;
+
+const LOCAL_EME_SOURCES = [{
+ "id": "gmp-gmpopenh264",
+ "src": "chrome://global/content/gmp-sources/openh264.json"
+}, {
+ "id": "gmp-widevinecdm",
+ "src": "chrome://global/content/gmp-sources/widevinecdm.json"
+}];
+
+this.EXPORTED_SYMBOLS = [ "ProductAddonChecker" ];
+
+Cu.importGlobalProperties(["XMLHttpRequest"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/CertUtils.jsm");
+/* globals checkCert, BadCertHandler*/
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+/* globals GMPPrefs */
+XPCOMUtils.defineLazyModuleGetter(this, "GMPPrefs",
+ "resource://gre/modules/GMPUtils.jsm");
+
+/* globals OS */
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
+ "resource://gre/modules/ServiceRequest.jsm");
+
+// This exists so that tests can override the XHR behaviour for downloading
+// the addon update XML file.
+var CreateXHR = function() {
+ return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsISupports);
+}
+
+var logger = Log.repository.getLogger("addons.productaddons");
+
+/**
+ * Number of milliseconds after which we need to cancel `downloadXML`.
+ *
+ * Bug 1087674 suggests that the XHR we use in `downloadXML` 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 TIMEOUT_DELAY_MS = 20000;
+// Chunk size for the incremental downloader
+const DOWNLOAD_CHUNK_BYTES_SIZE = 300000;
+// Incremental downloader interval
+const DOWNLOAD_INTERVAL = 0;
+// How much of a file to read into memory at a time for hashing
+const HASH_CHUNK_SIZE = 8192;
+
+/**
+ * Gets the status of an XMLHttpRequest either directly or from its underlying
+ * channel.
+ *
+ * @param request
+ * The XMLHttpRequest.
+ * @return an integer status value.
+ */
+function getRequestStatus(request) {
+ let status = null;
+ try {
+ status = request.status;
+ }
+ catch (e) {
+ }
+
+ if (status != null) {
+ return status;
+ }
+
+ return request.channel.QueryInterface(Ci.nsIRequest).status;
+}
+
+/**
+ * Downloads an XML document from a URL optionally testing the SSL certificate
+ * for certain attributes.
+ *
+ * @param url
+ * The url to download from.
+ * @param allowNonBuiltIn
+ * Whether to trust SSL certificates without a built-in CA issuer.
+ * @param allowedCerts
+ * The list of certificate attributes to match the SSL certificate
+ * against or null to skip checks.
+ * @return a promise that resolves to the DOM document downloaded or rejects
+ * with a JS exception in case of error.
+ */
+function downloadXML(url, allowNonBuiltIn = false, allowedCerts = null) {
+ return new Promise((resolve, reject) => {
+ let request = CreateXHR();
+ // This is here to let unit test code override XHR
+ if (request.wrappedJSObject) {
+ request = request.wrappedJSObject;
+ }
+ request.open("GET", url, true);
+ request.channel.notificationCallbacks = new BadCertHandler(allowNonBuiltIn);
+ // Prevent the request from reading from the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ // Use conservative TLS settings. See bug 1325501.
+ // TODO move to ServiceRequest.
+ if (request.channel instanceof Ci.nsIHttpChannelInternal) {
+ request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
+ }
+ request.timeout = TIMEOUT_DELAY_MS;
+
+ 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.
+ request.setRequestHeader("Cache-Control", "no-cache");
+ // HTTP/1.0 servers might not implement Cache-Control and
+ // might only implement Pragma: no-cache
+ request.setRequestHeader("Pragma", "no-cache");
+
+ let fail = (event) => {
+ let request = event.target;
+ let status = getRequestStatus(request);
+ let message = "Failed downloading XML, status: " + status + ", reason: " + event.type;
+ logger.warn(message);
+ let ex = new Error(message);
+ ex.status = status;
+ reject(ex);
+ };
+
+ let success = (event) => {
+ logger.info("Completed downloading document");
+ let request = event.target;
+
+ try {
+ checkCert(request.channel, allowNonBuiltIn, allowedCerts);
+ } catch (ex) {
+ logger.error("Request failed certificate checks: " + ex);
+ ex.status = getRequestStatus(request);
+ reject(ex);
+ return;
+ }
+
+ resolve(request.responseXML);
+ };
+
+ request.addEventListener("error", fail, false);
+ request.addEventListener("abort", fail, false);
+ request.addEventListener("timeout", fail, false);
+ request.addEventListener("load", success, false);
+
+ logger.info("sending request to: " + url);
+ request.send(null);
+ });
+}
+
+function downloadJSON(uri) {
+ logger.info("fetching config from: " + uri);
+ return new Promise((resolve, reject) => {
+ let xmlHttp = new 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();
+ });
+}
+
+
+/**
+ * Parses a list of add-ons from a DOM document.
+ *
+ * @param document
+ * The DOM document to parse.
+ * @return null if there is no <addons> element otherwise an object containing
+ * an array of the addons listed and a field notifying whether the
+ * fallback was used.
+ */
+function parseXML(document) {
+ // Check that the root element is correct
+ if (document.documentElement.localName != "updates") {
+ throw new Error("got node name: " + document.documentElement.localName +
+ ", expected: updates");
+ }
+
+ // Check if there are any addons elements in the updates element
+ let addons = document.querySelector("updates:root > addons");
+ if (!addons) {
+ return null;
+ }
+
+ let results = [];
+ let addonList = document.querySelectorAll("updates:root > addons > addon");
+ for (let addonElement of addonList) {
+ let addon = {};
+
+ for (let name of ["id", "URL", "hashFunction", "hashValue", "version", "size"]) {
+ if (addonElement.hasAttribute(name)) {
+ addon[name] = addonElement.getAttribute(name);
+ }
+ }
+ addon.size = Number(addon.size) || undefined;
+
+ results.push(addon);
+ }
+
+ return {
+ usedFallback: false,
+ gmpAddons: results
+ };
+}
+
+/**
+ * If downloading from the network fails (AUS server is down),
+ * load the sources from local build configuration.
+ */
+function downloadLocalConfig() {
+
+ if (!GMPPrefs.get(GMPPrefs.KEY_UPDATE_ENABLED, true)) {
+ logger.info("Updates are disabled via media.gmp-manager.updateEnabled");
+ return Promise.resolve({usedFallback: true, gmpAddons: []});
+ }
+
+ return Promise.all(LOCAL_EME_SOURCES.map(conf => {
+ return downloadJSON(conf.src).then(addons => {
+
+ let platforms = addons.vendors[conf.id].platforms;
+ let target = Services.appinfo.OS + "_" + 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
+ logger.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];
+ }
+ }
+
+ logger.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,
+ gmpAddons: addons
+ };
+ });
+}
+
+/**
+ * Downloads file from a URL using XHR.
+ *
+ * @param url
+ * The url to download from.
+ * @return a promise that resolves to the path of a temporary file or rejects
+ * with a JS exception in case of error.
+ */
+function downloadFile(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.onload = function(response) {
+ logger.info("downloadXHR File download. status=" + xhr.status);
+ if (xhr.status != 200 && xhr.status != 206) {
+ reject(Components.Exception("File download failed", xhr.status));
+ return;
+ }
+ Task.spawn(function* () {
+ let f = yield OS.File.openUnique(OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon"));
+ let path = f.path;
+ logger.info(`Downloaded file will be saved to ${path}`);
+ yield f.file.close();
+ yield OS.File.writeAtomic(path, new Uint8Array(xhr.response));
+ return path;
+ }).then(resolve, reject);
+ };
+
+ let fail = (event) => {
+ let request = event.target;
+ let status = getRequestStatus(request);
+ let message = "Failed downloading via XHR, status: " + status + ", reason: " + event.type;
+ logger.warn(message);
+ let ex = new Error(message);
+ ex.status = status;
+ reject(ex);
+ };
+ xhr.addEventListener("error", fail);
+ xhr.addEventListener("abort", fail);
+
+ xhr.responseType = "arraybuffer";
+ try {
+ xhr.open("GET", url);
+ // Use conservative TLS settings. See bug 1325501.
+ // TODO move to ServiceRequest.
+ if (xhr.channel instanceof Ci.nsIHttpChannelInternal) {
+ xhr.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
+ }
+ xhr.send(null);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+}
+
+/**
+ * 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;
+}
+
+/**
+ * Calculates the hash of a file.
+ *
+ * @param hashFunction
+ * The type of hash function to use, must be supported by nsICryptoHash.
+ * @param path
+ * The path of the file to hash.
+ * @return a promise that resolves to hash of the file or rejects with a JS
+ * exception in case of error.
+ */
+var computeHash = Task.async(function*(hashFunction, path) {
+ let file = yield OS.File.open(path, { existing: true, read: true });
+ try {
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.initWithString(hashFunction);
+
+ let bytes;
+ do {
+ bytes = yield file.read(HASH_CHUNK_SIZE);
+ hasher.update(bytes, bytes.length);
+ } while (bytes.length == HASH_CHUNK_SIZE);
+
+ return binaryToHex(hasher.finish(false));
+ }
+ finally {
+ yield file.close();
+ }
+});
+
+/**
+ * Verifies that a downloaded file matches what was expected.
+ *
+ * @param properties
+ * The properties to check, `size` and `hashFunction` with `hashValue`
+ * are supported. Any properties missing won't be checked.
+ * @param path
+ * The path of the file to check.
+ * @return a promise that resolves if the file matched or rejects with a JS
+ * exception in case of error.
+ */
+var verifyFile = Task.async(function*(properties, path) {
+ if (properties.size !== undefined) {
+ let stat = yield OS.File.stat(path);
+ if (stat.size != properties.size) {
+ throw new Error("Downloaded file was " + stat.size + " bytes but expected " + properties.size + " bytes.");
+ }
+ }
+
+ if (properties.hashFunction !== undefined) {
+ let expectedDigest = properties.hashValue.toLowerCase();
+ let digest = yield computeHash(properties.hashFunction, path);
+ if (digest != expectedDigest) {
+ throw new Error("Hash was `" + digest + "` but expected `" + expectedDigest + "`.");
+ }
+ }
+});
+
+const ProductAddonChecker = {
+ /**
+ * Downloads a list of add-ons from a URL optionally testing the SSL
+ * certificate for certain attributes.
+ *
+ * @param url
+ * The url to download from.
+ * @param allowNonBuiltIn
+ * Whether to trust SSL certificates without a built-in CA issuer.
+ * @param allowedCerts
+ * The list of certificate attributes to match the SSL certificate
+ * against or null to skip checks.
+ * @return a promise that resolves to an object containing the list of add-ons
+ * and whether the local fallback was used, or rejects with a JS
+ * exception in case of error.
+ */
+ getProductAddonList: function(url, allowNonBuiltIn = false, allowedCerts = null) {
+ if (!GMPPrefs.get(GMPPrefs.KEY_UPDATE_ENABLED, true)) {
+ logger.info("Updates are disabled via media.gmp-manager.updateEnabled");
+ return Promise.resolve({usedFallback: true, gmpAddons: []});
+ }
+
+ return downloadXML(url, allowNonBuiltIn, allowedCerts)
+ .then(parseXML)
+ .catch(downloadLocalConfig);
+ },
+
+ /**
+ * Downloads an add-on to a local file and checks that it matches the expected
+ * file. The caller is responsible for deleting the temporary file returned.
+ *
+ * @param addon
+ * The addon to download.
+ * @return a promise that resolves to the temporary file downloaded or rejects
+ * with a JS exception in case of error.
+ */
+ downloadAddon: Task.async(function*(addon) {
+ let path = yield downloadFile(addon.URL);
+ try {
+ yield verifyFile(addon, path);
+ return path;
+ }
+ catch (e) {
+ yield OS.File.remove(path);
+ throw e;
+ }
+ })
+}
diff --git a/components/global/content/gmp-sources/openh264.json b/components/global/content/gmp-sources/openh264.json
new file mode 100644
index 000000000..7c6eb0197
--- /dev/null
+++ b/components/global/content/gmp-sources/openh264.json
@@ -0,0 +1,57 @@
+{
+ "vendors": {
+ "gmp-gmpopenh264": {
+ "platforms": {
+ "WINNT_x86-msvc-x64": {
+ "alias": "WINNT_x86-msvc"
+ },
+ "WINNT_x86-msvc": {
+ "fileUrl": "http://ciscobinary.openh264.org/openh264-win32-0410d336bb748149a4f560eb6108090f078254b1.zip",
+ "hashValue": "991e01c3b95fa13fac52e0512e1936f1edae42ecbbbcc55447a36915eb3ca8f836546cc780343751691e0188872e5bc56fe3ad5f23f3243e90b96a637561b89e",
+ "filesize": 356940
+ },
+ "WINNT_x86-msvc-x86": {
+ "alias": "WINNT_x86-msvc"
+ },
+ "Linux_x86_64-gcc3": {
+ "fileUrl": "http://ciscobinary.openh264.org/openh264-linux64-0410d336bb748149a4f560eb6108090f078254b1.zip",
+ "hashValue": "e1086ee6e4fb60a1aa11b5626594b97695533a8e269d776877cebd5cf29088619e2c164e7bd1eba5486f772c943f2efec723f69cc48478ec84a11d7b61ca1865",
+ "filesize": 515722
+ },
+ "Darwin_x86-gcc3-u-i386-x86_64": {
+ "fileUrl": "http://ciscobinary.openh264.org/openh264-macosx32-0410d336bb748149a4f560eb6108090f078254b1.zip",
+ "hashValue": "64b0e13e6319b7a31ed35a46bea5abcfe6af04ba59a277db07677236cfb685813763731ff6b44b85e03e1489f3b15f8df0128a299a36720531b9f4ba6e1c1f58",
+ "filesize": 382435
+ },
+ "Darwin_x86_64-gcc3": {
+ "alias": "Darwin_x86_64-gcc3-u-i386-x86_64"
+ },
+ "Linux_x86-gcc3": {
+ "fileUrl": "http://ciscobinary.openh264.org/openh264-linux32-0410d336bb748149a4f560eb6108090f078254b1.zip",
+ "hashValue": "19084f0230218c584715861f4723e072b1af02e26995762f368105f670f60ecb4082531bc4e33065a4675dd1296f6872a6cb101547ef2d19ef3e25e2e16d4dc0",
+ "filesize": 515857
+ },
+ "Darwin_x86_64-gcc3-u-i386-x86_64": {
+ "fileUrl": "http://ciscobinary.openh264.org/openh264-macosx64-0410d336bb748149a4f560eb6108090f078254b1.zip",
+ "hashValue": "3b52343070a2f75e91b7b0d3bb33935352237c7e1d2fdc6a467d039ffbbda6a72087f9e0a369fe95e6c4c789ff3052f0c134af721d7273db9ba66d077d85b327",
+ "filesize": 390308
+ },
+ "Darwin_x86-gcc3": {
+ "alias": "Darwin_x86-gcc3-u-i386-x86_64"
+ },
+ "WINNT_x86_64-msvc-x64": {
+ "alias": "WINNT_x86_64-msvc"
+ },
+ "WINNT_x86_64-msvc": {
+ "fileUrl": "http://ciscobinary.openh264.org/openh264-win64-0410d336bb748149a4f560eb6108090f078254b1.zip",
+ "hashValue": "5030b47065e817db5c40bca9c62ac27292bbf636e24698f45dc67f03fa6420b97bd2f792c1cb39df65776c1e7597c70122ac7abf36fb2ad0603734e9e8ec4ef3",
+ "filesize": 404355
+ }
+ },
+ "version": "1.6"
+ }
+ },
+ "hashFunction": "sha512",
+ "name": "OpenH264-1.6",
+ "schema_version": 1000
+}
diff --git a/components/global/content/gmp-sources/widevinecdm.json b/components/global/content/gmp-sources/widevinecdm.json
new file mode 100644
index 000000000..02ef7fee5
--- /dev/null
+++ b/components/global/content/gmp-sources/widevinecdm.json
@@ -0,0 +1,49 @@
+{
+ "vendors": {
+ "gmp-widevinecdm": {
+ "platforms": {
+ "WINNT_x86-msvc-x64": {
+ "alias": "WINNT_x86-msvc"
+ },
+ "WINNT_x86-msvc": {
+ "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-win-ia32.zip",
+ "hashValue": "d7e10d09c87a157af865f8388ba70ae672bd9e38987bdd94077af52d6b1abaa745b3db92e9f93f607af6420c68210f7cfd518a9d2c99fecf79aed3385cbcbc0b",
+ "filesize": 2884452
+ },
+ "WINNT_x86-msvc-x86": {
+ "alias": "WINNT_x86-msvc"
+ },
+ "Linux_x86_64-gcc3": {
+ "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-linux-x64.zip",
+ "hashValue": "1edfb58a44792d2a53694f46fcc698161edafb2a1fe0e5c31b50c1d52408b5e8918d9f33271c62a19a65017694ebeacb4f390fe914688ca7b1952cdb84ed55ec",
+ "filesize": 2975492
+ },
+ "Darwin_x86_64-gcc3-u-i386-x86_64": {
+ "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-mac-x64.zip",
+ "hashValue": "1916805e84a49e04748204f4e4c48ae52c8312f7c04afedacacd7dfab2de424412bc988a8c3e5bcb0865f8844b569c0eb9589dae51e74d9bdfe46792c9d1631f",
+ "filesize": 2155607
+ },
+ "Darwin_x86_64-gcc3": {
+ "alias": "Darwin_x86_64-gcc3-u-i386-x86_64"
+ },
+ "Linux_x86-gcc3": {
+ "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-linux-ia32.zip",
+ "hashValue": "5c4beb72ea693740a013b60bc5491a042bb82fa5ca6845a2f450579e2e1e465263f19e7ab6d08d91deb8219b30f092ab6e6745300d5adda627f270c95e5a66e0",
+ "filesize": 3084582
+ },
+ "WINNT_x86_64-msvc-x64": {
+ "alias": "WINNT_x86_64-msvc"
+ },
+ "WINNT_x86_64-msvc": {
+ "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-win-x64.zip",
+ "hashValue": "33497f3458846e11fa52413f6477bfe1a7f502da262c3a2ce9fe6d773a4a2d023c54228596eb162444b55c87fb126de01f60fa729d897ef5e6eec73b2dfbdc7a",
+ "filesize": 2853777
+ }
+ },
+ "version": "1.4.8.903"
+ }
+ },
+ "hashFunction": "sha512",
+ "name": "Widevine-1.4.8.903",
+ "schema_version": 1000
+}
diff --git a/components/global/content/process-content.js b/components/global/content/process-content.js
index 9d7e37100..2ff8f908a 100644
--- a/components/global/content/process-content.js
+++ b/components/global/content/process-content.js
@@ -14,6 +14,13 @@ Cu.import("resource://gre/modules/Services.jsm");
const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+Services.cpmm.addMessageListener("gmp-plugin-crash", msg => {
+ let gmpservice = Cc["@mozilla.org/gecko-media-plugin-service;1"]
+ .getService(Ci.mozIGeckoMediaPluginService);
+
+ gmpservice.RunPluginCrashCallbacks(msg.data.pluginID, msg.data.pluginName);
+});
+
if (gInContentProcess) {
let ProcessObserver = {
TOPICS: [
diff --git a/components/global/jar.mn b/components/global/jar.mn
index b02587ec4..5086663c2 100644
--- a/components/global/jar.mn
+++ b/components/global/jar.mn
@@ -1,6 +1,8 @@
toolkit.jar:
% content global %content/global/ contentaccessible=yes
% content global-platform %content/global-platform/ platform
+ content/global/gmp-sources/openh264.json (content/gmp-sources/openh264.json)
+ content/global/gmp-sources/widevinecdm.json (content/gmp-sources/widevinecdm.json)
content/global/XPCNativeWrapper.js (content/XPCNativeWrapper.js)
content/global/minimal-xul.css (content/minimal-xul.css)
* content/global/xul.css (content/xul.css)