summaryrefslogtreecommitdiffstats
path: root/dom/manifest/ManifestProcessor.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'dom/manifest/ManifestProcessor.sys.mjs')
-rw-r--r--dom/manifest/ManifestProcessor.sys.mjs345
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;
+ }
+ },
+};