1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
6 * Implementation of Image Object processing algorithms from:
7 * http://www.w3.org/TR/appmanifest/#image-object-and-its-members
9 * This is intended to be used in conjunction with ManifestProcessor.sys.mjs
11 * Creates an object to process Image Objects as defined by the
12 * W3C specification. This is used to process things like the
13 * icon member and the splash_screen member.
17 * .process(aManifest, aBaseURL, aMemberName);
21 export function ImageObjectProcessor(aErrors, aExtractor, aBundle) {
22 this.errors = aErrors;
23 this.extractor = aExtractor;
24 this.domBundle = aBundle;
27 const iconPurposes = Object.freeze(["any", "maskable", "monochrome"]);
30 Object.defineProperties(ImageObjectProcessor, {
38 return new RegExp("any", "i");
43 ImageObjectProcessor.prototype.process = function (
49 objectName: "manifest",
51 property: aMemberName,
52 expectedType: "array",
55 const { domBundle, extractor, errors } = this;
57 const value = extractor.extractValue(spec);
58 if (Array.isArray(value)) {
61 // Filter out images that resulted in "failure", per spec.
62 .filter(image => image)
63 .forEach(image => images.push(image));
67 function toImageObject(aImageSpec, index) {
68 let img; // if "failure" happens below, we return undefined.
71 const src = processSrcMember(aImageSpec, aBaseURL, index);
73 const purpose = processPurposeMember(aImageSpec, index);
74 const type = processTypeMember(aImageSpec);
75 const sizes = processSizesMember(aImageSpec);
83 /* Errors are collected by each process* function */
88 function processPurposeMember(aImage, index) {
93 expectedType: "string",
98 // Type errors are treated at "any"...
101 value = extractor.extractValue(spec);
106 // Was only whitespace...
111 const keywords = value.split(/\s+/);
113 // Emtpy is treated as "any"...
114 if (keywords.length === 0) {
118 // We iterate over keywords and classify them into:
119 const purposes = new Set();
120 const unknownPurposes = new Set();
121 const repeatedPurposes = new Set();
123 for (const keyword of keywords) {
124 const canonicalKeyword = keyword.toLowerCase();
126 if (purposes.has(canonicalKeyword)) {
127 repeatedPurposes.add(keyword);
131 iconPurposes.includes(canonicalKeyword)
132 ? purposes.add(canonicalKeyword)
133 : unknownPurposes.add(keyword);
136 // Tell developer about unknown purposes...
137 if (unknownPurposes.size) {
138 const warn = domBundle.formatStringFromName(
139 "ManifestImageUnsupportedPurposes",
140 [aMemberName, index, [...unknownPurposes].join(" ")]
142 errors.push({ warn });
145 // Tell developer about repeated purposes...
146 if (repeatedPurposes.size) {
147 const warn = domBundle.formatStringFromName(
148 "ManifestImageRepeatedPurposes",
149 [aMemberName, index, [...repeatedPurposes].join(" ")]
151 errors.push({ warn });
154 if (purposes.size === 0) {
155 const warn = domBundle.formatStringFromName("ManifestImageUnusable", [
159 errors.push({ warn });
160 throw new TypeError(warn);
163 return [...purposes];
166 function processTypeMember(aImage) {
168 const hadCharset = {};
173 expectedType: "string",
176 let value = extractor.extractValue(spec);
178 value = Services.io.parseRequestContentType(value, charset, hadCharset);
180 return value || undefined;
183 function processSrcMember(aImage, aBaseURL, index) {
185 objectName: aMemberName,
188 expectedType: "string",
190 throwTypeError: true,
192 const value = extractor.extractValue(spec);
194 if (typeof value === "undefined" || value === "") {
195 // We throw here as the value is unusable,
196 // but it's not an developer error.
197 throw new TypeError();
199 if (value && value.length) {
201 url = new URL(value, aBaseURL).href;
203 const warn = domBundle.formatStringFromName(
204 "ManifestImageURLIsInvalid",
205 [aMemberName, index, "src", value]
207 errors.push({ warn });
214 function processSizesMember(aImage) {
215 const sizes = new Set();
220 expectedType: "string",
223 const value = extractor.extractValue(spec);
225 // Split on whitespace and filter out invalid values.
228 .filter(isValidSizeValue)
229 .reduce((collector, size) => collector.add(size), sizes);
231 return sizes.size ? Array.from(sizes) : undefined;
232 // Implementation of HTML's link@size attribute checker.
233 function isValidSizeValue(aSize) {
234 const size = aSize.toLowerCase();
235 if (ImageObjectProcessor.anyRegEx.test(aSize)) {
238 if (!size.includes("x") || size.indexOf("x") !== size.lastIndexOf("x")) {
241 // Split left of x for width, after x for height.
242 const widthAndHeight = size.split("x");
243 const w = widthAndHeight.shift();
244 const h = widthAndHeight.join("x");
245 const validStarts = !w.startsWith("0") && !h.startsWith("0");
246 const validDecimals = ImageObjectProcessor.decimals.test(w + h);
247 return validStarts && validDecimals;