From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/manifest/ImageObjectProcessor.sys.mjs | 250 +++++++++++++++ dom/manifest/Manifest.sys.mjs | 245 +++++++++++++++ dom/manifest/ManifestFinder.sys.mjs | 57 ++++ dom/manifest/ManifestIcons.sys.mjs | 82 +++++ dom/manifest/ManifestObtainer.sys.mjs | 162 ++++++++++ dom/manifest/ManifestProcessor.sys.mjs | 345 +++++++++++++++++++++ dom/manifest/ValueExtractor.sys.mjs | 97 ++++++ dom/manifest/moz.build | 21 ++ dom/manifest/test/blue-150.png | Bin 0 -> 534 bytes dom/manifest/test/browser.toml | 23 ++ ...rowser_ManifestFinder_browserHasManifestLink.js | 89 ++++++ .../test/browser_ManifestIcons_browserFetchIcon.js | 66 ++++ .../test/browser_ManifestObtainer_credentials.js | 45 +++ .../test/browser_ManifestObtainer_obtain.js | 268 ++++++++++++++++ dom/manifest/test/browser_Manifest_install.js | 58 ++++ dom/manifest/test/common.js | 139 +++++++++ dom/manifest/test/cookie_checker.sjs | 24 ++ dom/manifest/test/cookie_setter.html | 8 + .../test/cookie_setter_with_credentials.html | 6 + ...ookie_setter_with_credentials_cross_origin.html | 14 + dom/manifest/test/file_testserver.sjs | 56 ++++ dom/manifest/test/icon.png | Bin 0 -> 8156 bytes dom/manifest/test/manifestLoader.html | 13 + dom/manifest/test/mochitest.toml | 43 +++ dom/manifest/test/red-50.png | Bin 0 -> 141 bytes dom/manifest/test/resource.sjs | 85 +++++ .../test/test_ImageObjectProcessor_purpose.html | 120 +++++++ .../test/test_ImageObjectProcessor_sizes.html | 88 ++++++ .../test/test_ImageObjectProcessor_src.html | 110 +++++++ .../test/test_ImageObjectProcessor_type.html | 57 ++++ dom/manifest/test/test_ManifestProcessor_JSON.html | 48 +++ .../test_ManifestProcessor_background_color.html | 22 ++ dom/manifest/test/test_ManifestProcessor_dir.html | 57 ++++ .../test/test_ManifestProcessor_display.html | 78 +++++ .../test/test_ManifestProcessor_icons.html | 30 ++ dom/manifest/test/test_ManifestProcessor_id.html | 123 ++++++++ dom/manifest/test/test_ManifestProcessor_lang.html | 139 +++++++++ ...test_ManifestProcessor_name_and_short_name.html | 79 +++++ .../test/test_ManifestProcessor_orientation.html | 86 +++++ .../test/test_ManifestProcessor_scope.html | 113 +++++++ .../test/test_ManifestProcessor_start_url.html | 83 +++++ .../test/test_ManifestProcessor_theme_color.html | 22 ++ .../test/test_ManifestProcessor_warnings.html | 149 +++++++++ .../test/test_link_relList_supports_manifest.html | 47 +++ 44 files changed, 3647 insertions(+) create mode 100644 dom/manifest/ImageObjectProcessor.sys.mjs create mode 100644 dom/manifest/Manifest.sys.mjs create mode 100644 dom/manifest/ManifestFinder.sys.mjs create mode 100644 dom/manifest/ManifestIcons.sys.mjs create mode 100644 dom/manifest/ManifestObtainer.sys.mjs create mode 100644 dom/manifest/ManifestProcessor.sys.mjs create mode 100644 dom/manifest/ValueExtractor.sys.mjs create mode 100644 dom/manifest/moz.build create mode 100644 dom/manifest/test/blue-150.png create mode 100644 dom/manifest/test/browser.toml create mode 100644 dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js create mode 100644 dom/manifest/test/browser_ManifestIcons_browserFetchIcon.js create mode 100644 dom/manifest/test/browser_ManifestObtainer_credentials.js create mode 100644 dom/manifest/test/browser_ManifestObtainer_obtain.js create mode 100644 dom/manifest/test/browser_Manifest_install.js create mode 100644 dom/manifest/test/common.js create mode 100644 dom/manifest/test/cookie_checker.sjs create mode 100644 dom/manifest/test/cookie_setter.html create mode 100644 dom/manifest/test/cookie_setter_with_credentials.html create mode 100644 dom/manifest/test/cookie_setter_with_credentials_cross_origin.html create mode 100644 dom/manifest/test/file_testserver.sjs create mode 100644 dom/manifest/test/icon.png create mode 100644 dom/manifest/test/manifestLoader.html create mode 100644 dom/manifest/test/mochitest.toml create mode 100644 dom/manifest/test/red-50.png create mode 100644 dom/manifest/test/resource.sjs create mode 100644 dom/manifest/test/test_ImageObjectProcessor_purpose.html create mode 100644 dom/manifest/test/test_ImageObjectProcessor_sizes.html create mode 100644 dom/manifest/test/test_ImageObjectProcessor_src.html create mode 100644 dom/manifest/test/test_ImageObjectProcessor_type.html create mode 100644 dom/manifest/test/test_ManifestProcessor_JSON.html create mode 100644 dom/manifest/test/test_ManifestProcessor_background_color.html create mode 100644 dom/manifest/test/test_ManifestProcessor_dir.html create mode 100644 dom/manifest/test/test_ManifestProcessor_display.html create mode 100644 dom/manifest/test/test_ManifestProcessor_icons.html create mode 100644 dom/manifest/test/test_ManifestProcessor_id.html create mode 100644 dom/manifest/test/test_ManifestProcessor_lang.html create mode 100644 dom/manifest/test/test_ManifestProcessor_name_and_short_name.html create mode 100644 dom/manifest/test/test_ManifestProcessor_orientation.html create mode 100644 dom/manifest/test/test_ManifestProcessor_scope.html create mode 100644 dom/manifest/test/test_ManifestProcessor_start_url.html create mode 100644 dom/manifest/test/test_ManifestProcessor_theme_color.html create mode 100644 dom/manifest/test/test_ManifestProcessor_warnings.html create mode 100644 dom/manifest/test/test_link_relList_supports_manifest.html (limited to 'dom/manifest') diff --git a/dom/manifest/ImageObjectProcessor.sys.mjs b/dom/manifest/ImageObjectProcessor.sys.mjs new file mode 100644 index 0000000000..f72c5ff9fe --- /dev/null +++ b/dom/manifest/ImageObjectProcessor.sys.mjs @@ -0,0 +1,250 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ +/* + * ImageObjectProcessor + * Implementation of Image Object processing algorithms from: + * http://www.w3.org/TR/appmanifest/#image-object-and-its-members + * + * This is intended to be used in conjunction with ManifestProcessor.jsm + * + * Creates an object to process Image Objects as defined by the + * W3C specification. This is used to process things like the + * icon member and the splash_screen member. + * + * Usage: + * + * .process(aManifest, aBaseURL, aMemberName); + * + */ + +export function ImageObjectProcessor(aErrors, aExtractor, aBundle) { + this.errors = aErrors; + this.extractor = aExtractor; + this.domBundle = aBundle; +} + +const iconPurposes = Object.freeze(["any", "maskable", "monochrome"]); + +// Static getters +Object.defineProperties(ImageObjectProcessor, { + decimals: { + get() { + return /^\d+$/; + }, + }, + anyRegEx: { + get() { + return new RegExp("any", "i"); + }, + }, +}); + +ImageObjectProcessor.prototype.process = function ( + aManifest, + aBaseURL, + aMemberName +) { + const spec = { + objectName: "manifest", + object: aManifest, + property: aMemberName, + expectedType: "array", + trim: false, + }; + const { domBundle, extractor, errors } = this; + const images = []; + const value = extractor.extractValue(spec); + if (Array.isArray(value)) { + value + .map(toImageObject) + // Filter out images that resulted in "failure", per spec. + .filter(image => image) + .forEach(image => images.push(image)); + } + return images; + + function toImageObject(aImageSpec, index) { + let img; // if "failure" happens below, we return undefined. + try { + // can throw + const src = processSrcMember(aImageSpec, aBaseURL, index); + // can throw + const purpose = processPurposeMember(aImageSpec, index); + const type = processTypeMember(aImageSpec); + const sizes = processSizesMember(aImageSpec); + img = { + src, + purpose, + type, + sizes, + }; + } catch (err) { + /* Errors are collected by each process* function */ + } + return img; + } + + function processPurposeMember(aImage, index) { + const spec = { + objectName: "image", + object: aImage, + property: "purpose", + expectedType: "string", + trim: true, + throwTypeError: true, + }; + + // Type errors are treated at "any"... + let value; + try { + value = extractor.extractValue(spec); + } catch (err) { + return ["any"]; + } + + // Was only whitespace... + if (!value) { + return ["any"]; + } + + const keywords = value.split(/\s+/); + + // Emtpy is treated as "any"... + if (keywords.length === 0) { + return ["any"]; + } + + // We iterate over keywords and classify them into: + const purposes = new Set(); + const unknownPurposes = new Set(); + const repeatedPurposes = new Set(); + + for (const keyword of keywords) { + const canonicalKeyword = keyword.toLowerCase(); + + if (purposes.has(canonicalKeyword)) { + repeatedPurposes.add(keyword); + continue; + } + + iconPurposes.includes(canonicalKeyword) + ? purposes.add(canonicalKeyword) + : unknownPurposes.add(keyword); + } + + // Tell developer about unknown purposes... + if (unknownPurposes.size) { + const warn = domBundle.formatStringFromName( + "ManifestImageUnsupportedPurposes", + [aMemberName, index, [...unknownPurposes].join(" ")] + ); + errors.push({ warn }); + } + + // Tell developer about repeated purposes... + if (repeatedPurposes.size) { + const warn = domBundle.formatStringFromName( + "ManifestImageRepeatedPurposes", + [aMemberName, index, [...repeatedPurposes].join(" ")] + ); + errors.push({ warn }); + } + + if (purposes.size === 0) { + const warn = domBundle.formatStringFromName("ManifestImageUnusable", [ + aMemberName, + index, + ]); + errors.push({ warn }); + throw new TypeError(warn); + } + + return [...purposes]; + } + + function processTypeMember(aImage) { + const charset = {}; + const hadCharset = {}; + const spec = { + objectName: "image", + object: aImage, + property: "type", + expectedType: "string", + trim: true, + }; + let value = extractor.extractValue(spec); + if (value) { + value = Services.io.parseRequestContentType(value, charset, hadCharset); + } + return value || undefined; + } + + function processSrcMember(aImage, aBaseURL, index) { + const spec = { + objectName: aMemberName, + object: aImage, + property: "src", + expectedType: "string", + trim: false, + throwTypeError: true, + }; + const value = extractor.extractValue(spec); + let url; + if (typeof value === "undefined" || value === "") { + // We throw here as the value is unusable, + // but it's not an developer error. + throw new TypeError(); + } + if (value && value.length) { + try { + url = new URL(value, aBaseURL).href; + } catch (e) { + const warn = domBundle.formatStringFromName( + "ManifestImageURLIsInvalid", + [aMemberName, index, "src", value] + ); + errors.push({ warn }); + throw e; + } + } + return url; + } + + function processSizesMember(aImage) { + const sizes = new Set(); + const spec = { + objectName: "image", + object: aImage, + property: "sizes", + expectedType: "string", + trim: true, + }; + const value = extractor.extractValue(spec); + if (value) { + // Split on whitespace and filter out invalid values. + value + .split(/\s+/) + .filter(isValidSizeValue) + .reduce((collector, size) => collector.add(size), sizes); + } + return sizes.size ? Array.from(sizes) : undefined; + // Implementation of HTML's link@size attribute checker. + function isValidSizeValue(aSize) { + const size = aSize.toLowerCase(); + if (ImageObjectProcessor.anyRegEx.test(aSize)) { + return true; + } + if (!size.includes("x") || size.indexOf("x") !== size.lastIndexOf("x")) { + return false; + } + // Split left of x for width, after x for height. + const widthAndHeight = size.split("x"); + const w = widthAndHeight.shift(); + const h = widthAndHeight.join("x"); + const validStarts = !w.startsWith("0") && !h.startsWith("0"); + const validDecimals = ImageObjectProcessor.decimals.test(w + h); + return validStarts && validDecimals; + } + } +}; diff --git a/dom/manifest/Manifest.sys.mjs b/dom/manifest/Manifest.sys.mjs new file mode 100644 index 0000000000..f6fab11277 --- /dev/null +++ b/dom/manifest/Manifest.sys.mjs @@ -0,0 +1,245 @@ +/* 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/. */ + +/* + * Manifest.jsm is the top level api for managing installed web applications + * https://www.w3.org/TR/appmanifest/ + * + * It is used to trigger the installation of a web application via .install() + * and to access the manifest data (including icons). + * + * TODO: + * - Trigger appropriate app installed events + */ + +import { ManifestObtainer } from "resource://gre/modules/ManifestObtainer.sys.mjs"; + +import { ManifestIcons } from "resource://gre/modules/ManifestIcons.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + JSONFile: "resource://gre/modules/JSONFile.sys.mjs", +}); + +/** + * Generates an hash for the given string. + * + * @note The generated hash is returned in base64 form. Mind the fact base64 + * is case-sensitive if you are going to reuse this code. + */ +function generateHash(aString) { + const cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + cryptoHash.init(Ci.nsICryptoHash.MD5); + const stringStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + stringStream.data = aString; + cryptoHash.updateFromStream(stringStream, -1); + // base64 allows the '/' char, but we can't use it for filenames. + return cryptoHash.finish(true).replace(/\//g, "-"); +} + +/** + * Trims the query parameters from a url + */ +function stripQuery(url) { + return url.split("?")[0]; +} + +// Folder in which we store the manifest files +const MANIFESTS_DIR = PathUtils.join(PathUtils.profileDir, "manifests"); + +// We maintain a list of scopes for installed webmanifests so we can determine +// whether a given url is within the scope of a previously installed manifest +const MANIFESTS_FILE = "manifest-scopes.json"; + +/** + * Manifest object + */ + +class Manifest { + constructor(browser, manifestUrl) { + this._manifestUrl = manifestUrl; + // The key for this is the manifests URL that is required to be unique. + // However arbitrary urls are not safe file paths so lets hash it. + const fileName = generateHash(manifestUrl) + ".json"; + this._path = PathUtils.join(MANIFESTS_DIR, fileName); + this.browser = browser; + } + + get browser() { + return this._browser; + } + + set browser(aBrowser) { + this._browser = aBrowser; + } + + async initialize() { + this._store = new lazy.JSONFile({ path: this._path, saveDelayMs: 100 }); + await this._store.load(); + } + + async prefetch(browser) { + const manifestData = await ManifestObtainer.browserObtainManifest(browser); + const icon = await ManifestIcons.browserFetchIcon( + browser, + manifestData, + 192 + ); + const data = { + installed: false, + manifest: manifestData, + cached_icon: icon, + }; + return data; + } + + async install() { + const manifestData = await ManifestObtainer.browserObtainManifest( + this._browser + ); + this._store.data = { + installed: true, + manifest: manifestData, + }; + Manifests.manifestInstalled(this); + this._store.saveSoon(); + } + + async icon(expectedSize) { + if ("cached_icon" in this._store.data) { + return this._store.data.cached_icon; + } + const icon = await ManifestIcons.browserFetchIcon( + this._browser, + this._store.data.manifest, + expectedSize + ); + // Cache the icon so future requests do not go over the network + this._store.data.cached_icon = icon; + this._store.saveSoon(); + return icon; + } + + get scope() { + const scope = + this._store.data.manifest.scope || this._store.data.manifest.start_url; + return stripQuery(scope); + } + + get name() { + return ( + this._store.data.manifest.short_name || + this._store.data.manifest.name || + this._store.data.manifest.short_url + ); + } + + get url() { + return this._manifestUrl; + } + + get installed() { + return (this._store.data && this._store.data.installed) || false; + } + + get start_url() { + return this._store.data.manifest.start_url; + } + + get path() { + return this._path; + } +} + +/* + * Manifests maintains the list of installed manifests + */ +export var Manifests = { + async _initialize() { + if (this._readyPromise) { + return this._readyPromise; + } + + // Prevent multiple initializations + this._readyPromise = (async () => { + // Make sure the manifests have the folder needed to save into + await IOUtils.makeDirectory(MANIFESTS_DIR, { ignoreExisting: true }); + + // Ensure any existing scope data we have about manifests is loaded + this._path = PathUtils.join(PathUtils.profileDir, MANIFESTS_FILE); + this._store = new lazy.JSONFile({ path: this._path }); + await this._store.load(); + + // If we don't have any existing data, initialize empty + if (!this._store.data.hasOwnProperty("scopes")) { + this._store.data.scopes = new Map(); + } + })(); + + // Cache the Manifest objects creates as they are references to files + // and we do not want multiple file handles + this.manifestObjs = new Map(); + return this._readyPromise; + }, + + // When a manifest is installed, we save its scope so we can determine if + // future visits fall within this manifests scope + manifestInstalled(manifest) { + this._store.data.scopes[manifest.scope] = manifest.url; + this._store.saveSoon(); + }, + + // Given a url, find if it is within an installed manifests scope and if so + // return that manifests url + findManifestUrl(url) { + for (let scope in this._store.data.scopes) { + if (url.startsWith(scope)) { + return this._store.data.scopes[scope]; + } + } + return null; + }, + + // Get the manifest given a url, or if not look for a manifest that is + // tied to the current page + async getManifest(browser, manifestUrl) { + // Ensure we have all started up + if (!this._readyPromise) { + await this._initialize(); + } + + // If the client does not already know its manifestUrl, we take the + // url of the client and see if it matches the scope of any installed + // manifests + if (!manifestUrl) { + const url = stripQuery(browser.currentURI.spec); + manifestUrl = this.findManifestUrl(url); + } + + // No matches so no manifest + if (manifestUrl === null) { + return null; + } + + // If we have already created this manifest return cached + if (this.manifestObjs.has(manifestUrl)) { + const manifest = this.manifestObjs.get(manifestUrl); + if (manifest.browser !== browser) { + manifest.browser = browser; + } + return manifest; + } + + // Otherwise create a new manifest object + const manifest = new Manifest(browser, manifestUrl); + this.manifestObjs.set(manifestUrl, manifest); + await manifest.initialize(); + return manifest; + }, +}; diff --git a/dom/manifest/ManifestFinder.sys.mjs b/dom/manifest/ManifestFinder.sys.mjs new file mode 100644 index 0000000000..1f8a23462b --- /dev/null +++ b/dom/manifest/ManifestFinder.sys.mjs @@ -0,0 +1,57 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +export var ManifestFinder = { + /** + * Check from content process if DOM Window has a conforming + * manifest link relationship. + * @param aContent DOM Window to check. + * @return {Promise} + */ + contentHasManifestLink(aContent) { + if (!aContent || isXULBrowser(aContent)) { + throw new TypeError("Invalid input."); + } + return checkForManifest(aContent); + }, + + /** + * Check from a XUL browser (parent process) if it's content document has a + * manifest link relationship. + * @param aBrowser The XUL browser to check. + * @return {Promise} + */ + async browserHasManifestLink(aBrowser) { + if (!isXULBrowser(aBrowser)) { + throw new TypeError("Invalid input."); + } + + const actor = + aBrowser.browsingContext.currentWindowGlobal.getActor("ManifestMessages"); + const reply = await actor.sendQuery("DOM:WebManifest:hasManifestLink"); + return reply.result; + }, +}; + +function isXULBrowser(aBrowser) { + if (!aBrowser || !aBrowser.namespaceURI || !aBrowser.localName) { + return false; + } + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + return aBrowser.namespaceURI === XUL_NS && aBrowser.localName === "browser"; +} + +function checkForManifest(aWindow) { + // Only top-level browsing contexts are valid. + if (!aWindow || aWindow.top !== aWindow) { + return false; + } + const elem = aWindow.document.querySelector("link[rel~='manifest']"); + // Only if we have an element and a non-empty href attribute. + if (!elem || !elem.getAttribute("href")) { + return false; + } + return true; +} diff --git a/dom/manifest/ManifestIcons.sys.mjs b/dom/manifest/ManifestIcons.sys.mjs new file mode 100644 index 0000000000..9994c40d55 --- /dev/null +++ b/dom/manifest/ManifestIcons.sys.mjs @@ -0,0 +1,82 @@ +/* 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/. */ + +export var ManifestIcons = { + async browserFetchIcon(aBrowser, manifest, iconSize) { + const msgKey = "DOM:WebManifest:fetchIcon"; + + const actor = + aBrowser.browsingContext.currentWindowGlobal.getActor("ManifestMessages"); + const reply = await actor.sendQuery(msgKey, { manifest, iconSize }); + if (!reply.success) { + throw reply.result; + } + return reply.result; + }, + + async contentFetchIcon(aWindow, manifest, iconSize) { + return getIcon(aWindow, toIconArray(manifest.icons), iconSize); + }, +}; + +function parseIconSize(size) { + if (size === "any" || size === "") { + // We want icons without size specified to sorted + // as the largest available icons + return Number.MAX_SAFE_INTEGER; + } + // 100x100 will parse as 100 + return parseInt(size, 10); +} + +// Create an array of icons sorted by their size +function toIconArray(icons) { + const iconBySize = []; + icons.forEach(icon => { + const sizes = "sizes" in icon ? icon.sizes : ""; + sizes.forEach(size => { + iconBySize.push({ src: icon.src, size: parseIconSize(size) }); + }); + }); + return iconBySize.sort((a, b) => a.size - b.size); +} + +async function getIcon(aWindow, icons, expectedSize) { + if (!icons.length) { + throw new Error("Could not find valid icon"); + } + // We start trying the smallest icon that is larger than the requested + // size and go up to the largest icon if they fail, if all those fail + // go back down to the smallest + let index = icons.findIndex(icon => icon.size >= expectedSize); + if (index === -1) { + index = icons.length - 1; + } + + return fetchIcon(aWindow, icons[index].src).catch(err => { + // Remove all icons with the failed source, the same source + // may have been used for multiple sizes + icons = icons.filter(x => x.src !== icons[index].src); + return getIcon(aWindow, icons, expectedSize); + }); +} + +async function fetchIcon(aWindow, src) { + const iconURL = new aWindow.URL(src, aWindow.location); + // If this is already a data URL then no need to load it again. + if (iconURL.protocol === "data:") { + return iconURL.href; + } + + const request = new aWindow.Request(iconURL, { mode: "cors" }); + request.overrideContentPolicyType(Ci.nsIContentPolicy.TYPE_IMAGE); + const response = await aWindow.fetch(request); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} diff --git a/dom/manifest/ManifestObtainer.sys.mjs b/dom/manifest/ManifestObtainer.sys.mjs new file mode 100644 index 0000000000..de2863442a --- /dev/null +++ b/dom/manifest/ManifestObtainer.sys.mjs @@ -0,0 +1,162 @@ +/* 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/. + */ +/* + * ManifestObtainer is an implementation of: + * http://w3c.github.io/manifest/#obtaining + * + * Exposes 2 public method: + * + * .contentObtainManifest(aContent) - used in content process + * .browserObtainManifest(aBrowser) - used in browser/parent process + * + * both return a promise. If successful, you get back a manifest object. + * + * Import it with URL: + * 'chrome://global/content/manifestMessages.js' + * + * e10s IPC message from this components are handled by: + * dom/ipc/manifestMessages.js + * + * Which is injected into every browser instance via browser.js. + */ + +import { ManifestProcessor } from "resource://gre/modules/ManifestProcessor.sys.mjs"; + +export var ManifestObtainer = { + /** + * Public interface for obtaining a web manifest from a XUL browser, to use + * on the parent process. + * @param {XULBrowser} The browser to check for the manifest. + * @param {Object} aOptions + * @param {Boolean} aOptions.checkConformance If spec conformance messages should be collected. + * Adds proprietary moz_* members to manifest. + * @return {Promise} The processed manifest. + */ + async browserObtainManifest( + aBrowser, + aOptions = { checkConformance: false } + ) { + if (!isXULBrowser(aBrowser)) { + throw new TypeError("Invalid input. Expected XUL browser."); + } + + const actor = + aBrowser.browsingContext.currentWindowGlobal.getActor("ManifestMessages"); + + const reply = await actor.sendQuery( + "DOM:ManifestObtainer:Obtain", + aOptions + ); + if (!reply.success) { + const error = toError(reply.result); + throw error; + } + return reply.result; + }, + /** + * Public interface for obtaining a web manifest from a XUL browser. + * @param {Window} aContent A content Window from which to extract the manifest. + * @param {Object} aOptions + * @param {Boolean} aOptions.checkConformance If spec conformance messages should be collected. + * Adds proprietary moz_* members to manifest. + * @return {Promise} The processed manifest. + */ + async contentObtainManifest( + aContent, + aOptions = { checkConformance: false } + ) { + if (!Services.prefs.getBoolPref("dom.manifest.enabled")) { + throw new Error( + "Obtaining manifest is disabled by pref: dom.manifest.enabled" + ); + } + if (!aContent || isXULBrowser(aContent)) { + const err = new TypeError("Invalid input. Expected a DOM Window."); + return Promise.reject(err); + } + const response = await fetchManifest(aContent); + const result = await processResponse(response, aContent, aOptions); + const clone = Cu.cloneInto(result, aContent); + return clone; + }, +}; + +function toError(aErrorClone) { + let error; + switch (aErrorClone.name) { + case "TypeError": + error = new TypeError(); + break; + default: + error = new Error(); + } + Object.getOwnPropertyNames(aErrorClone).forEach( + name => (error[name] = aErrorClone[name]) + ); + return error; +} + +function isXULBrowser(aBrowser) { + if (!aBrowser || !aBrowser.namespaceURI || !aBrowser.localName) { + return false; + } + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + return aBrowser.namespaceURI === XUL_NS && aBrowser.localName === "browser"; +} + +/** + * Asynchronously processes the result of response after having fetched + * a manifest. + * @param {Response} aResp Response from fetch(). + * @param {Window} aContentWindow The content window. + * @return {Promise} The processed manifest. + */ +async function processResponse(aResp, aContentWindow, aOptions) { + const badStatus = aResp.status < 200 || aResp.status >= 300; + if (aResp.type === "error" || badStatus) { + const msg = `Fetch error: ${aResp.status} - ${aResp.statusText} at ${aResp.url}`; + throw new Error(msg); + } + const text = await aResp.text(); + const args = { + jsonText: text, + manifestURL: aResp.url, + docURL: aContentWindow.location.href, + }; + const processingOptions = Object.assign({}, args, aOptions); + const manifest = ManifestProcessor.process(processingOptions); + return manifest; +} + +/** + * Asynchronously fetches a web manifest. + * @param {Window} a The content Window from where to extract the manifest. + * @return {Promise} + */ +async function fetchManifest(aWindow) { + if (!aWindow || aWindow.top !== aWindow) { + const msg = "Window must be a top-level browsing context."; + throw new Error(msg); + } + const elem = aWindow.document.querySelector("link[rel~='manifest']"); + if (!elem || !elem.getAttribute("href")) { + // There is no actual manifest to fetch, we just return null. + return new aWindow.Response("null"); + } + // Throws on malformed URLs + const manifestURL = new aWindow.URL(elem.href, elem.baseURI); + const reqInit = { + credentials: "omit", + mode: "cors", + }; + if (elem.crossOrigin === "use-credentials") { + reqInit.credentials = "include"; + } + const request = new aWindow.Request(manifestURL, reqInit); + request.overrideContentPolicyType(Ci.nsIContentPolicy.TYPE_WEB_MANIFEST); + // Can reject... + return aWindow.fetch(request); +} 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; + } + }, +}; diff --git a/dom/manifest/ValueExtractor.sys.mjs b/dom/manifest/ValueExtractor.sys.mjs new file mode 100644 index 0000000000..d08cd24e1c --- /dev/null +++ b/dom/manifest/ValueExtractor.sys.mjs @@ -0,0 +1,97 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ +/* + * Helper functions extract values from manifest members + * and reports conformance errors. + */ + +export class ValueExtractor { + constructor(errors, aBundle) { + this.errors = errors; + this.domBundle = aBundle; + } + + /** + * @param options + * The 'spec' object. + * @note This function takes a 'spec' object and destructures it to extract + * a value. If the value is of the wrong type, it warns the developer + * and returns undefined. + * expectedType: is the type of a JS primitive (string, number, etc.) + * object: is the object from which to extract the value. + * objectName: string used to construct the developer warning. + * property: the name of the property being extracted. + * throwTypeError: boolean, throw a TypeError if the type is incorrect. + * trim: boolean, if the value should be trimmed (used by string type). + */ + extractValue(options) { + const { expectedType, object, objectName, property, throwTypeError, trim } = + options; + const value = object[property]; + const isArray = Array.isArray(value); + + // We need to special-case "array", as it's not a JS primitive. + const type = isArray ? "array" : typeof value; + if (type !== expectedType) { + if (type !== "undefined") { + const warn = this.domBundle.formatStringFromName( + "ManifestInvalidType", + [objectName, property, expectedType] + ); + this.errors.push({ warn }); + if (throwTypeError) { + throw new TypeError(warn); + } + } + return undefined; + } + + // Trim string and returned undefined if the empty string. + const shouldTrim = expectedType === "string" && value && trim; + if (shouldTrim) { + return value.trim() || undefined; + } + return value; + } + + extractColorValue(spec) { + const value = this.extractValue(spec); + let color; + if (InspectorUtils.isValidCSSColor(value)) { + const rgba = InspectorUtils.colorToRGBA(value); + color = + "#" + + rgba.r.toString(16).padStart(2, "0") + + rgba.g.toString(16).padStart(2, "0") + + rgba.b.toString(16).padStart(2, "0") + + Math.round(rgba.a * 255) + .toString(16) + .padStart(2, "0"); + } else if (value) { + const warn = this.domBundle.formatStringFromName( + "ManifestInvalidCSSColor", + [spec.property, value] + ); + this.errors.push({ warn }); + } + return color; + } + + extractLanguageValue(spec) { + let langTag; + const value = this.extractValue(spec); + if (value !== undefined) { + try { + langTag = Intl.getCanonicalLocales(value)[0]; + } catch (err) { + const warn = this.domBundle.formatStringFromName( + "ManifestLangIsInvalid", + [spec.property, value] + ); + this.errors.push({ warn }); + } + } + return langTag; + } +} diff --git a/dom/manifest/moz.build b/dom/manifest/moz.build new file mode 100644 index 0000000000..9f25d2273d --- /dev/null +++ b/dom/manifest/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +EXTRA_JS_MODULES += [ + "ImageObjectProcessor.sys.mjs", + "Manifest.sys.mjs", + "ManifestFinder.sys.mjs", + "ManifestIcons.sys.mjs", + "ManifestObtainer.sys.mjs", + "ManifestProcessor.sys.mjs", + "ValueExtractor.sys.mjs", +] + +MOCHITEST_MANIFESTS += ["test/mochitest.toml"] +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] diff --git a/dom/manifest/test/blue-150.png b/dom/manifest/test/blue-150.png new file mode 100644 index 0000000000..f4a62faddf Binary files /dev/null and b/dom/manifest/test/blue-150.png differ diff --git a/dom/manifest/test/browser.toml b/dom/manifest/test/browser.toml new file mode 100644 index 0000000000..f5c28fcfb3 --- /dev/null +++ b/dom/manifest/test/browser.toml @@ -0,0 +1,23 @@ +[DEFAULT] +support-files = [ + "cookie_setter_with_credentials_cross_origin.html", + "cookie_setter_with_credentials.html", + "blue-150.png", + "cookie_checker.sjs", + "cookie_setter.html", + "file_testserver.sjs", + "manifestLoader.html", + "red-50.png", + "resource.sjs", +] + +["browser_ManifestFinder_browserHasManifestLink.js"] + +["browser_ManifestIcons_browserFetchIcon.js"] + +["browser_ManifestObtainer_credentials.js"] + +["browser_ManifestObtainer_obtain.js"] + +["browser_Manifest_install.js"] +skip-if = ["verify"] diff --git a/dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js b/dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js new file mode 100644 index 0000000000..360c98220b --- /dev/null +++ b/dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js @@ -0,0 +1,89 @@ +"use strict"; +const { ManifestFinder } = ChromeUtils.importESModule( + "resource://gre/modules/ManifestFinder.sys.mjs" +); +const defaultURL = new URL( + "http://example.org/browser/dom/manifest/test/resource.sjs" +); +defaultURL.searchParams.set("Content-Type", "text/html; charset=utf-8"); + +const tests = [ + { + body: ` + + + + `, + run(result) { + ok(result, "Document has a web manifest."); + }, + }, + { + body: ` + + + `, + run(result) { + ok(!result, "Document does not have a web manifest."); + }, + }, + { + body: ` + + `, + run(result) { + ok(!result, "Manifest link is has empty href."); + }, + }, + { + body: ` + + `, + run(result) { + ok(!result, "Manifest link is missing."); + }, + }, +]; + +function makeTestURL({ body }) { + const url = new URL(defaultURL); + url.searchParams.set("body", encodeURIComponent(body)); + return url.href; +} + +/** + * Test basic API error conditions + */ +add_task(async function () { + const expected = "Invalid types should throw a TypeError."; + for (let invalidValue of [undefined, null, 1, {}, "test"]) { + try { + await ManifestFinder.contentManifestLink(invalidValue); + ok(false, expected); + } catch (e) { + is(e.name, "TypeError", expected); + } + try { + await ManifestFinder.browserManifestLink(invalidValue); + ok(false, expected); + } catch (e) { + is(e.name, "TypeError", expected); + } + } +}); + +add_task(async function () { + const runningTests = tests + .map(test => ({ + gBrowser, + test, + url: makeTestURL(test), + })) + .map(tabOptions => + BrowserTestUtils.withNewTab(tabOptions, async function (browser) { + const result = await ManifestFinder.browserHasManifestLink(browser); + tabOptions.test.run(result); + }) + ); + await Promise.all(runningTests); +}); diff --git a/dom/manifest/test/browser_ManifestIcons_browserFetchIcon.js b/dom/manifest/test/browser_ManifestIcons_browserFetchIcon.js new file mode 100644 index 0000000000..e56e1e25c2 --- /dev/null +++ b/dom/manifest/test/browser_ManifestIcons_browserFetchIcon.js @@ -0,0 +1,66 @@ +"use strict"; + +Services.prefs.setBoolPref("dom.manifest.enabled", true); + +const { ManifestIcons } = ChromeUtils.importESModule( + "resource://gre/modules/ManifestIcons.sys.mjs" +); +const { ManifestObtainer } = ChromeUtils.importESModule( + "resource://gre/modules/ManifestObtainer.sys.mjs" +); + +const defaultURL = new URL( + "https://example.org/browser/dom/manifest/test/resource.sjs" +); +defaultURL.searchParams.set("Content-Type", "application/manifest+json"); + +const manifestMock = JSON.stringify({ + icons: [ + { + sizes: "50x50", + src: "red-50.png?Content-type=image/png", + }, + { + sizes: "150x150", + src: "blue-150.png?Content-type=image/png", + }, + ], +}); + +function makeTestURL() { + const url = new URL(defaultURL); + const body = ``; + url.searchParams.set("Content-Type", "text/html; charset=utf-8"); + url.searchParams.set("body", encodeURIComponent(body)); + return url.href; +} + +function getIconColor(icon) { + return new Promise((resolve, reject) => { + const canvas = content.document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const image = new content.Image(); + image.onload = function () { + ctx.drawImage(image, 0, 0); + resolve(ctx.getImageData(1, 1, 1, 1).data); + }; + image.onerror = function () { + reject(new Error("could not create image")); + }; + image.src = icon; + }); +} + +add_task(async function () { + const tabOptions = { gBrowser, url: makeTestURL() }; + await BrowserTestUtils.withNewTab(tabOptions, async function (browser) { + const manifest = await ManifestObtainer.browserObtainManifest(browser); + let icon = await ManifestIcons.browserFetchIcon(browser, manifest, 25); + let color = await SpecialPowers.spawn(browser, [icon], getIconColor); + is(color[0], 255, "Fetched red icon"); + + icon = await ManifestIcons.browserFetchIcon(browser, manifest, 500); + color = await SpecialPowers.spawn(browser, [icon], getIconColor); + is(color[2], 255, "Fetched blue icon"); + }); +}); diff --git a/dom/manifest/test/browser_ManifestObtainer_credentials.js b/dom/manifest/test/browser_ManifestObtainer_credentials.js new file mode 100644 index 0000000000..826ec24fc0 --- /dev/null +++ b/dom/manifest/test/browser_ManifestObtainer_credentials.js @@ -0,0 +1,45 @@ +"use strict"; + +Services.prefs.setBoolPref("dom.manifest.enabled", true); + +const { ManifestObtainer } = ChromeUtils.importESModule( + "resource://gre/modules/ManifestObtainer.sys.mjs" +); + +// Don't send cookies +add_task(async function () { + const testPath = "/browser/dom/manifest/test/cookie_setter.html"; + const tabURL = `https://example.com${testPath}`; + const browser = BrowserTestUtils.addTab(gBrowser, tabURL).linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + const { short_name } = await ManifestObtainer.browserObtainManifest(browser); + is(short_name, "no cookie"); + const tab = gBrowser.getTabForBrowser(browser); + gBrowser.removeTab(tab); +}); + +// Send cookies +add_task(async function () { + const testPath = + "/browser/dom/manifest/test/cookie_setter_with_credentials.html"; + const tabURL = `https://example.com${testPath}`; + const browser = BrowserTestUtils.addTab(gBrowser, tabURL).linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + const { short_name } = await ManifestObtainer.browserObtainManifest(browser); + is(short_name, "🍪"); + const tab = gBrowser.getTabForBrowser(browser); + gBrowser.removeTab(tab); +}); + +// Cross origin - we go from example.com to example.org +add_task(async function () { + const testPath = + "/browser/dom/manifest/test/cookie_setter_with_credentials_cross_origin.html"; + const tabURL = `https://example.com${testPath}`; + const browser = BrowserTestUtils.addTab(gBrowser, tabURL).linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + const { short_name } = await ManifestObtainer.browserObtainManifest(browser); + is(short_name, "no cookie"); + const tab = gBrowser.getTabForBrowser(browser); + gBrowser.removeTab(tab); +}); diff --git a/dom/manifest/test/browser_ManifestObtainer_obtain.js b/dom/manifest/test/browser_ManifestObtainer_obtain.js new file mode 100644 index 0000000000..a6d1dd6f23 --- /dev/null +++ b/dom/manifest/test/browser_ManifestObtainer_obtain.js @@ -0,0 +1,268 @@ +"use strict"; + +Services.prefs.setBoolPref("dom.manifest.enabled", true); +Services.prefs.setBoolPref("dom.security.https_first", false); + +const { ManifestObtainer } = ChromeUtils.importESModule( + "resource://gre/modules/ManifestObtainer.sys.mjs" +); +const remoteURL = + "http://mochi.test:8888/browser/dom/manifest/test/resource.sjs"; +const defaultURL = new URL( + "http://example.org/browser/dom/manifest/test/resource.sjs" +); +defaultURL.searchParams.set("Content-Type", "text/html; charset=utf-8"); +requestLongerTimeout(4); + +const tests = [ + // Fetch tests. + { + body: ``, + run(manifest) { + is(manifest, null, "Manifest without a href yields a null manifest."); + }, + }, + { + body: ``, + run(manifest) { + is(manifest, null, "Manifest without a href yields a null manifest."); + }, + }, + { + body: ` + + + `, + run(manifest) { + is( + manifest.name, + "pass-1", + "Manifest is first `link` where @rel contains token manifest." + ); + }, + }, + { + body: ` + + + `, + run(manifest) { + is( + manifest.name, + "pass-2", + "Manifest is first `link` where @rel contains token manifest." + ); + }, + }, + { + body: ``, + run(err) { + is( + err.name, + "TypeError", + "By default, manifest cannot load cross-origin." + ); + }, + }, + // CORS Tests. + { + get body() { + const body = 'body={"name": "pass-4"}'; + const CORS = `Access-Control-Allow-Origin=${defaultURL.origin}`; + const link = ``; + return link; + }, + run(manifest) { + is(manifest.name, "pass-4", "CORS enabled, manifest must be fetched."); + }, + }, + { + get body() { + const body = 'body={"name": "fail"}'; + const CORS = "Access-Control-Allow-Origin=http://not-here"; + const link = ``; + return link; + }, + run(err) { + is( + err.name, + "TypeError", + "Fetch blocked by CORS - origin does not match." + ); + }, + }, + { + body: ``, + run(err) { + is( + err.name, + "TypeError", + "Trying to load from about:whatever is TypeError." + ); + }, + }, + { + body: ``, + run(err) { + is( + err.name, + "TypeError", + "Trying to load from file://whatever is a TypeError." + ); + }, + }, + // URL parsing tests + { + body: ``, + run(err) { + is(err.name, "TypeError", "Trying to load invalid URL is a TypeError."); + }, + }, +]; + +function makeTestURL({ body }) { + const url = new URL(defaultURL); + url.searchParams.set("body", encodeURIComponent(body)); + return url.href; +} + +add_task(async function () { + const promises = tests + .map(test => ({ + gBrowser, + testRunner: testObtainingManifest(test), + url: makeTestURL(test), + })) + .reduce((collector, tabOpts) => { + const promise = BrowserTestUtils.withNewTab(tabOpts, tabOpts.testRunner); + collector.push(promise); + return collector; + }, []); + + await Promise.all(promises); + + function testObtainingManifest(aTest) { + return async function (aBrowser) { + try { + const manifest = await ManifestObtainer.browserObtainManifest(aBrowser); + aTest.run(manifest); + } catch (e) { + aTest.run(e); + } + }; + } +}); + +add_task(async () => { + // This loads a generic html page. + const url = new URL(defaultURL); + // The body get injected into the page on the server. + const body = ``; + url.searchParams.set("body", encodeURIComponent(body)); + + // Let's open a tab! + const tabOpts = { + gBrowser, + url: url.href, + }; + // Let's do the test + await BrowserTestUtils.withNewTab(tabOpts, async aBrowser => { + const obtainerOpts = { + checkConformance: true, // gives us back "moz_manifest_url" member + }; + const manifest = await ManifestObtainer.browserObtainManifest( + aBrowser, + obtainerOpts + ); + is(manifest.name, "conformance check"); + ok("moz_manifest_url" in manifest, "Has a moz_manifest_url member"); + const testString = defaultURL.origin + defaultURL.pathname; + ok( + manifest.moz_manifest_url.startsWith(testString), + `Expect to start with with the testString, but got ${manifest.moz_manifest_url} instead,` + ); + // Clean up! + gBrowser.removeTab(gBrowser.getTabForBrowser(aBrowser)); + }); +}); + +/* + * e10s race condition tests + * Open a bunch of tabs and load manifests + * in each tab. They should all return pass. + */ +add_task(async function () { + const defaultPath = "/browser/dom/manifest/test/manifestLoader.html"; + const tabURLs = [ + `http://example.com:80${defaultPath}`, + `http://example.org:80${defaultPath}`, + `http://example.org:8000${defaultPath}`, + `http://mochi.test:8888${defaultPath}`, + `http://sub1.test1.example.com:80${defaultPath}`, + `http://sub1.test1.example.org:80${defaultPath}`, + `http://sub1.test1.example.org:8000${defaultPath}`, + `http://sub1.test1.mochi.test:8888${defaultPath}`, + `http://sub1.test2.example.com:80${defaultPath}`, + `http://sub1.test2.example.org:80${defaultPath}`, + `http://sub1.test2.example.org:8000${defaultPath}`, + `http://sub2.test1.example.com:80${defaultPath}`, + `http://sub2.test1.example.org:80${defaultPath}`, + `http://sub2.test1.example.org:8000${defaultPath}`, + `http://sub2.test2.example.com:80${defaultPath}`, + `http://sub2.test2.example.org:80${defaultPath}`, + `http://sub2.test2.example.org:8000${defaultPath}`, + `http://sub2.xn--lt-uia.mochi.test:8888${defaultPath}`, + `http://test1.example.com:80${defaultPath}`, + `http://test1.example.org:80${defaultPath}`, + `http://test1.example.org:8000${defaultPath}`, + `http://test1.mochi.test:8888${defaultPath}`, + `http://test2.example.com:80${defaultPath}`, + `http://test2.example.org:80${defaultPath}`, + `http://test2.example.org:8000${defaultPath}`, + `http://test2.mochi.test:8888${defaultPath}`, + `http://test:80${defaultPath}`, + `http://www.example.com:80${defaultPath}`, + ]; + // Open tabs an collect corresponding browsers + let browsers = tabURLs.map( + url => BrowserTestUtils.addTab(gBrowser, url).linkedBrowser + ); + + // Once all the pages have loaded, run a bunch of tests in "parallel". + await Promise.all( + (function* () { + for (let browser of browsers) { + yield BrowserTestUtils.browserLoaded(browser); + } + })() + ); + // Flood random browsers with requests. Once promises settle, check that + // responses all pass. + const results = await Promise.all( + (function* () { + for (let browser of randBrowsers(browsers, 50)) { + yield ManifestObtainer.browserObtainManifest(browser); + } + })() + ); + const pass = results.every(manifest => manifest.name === "pass"); + ok(pass, "Expect every manifest to have name equal to `pass`."); + // cleanup + browsers + .map(browser => gBrowser.getTabForBrowser(browser)) + .forEach(tab => gBrowser.removeTab(tab)); + + // Helper generator, spits out random browsers + function* randBrowsers(aBrowsers, aMax) { + for (let i = 0; i < aMax; i++) { + const randNum = Math.round(Math.random() * (aBrowsers.length - 1)); + yield aBrowsers[randNum]; + } + } +}); diff --git a/dom/manifest/test/browser_Manifest_install.js b/dom/manifest/test/browser_Manifest_install.js new file mode 100644 index 0000000000..d3b949be19 --- /dev/null +++ b/dom/manifest/test/browser_Manifest_install.js @@ -0,0 +1,58 @@ +"use strict"; + +const { Manifests } = ChromeUtils.importESModule( + "resource://gre/modules/Manifest.sys.mjs" +); + +const defaultURL = new URL( + "http://example.org/browser/dom/manifest/test/resource.sjs" +); +defaultURL.searchParams.set("Content-Type", "application/manifest+json"); + +const manifestMock = JSON.stringify({ + short_name: "hello World", + scope: "/browser/", +}); +const manifestUrl = `${defaultURL}&body=${manifestMock}`; + +function makeTestURL() { + const url = new URL(defaultURL); + const body = ``; + url.searchParams.set("Content-Type", "text/html; charset=utf-8"); + url.searchParams.set("body", encodeURIComponent(body)); + return url.href; +} + +add_task(async function () { + const tabOptions = { gBrowser, url: makeTestURL() }; + + await BrowserTestUtils.withNewTab(tabOptions, async function (browser) { + let manifest = await Manifests.getManifest(browser, manifestUrl); + is(manifest.installed, false, "We haven't installed this manifest yet"); + + await manifest.install(browser); + is(manifest.name, "hello World", "Manifest has correct name"); + is(manifest.installed, true, "Manifest is installed"); + is(manifest.url, manifestUrl, "has correct url"); + is(manifest.browser, browser, "has correct browser"); + + manifest = await Manifests.getManifest(browser, manifestUrl); + is(manifest.installed, true, "New instances are installed"); + + manifest = await Manifests.getManifest(browser); + is(manifest.installed, true, "Will find manifest without being given url"); + + let foundManifest = Manifests.findManifestUrl( + "http://example.org/browser/dom/" + ); + is(foundManifest, manifestUrl, "Finds manifests within scope"); + + foundManifest = Manifests.findManifestUrl("http://example.org/"); + is(foundManifest, null, "Does not find manifests outside scope"); + }); + // Get the cached one now + await BrowserTestUtils.withNewTab(tabOptions, async browser => { + const manifest = await Manifests.getManifest(browser, manifestUrl); + is(manifest.browser, browser, "has updated browser object"); + }); +}); diff --git a/dom/manifest/test/common.js b/dom/manifest/test/common.js new file mode 100644 index 0000000000..50f27c119a --- /dev/null +++ b/dom/manifest/test/common.js @@ -0,0 +1,139 @@ +/** + * Common infrastructure for manifest tests. + **/ +"use strict"; +const { ManifestProcessor } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/ManifestProcessor.sys.mjs" +); +const processor = ManifestProcessor; +const manifestURL = new URL(document.location.origin + "/manifest.json"); +const docURL = document.location; +const seperators = + "\u2028\u2029\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000"; +const lineTerminators = "\u000D\u000A\u2028\u2029"; +const whiteSpace = `${seperators}${lineTerminators}`; +const typeTests = [1, null, {}, [], false]; +const data = { + jsonText: "{}", + manifestURL, + docURL, +}; + +const validThemeColors = [ + ["maroon", "#800000ff"], + ["#f00", "#ff0000ff"], + ["#ff0000", "#ff0000ff"], + ["rgb(255,0,0)", "#ff0000ff"], + ["rgb(255,0,0,1)", "#ff0000ff"], + ["rgb(255,0,0,1.0)", "#ff0000ff"], + ["rgb(255,0,0,100%)", "#ff0000ff"], + ["rgb(255 0 0)", "#ff0000ff"], + ["rgb(255 0 0 / 1)", "#ff0000ff"], + ["rgb(255 0 0 / 1.0)", "#ff0000ff"], + ["rgb(255 0 0 / 100%)", "#ff0000ff"], + ["rgb(100%, 0%, 0%)", "#ff0000ff"], + ["rgb(100%, 0%, 0%, 1)", "#ff0000ff"], + ["rgb(100%, 0%, 0%, 1.0)", "#ff0000ff"], + ["rgb(100%, 0%, 0%, 100%)", "#ff0000ff"], + ["rgb(100% 0% 0%)", "#ff0000ff"], + ["rgb(100% 0% 0% / 1)", "#ff0000ff"], + ["rgb(100%, 0%, 0%, 1.0)", "#ff0000ff"], + ["rgb(100%, 0%, 0%, 100%)", "#ff0000ff"], + ["rgb(300,0,0)", "#ff0000ff"], + ["rgb(300 0 0)", "#ff0000ff"], + ["rgb(255,-10,0)", "#ff0000ff"], + ["rgb(110%, 0%, 0%)", "#ff0000ff"], + ["rgba(255,0,0)", "#ff0000ff"], + ["rgba(255,0,0,1)", "#ff0000ff"], + ["rgba(255 0 0 / 1)", "#ff0000ff"], + ["rgba(100%,0%,0%,1)", "#ff0000ff"], + ["rgba(0,0,255,0.5)", "#0000ff80"], + ["rgba(100%, 50%, 0%, 0.1)", "#ff80001a"], + ["hsl(120, 100%, 50%)", "#00ff00ff"], + ["hsl(120 100% 50%)", "#00ff00ff"], + ["hsl(120, 100%, 50%, 1.0)", "#00ff00ff"], + ["hsl(120 100% 50% / 1.0)", "#00ff00ff"], + ["hsla(120, 100%, 50%)", "#00ff00ff"], + ["hsla(120 100% 50%)", "#00ff00ff"], + ["hsla(120, 100%, 50%, 1.0)", "#00ff00ff"], + ["hsla(120 100% 50% / 1.0)", "#00ff00ff"], + ["hsl(120deg, 100%, 50%)", "#00ff00ff"], + ["hsl(133.33333333grad, 100%, 50%)", "#00ff00ff"], + ["hsl(2.0943951024rad, 100%, 50%)", "#00ff00ff"], + ["hsl(0.3333333333turn, 100%, 50%)", "#00ff00ff"], +]; + +function setupManifest(key, value) { + const manifest = {}; + manifest[key] = value; + data.jsonText = JSON.stringify(manifest); +} + +function testValidColors(key) { + validThemeColors.forEach(item => { + const [manifest_color, parsed_color] = item; + setupManifest(key, manifest_color); + const result = processor.process(data); + + is( + result[key], + parsed_color, + `Expect ${key} to be returned for ${manifest_color}` + ); + }); + + // Trim tests + validThemeColors.forEach(item => { + const [manifest_color, parsed_color] = item; + const expandedThemeColor = `${seperators}${lineTerminators}${manifest_color}${lineTerminators}${seperators}`; + setupManifest(key, expandedThemeColor); + const result = processor.process(data); + + is( + result[key], + parsed_color, + `Expect trimmed ${key} to be returned for ${manifest_color}` + ); + }); +} + +const invalidThemeColors = [ + "marooon", + "f000000", + "#ff00000", + "rgb(100, 0%, 0%)", + "rgb(255,0)", + "rbg(255,-10,0)", + "rgb(110, 0%, 0%)", + "(255,0,0) }", + "rgba(255)", + " rgb(100%,0%,0%) }", + "hsl(120, 100%, 50)", + "hsl(120, 100%, 50.0)", + "hsl 120, 100%, 50%", + "hsla{120, 100%, 50%, 1}", +]; + +function testInvalidColors(key) { + typeTests.forEach(type => { + setupManifest(key, type); + const result = processor.process(data); + + is( + result[key], + undefined, + `Expect non-string ${key} to be undefined: ${typeof type}.` + ); + }); + + invalidThemeColors.forEach(manifest_color => { + setupManifest(key, manifest_color); + const result = processor.process(data); + + is( + result[key], + undefined, + `Expect ${key} to be undefined: ${manifest_color}.` + ); + }); +} diff --git a/dom/manifest/test/cookie_checker.sjs b/dom/manifest/test/cookie_checker.sjs new file mode 100644 index 0000000000..5405a6207b --- /dev/null +++ b/dom/manifest/test/cookie_checker.sjs @@ -0,0 +1,24 @@ +"use strict"; +let { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200); + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/json", false); + + // CORS stuff + const origin = request.hasHeader("Origin") + ? request.getHeader("Origin") + : null; + if (origin) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Credentials", "true"); + } + const short_name = request.hasHeader("Cookie") + ? request.getHeader("Cookie") + : "no cookie"; + response.write(JSON.stringify({ short_name })); +} diff --git a/dom/manifest/test/cookie_setter.html b/dom/manifest/test/cookie_setter.html new file mode 100644 index 0000000000..d8f412fbcb --- /dev/null +++ b/dom/manifest/test/cookie_setter.html @@ -0,0 +1,8 @@ + + + + diff --git a/dom/manifest/test/cookie_setter_with_credentials.html b/dom/manifest/test/cookie_setter_with_credentials.html new file mode 100644 index 0000000000..0d721b55ef --- /dev/null +++ b/dom/manifest/test/cookie_setter_with_credentials.html @@ -0,0 +1,6 @@ + + + + diff --git a/dom/manifest/test/cookie_setter_with_credentials_cross_origin.html b/dom/manifest/test/cookie_setter_with_credentials_cross_origin.html new file mode 100644 index 0000000000..6d1c4045c3 --- /dev/null +++ b/dom/manifest/test/cookie_setter_with_credentials_cross_origin.html @@ -0,0 +1,14 @@ + + +

