forked from mirrors/gecko-dev
Bug 1875065 - [bidi] Implement "permissions.setPermission" command. r=webdriver-reviewers,jdescottes,whimboo
Differential Revision: https://phabricator.services.mozilla.com/D208864
This commit is contained in:
parent
0b87a388da
commit
c7406cf09f
9 changed files with 262 additions and 139 deletions
|
|
@ -3357,22 +3357,16 @@ GeckoDriver.prototype.setPermission = async function (cmd) {
|
|||
const { descriptor, state, oneRealm = false } = cmd.parameters;
|
||||
const browsingContext = lazy.assert.open(this.getBrowsingContext());
|
||||
|
||||
// XXX: We currently depend on camera/microphone tests throwing UnsupportedOperationError,
|
||||
// the fix is ongoing in bug 1609427.
|
||||
if (["camera", "microphone"].includes(descriptor.name)) {
|
||||
throw new lazy.error.UnsupportedOperationError(
|
||||
"setPermission: camera and microphone permissions are currently unsupported"
|
||||
);
|
||||
}
|
||||
lazy.permissions.validatePermission(descriptor.name);
|
||||
|
||||
// XXX: Allowing this permission causes timing related Android crash, see also bug 1878741
|
||||
// Bug 1878741: Allowing this permission causes timing related Android crash.
|
||||
if (descriptor.name === "notifications") {
|
||||
if (Services.prefs.getBoolPref("notification.prompt.testing", false)) {
|
||||
// Okay, do nothing. The notifications module will work without permission.
|
||||
return;
|
||||
}
|
||||
throw new lazy.error.UnsupportedOperationError(
|
||||
"setPermission: expected notification.prompt.testing to be set"
|
||||
`Setting "descriptor.name" "notifications" expected "notification.prompt.testing" preference to be set`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -3389,7 +3383,26 @@ GeckoDriver.prototype.setPermission = async function (cmd) {
|
|||
|
||||
lazy.assert.boolean(oneRealm);
|
||||
|
||||
lazy.permissions.set(params.type, params.state, oneRealm, browsingContext);
|
||||
if (!lazy.MarionettePrefs.setPermissionEnabled) {
|
||||
throw new lazy.error.UnsupportedOperationError(
|
||||
"'Set Permission' is not available"
|
||||
);
|
||||
}
|
||||
|
||||
let origin = browsingContext.currentURI.prePath;
|
||||
|
||||
// storage-access is a special case.
|
||||
if (descriptor.name === "storage-access") {
|
||||
origin = browsingContext.top.currentURI.prePath;
|
||||
|
||||
params = {
|
||||
type: lazy.permissions.getStorageAccessPermissionsType(
|
||||
browsingContext.currentWindowGlobal.documentURI
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
lazy.permissions.set(params, state, origin);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,71 +6,48 @@ const lazy = {};
|
|||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
||||
MarionettePrefs: "chrome://remote/content/marionette/prefs.sys.mjs",
|
||||
});
|
||||
|
||||
/** @namespace */
|
||||
export const permissions = {};
|
||||
|
||||
function mapToInternalPermissionParameters(browsingContext, permissionType) {
|
||||
const currentURI = browsingContext.currentWindowGlobal.documentURI;
|
||||
|
||||
// storage-access is quite special...
|
||||
if (permissionType === "storage-access") {
|
||||
const thirdPartyPrincipalSite = Services.eTLD.getSite(currentURI);
|
||||
|
||||
const topLevelURI = browsingContext.top.currentWindowGlobal.documentURI;
|
||||
const topLevelPrincipal =
|
||||
Services.scriptSecurityManager.createContentPrincipal(topLevelURI, {});
|
||||
|
||||
return {
|
||||
name: "3rdPartyFrameStorage^" + thirdPartyPrincipalSite,
|
||||
principal: topLevelPrincipal,
|
||||
};
|
||||
}
|
||||
|
||||
const currentPrincipal =
|
||||
Services.scriptSecurityManager.createContentPrincipal(currentURI, {});
|
||||
|
||||
return {
|
||||
name: permissionType,
|
||||
principal: currentPrincipal,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get a permission type for the "storage-access" permission.
|
||||
*
|
||||
* @param {nsIURI} uri
|
||||
* The URI to use for building the permission type.
|
||||
*
|
||||
* @returns {string} permissionType
|
||||
* The permission type for the "storage-access" permission.
|
||||
*/
|
||||
permissions.getStorageAccessPermissionsType = function (uri) {
|
||||
const thirdPartyPrincipalSite = Services.eTLD.getSite(uri);
|
||||
return "3rdPartyFrameStorage^" + thirdPartyPrincipalSite;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a permission's state.
|
||||
* Note: Currently just a shim to support testdriver's set_permission.
|
||||
* Set a permission given a permission descriptor, a permission state,
|
||||
* an origin.
|
||||
*
|
||||
* @param {object} permissionType
|
||||
* The Gecko internal permission type
|
||||
* @param {PermissionDescriptor} descriptor
|
||||
* The descriptor of the permission which will be updated.
|
||||
* @param {string} state
|
||||
* State of the permission. It can be `granted`, `denied` or `prompt`.
|
||||
* @param {boolean} oneRealm
|
||||
* Currently ignored
|
||||
* @param {browsingContext=} browsingContext
|
||||
* Current browsing context object
|
||||
* @param {string} origin
|
||||
* The origin which is used as a target for permission update.
|
||||
*
|
||||
* @throws {UnsupportedOperationError}
|
||||
* If `marionette.setpermission.enabled` is not set or
|
||||
* an unsupported permission is used.
|
||||
* If <var>state</var> has unsupported value.
|
||||
*/
|
||||
permissions.set = function (permissionType, state, oneRealm, browsingContext) {
|
||||
if (!lazy.MarionettePrefs.setPermissionEnabled) {
|
||||
throw new lazy.error.UnsupportedOperationError(
|
||||
"'Set Permission' is not available"
|
||||
);
|
||||
}
|
||||
|
||||
const { name, principal } = mapToInternalPermissionParameters(
|
||||
browsingContext,
|
||||
permissionType
|
||||
);
|
||||
permissions.set = function (descriptor, state, origin) {
|
||||
const principal =
|
||||
Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
|
||||
|
||||
switch (state) {
|
||||
case "granted": {
|
||||
Services.perms.addFromPrincipal(
|
||||
principal,
|
||||
name,
|
||||
descriptor.type,
|
||||
Services.perms.ALLOW_ACTION
|
||||
);
|
||||
return;
|
||||
|
|
@ -78,13 +55,13 @@ permissions.set = function (permissionType, state, oneRealm, browsingContext) {
|
|||
case "denied": {
|
||||
Services.perms.addFromPrincipal(
|
||||
principal,
|
||||
name,
|
||||
descriptor.type,
|
||||
Services.perms.DENY_ACTION
|
||||
);
|
||||
return;
|
||||
}
|
||||
case "prompt": {
|
||||
Services.perms.removeFromPrincipal(principal, name);
|
||||
Services.perms.removeFromPrincipal(principal, descriptor.type);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
|
|
@ -93,3 +70,21 @@ permissions.set = function (permissionType, state, oneRealm, browsingContext) {
|
|||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the permission.
|
||||
*
|
||||
* @param {string} permissionName
|
||||
* The name of the permission which will be validated.
|
||||
*
|
||||
* @throws {UnsupportedOperationError}
|
||||
* If <var>permissionName</var> is not supported.
|
||||
*/
|
||||
permissions.validatePermission = function (permissionName) {
|
||||
// Bug 1609427: PermissionDescriptor for "camera" and "microphone" are not yet implemented.
|
||||
if (["camera", "microphone"].includes(permissionName)) {
|
||||
throw new lazy.error.UnsupportedOperationError(
|
||||
`"descriptor.name" "${permissionName}" is currently unsupported`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1028,6 +1028,13 @@
|
|||
"expectations": ["FAIL"],
|
||||
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should deny permission when not listed",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "https://bugzilla.mozilla.org/show_bug.cgi?id=1894217"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should fail when bad permission is given",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
|
|
@ -1041,6 +1048,13 @@
|
|||
"expectations": ["FAIL"],
|
||||
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should grant permission when listed",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "https://bugzilla.mozilla.org/show_bug.cgi?id=1894217"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should grant persistent-storage",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
|
|
@ -1048,6 +1062,13 @@
|
|||
"expectations": ["FAIL"],
|
||||
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should grant persistent-storage",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "https://bugzilla.mozilla.org/show_bug.cgi?id=1894217"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should isolate permissions between browser contexts",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
|
|
@ -1055,6 +1076,13 @@
|
|||
"expectations": ["FAIL"],
|
||||
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should isolate permissions between browser contexts",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "https://bugzilla.mozilla.org/show_bug.cgi?id=1894217"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should reset permissions",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
|
|
@ -1062,12 +1090,26 @@
|
|||
"expectations": ["FAIL"],
|
||||
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should reset permissions",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "https://bugzilla.mozilla.org/show_bug.cgi?id=1894217"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should trigger permission onchange",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should trigger permission onchange",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
|
||||
"comment": "https://bugzilla.mozilla.org/show_bug.cgi?id=1894217"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[browsercontext.spec] BrowserContext should fire target events",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ remote.jar:
|
|||
content/webdriver-bidi/modules/root/input.sys.mjs (modules/root/input.sys.mjs)
|
||||
content/webdriver-bidi/modules/root/log.sys.mjs (modules/root/log.sys.mjs)
|
||||
content/webdriver-bidi/modules/root/network.sys.mjs (modules/root/network.sys.mjs)
|
||||
content/webdriver-bidi/modules/root/permissions.sys.mjs (modules/root/permissions.sys.mjs)
|
||||
content/webdriver-bidi/modules/root/script.sys.mjs (modules/root/script.sys.mjs)
|
||||
content/webdriver-bidi/modules/root/session.sys.mjs (modules/root/session.sys.mjs)
|
||||
content/webdriver-bidi/modules/root/storage.sys.mjs (modules/root/storage.sys.mjs)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ ChromeUtils.defineESModuleGetters(modules.root, {
|
|||
log: "chrome://remote/content/webdriver-bidi/modules/root/log.sys.mjs",
|
||||
network:
|
||||
"chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs",
|
||||
permissions:
|
||||
"chrome://remote/content/webdriver-bidi/modules/root/permissions.sys.mjs",
|
||||
script: "chrome://remote/content/webdriver-bidi/modules/root/script.sys.mjs",
|
||||
session:
|
||||
"chrome://remote/content/webdriver-bidi/modules/root/session.sys.mjs",
|
||||
|
|
|
|||
140
remote/webdriver-bidi/modules/root/permissions.sys.mjs
Normal file
140
remote/webdriver-bidi/modules/root/permissions.sys.mjs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/* 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",
|
||||
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
||||
permissions: "chrome://remote/content/shared/Permissions.sys.mjs",
|
||||
});
|
||||
|
||||
export const PermissionState = {
|
||||
denied: "denied",
|
||||
granted: "granted",
|
||||
prompt: "prompt",
|
||||
};
|
||||
|
||||
class PermissionsModule extends Module {
|
||||
constructor(messageHandler) {
|
||||
super(messageHandler);
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
||||
/**
|
||||
* An object that holds the information about permission descriptor
|
||||
* for Webdriver BiDi permissions.setPermission command.
|
||||
*
|
||||
* @typedef PermissionDescriptor
|
||||
*
|
||||
* @property {string} name
|
||||
* The name of the permission.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set to a given permission descriptor a given state on a provided origin.
|
||||
*
|
||||
* @param {object=} options
|
||||
* @param {PermissionDescriptor} options.descriptor
|
||||
* The descriptor of the permission which will be updated.
|
||||
* @param {PermissionState} options.state
|
||||
* The state which will be set to the permission.
|
||||
* @param {string} options.origin
|
||||
* The origin which is used as a target for permission update.
|
||||
* @param {string=} options.userContext [unsupported]
|
||||
* The id of the user context which should be used as a target
|
||||
* for permission update.
|
||||
*
|
||||
* @throws {InvalidArgumentError}
|
||||
* Raised if an argument is of an invalid type or value.
|
||||
* @throws {UnsupportedOperationError}
|
||||
* Raised when unsupported permissions are set or <var>userContext</var>
|
||||
* argument is used.
|
||||
*/
|
||||
async setPermission(options = {}) {
|
||||
const {
|
||||
descriptor,
|
||||
state,
|
||||
origin,
|
||||
userContext: userContextId = null,
|
||||
} = options;
|
||||
|
||||
lazy.assert.object(
|
||||
descriptor,
|
||||
`Expected "descriptor" to be an object, got ${descriptor}`
|
||||
);
|
||||
const permissionName = descriptor.name;
|
||||
lazy.assert.string(
|
||||
permissionName,
|
||||
`Expected "descriptor.name" to be a string, got ${permissionName}`
|
||||
);
|
||||
|
||||
lazy.permissions.validatePermission(permissionName);
|
||||
|
||||
// Bug 1878741: Allowing this permission causes timing related Android crash.
|
||||
if (descriptor.name === "notifications") {
|
||||
if (Services.prefs.getBoolPref("notification.prompt.testing", false)) {
|
||||
// Okay, do nothing. The notifications module will work without permission.
|
||||
return;
|
||||
}
|
||||
throw new lazy.error.UnsupportedOperationError(
|
||||
`Setting "descriptor.name" "notifications" expected "notification.prompt.testing" preference to be set`
|
||||
);
|
||||
}
|
||||
|
||||
if (permissionName === "storage-access") {
|
||||
// TODO: Bug 1895457. Add support for "storage-access" permission.
|
||||
throw new lazy.error.UnsupportedOperationError(
|
||||
`"descriptor.name" "${permissionName}" is currently unsupported`
|
||||
);
|
||||
}
|
||||
|
||||
const permissionStateTypes = Object.keys(PermissionState);
|
||||
lazy.assert.that(
|
||||
state => permissionStateTypes.includes(state),
|
||||
`Expected "state" to be one of ${permissionStateTypes}, got ${state}`
|
||||
)(state);
|
||||
|
||||
lazy.assert.string(
|
||||
origin,
|
||||
`Expected "origin" to be a string, got ${origin}`
|
||||
);
|
||||
lazy.assert.that(
|
||||
origin => URL.canParse(origin),
|
||||
`Expected "origin" to be a valid URL, got ${origin}`
|
||||
)(origin);
|
||||
|
||||
if (userContextId !== null) {
|
||||
lazy.assert.string(
|
||||
userContextId,
|
||||
`Expected "userContext" to be a string, got ${userContextId}`
|
||||
);
|
||||
|
||||
// TODO: Bug 1894217. Add support for "userContext" argument.
|
||||
throw new lazy.error.UnsupportedOperationError(
|
||||
`"userContext" is not supported yet`
|
||||
);
|
||||
}
|
||||
|
||||
const activeWindow = Services.wm.getMostRecentBrowserWindow();
|
||||
let typedDescriptor;
|
||||
try {
|
||||
typedDescriptor = activeWindow.navigator.permissions.parseSetParameters({
|
||||
descriptor,
|
||||
state,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new lazy.error.InvalidArgumentError(
|
||||
`The conversion of "descriptor" was not successful: ${err.message}`
|
||||
);
|
||||
}
|
||||
|
||||
lazy.permissions.set(typedDescriptor, state, origin);
|
||||
}
|
||||
}
|
||||
|
||||
export const permissions = PermissionsModule;
|
||||
|
|
@ -48,3 +48,7 @@ class TestSetPermission(MarionetteTestCase):
|
|||
self.assertEqual(
|
||||
self.query_permission({"name": "midi", "sysex": True}), "prompt"
|
||||
)
|
||||
|
||||
def test_storage_access(self):
|
||||
self.marionette.set_permission({"name": "storage-access"}, "granted")
|
||||
self.assertEqual(self.query_permission({"name": "storage-access"}), "granted")
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
[invalid.py]
|
||||
expected:
|
||||
if (processor == "x86") and not debug: [OK, TIMEOUT]
|
||||
[test_params_descriptor_invalid_type[False\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_descriptor_invalid_type[SOME_STRING\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_descriptor_invalid_type[42\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_descriptor_invalid_type[descriptor3\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_descriptor_invalid_type[descriptor4\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_descriptor_invalid_type[descriptor5\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_descriptor_invalid_type[None\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_descriptor_invalid_type[descriptor7\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_descriptor_invalid_value[descriptor0\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_state_invalid_type[False\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_state_invalid_type[42\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_state_invalid_type[state2\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_state_invalid_type[state3\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_state_invalid_type[None\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_state_invalid_type[state5\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_state_invalid_value[Granted\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_origin_invalid_type[False\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_origin_invalid_type[42\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_origin_invalid_type[None\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_state_invalid_value[UNKNOWN\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_origin_invalid_type[user_context2\]]
|
||||
expected: FAIL
|
||||
|
||||
[test_params_origin_invalid_type[user_context3\]]
|
||||
expected: FAIL
|
||||
|
|
@ -1,13 +1,4 @@
|
|||
[set_permission.py]
|
||||
[test_set_permission]
|
||||
expected: FAIL
|
||||
|
||||
[test_set_permission_insecure_context]
|
||||
expected: FAIL
|
||||
|
||||
[test_set_permission_new_context]
|
||||
expected: FAIL
|
||||
|
||||
[test_set_permission_origin_unknown[UNKNOWN\]]
|
||||
expected: FAIL
|
||||
|
||||
|
|
@ -16,3 +7,6 @@
|
|||
|
||||
[test_set_permission_user_context]
|
||||
expected: FAIL
|
||||
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1894217
|
||||
disabled:
|
||||
if os == "android": bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1877953
|
||||
|
|
|
|||
Loading…
Reference in a new issue