ImageObjectProcessor.sys.mjs (6698B)
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/. */ 4 /* 5 * ImageObjectProcessor 6 * Implementation of Image Object processing algorithms from: 7 * http://www.w3.org/TR/appmanifest/#image-object-and-its-members 8 * 9 * This is intended to be used in conjunction with ManifestProcessor.sys.mjs 10 * 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. 14 * 15 * Usage: 16 * 17 * .process(aManifest, aBaseURL, aMemberName); 18 * 19 */ 20 21 export function ImageObjectProcessor(aErrors, aExtractor, aBundle) { 22 this.errors = aErrors; 23 this.extractor = aExtractor; 24 this.domBundle = aBundle; 25 } 26 27 const iconPurposes = Object.freeze(["any", "maskable", "monochrome"]); 28 29 // Static getters 30 Object.defineProperties(ImageObjectProcessor, { 31 decimals: { 32 get() { 33 return /^\d+$/; 34 }, 35 }, 36 anyRegEx: { 37 get() { 38 return new RegExp("any", "i"); 39 }, 40 }, 41 }); 42 43 ImageObjectProcessor.prototype.process = function ( 44 aManifest, 45 aBaseURL, 46 aMemberName 47 ) { 48 const spec = { 49 objectName: "manifest", 50 object: aManifest, 51 property: aMemberName, 52 expectedType: "array", 53 trim: false, 54 }; 55 const { domBundle, extractor, errors } = this; 56 const images = []; 57 const value = extractor.extractValue(spec); 58 if (Array.isArray(value)) { 59 value 60 .map(toImageObject) 61 // Filter out images that resulted in "failure", per spec. 62 .filter(image => image) 63 .forEach(image => images.push(image)); 64 } 65 return images; 66 67 function toImageObject(aImageSpec, index) { 68 let img; // if "failure" happens below, we return undefined. 69 try { 70 // can throw 71 const src = processSrcMember(aImageSpec, aBaseURL, index); 72 // can throw 73 const purpose = processPurposeMember(aImageSpec, index); 74 const type = processTypeMember(aImageSpec); 75 const sizes = processSizesMember(aImageSpec); 76 img = { 77 src, 78 purpose, 79 type, 80 sizes, 81 }; 82 } catch (err) { 83 /* Errors are collected by each process* function */ 84 } 85 return img; 86 } 87 88 function processPurposeMember(aImage, index) { 89 const spec = { 90 objectName: "image", 91 object: aImage, 92 property: "purpose", 93 expectedType: "string", 94 trim: true, 95 throwTypeError: true, 96 }; 97 98 // Type errors are treated at "any"... 99 let value; 100 try { 101 value = extractor.extractValue(spec); 102 } catch (err) { 103 return ["any"]; 104 } 105 106 // Was only whitespace... 107 if (!value) { 108 return ["any"]; 109 } 110 111 const keywords = value.split(/\s+/); 112 113 // Emtpy is treated as "any"... 114 if (keywords.length === 0) { 115 return ["any"]; 116 } 117 118 // We iterate over keywords and classify them into: 119 const purposes = new Set(); 120 const unknownPurposes = new Set(); 121 const repeatedPurposes = new Set(); 122 123 for (const keyword of keywords) { 124 const canonicalKeyword = keyword.toLowerCase(); 125 126 if (purposes.has(canonicalKeyword)) { 127 repeatedPurposes.add(keyword); 128 continue; 129 } 130 131 iconPurposes.includes(canonicalKeyword) 132 ? purposes.add(canonicalKeyword) 133 : unknownPurposes.add(keyword); 134 } 135 136 // Tell developer about unknown purposes... 137 if (unknownPurposes.size) { 138 const warn = domBundle.formatStringFromName( 139 "ManifestImageUnsupportedPurposes", 140 [aMemberName, index, [...unknownPurposes].join(" ")] 141 ); 142 errors.push({ warn }); 143 } 144 145 // Tell developer about repeated purposes... 146 if (repeatedPurposes.size) { 147 const warn = domBundle.formatStringFromName( 148 "ManifestImageRepeatedPurposes", 149 [aMemberName, index, [...repeatedPurposes].join(" ")] 150 ); 151 errors.push({ warn }); 152 } 153 154 if (purposes.size === 0) { 155 const warn = domBundle.formatStringFromName("ManifestImageUnusable", [ 156 aMemberName, 157 index, 158 ]); 159 errors.push({ warn }); 160 throw new TypeError(warn); 161 } 162 163 return [...purposes]; 164 } 165 166 function processTypeMember(aImage) { 167 const charset = {}; 168 const hadCharset = {}; 169 const spec = { 170 objectName: "image", 171 object: aImage, 172 property: "type", 173 expectedType: "string", 174 trim: true, 175 }; 176 let value = extractor.extractValue(spec); 177 if (value) { 178 value = Services.io.parseRequestContentType(value, charset, hadCharset); 179 } 180 return value || undefined; 181 } 182 183 function processSrcMember(aImage, aBaseURL, index) { 184 const spec = { 185 objectName: aMemberName, 186 object: aImage, 187 property: "src", 188 expectedType: "string", 189 trim: false, 190 throwTypeError: true, 191 }; 192 const value = extractor.extractValue(spec); 193 let url; 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(); 198 } 199 if (value && value.length) { 200 try { 201 url = new URL(value, aBaseURL).href; 202 } catch (e) { 203 const warn = domBundle.formatStringFromName( 204 "ManifestImageURLIsInvalid", 205 [aMemberName, index, "src", value] 206 ); 207 errors.push({ warn }); 208 throw e; 209 } 210 } 211 return url; 212 } 213 214 function processSizesMember(aImage) { 215 const sizes = new Set(); 216 const spec = { 217 objectName: "image", 218 object: aImage, 219 property: "sizes", 220 expectedType: "string", 221 trim: true, 222 }; 223 const value = extractor.extractValue(spec); 224 if (value) { 225 // Split on whitespace and filter out invalid values. 226 value 227 .split(/\s+/) 228 .filter(isValidSizeValue) 229 .reduce((collector, size) => collector.add(size), sizes); 230 } 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)) { 236 return true; 237 } 238 if (!size.includes("x") || size.indexOf("x") !== size.lastIndexOf("x")) { 239 return false; 240 } 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; 248 } 249 } 250 };