Cross origin cookie sender

+ + + diff --git a/dom/manifest/test/file_testserver.sjs b/dom/manifest/test/file_testserver.sjs new file mode 100644 index 0000000000..aec29ee144 --- /dev/null +++ b/dom/manifest/test/file_testserver.sjs @@ -0,0 +1,56 @@ +"use strict"; +let { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +function loadHTMLFromFile(path) { + // Load the HTML to return in the response from file. + // Since it's relative to the cwd of the test runner, we start there and + // append to get to the actual path of the file. + const testHTMLFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + const testHTMLFileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + + path + .split("/") + .filter(path_1 => path_1) + .reduce((file, path_2) => { + testHTMLFile.append(path_2); + return testHTMLFile; + }, testHTMLFile); + testHTMLFileStream.init(testHTMLFile, -1, 0, 0); + const isAvailable = testHTMLFileStream.available(); + return NetUtil.readInputStreamToString(testHTMLFileStream, isAvailable); +} + +function handleRequest(request, response) { + const query = new URLSearchParams(request.queryString); + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // Deliver the CSP policy encoded in the URL + if (query.has("csp")) { + response.setHeader("Content-Security-Policy", query.get("csp"), false); + } + + // Deliver the CSPRO policy encoded in the URL + if (query.has("cspro")) { + response.setHeader( + "Content-Security-Policy-Report-Only", + query.get("cspro"), + false + ); + } + + // Deliver the CORS header in the URL + if (query.has("cors")) { + response.setHeader("Access-Control-Allow-Origin", query.get("cors"), false); + } + + // Send HTML to test allowed/blocked behaviors + response.setHeader("Content-Type", "text/html", false); + response.write(loadHTMLFromFile(query.get("file"))); +} diff --git a/dom/manifest/test/icon.png b/dom/manifest/test/icon.png new file mode 100644 index 0000000000..bb87f783b7 Binary files /dev/null and b/dom/manifest/test/icon.png differ diff --git a/dom/manifest/test/manifestLoader.html b/dom/manifest/test/manifestLoader.html new file mode 100644 index 0000000000..e244260902 --- /dev/null +++ b/dom/manifest/test/manifestLoader.html @@ -0,0 +1,13 @@ + + + + +

Manifest loader

+

Uses resource.sjs to load a Web Manifest that can be loaded cross-origin. The manifest looks like this:

+
+{
+	"name":"pass"
+}
+
diff --git a/dom/manifest/test/mochitest.toml b/dom/manifest/test/mochitest.toml new file mode 100644 index 0000000000..e674c3229e --- /dev/null +++ b/dom/manifest/test/mochitest.toml @@ -0,0 +1,43 @@ +[DEFAULT] +support-files = [ + "common.js", + "resource.sjs", + "manifestLoader.html", + "file_testserver.sjs", +] + +["test_ImageObjectProcessor_purpose.html"] + +["test_ImageObjectProcessor_sizes.html"] + +["test_ImageObjectProcessor_src.html"] + +["test_ImageObjectProcessor_type.html"] + +["test_ManifestProcessor_JSON.html"] + +["test_ManifestProcessor_background_color.html"] + +["test_ManifestProcessor_dir.html"] + +["test_ManifestProcessor_display.html"] + +["test_ManifestProcessor_icons.html"] + +["test_ManifestProcessor_id.html"] + +["test_ManifestProcessor_lang.html"] + +["test_ManifestProcessor_name_and_short_name.html"] + +["test_ManifestProcessor_orientation.html"] + +["test_ManifestProcessor_scope.html"] + +["test_ManifestProcessor_start_url.html"] + +["test_ManifestProcessor_theme_color.html"] + +["test_ManifestProcessor_warnings.html"] + +["test_link_relList_supports_manifest.html"] diff --git a/dom/manifest/test/red-50.png b/dom/manifest/test/red-50.png new file mode 100644 index 0000000000..e2ce365c3c Binary files /dev/null and b/dom/manifest/test/red-50.png differ diff --git a/dom/manifest/test/resource.sjs b/dom/manifest/test/resource.sjs new file mode 100644 index 0000000000..eadac95b64 --- /dev/null +++ b/dom/manifest/test/resource.sjs @@ -0,0 +1,85 @@ +/* Generic responder that composes a response from + * the query string of a request. + * + * It reserves some special prop names: + * - body: get's used as the response body + * - statusCode: override the 200 OK response code + * (response text is set automatically) + * + * Any property names it doesn't know about get converted into + * HTTP headers. + * + * For example: + * http://test/resource.sjs?Content-Type=text/html&body=

hello

&Hello=hi + * + * Outputs: + * HTTP/1.1 200 OK + * Content-Type: text/html + * Hello: hi + *

hello

+ */ +//global handleRequest +"use strict"; + +const HTTPStatus = new Map([ + [100, "Continue"], + [101, "Switching Protocol"], + [200, "OK"], + [201, "Created"], + [202, "Accepted"], + [203, "Non-Authoritative Information"], + [204, "No Content"], + [205, "Reset Content"], + [206, "Partial Content"], + [300, "Multiple Choice"], + [301, "Moved Permanently"], + [302, "Found"], + [303, "See Other"], + [304, "Not Modified"], + [305, "Use Proxy"], + [306, "unused"], + [307, "Temporary Redirect"], + [308, "Permanent Redirect"], + [400, "Bad Request"], + [401, "Unauthorized"], + [402, "Payment Required"], + [403, "Forbidden"], + [404, "Not Found"], + [405, "Method Not Allowed"], + [406, "Not Acceptable"], + [407, "Proxy Authentication Required"], + [408, "Request Timeout"], + [409, "Conflict"], + [410, "Gone"], + [411, "Length Required"], + [412, "Precondition Failed"], + [413, "Request Entity Too Large"], + [414, "Request-URI Too Long"], + [415, "Unsupported Media Type"], + [416, "Requested Range Not Satisfiable"], + [417, "Expectation Failed"], + [500, "Internal Server Error"], + [501, "Not Implemented"], + [502, "Bad Gateway"], + [503, "Service Unavailable"], + [504, "Gateway Timeout"], + [505, "HTTP Version Not Supported"], +]); + +function handleRequest(request, response) { + const queryMap = new URLSearchParams(request.queryString); + if (queryMap.has("statusCode")) { + let statusCode = parseInt(queryMap.get("statusCode")); + let statusText = HTTPStatus.get(statusCode); + queryMap.delete("statusCode"); + response.setStatusLine("1.1", statusCode, statusText); + } + if (queryMap.has("body")) { + let body = queryMap.get("body") || ""; + queryMap.delete("body"); + response.write(decodeURIComponent(body)); + } + for (let [key, value] of queryMap.entries()) { + response.setHeader(key, value); + } +} diff --git a/dom/manifest/test/test_ImageObjectProcessor_purpose.html b/dom/manifest/test/test_ImageObjectProcessor_purpose.html new file mode 100644 index 0000000000..386c4566d4 --- /dev/null +++ b/dom/manifest/test/test_ImageObjectProcessor_purpose.html @@ -0,0 +1,120 @@ + + + + + + Test for Bug + + + + + diff --git a/dom/manifest/test/test_ImageObjectProcessor_sizes.html b/dom/manifest/test/test_ImageObjectProcessor_sizes.html new file mode 100644 index 0000000000..9418b44118 --- /dev/null +++ b/dom/manifest/test/test_ImageObjectProcessor_sizes.html @@ -0,0 +1,88 @@ + + + + + + Test for Bug 1079453 + + + + + diff --git a/dom/manifest/test/test_ImageObjectProcessor_src.html b/dom/manifest/test/test_ImageObjectProcessor_src.html new file mode 100644 index 0000000000..8d17c3048f --- /dev/null +++ b/dom/manifest/test/test_ImageObjectProcessor_src.html @@ -0,0 +1,110 @@ + + + + + + Test for Bug 1079453 + + + + + diff --git a/dom/manifest/test/test_ImageObjectProcessor_type.html b/dom/manifest/test/test_ImageObjectProcessor_type.html new file mode 100644 index 0000000000..29627c5e46 --- /dev/null +++ b/dom/manifest/test/test_ImageObjectProcessor_type.html @@ -0,0 +1,57 @@ + + + + + + Test for Bug 1079453 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_JSON.html b/dom/manifest/test/test_ManifestProcessor_JSON.html new file mode 100644 index 0000000000..b072355f20 --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_JSON.html @@ -0,0 +1,48 @@ + + + + + + Test for Bug 1079453 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_background_color.html b/dom/manifest/test/test_ManifestProcessor_background_color.html new file mode 100644 index 0000000000..38073b8e4f --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_background_color.html @@ -0,0 +1,22 @@ + + + + + + Test for Bug 1195018 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_dir.html b/dom/manifest/test/test_ManifestProcessor_dir.html new file mode 100644 index 0000000000..b5dc84a9e4 --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_dir.html @@ -0,0 +1,57 @@ + + + + + + Test for Bug 1258899 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_display.html b/dom/manifest/test/test_ManifestProcessor_display.html new file mode 100644 index 0000000000..f5ea5fbc4f --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_display.html @@ -0,0 +1,78 @@ + + + + + + Test for Bug 1079453 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_icons.html b/dom/manifest/test/test_ManifestProcessor_icons.html new file mode 100644 index 0000000000..578f49f5dc --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_icons.html @@ -0,0 +1,30 @@ + + + + + + Test for Bug 1079453 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_id.html b/dom/manifest/test/test_ManifestProcessor_id.html new file mode 100644 index 0000000000..336d1d3a77 --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_id.html @@ -0,0 +1,123 @@ + + + + + + Test for Bug 1731940 - implement id member + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_lang.html b/dom/manifest/test/test_ManifestProcessor_lang.html new file mode 100644 index 0000000000..568950c2ba --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_lang.html @@ -0,0 +1,139 @@ + + + + +Test for Bug 1143879 - Implement lang member of Web manifest + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_name_and_short_name.html b/dom/manifest/test/test_ManifestProcessor_name_and_short_name.html new file mode 100644 index 0000000000..e843b583e4 --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_name_and_short_name.html @@ -0,0 +1,79 @@ + + + + + + Test for Bug 1079453 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_orientation.html b/dom/manifest/test/test_ManifestProcessor_orientation.html new file mode 100644 index 0000000000..9333eecbe3 --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_orientation.html @@ -0,0 +1,86 @@ + + + + + + Test for Bug 1079453 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_scope.html b/dom/manifest/test/test_ManifestProcessor_scope.html new file mode 100644 index 0000000000..590d76a251 --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_scope.html @@ -0,0 +1,113 @@ + + + + + + Test for Bug 1079453 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_start_url.html b/dom/manifest/test/test_ManifestProcessor_start_url.html new file mode 100644 index 0000000000..1d172df555 --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_start_url.html @@ -0,0 +1,83 @@ + + + + + + Test for Bug 1079453 + + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_theme_color.html b/dom/manifest/test/test_ManifestProcessor_theme_color.html new file mode 100644 index 0000000000..c73832748d --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_theme_color.html @@ -0,0 +1,22 @@ + + + + + + Test for Bug 1195018 + + + + + diff --git a/dom/manifest/test/test_ManifestProcessor_warnings.html b/dom/manifest/test/test_ManifestProcessor_warnings.html new file mode 100644 index 0000000000..f2092c1dcf --- /dev/null +++ b/dom/manifest/test/test_ManifestProcessor_warnings.html @@ -0,0 +1,149 @@ + + + + + + Test for Bug 1086997 + + + + + diff --git a/dom/manifest/test/test_link_relList_supports_manifest.html b/dom/manifest/test/test_link_relList_supports_manifest.html new file mode 100644 index 0000000000..2a5c0e1ee1 --- /dev/null +++ b/dom/manifest/test/test_link_relList_supports_manifest.html @@ -0,0 +1,47 @@ + + + + + + + Test for Bug 1596040 - Link relList support returns false for manifest + + + + -- cgit v1.2.3