diff --git a/dom/locales/en-US/chrome/dom/dom.properties b/dom/locales/en-US/chrome/dom/dom.properties
index 07b9fdc8afc8..c8d083aa6b09 100644
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -250,6 +250,12 @@ ManifestInvalidCSSColor=%1$S: %2$S is not a valid CSS color.
ManifestLangIsInvalid=%1$S: %2$S is not a valid language code.
# LOCALIZATION NOTE: %1$S is the name of the parent property whose value is invalid (e.g., "icons"). %2$S is the index of the image object that is invalid (from 0). %3$S is the name of actual member that is invalid. %4$S is the invalid value. E.g. "icons item at index 2 is invalid. The src member is an invalid URL http://:Invalid"
ManifestImageURLIsInvalid=%1$S item at index %2$S is invalid. The %3$S member is an invalid URL %4$S
+# LOCALIZATION NOTE: %1$S is the name of the parent property that that contains the unusable image object (e.g., "icons"). %2$S is the index of the image object that is unusable (from 0). E.g. "icons item at index 2 lacks a usable purpose. It will be ignored."
+ManifestImageUnusable=%1$S item at index %2$S lacks a usable purpose. It will be ignored.
+# LOCALIZATION NOTE: %1$S is the name of the parent property that contains the unsupported value (e.g., "icons"). %2$S is the index of the image object that has the unsupported value (from 0). %3$S are the unknown purposes. E.g. "icons item at index 2 includes unsupported purpose(s): a b."
+ManifestImageUnsupportedPurposes=%1$S item at index %2$S includes unsupported purpose(s): %3$S.
+# LOCALIZATION NOTE: %1$S is the name of the parent property that has a repeated purpose (e.g., "icons"). %2$S is the index of the image object that has the repeated purpose (from 0). %3$S is the repeated purposes. E.g. "icons item at index 2 includes repeated purpose(s): a b."
+ManifestImageRepeatedPurposes=%1$S item at index %2$S includes repeated purpose(s): %3$S.
PatternAttributeCompileFailure=Unable to check because the pattern is not a valid regexp: %S
# LOCALIZATION NOTE: Do not translate "postMessage" or DOMWindow. %S values are origins, like https://domain.com:port
TargetPrincipalDoesNotMatch=Failed to execute ‘postMessage’ on ‘DOMWindow’: The target origin provided (‘%S’) does not match the recipient window’s origin (‘%S’).
diff --git a/dom/manifest/ImageObjectProcessor.jsm b/dom/manifest/ImageObjectProcessor.jsm
index b9b0961987fb..323096cd5642 100644
--- a/dom/manifest/ImageObjectProcessor.jsm
+++ b/dom/manifest/ImageObjectProcessor.jsm
@@ -34,6 +34,8 @@ function ImageObjectProcessor(aErrors, aExtractor, aBundle) {
this.domBundle = aBundle;
}
+const iconPurposes = Object.freeze(["any", "maskable"]);
+
// Static getters
Object.defineProperties(ImageObjectProcessor, {
decimals: {
@@ -64,20 +66,111 @@ ImageObjectProcessor.prototype.process = function(
const images = [];
const value = extractor.extractValue(spec);
if (Array.isArray(value)) {
- // Filter out images whose "src" is not useful.
value
- .filter((item, index) => !!processSrcMember(item, aBaseURL, index))
.map(toImageObject)
+ // Filter out images that resulted in "failure", per spec.
+ .filter(image => image)
.forEach(image => images.push(image));
}
return images;
- function toImageObject(aImageSpec) {
- return {
- src: processSrcMember(aImageSpec, aBaseURL),
- type: processTypeMember(aImageSpec),
- sizes: processSizesMember(aImageSpec),
+ function toImageObject(aImageSpec, index) {
+ let img; // if "failure" happens below, we return undefined.
+ try {
+ // can throw
+ const src = processSrcMember(aImageSpec, aBaseURL, index);
+ // can throw
+ const purpose = processPurposeMember(aImageSpec, index);
+ const type = processTypeMember(aImageSpec);
+ const sizes = processSizesMember(aImageSpec);
+ img = {
+ src,
+ purpose,
+ type,
+ sizes,
+ };
+ } catch (err) {
+ /* Errors are collected by each process* function */
+ }
+ return img;
+ }
+
+ function processPurposeMember(aImage, index) {
+ const spec = {
+ objectName: "image",
+ object: aImage,
+ property: "purpose",
+ expectedType: "string",
+ trim: true,
+ throwTypeError: true,
};
+
+ // Type errors are treated at "any"...
+ let value;
+ try {
+ value = extractor.extractValue(spec);
+ } catch (err) {
+ return ["any"];
+ }
+
+ // Was only whitespace...
+ if (!value) {
+ return ["any"];
+ }
+
+ const keywords = value.split(/\s+/);
+
+ // Emtpy is treated as "any"...
+ if (keywords.length === 0) {
+ return ["any"];
+ }
+
+ // We iterate over keywords and classify them into:
+ const purposes = new Set();
+ const unknownPurposes = new Set();
+ const repeatedPurposes = new Set();
+
+ for (const keyword of keywords) {
+ const canonicalKeyword = keyword.toLowerCase();
+
+ if (purposes.has(canonicalKeyword)) {
+ repeatedPurposes.add(keyword);
+ continue;
+ }
+
+ iconPurposes.includes(canonicalKeyword)
+ ? purposes.add(canonicalKeyword)
+ : unknownPurposes.add(keyword);
+ }
+
+ // Tell developer about unknown purposes...
+ if (unknownPurposes.size) {
+ const warn = domBundle.formatStringFromName(
+ "ManifestImageUnsupportedPurposes",
+ [aMemberName, index, [...unknownPurposes].join(" ")]
+ );
+ errors.push({ warn });
+ }
+
+ // Tell developer about repeated purposes...
+ if (repeatedPurposes.size) {
+ const warn = domBundle.formatStringFromName(
+ "ManifestImageRepeatedPurposes",
+ [aMemberName, index, [...repeatedPurposes].join(" ")]
+ );
+ errors.push({ warn });
+ }
+
+ if (purposes.size === 0) {
+ const warn = domBundle.formatStringFromName("ManifestImageUnusable", [
+ aMemberName,
+ index,
+ ]);
+ errors.push({ warn });
+ throw new TypeError(warn);
+ }
+
+ return [...purposes];
}
function processTypeMember(aImage) {
@@ -108,9 +201,15 @@ ImageObjectProcessor.prototype.process = function(
property: "src",
expectedType: "string",
trim: false,
+ throwTypeError: true,
};
const value = extractor.extractValue(spec);
let url;
+ if (typeof value === "undefined" || value === "") {
+ // We throw here as the value is unusable,
+ // but it's not an developer error.
+ throw new TypeError();
+ }
if (value && value.length) {
try {
url = new URL(value, aBaseURL).href;
@@ -120,6 +219,7 @@ ImageObjectProcessor.prototype.process = function(
[aMemberName, index, "src", value]
);
errors.push({ warn });
+ throw e;
}
}
return url;
diff --git a/dom/manifest/ManifestObtainer.jsm b/dom/manifest/ManifestObtainer.jsm
index 96cabd64732e..40a026cd51b1 100644
--- a/dom/manifest/ManifestObtainer.jsm
+++ b/dom/manifest/ManifestObtainer.jsm
@@ -69,14 +69,18 @@ var ManifestObtainer = {
* Adds proprietary moz_* members to manifest.
* @return {Promise