diff options
Diffstat (limited to 'dom/manifest/ManifestProcessor.sys.mjs')
-rw-r--r-- | dom/manifest/ManifestProcessor.sys.mjs | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/dom/manifest/ManifestProcessor.sys.mjs b/dom/manifest/ManifestProcessor.sys.mjs new file mode 100644 index 0000000000..6a7ea3b159 --- /dev/null +++ b/dom/manifest/ManifestProcessor.sys.mjs @@ -0,0 +1,345 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* + * ManifestProcessor + * Implementation of processing algorithms from: + * http://www.w3.org/2008/webapps/manifest/ + * + * Creates manifest processor that lets you process a JSON file + * or individual parts of a manifest object. A manifest is just a + * standard JS object that has been cleaned up. + * + * .process({jsonText,manifestURL,docURL}); + * + * Depends on ImageObjectProcessor to process things like + * icons and splash_screens. + * + * TODO: The constructor should accept the UA's supported orientations. + * TODO: The constructor should accept the UA's supported display modes. + */ + +const displayModes = new Set([ + "fullscreen", + "standalone", + "minimal-ui", + "browser", +]); +const orientationTypes = new Set([ + "any", + "natural", + "landscape", + "portrait", + "portrait-primary", + "portrait-secondary", + "landscape-primary", + "landscape-secondary", +]); +const textDirections = new Set(["ltr", "rtl", "auto"]); + +// ValueExtractor is used by the various processors to get values +// from the manifest and to report errors. +import { ValueExtractor } from "resource://gre/modules/ValueExtractor.sys.mjs"; + +// ImageObjectProcessor is used to process things like icons and images +import { ImageObjectProcessor } from "resource://gre/modules/ImageObjectProcessor.sys.mjs"; + +const domBundle = Services.strings.createBundle( + "chrome://global/locale/dom/dom.properties" +); + +export var ManifestProcessor = { + get defaultDisplayMode() { + return "browser"; + }, + get displayModes() { + return displayModes; + }, + get orientationTypes() { + return orientationTypes; + }, + get textDirections() { + return textDirections; + }, + // process() method processes JSON text into a clean manifest + // that conforms with the W3C specification. Takes an object + // expecting the following dictionary items: + // * jsonText: the JSON string to be processed. + // * manifestURL: the URL of the manifest, to resolve URLs. + // * docURL: the URL of the owner doc, for security checks + // * checkConformance: boolean. If true, collects any conformance + // errors into a "moz_validation" property on the returned manifest. + process(aOptions) { + const { + jsonText, + manifestURL: aManifestURL, + docURL: aDocURL, + checkConformance, + } = aOptions; + + // The errors get populated by the different process* functions. + const errors = []; + + let rawManifest = {}; + try { + rawManifest = JSON.parse(jsonText); + } catch (e) { + errors.push({ type: "json", error: e.message }); + } + if (rawManifest === null) { + return null; + } + if (typeof rawManifest !== "object") { + const warn = domBundle.GetStringFromName("ManifestShouldBeObject"); + errors.push({ warn }); + rawManifest = {}; + } + const manifestURL = new URL(aManifestURL); + const docURL = new URL(aDocURL); + const extractor = new ValueExtractor(errors, domBundle); + const imgObjProcessor = new ImageObjectProcessor( + errors, + extractor, + domBundle + ); + const processedManifest = { + dir: processDirMember.call(this), + lang: processLangMember(), + start_url: processStartURLMember(), + display: processDisplayMember.call(this), + orientation: processOrientationMember.call(this), + name: processNameMember(), + icons: imgObjProcessor.process(rawManifest, manifestURL, "icons"), + short_name: processShortNameMember(), + theme_color: processThemeColorMember(), + background_color: processBackgroundColorMember(), + }; + processedManifest.scope = processScopeMember(); + processedManifest.id = processIdMember(); + if (checkConformance) { + processedManifest.moz_validation = errors; + processedManifest.moz_manifest_url = manifestURL.href; + } + return processedManifest; + + function processDirMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "dir", + expectedType: "string", + trim: true, + }; + const value = extractor.extractValue(spec); + if (this.textDirections.has(value)) { + return value; + } + return "auto"; + } + + function processNameMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "name", + expectedType: "string", + trim: true, + }; + return extractor.extractValue(spec); + } + + function processShortNameMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "short_name", + expectedType: "string", + trim: true, + }; + return extractor.extractValue(spec); + } + + function processOrientationMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "orientation", + expectedType: "string", + trim: true, + }; + const value = extractor.extractValue(spec); + if ( + value && + typeof value === "string" && + this.orientationTypes.has(value.toLowerCase()) + ) { + return value.toLowerCase(); + } + return undefined; + } + + function processDisplayMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "display", + expectedType: "string", + trim: true, + }; + const value = extractor.extractValue(spec); + if ( + value && + typeof value === "string" && + displayModes.has(value.toLowerCase()) + ) { + return value.toLowerCase(); + } + return this.defaultDisplayMode; + } + + function processScopeMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "scope", + expectedType: "string", + trim: false, + }; + let scopeURL; + const startURL = new URL(processedManifest.start_url); + const defaultScope = new URL(".", startURL).href; + const value = extractor.extractValue(spec); + if (value === undefined || value === "") { + return defaultScope; + } + try { + scopeURL = new URL(value, manifestURL); + } catch (e) { + const warn = domBundle.GetStringFromName("ManifestScopeURLInvalid"); + errors.push({ warn }); + return defaultScope; + } + if (scopeURL.origin !== docURL.origin) { + const warn = domBundle.GetStringFromName("ManifestScopeNotSameOrigin"); + errors.push({ warn }); + return defaultScope; + } + // If start URL is not within scope of scope URL: + if ( + startURL.origin !== scopeURL.origin || + startURL.pathname.startsWith(scopeURL.pathname) === false + ) { + const warn = domBundle.GetStringFromName( + "ManifestStartURLOutsideScope" + ); + errors.push({ warn }); + return defaultScope; + } + // Drop search params and fragment + // https://github.com/w3c/manifest/pull/961 + scopeURL.hash = ""; + scopeURL.search = ""; + return scopeURL.href; + } + + function processStartURLMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "start_url", + expectedType: "string", + trim: false, + }; + const defaultStartURL = new URL(docURL).href; + const value = extractor.extractValue(spec); + if (value === undefined || value === "") { + return defaultStartURL; + } + let potentialResult; + try { + potentialResult = new URL(value, manifestURL); + } catch (e) { + const warn = domBundle.GetStringFromName("ManifestStartURLInvalid"); + errors.push({ warn }); + return defaultStartURL; + } + if (potentialResult.origin !== docURL.origin) { + const warn = domBundle.GetStringFromName( + "ManifestStartURLShouldBeSameOrigin" + ); + errors.push({ warn }); + return defaultStartURL; + } + return potentialResult.href; + } + + function processThemeColorMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "theme_color", + expectedType: "string", + trim: true, + }; + return extractor.extractColorValue(spec); + } + + function processBackgroundColorMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "background_color", + expectedType: "string", + trim: true, + }; + return extractor.extractColorValue(spec); + } + + function processLangMember() { + const spec = { + objectName: "manifest", + object: rawManifest, + property: "lang", + expectedType: "string", + trim: true, + }; + return extractor.extractLanguageValue(spec); + } + + function processIdMember() { + // the start_url serves as the fallback, in case the id is not specified + // or in error. A start_url is assured. + const startURL = new URL(processedManifest.start_url); + + const spec = { + objectName: "manifest", + object: rawManifest, + property: "id", + expectedType: "string", + trim: false, + }; + const extractedValue = extractor.extractValue(spec); + + if (typeof extractedValue !== "string" || extractedValue === "") { + return startURL.href; + } + + let appId; + try { + appId = new URL(extractedValue, startURL.origin); + } catch { + const warn = domBundle.GetStringFromName("ManifestIdIsInvalid"); + errors.push({ warn }); + return startURL.href; + } + + if (appId.origin !== startURL.origin) { + const warn = domBundle.GetStringFromName("ManifestIdNotSameOrigin"); + errors.push({ warn }); + return startURL.href; + } + + return appId.href; + } + }, +}; |