/* 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; } }, };