summaryrefslogtreecommitdiffstats
path: root/remote/webdriver-bidi/modules/root/storage.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /remote/webdriver-bidi/modules/root/storage.sys.mjs
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/webdriver-bidi/modules/root/storage.sys.mjs')
-rw-r--r--remote/webdriver-bidi/modules/root/storage.sys.mjs770
1 files changed, 770 insertions, 0 deletions
diff --git a/remote/webdriver-bidi/modules/root/storage.sys.mjs b/remote/webdriver-bidi/modules/root/storage.sys.mjs
new file mode 100644
index 0000000000..50fbd8ecd6
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/storage.sys.mjs
@@ -0,0 +1,770 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
+ BytesValueType:
+ "chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+});
+
+const CookieFieldsMapping = {
+ domain: "host",
+ expiry: "expiry",
+ httpOnly: "isHttpOnly",
+ name: "name",
+ path: "path",
+ sameSite: "sameSite",
+ secure: "isSecure",
+ size: "size",
+ value: "value",
+};
+
+const MAX_COOKIE_EXPIRY = Number.MAX_SAFE_INTEGER;
+
+/**
+ * Enum of possible partition types supported by the
+ * storage.getCookies command.
+ *
+ * @readonly
+ * @enum {PartitionType}
+ */
+const PartitionType = {
+ Context: "context",
+ StorageKey: "storageKey",
+};
+
+const PartitionKeyAttributes = ["sourceOrigin", "userContext"];
+
+/**
+ * Enum of possible SameSite types supported by the
+ * storage.getCookies command.
+ *
+ * @readonly
+ * @enum {SameSiteType}
+ */
+const SameSiteType = {
+ [Ci.nsICookie.SAMESITE_NONE]: "none",
+ [Ci.nsICookie.SAMESITE_LAX]: "lax",
+ [Ci.nsICookie.SAMESITE_STRICT]: "strict",
+};
+
+class StorageModule extends Module {
+ destroy() {}
+
+ /**
+ * Used as an argument for storage.getCookies command
+ * to represent fields which should be used to filter the output
+ * of the command.
+ *
+ * @typedef CookieFilter
+ *
+ * @property {string=} domain
+ * @property {number=} expiry
+ * @property {boolean=} httpOnly
+ * @property {string=} name
+ * @property {string=} path
+ * @property {SameSiteType=} sameSite
+ * @property {boolean=} secure
+ * @property {number=} size
+ * @property {Network.BytesValueType=} value
+ */
+
+ /**
+ * Used as an argument for storage.getCookies command as one of the available variants
+ * {BrowsingContextPartitionDescriptor} or {StorageKeyPartitionDescriptor}, to represent
+ * fields should be used to build a partition key.
+ *
+ * @typedef PartitionDescriptor
+ */
+
+ /**
+ * @typedef BrowsingContextPartitionDescriptor
+ *
+ * @property {PartitionType} [type=PartitionType.context]
+ * @property {string} context
+ */
+
+ /**
+ * @typedef StorageKeyPartitionDescriptor
+ *
+ * @property {PartitionType} [type=PartitionType.storageKey]
+ * @property {string=} sourceOrigin
+ * @property {string=} userContext (not supported)
+ */
+
+ /**
+ * @typedef PartitionKey
+ *
+ * @property {string=} sourceOrigin
+ * @property {string=} userContext (not supported)
+ */
+
+ /**
+ * An object that holds the result of storage.getCookies command.
+ *
+ * @typedef GetCookiesResult
+ *
+ * @property {Array<Cookie>} cookies
+ * List of cookies.
+ * @property {PartitionKey} partitionKey
+ * An object which represent the partition key which was used
+ * to retrieve the cookies.
+ */
+
+ /**
+ * Retrieve zero or more cookies which match a set of provided parameters.
+ *
+ * @param {object=} options
+ * @param {CookieFilter=} options.filter
+ * An object which holds field names and values, which
+ * should be used to filter the output of the command.
+ * @param {PartitionDescriptor=} options.partition
+ * An object which holds the information which
+ * should be used to build a partition key.
+ *
+ * @returns {GetCookiesResult}
+ * An object which holds a list of retrieved cookies and
+ * the partition key which was used.
+ * @throws {InvalidArgumentError}
+ * If the provided arguments are not valid.
+ * @throws {NoSuchFrameError}
+ * If the provided browsing context cannot be found.
+ * @throws {UnsupportedOperationError}
+ * Raised when the command is called with `userContext` as
+ * in `partition` argument.
+ */
+ async getCookies(options = {}) {
+ let { filter = {} } = options;
+ const { partition: partitionSpec = null } = options;
+
+ this.#assertPartition(partitionSpec);
+ filter = this.#assertGetCookieFilter(filter);
+
+ const partitionKey = this.#expandStoragePartitionSpec(partitionSpec);
+ const store = this.#getTheCookieStore(partitionKey);
+ const cookies = this.#getMatchingCookies(store, filter);
+
+ // Bug 1875255. Exchange platform id for Webdriver BiDi id for the user context to return it to the client.
+ // For now we use platform user context id for returning cookies for a specific browsing context in the platform API,
+ // but we can not return it directly to the client, so for now we just remove it from the response.
+ delete partitionKey.userContext;
+
+ return { cookies, partitionKey };
+ }
+
+ /**
+ * An object representation of the cookie which should be set.
+ *
+ * @typedef PartialCookie
+ *
+ * @property {string} domain
+ * @property {number=} expiry
+ * @property {boolean=} httpOnly
+ * @property {string} name
+ * @property {string=} path
+ * @property {SameSiteType=} sameSite
+ * @property {boolean=} secure
+ * @property {number=} size
+ * @property {Network.BytesValueType} value
+ */
+
+ /**
+ * Create a new cookie in a cookie store.
+ *
+ * @param {object=} options
+ * @param {PartialCookie} options.cookie
+ * An object representation of the cookie which
+ * should be set.
+ * @param {PartitionDescriptor=} options.partition
+ * An object which holds the information which
+ * should be used to build a partition key.
+ *
+ * @returns {PartitionKey}
+ * An object with the partition key which was used to
+ * add the cookie.
+ * @throws {InvalidArgumentError}
+ * If the provided arguments are not valid.
+ * @throws {NoSuchFrameError}
+ * If the provided browsing context cannot be found.
+ * @throws {UnableToSetCookieError}
+ * If the cookie was not added.
+ * @throws {UnsupportedOperationError}
+ * Raised when the command is called with `userContext` as
+ * in `partition` argument.
+ */
+ async setCookie(options = {}) {
+ const { cookie: cookieSpec, partition: partitionSpec = null } = options;
+ lazy.assert.object(
+ cookieSpec,
+ `Expected "cookie" to be an object, got ${cookieSpec}`
+ );
+
+ const {
+ domain,
+ expiry = null,
+ httpOnly = null,
+ name,
+ path = null,
+ sameSite = null,
+ secure = null,
+ value,
+ } = cookieSpec;
+ this.#assertCookie({
+ domain,
+ expiry,
+ httpOnly,
+ name,
+ path,
+ sameSite,
+ secure,
+ value,
+ });
+ this.#assertPartition(partitionSpec);
+
+ const partitionKey = this.#expandStoragePartitionSpec(partitionSpec);
+
+ // The cookie store is defined by originAttributes.
+ const originAttributes = this.#getOriginAttributes(partitionKey);
+
+ const deserializedValue = this.#deserializeProtocolBytes(value);
+
+ // The XPCOM interface requires to be specified if a cookie is session.
+ const isSession = expiry === null;
+
+ let schemeType;
+ if (secure) {
+ schemeType = Ci.nsICookie.SCHEME_HTTPS;
+ } else {
+ schemeType = Ci.nsICookie.SCHEME_HTTP;
+ }
+
+ try {
+ Services.cookies.add(
+ domain,
+ path === null ? "/" : path,
+ name,
+ deserializedValue,
+ secure === null ? false : secure,
+ httpOnly === null ? false : httpOnly,
+ isSession,
+ // The XPCOM interface requires the expiry field even for session cookies.
+ expiry === null ? MAX_COOKIE_EXPIRY : expiry,
+ originAttributes,
+ this.#getSameSitePlatformProperty(sameSite),
+ schemeType
+ );
+ } catch (e) {
+ throw new lazy.error.UnableToSetCookieError(e);
+ }
+
+ // Bug 1875255. Exchange platform id for Webdriver BiDi id for the user context to return it to the client.
+ // For now we use platform user context id for returning cookies for a specific browsing context in the platform API,
+ // but we can not return it directly to the client, so for now we just remove it from the response.
+ delete partitionKey.userContext;
+
+ return { partitionKey };
+ }
+
+ #assertCookie(cookie) {
+ lazy.assert.object(
+ cookie,
+ `Expected "cookie" to be an object, got ${cookie}`
+ );
+
+ const { domain, expiry, httpOnly, name, path, sameSite, secure, value } =
+ cookie;
+
+ lazy.assert.string(
+ domain,
+ `Expected "domain" to be a string, got ${domain}`
+ );
+
+ lazy.assert.string(name, `Expected "name" to be a string, got ${name}`);
+
+ this.#assertValue(value);
+
+ if (expiry !== null) {
+ lazy.assert.positiveInteger(
+ expiry,
+ `Expected "expiry" to be a positive number, got ${expiry}`
+ );
+ }
+
+ if (httpOnly !== null) {
+ lazy.assert.boolean(
+ httpOnly,
+ `Expected "httpOnly" to be a boolean, got ${httpOnly}`
+ );
+ }
+
+ if (path !== null) {
+ lazy.assert.string(path, `Expected "path" to be a string, got ${path}`);
+ }
+
+ this.#assertSameSite(sameSite);
+
+ if (secure !== null) {
+ lazy.assert.boolean(
+ secure,
+ `Expected "secure" to be a boolean, got ${secure}`
+ );
+ }
+ }
+
+ #assertGetCookieFilter(filter) {
+ lazy.assert.object(
+ filter,
+ `Expected "filter" to be an object, got ${filter}`
+ );
+
+ const {
+ domain = null,
+ expiry = null,
+ httpOnly = null,
+ name = null,
+ path = null,
+ sameSite = null,
+ secure = null,
+ size = null,
+ value = null,
+ } = filter;
+
+ if (domain !== null) {
+ lazy.assert.string(
+ domain,
+ `Expected "filter.domain" to be a string, got ${domain}`
+ );
+ }
+
+ if (expiry !== null) {
+ lazy.assert.positiveInteger(
+ expiry,
+ `Expected "filter.expiry" to be a positive number, got ${expiry}`
+ );
+ }
+
+ if (httpOnly !== null) {
+ lazy.assert.boolean(
+ httpOnly,
+ `Expected "filter.httpOnly" to be a boolean, got ${httpOnly}`
+ );
+ }
+
+ if (name !== null) {
+ lazy.assert.string(
+ name,
+ `Expected "filter.name" to be a string, got ${name}`
+ );
+ }
+
+ if (path !== null) {
+ lazy.assert.string(
+ path,
+ `Expected "filter.path" to be a string, got ${path}`
+ );
+ }
+
+ this.#assertSameSite(sameSite, "filter.sameSite");
+
+ if (secure !== null) {
+ lazy.assert.boolean(
+ secure,
+ `Expected "filter.secure" to be a boolean, got ${secure}`
+ );
+ }
+
+ if (size !== null) {
+ lazy.assert.positiveInteger(
+ size,
+ `Expected "filter.size" to be a positive number, got ${size}`
+ );
+ }
+
+ if (value !== null) {
+ this.#assertValue(value, "filter.value");
+ }
+
+ return {
+ domain,
+ expiry,
+ httpOnly,
+ name,
+ path,
+ sameSite,
+ secure,
+ size,
+ value,
+ };
+ }
+
+ #assertPartition(partitionSpec) {
+ if (partitionSpec === null) {
+ return;
+ }
+ lazy.assert.object(
+ partitionSpec,
+ `Expected "partition" to be an object, got ${partitionSpec}`
+ );
+
+ const { type } = partitionSpec;
+ lazy.assert.string(
+ type,
+ `Expected "partition.type" to be a string, got ${type}`
+ );
+
+ switch (type) {
+ case PartitionType.Context: {
+ const { context } = partitionSpec;
+ lazy.assert.string(
+ context,
+ `Expected "partition.context" to be a string, got ${context}`
+ );
+
+ break;
+ }
+
+ case PartitionType.StorageKey: {
+ const { sourceOrigin = null, userContext = null } = partitionSpec;
+ if (sourceOrigin !== null) {
+ lazy.assert.string(
+ sourceOrigin,
+ `Expected "partition.sourceOrigin" to be a string, got ${sourceOrigin}`
+ );
+ lazy.assert.that(
+ sourceOrigin => URL.canParse(sourceOrigin),
+ `Expected "partition.sourceOrigin" to be a valid URL, got ${sourceOrigin}`
+ )(sourceOrigin);
+
+ const url = new URL(sourceOrigin);
+ lazy.assert.that(
+ url => url.pathname === "/" && url.hash === "" && url.search === "",
+ `Expected "partition.sourceOrigin" to contain only origin, got ${sourceOrigin}`
+ )(url);
+ }
+ if (userContext !== null) {
+ lazy.assert.string(
+ userContext,
+ `Expected "partition.userContext" to be a string, got ${userContext}`
+ );
+
+ // TODO: Bug 1875255. Implement support for "userContext" field.
+ throw new lazy.error.UnsupportedOperationError(
+ `"userContext" as a field on "partition" argument is not supported yet for "storage.getCookies" command`
+ );
+ }
+ break;
+ }
+
+ default: {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "partition.type" to be one of ${Object.values(
+ PartitionType
+ )}, got ${type}`
+ );
+ }
+ }
+ }
+
+ #assertSameSite(sameSite, fieldName = "sameSite") {
+ if (sameSite !== null) {
+ const sameSiteTypeValue = Object.values(SameSiteType);
+ lazy.assert.in(
+ sameSite,
+ sameSiteTypeValue,
+ `Expected "${fieldName}" to be one of ${sameSiteTypeValue}, got ${sameSite}`
+ );
+ }
+ }
+
+ #assertValue(value, fieldName = "value") {
+ lazy.assert.object(
+ value,
+ `Expected "${fieldName}" to be an object, got ${value}`
+ );
+
+ const { type, value: protocolBytesValue } = value;
+
+ const bytesValueTypeValue = Object.values(lazy.BytesValueType);
+ lazy.assert.in(
+ type,
+ bytesValueTypeValue,
+ `Expected "${fieldName}.type" to be one of ${bytesValueTypeValue}, got ${type}`
+ );
+
+ lazy.assert.string(
+ protocolBytesValue,
+ `Expected "${fieldName}.value" to be string, got ${protocolBytesValue}`
+ );
+ }
+
+ /**
+ * Deserialize the value to string, since platform API
+ * returns cookie's value as a string.
+ */
+ #deserializeProtocolBytes(cookieValue) {
+ const { type, value } = cookieValue;
+
+ if (type === lazy.BytesValueType.String) {
+ return value;
+ }
+
+ // For type === BytesValueType.Base64.
+ return atob(value);
+ }
+
+ /**
+ * Build a partition key.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#expand-a-storage-partition-spec
+ */
+ #expandStoragePartitionSpec(partitionSpec) {
+ if (partitionSpec === null) {
+ partitionSpec = {};
+ }
+
+ if (partitionSpec.type === PartitionType.Context) {
+ const { context: contextId } = partitionSpec;
+ const browsingContext = this.#getBrowsingContext(contextId);
+
+ // Define browsing context’s associated storage partition as combination of user context id
+ // and the origin of the document in this browsing context.
+ return {
+ sourceOrigin: browsingContext.currentURI.prePath,
+ userContext: browsingContext.originAttributes.userContextId,
+ };
+ }
+
+ const partitionKey = {};
+ for (const keyName of PartitionKeyAttributes) {
+ if (keyName in partitionSpec) {
+ partitionKey[keyName] = partitionSpec[keyName];
+ }
+ }
+
+ return partitionKey;
+ }
+
+ /**
+ * Retrieves a browsing context based on its id.
+ *
+ * @param {number} contextId
+ * Id of the browsing context.
+ * @returns {BrowsingContext}
+ * The browsing context.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ */
+ #getBrowsingContext(contextId) {
+ const context = lazy.TabManager.getBrowsingContextById(contextId);
+ if (context === null) {
+ throw new lazy.error.NoSuchFrameError(
+ `Browsing Context with id ${contextId} not found`
+ );
+ }
+
+ return context;
+ }
+
+ /**
+ * Since cookies retrieved from the platform API
+ * always contain expiry even for session cookies,
+ * we should check ourselves if it's a session cookie
+ * and do not return expiry in case it is.
+ */
+ #getCookieExpiry(cookie) {
+ const { expiry, isSession } = cookie;
+ return isSession ? null : expiry;
+ }
+
+ #getCookieSize(cookie) {
+ const { name, value } = cookie;
+ return name.length + value.length;
+ }
+
+ /**
+ * Filter and serialize given cookies with provided filter.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#get-matching-cookies
+ */
+ #getMatchingCookies(cookieStore, filter) {
+ const cookies = [];
+
+ for (const storedCookie of cookieStore) {
+ const serializedCookie = this.#serializeCookie(storedCookie);
+ if (this.#matchCookie(serializedCookie, filter)) {
+ cookies.push(serializedCookie);
+ }
+ }
+ return cookies;
+ }
+
+ /**
+ * Prepare the data in the required for platform API format.
+ */
+ #getOriginAttributes(partitionKey) {
+ const originAttributes = {};
+
+ if (partitionKey.sourceOrigin) {
+ originAttributes.partitionKey = ChromeUtils.getPartitionKeyFromURL(
+ partitionKey.sourceOrigin
+ );
+ }
+ if ("userContext" in partitionKey) {
+ originAttributes.userContextId = partitionKey.userContext;
+ }
+
+ return originAttributes;
+ }
+
+ #getSameSitePlatformProperty(sameSite) {
+ switch (sameSite) {
+ case "lax": {
+ return Ci.nsICookie.SAMESITE_LAX;
+ }
+ case "strict": {
+ return Ci.nsICookie.SAMESITE_STRICT;
+ }
+ }
+
+ return Ci.nsICookie.SAMESITE_NONE;
+ }
+
+ /**
+ * Return a cookie store of the storage partition for a given storage partition key.
+ *
+ * The implementation differs here from the spec, since in gecko there is no
+ * direct way to get all the cookies for a given partition key.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#get-the-cookie-store
+ */
+ #getTheCookieStore(storagePartitionKey) {
+ let store = [];
+
+ // Prepare the data in the format required for the platform API.
+ const originAttributes = this.#getOriginAttributes(storagePartitionKey);
+ // In case we want to get the cookies for a certain `sourceOrigin`,
+ // we have to additionally specify `hostname`. When `sourceOrigin` is not present
+ // `hostname` will stay equal undefined.
+ let hostname;
+
+ // In case we want to get the cookies for a certain `sourceOrigin`,
+ // we have to separately retrieve cookies for a hostname built from `sourceOrigin`,
+ // and with `partitionKey` equal an empty string to retrieve the cookies that which were set
+ // by this hostname but without `partitionKey`, e.g. with `document.cookie`
+ if (storagePartitionKey.sourceOrigin) {
+ const url = new URL(storagePartitionKey.sourceOrigin);
+ hostname = url.hostname;
+
+ const principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(url),
+ {}
+ );
+ const isSecureProtocol = principal.isOriginPotentiallyTrustworthy;
+
+ // We want to keep `userContext` id here, if it's present,
+ // but set the `partitionKey` to an empty string.
+ const cookiesMatchingHostname =
+ Services.cookies.getCookiesWithOriginAttributes(
+ JSON.stringify({ ...originAttributes, partitionKey: "" }),
+ hostname
+ );
+
+ for (const cookie of cookiesMatchingHostname) {
+ // Ignore secure cookies for non-secure protocols.
+ if (cookie.isSecure && !isSecureProtocol) {
+ continue;
+ }
+ store.push(cookie);
+ }
+ }
+
+ // Add the cookies which exactly match a built partition attributes.
+ store = store.concat(
+ Services.cookies.getCookiesWithOriginAttributes(
+ JSON.stringify(originAttributes),
+ hostname
+ )
+ );
+
+ return store;
+ }
+
+ /**
+ * Match a provided cookie with provided filter.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#match-cookie
+ */
+ #matchCookie(storedCookie, filter) {
+ for (const [fieldName] of Object.entries(CookieFieldsMapping)) {
+ let value = filter[fieldName];
+ if (value !== null) {
+ let storedCookieValue = storedCookie[fieldName];
+
+ if (fieldName === "value") {
+ value = this.#deserializeProtocolBytes(value);
+ storedCookieValue = this.#deserializeProtocolBytes(storedCookieValue);
+ }
+
+ if (storedCookieValue !== value) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Serialize a cookie.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#serialize-cookie
+ */
+ #serializeCookie(storedCookie) {
+ const cookie = {};
+ for (const [serializedName, cookieName] of Object.entries(
+ CookieFieldsMapping
+ )) {
+ switch (serializedName) {
+ case "expiry": {
+ const expiry = this.#getCookieExpiry(storedCookie);
+ if (expiry !== null) {
+ cookie.expiry = expiry;
+ }
+ break;
+ }
+
+ case "sameSite":
+ cookie.sameSite = SameSiteType[storedCookie.sameSite];
+ break;
+
+ case "size":
+ cookie.size = this.#getCookieSize(storedCookie);
+ break;
+
+ case "value":
+ // Bug 1879309. Add support for non-UTF8 cookies,
+ // when a byte representation of value is available.
+ // For now, use a value field, which is returned as a string.
+ cookie.value = {
+ type: lazy.BytesValueType.String,
+ value: storedCookie.value,
+ };
+ break;
+
+ default:
+ cookie[serializedName] = storedCookie[cookieName];
+ }
+ }
+
+ return cookie;
+ }
+}
+
+export const storage = StorageModule;