ManifestProcessor.sys.mjs (10086B)
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 http://mozilla.org/MPL/2.0/. */ 4 /* 5 * ManifestProcessor 6 * Implementation of processing algorithms from: 7 * http://www.w3.org/2008/webapps/manifest/ 8 * 9 * Creates manifest processor that lets you process a JSON file 10 * or individual parts of a manifest object. A manifest is just a 11 * standard JS object that has been cleaned up. 12 * 13 * .process({jsonText,manifestURL,docURL}); 14 * 15 * Depends on ImageObjectProcessor to process things like 16 * icons and splash_screens. 17 * 18 * TODO: The constructor should accept the UA's supported orientations. 19 * TODO: The constructor should accept the UA's supported display modes. 20 */ 21 22 const displayModes = new Set([ 23 "fullscreen", 24 "standalone", 25 "minimal-ui", 26 "browser", 27 ]); 28 const orientationTypes = new Set([ 29 "any", 30 "natural", 31 "landscape", 32 "portrait", 33 "portrait-primary", 34 "portrait-secondary", 35 "landscape-primary", 36 "landscape-secondary", 37 ]); 38 const textDirections = new Set(["ltr", "rtl", "auto"]); 39 40 // ValueExtractor is used by the various processors to get values 41 // from the manifest and to report errors. 42 import { ValueExtractor } from "resource://gre/modules/ValueExtractor.sys.mjs"; 43 44 // ImageObjectProcessor is used to process things like icons and images 45 import { ImageObjectProcessor } from "resource://gre/modules/ImageObjectProcessor.sys.mjs"; 46 47 const domBundle = Services.strings.createBundle( 48 "chrome://global/locale/dom/dom.properties" 49 ); 50 51 export var ManifestProcessor = { 52 get defaultDisplayMode() { 53 return "browser"; 54 }, 55 get displayModes() { 56 return displayModes; 57 }, 58 get orientationTypes() { 59 return orientationTypes; 60 }, 61 get textDirections() { 62 return textDirections; 63 }, 64 // process() method processes JSON text into a clean manifest 65 // that conforms with the W3C specification. Takes an object 66 // expecting the following dictionary items: 67 // * jsonText: the JSON string to be processed. 68 // * manifestURL: the URL of the manifest, to resolve URLs. 69 // * docURL: the URL of the owner doc, for security checks 70 // * checkConformance: boolean. If true, collects any conformance 71 // errors into a "moz_validation" property on the returned manifest. 72 process(aOptions) { 73 const { 74 jsonText, 75 manifestURL: aManifestURL, 76 docURL: aDocURL, 77 checkConformance, 78 } = aOptions; 79 80 // The errors get populated by the different process* functions. 81 const errors = []; 82 83 let rawManifest = {}; 84 try { 85 rawManifest = JSON.parse(jsonText); 86 } catch (e) { 87 errors.push({ type: "json", error: e.message }); 88 } 89 if (rawManifest === null) { 90 return null; 91 } 92 if (typeof rawManifest !== "object") { 93 const warn = domBundle.GetStringFromName("ManifestShouldBeObject"); 94 errors.push({ warn }); 95 rawManifest = {}; 96 } 97 const manifestURL = new URL(aManifestURL); 98 const docURL = new URL(aDocURL); 99 const extractor = new ValueExtractor(errors, domBundle); 100 const imgObjProcessor = new ImageObjectProcessor( 101 errors, 102 extractor, 103 domBundle 104 ); 105 const processedManifest = { 106 dir: processDirMember.call(this), 107 lang: processLangMember(), 108 start_url: processStartURLMember(), 109 display: processDisplayMember.call(this), 110 orientation: processOrientationMember.call(this), 111 name: processNameMember(), 112 icons: imgObjProcessor.process(rawManifest, manifestURL, "icons"), 113 short_name: processShortNameMember(), 114 theme_color: processThemeColorMember(), 115 background_color: processBackgroundColorMember(), 116 }; 117 processedManifest.scope = processScopeMember(); 118 processedManifest.id = processIdMember(); 119 if (checkConformance) { 120 processedManifest.moz_validation = errors; 121 processedManifest.moz_manifest_url = manifestURL.href; 122 } 123 return processedManifest; 124 125 function processDirMember() { 126 const spec = { 127 objectName: "manifest", 128 object: rawManifest, 129 property: "dir", 130 expectedType: "string", 131 trim: true, 132 }; 133 const value = extractor.extractValue(spec); 134 if ( 135 value && 136 typeof value === "string" && 137 this.textDirections.has(value.toLowerCase()) 138 ) { 139 return value.toLowerCase(); 140 } 141 return "auto"; 142 } 143 144 function processNameMember() { 145 const spec = { 146 objectName: "manifest", 147 object: rawManifest, 148 property: "name", 149 expectedType: "string", 150 trim: true, 151 }; 152 return extractor.extractValue(spec); 153 } 154 155 function processShortNameMember() { 156 const spec = { 157 objectName: "manifest", 158 object: rawManifest, 159 property: "short_name", 160 expectedType: "string", 161 trim: true, 162 }; 163 return extractor.extractValue(spec); 164 } 165 166 function processOrientationMember() { 167 const spec = { 168 objectName: "manifest", 169 object: rawManifest, 170 property: "orientation", 171 expectedType: "string", 172 trim: true, 173 }; 174 const value = extractor.extractValue(spec); 175 if ( 176 value && 177 typeof value === "string" && 178 this.orientationTypes.has(value.toLowerCase()) 179 ) { 180 return value.toLowerCase(); 181 } 182 return undefined; 183 } 184 185 function processDisplayMember() { 186 const spec = { 187 objectName: "manifest", 188 object: rawManifest, 189 property: "display", 190 expectedType: "string", 191 trim: true, 192 }; 193 const value = extractor.extractValue(spec); 194 if ( 195 value && 196 typeof value === "string" && 197 displayModes.has(value.toLowerCase()) 198 ) { 199 return value.toLowerCase(); 200 } 201 return this.defaultDisplayMode; 202 } 203 204 function processScopeMember() { 205 const spec = { 206 objectName: "manifest", 207 object: rawManifest, 208 property: "scope", 209 expectedType: "string", 210 trim: false, 211 }; 212 const startURL = new URL(processedManifest.start_url); 213 const defaultScope = new URL(".", startURL).href; 214 const value = extractor.extractValue(spec); 215 if (value === undefined || value === "") { 216 return defaultScope; 217 } 218 let scopeURL = URL.parse(value, manifestURL); 219 if (!scopeURL) { 220 const warn = domBundle.GetStringFromName("ManifestScopeURLInvalid"); 221 errors.push({ warn }); 222 return defaultScope; 223 } 224 if (scopeURL.origin !== docURL.origin) { 225 const warn = domBundle.GetStringFromName("ManifestScopeNotSameOrigin"); 226 errors.push({ warn }); 227 return defaultScope; 228 } 229 // If start URL is not within scope of scope URL: 230 if ( 231 startURL.origin !== scopeURL.origin || 232 startURL.pathname.startsWith(scopeURL.pathname) === false 233 ) { 234 const warn = domBundle.GetStringFromName( 235 "ManifestStartURLOutsideScope" 236 ); 237 errors.push({ warn }); 238 return defaultScope; 239 } 240 // Drop search params and fragment 241 // https://github.com/w3c/manifest/pull/961 242 scopeURL.hash = ""; 243 scopeURL.search = ""; 244 return scopeURL.href; 245 } 246 247 function processStartURLMember() { 248 const spec = { 249 objectName: "manifest", 250 object: rawManifest, 251 property: "start_url", 252 expectedType: "string", 253 trim: false, 254 }; 255 const defaultStartURL = new URL(docURL).href; 256 const value = extractor.extractValue(spec); 257 if (value === undefined || value === "") { 258 return defaultStartURL; 259 } 260 let potentialResult = URL.parse(value, manifestURL); 261 if (!potentialResult) { 262 const warn = domBundle.GetStringFromName("ManifestStartURLInvalid"); 263 errors.push({ warn }); 264 return defaultStartURL; 265 } 266 if (potentialResult.origin !== docURL.origin) { 267 const warn = domBundle.GetStringFromName( 268 "ManifestStartURLShouldBeSameOrigin" 269 ); 270 errors.push({ warn }); 271 return defaultStartURL; 272 } 273 return potentialResult.href; 274 } 275 276 function processThemeColorMember() { 277 const spec = { 278 objectName: "manifest", 279 object: rawManifest, 280 property: "theme_color", 281 expectedType: "string", 282 trim: true, 283 }; 284 return extractor.extractColorValue(spec); 285 } 286 287 function processBackgroundColorMember() { 288 const spec = { 289 objectName: "manifest", 290 object: rawManifest, 291 property: "background_color", 292 expectedType: "string", 293 trim: true, 294 }; 295 return extractor.extractColorValue(spec); 296 } 297 298 function processLangMember() { 299 const spec = { 300 objectName: "manifest", 301 object: rawManifest, 302 property: "lang", 303 expectedType: "string", 304 trim: true, 305 }; 306 return extractor.extractLanguageValue(spec); 307 } 308 309 function processIdMember() { 310 // the start_url serves as the fallback, in case the id is not specified 311 // or in error. A start_url is assured. 312 const startURL = new URL(processedManifest.start_url); 313 314 const spec = { 315 objectName: "manifest", 316 object: rawManifest, 317 property: "id", 318 expectedType: "string", 319 trim: false, 320 }; 321 const extractedValue = extractor.extractValue(spec); 322 323 if (typeof extractedValue !== "string" || extractedValue === "") { 324 return startURL.href; 325 } 326 327 let appId = URL.parse(extractedValue, startURL.origin); 328 if (!appId) { 329 const warn = domBundle.GetStringFromName("ManifestIdIsInvalid"); 330 errors.push({ warn }); 331 return startURL.href; 332 } 333 334 if (appId.origin !== startURL.origin) { 335 const warn = domBundle.GetStringFromName("ManifestIdNotSameOrigin"); 336 errors.push({ warn }); 337 return startURL.href; 338 } 339 340 return appId.href; 341 } 342 }, 343 };