diff options
Diffstat (limited to 'toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm')
-rw-r--r-- | toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm | 335 |
1 files changed, 87 insertions, 248 deletions
diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm index 63c16737c..939e2e269 100644 --- a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm +++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm @@ -22,6 +22,9 @@ 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 PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts"; @@ -31,16 +34,11 @@ 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() { +XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() { let certUtils = {}; Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils); return certUtils; @@ -82,7 +80,7 @@ RDFSerializer.prototype = { * @return a string with all characters invalid in XML character data * converted to entity references. */ - escapeEntities: function(aString) { + escapeEntities: function RDFS_escapeEntities(aString) { aString = aString.replace(/&/g, "&"); aString = aString.replace(/</g, "<"); aString = aString.replace(/>/g, ">"); @@ -100,7 +98,8 @@ RDFSerializer.prototype = { * The current level of indent for pretty-printing * @return a string containing the serialized elements. */ - serializeContainerItems: function(aDs, aContainer, aIndent) { + serializeContainerItems: function RDFS_serializeContainerItems(aDs, aContainer, + aIndent) { var result = ""; var items = aContainer.GetElements(); while (items.hasMoreElements()) { @@ -126,7 +125,9 @@ RDFSerializer.prototype = { * @return a string containing the serialized properties. * @throws if the resource contains a property that cannot be serialized */ - serializeResourceProperties: function(aDs, aResource, aIndent) { + serializeResourceProperties: function RDFS_serializeResourceProperties(aDs, + aResource, + aIndent) { var result = ""; var items = []; var arcs = aDs.ArcLabelsOut(aResource); @@ -180,7 +181,7 @@ RDFSerializer.prototype = { * @return a string containing the serialized resource. * @throws if the RDF data contains multiple references to the same resource. */ - serializeResource: function(aDs, aResource, aIndent) { + serializeResource: function RDFS_serializeResource(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); @@ -220,48 +221,6 @@ 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 @@ -270,17 +229,10 @@ function sanitizeUpdateURL(aUpdate, aRequest, aHashPattern, aHashString) { * 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, aManifestData) { - if (aManifestData.documentElement.namespaceURI != PREFIX_NS_RDF) { - throw Components.Exception("Update manifest had an unrecognised namespace: " + - aManifestData.documentElement.namespaceURI); - } - +function parseRDFManifest(aId, aUpdateKey, aRequest) { function EM_R(aProp) { return gRDF.GetResource(PREFIX_NS_EM + aProp); } @@ -323,13 +275,9 @@ function parseRDFManifest(aId, aUpdateKey, aRequest, aManifestData) { let extensionRes = gRDF.GetResource(PREFIX_EXTENSION + aId); let themeRes = gRDF.GetResource(PREFIX_THEME + aId); let itemRes = gRDF.GetResource(PREFIX_ITEM + aId); - let addonRes; - if (ds.ArcLabelsOut(extensionRes).hasMoreElements()) - addonRes = extensionRes; - else if (ds.ArcLabelsOut(themeRes).hasMoreElements()) - addonRes = themeRes; - else - addonRes = itemRes; + let addonRes = ds.ArcLabelsOut(extensionRes).hasMoreElements() ? extensionRes + : ds.ArcLabelsOut(themeRes).hasMoreElements() ? themeRes + : itemRes; // If we have an update key then the update manifest must be signed if (aUpdateKey) { @@ -421,134 +369,16 @@ function parseRDFManifest(aId, aUpdateKey, aRequest, aManifestData) { targetApplications: [appEntry] }; - // 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); - } - } - return results; -} - -/** - * 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 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 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 manifest is missing a required ${aProperty} property.`); - return value; - } - - let manifest = aManifestData; - - if (!TYPE_CHECK["object"](manifest)) - throw Components.Exception("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 manifest did not contain an entry for " + aId); - return []; - } - - // 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(`Found an update entry for ${aId} version ${version}`); - - let applications = getProperty(update, "applications", "object", - { gecko: {} }); - - // "gecko" is currently the only supported application entry. If - // it's missing, skip this update. - if (!("gecko" in applications)) { - logger.debug("gecko not in application entry, skipping update of ${addon}") - continue; - } - - let app = getProperty(applications, "gecko", "object"); - - let appEntry = { - id: TOOLKIT_ID, - minVersion: getProperty(app, "strict_min_version", "string", - AddonManagerPrivate.webExtensionsMinPlatformVersion), - maxVersion: "*", - }; - - let result = { - id: aId, - version: version, - multiprocessCompatible: getProperty(update, "multiprocess_compatible", "boolean", true), - updateURL: getProperty(update, "update_link", "string"), - updateHash: getProperty(update, "update_hash", "string"), - updateInfoURL: getProperty(update, "update_info_url", "string"), - strictCompatibility: false, - targetApplications: [appEntry], - }; - - if ("strict_max_version" in app) { - if ("advisory_max_version" in app) { - logger.warn("Ignoring 'advisory_max_version' update manifest property for " + - aId + " property since 'strict_max_version' also present"); + 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; } - - appEntry.maxVersion = getProperty(app, "strict_max_version", "string"); - result.strictCompatibility = appEntry.maxVersion != "*"; - } else if ("advisory_max_version" in app) { - appEntry.maxVersion = getProperty(app, "advisory_max_version", "string"); + results.push(result); } - - // 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; } @@ -581,18 +411,20 @@ function UpdateParser(aId, aUpdateKey, aUrl, aObserver) { logger.debug("Requesting " + aUrl); try { - this.request = new ServiceRequest(); + this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); 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/plain"); + this.request.overrideMimeType("text/xml"); this.request.setRequestHeader("Moz-XPI-Update", "1", true); this.request.timeout = TIMEOUT; - this.request.addEventListener("load", () => this.onLoad(), false); - this.request.addEventListener("error", () => this.onError(), false); - this.request.addEventListener("timeout", () => this.onTimeout(), false); + 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.send(null); } catch (e) { @@ -610,7 +442,7 @@ UpdateParser.prototype = { /** * Called when the manifest has been successfully loaded. */ - onLoad: function() { + onLoad: function UP_onLoad() { let request = this.request; this.request = null; this._doneAt = new Error("place holder"); @@ -626,7 +458,6 @@ UpdateParser.prototype = { CertUtils.checkCert(request.channel, !requireBuiltIn); } catch (e) { - logger.warn("Request failed: " + this.url + " - " + e); this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR); return; } @@ -645,52 +476,41 @@ UpdateParser.prototype = { return; } - // 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; - } - - let results; - try { - results = parser(); - } - catch (e) { - logger.warn("onUpdateCheckComplete failed to parse update manifest", e); + 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); return; } - if ("onUpdateCheckComplete" in this.observer) { + // We currently only know about RDF update manifests + if (xml.documentElement.namespaceURI == PREFIX_NS_RDF) { + let results = null; + try { - this.observer.onUpdateCheckComplete(results); + results = parseRDFManifest(this.id, this.updateKey, request); } catch (e) { - logger.warn("onUpdateCheckComplete notification failed", 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")); + } + return; } - else { - logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker")); - } + + logger.warn("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI); + this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT); }, /** @@ -706,7 +526,7 @@ UpdateParser.prototype = { /** * Called when the manifest failed to load. */ - onError: function() { + onError: function UP_onError() { if (!Components.isSuccessCode(this.request.status)) { logger.warn("Request failed: " + this.url + " - " + this.request.status); } @@ -735,7 +555,7 @@ UpdateParser.prototype = { /** * Helper method to notify the observer that an error occured. */ - notifyError: function(aStatus) { + notifyError: function UP_notifyError(aStatus) { if ("onUpdateCheckError" in this.observer) { try { this.observer.onUpdateCheckError(aStatus); @@ -749,7 +569,7 @@ UpdateParser.prototype = { /** * Called to cancel an in-progress update check. */ - cancel: function() { + cancel: function UP_cancel() { if (!this.request) { logger.error("Trying to cancel already-complete request", this._doneAt); return; @@ -799,6 +619,12 @@ function matchesVersions(aUpdate, aAppVersion, aPlatformVersion, return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) && (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0)); } +#ifdef MOZ_PHOENIX_EXTENSIONS + if (app.id == FIREFOX_ID) { + return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) && + (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0)); + } +#endif if (app.id == TOOLKIT_ID) { result = (Services.vc.compare(aPlatformVersion, app.minVersion) >= 0) && (aIgnoreMaxVersion || (Services.vc.compare(aPlatformVersion, app.maxVersion) <= 0)); @@ -843,9 +669,12 @@ this.AddonUpdateChecker = { * Ignore strictCompatibility when testing if an update matches. Optional. * @return an update object if one matches or null if not */ - getCompatibilityUpdate: function(aUpdates, aVersion, aIgnoreCompatibility, - aAppVersion, aPlatformVersion, - aIgnoreMaxVersion, aIgnoreStrictCompat) { + getCompatibilityUpdate: function AUC_getCompatibilityUpdate(aUpdates, aVersion, + aIgnoreCompatibility, + aAppVersion, + aPlatformVersion, + aIgnoreMaxVersion, + aIgnoreStrictCompat) { if (!aAppVersion) aAppVersion = Services.appinfo.version; if (!aPlatformVersion) @@ -856,7 +685,12 @@ this.AddonUpdateChecker = { if (aIgnoreCompatibility) { 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) +#else if (id == Services.appinfo.ID || id == TOOLKIT_ID) +#endif return update; } } @@ -886,9 +720,12 @@ this.AddonUpdateChecker = { * Array of AddonCompatibilityOverride to take into account. Optional. * @return an update object if one matches or null if not */ - getNewestCompatibleUpdate: function(aUpdates, aAppVersion, aPlatformVersion, - aIgnoreMaxVersion, aIgnoreStrictCompat, - aCompatOverrides) { + getNewestCompatibleUpdate: function AUC_getNewestCompatibleUpdate(aUpdates, + aAppVersion, + aPlatformVersion, + aIgnoreMaxVersion, + aIgnoreStrictCompat, + aCompatOverrides) { if (!aAppVersion) aAppVersion = Services.appinfo.version; if (!aPlatformVersion) @@ -928,7 +765,9 @@ this.AddonUpdateChecker = { * @return UpdateParser so that the caller can use UpdateParser.cancel() to shut * down in-progress update requests */ - checkForUpdates: function(aId, aUpdateKey, aUrl, aObserver) { - return new UpdateParser(aId, aUpdateKey, aUrl, aObserver); + checkForUpdates: function AUC_checkForUpdates(aId, aUpdateKey, aUrl, aObserver) { + // Exclude default theme + if (aId != "{972ce4c6-7e08-4474-a285-3208198ce6fd}") + return new UpdateParser(aId, aUpdateKey, aUrl, aObserver); } }; |