diff options
author | janekptacijarabaci <janekptacijarabaci@seznam.cz> | 2018-07-06 15:53:52 +0200 |
---|---|---|
committer | janekptacijarabaci <janekptacijarabaci@seznam.cz> | 2018-07-06 15:53:52 +0200 |
commit | 941e54654eabed0a3568f7fefe424a45aa02eddb (patch) | |
tree | 49aa02b174c428962d99142d8061267bfcd79e69 /toolkit/mozapps/extensions/internal | |
parent | ad9ee72dcd7981bc47b3844a224d69fadfdfd8ef (diff) | |
parent | 0daa12376295d5d796256a116eb2a348a3a9273f (diff) | |
download | UXP-941e54654eabed0a3568f7fefe424a45aa02eddb.tar UXP-941e54654eabed0a3568f7fefe424a45aa02eddb.tar.gz UXP-941e54654eabed0a3568f7fefe424a45aa02eddb.tar.lz UXP-941e54654eabed0a3568f7fefe424a45aa02eddb.tar.xz UXP-941e54654eabed0a3568f7fefe424a45aa02eddb.zip |
Merge branch 'master' of https://github.com/MoonchildProductions/UXP into _testBranch_test_1
Diffstat (limited to 'toolkit/mozapps/extensions/internal')
12 files changed, 952 insertions, 221 deletions
diff --git a/toolkit/mozapps/extensions/internal/AddonLogging.jsm b/toolkit/mozapps/extensions/internal/AddonLogging.jsm index 362439bae..f05a6fe6c 100644 --- a/toolkit/mozapps/extensions/internal/AddonLogging.jsm +++ b/toolkit/mozapps/extensions/internal/AddonLogging.jsm @@ -81,7 +81,7 @@ function AddonLogger(aName) { AddonLogger.prototype = { name: null, - error: function AddonLogger_error(aStr, aException) { + error: function(aStr, aException) { let message = formatLogMessage("error", this.name, aStr, aException); let stack = getStackDetails(aException); @@ -95,6 +95,18 @@ AddonLogger.prototype = { // Always dump errors, in case the Console Service isn't listening yet dump("*** " + message + "\n"); + function formatTimestamp(date) { + // Format timestamp as: "%Y-%m-%d %H:%M:%S" + let year = String(date.getFullYear()); + let month = String(date.getMonth() + 1).padStart(2, "0"); + let day = String(date.getDate()).padStart(2, "0"); + let hours = String(date.getHours()).padStart(2, "0"); + let minutes = String(date.getMinutes()).padStart(2, "0"); + let seconds = String(date.getSeconds()).padStart(2, "0"); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + } + try { var tstamp = new Date(); var logfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS_LOG]); @@ -104,7 +116,7 @@ AddonLogger.prototype = { var writer = Cc["@mozilla.org/intl/converter-output-stream;1"]. createInstance(Ci.nsIConverterOutputStream); writer.init(stream, "UTF-8", 0, 0x0000); - writer.writeString(tstamp.toLocaleFormat("%Y-%m-%d %H:%M:%S ") + + writer.writeString(formatTimestamp(tstamp) + " " + message + " at " + stack.sourceName + ":" + stack.lineNumber + "\n"); writer.close(); @@ -112,7 +124,7 @@ AddonLogger.prototype = { catch (e) { } }, - warn: function AddonLogger_warn(aStr, aException) { + warn: function(aStr, aException) { let message = formatLogMessage("warn", this.name, aStr, aException); let stack = getStackDetails(aException); @@ -127,7 +139,7 @@ AddonLogger.prototype = { dump("*** " + message + "\n"); }, - log: function AddonLogger_log(aStr, aException) { + log: function(aStr, aException) { if (gDebugLogEnabled) { let message = formatLogMessage("log", this.name, aStr, aException); dump("*** " + message + "\n"); @@ -137,14 +149,14 @@ AddonLogger.prototype = { }; this.LogManager = { - getLogger: function LogManager_getLogger(aName, aTarget) { + getLogger: function(aName, aTarget) { let logger = new AddonLogger(aName); if (aTarget) { ["error", "warn", "log"].forEach(function(name) { let fname = name.toUpperCase(); delete aTarget[fname]; - aTarget[fname] = function LogManager_targetName(aStr, aException) { + aTarget[fname] = function(aStr, aException) { logger[name](aStr, aException); }; }); @@ -155,13 +167,13 @@ this.LogManager = { }; var PrefObserver = { - init: function PrefObserver_init() { + init: function() { Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false); Services.obs.addObserver(this, "xpcom-shutdown", false); this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED); }, - observe: function PrefObserver_observe(aSubject, aTopic, aData) { + observe: function(aSubject, aTopic, aData) { if (aTopic == "xpcom-shutdown") { Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this); Services.obs.removeObserver(this, "xpcom-shutdown"); diff --git a/toolkit/mozapps/extensions/internal/AddonRepository.jsm b/toolkit/mozapps/extensions/internal/AddonRepository.jsm index adcecbee7..76a7528c7 100644 --- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm +++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm @@ -72,7 +72,7 @@ const LOGGER_ID = "addons.repository"; // Create a new logger for use by the Addons Repository // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); // A map between XML keys to AddonSearchResult keys for string values // that require no extra parsing from XML @@ -101,7 +101,7 @@ const INTEGER_KEY_MAP = { }; // Wrap the XHR factory so that tests can override with a mock -let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", +var XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", "nsIXMLHttpRequest"); function convertHTMLToPlainText(html) { diff --git a/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm index 128146bbe..66147b9aa 100644 --- a/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm +++ b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm @@ -30,7 +30,7 @@ const LOGGER_ID = "addons.repository.sqlmigrator"; // Create a new logger for use by the Addons Repository SQL Migrator // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"]; @@ -60,7 +60,11 @@ this.AddonRepository_SQLiteMigrator = { this._retrieveStoredData((results) => { this._closeConnection(); - let resultArray = [addon for ([,addon] of Iterator(results))]; + // Tycho: let resultArray = [addon for ([,addon] of Iterator(results))]; + let resultArray = []; + for (let [,addon] of Iterator(results)) { + resultArray.push(addon); + } logger.debug(resultArray.length + " addons imported.") aCallback(resultArray); }); diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm index d68a0f175..f927bc745 100644 --- a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm +++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm @@ -15,30 +15,36 @@ const Cu = Components.utils; this.EXPORTED_SYMBOLS = [ "AddonUpdateChecker" ]; -const TIMEOUT = 60 * 1000; -const PREFIX_NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; -const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; -const PREFIX_ITEM = "urn:mozilla:item:"; -const PREFIX_EXTENSION = "urn:mozilla:extension:"; -const PREFIX_THEME = "urn:mozilla:theme:"; -const TOOLKIT_ID = "toolkit@mozilla.org" -#ifdef MOZ_PHOENIX_EXTENSIONS -const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}" -#endif -const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml" +const TIMEOUT = 60 * 1000; +const PREFIX_NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; +const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; +const PREFIX_ITEM = "urn:mozilla:item:"; +const PREFIX_EXTENSION = "urn:mozilla:extension:"; +const PREFIX_THEME = "urn:mozilla:theme:"; +const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"; + +const TOOLKIT_ID = "toolkit@mozilla.org"; +const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; +const FIREFOX_APPCOMPATVERSION = "56.9" const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts"; +const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"; Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate", + "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", "resource://gre/modules/addons/AddonRepository.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest", + "resource://gre/modules/ServiceRequest.jsm"); + // Shared code for suppressing bad cert dialogs. -XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() { +XPCOMUtils.defineLazyGetter(this, "CertUtils", function() { let certUtils = {}; Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils); return certUtils; @@ -52,7 +58,7 @@ const LOGGER_ID = "addons.update-checker"; // Create a new logger for use by the Addons Update Checker // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); /** * A serialisation method for RDF data that produces an identical string @@ -80,7 +86,7 @@ RDFSerializer.prototype = { * @return a string with all characters invalid in XML character data * converted to entity references. */ - escapeEntities: function RDFS_escapeEntities(aString) { + escapeEntities: function(aString) { aString = aString.replace(/&/g, "&"); aString = aString.replace(/</g, "<"); aString = aString.replace(/>/g, ">"); @@ -98,8 +104,7 @@ RDFSerializer.prototype = { * The current level of indent for pretty-printing * @return a string containing the serialized elements. */ - serializeContainerItems: function RDFS_serializeContainerItems(aDs, aContainer, - aIndent) { + serializeContainerItems: function(aDs, aContainer, aIndent) { var result = ""; var items = aContainer.GetElements(); while (items.hasMoreElements()) { @@ -125,9 +130,7 @@ RDFSerializer.prototype = { * @return a string containing the serialized properties. * @throws if the resource contains a property that cannot be serialized */ - serializeResourceProperties: function RDFS_serializeResourceProperties(aDs, - aResource, - aIndent) { + serializeResourceProperties: function(aDs, aResource, aIndent) { var result = ""; var items = []; var arcs = aDs.ArcLabelsOut(aResource); @@ -181,7 +184,7 @@ RDFSerializer.prototype = { * @return a string containing the serialized resource. * @throws if the RDF data contains multiple references to the same resource. */ - serializeResource: function RDFS_serializeResource(aDs, aResource, aIndent) { + serializeResource: function(aDs, aResource, aIndent) { if (this.resources.indexOf(aResource) != -1 ) { // We cannot output multiple references to the same resource. throw Components.Exception("Cannot serialize multiple references to " + aResource.Value); @@ -221,6 +224,48 @@ RDFSerializer.prototype = { } /** + * Sanitizes the update URL in an update item, as returned by + * parseRDFManifest and parseJSONManifest. Ensures that: + * + * - The URL is secure, or secured by a strong enough hash. + * - The security principal of the update manifest has permission to + * load the URL. + * + * @param aUpdate + * The update item to sanitize. + * @param aRequest + * The XMLHttpRequest used to load the manifest. + * @param aHashPattern + * The regular expression used to validate the update hash. + * @param aHashString + * The human-readable string specifying which hash functions + * are accepted. + */ +function sanitizeUpdateURL(aUpdate, aRequest, aHashPattern, aHashString) { + if (aUpdate.updateURL) { + let scriptSecurity = Services.scriptSecurityManager; + let principal = scriptSecurity.getChannelURIPrincipal(aRequest.channel); + try { + // This logs an error on failure, so no need to log it a second time + scriptSecurity.checkLoadURIStrWithPrincipal(principal, aUpdate.updateURL, + scriptSecurity.DISALLOW_SCRIPT); + } catch (e) { + delete aUpdate.updateURL; + return; + } + + if (AddonManager.checkUpdateSecurity && + !aUpdate.updateURL.startsWith("https:") && + !aHashPattern.test(aUpdate.updateHash)) { + logger.warn(`Update link ${aUpdate.updateURL} is not secure and is not verified ` + + `by a strong enough hash (needs to be ${aHashString}).`); + delete aUpdate.updateURL; + delete aUpdate.updateHash; + } + } +} + +/** * Parses an RDF style update manifest into an array of update objects. * * @param aId @@ -229,10 +274,17 @@ RDFSerializer.prototype = { * An optional update key for the add-on * @param aRequest * The XMLHttpRequest that has retrieved the update manifest + * @param aManifestData + * The pre-parsed manifest, as a bare XML DOM document * @return an array of update objects * @throws if the update manifest is invalid in any way */ -function parseRDFManifest(aId, aUpdateKey, aRequest) { +function parseRDFManifest(aId, aUpdateKey, aRequest, aManifestData) { + if (aManifestData.documentElement.namespaceURI != PREFIX_NS_RDF) { + throw Components.Exception("update.rdf: Update manifest had an unrecognised namespace: " + + aManifestData.documentElement.namespaceURI); + } + function EM_R(aProp) { return gRDF.GetResource(PREFIX_NS_EM + aProp); } @@ -261,7 +313,7 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { function getRequiredProperty(aDs, aSource, aProperty) { let value = getProperty(aDs, aSource, aProperty); if (!value) - throw Components.Exception("Update manifest is missing a required " + aProperty + " property."); + throw Components.Exception("update.rdf: Update manifest is missing a required " + aProperty + " property."); return value; } @@ -275,15 +327,19 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { let extensionRes = gRDF.GetResource(PREFIX_EXTENSION + aId); let themeRes = gRDF.GetResource(PREFIX_THEME + aId); let itemRes = gRDF.GetResource(PREFIX_ITEM + aId); - let addonRes = ds.ArcLabelsOut(extensionRes).hasMoreElements() ? extensionRes - : ds.ArcLabelsOut(themeRes).hasMoreElements() ? themeRes - : itemRes; + let addonRes; + if (ds.ArcLabelsOut(extensionRes).hasMoreElements()) + addonRes = extensionRes; + else if (ds.ArcLabelsOut(themeRes).hasMoreElements()) + addonRes = themeRes; + else + addonRes = itemRes; // If we have an update key then the update manifest must be signed if (aUpdateKey) { let signature = getProperty(ds, addonRes, "signature"); if (!signature) - throw Components.Exception("Update manifest for " + aId + " does not contain a required signature"); + throw Components.Exception("update.rdf: Update manifest for " + aId + " does not contain a required signature"); let serializer = new RDFSerializer(); let updateString = null; @@ -291,7 +347,7 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { updateString = serializer.serializeResource(ds, addonRes); } catch (e) { - throw Components.Exception("Failed to generate signed string for " + aId + ". Serializer threw " + e, + throw Components.Exception("update.rdf: Failed to generate signed string for " + aId + ". Serializer threw " + e, e.result); } @@ -303,7 +359,7 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { result = verifier.verifyData(updateString, signature, aUpdateKey); } catch (e) { - throw Components.Exception("The signature or updateKey for " + aId + " is malformed." + + throw Components.Exception("update.rdf: The signature or updateKey for " + aId + " is malformed." + "Verifier threw " + e, e.result); } @@ -316,7 +372,7 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { // A missing updates property doesn't count as a failure, just as no avialable // update information if (!updates) { - logger.warn("Update manifest for " + aId + " did not contain an updates property"); + logger.warn("update.rdf: Update manifest for " + aId + " did not contain an updates property"); return []; } @@ -326,7 +382,7 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { let cu = Cc["@mozilla.org/rdf/container-utils;1"]. getService(Ci.nsIRDFContainerUtils); if (!cu.IsContainer(ds, updates)) - throw Components.Exception("Updates property was not an RDF container"); + throw Components.Exception("update.rdf: Updates property was not an RDF container"); let results = []; let ctr = Cc["@mozilla.org/rdf/container;1"]. @@ -337,11 +393,11 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { let item = items.getNext().QueryInterface(Ci.nsIRDFResource); let version = getProperty(ds, item, "version"); if (!version) { - logger.warn("Update manifest is missing a required version property."); + logger.warn("update.rdf: Update manifest is missing a required version property."); continue; } - logger.debug("Found an update entry for " + aId + " version " + version); + logger.debug("update.rdf: Found an update entry for " + aId + " version " + version); let targetApps = ds.GetTargets(item, EM_R("targetApplication"), true); while (targetApps.hasMoreElements()) { @@ -369,14 +425,10 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { targetApplications: [appEntry] }; - if (result.updateURL && AddonManager.checkUpdateSecurity && - result.updateURL.substring(0, 6) != "https:" && - (!result.updateHash || result.updateHash.substring(0, 3) != "sha")) { - logger.warn("updateLink " + result.updateURL + " is not secure and is not verified" + - " by a strong enough hash (needs to be sha1 or stronger)."); - delete result.updateURL; - delete result.updateHash; - } + // The JSON update protocol requires an SHA-2 hash. RDF still + // supports SHA-1, for compatibility reasons. + sanitizeUpdateURL(result, aRequest, /^sha/, "sha1 or stronger"); + results.push(result); } } @@ -384,6 +436,160 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { } /** + * Parses an JSON update manifest into an array of update objects. + * + * @param aId + * The ID of the add-on being checked for updates + * @param aUpdateKey + * An optional update key for the add-on + * @param aRequest + * The XMLHttpRequest that has retrieved the update manifest + * @param aManifestData + * The pre-parsed manifest, as a JSON object tree + * @return an array of update objects + * @throws if the update manifest is invalid in any way + */ +function parseJSONManifest(aId, aUpdateKey, aRequest, aManifestData) { + if (aUpdateKey) + throw Components.Exception("update.json: Update keys are not supported for JSON update manifests"); + + let TYPE_CHECK = { + "array": val => Array.isArray(val), + "object": val => val && typeof val == "object" && !Array.isArray(val), + }; + + function getProperty(aObj, aProperty, aType, aDefault = undefined) { + if (!(aProperty in aObj)) + return aDefault; + + let value = aObj[aProperty]; + + let matchesType = aType in TYPE_CHECK ? TYPE_CHECK[aType](value) : typeof value == aType; + if (!matchesType) + throw Components.Exception(`update.json: Update manifest property '${aProperty}' has incorrect type (expected ${aType})`); + + return value; + } + + function getRequiredProperty(aObj, aProperty, aType) { + let value = getProperty(aObj, aProperty, aType); + if (value === undefined) + throw Components.Exception(`update.json: Update manifest is missing a required ${aProperty} property.`); + return value; + } + + let manifest = aManifestData; + + if (!TYPE_CHECK["object"](manifest)) + throw Components.Exception("update.json: Root element of update manifest must be a JSON object literal"); + + // The set of add-ons this manifest has updates for + let addons = getRequiredProperty(manifest, "addons", "object"); + + // The entry for this particular add-on + let addon = getProperty(addons, aId, "object"); + + // A missing entry doesn't count as a failure, just as no avialable update + // information + if (!addon) { + logger.warn("update.json: Update manifest did not contain an entry for " + aId); + return []; + } + + let appID = Services.appinfo.ID; + let platformVersion = Services.appinfo.platformVersion; + + // The list of available updates + let updates = getProperty(addon, "updates", "array", []); + + let results = []; + + for (let update of updates) { + let version = getRequiredProperty(update, "version", "string"); + logger.debug(`update.json: Found an update entry for ${aId} version ${version}`); + + let applications = getRequiredProperty(update, "applications", "object"); + + let app; + let appEntry; + + if (appID in applications) { + logger.debug("update.json: Native targetApplication"); + app = getProperty(applications, appID, "object"); + + appEntry = { + id: appID, + minVersion: getRequiredProperty(app, "min_version", "string"), + maxVersion: getRequiredProperty(app, "max_version", "string"), + } + } +#ifdef MOZ_PHOENIX_EXTENSIONS + else if (FIREFOX_ID in applications) { + logger.debug("update.json: Dual-GUID targetApplication"); + app = getProperty(applications, FIREFOX_ID, "object"); + + appEntry = { + id: FIREFOX_ID, + minVersion: getRequiredProperty(app, "min_version", "string"), + maxVersion: getRequiredProperty(app, "max_version", "string"), + } + } +#endif + else if (TOOLKIT_ID in applications) { + logger.debug("update.json: Toolkit targetApplication"); + app = getProperty(applications, TOOLKIT_ID, "object"); + + appEntry = { + id: TOOLKIT_ID, + minVersion: getRequiredProperty(app, "min_version", "string"), + maxVersion: getRequiredProperty(app, "max_version", "string"), + } + } + else if ("gecko" in applications) { + logger.debug("update.json: Mozilla Compatiblity Mode"); + app = getProperty(applications, "gecko", "object"); + + appEntry = { +#ifdef MOZ_PHOENIX + id: FIREFOX_ID, + minVersion: getProperty(app, "strict_min_version", "string", + Services.prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION)), +#else + id: TOOLKIT_ID, + minVersion: platformVersion, +#endif +#if defined(MOZ_PHOENIX) && defined(MOZ_PHOENIX_EXTENSIONS) + maxVersion: FIREFOX_APPCOMPATVERSION, +#else + maxVersion: '*', +#endif + }; + } + else { + continue; + } + + let result = { + id: aId, + version: version, + multiprocessCompatible: getProperty(update, "multiprocess_compatible", "boolean", false), + updateURL: getProperty(update, "update_link", "string"), + updateHash: getProperty(update, "update_hash", "string"), + updateInfoURL: getProperty(update, "update_info_url", "string"), + strictCompatibility: getProperty(app, "strict_compatibility", "boolean", false), + targetApplications: [appEntry], + }; + + // The JSON update protocol requires an SHA-2 hash. RDF still + // supports SHA-1, for compatibility reasons. + sanitizeUpdateURL(result, aRequest, /^sha(256|512):/, "sha256 or sha512"); + + results.push(result); + } + return results; +} + +/** * Starts downloading an update manifest and then passes it to an appropriate * parser to convert to an array of update objects * @@ -411,20 +617,18 @@ function UpdateParser(aId, aUpdateKey, aUrl, aObserver) { logger.debug("Requesting " + aUrl); try { - this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(Ci.nsIXMLHttpRequest); + this.request = new ServiceRequest(); this.request.open("GET", this.url, true); this.request.channel.notificationCallbacks = new CertUtils.BadCertHandler(!requireBuiltIn); this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // Prevent the request from writing to cache. this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; - this.request.overrideMimeType("text/xml"); + this.request.overrideMimeType("text/plain"); this.request.setRequestHeader("Moz-XPI-Update", "1", true); this.request.timeout = TIMEOUT; - var self = this; - this.request.addEventListener("load", function loadEventListener(event) { self.onLoad() }, false); - this.request.addEventListener("error", function errorEventListener(event) { self.onError() }, false); - this.request.addEventListener("timeout", function timeoutEventListener(event) { self.onTimeout() }, false); + this.request.addEventListener("load", () => this.onLoad(), false); + this.request.addEventListener("error", () => this.onError(), false); + this.request.addEventListener("timeout", () => this.onTimeout(), false); this.request.send(null); } catch (e) { @@ -442,7 +646,7 @@ UpdateParser.prototype = { /** * Called when the manifest has been successfully loaded. */ - onLoad: function UP_onLoad() { + onLoad: function() { let request = this.request; this.request = null; this._doneAt = new Error("place holder"); @@ -458,6 +662,7 @@ UpdateParser.prototype = { CertUtils.checkCert(request.channel, !requireBuiltIn); } catch (e) { + logger.warn("Request failed: " + this.url + " - " + e); this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR); return; } @@ -476,41 +681,52 @@ UpdateParser.prototype = { return; } - let xml = request.responseXML; - if (!xml || xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR) { - logger.warn("Update manifest was not valid XML"); - this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR); + // Detect the manifest type by first attempting to parse it as + // JSON, and falling back to parsing it as XML if that fails. + let parser; + try { + try { + let json = JSON.parse(request.responseText); + + parser = () => parseJSONManifest(this.id, this.updateKey, request, json); + } catch (e) { + if (!(e instanceof SyntaxError)) + throw e; + let domParser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser); + let xml = domParser.parseFromString(request.responseText, "text/xml"); + + if (xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR) + throw new Error("Update manifest was not valid XML or JSON"); + + parser = () => parseRDFManifest(this.id, this.updateKey, request, xml); + } + } catch (e) { + logger.warn("onUpdateCheckComplete failed to determine manifest type"); + this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT); return; } - // We currently only know about RDF update manifests - if (xml.documentElement.namespaceURI == PREFIX_NS_RDF) { - let results = null; + let results; + try { + results = parser(); + } + catch (e) { + logger.warn("onUpdateCheckComplete failed to parse update manifest", e); + this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR); + return; + } + if ("onUpdateCheckComplete" in this.observer) { try { - results = parseRDFManifest(this.id, this.updateKey, request); + this.observer.onUpdateCheckComplete(results); } catch (e) { - logger.warn("onUpdateCheckComplete failed to parse RDF manifest", e); - this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR); - return; - } - if ("onUpdateCheckComplete" in this.observer) { - try { - this.observer.onUpdateCheckComplete(results); - } - catch (e) { - logger.warn("onUpdateCheckComplete notification failed", e); - } - } - else { - logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker")); + logger.warn("onUpdateCheckComplete notification failed", e); } - return; } - - logger.warn("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI); - this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT); + else { + logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker")); + } }, /** @@ -526,7 +742,7 @@ UpdateParser.prototype = { /** * Called when the manifest failed to load. */ - onError: function UP_onError() { + onError: function() { if (!Components.isSuccessCode(this.request.status)) { logger.warn("Request failed: " + this.url + " - " + this.request.status); } @@ -555,7 +771,7 @@ UpdateParser.prototype = { /** * Helper method to notify the observer that an error occured. */ - notifyError: function UP_notifyError(aStatus) { + notifyError: function(aStatus) { if ("onUpdateCheckError" in this.observer) { try { this.observer.onUpdateCheckError(aStatus); @@ -569,7 +785,7 @@ UpdateParser.prototype = { /** * Called to cancel an in-progress update check. */ - cancel: function UP_cancel() { + cancel: function() { if (!this.request) { logger.error("Trying to cancel already-complete request", this._doneAt); return; @@ -669,12 +885,9 @@ this.AddonUpdateChecker = { * Ignore strictCompatibility when testing if an update matches. Optional. * @return an update object if one matches or null if not */ - getCompatibilityUpdate: function AUC_getCompatibilityUpdate(aUpdates, aVersion, - aIgnoreCompatibility, - aAppVersion, - aPlatformVersion, - aIgnoreMaxVersion, - aIgnoreStrictCompat) { + getCompatibilityUpdate: function(aUpdates, aVersion, aIgnoreCompatibility, + aAppVersion, aPlatformVersion, + aIgnoreMaxVersion, aIgnoreStrictCompat) { if (!aAppVersion) aAppVersion = Services.appinfo.version; if (!aPlatformVersion) @@ -686,8 +899,8 @@ this.AddonUpdateChecker = { for (let targetApp of update.targetApplications) { let id = targetApp.id; #ifdef MOZ_PHOENIX_EXTENSIONS - if (id == Services.appinfo.ID || id == FIREFOX_ID || - id == TOOLKIT_ID) + if (id == Services.appinfo.ID || id == FIREFOX_ID || + id == TOOLKIT_ID) #else if (id == Services.appinfo.ID || id == TOOLKIT_ID) #endif @@ -720,12 +933,9 @@ this.AddonUpdateChecker = { * Array of AddonCompatibilityOverride to take into account. Optional. * @return an update object if one matches or null if not */ - getNewestCompatibleUpdate: function AUC_getNewestCompatibleUpdate(aUpdates, - aAppVersion, - aPlatformVersion, - aIgnoreMaxVersion, - aIgnoreStrictCompat, - aCompatOverrides) { + getNewestCompatibleUpdate: function(aUpdates, aAppVersion, aPlatformVersion, + aIgnoreMaxVersion, aIgnoreStrictCompat, + aCompatOverrides) { if (!aAppVersion) aAppVersion = Services.appinfo.version; if (!aPlatformVersion) @@ -765,9 +975,18 @@ this.AddonUpdateChecker = { * @return UpdateParser so that the caller can use UpdateParser.cancel() to shut * down in-progress update requests */ - checkForUpdates: function AUC_checkForUpdates(aId, aUpdateKey, aUrl, aObserver) { - // Exclude default theme - if (aId != "{972ce4c6-7e08-4474-a285-3208198ce6fd}") + checkForUpdates: function(aId, aUpdateKey, aUrl, aObserver) { + // Define an array of internally used IDs to NOT send to AUS such as the + // Default Theme. Please keep this list in sync with: + // toolkit/mozapps/extensions/AddonUpdateChecker.jsm + let internalIDS = [ + '{972ce4c6-7e08-4474-a285-3208198ce6fd}', + 'modern@themes.mozilla.org' + ]; + + // If the ID is not in the array then go ahead and query AUS + if (internalIDS.indexOf(aId) == -1) { return new UpdateParser(aId, aUpdateKey, aUrl, aObserver); + } } }; diff --git a/toolkit/mozapps/extensions/internal/Content.js b/toolkit/mozapps/extensions/internal/Content.js index 29c0ed8ce..9f366ba32 100644 --- a/toolkit/mozapps/extensions/internal/Content.js +++ b/toolkit/mozapps/extensions/internal/Content.js @@ -2,29 +2,36 @@ * 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/. */ +/* globals addMessageListener*/ + "use strict"; (function() { -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; -let {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); +var {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); -let nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", +var nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", "initWithPath"); const MSG_JAR_FLUSH = "AddonJarFlush"; +const MSG_MESSAGE_MANAGER_CACHES_FLUSH = "AddonMessageManagerCachesFlush"; try { if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) { - // Propagate JAR cache flush notifications across process boundaries. - addMessageListener(MSG_JAR_FLUSH, function jar_flushMessageListener(message) { + // Propagate JAR cache flush notifications across process boundaries. + addMessageListener(MSG_JAR_FLUSH, function(message) { let file = new nsIFile(message.data); Services.obs.notifyObservers(file, "flush-cache-entry", null); }); + // Propagate message manager caches flush notifications across processes. + addMessageListener(MSG_MESSAGE_MANAGER_CACHES_FLUSH, function() { + Services.obs.notifyObservers(null, "message-manager-flush-caches", null); + }); } -} catch(e) { +} catch (e) { Cu.reportError(e); } diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.jsm b/toolkit/mozapps/extensions/internal/GMPProvider.jsm index a55457f6e..25651f1b8 100644 --- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm +++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm @@ -4,9 +4,9 @@ "use strict"; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; this.EXPORTED_SYMBOLS = []; @@ -58,17 +58,18 @@ const GMP_PLUGINS = [ 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)); -let messageManager = Cc["@mozilla.org/globalmessagemanager;1"] +var messageManager = Cc["@mozilla.org/globalmessagemanager;1"] .getService(Ci.nsIMessageListenerManager); -let gLogger; -let gLogAppenderDump = null; +var gLogger; +var gLogAppenderDump = null; function configureLogging() { if (!gLogger) { @@ -443,7 +444,7 @@ GMPWrapper.prototype = { }, }; -let GMPProvider = { +var GMPProvider = { get name() { return "GMPProvider"; }, _plugins: null, diff --git a/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm index fccde9a81..a9201c3da 100644 --- a/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm +++ b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm @@ -49,7 +49,7 @@ this.LightweightThemeImageOptimizer = { Object.freeze(LightweightThemeImageOptimizer); -let ImageCropper = { +var ImageCropper = { _inProgress: {}, getCroppedImageURL: @@ -59,10 +59,14 @@ let ImageCropper = { return aImageURL; } - if (Services.prefs.getBoolPref("lightweightThemes.animation.enabled")) { - //Don't crop if animated - return aImageURL; - } + try { + if (Services.prefs.getBoolPref("lightweightThemes.animation.enabled")) { + //Don't crop if animated + return aImageURL; + } + } catch(e) { + // Continue of pref doesn't exist. + } // Generate the cropped image's file name using its // base name and the current screen size. @@ -119,23 +123,20 @@ let ImageCropper = { } }; -let ImageFile = { - read: function ImageFile_read(aURI, aCallback) { - this._netUtil.asyncFetch2( - aURI, - function read_asyncFetch(aInputStream, aStatus, aRequest) { +var ImageFile = { + read: function(aURI, aCallback) { + this._netUtil.asyncFetch({ + uri: aURI, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE + }, function(aInputStream, aStatus, aRequest) { if (Components.isSuccessCode(aStatus) && aRequest instanceof Ci.nsIChannel) { let channel = aRequest.QueryInterface(Ci.nsIChannel); aCallback(aInputStream, channel.contentType); } else { aCallback(); } - }, - null, // aLoadingNode - Services.scriptSecurityManager.getSystemPrincipal(), - null, // aTriggeringPrincipal - Ci.nsILoadInfo.SEC_NORMAL, - Ci.nsIContentPolicy.TYPE_IMAGE); + }); }, write: function ImageFile_write(aFile, aInputStream, aCallback) { @@ -158,7 +159,7 @@ let ImageFile = { XPCOMUtils.defineLazyModuleGetter(ImageFile, "_netUtil", "resource://gre/modules/NetUtil.jsm", "NetUtil"); -let ImageTools = { +var ImageTools = { decode: function ImageTools_decode(aInputStream, aContentType) { let outParam = {value: null}; @@ -187,7 +188,7 @@ let ImageTools = { XPCOMUtils.defineLazyServiceGetter(ImageTools, "_imgTools", "@mozilla.org/image/tools;1", "imgITools"); -let Utils = { +var Utils = { createCopy: function Utils_createCopy(aData) { let copy = {}; for (let [k, v] in Iterator(aData)) { diff --git a/toolkit/mozapps/extensions/internal/PluginProvider.jsm b/toolkit/mozapps/extensions/internal/PluginProvider.jsm index 04a4f9d7c..cb07dcb12 100644 --- a/toolkit/mozapps/extensions/internal/PluginProvider.jsm +++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm @@ -23,7 +23,7 @@ const LOGGER_ID = "addons.plugins"; // Create a new logger for use by the Addons Plugin Provider // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); function getIDHashForString(aStr) { // return the two-digit hexadecimal code for a byte diff --git a/toolkit/mozapps/extensions/internal/ProductAddonChecker.jsm b/toolkit/mozapps/extensions/internal/ProductAddonChecker.jsm new file mode 100644 index 000000000..f98dd2a94 --- /dev/null +++ b/toolkit/mozapps/extensions/internal/ProductAddonChecker.jsm @@ -0,0 +1,467 @@ +/* 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-eme-adobe", + "src": "chrome://global/content/gmp-sources/eme-adobe.json" +}, { + "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/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm index d5f1ab5dd..9ea876f6c 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -36,8 +36,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); +#ifdef MOZ_DEVTOOLS XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm"); +#endif XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI", "resource://gre/modules/Console.jsm"); @@ -52,6 +54,10 @@ XPCOMUtils.defineLazyServiceGetter(this, "ResProtocolHandler", "@mozilla.org/network/protocol;1?name=resource", "nsIResProtocolHandler"); +XPCOMUtils.defineLazyServiceGetter(this, + "AddonPathService", + "@mozilla.org/addon-path-service;1", + "amIAddonPathService"); const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", "initWithPath"); @@ -90,8 +96,6 @@ const PREF_INTERPOSITION_ENABLED = "extensions.interposition.enabled"; const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"; const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion"; -const PREF_CHECKCOMAT_THEMEOVERRIDE = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion"; - const URI_EXTENSION_SELECT_DIALOG = "chrome://mozapps/content/extensions/selectAddons.xul"; const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul"; const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; @@ -106,6 +110,10 @@ const DIR_TRASH = "trash"; const FILE_DATABASE = "extensions.json"; const FILE_OLD_CACHE = "extensions.cache"; const FILE_INSTALL_MANIFEST = "install.rdf"; +#ifndef MOZ_JETPACK +const FILE_JETPACK_MANIFEST_1 = "harness-options.json"; +const FILE_JETPACK_MANIFEST_2 = "package.json"; +#endif const FILE_WEBEXT_MANIFEST = "manifest.json"; const FILE_XPI_ADDONS_LIST = "extensions.ini"; @@ -129,12 +137,15 @@ const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; const TOOLKIT_ID = "toolkit@mozilla.org"; #ifdef MOZ_PHOENIX_EXTENSIONS const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}" -const FIREFOX_APPCOMPATVERSION = "27.9" +const FIREFOX_APPCOMPATVERSION = "56.9" #endif // The value for this is in Makefile.in #expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__; +XPCOMUtils.defineConstant(this, "DB_SCHEMA", DB_SCHEMA); +#ifdef MOZ_DEVTOOLS const NOTIFICATION_TOOLBOXPROCESS_LOADED = "ToolboxProcessLoaded"; +#endif // Properties that exist in the install manifest const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", @@ -222,7 +233,7 @@ const LOGGER_ID = "addons.xpi"; // Create a new logger for use by all objects in this Addons XPI Provider module // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); const LAZY_OBJECTS = ["XPIDatabase"]; @@ -649,22 +660,8 @@ function isUsableAddon(aAddon) { return false; } else { - let app = aAddon.matchingTargetApplication; - if (!app) + if (!aAddon.matchingTargetApplication) return false; - - // XXX Temporary solution to let applications opt-in to make themes safer - // following significant UI changes even if checkCompatibility=false has - // been set, until we get bug 962001. - if (aAddon.type == "theme" && app.id == Services.appinfo.ID) { - try { - let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE); - if (minCompatVersion && - Services.vc.compare(minCompatVersion, app.maxVersion) > 0) { - return false; - } - } catch (e) {} - } } return true; @@ -1059,37 +1056,36 @@ function loadManifestFromDir(aDir) { * @throws if the XPI file does not contain a valid install manifest. * Throws with |webext:true| if a WebExtension manifest was found * to distinguish between WebExtensions and corrupt files. + * Throws with |jetpacksdk:true| if a Jetpack files were found + * if Jetpack its self isn't built. */ function loadManifestFromZipReader(aZipReader) { - let zis; - try { - zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST); - } catch (e) { - // We're going to throw here, but depending on whether we have a - // WebExtension manifest in the XPI, we'll throw with the webext flag. - try { - let zws = aZipReader.getInputStream(FILE_WEBEXT_MANIFEST); - zws.close(); - } catch(e2) { - // We have neither an install manifest nor a WebExtension manifest; - // this means the extension file has a structural problem. - // Just pass the original error up the chain in that case. + // If WebExtension but not install.rdf throw an error + if (aZipReader.hasEntry(FILE_WEBEXT_MANIFEST)) { + if (!aZipReader.hasEntry(FILE_INSTALL_MANIFEST)) { throw { - name: e.name, - message: e.message + name: "UnsupportedExtension", + message: Services.appinfo.name + " does not support WebExtensions", + webext: true }; } - // If we get here, we have a WebExtension manifest but no install - // manifest. Pass the error up the chain with the webext flag. + } + +#ifndef MOZ_JETPACK + // If Jetpack is not built throw an error + if (aZipReader.hasEntry(FILE_JETPACK_MANIFEST_1) || + aZipReader.hasEntry(FILE_JETPACK_MANIFEST_2)) { throw { - name: e.name, - message: e.message, - webext: true + name: "UnsupportedExtension", + message: Services.appinfo.name + " does not support Jetpack Extensions", + jetpacksdk: true }; } - - // We found an install manifest, so it's either a regular or hybrid - // extension. Continue processing. +#endif + + // Attempt to open install.rdf else throw normally + let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST); + // Create a buffered input stream for install.rdf let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. createInstance(Ci.nsIBufferedInputStream); bis.init(zis, 4096); @@ -1118,7 +1114,9 @@ function loadManifestFromZipReader(aZipReader) { return addon; } finally { + // Close the buffered input stream bis.close(); + // Close the input stream to install.rdf zis.close(); } } @@ -1842,8 +1840,10 @@ this.XPIProvider = { _enabledExperiments: null, // A Map from an add-on install to its ID _addonFileMap: new Map(), +#ifdef MOZ_DEVTOOLS // Flag to know if ToolboxProcess.jsm has already been loaded by someone or not _toolboxProcessLoaded: false, +#endif // Have we started shutting down bootstrap add-ons? _closing: false, @@ -1887,8 +1887,7 @@ this.XPIProvider = { logger.info("Mapping " + aID + " to " + aFile.path); this._addonFileMap.set(aID, aFile.path); - let service = Cc["@mozilla.org/addon-path-service;1"].getService(Ci.amIAddonPathService); - service.insertPath(aFile.path, aID); + AddonPathService.insertPath(aFile.path, aID); }, /** @@ -1931,12 +1930,10 @@ this.XPIProvider = { let chan; try { - chan = Services.io.newChannelFromURI2(aURI, - null, // aLoadingNode - Services.scriptSecurityManager.getSystemPrincipal(), - null, // aTriggeringPrincipal - Ci.nsILoadInfo.SEC_NORMAL, - Ci.nsIContentPolicy.TYPE_OTHER); + chan = NetUtil.newChannel({ + uri: aURI, + loadUsingSystemPrincipal: true + }); } catch (ex) { return null; @@ -2082,6 +2079,8 @@ this.XPIProvider = { Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false); Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false); Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false); + +#ifdef MOZ_DEVTOOLS if (Cu.isModuleLoaded("resource://devtools/client/framework/ToolboxProcess.jsm")) { // If BrowserToolboxProcess is already loaded, set the boolean to true // and do whatever is needed @@ -2093,6 +2092,7 @@ this.XPIProvider = { // Else, wait for it to load Services.obs.addObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false); } +#endif let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion, aOldPlatformVersion); @@ -3918,16 +3918,8 @@ this.XPIProvider = { * @see amIAddonManager.mapURIToAddonID */ mapURIToAddonID: function XPI_mapURIToAddonID(aURI) { - let resolved = this._resolveURIToFile(aURI); - if (!resolved || !(resolved instanceof Ci.nsIFileURL)) - return null; - - for (let [id, path] of this._addonFileMap) { - if (resolved.file.path.startsWith(path)) - return id; - } - - return null; + // Returns `null` instead of empty string if the URI can't be mapped. + return AddonPathService.mapURIToAddonId(aURI) || null; }, /** @@ -4095,12 +4087,14 @@ this.XPIProvider = { } return; } +#ifdef MOZ_DEVTOOLS else if (aTopic == NOTIFICATION_TOOLBOXPROCESS_LOADED) { Services.obs.removeObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false); this._toolboxProcessLoaded = true; BrowserToolboxProcess.on("connectionchange", this.onDebugConnectionChange.bind(this)); } +#endif if (aTopic == "nsPref:changed") { switch (aData) { @@ -4365,12 +4359,14 @@ this.XPIProvider = { logger.warn("Error loading bootstrap.js for " + aId, e); } +#ifdef MOZ_DEVTOOLS // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been // initialized as otherwise, when it will be initialized, all addons' // globals will be added anyways if (this._toolboxProcessLoaded) { BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] }); } +#endif }, /** @@ -4390,11 +4386,13 @@ this.XPIProvider = { this.persistBootstrappedAddons(); this.addAddonsToCrashReporter(); +#ifdef MOZ_DEVTOOLS // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been // initialized as otherwise, there won't be any addon globals added to it if (this._toolboxProcessLoaded) { BrowserToolboxProcess.setAddonOptions(aId, { global: null }); } +#endif }, /** @@ -4438,7 +4436,18 @@ this.XPIProvider = { if (aAddon.type == "locale") return; - if (!(aMethod in this.bootstrapScopes[aAddon.id])) { + let method = undefined; + try { + method = Components.utils.evalInSandbox(`${aMethod};`, + this.bootstrapScopes[aAddon.id], + "ECMAv5"); + } + catch (e) { + // An exception will be caught if the expected method is not defined. + // That will be logged below. + } + + if (!method) { logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod); return; } @@ -4457,9 +4466,9 @@ this.XPIProvider = { } logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " + - aAddon.version); + aAddon.version); try { - this.bootstrapScopes[aAddon.id][aMethod](params, aReason); + method(params, aReason); } catch (e) { logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e); @@ -5001,6 +5010,11 @@ AddonInstall.prototype = { if (e.webext) { logger.warn("WebExtension XPI", e); this.error = AddonManager.ERROR_WEBEXT_FILE; +#ifndef MOZ_JETPACK + } else if (e.jetpacksdk) { + logger.warn("Jetpack XPI", e); + this.error = AddonManager.ERROR_JETPACKSDK_FILE; +#endif } else { logger.warn("Invalid XPI", e); this.error = AddonManager.ERROR_CORRUPT_FILE; @@ -5456,21 +5470,17 @@ AddonInstall.prototype = { let requireBuiltIn = Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true); this.badCertHandler = new BadCertHandler(!requireBuiltIn); - this.channel = NetUtil.newChannel2(this.sourceURI, - null, - null, - null, // aLoadingNode - Services.scriptSecurityManager.getSystemPrincipal(), - null, // aTriggeringPrincipal - Ci.nsILoadInfo.SEC_NORMAL, - Ci.nsIContentPolicy.TYPE_OTHER); + this.channel = NetUtil.newChannel({ + uri: this.sourceURI, + loadUsingSystemPrincipal: true + }); this.channel.notificationCallbacks = this; if (this.channel instanceof Ci.nsIHttpChannel) { this.channel.setRequestHeader("Moz-XPI-Update", "1", true); if (this.channel instanceof Ci.nsIHttpChannelInternal) this.channel.forceAllowThirdPartyCookie = true; } - this.channel.asyncOpen(listener, null); + this.channel.asyncOpen2(listener); Services.obs.addObserver(this, "network:offline-about-to-go-offline", false); } @@ -5646,6 +5656,10 @@ AddonInstall.prototype = { catch (e) { if (e.webext) { this.downloadFailed(AddonManager.ERROR_WEBEXT_FILE, e); +#ifndef MOZ_JETPACK + } else if (e.jetpacksdk) { + this.downloadFailed(AddonManager.ERROR_JETPACKSDK_FILE, e); +#endif } else { this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e); } @@ -6442,21 +6456,23 @@ AddonInternal.prototype = { if (!aPlatformVersion) aPlatformVersion = Services.appinfo.platformVersion; +#ifdef MOZ_PHOENIX_EXTENSIONS this.native = false; - +#endif + let version; if (app.id == Services.appinfo.ID) { version = aAppVersion; +#ifdef MOZ_PHOENIX_EXTENSIONS this.native = true; } -#ifdef MOZ_PHOENIX_EXTENSIONS else if (app.id == FIREFOX_ID) { version = FIREFOX_APPCOMPATVERSION; if (this.type == "locale") //Never allow language packs in Firefox compatibility mode return false; - } #endif + } else if (app.id == TOOLKIT_ID) version = aPlatformVersion @@ -7822,7 +7838,7 @@ WinRegInstallLocation.prototype = { }; #endif -let addonTypes = [ +var addonTypes = [ new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS, STRING_TYPE_NAME, AddonManager.VIEW_TYPE_LIST, 4000, diff --git a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js index 2cef907f1..f2420bd1a 100644 --- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js +++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js @@ -4,10 +4,10 @@ "use strict"; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; +var Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); @@ -29,7 +29,7 @@ const LOGGER_ID = "addons.xpi-utils"; // Create a new logger for use by the Addons XPI Provider Utils // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); const KEY_PROFILEDIR = "ProfD"; const FILE_DATABASE = "extensions.sqlite"; @@ -70,7 +70,11 @@ const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type", "skinnable", "size", "sourceURI", "releaseNotesURI", "softDisabled", "foreignInstall", "hasBinaryComponents", "strictCompatibility", "locales", "targetApplications", - "targetPlatforms", "multiprocessCompatible", "native"]; + "targetPlatforms", "multiprocessCompatible", +#ifdef MOZ_PHOENIX_EXTENSIONS + "native" +#endif + ]; // Time to wait before async save of XPI JSON database, in milliseconds const ASYNC_SAVE_DELAY_MS = 20; diff --git a/toolkit/mozapps/extensions/internal/moz.build b/toolkit/mozapps/extensions/internal/moz.build index efcd2af21..337df3104 100644 --- a/toolkit/mozapps/extensions/internal/moz.build +++ b/toolkit/mozapps/extensions/internal/moz.build @@ -5,12 +5,12 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXTRA_JS_MODULES.addons += [ - '../../webextensions/internal/ProductAddonChecker.jsm', 'AddonLogging.jsm', 'AddonRepository_SQLiteMigrator.jsm', 'Content.js', 'GMPProvider.jsm', 'LightweightThemeImageOptimizer.jsm', + 'ProductAddonChecker.jsm', 'SpellCheckDictionaryBootstrap.js', ] |