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/. */
6 * Implementation of processing algorithms from:
7 * http://www.w3.org/2008/webapps/manifest/
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.
13 * .process({jsonText,manifestURL,docURL});
15 * Depends on ImageObjectProcessor to process things like
16 * icons and splash_screens.
18 * TODO: The constructor should accept the UA's supported orientations.
19 * TODO: The constructor should accept the UA's supported display modes.
22 const displayModes = new Set([
28 const orientationTypes = new Set([
36 "landscape-secondary",
38 const textDirections = new Set(["ltr", "rtl", "auto"]);
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";
44 // ImageObjectProcessor is used to process things like icons and images
45 import { ImageObjectProcessor } from "resource://gre/modules/ImageObjectProcessor.sys.mjs";
47 const domBundle = Services.strings.createBundle(
48 "chrome://global/locale/dom/dom.properties"
51 export var ManifestProcessor = {
52 get defaultDisplayMode() {
58 get orientationTypes() {
59 return orientationTypes;
61 get textDirections() {
62 return textDirections;
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.
75 manifestURL: aManifestURL,
80 // The errors get populated by the different process* functions.
85 rawManifest = JSON.parse(jsonText);
87 errors.push({ type: "json", error: e.message });
89 if (rawManifest === null) {
92 if (typeof rawManifest !== "object") {
93 const warn = domBundle.GetStringFromName("ManifestShouldBeObject");
94 errors.push({ warn });
97 const manifestURL = new URL(aManifestURL);
98 const docURL = new URL(aDocURL);
99 const extractor = new ValueExtractor(errors, domBundle);
100 const imgObjProcessor = new ImageObjectProcessor(
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(),
117 processedManifest.scope = processScopeMember();
118 processedManifest.id = processIdMember();
119 if (checkConformance) {
120 processedManifest.moz_validation = errors;
121 processedManifest.moz_manifest_url = manifestURL.href;
123 return processedManifest;
125 function processDirMember() {
127 objectName: "manifest",
130 expectedType: "string",
133 const value = extractor.extractValue(spec);
134 if (this.textDirections.has(value)) {
140 function processNameMember() {
142 objectName: "manifest",
145 expectedType: "string",
148 return extractor.extractValue(spec);
151 function processShortNameMember() {
153 objectName: "manifest",
155 property: "short_name",
156 expectedType: "string",
159 return extractor.extractValue(spec);
162 function processOrientationMember() {
164 objectName: "manifest",
166 property: "orientation",
167 expectedType: "string",
170 const value = extractor.extractValue(spec);
173 typeof value === "string" &&
174 this.orientationTypes.has(value.toLowerCase())
176 return value.toLowerCase();
181 function processDisplayMember() {
183 objectName: "manifest",
186 expectedType: "string",
189 const value = extractor.extractValue(spec);
192 typeof value === "string" &&
193 displayModes.has(value.toLowerCase())
195 return value.toLowerCase();
197 return this.defaultDisplayMode;
200 function processScopeMember() {
202 objectName: "manifest",
205 expectedType: "string",
209 const startURL = new URL(processedManifest.start_url);
210 const defaultScope = new URL(".", startURL).href;
211 const value = extractor.extractValue(spec);
212 if (value === undefined || value === "") {
216 scopeURL = new URL(value, manifestURL);
218 const warn = domBundle.GetStringFromName("ManifestScopeURLInvalid");
219 errors.push({ warn });
222 if (scopeURL.origin !== docURL.origin) {
223 const warn = domBundle.GetStringFromName("ManifestScopeNotSameOrigin");
224 errors.push({ warn });
227 // If start URL is not within scope of scope URL:
229 startURL.origin !== scopeURL.origin ||
230 startURL.pathname.startsWith(scopeURL.pathname) === false
232 const warn = domBundle.GetStringFromName(
233 "ManifestStartURLOutsideScope"
235 errors.push({ warn });
238 // Drop search params and fragment
239 // https://github.com/w3c/manifest/pull/961
241 scopeURL.search = "";
242 return scopeURL.href;
245 function processStartURLMember() {
247 objectName: "manifest",
249 property: "start_url",
250 expectedType: "string",
253 const defaultStartURL = new URL(docURL).href;
254 const value = extractor.extractValue(spec);
255 if (value === undefined || value === "") {
256 return defaultStartURL;
260 potentialResult = new URL(value, manifestURL);
262 const warn = domBundle.GetStringFromName("ManifestStartURLInvalid");
263 errors.push({ warn });
264 return defaultStartURL;
266 if (potentialResult.origin !== docURL.origin) {
267 const warn = domBundle.GetStringFromName(
268 "ManifestStartURLShouldBeSameOrigin"
270 errors.push({ warn });
271 return defaultStartURL;
273 return potentialResult.href;
276 function processThemeColorMember() {
278 objectName: "manifest",
280 property: "theme_color",
281 expectedType: "string",
284 return extractor.extractColorValue(spec);
287 function processBackgroundColorMember() {
289 objectName: "manifest",
291 property: "background_color",
292 expectedType: "string",
295 return extractor.extractColorValue(spec);
298 function processLangMember() {
300 objectName: "manifest",
303 expectedType: "string",
306 return extractor.extractLanguageValue(spec);
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);
315 objectName: "manifest",
318 expectedType: "string",
321 const extractedValue = extractor.extractValue(spec);
323 if (typeof extractedValue !== "string" || extractedValue === "") {
324 return startURL.href;
329 appId = new URL(extractedValue, startURL.origin);
331 const warn = domBundle.GetStringFromName("ManifestIdIsInvalid");
332 errors.push({ warn });
333 return startURL.href;
336 if (appId.origin !== startURL.origin) {
337 const warn = domBundle.GetStringFromName("ManifestIdNotSameOrigin");
338 errors.push({ warn });
339 return startURL.href;