summaryrefslogtreecommitdiffstats
path: root/dom/manifest/ImageObjectProcessor.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'dom/manifest/ImageObjectProcessor.sys.mjs')
-rw-r--r--dom/manifest/ImageObjectProcessor.sys.mjs250
1 files changed, 250 insertions, 0 deletions
diff --git a/dom/manifest/ImageObjectProcessor.sys.mjs b/dom/manifest/ImageObjectProcessor.sys.mjs
new file mode 100644
index 0000000000..f72c5ff9fe
--- /dev/null
+++ b/dom/manifest/ImageObjectProcessor.sys.mjs
@@ -0,0 +1,250 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+/*
+ * ImageObjectProcessor
+ * Implementation of Image Object processing algorithms from:
+ * http://www.w3.org/TR/appmanifest/#image-object-and-its-members
+ *
+ * This is intended to be used in conjunction with ManifestProcessor.jsm
+ *
+ * Creates an object to process Image Objects as defined by the
+ * W3C specification. This is used to process things like the
+ * icon member and the splash_screen member.
+ *
+ * Usage:
+ *
+ * .process(aManifest, aBaseURL, aMemberName);
+ *
+ */
+
+export function ImageObjectProcessor(aErrors, aExtractor, aBundle) {
+ this.errors = aErrors;
+ this.extractor = aExtractor;
+ this.domBundle = aBundle;
+}
+
+const iconPurposes = Object.freeze(["any", "maskable", "monochrome"]);
+
+// Static getters
+Object.defineProperties(ImageObjectProcessor, {
+ decimals: {
+ get() {
+ return /^\d+$/;
+ },
+ },
+ anyRegEx: {
+ get() {
+ return new RegExp("any", "i");
+ },
+ },
+});
+
+ImageObjectProcessor.prototype.process = function (
+ aManifest,
+ aBaseURL,
+ aMemberName
+) {
+ const spec = {
+ objectName: "manifest",
+ object: aManifest,
+ property: aMemberName,
+ expectedType: "array",
+ trim: false,
+ };
+ const { domBundle, extractor, errors } = this;
+ const images = [];
+ const value = extractor.extractValue(spec);
+ if (Array.isArray(value)) {
+ value
+ .map(toImageObject)
+ // Filter out images that resulted in "failure", per spec.
+ .filter(image => image)
+ .forEach(image => images.push(image));
+ }
+ return images;
+
+ 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) {
+ const charset = {};
+ const hadCharset = {};
+ const spec = {
+ objectName: "image",
+ object: aImage,
+ property: "type",
+ expectedType: "string",
+ trim: true,
+ };
+ let value = extractor.extractValue(spec);
+ if (value) {
+ value = Services.io.parseRequestContentType(value, charset, hadCharset);
+ }
+ return value || undefined;
+ }
+
+ function processSrcMember(aImage, aBaseURL, index) {
+ const spec = {
+ objectName: aMemberName,
+ object: aImage,
+ 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;
+ } catch (e) {
+ const warn = domBundle.formatStringFromName(
+ "ManifestImageURLIsInvalid",
+ [aMemberName, index, "src", value]
+ );
+ errors.push({ warn });
+ throw e;
+ }
+ }
+ return url;
+ }
+
+ function processSizesMember(aImage) {
+ const sizes = new Set();
+ const spec = {
+ objectName: "image",
+ object: aImage,
+ property: "sizes",
+ expectedType: "string",
+ trim: true,
+ };
+ const value = extractor.extractValue(spec);
+ if (value) {
+ // Split on whitespace and filter out invalid values.
+ value
+ .split(/\s+/)
+ .filter(isValidSizeValue)
+ .reduce((collector, size) => collector.add(size), sizes);
+ }
+ return sizes.size ? Array.from(sizes) : undefined;
+ // Implementation of HTML's link@size attribute checker.
+ function isValidSizeValue(aSize) {
+ const size = aSize.toLowerCase();
+ if (ImageObjectProcessor.anyRegEx.test(aSize)) {
+ return true;
+ }
+ if (!size.includes("x") || size.indexOf("x") !== size.lastIndexOf("x")) {
+ return false;
+ }
+ // Split left of x for width, after x for height.
+ const widthAndHeight = size.split("x");
+ const w = widthAndHeight.shift();
+ const h = widthAndHeight.join("x");
+ const validStarts = !w.startsWith("0") && !h.startsWith("0");
+ const validDecimals = ImageObjectProcessor.decimals.test(w + h);
+ return validStarts && validDecimals;
+ }
+ }
+};