no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / dom / manifest / ManifestProcessor.sys.mjs
blob6a7ea3b159699fbc1b7a46b5bb391390816905c9
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  */
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"]);
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() {
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;
80     // The errors get populated by the different process* functions.
81     const errors = [];
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;
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 (this.textDirections.has(value)) {
135         return value;
136       }
137       return "auto";
138     }
140     function processNameMember() {
141       const spec = {
142         objectName: "manifest",
143         object: rawManifest,
144         property: "name",
145         expectedType: "string",
146         trim: true,
147       };
148       return extractor.extractValue(spec);
149     }
151     function processShortNameMember() {
152       const spec = {
153         objectName: "manifest",
154         object: rawManifest,
155         property: "short_name",
156         expectedType: "string",
157         trim: true,
158       };
159       return extractor.extractValue(spec);
160     }
162     function processOrientationMember() {
163       const spec = {
164         objectName: "manifest",
165         object: rawManifest,
166         property: "orientation",
167         expectedType: "string",
168         trim: true,
169       };
170       const value = extractor.extractValue(spec);
171       if (
172         value &&
173         typeof value === "string" &&
174         this.orientationTypes.has(value.toLowerCase())
175       ) {
176         return value.toLowerCase();
177       }
178       return undefined;
179     }
181     function processDisplayMember() {
182       const spec = {
183         objectName: "manifest",
184         object: rawManifest,
185         property: "display",
186         expectedType: "string",
187         trim: true,
188       };
189       const value = extractor.extractValue(spec);
190       if (
191         value &&
192         typeof value === "string" &&
193         displayModes.has(value.toLowerCase())
194       ) {
195         return value.toLowerCase();
196       }
197       return this.defaultDisplayMode;
198     }
200     function processScopeMember() {
201       const spec = {
202         objectName: "manifest",
203         object: rawManifest,
204         property: "scope",
205         expectedType: "string",
206         trim: false,
207       };
208       let scopeURL;
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 === "") {
213         return defaultScope;
214       }
215       try {
216         scopeURL = new URL(value, manifestURL);
217       } catch (e) {
218         const warn = domBundle.GetStringFromName("ManifestScopeURLInvalid");
219         errors.push({ warn });
220         return defaultScope;
221       }
222       if (scopeURL.origin !== docURL.origin) {
223         const warn = domBundle.GetStringFromName("ManifestScopeNotSameOrigin");
224         errors.push({ warn });
225         return defaultScope;
226       }
227       // If start URL is not within scope of scope URL:
228       if (
229         startURL.origin !== scopeURL.origin ||
230         startURL.pathname.startsWith(scopeURL.pathname) === false
231       ) {
232         const warn = domBundle.GetStringFromName(
233           "ManifestStartURLOutsideScope"
234         );
235         errors.push({ warn });
236         return defaultScope;
237       }
238       // Drop search params and fragment
239       // https://github.com/w3c/manifest/pull/961
240       scopeURL.hash = "";
241       scopeURL.search = "";
242       return scopeURL.href;
243     }
245     function processStartURLMember() {
246       const spec = {
247         objectName: "manifest",
248         object: rawManifest,
249         property: "start_url",
250         expectedType: "string",
251         trim: false,
252       };
253       const defaultStartURL = new URL(docURL).href;
254       const value = extractor.extractValue(spec);
255       if (value === undefined || value === "") {
256         return defaultStartURL;
257       }
258       let potentialResult;
259       try {
260         potentialResult = new URL(value, manifestURL);
261       } catch (e) {
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     }
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     }
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     }
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     }
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);
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);
323       if (typeof extractedValue !== "string" || extractedValue === "") {
324         return startURL.href;
325       }
327       let appId;
328       try {
329         appId = new URL(extractedValue, startURL.origin);
330       } catch {
331         const warn = domBundle.GetStringFromName("ManifestIdIsInvalid");
332         errors.push({ warn });
333         return startURL.href;
334       }
336       if (appId.origin !== startURL.origin) {
337         const warn = domBundle.GetStringFromName("ManifestIdNotSameOrigin");
338         errors.push({ warn });
339         return startURL.href;
340       }
342       return appId.href;
343     }
344   },