diff options
Diffstat (limited to 'dom/manifest/ImageObjectProcessor.sys.mjs')
-rw-r--r-- | dom/manifest/ImageObjectProcessor.sys.mjs | 250 |
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; + } + } +}; |