Update On Thu Oct 5 20:53:07 CEST 2023
This commit is contained in:
parent
10755d3d3c
commit
9122d956af
1215 changed files with 74741 additions and 38293 deletions
2
CLOBBER
2
CLOBBER
|
@ -22,4 +22,4 @@
|
|||
# changes to stick? As of bug 928195, this shouldn't be necessary! Please
|
||||
# don't change CLOBBER for WebIDL changes any more.
|
||||
|
||||
Modified build files in third_party/libwebrtc - Bug 1851693 (MOZ) - Moved flexfec_03_header_reader_writer.cc back under non-unified sources to fix build after rename from upstream change ade07ca45e.
|
||||
Modified build files in third_party/libwebrtc - Bug 1855330 - pt4 - BUILD.gn fixes for updated libwebrtc/third_party. r?ng!
|
||||
|
|
28
Cargo.lock
generated
28
Cargo.lock
generated
|
@ -85,6 +85,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.69"
|
||||
|
@ -750,33 +756,31 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.1.14"
|
||||
version = "4.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "906f7fe1da4185b7a282b2bc90172a496f9def1aca4545fe7526810741591e14"
|
||||
checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.1.14"
|
||||
version = "4.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "351f9ad9688141ed83dfd8f5fb998a06225ef444b48ff4dc43de6d409b7fd10b"
|
||||
checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab"
|
||||
dependencies = [
|
||||
"bitflags 1.999.999",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
"terminal_size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.1.14"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81d7dc0031c3a59a04fc2ba395c8e2dd463cba1859275f065d225f6122221b45"
|
||||
checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
@ -786,9 +790,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.4.1"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1"
|
||||
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
|
@ -5424,7 +5428,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.2.999"
|
||||
version = "0.3.999"
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
|
|
33
accessible/interfaces/ia2/IA2Typelib.idl
Normal file
33
accessible/interfaces/ia2/IA2Typelib.idl
Normal file
|
@ -0,0 +1,33 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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 "Accessible2.idl";
|
||||
import "Accessible2_2.idl";
|
||||
import "AccessibleAction.idl";
|
||||
import "AccessibleApplication.idl";
|
||||
import "AccessibleComponent.idl";
|
||||
import "AccessibleDocument.idl";
|
||||
import "AccessibleEditableText.idl";
|
||||
import "AccessibleEventId.idl";
|
||||
import "AccessibleHyperlink.idl";
|
||||
import "AccessibleHypertext.idl";
|
||||
import "AccessibleHypertext2.idl";
|
||||
import "AccessibleImage.idl";
|
||||
import "AccessibleRelation.idl";
|
||||
import "AccessibleRole.idl";
|
||||
import "AccessibleStates.idl";
|
||||
import "AccessibleTable.idl";
|
||||
import "AccessibleTable2.idl";
|
||||
import "AccessibleTableCell.idl";
|
||||
import "AccessibleText.idl";
|
||||
import "AccessibleText2.idl";
|
||||
import "AccessibleTextSelectionContainer.idl";
|
||||
import "AccessibleValue.idl";
|
||||
import "IA2CommonTypes.idl";
|
||||
|
||||
// We are explicitly using #include instead of import so that the imported
|
||||
// IDL is treated as part of this IDL file.
|
||||
#include "IA2TypeLibrary.idl"
|
|
@ -76,3 +76,17 @@ for iface in midl_interfaces:
|
|||
SOURCES["!%s" % p].flags += [
|
||||
"-DUserMarshalRoutines=UserMarshalRoutines__%s" % p[:-2]
|
||||
]
|
||||
|
||||
GeneratedFile(
|
||||
"IA2Typelib.h",
|
||||
"IA2Typelib_i.c",
|
||||
"IA2Typelib.tlb",
|
||||
inputs=["IA2Typelib.idl"],
|
||||
script="/build/midl.py",
|
||||
entry_point="midl",
|
||||
flags=[
|
||||
"-app_config",
|
||||
"-I",
|
||||
TOPSRCDIR + "/other-licenses/ia2",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -48,6 +48,8 @@ BROWSER_CHROME_MANIFESTS += [
|
|||
"tests/browser/telemetry/browser.toml",
|
||||
"tests/browser/text/browser.toml",
|
||||
"tests/browser/tree/browser.toml",
|
||||
"tests/browser/windows/ia2/browser.toml",
|
||||
"tests/browser/windows/uia/browser.toml",
|
||||
]
|
||||
|
||||
with Files("**"):
|
||||
|
|
|
@ -5,7 +5,10 @@ support-files = [
|
|||
"!/accessible/tests/mochitest/*.js",
|
||||
"*.sys.mjs",
|
||||
"head.js",
|
||||
"python_runner_wsh.py",
|
||||
"shared-head.js",
|
||||
"windows/a11y_setup.py",
|
||||
"windows/a11y_setup_requirements.txt",
|
||||
]
|
||||
prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
|
||||
|
||||
|
|
89
accessible/tests/browser/python_runner_wsh.py
Normal file
89
accessible/tests/browser/python_runner_wsh.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
# 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/.
|
||||
|
||||
"""A pywebsocket3 handler which runs arbitrary Python code and returns the
|
||||
result.
|
||||
This is used to test OS specific accessibility APIs which can't be tested in JS.
|
||||
It is intended to be called from JS browser tests.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from mod_pywebsocket import msgutil
|
||||
|
||||
|
||||
def web_socket_do_extra_handshake(request):
|
||||
pass
|
||||
|
||||
|
||||
def web_socket_transfer_data(request):
|
||||
def send(*args):
|
||||
"""Send a response to the client as a JSON array."""
|
||||
msgutil.send_message(request, json.dumps(args))
|
||||
|
||||
cleanNamespace = {}
|
||||
testDir = None
|
||||
if sys.platform == "win32":
|
||||
testDir = "windows"
|
||||
elif sys.platform == "linux":
|
||||
# XXX ATK code goes here.
|
||||
pass
|
||||
if testDir:
|
||||
sys.path.append(
|
||||
os.path.join(
|
||||
os.getcwd(), "browser", "accessible", "tests", "browser", testDir
|
||||
)
|
||||
)
|
||||
try:
|
||||
import a11y_setup
|
||||
|
||||
cleanNamespace = a11y_setup.__dict__
|
||||
setupExc = None
|
||||
except Exception:
|
||||
setupExc = traceback.format_exc()
|
||||
sys.path.pop()
|
||||
|
||||
def info(message):
|
||||
"""Log an info message."""
|
||||
send("info", str(message))
|
||||
|
||||
cleanNamespace["info"] = info
|
||||
namespace = cleanNamespace.copy()
|
||||
|
||||
# Keep handling messages until the WebSocket is closed.
|
||||
while True:
|
||||
code = msgutil.receive_message(request)
|
||||
if not code:
|
||||
return
|
||||
if code == "__reset__":
|
||||
namespace = cleanNamespace.copy()
|
||||
continue
|
||||
|
||||
if setupExc:
|
||||
# a11y_setup failed. Report an exception immediately.
|
||||
send("exception", setupExc)
|
||||
continue
|
||||
|
||||
# Wrap the code in a function called run(). This allows the code to
|
||||
# return a result by simply using the return statement.
|
||||
if "\n" not in code and not code.lstrip().startswith("return "):
|
||||
# Single line without return. Assume this is an expression. We use
|
||||
# a lambda to return the result.
|
||||
code = f"run = lambda: {code}"
|
||||
else:
|
||||
lines = ["def run():"]
|
||||
# Indent each line inside the function.
|
||||
lines.extend(f" {line}" for line in code.splitlines())
|
||||
code = "\n".join(lines)
|
||||
try:
|
||||
# Execute this Python code, which will define the run() function.
|
||||
exec(code, namespace)
|
||||
# Run the function we just defined.
|
||||
ret = namespace["run"]()
|
||||
send("return", ret)
|
||||
except Exception:
|
||||
send("exception", traceback.format_exc())
|
|
@ -15,7 +15,9 @@
|
|||
Cc, Cu, arrayFromChildren, forceGC, contentSpawnMutation,
|
||||
DEFAULT_IFRAME_ID, DEFAULT_IFRAME_DOC_BODY_ID, invokeContentTask,
|
||||
matchContentDoc, currentContentDoc, getContentDPR,
|
||||
waitForImageMap, getContentBoundsForDOMElm, untilCacheIs, untilCacheOk, testBoundsWithContent, waitForContentPaint */
|
||||
waitForImageMap, getContentBoundsForDOMElm, untilCacheIs,
|
||||
untilCacheOk, testBoundsWithContent, waitForContentPaint,
|
||||
runPython */
|
||||
|
||||
const CURRENT_FILE_DIR = "/browser/accessible/tests/browser/";
|
||||
|
||||
|
@ -446,6 +448,10 @@ function accessibleTask(doc, task, options = {}) {
|
|||
)) {
|
||||
Services.obs.removeObserver(observer, "accessible-event");
|
||||
}
|
||||
if (gPythonSocket) {
|
||||
// Remove any globals set by Python code run in this test.
|
||||
runPython(`__reset__`);
|
||||
}
|
||||
});
|
||||
|
||||
let onContentDocLoad;
|
||||
|
@ -916,3 +922,52 @@ async function testBoundsWithContent(iframeDocAcc, id, browser) {
|
|||
|
||||
return accBounds;
|
||||
}
|
||||
|
||||
let gPythonSocket = null;
|
||||
|
||||
/**
|
||||
* Run some Python code. This is useful for testing OS APIs.
|
||||
* This function returns a Promise which is resolved or rejected when the Python
|
||||
* code completes. The Python code can return a result with the return
|
||||
* statement, as long as the result can be serialized to JSON. For convenience,
|
||||
* if the code is a single line which does not begin with return, it will be
|
||||
* treated as an expression and its result will be returned. The JS Promise will
|
||||
* be resolved with the deserialized result. If the Python code raises an
|
||||
* exception, the JS Promise will be rejected with the Python traceback.
|
||||
* An info() function is provided in Python to log an info message.
|
||||
* See windows/a11y_setup.py for other things available in the Python
|
||||
* environment.
|
||||
*/
|
||||
function runPython(code) {
|
||||
if (!gPythonSocket) {
|
||||
// Keep the socket open across calls to avoid repeated setup overhead.
|
||||
gPythonSocket = new WebSocket(
|
||||
"ws://mochi.test:8888/browser/accessible/tests/browser/python_runner"
|
||||
);
|
||||
if (gPythonSocket.readyState != WebSocket.OPEN) {
|
||||
gPythonSocket.onopen = evt => {
|
||||
gPythonSocket.send(code);
|
||||
gPythonSocket.onopen = null;
|
||||
};
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
gPythonSocket.onmessage = evt => {
|
||||
const message = JSON.parse(evt.data);
|
||||
if (message[0] == "return") {
|
||||
gPythonSocket.onmessage = null;
|
||||
resolve(message[1]);
|
||||
} else if (message[0] == "exception") {
|
||||
gPythonSocket.onmessage = null;
|
||||
reject(new Error(message[1]));
|
||||
} else if (message[0] == "info") {
|
||||
info(message[1]);
|
||||
}
|
||||
};
|
||||
// If gPythonSocket isn't open yet, we'll send the message when .onopen is
|
||||
// called. If it's open, we can send it immediately.
|
||||
if (gPythonSocket.readyState == WebSocket.OPEN) {
|
||||
gPythonSocket.send(code);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
145
accessible/tests/browser/windows/a11y_setup.py
Normal file
145
accessible/tests/browser/windows/a11y_setup.py
Normal file
|
@ -0,0 +1,145 @@
|
|||
# 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/.
|
||||
|
||||
"""Python environment for Windows a11y browser tests.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
from ctypes import POINTER, byref
|
||||
from ctypes.wintypes import BOOL, HWND, LPARAM, POINT # noqa: F401
|
||||
|
||||
import comtypes.client
|
||||
import psutil
|
||||
from comtypes import COMError, IServiceProvider
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
oleacc = ctypes.oledll.oleacc
|
||||
oleaccMod = comtypes.client.GetModule("oleacc.dll")
|
||||
IAccessible = oleaccMod.IAccessible
|
||||
del oleaccMod
|
||||
OBJID_CLIENT = -4
|
||||
CHILDID_SELF = 0
|
||||
NAVRELATION_EMBEDS = 0x1009
|
||||
# This is the path if running locally.
|
||||
ia2Tlb = os.path.join(
|
||||
os.getcwd(),
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"accessible",
|
||||
"interfaces",
|
||||
"ia2",
|
||||
"IA2Typelib.tlb",
|
||||
)
|
||||
if not os.path.isfile(ia2Tlb):
|
||||
# This is the path if running in CI.
|
||||
ia2Tlb = os.path.join(os.getcwd(), "ia2Typelib.tlb")
|
||||
ia2Mod = comtypes.client.GetModule(ia2Tlb)
|
||||
del ia2Tlb
|
||||
# Shove all the IAccessible* interfaces and IA2_* constants directly
|
||||
# into our namespace for convenience.
|
||||
globals().update((k, getattr(ia2Mod, k)) for k in ia2Mod.__all__)
|
||||
# We use this below. The linter doesn't understand our globals() update hack.
|
||||
IAccessible2 = ia2Mod.IAccessible2
|
||||
del ia2Mod
|
||||
|
||||
uiaMod = comtypes.client.GetModule("UIAutomationCore.dll")
|
||||
globals().update((k, getattr(uiaMod, k)) for k in uiaMod.__all__)
|
||||
uiaClient = comtypes.CoCreateInstance(
|
||||
uiaMod.CUIAutomation._reg_clsid_,
|
||||
interface=uiaMod.IUIAutomation,
|
||||
clsctx=comtypes.CLSCTX_INPROC_SERVER,
|
||||
)
|
||||
TreeScope_Descendants = uiaMod.TreeScope_Descendants
|
||||
UIA_AutomationIdPropertyId = uiaMod.UIA_AutomationIdPropertyId
|
||||
del uiaMod
|
||||
|
||||
|
||||
def AccessibleObjectFromWindow(hwnd, objectID=OBJID_CLIENT):
|
||||
p = POINTER(IAccessible)()
|
||||
oleacc.AccessibleObjectFromWindow(
|
||||
hwnd, objectID, byref(IAccessible._iid_), byref(p)
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
def getFirefoxHwnd():
|
||||
"""Search all top level windows for the Firefox instance being
|
||||
tested.
|
||||
We search by window class name and window title prefix.
|
||||
"""
|
||||
# We can compare the grandparent process ids to find the Firefox started by
|
||||
# the test harness.
|
||||
commonPid = psutil.Process().parent().ppid()
|
||||
# We need something mutable to store the result from the callback.
|
||||
found = []
|
||||
|
||||
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
|
||||
def callback(hwnd, lParam):
|
||||
name = ctypes.create_unicode_buffer(100)
|
||||
user32.GetClassNameW(hwnd, name, 100)
|
||||
if name.value != "MozillaWindowClass":
|
||||
return True
|
||||
pid = ctypes.wintypes.DWORD()
|
||||
user32.GetWindowThreadProcessId(hwnd, byref(pid))
|
||||
if psutil.Process(pid.value).parent().ppid() != commonPid:
|
||||
return True # Not the Firefox being tested.
|
||||
found.append(hwnd)
|
||||
return False
|
||||
|
||||
user32.EnumWindows(callback, LPARAM(0))
|
||||
if not found:
|
||||
raise LookupError("Couldn't find Firefox HWND")
|
||||
return found[0]
|
||||
|
||||
|
||||
def toIa2(obj):
|
||||
serv = obj.QueryInterface(IServiceProvider)
|
||||
return serv.QueryService(IAccessible2._iid_, IAccessible2)
|
||||
|
||||
|
||||
def getDocIa2():
|
||||
"""Get the IAccessible2 for the document being tested."""
|
||||
hwnd = getFirefoxHwnd()
|
||||
root = AccessibleObjectFromWindow(hwnd)
|
||||
doc = root.accNavigate(NAVRELATION_EMBEDS, 0)
|
||||
try:
|
||||
child = toIa2(doc.accChild(1))
|
||||
if "id:default-iframe-id;" in child.attributes:
|
||||
# This is an iframe or remoteIframe test.
|
||||
doc = child.accChild(1)
|
||||
except COMError:
|
||||
pass # No child.
|
||||
return toIa2(doc)
|
||||
|
||||
|
||||
def findIa2ByDomId(root, id):
|
||||
search = f"id:{id};"
|
||||
# Child ids begin at 1.
|
||||
for i in range(1, root.accChildCount + 1):
|
||||
child = toIa2(root.accChild(i))
|
||||
if search in child.attributes:
|
||||
return child
|
||||
descendant = findIa2ByDomId(child, id)
|
||||
if descendant:
|
||||
return descendant
|
||||
|
||||
|
||||
def getDocUia():
|
||||
"""Get the IUIAutomationElement for the document being tested."""
|
||||
# We start with IAccessible2 because there's no efficient way to
|
||||
# find the document we want with UIA.
|
||||
ia2 = getDocIa2()
|
||||
return uiaClient.ElementFromIAccessible(ia2, CHILDID_SELF)
|
||||
|
||||
|
||||
def findUiaByDomId(root, id):
|
||||
cond = uiaClient.CreatePropertyCondition(UIA_AutomationIdPropertyId, id)
|
||||
# FindFirst ignores elements in the raw tree, so we have to use
|
||||
# FindFirstBuildCache to override that, even though we don't want to cache
|
||||
# anything.
|
||||
request = uiaClient.CreateCacheRequest()
|
||||
request.TreeFilter = uiaClient.RawViewCondition
|
||||
return root.FindFirstBuildCache(TreeScope_Descendants, cond, request)
|
|
@ -0,0 +1 @@
|
|||
comtypes==1.2.0
|
9
accessible/tests/browser/windows/ia2/browser.toml
Normal file
9
accessible/tests/browser/windows/ia2/browser.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[DEFAULT]
|
||||
subsuite = "a11y"
|
||||
skip-if = [
|
||||
"os != 'win'",
|
||||
"headless",
|
||||
]
|
||||
support-files = ["head.js"]
|
||||
|
||||
["browser_role.js"]
|
39
accessible/tests/browser/windows/ia2/browser_role.js
Normal file
39
accessible/tests/browser/windows/ia2/browser_role.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const ROLE_SYSTEM_DOCUMENT = 15;
|
||||
const ROLE_SYSTEM_GROUPING = 20;
|
||||
const IA2_ROLE_PARAGRAPH = 1054;
|
||||
|
||||
addAccessibleTask(
|
||||
`
|
||||
<p id="p">p</p>
|
||||
`,
|
||||
async function (browser, docAcc) {
|
||||
let role = await runPython(`
|
||||
global doc
|
||||
doc = getDocIa2()
|
||||
return doc.accRole(CHILDID_SELF)
|
||||
`);
|
||||
is(role, ROLE_SYSTEM_DOCUMENT, "doc has correct MSAA role");
|
||||
role = await runPython(`doc.role()`);
|
||||
is(role, ROLE_SYSTEM_DOCUMENT, "doc has correct IA2 role");
|
||||
ok(
|
||||
await runPython(`
|
||||
global p
|
||||
p = findIa2ByDomId(doc, "p")
|
||||
firstChild = toIa2(doc.accChild(1))
|
||||
return p == firstChild
|
||||
`),
|
||||
"doc's first child is p"
|
||||
);
|
||||
role = await runPython(`p.accRole(CHILDID_SELF)`);
|
||||
is(role, ROLE_SYSTEM_GROUPING, "p has correct MSAA role");
|
||||
role = await runPython(`p.role()`);
|
||||
is(role, IA2_ROLE_PARAGRAPH, "p has correct IA2 role");
|
||||
},
|
||||
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
|
||||
);
|
18
accessible/tests/browser/windows/ia2/head.js
Normal file
18
accessible/tests/browser/windows/ia2/head.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Load the shared-head file first.
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
|
||||
this
|
||||
);
|
||||
|
||||
// Loading and common.js from accessible/tests/mochitest/ for all tests, as
|
||||
// well as promisified-events.js.
|
||||
loadScripts(
|
||||
{ name: "common.js", dir: MOCHITESTS_DIR },
|
||||
{ name: "promisified-events.js", dir: MOCHITESTS_DIR }
|
||||
);
|
11
accessible/tests/browser/windows/uia/browser.toml
Normal file
11
accessible/tests/browser/windows/uia/browser.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[DEFAULT]
|
||||
subsuite = "a11y"
|
||||
skip-if = [
|
||||
"os != 'win'",
|
||||
"headless",
|
||||
]
|
||||
support-files = ["head.js"]
|
||||
|
||||
["browser_controlType.js"]
|
||||
|
||||
["browser_elementFromPoint.js"]
|
30
accessible/tests/browser/windows/uia/browser_controlType.js
Normal file
30
accessible/tests/browser/windows/uia/browser_controlType.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const UIA_ButtonControlTypeId = 50000;
|
||||
const UIA_DocumentControlTypeId = 50030;
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
addAccessibleTask(
|
||||
`
|
||||
<button id="button">button</button>
|
||||
`,
|
||||
async function (browser, docAcc) {
|
||||
let controlType = await runPython(`
|
||||
global doc
|
||||
doc = getDocUia()
|
||||
return doc.CurrentControlType
|
||||
`);
|
||||
is(controlType, UIA_DocumentControlTypeId, "doc has correct control type");
|
||||
controlType = await runPython(`
|
||||
button = findUiaByDomId(doc, "button")
|
||||
return button.CurrentControlType
|
||||
`);
|
||||
is(controlType, UIA_ButtonControlTypeId, "button has correct control type");
|
||||
},
|
||||
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
|
||||
);
|
|
@ -0,0 +1,34 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
addAccessibleTask(
|
||||
`
|
||||
<button id="button">button</p>
|
||||
<a id="a" href="#">a</a>
|
||||
`,
|
||||
async function (browser, docAcc) {
|
||||
ok(
|
||||
await runPython(`
|
||||
global doc
|
||||
doc = getDocUia()
|
||||
button = findUiaByDomId(doc, "button")
|
||||
rect = button.CurrentBoundingRectangle
|
||||
found = uiaClient.ElementFromPoint(POINT(rect.left + 1, rect.top + 1))
|
||||
return uiaClient.CompareElements(button, found)
|
||||
`),
|
||||
"ElementFromPoint on button returns button"
|
||||
);
|
||||
ok(
|
||||
await runPython(`
|
||||
a = findUiaByDomId(doc, "a")
|
||||
rect = a.CurrentBoundingRectangle
|
||||
found = uiaClient.ElementFromPoint(POINT(rect.left + 1, rect.top + 1))
|
||||
return uiaClient.CompareElements(a, found)
|
||||
`),
|
||||
"ElementFromPoint on a returns a"
|
||||
);
|
||||
}
|
||||
);
|
18
accessible/tests/browser/windows/uia/head.js
Normal file
18
accessible/tests/browser/windows/uia/head.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Load the shared-head file first.
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
|
||||
this
|
||||
);
|
||||
|
||||
// Loading and common.js from accessible/tests/mochitest/ for all tests, as
|
||||
// well as promisified-events.js.
|
||||
loadScripts(
|
||||
{ name: "common.js", dir: MOCHITESTS_DIR },
|
||||
{ name: "promisified-events.js", dir: MOCHITESTS_DIR }
|
||||
);
|
|
@ -1241,6 +1241,19 @@ MsaaAccessible::accHitTest(
|
|||
|
||||
// if we got a child
|
||||
if (accessible) {
|
||||
if (accessible != mAcc && accessible->IsTextLeaf()) {
|
||||
Accessible* parent = accessible->Parent();
|
||||
if (parent != mAcc && parent->Role() == roles::LINK) {
|
||||
// Bug 1843832: The UI Automation -> IAccessible2 proxy barfs if we
|
||||
// return the text leaf child of a link when hit testing an ancestor of
|
||||
// the link. Therefore, we return the link instead. MSAA clients which
|
||||
// call AccessibleObjectFromPoint will still get to the text leaf, since
|
||||
// AccessibleObjectFromPoint keeps calling accHitTest until it can't
|
||||
// descend any further. We should remove this tragic hack once we have
|
||||
// a native UIA implementation.
|
||||
accessible = parent;
|
||||
}
|
||||
}
|
||||
if (accessible == mAcc) {
|
||||
pvarChild->vt = VT_I4;
|
||||
pvarChild->lVal = CHILDID_SELF;
|
||||
|
|
|
@ -284,6 +284,12 @@ pref("browser.shell.setDefaultPDFHandler", true);
|
|||
// is a known browser, and not when existing handler is another PDF handler such
|
||||
// as Acrobat Reader or Nitro PDF.
|
||||
pref("browser.shell.setDefaultPDFHandler.onlyReplaceBrowsers", true);
|
||||
// Whether or not to we are allowed to prompt the user to set Firefox as their
|
||||
// default PDF handler.
|
||||
pref("browser.shell.checkDefaultPDF", true);
|
||||
// Will be set to `true` if the user indicates that they don't want to be asked
|
||||
// again about Firefox being their default PDF handler any more.
|
||||
pref("browser.shell.checkDefaultPDF.silencedByUser", false);
|
||||
// URL to navigate to when launching Firefox after accepting the Windows Default
|
||||
// Browser Agent "Set Firefox as default" call to action.
|
||||
pref("browser.shell.defaultBrowserAgent.thanksURL", "https://www.mozilla.org/%LOCALE%/firefox/set-as-default/thanks/");
|
||||
|
@ -1789,10 +1795,6 @@ pref("security.mixed_content.block_active_content", true);
|
|||
pref("security.insecure_connection_text.enabled", false);
|
||||
pref("security.insecure_connection_text.pbmode.enabled", false);
|
||||
|
||||
// 1 = allow MITM for certificate pinning checks.
|
||||
pref("security.cert_pinning.enforcement_level", 1);
|
||||
|
||||
|
||||
// If this turns true, Moz*Gesture events are not called stopPropagation()
|
||||
// before content.
|
||||
pref("dom.debug.propagate_gesture_events_through_content", false);
|
||||
|
@ -2151,11 +2153,6 @@ pref("browser.tabs.crashReporting.includeURL", false);
|
|||
// nightly and developer edition.
|
||||
pref("extensions.experiments.enabled", false);
|
||||
|
||||
#if defined(XP_LINUX) || defined(XP_WIN) || defined(XP_MACOSX)
|
||||
// Allows us to adjust the priority of child processes at the OS level
|
||||
pref("dom.ipc.processPriorityManager.enabled", true);
|
||||
#endif
|
||||
|
||||
#if defined(XP_WIN)
|
||||
pref("dom.ipc.processPriorityManager.backgroundUsesEcoQoS", true);
|
||||
#endif
|
||||
|
@ -2881,7 +2878,11 @@ pref("cookiebanners.ui.desktop.cfrVariant", 0);
|
|||
#endif
|
||||
|
||||
// Reset Private Browsing Session feature
|
||||
pref("browser.privatebrowsing.resetPBM.enabled", false);
|
||||
#if defined(NIGHTLY_BUILD)
|
||||
pref("browser.privatebrowsing.resetPBM.enabled", true);
|
||||
#else
|
||||
pref("browser.privatebrowsing.resetPBM.enabled", false);
|
||||
#endif
|
||||
// Whether the reset private browsing panel should ask for confirmation before
|
||||
// performing the clear action.
|
||||
pref("browser.privatebrowsing.resetPBM.showConfirmationDialog", true);
|
||||
|
|
|
@ -699,12 +699,12 @@
|
|||
|
||||
<html:moz-button-group id="reset-pbm-panel-footer" class="panel-footer">
|
||||
<button id="reset-pbm-panel-cancel-button"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
data-l10n-id="reset-pbm-panel-cancel-button"
|
||||
oncommand="ResetPBMPanel.onCancel(this)"></button>
|
||||
<button slot="primary"
|
||||
id="reset-pbm-panel-confirm-button"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
data-l10n-id="reset-pbm-panel-confirm-button"
|
||||
oncommand="ResetPBMPanel.onConfirm(this)"></button>
|
||||
</html:moz-button-group>
|
||||
|
|
|
@ -7561,8 +7561,8 @@ var WebAuthnPromptHelper = {
|
|||
return;
|
||||
}
|
||||
|
||||
let mgr = Cc["@mozilla.org/webauthn/transport;1"].getService(
|
||||
Ci.nsIWebAuthnTransport
|
||||
let mgr = Cc["@mozilla.org/webauthn/service;1"].getService(
|
||||
Ci.nsIWebAuthnService
|
||||
);
|
||||
|
||||
if (data.prompt.type == "presence") {
|
||||
|
|
|
@ -192,12 +192,12 @@
|
|||
data-l10n-id="bookmark-panel"
|
||||
data-l10n-attrs="style">
|
||||
<button id="editBookmarkPanelDoneButton"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
data-l10n-id="bookmark-panel-save-button"
|
||||
default="true"
|
||||
oncommand="StarUI.panel.hidePopup();"/>
|
||||
<button id="editBookmarkPanelRemoveButton"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
oncommand="StarUI.removeBookmarkButtonCommand();"/>
|
||||
</html:moz-button-group>
|
||||
</vbox>
|
||||
|
|
|
@ -372,11 +372,11 @@
|
|||
<html:moz-button-group id="protections-popup-sendReportView-footer"
|
||||
class="panel-footer">
|
||||
<button id="protections-popup-sendReportView-cancel"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
data-l10n-id="protections-panel-content-blocking-breakage-report-view-cancel"
|
||||
oncommand="gProtectionsHandler._protectionsPopupMultiView.goBack();"/>
|
||||
<button id="protections-popup-sendReportView-submit"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
default="true"
|
||||
data-l10n-id="protections-panel-content-blocking-breakage-report-view-send-report"
|
||||
oncommand="gProtectionsHandler.onSendReportClicked(); gProtectionsHandler.recordClick('send_report_submit');"/>
|
||||
|
@ -399,17 +399,17 @@
|
|||
<button id="protections-popup-cookieBannerView-cancel"
|
||||
data-l10n-id="protections-panel-cookie-banner-view-cancel"
|
||||
oncommand="gProtectionsHandler._protectionsPopupMultiView.goBack();"
|
||||
class="footer-button" />
|
||||
class="panel-footer-button" />
|
||||
<button id="protections-popup-cookieBannerView-enable-button"
|
||||
data-l10n-id="protections-panel-cookie-banner-view-turn-on"
|
||||
oncommand="gProtectionsHandler.onCookieBannerToggleCommand()"
|
||||
default="true"
|
||||
class="footer-button" />
|
||||
class="panel-footer-button" />
|
||||
<button id="protections-popup-cookieBannerView-disable-button"
|
||||
data-l10n-id="protections-panel-cookie-banner-view-turn-off"
|
||||
oncommand="gProtectionsHandler.onCookieBannerToggleCommand()"
|
||||
default="true"
|
||||
class="footer-button" />
|
||||
class="panel-footer-button" />
|
||||
</html:moz-button-group>
|
||||
</panelview>
|
||||
</panelmultiview>
|
||||
|
|
|
@ -29,12 +29,12 @@
|
|||
# NB: because oncommand fires after click, by the time we've fired, the checkbox binding
|
||||
# will already have switched the button's state, so this is correct:
|
||||
oncommand="gCustomizeMode.toggleTitlebar(this.checked)" data-l10n-id="customize-mode-titlebar"/>
|
||||
<button id="customization-toolbar-visibility-button" class="footer-button" type="menu" data-l10n-id="customize-mode-toolbars">
|
||||
<button id="customization-toolbar-visibility-button" class="customizationmode-button" type="menu" data-l10n-id="customize-mode-toolbars">
|
||||
<menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
|
||||
</button>
|
||||
<button id="customization-uidensity-button"
|
||||
data-l10n-id="customize-mode-uidensity"
|
||||
class="footer-button"
|
||||
class="customizationmode-button"
|
||||
type="menu"
|
||||
hidden="true">
|
||||
<panel type="arrow" id="customization-uidensity-menu"
|
||||
|
@ -90,31 +90,30 @@
|
|||
|
||||
<button id="whimsy-button"
|
||||
type="checkbox"
|
||||
class="footer-button"
|
||||
class="customizationmode-button"
|
||||
oncommand="gCustomizeMode.togglePong(this.checked);"
|
||||
hidden="true"/>
|
||||
|
||||
<spacer id="customization-footer-spacer"/>
|
||||
#ifdef XP_MACOSX
|
||||
<button id="customization-touchbar-button"
|
||||
class="footer-button"
|
||||
class="customizationmode-button"
|
||||
hidden="true"
|
||||
oncommand="gCustomizeMode.customizeTouchBar();"
|
||||
data-l10n-id="customize-mode-touchbar-cmd"/>
|
||||
<spacer hidden="true" id="customization-touchbar-spacer"/>
|
||||
#endif
|
||||
<button id="customization-undo-reset-button"
|
||||
class="footer-button"
|
||||
class="customizationmode-button"
|
||||
hidden="true"
|
||||
oncommand="gCustomizeMode.undoReset();"
|
||||
data-l10n-id="customize-mode-undo-cmd"/>
|
||||
<button id="customization-reset-button"
|
||||
oncommand="gCustomizeMode.reset();"
|
||||
data-l10n-id="customize-mode-restore-defaults"
|
||||
class="footer-button"/>
|
||||
class="customizationmode-button"/>
|
||||
<button id="customization-done-button"
|
||||
oncommand="gCustomizeMode.exit();"
|
||||
data-l10n-id="customize-mode-done"
|
||||
default="true"
|
||||
class="footer-button"/>
|
||||
class="customizationmode-button"/>
|
||||
</hbox>
|
||||
|
|
|
@ -182,10 +182,10 @@
|
|||
<html:moz-button-group id="downloadsPanel-blockedSubview-buttons"
|
||||
class="panel-footer">
|
||||
<button id="downloadsPanel-blockedSubview-unblockButton"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
command="downloadsCmd_unblockAndOpen"/>
|
||||
<button id="downloadsPanel-blockedSubview-deleteButton"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
oncommand="DownloadsBlockedSubview.confirmBlock();"
|
||||
default="true"/>
|
||||
</html:moz-button-group>
|
||||
|
|
|
@ -222,6 +222,7 @@ serp:
|
|||
`ad_sidebar`,
|
||||
`ad_sitelink`,
|
||||
`incontent_searchbox`,
|
||||
`non_ads_link`,
|
||||
`refined_search_buttons`,
|
||||
`shopping_tab`.
|
||||
type: string
|
||||
|
|
|
@ -107,9 +107,6 @@ support-files = [
|
|||
["browser_search_telemetry_searchbar.js"]
|
||||
https_first_disabled = true
|
||||
support-files = [
|
||||
"slow_loading_page_with_ads_on_load_event.html",
|
||||
"slow_loading_page_with_ads.html",
|
||||
"slow_loading_page_with_ads.sjs",
|
||||
"telemetrySearchSuggestions.sjs",
|
||||
"telemetrySearchSuggestions.xml",
|
||||
]
|
||||
|
@ -127,11 +124,27 @@ support-files = [
|
|||
support-files = [
|
||||
"searchTelemetry.html",
|
||||
"searchTelemetryAd.html",
|
||||
]
|
||||
|
||||
["browser_search_telemetry_sources_ads_clicks.js"]
|
||||
support-files = [
|
||||
"searchTelemetryAd.html",
|
||||
]
|
||||
|
||||
["browser_search_telemetry_sources_ads_data_attributes.js"]
|
||||
support-files = [
|
||||
"searchTelemetryAd_dataAttributes.html",
|
||||
"searchTelemetryAd_dataAttributes_href.html",
|
||||
"searchTelemetryAd_dataAttributes_none.html",
|
||||
]
|
||||
|
||||
["browser_search_telemetry_sources_ads_load_events.js"]
|
||||
support-files = [
|
||||
"slow_loading_page_with_ads_on_load_event.html",
|
||||
"slow_loading_page_with_ads.html",
|
||||
"slow_loading_page_with_ads.sjs",
|
||||
]
|
||||
|
||||
["browser_search_telemetry_sources_in_content.js"]
|
||||
support-files = ["searchTelemetryAd_searchbox_with_content.html"]
|
||||
|
||||
|
|
|
@ -26,38 +26,6 @@ const TEST_PROVIDER_INFO = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
telemetryId: "example-data-attributes",
|
||||
searchPageRegexp:
|
||||
/^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetryAd_dataAttributes(?:_none|_href)?.html/,
|
||||
queryParamName: "s",
|
||||
codeParamName: "abc",
|
||||
taggedCodes: ["ff"],
|
||||
adServerAttributes: ["xyz"],
|
||||
extraAdServersRegexps: [/^https:\/\/example\.com\/ad/],
|
||||
components: [
|
||||
{
|
||||
type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
telemetryId: "slow-page-load",
|
||||
searchPageRegexp:
|
||||
/^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/slow_loading_page_with_ads(_on_load_event)?.html/,
|
||||
queryParamName: "s",
|
||||
codeParamName: "abc",
|
||||
taggedCodes: ["ff"],
|
||||
followOnParamNames: ["a"],
|
||||
extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
|
||||
components: [
|
||||
{
|
||||
type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function getSERPFollowOnUrl(page) {
|
||||
|
@ -229,188 +197,6 @@ add_task(async function test_track_ad() {
|
|||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_on_data_attributes() {
|
||||
resetTelemetry();
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("searchTelemetryAd_dataAttributes.html")
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": {
|
||||
"example-data-attributes:tagged:ff": 1,
|
||||
},
|
||||
"browser.search.withads.unknown": {
|
||||
"example-data-attributes:tagged": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example-data-attributes",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_on_data_attributes_and_hrefs() {
|
||||
resetTelemetry();
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("searchTelemetryAd_dataAttributes_href.html")
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": {
|
||||
"example-data-attributes:tagged:ff": 1,
|
||||
},
|
||||
"browser.search.withads.unknown": {
|
||||
"example-data-attributes:tagged": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example-data-attributes",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_track_no_ad_on_data_attributes_and_hrefs() {
|
||||
resetTelemetry();
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("searchTelemetryAd_dataAttributes_none.html")
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": {
|
||||
"example-data-attributes:tagged:ff": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example-data-attributes",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_on_DOMContentLoaded() {
|
||||
resetTelemetry();
|
||||
|
||||
let observeAdPreviouslyRecorded = TestUtils.consoleMessageObserved(msg => {
|
||||
return (
|
||||
typeof msg.wrappedJSObject.arguments?.[0] == "string" &&
|
||||
msg.wrappedJSObject.arguments[0].includes(
|
||||
"Ad was previously reported for browser with URI"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("slow_loading_page_with_ads.html")
|
||||
);
|
||||
|
||||
// Observe ad was counted on DOMContentLoaded.
|
||||
// We do not count the ad again on load.
|
||||
await observeAdPreviouslyRecorded;
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { "slow-page-load:tagged:ff": 1 },
|
||||
"browser.search.withads.unknown": { "slow-page-load:tagged": 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "slow-page-load",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_on_load_event() {
|
||||
resetTelemetry();
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("slow_loading_page_with_ads_on_load_event.html")
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { "slow-page-load:tagged:ff": 1 },
|
||||
"browser.search.withads.unknown": { "slow-page-load:tagged": 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "slow-page-load",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_organic() {
|
||||
resetTelemetry();
|
||||
|
||||
|
@ -534,257 +320,3 @@ add_task(async function test_track_ad_pages_without_ads() {
|
|||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
});
|
||||
|
||||
async function track_ad_click(testOrganic) {
|
||||
// Note: the above tests have already checked a page with no ad-urls.
|
||||
resetTelemetry();
|
||||
|
||||
let expectedScalarKey = `example:${testOrganic ? "organic" : "tagged"}`;
|
||||
let expectedContentScalarKey = `example:${
|
||||
testOrganic ? "organic:none" : "tagged:ff"
|
||||
}`;
|
||||
let tagged = testOrganic ? "false" : "true";
|
||||
let partnerCode = testOrganic ? "" : "ff";
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("searchTelemetryAd.html", testOrganic)
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.withads.unknown": {
|
||||
[expectedScalarKey.replace("sap", "tagged")]: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
await promiseAdImpressionReceived(1);
|
||||
|
||||
let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
|
||||
BrowserTestUtils.synthesizeMouseAtCenter("#ad1", {}, tab.linkedBrowser);
|
||||
await pageLoadPromise;
|
||||
await promiseWaitForAdLinkCheck();
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.withads.unknown": { [expectedScalarKey]: 1 },
|
||||
"browser.search.adclicks.unknown": { [expectedScalarKey]: 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// Now go back, and click again.
|
||||
pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
|
||||
gBrowser.goBack();
|
||||
await pageLoadPromise;
|
||||
await promiseWaitForAdLinkCheck();
|
||||
|
||||
// We've gone back, so we register an extra display & if it is with ads or not.
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.tabhistory": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.content.unknown": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.withads.tabhistory": { [expectedScalarKey]: 1 },
|
||||
"browser.search.withads.unknown": { [expectedScalarKey]: 1 },
|
||||
"browser.search.adclicks.unknown": { [expectedScalarKey]: 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "tabhistory",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
await promiseAdImpressionReceived(2);
|
||||
|
||||
pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
|
||||
BrowserTestUtils.synthesizeMouseAtCenter("#ad1", {}, tab.linkedBrowser);
|
||||
await pageLoadPromise;
|
||||
await promiseWaitForAdLinkCheck();
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.tabhistory": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.content.unknown": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.withads.tabhistory": { [expectedScalarKey]: 1 },
|
||||
"browser.search.withads.unknown": { [expectedScalarKey]: 1 },
|
||||
"browser.search.adclicks.tabhistory": { [expectedScalarKey]: 1 },
|
||||
"browser.search.adclicks.unknown": { [expectedScalarKey]: 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "tabhistory",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
|
||||
add_task(async function test_track_ad_click() {
|
||||
await track_ad_click(false);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_click_organic() {
|
||||
await track_ad_click(true);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_click_with_location_change_other_tab() {
|
||||
resetTelemetry();
|
||||
const url = getSERPUrl("searchTelemetryAd.html");
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { "example:tagged:ff": 1 },
|
||||
"browser.search.withads.unknown": { "example:tagged": 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
const newTab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"https://example.com"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.switchTab(gBrowser, tab);
|
||||
|
||||
let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
|
||||
BrowserTestUtils.synthesizeMouseAtCenter("#ad1", {}, tab.linkedBrowser);
|
||||
await pageLoadPromise;
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { "example:tagged:ff": 1 },
|
||||
"browser.search.withads.unknown": { "example:tagged": 1 },
|
||||
"browser.search.adclicks.unknown": { "example:tagged": 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(newTab);
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,303 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/*
|
||||
* Tests for SearchSERPTelemetry associated with ad clicks.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Note: example.org is used for the SERP page, and example.com is used to serve
|
||||
// the ads. This is done to simulate different domains like the real servers.
|
||||
const TEST_PROVIDER_INFO = [
|
||||
{
|
||||
telemetryId: "example",
|
||||
searchPageRegexp:
|
||||
/^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetry(?:Ad)?.html/,
|
||||
queryParamName: "s",
|
||||
codeParamName: "abc",
|
||||
taggedCodes: ["ff"],
|
||||
followOnParamNames: ["a"],
|
||||
extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
|
||||
components: [
|
||||
{
|
||||
type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
add_setup(async function () {
|
||||
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
|
||||
await waitForIdle();
|
||||
// Enable local telemetry recording for the duration of the tests.
|
||||
let oldCanRecord = Services.telemetry.canRecordExtended;
|
||||
Services.telemetry.canRecordExtended = true;
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.search.log", true],
|
||||
["browser.search.serpEventTelemetry.enabled", true],
|
||||
],
|
||||
});
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
SearchSERPTelemetry.overrideSearchTelemetryForTests();
|
||||
Services.telemetry.canRecordExtended = oldCanRecord;
|
||||
resetTelemetry();
|
||||
});
|
||||
});
|
||||
|
||||
async function track_ad_click(testOrganic) {
|
||||
// Note: the above tests have already checked a page with no ad-urls.
|
||||
resetTelemetry();
|
||||
|
||||
let expectedScalarKey = `example:${testOrganic ? "organic" : "tagged"}`;
|
||||
let expectedContentScalarKey = `example:${
|
||||
testOrganic ? "organic:none" : "tagged:ff"
|
||||
}`;
|
||||
let tagged = testOrganic ? "false" : "true";
|
||||
let partnerCode = testOrganic ? "" : "ff";
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("searchTelemetryAd.html", testOrganic)
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.withads.unknown": {
|
||||
[expectedScalarKey.replace("sap", "tagged")]: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
await promiseAdImpressionReceived(1);
|
||||
|
||||
let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
|
||||
BrowserTestUtils.synthesizeMouseAtCenter("#ad1", {}, tab.linkedBrowser);
|
||||
await pageLoadPromise;
|
||||
await promiseWaitForAdLinkCheck();
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.withads.unknown": { [expectedScalarKey]: 1 },
|
||||
"browser.search.adclicks.unknown": { [expectedScalarKey]: 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// Now go back, and click again.
|
||||
pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
|
||||
gBrowser.goBack();
|
||||
await pageLoadPromise;
|
||||
await promiseWaitForAdLinkCheck();
|
||||
|
||||
// We've gone back, so we register an extra display & if it is with ads or not.
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.tabhistory": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.content.unknown": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.withads.tabhistory": { [expectedScalarKey]: 1 },
|
||||
"browser.search.withads.unknown": { [expectedScalarKey]: 1 },
|
||||
"browser.search.adclicks.unknown": { [expectedScalarKey]: 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "tabhistory",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
await promiseAdImpressionReceived(2);
|
||||
|
||||
pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
|
||||
BrowserTestUtils.synthesizeMouseAtCenter("#ad1", {}, tab.linkedBrowser);
|
||||
await pageLoadPromise;
|
||||
await promiseWaitForAdLinkCheck();
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.tabhistory": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.content.unknown": { [expectedContentScalarKey]: 1 },
|
||||
"browser.search.withads.tabhistory": { [expectedScalarKey]: 1 },
|
||||
"browser.search.withads.unknown": { [expectedScalarKey]: 1 },
|
||||
"browser.search.adclicks.tabhistory": { [expectedScalarKey]: 1 },
|
||||
"browser.search.adclicks.unknown": { [expectedScalarKey]: 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged,
|
||||
partner_code: partnerCode,
|
||||
source: "tabhistory",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
|
||||
add_task(async function test_track_ad_click() {
|
||||
await track_ad_click(false);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_click_organic() {
|
||||
await track_ad_click(true);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_click_with_location_change_other_tab() {
|
||||
resetTelemetry();
|
||||
const url = getSERPUrl("searchTelemetryAd.html");
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { "example:tagged:ff": 1 },
|
||||
"browser.search.withads.unknown": { "example:tagged": 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
const newTab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"https://example.com"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.switchTab(gBrowser, tab);
|
||||
|
||||
let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
|
||||
BrowserTestUtils.synthesizeMouseAtCenter("#ad1", {}, tab.linkedBrowser);
|
||||
await pageLoadPromise;
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { "example:tagged:ff": 1 },
|
||||
"browser.search.withads.unknown": { "example:tagged": 1 },
|
||||
"browser.search.adclicks.unknown": { "example:tagged": 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
engagements: [
|
||||
{
|
||||
action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
|
||||
target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(newTab);
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/*
|
||||
* Tests for SearchSERPTelemetry associated with ad links found in data attributes.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Note: example.org is used for the SERP page, and example.com is used to serve
|
||||
// the ads. This is done to simulate different domains like the real servers.
|
||||
const TEST_PROVIDER_INFO = [
|
||||
{
|
||||
telemetryId: "example-data-attributes",
|
||||
searchPageRegexp:
|
||||
/^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetryAd_dataAttributes(?:_none|_href)?.html/,
|
||||
queryParamName: "s",
|
||||
codeParamName: "abc",
|
||||
taggedCodes: ["ff"],
|
||||
adServerAttributes: ["xyz"],
|
||||
extraAdServersRegexps: [/^https:\/\/example\.com\/ad/],
|
||||
components: [
|
||||
{
|
||||
type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
add_setup(async function () {
|
||||
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
|
||||
await waitForIdle();
|
||||
// Enable local telemetry recording for the duration of the tests.
|
||||
let oldCanRecord = Services.telemetry.canRecordExtended;
|
||||
Services.telemetry.canRecordExtended = true;
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.search.log", true],
|
||||
["browser.search.serpEventTelemetry.enabled", true],
|
||||
],
|
||||
});
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
SearchSERPTelemetry.overrideSearchTelemetryForTests();
|
||||
Services.telemetry.canRecordExtended = oldCanRecord;
|
||||
resetTelemetry();
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_on_data_attributes() {
|
||||
resetTelemetry();
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("searchTelemetryAd_dataAttributes.html")
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": {
|
||||
"example-data-attributes:tagged:ff": 1,
|
||||
},
|
||||
"browser.search.withads.unknown": {
|
||||
"example-data-attributes:tagged": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example-data-attributes",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_on_data_attributes_and_hrefs() {
|
||||
resetTelemetry();
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("searchTelemetryAd_dataAttributes_href.html")
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": {
|
||||
"example-data-attributes:tagged:ff": 1,
|
||||
},
|
||||
"browser.search.withads.unknown": {
|
||||
"example-data-attributes:tagged": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example-data-attributes",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_track_no_ad_on_data_attributes_and_hrefs() {
|
||||
resetTelemetry();
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("searchTelemetryAd_dataAttributes_none.html")
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": {
|
||||
"example-data-attributes:tagged:ff": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "example-data-attributes",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
|
@ -0,0 +1,126 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/*
|
||||
* Tests for SearchSERPTelemetry associated with ad links and load events.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Note: example.org is used for the SERP page, and example.com is used to serve
|
||||
// the ads. This is done to simulate different domains like the real servers.
|
||||
const TEST_PROVIDER_INFO = [
|
||||
{
|
||||
telemetryId: "slow-page-load",
|
||||
searchPageRegexp:
|
||||
/^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/slow_loading_page_with_ads(_on_load_event)?.html/,
|
||||
queryParamName: "s",
|
||||
codeParamName: "abc",
|
||||
taggedCodes: ["ff"],
|
||||
followOnParamNames: ["a"],
|
||||
extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
|
||||
components: [
|
||||
{
|
||||
type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
add_setup(async function () {
|
||||
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
|
||||
await waitForIdle();
|
||||
// Enable local telemetry recording for the duration of the tests.
|
||||
let oldCanRecord = Services.telemetry.canRecordExtended;
|
||||
Services.telemetry.canRecordExtended = true;
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.search.log", true],
|
||||
["browser.search.serpEventTelemetry.enabled", true],
|
||||
],
|
||||
});
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
SearchSERPTelemetry.overrideSearchTelemetryForTests();
|
||||
Services.telemetry.canRecordExtended = oldCanRecord;
|
||||
resetTelemetry();
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_on_DOMContentLoaded() {
|
||||
resetTelemetry();
|
||||
|
||||
let observeAdPreviouslyRecorded = TestUtils.consoleMessageObserved(msg => {
|
||||
return (
|
||||
typeof msg.wrappedJSObject.arguments?.[0] == "string" &&
|
||||
msg.wrappedJSObject.arguments[0].includes(
|
||||
"Ad was previously reported for browser with URI"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("slow_loading_page_with_ads.html")
|
||||
);
|
||||
|
||||
// Observe ad was counted on DOMContentLoaded.
|
||||
// We do not count the ad again on load.
|
||||
await observeAdPreviouslyRecorded;
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { "slow-page-load:tagged:ff": 1 },
|
||||
"browser.search.withads.unknown": { "slow-page-load:tagged": 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "slow-page-load",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_track_ad_on_load_event() {
|
||||
resetTelemetry();
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
getSERPUrl("slow_loading_page_with_ads_on_load_event.html")
|
||||
);
|
||||
|
||||
await assertSearchSourcesTelemetry(
|
||||
{},
|
||||
{
|
||||
"browser.search.content.unknown": { "slow-page-load:tagged:ff": 1 },
|
||||
"browser.search.withads.unknown": { "slow-page-load:tagged": 1 },
|
||||
}
|
||||
);
|
||||
|
||||
assertImpressionEvents([
|
||||
{
|
||||
impression: {
|
||||
provider: "slow-page-load",
|
||||
tagged: "true",
|
||||
partner_code: "ff",
|
||||
source: "unknown",
|
||||
is_shopping_page: "false",
|
||||
shopping_tab_displayed: "false",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
|
@ -96,6 +96,33 @@ add_task(async function test_reactivated_product_button_click() {
|
|||
});
|
||||
});
|
||||
|
||||
add_task(async function test_no_reliability_available_request_click() {
|
||||
await Services.fog.testFlushAllChildren();
|
||||
Services.fog.testResetFOG();
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
url: "about:shoppingsidebar",
|
||||
gBrowser,
|
||||
},
|
||||
async browser => {
|
||||
await clickCheckReviewQualityButton(
|
||||
browser,
|
||||
MOCK_UNANALYZED_PRODUCT_RESPONSE
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await Services.fog.testFlushAllChildren();
|
||||
var requestEvents =
|
||||
Glean.shopping.surfaceAnalyzeReviewsNoneAvailableClicked.testGetValue();
|
||||
|
||||
assertEventMatches(requestEvents[0], {
|
||||
category: "shopping",
|
||||
name: "surface_analyze_reviews_none_available_clicked",
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_shopping_sidebar_displayed() {
|
||||
Services.fog.testResetFOG();
|
||||
|
||||
|
@ -330,3 +357,18 @@ function clickShowMoreButton(browser, data) {
|
|||
button.click();
|
||||
});
|
||||
}
|
||||
|
||||
function clickCheckReviewQualityButton(browser, data) {
|
||||
return SpecialPowers.spawn(browser, [data], async mockData => {
|
||||
let shoppingContainer =
|
||||
content.document.querySelector("shopping-container").wrappedJSObject;
|
||||
shoppingContainer.data = Cu.cloneInto(mockData, content);
|
||||
await shoppingContainer.updateComplete;
|
||||
|
||||
let button = shoppingContainer.unanalyzedProductEl.shadowRoot
|
||||
.querySelector("shopping-card")
|
||||
.querySelector("button");
|
||||
|
||||
button.click();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -85,17 +85,17 @@
|
|||
|
||||
<html:moz-button-group class="panel-footer translations-panel-footer">
|
||||
<button id="translations-panel-restore-button"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
oncommand="TranslationsPanel.onRestore(event);"
|
||||
data-l10n-id="translations-panel-restore-button">
|
||||
</button>
|
||||
<button id="translations-panel-cancel"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
oncommand="TranslationsPanel.onCancel(event);"
|
||||
data-l10n-id="translations-panel-translate-cancel">
|
||||
</button>
|
||||
<button id="translations-panel-translate"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
oncommand="TranslationsPanel.onTranslate(event);"
|
||||
data-l10n-id="translations-panel-translate-button"
|
||||
default="true">
|
||||
|
@ -127,12 +127,12 @@
|
|||
|
||||
<html:moz-button-group class="panel-footer translations-panel-footer">
|
||||
<button id="translations-panel-change-source-language"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
oncommand="TranslationsPanel.onChangeSourceLanguage(event);"
|
||||
data-l10n-id="translations-panel-error-change-button">
|
||||
</button>
|
||||
<button id="translations-panel-dismiss-error"
|
||||
class="footer-button"
|
||||
class="panel-footer-button"
|
||||
oncommand="TranslationsPanel.onCancel(event);"
|
||||
data-l10n-id="translations-panel-error-dismiss-button"
|
||||
default="true">
|
||||
|
|
|
@ -249,3 +249,12 @@ device-migration-fxa-spotlight-header = Using an older device?
|
|||
device-migration-fxa-spotlight-body = Back up your data to make sure you don’t lose important info like bookmarks and passwords — especially if you switch to a new device.
|
||||
device-migration-fxa-spotlight-primary-button = How to back up my data
|
||||
device-migration-fxa-spotlight-link = Remind me later
|
||||
|
||||
## Set as Default PDF Reader Infobar
|
||||
|
||||
# The question portion of the following message should have the <strong> and </strong> tags surrounding it.
|
||||
pdf-default-notification-message = <strong>Make { -brand-short-name } your default PDF reader?</strong> Use { -brand-short-name } to read and edit PDFs saved to your computer.
|
||||
pdf-default-notification-set-default-button =
|
||||
.label = Set as default
|
||||
pdf-default-notification-decline-button =
|
||||
.label = Not now
|
||||
|
|
|
@ -357,7 +357,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "2bf954ef211e39a38b567368e63a8a163ca3244c"
|
||||
"revision": "36529d62176ad341ce92aa19406bbd26b506c7f1"
|
||||
},
|
||||
"de": {
|
||||
"pin": false,
|
||||
|
@ -411,7 +411,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "42f6d9c818d6356082d83bc084c2f80f913fa8ca"
|
||||
"revision": "0825b19746e8d434ba8ca40a38146e57d80b28c2"
|
||||
},
|
||||
"en-CA": {
|
||||
"pin": false,
|
||||
|
@ -645,7 +645,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "2c308a50f0a8a93bc31f7c73ff61c492ae7d42f3"
|
||||
"revision": "636426d9b5cb4b7571e015a954b5fc85249e27cf"
|
||||
},
|
||||
"fur": {
|
||||
"pin": false,
|
||||
|
@ -663,7 +663,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "e4550638548ad75946bee43a752d8bc8da1b55cf"
|
||||
"revision": "b2a3c6241b19300705d3604a711dd477e38ca15a"
|
||||
},
|
||||
"fy-NL": {
|
||||
"pin": false,
|
||||
|
@ -985,7 +985,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "6c5306bf0a8462ea89c76c346ca14e4abea3615d"
|
||||
"revision": "1f4301daf719ed4caf8c2e8a710c3ffd5f4ee13b"
|
||||
},
|
||||
"ja-JP-mac": {
|
||||
"pin": false,
|
||||
|
@ -993,7 +993,7 @@
|
|||
"macosx64",
|
||||
"macosx64-devedition"
|
||||
],
|
||||
"revision": "a158458dacb8c97fa811c986949efbb7681195d8"
|
||||
"revision": "5d6e06b27c3343b651c3122b3e5a77eea31da6e4"
|
||||
},
|
||||
"ka": {
|
||||
"pin": false,
|
||||
|
@ -1353,7 +1353,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "9f91aa4fe4ed5a5425c1f891903f2d251cb402a6"
|
||||
"revision": "05f957b1b5860ecc80e698fe898fb9c85a1e1a98"
|
||||
},
|
||||
"oc": {
|
||||
"pin": false,
|
||||
|
@ -1785,7 +1785,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "7755cd8a802c951464295215b1e6b72cd7f3234a"
|
||||
"revision": "539788cf7d8be76a73a7558b063f36b16124e56a"
|
||||
},
|
||||
"th": {
|
||||
"pin": false,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "terminal_size"
|
||||
version = "0.2.999"
|
||||
version = "0.3.999"
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
|
|
|
@ -17078,7 +17078,6 @@ neqo_http3conn_is_zero_rtt
|
|||
?ClassName@js@@YA?AV?$Handle@PAVPropertyName@js@@@JS@@W4JSProtoKey@@PAUJSContext@@@Z
|
||||
?FormatStackDump@JS@@YA?AV?$UniquePtr@$$BY0A@DUFreePolicy@JS@@@mozilla@@PAUJSContext@@_N11@Z
|
||||
?callee@FrameIter@js@@QBEPAVJSFunction@@PAUJSContext@@@Z
|
||||
?jsprintf@Sprinter@js@@QAA_NPBDZZ
|
||||
?vprintf@GenericPrinter@js@@QAE_NPBDPAD@Z
|
||||
?release@Sprinter@js@@QAE?AV?$UniquePtr@$$BY0A@DUFreePolicy@JS@@@mozilla@@XZ
|
||||
??1Sprinter@js@@QAE@XZ
|
||||
|
|
|
@ -16849,7 +16849,6 @@ neqo_http3conn_is_zero_rtt
|
|||
?ClassName@js@@YA?AV?$Handle@PEAVPropertyName@js@@@JS@@W4JSProtoKey@@PEAUJSContext@@@Z
|
||||
?FormatStackDump@JS@@YA?AV?$UniquePtr@$$BY0A@DUFreePolicy@JS@@@mozilla@@PEAUJSContext@@_N11@Z
|
||||
?callee@FrameIter@js@@QEBAPEAVJSFunction@@PEAUJSContext@@@Z
|
||||
?jsprintf@Sprinter@js@@QEAA_NPEBDZZ
|
||||
?vprintf@GenericPrinter@js@@QEAA_NPEBDPEAD@Z
|
||||
?release@Sprinter@js@@QEAA?AV?$UniquePtr@$$BY0A@DUFreePolicy@JS@@@mozilla@@XZ
|
||||
??1Sprinter@js@@QEAA@XZ
|
||||
|
|
|
@ -499,6 +499,9 @@ MarkupView.prototype = {
|
|||
},
|
||||
|
||||
_disableImagePreviewTooltip() {
|
||||
if (!this.imagePreviewTooltip) {
|
||||
return;
|
||||
}
|
||||
this.imagePreviewTooltip.stopTogglingOnHover();
|
||||
},
|
||||
|
||||
|
@ -825,7 +828,9 @@ MarkupView.prototype = {
|
|||
const container = this.getContainer(nodeFront);
|
||||
const badge = container?.editor?.displayBadge;
|
||||
if (badge) {
|
||||
badge.classList.toggle("active", eventName == "highlighter-shown");
|
||||
const isActive = eventName == "highlighter-shown";
|
||||
badge.classList.toggle("active", isActive);
|
||||
badge.setAttribute("aria-pressed", isActive);
|
||||
}
|
||||
|
||||
// There is a limit to how many grid highlighters can be active at the same time.
|
||||
|
@ -1276,6 +1281,15 @@ MarkupView.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
// If the selected element is a button (e.g. `flex` badge), we don't want to highjack
|
||||
// keyboard activation.
|
||||
if (
|
||||
event.target.closest(":is(button, [role=button])") &&
|
||||
(name === "Enter" || name === "Space")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = shortcutHandlers[name];
|
||||
const shouldPropagate = handler(this);
|
||||
if (shouldPropagate) {
|
||||
|
|
|
@ -179,6 +179,8 @@ skip-if = ["true"] # Bug 1177550
|
|||
|
||||
["browser_markup_events_jquery_2.1.1.js"]
|
||||
|
||||
["browser_markup_events_keyboard_navigation.js"]
|
||||
|
||||
["browser_markup_events_object_listener.js"]
|
||||
|
||||
["browser_markup_events_react_development_15.4.1.js"]
|
||||
|
|
|
@ -33,6 +33,7 @@ const TEST_DATA = [
|
|||
before: {
|
||||
textContent: "grid",
|
||||
visible: true,
|
||||
interactive: true,
|
||||
},
|
||||
async changeStyle() {
|
||||
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
|
||||
|
@ -59,6 +60,27 @@ const TEST_DATA = [
|
|||
after: {
|
||||
textContent: "grid",
|
||||
visible: true,
|
||||
interactive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Showing a 'contents' node by changing its style property",
|
||||
selector: "#grid",
|
||||
before: {
|
||||
textContent: "grid",
|
||||
visible: true,
|
||||
interactive: true,
|
||||
},
|
||||
async changeStyle() {
|
||||
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
|
||||
const node = content.document.getElementById("grid");
|
||||
node.style.display = "contents";
|
||||
});
|
||||
},
|
||||
after: {
|
||||
textContent: "contents",
|
||||
visible: true,
|
||||
interactive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -76,6 +98,7 @@ const TEST_DATA = [
|
|||
after: {
|
||||
textContent: "grid",
|
||||
visible: true,
|
||||
interactive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -92,6 +115,7 @@ const TEST_DATA = [
|
|||
after: {
|
||||
textContent: "flex",
|
||||
visible: true,
|
||||
interactive: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -115,7 +139,7 @@ async function runTestData(
|
|||
const container = await getContainerForSelector(selector, inspector);
|
||||
|
||||
const beforeBadge = container.elt.querySelector(
|
||||
".inspector-badge.interactive[data-display]"
|
||||
".inspector-badge[data-display]"
|
||||
);
|
||||
is(
|
||||
!!beforeBadge,
|
||||
|
@ -128,6 +152,7 @@ async function runTestData(
|
|||
before.textContent,
|
||||
`Got the correct before display type for ${selector}: ${beforeBadge.textContent}`
|
||||
);
|
||||
checkBadgeInteractiveState(beforeBadge, before.interactive, selector);
|
||||
}
|
||||
|
||||
info("Listening for the display-change event");
|
||||
|
@ -148,7 +173,7 @@ async function runTestData(
|
|||
ok(foundContainer, "Container is part of the list of changed nodes");
|
||||
|
||||
const afterBadge = container.elt.querySelector(
|
||||
".inspector-badge.interactive[data-display]"
|
||||
".inspector-badge[data-display]"
|
||||
);
|
||||
is(
|
||||
!!afterBadge,
|
||||
|
@ -161,5 +186,31 @@ async function runTestData(
|
|||
after.textContent,
|
||||
`Got the correct after display type for ${selector}: ${afterBadge.textContent}`
|
||||
);
|
||||
|
||||
checkBadgeInteractiveState(afterBadge, after.interactive, selector);
|
||||
}
|
||||
}
|
||||
|
||||
function checkBadgeInteractiveState(badgeEl, interactive, selector) {
|
||||
if (interactive) {
|
||||
ok(
|
||||
!badgeEl.hasAttribute("role"),
|
||||
`${badgeEl.textContent} badge for ${selector} does not override the default role`
|
||||
);
|
||||
is(
|
||||
badgeEl.getAttribute("aria-pressed"),
|
||||
"false",
|
||||
`${badgeEl.textContent} badge for ${selector} has the expected aria-pressed attribute`
|
||||
);
|
||||
} else {
|
||||
is(
|
||||
badgeEl.getAttribute("role"),
|
||||
"presentation",
|
||||
`${badgeEl.textContent} badge for ${selector} is not interactive`
|
||||
);
|
||||
ok(
|
||||
!badgeEl.hasAttribute("aria-pressed"),
|
||||
`${badgeEl.textContent} badge for ${selector} does not have an aria-pressed attribute`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,32 +49,22 @@ async function runTests(inspector) {
|
|||
const tooltip = inspector.markup.eventDetailsTooltip;
|
||||
|
||||
info("Clicking to open event tooltip.");
|
||||
|
||||
let onInspectorUpdated = inspector.once("inspector-updated");
|
||||
const onTooltipShown = tooltip.once("shown");
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
evHolder,
|
||||
{},
|
||||
inspector.markup.doc.defaultView
|
||||
);
|
||||
|
||||
await onTooltipShown;
|
||||
// New node is selected when clicking on the events bubble, wait for inspector-updated.
|
||||
await onInspectorUpdated;
|
||||
|
||||
ok(tooltip.isVisible(), "EventTooltip visible.");
|
||||
|
||||
onInspectorUpdated = inspector.once("inspector-updated");
|
||||
const onTooltipHidden = tooltip.once("hidden");
|
||||
|
||||
info("Click on another tag to hide the event tooltip");
|
||||
const onTooltipHidden = tooltip.once("hidden");
|
||||
const script = await getContainerForSelector("script", inspector);
|
||||
const tag = script.elt.querySelector(".tag");
|
||||
EventUtils.synthesizeMouseAtCenter(tag, {}, inspector.markup.doc.defaultView);
|
||||
|
||||
await onTooltipHidden;
|
||||
// New node is selected, wait for inspector-updated.
|
||||
await onInspectorUpdated;
|
||||
|
||||
ok(!tooltip.isVisible(), "EventTooltip hidden.");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* import-globals-from helper_events_test_runner.js */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the event listeners popup can be used from the keyboard.
|
||||
|
||||
const TEST_URL = URL_ROOT_SSL + "doc_markup_events_toggle.html";
|
||||
|
||||
loadHelperScript("helper_events_test_runner.js");
|
||||
|
||||
add_task(async function () {
|
||||
const { inspector } = await openInspectorForURL(TEST_URL);
|
||||
await inspector.markup.expandAll();
|
||||
await selectNode("#target", inspector);
|
||||
|
||||
info("Check that the event tooltip has the expected content");
|
||||
const container = await getContainerForSelector("#target", inspector);
|
||||
const eventTooltipBadge = container.elt.querySelector(
|
||||
".inspector-badge.interactive[data-event]"
|
||||
);
|
||||
ok(eventTooltipBadge, "The event tooltip badge is displayed");
|
||||
|
||||
const tooltip = inspector.markup.eventDetailsTooltip;
|
||||
const onTooltipShown = tooltip.once("shown");
|
||||
eventTooltipBadge.focus();
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, eventTooltipBadge.ownerGlobal);
|
||||
await onTooltipShown;
|
||||
ok(true, "The tooltip is shown");
|
||||
|
||||
// TODO: More keyboard interactions will be added as part of Bug 1843330
|
||||
});
|
|
@ -44,6 +44,11 @@ add_task(async function () {
|
|||
!flexDisplayBadge.classList.contains("active"),
|
||||
"flex display badge is not active."
|
||||
);
|
||||
is(
|
||||
flexDisplayBadge.getAttribute("aria-pressed"),
|
||||
"false",
|
||||
"flex display badge is not pressed."
|
||||
);
|
||||
ok(
|
||||
flexDisplayBadge.classList.contains("interactive"),
|
||||
"flex display badge is interactive."
|
||||
|
@ -60,7 +65,7 @@ add_task(async function () {
|
|||
);
|
||||
|
||||
info("Toggling ON the flexbox highlighter from the flex display badge.");
|
||||
const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
|
||||
let onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
|
||||
let onCheckboxChange = waitUntilState(
|
||||
store,
|
||||
state => state.flexbox.highlighted
|
||||
|
@ -84,13 +89,18 @@ add_task(async function () {
|
|||
flexDisplayBadge.classList.contains("active"),
|
||||
"flex display badge is active."
|
||||
);
|
||||
is(
|
||||
flexDisplayBadge.getAttribute("aria-pressed"),
|
||||
"true",
|
||||
"flex display badge is pressed."
|
||||
);
|
||||
ok(
|
||||
flexDisplayBadge.classList.contains("interactive"),
|
||||
"flex display badge is interactive."
|
||||
);
|
||||
|
||||
info("Toggling OFF the flexbox highlighter from the flex display badge.");
|
||||
const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
|
||||
let onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
|
||||
onCheckboxChange = waitUntilState(store, state => !state.flexbox.highlighted);
|
||||
flexDisplayBadge.click();
|
||||
await onHighlighterHidden;
|
||||
|
@ -100,8 +110,54 @@ add_task(async function () {
|
|||
!flexDisplayBadge.classList.contains("active"),
|
||||
"flex display badge is not active."
|
||||
);
|
||||
is(
|
||||
flexDisplayBadge.getAttribute("aria-pressed"),
|
||||
"false",
|
||||
"flex display badge is no longer pressed."
|
||||
);
|
||||
ok(
|
||||
flexDisplayBadge.classList.contains("interactive"),
|
||||
"flex display badge is interactive."
|
||||
);
|
||||
|
||||
info("Toggling ON the flexbox highlighter from the keyboard.");
|
||||
onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
|
||||
onCheckboxChange = waitUntilState(store, state => state.flexbox.highlighted);
|
||||
|
||||
flexDisplayBadge.focus();
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, flexDisplayBadge.ownerGlobal);
|
||||
await onHighlighterShown;
|
||||
await onCheckboxChange;
|
||||
|
||||
ok(
|
||||
getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
|
||||
"Flexbox highlighter was displayed from the keyboard."
|
||||
);
|
||||
ok(
|
||||
flexDisplayBadge.classList.contains("active"),
|
||||
"flex display badge is active."
|
||||
);
|
||||
is(
|
||||
flexDisplayBadge.getAttribute("aria-pressed"),
|
||||
"true",
|
||||
"flex display badge is pressed."
|
||||
);
|
||||
|
||||
info("Toggling OFF the flexbox highlighter from the keyboard.");
|
||||
onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
|
||||
onCheckboxChange = waitUntilState(store, state => !state.flexbox.highlighted);
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, flexDisplayBadge.ownerGlobal);
|
||||
await onHighlighterHidden;
|
||||
await onCheckboxChange;
|
||||
|
||||
ok(true, "Highlighter was hidden from the keyboard");
|
||||
ok(
|
||||
!flexDisplayBadge.classList.contains("active"),
|
||||
"flex display badge was deactivated from the keyboard"
|
||||
);
|
||||
is(
|
||||
flexDisplayBadge.getAttribute("aria-pressed"),
|
||||
"false",
|
||||
"flex display badge is no longer pressed."
|
||||
);
|
||||
});
|
||||
|
|
|
@ -41,11 +41,17 @@ add_task(async function () {
|
|||
);
|
||||
|
||||
const container = await getContainerForSelector("#top", inspector);
|
||||
const scrollableBage = container.elt.querySelector(".scrollable-badge");
|
||||
is(
|
||||
scrollableBage.getAttribute("aria-pressed"),
|
||||
"false",
|
||||
"Scrollable badge is not pressed by default"
|
||||
);
|
||||
|
||||
info(
|
||||
"Clicking on the scrollable badge so that the overflow causing elements show up in the markup view."
|
||||
);
|
||||
container.editor._scrollableBadge.click();
|
||||
scrollableBage.click();
|
||||
|
||||
await waitForContainers(["#child1", "#child3", "#child4"], inspector);
|
||||
|
||||
|
@ -55,9 +61,11 @@ add_task(async function () {
|
|||
inspector
|
||||
);
|
||||
|
||||
ok(
|
||||
container.editor._scrollableBadge.classList.contains("active"),
|
||||
"Scrollable badge is active"
|
||||
ok(scrollableBage.classList.contains("active"), "Scrollable badge is active");
|
||||
is(
|
||||
scrollableBage.getAttribute("aria-pressed"),
|
||||
"true",
|
||||
"Scrollable badge is pressed"
|
||||
);
|
||||
|
||||
checkTelemetry("devtools.markup.scrollable.badge.clicked", "", 1, "scalar");
|
||||
|
@ -81,7 +89,7 @@ add_task(async function () {
|
|||
info(
|
||||
"Clicking on the scrollable badge again so that all the overflow highlight gets removed."
|
||||
);
|
||||
container.editor._scrollableBadge.click();
|
||||
scrollableBage.click();
|
||||
|
||||
await checkOverflowHighlight(
|
||||
[],
|
||||
|
@ -90,17 +98,53 @@ add_task(async function () {
|
|||
);
|
||||
|
||||
ok(
|
||||
!container.editor._scrollableBadge.classList.contains("active"),
|
||||
!scrollableBage.classList.contains("active"),
|
||||
"Scrollable badge is not active"
|
||||
);
|
||||
is(
|
||||
scrollableBage.getAttribute("aria-pressed"),
|
||||
"false",
|
||||
"Scrollable badge is not pressed anymore"
|
||||
);
|
||||
|
||||
checkTelemetry("devtools.markup.scrollable.badge.clicked", "", 2, "scalar");
|
||||
|
||||
info("Double-click on the scrollable badge");
|
||||
EventUtils.sendMouseEvent(
|
||||
{ type: "dblclick" },
|
||||
container.editor._scrollableBadge
|
||||
info("Triggering badge with the keyboard");
|
||||
scrollableBage.focus();
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, scrollableBage.ownerGlobal);
|
||||
await checkOverflowHighlight(
|
||||
["#child2", "#child3"],
|
||||
["#child1", "#child4"],
|
||||
inspector
|
||||
);
|
||||
ok(
|
||||
scrollableBage.classList.contains("active"),
|
||||
"badge can be activated with the keyboard"
|
||||
);
|
||||
is(
|
||||
scrollableBage.getAttribute("aria-pressed"),
|
||||
"true",
|
||||
"Scrollable badge is pressed"
|
||||
);
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, scrollableBage.ownerGlobal);
|
||||
await checkOverflowHighlight(
|
||||
[],
|
||||
["#child1", "#child2", "#child3", "#child4"],
|
||||
inspector
|
||||
);
|
||||
ok(
|
||||
!scrollableBage.classList.contains("active"),
|
||||
"Scrollable badge can be deactivated with the keyboard"
|
||||
);
|
||||
is(
|
||||
scrollableBage.getAttribute("aria-pressed"),
|
||||
"false",
|
||||
"Scrollable badge is not pressed anymore"
|
||||
);
|
||||
|
||||
info("Double-click on the scrollable badge");
|
||||
EventUtils.sendMouseEvent({ type: "dblclick" }, scrollableBage);
|
||||
ok(
|
||||
container.expanded,
|
||||
"Double clicking on the badge did not collapse the container"
|
||||
|
|
|
@ -91,6 +91,10 @@ async function runTest(inspector, toolbox, selector, contentMethod) {
|
|||
".inspector-badge.interactive[data-custom]"
|
||||
);
|
||||
ok(customBadge, "[custom] badge is visible");
|
||||
ok(
|
||||
!customBadge.hasAttribute("aria-pressed"),
|
||||
"[custom] badge is not a toggle button"
|
||||
);
|
||||
|
||||
info("Click on the `custom` badge and verify that the debugger opens.");
|
||||
let onDebuggerReady = toolbox.getPanelWhenReady("jsdebugger");
|
||||
|
@ -99,6 +103,20 @@ async function runTest(inspector, toolbox, selector, contentMethod) {
|
|||
|
||||
const debuggerContext = createDebuggerContext(toolbox);
|
||||
await waitUntilDebuggerReady(debuggerContext);
|
||||
ok(true, "The debugger was opened when clicking on the custom badge");
|
||||
|
||||
info("Switch to the inspector");
|
||||
await toolbox.selectTool("inspector");
|
||||
|
||||
// Check that the debugger can be opened with the keyboard.
|
||||
info("Press the Enter key and verify that the debugger opens.");
|
||||
customBadge.focus();
|
||||
onDebuggerReady = toolbox.getPanelWhenReady("jsdebugger");
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, customBadge.ownerGlobal);
|
||||
|
||||
await onDebuggerReady;
|
||||
await waitUntilDebuggerReady(debuggerContext);
|
||||
ok(true, "The debugger was opened via the keyboard");
|
||||
|
||||
info("Switch to the inspector");
|
||||
await toolbox.selectTool("inspector");
|
||||
|
@ -114,6 +132,7 @@ async function runTest(inspector, toolbox, selector, contentMethod) {
|
|||
await onDebuggerReady;
|
||||
|
||||
await waitUntilDebuggerReady(debuggerContext);
|
||||
ok(true, "The debugger was opened via the context menu");
|
||||
|
||||
info("Switch to the inspector");
|
||||
await toolbox.selectTool("inspector");
|
||||
|
|
|
@ -42,10 +42,11 @@ add_task(async function () {
|
|||
await expandContainer(inspector, slotContainer);
|
||||
|
||||
info("Find the 'Show all nodes' button");
|
||||
const button = slotContainer.elt.querySelector("button");
|
||||
console.log(button);
|
||||
const button = slotContainer.elt.querySelector(
|
||||
"button:not(.inspector-badge)"
|
||||
);
|
||||
ok(
|
||||
button.innerText.includes(NODE_COUNT),
|
||||
"'Show all nodes' button contains correct node count"
|
||||
`'Show all nodes' button contains correct node count (expected "${button.innerText}" to include "${NODE_COUNT}")`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -357,13 +357,14 @@ ElementEditor.prototype = {
|
|||
},
|
||||
|
||||
_createEventBadge() {
|
||||
this._eventBadge = this.doc.createElement("div");
|
||||
this._eventBadge = this.doc.createElement("button");
|
||||
this._eventBadge.className = "inspector-badge interactive";
|
||||
this._eventBadge.dataset.event = "true";
|
||||
this._eventBadge.textContent = "event";
|
||||
this._eventBadge.title = INSPECTOR_L10N.getStr(
|
||||
"markupView.event.tooltiptext"
|
||||
);
|
||||
this._eventBadge.setAttribute("aria-pressed", "false");
|
||||
// Badges order is [event][display][custom], insert event badge before others.
|
||||
this.elt.insertBefore(
|
||||
this._eventBadge,
|
||||
|
@ -388,7 +389,9 @@ ElementEditor.prototype = {
|
|||
// overflow causing elements is not supported.
|
||||
!this.node.isDocumentElement;
|
||||
|
||||
this._scrollableBadge = this.doc.createElement("div");
|
||||
this._scrollableBadge = this.doc.createElement(
|
||||
isInteractive ? "button" : "div"
|
||||
);
|
||||
this._scrollableBadge.className = `inspector-badge scrollable-badge ${
|
||||
isInteractive ? "interactive" : ""
|
||||
}`;
|
||||
|
@ -407,6 +410,7 @@ ElementEditor.prototype = {
|
|||
"click",
|
||||
this.onScrollableBadgeClick
|
||||
);
|
||||
this._scrollableBadge.setAttribute("aria-pressed", "false");
|
||||
}
|
||||
this.elt.insertBefore(this._scrollableBadge, this._customBadge);
|
||||
},
|
||||
|
@ -431,7 +435,7 @@ ElementEditor.prototype = {
|
|||
},
|
||||
|
||||
_createDisplayBadge() {
|
||||
this._displayBadge = this.doc.createElement("div");
|
||||
this._displayBadge = this.doc.createElement("button");
|
||||
this._displayBadge.className = "inspector-badge";
|
||||
this._displayBadge.addEventListener("click", this.onDisplayBadgeClick);
|
||||
// Badges order is [event][display][custom], insert display badge before custom.
|
||||
|
@ -455,6 +459,18 @@ ElementEditor.prototype = {
|
|||
(isGrid && this.highlighters.canGridHighlighterToggle(this.node));
|
||||
|
||||
this._displayBadge.classList.toggle("interactive", isInteractive);
|
||||
|
||||
// Since the badge is a <button>, if it's not interactive we need to indicate
|
||||
// to screen readers that it shouldn't behave like a button.
|
||||
// It's easier to have the badge being a button and "downgrading" it like this,
|
||||
// than having it as a div and adding interactivity.
|
||||
if (isInteractive) {
|
||||
this._displayBadge.removeAttribute("role");
|
||||
this._displayBadge.setAttribute("aria-pressed", "false");
|
||||
} else {
|
||||
this._displayBadge.setAttribute("role", "presentation");
|
||||
this._displayBadge.removeAttribute("aria-pressed");
|
||||
}
|
||||
},
|
||||
|
||||
updateOverflowBadge() {
|
||||
|
@ -496,7 +512,7 @@ ElementEditor.prototype = {
|
|||
},
|
||||
|
||||
_createCustomBadge() {
|
||||
this._customBadge = this.doc.createElement("div");
|
||||
this._customBadge = this.doc.createElement("button");
|
||||
this._customBadge.className = "inspector-badge interactive";
|
||||
this._customBadge.dataset.custom = "true";
|
||||
this._customBadge.textContent = "custom…";
|
||||
|
@ -1102,6 +1118,10 @@ ElementEditor.prototype = {
|
|||
async onScrollableBadgeClick() {
|
||||
this.highlightingOverflowCausingElements =
|
||||
this._scrollableBadge.classList.toggle("active");
|
||||
this._scrollableBadge.setAttribute(
|
||||
"aria-pressed",
|
||||
this.highlightingOverflowCausingElements
|
||||
);
|
||||
|
||||
const { nodes } = await this.node.walkerFront.getOverflowCausingElements(
|
||||
this.node
|
||||
|
|
|
@ -512,6 +512,9 @@ class Connector {
|
|||
async updateNetworkThrottling(enabled, profile) {
|
||||
if (!enabled) {
|
||||
this.networkFront.clearNetworkThrottling();
|
||||
await this.commands.targetConfigurationCommand.updateConfiguration({
|
||||
setTabOffline: false,
|
||||
});
|
||||
} else {
|
||||
// The profile can be either a profile id which is used to
|
||||
// search the predefined throttle profiles or a profile object
|
||||
|
@ -520,6 +523,11 @@ class Connector {
|
|||
profile = throttlingProfiles.find(({ id }) => id == profile);
|
||||
}
|
||||
const { download, upload, latency } = profile;
|
||||
if (!download && !upload) {
|
||||
await this.commands.targetConfigurationCommand.updateConfiguration({
|
||||
setTabOffline: !download,
|
||||
});
|
||||
}
|
||||
await this.networkFront.setNetworkThrottling({
|
||||
downloadThroughput: download,
|
||||
uploadThroughput: upload,
|
||||
|
|
|
@ -57,10 +57,12 @@ add_task(async function () {
|
|||
await waitForRequestData(store, ["eventTimings"]);
|
||||
|
||||
const requestItem = getSortedRequests(store.getState()).at(-1);
|
||||
ok(
|
||||
requestItem.eventTimings.timings.receive > 1000,
|
||||
`Request was properly throttled for profile ${profile.id}`
|
||||
);
|
||||
if (requestItem.eventTimings) {
|
||||
ok(
|
||||
requestItem.eventTimings.timings.receive > 1000,
|
||||
`Request was properly throttled for profile ${profile.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await teardown(monitor);
|
||||
|
|
|
@ -173,6 +173,7 @@ const presets = {
|
|||
"SceneBuilder",
|
||||
"WrWorker",
|
||||
"CanvasWorkers",
|
||||
"TextureUpdate",
|
||||
],
|
||||
duration: 0,
|
||||
l10nIds: {
|
||||
|
@ -220,6 +221,7 @@ const presets = {
|
|||
"Socket Thread",
|
||||
"SwComposite",
|
||||
"webrtc",
|
||||
"TextureUpdate",
|
||||
],
|
||||
duration: 0,
|
||||
l10nIds: {
|
||||
|
|
|
@ -99,6 +99,12 @@ const profiles = [
|
|||
upload: 15 * MBps,
|
||||
latency: 2,
|
||||
},
|
||||
{
|
||||
id: "Offline",
|
||||
download: 0,
|
||||
upload: 0,
|
||||
latency: 5,
|
||||
},
|
||||
].map(profile => new ThrottlingProfile(profile));
|
||||
|
||||
module.exports = profiles;
|
||||
|
|
|
@ -32,6 +32,7 @@ The table below lists the numbers associated with each network type, but please
|
|||
Regular 4G/LTE, 4 Mbps, 3 Mbps, 20
|
||||
DSL, 2 Mbps, 1 Mbps, 5
|
||||
Wi-Fi, 30 Mbps, 15 Mbps, 2
|
||||
Offline, 0 Mbps, 0 Mbps, 5
|
||||
|
||||
Network Monitor Features
|
||||
************************
|
||||
|
|
|
@ -202,6 +202,11 @@ The table below lists the numbers associated with each network type, but please
|
|||
- 15 Mb/s
|
||||
- 2
|
||||
|
||||
* - Offline
|
||||
- 0 Mb/s
|
||||
- 0 Mb/s
|
||||
- 5
|
||||
|
||||
To select a network, click the list box that's initially labeled "No throttling":
|
||||
|
||||
.. image:: rdm_throttling.png
|
||||
|
|
|
@ -47,6 +47,8 @@ const SUPPORTED_OPTIONS = {
|
|||
restoreFocus: true,
|
||||
// Enable service worker testing over HTTP (instead of HTTPS only).
|
||||
serviceWorkersTestingEnabled: true,
|
||||
// Set the current tab offline
|
||||
setTabOffline: true,
|
||||
// Enable touch events simulation
|
||||
touchEventsOverride: true,
|
||||
// Use simplified highlighters when prefers-reduced-motion is enabled.
|
||||
|
@ -266,6 +268,9 @@ class TargetConfigurationActor extends Actor {
|
|||
case "cacheDisabled":
|
||||
this._setCacheDisabled(value);
|
||||
break;
|
||||
case "setTabOffline":
|
||||
this._setTabOffline(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,6 +287,7 @@ class TargetConfigurationActor extends Actor {
|
|||
this._setServiceWorkersTestingEnabled(false);
|
||||
this._setPrintSimulationEnabled(false);
|
||||
this._setCacheDisabled(false);
|
||||
this._setTabOffline(false);
|
||||
|
||||
// Restore the color scheme simulation only if it was explicitly updated
|
||||
// by this actor. This will avoid side effects caused when destroying additional
|
||||
|
@ -453,6 +459,17 @@ class TargetConfigurationActor extends Actor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the browsing context to offline.
|
||||
*
|
||||
* @param {Boolean} offline: Whether the network throttling is set to offline
|
||||
*/
|
||||
_setTabOffline(offline) {
|
||||
if (!this._browsingContext.isDiscarded) {
|
||||
this._browsingContext.forceOffline = offline;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
Services.obs.removeObserver(
|
||||
this._onBrowsingContextAttached,
|
||||
|
|
|
@ -62,6 +62,34 @@ add_task(async function () {
|
|||
"Option colorSchemeSimulation was set, with a string value"
|
||||
);
|
||||
|
||||
await targetConfigurationCommand.updateConfiguration({
|
||||
setTabOffline: true,
|
||||
});
|
||||
compareOptions(
|
||||
targetConfigurationCommand.configuration,
|
||||
{
|
||||
cacheDisabled: false,
|
||||
colorSchemeSimulation: "dark",
|
||||
javascriptEnabled: false,
|
||||
setTabOffline: true,
|
||||
},
|
||||
"Option setTabOffline was set on"
|
||||
);
|
||||
|
||||
await targetConfigurationCommand.updateConfiguration({
|
||||
setTabOffline: false,
|
||||
});
|
||||
compareOptions(
|
||||
targetConfigurationCommand.configuration,
|
||||
{
|
||||
setTabOffline: false,
|
||||
cacheDisabled: false,
|
||||
colorSchemeSimulation: "dark",
|
||||
javascriptEnabled: false,
|
||||
},
|
||||
"Option setTabOffline was set off"
|
||||
);
|
||||
|
||||
targetCommand.destroy();
|
||||
await commands.destroy();
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ types.addDictType("target-configuration.configuration", {
|
|||
reloadOnTouchSimulationToggle: "nullable:boolean",
|
||||
restoreFocus: "nullable:boolean",
|
||||
serviceWorkersTestingEnabled: "nullable:boolean",
|
||||
setTabOffline: "nullable:boolean",
|
||||
touchEventsOverride: "nullable:string",
|
||||
});
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ categories:
|
|||
- tools/moztreedocs
|
||||
testing_doc:
|
||||
- testing/automated-testing
|
||||
- testing/sheriffed-intermittents
|
||||
- testing/tests-for-new-config
|
||||
- testing/intermittent
|
||||
- testing/testing-policy
|
||||
|
|
|
@ -3529,6 +3529,11 @@ bool BrowsingContext::CanSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>,
|
|||
return true;
|
||||
}
|
||||
|
||||
bool BrowsingContext::CanSet(FieldIndex<IDX_ForceOffline>, bool aNewValue,
|
||||
ContentParent* aSource) {
|
||||
return XRE_IsParentProcess() && !aSource;
|
||||
}
|
||||
|
||||
void BrowsingContext::DidSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>,
|
||||
bool aOldValue) {
|
||||
nsIDocShell* shell = GetDocShell();
|
||||
|
|
|
@ -267,7 +267,9 @@ struct EmbedderColorSchemes {
|
|||
* a content process. */ \
|
||||
FIELD(EmbeddedInContentDocument, bool) \
|
||||
/* If true, this browsing context is within a hidden embedded document. */ \
|
||||
FIELD(IsUnderHiddenEmbedderElement, bool)
|
||||
FIELD(IsUnderHiddenEmbedderElement, bool) \
|
||||
/* If true, this browsing context is offline */ \
|
||||
FIELD(ForceOffline, bool)
|
||||
|
||||
// BrowsingContext, in this context, is the cross process replicated
|
||||
// environment in which information about documents is stored. In
|
||||
|
@ -611,6 +613,8 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
|
|||
aRv);
|
||||
}
|
||||
|
||||
bool ForceOffline() const { return GetForceOffline(); }
|
||||
|
||||
bool ForceDesktopViewport() const { return GetForceDesktopViewport(); }
|
||||
|
||||
bool AuthorStyleDisabledDefault() const {
|
||||
|
@ -1229,6 +1233,9 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
|
|||
const bool& aIsUnderHiddenEmbedderElement,
|
||||
ContentParent* aSource);
|
||||
|
||||
bool CanSet(FieldIndex<IDX_ForceOffline>, bool aNewValue,
|
||||
ContentParent* aSource);
|
||||
|
||||
bool CanSet(FieldIndex<IDX_EmbeddedInContentDocument>, bool,
|
||||
ContentParent* aSource) {
|
||||
return CheckOnlyEmbedderCanSet(aSource);
|
||||
|
|
|
@ -320,6 +320,7 @@ void CanonicalBrowsingContext::ReplacedBy(
|
|||
txn.SetEmbedderColorSchemes(GetEmbedderColorSchemes());
|
||||
txn.SetHasRestoreData(GetHasRestoreData());
|
||||
txn.SetShouldDelayMediaFromStart(GetShouldDelayMediaFromStart());
|
||||
txn.SetForceOffline(GetForceOffline());
|
||||
|
||||
// Propagate some settings on BrowsingContext replacement so they're not lost
|
||||
// on bfcached navigations. These are important for GeckoView (see bug
|
||||
|
|
|
@ -795,6 +795,7 @@ if (AppConstants.platform == "win") {
|
|||
});
|
||||
} else {
|
||||
const homeDir = Services.dirsvc.get("Home", Ci.nsIFile).path;
|
||||
const homeBase = AppConstants.platform == "macosx" ? "/Users" : "/home";
|
||||
|
||||
testcases.push({
|
||||
input: "~",
|
||||
|
@ -806,6 +807,16 @@ if (AppConstants.platform == "win") {
|
|||
fixedURI: `file://${homeDir}/foo`,
|
||||
protocolChange: true,
|
||||
});
|
||||
testcases.push({
|
||||
input: "~foo",
|
||||
fixedURI: `file://${homeBase}/foo`,
|
||||
protocolChange: true,
|
||||
});
|
||||
testcases.push({
|
||||
input: "~foo/bar",
|
||||
fixedURI: `file://${homeBase}/foo/bar`,
|
||||
protocolChange: true,
|
||||
});
|
||||
testcases.push({
|
||||
input: "/some/file.txt",
|
||||
fixedURI: "file:///some/file.txt",
|
||||
|
|
|
@ -1131,7 +1131,8 @@ void CustomElementRegistry::Define(
|
|||
/* Cancelable */ true, detail);
|
||||
event->SetTrusted(true);
|
||||
|
||||
AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(doc, event);
|
||||
AsyncEventDispatcher* dispatcher =
|
||||
new AsyncEventDispatcher(doc, event.forget());
|
||||
dispatcher->mOnlyChromeDispatch = ChromeOnlyDispatch::eYes;
|
||||
|
||||
dispatcher->PostDOMEvent();
|
||||
|
|
|
@ -7423,8 +7423,7 @@ void Document::PostStyleSheetApplicableStateChangeEvent(StyleSheet& aSheet) {
|
|||
event->SetTrusted(true);
|
||||
event->SetTarget(this);
|
||||
RefPtr<AsyncEventDispatcher> asyncDispatcher =
|
||||
new AsyncEventDispatcher(this, event);
|
||||
asyncDispatcher->mOnlyChromeDispatch = ChromeOnlyDispatch::eYes;
|
||||
new AsyncEventDispatcher(this, event.forget(), ChromeOnlyDispatch::eYes);
|
||||
asyncDispatcher->PostDOMEvent();
|
||||
}
|
||||
|
||||
|
@ -7443,8 +7442,7 @@ void Document::PostStyleSheetRemovedEvent(StyleSheet& aSheet) {
|
|||
event->SetTrusted(true);
|
||||
event->SetTarget(this);
|
||||
RefPtr<AsyncEventDispatcher> asyncDispatcher =
|
||||
new AsyncEventDispatcher(this, event);
|
||||
asyncDispatcher->mOnlyChromeDispatch = ChromeOnlyDispatch::eYes;
|
||||
new AsyncEventDispatcher(this, event.forget(), ChromeOnlyDispatch::eYes);
|
||||
asyncDispatcher->PostDOMEvent();
|
||||
}
|
||||
|
||||
|
|
|
@ -561,7 +561,17 @@ bool Navigator::CookieEnabled() {
|
|||
return granted;
|
||||
}
|
||||
|
||||
bool Navigator::OnLine() { return !NS_IsOffline(); }
|
||||
bool Navigator::OnLine() {
|
||||
if (mWindow) {
|
||||
// Check if this tab is set to be offline.
|
||||
BrowsingContext* bc = mWindow->GetBrowsingContext();
|
||||
if (bc && bc->Top()->GetForceOffline()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Return the default browser value
|
||||
return !NS_IsOffline();
|
||||
}
|
||||
|
||||
void Navigator::GetBuildID(nsAString& aBuildID, CallerType aCallerType,
|
||||
ErrorResult& aRv) const {
|
||||
|
|
|
@ -195,6 +195,7 @@ nsFrameLoader::nsFrameLoader(Element* aOwner, BrowsingContext* aBrowsingContext,
|
|||
mIsRemoteFrame(aIsRemoteFrame),
|
||||
mWillChangeProcess(false),
|
||||
mObservingOwnerContent(false),
|
||||
mHadDetachedFrame(false),
|
||||
mTabProcessCrashFired(false) {
|
||||
nsCOMPtr<nsFrameLoaderOwner> owner = do_QueryInterface(aOwner);
|
||||
owner->AttachFrameLoader(this);
|
||||
|
@ -3085,15 +3086,15 @@ already_AddRefed<Element> nsFrameLoader::GetOwnerElement() {
|
|||
return do_AddRef(mOwnerContent);
|
||||
}
|
||||
|
||||
void nsFrameLoader::SetDetachedSubdocFrame(nsIFrame* aDetachedFrame,
|
||||
Document* aContainerDoc) {
|
||||
void nsFrameLoader::SetDetachedSubdocFrame(nsIFrame* aDetachedFrame) {
|
||||
mDetachedSubdocFrame = aDetachedFrame;
|
||||
mContainerDocWhileDetached = aContainerDoc;
|
||||
mHadDetachedFrame = !!aDetachedFrame;
|
||||
}
|
||||
|
||||
nsIFrame* nsFrameLoader::GetDetachedSubdocFrame(
|
||||
Document** aContainerDoc) const {
|
||||
NS_IF_ADDREF(*aContainerDoc = mContainerDocWhileDetached);
|
||||
nsIFrame* nsFrameLoader::GetDetachedSubdocFrame(bool* aOutIsSet) const {
|
||||
if (aOutIsSet) {
|
||||
*aOutIsSet = mHadDetachedFrame;
|
||||
}
|
||||
return mDetachedSubdocFrame.GetFrame();
|
||||
}
|
||||
|
||||
|
|
|
@ -358,19 +358,14 @@ class nsFrameLoader final : public nsStubMutationObserver,
|
|||
* destroying the nsSubDocumentFrame. If the nsSubdocumentFrame is
|
||||
* being reframed we'll restore the detached nsIFrame when it's recreated,
|
||||
* otherwise we'll discard the old presentation and set the detached
|
||||
* subdoc nsIFrame to null. aContainerDoc is the document containing the
|
||||
* the subdoc frame. This enables us to detect when the containing
|
||||
* document has changed during reframe, so we can discard the presentation
|
||||
* in that case.
|
||||
* subdoc nsIFrame to null.
|
||||
*/
|
||||
void SetDetachedSubdocFrame(nsIFrame* aDetachedFrame,
|
||||
Document* aContainerDoc);
|
||||
void SetDetachedSubdocFrame(nsIFrame* aDetachedFrame);
|
||||
|
||||
/**
|
||||
* Retrieves the detached nsIFrame and the document containing the nsIFrame,
|
||||
* as set by SetDetachedSubdocFrame().
|
||||
* Retrieves the detached nsIFrame as set by SetDetachedSubdocFrame().
|
||||
*/
|
||||
nsIFrame* GetDetachedSubdocFrame(Document** aContainerDoc) const;
|
||||
nsIFrame* GetDetachedSubdocFrame(bool* aOutIsSet = nullptr) const;
|
||||
|
||||
/**
|
||||
* Applies a new set of sandbox flags. These are merged with the sandbox
|
||||
|
@ -510,12 +505,6 @@ class nsFrameLoader final : public nsStubMutationObserver,
|
|||
// Stores the root frame of the subdocument while the subdocument is being
|
||||
// reframed. Used to restore the presentation after reframing.
|
||||
WeakFrame mDetachedSubdocFrame;
|
||||
// Stores the containing document of the frame corresponding to this
|
||||
// frame loader. This is reference is kept valid while the subframe's
|
||||
// presentation is detached and stored in mDetachedSubdocFrame. This
|
||||
// enables us to detect whether the frame has moved documents during
|
||||
// a reframe, so that we know not to restore the presentation.
|
||||
RefPtr<Document> mContainerDocWhileDetached;
|
||||
|
||||
// When performing a process switch, this value is used rather than mURIToLoad
|
||||
// to identify the process-switching load which should be resumed in the
|
||||
|
@ -559,6 +548,8 @@ class nsFrameLoader final : public nsStubMutationObserver,
|
|||
// but for a different process, after it is destroyed.
|
||||
bool mWillChangeProcess : 1;
|
||||
bool mObservingOwnerContent : 1;
|
||||
// Whether we had a (possibly dead now) mDetachedSubdocFrame.
|
||||
bool mHadDetachedFrame : 1;
|
||||
|
||||
// When an out-of-process nsFrameLoader crashes, an event is fired on the
|
||||
// frame. To ensure this is only fired once, this bit is checked.
|
||||
|
|
|
@ -2913,7 +2913,8 @@ void CanvasRenderingContext2D::UpdateFilter() {
|
|||
MOZ_RELEASE_ASSERT(!mStyleStack.IsEmpty());
|
||||
|
||||
CurrentState().filter = FilterInstance::GetFilterDescription(
|
||||
mCanvasElement, CurrentState().filterChain.AsSpan(), writeOnly,
|
||||
mCanvasElement, CurrentState().filterChain.AsSpan(),
|
||||
CurrentState().autoSVGFiltersObserver, writeOnly,
|
||||
CanvasUserSpaceMetrics(GetSize(), CurrentState().fontFont, canvasStyle,
|
||||
presContext),
|
||||
gfxRect(0, 0, mWidth, mHeight), CurrentState().filterAdditionalImages);
|
||||
|
|
|
@ -129,6 +129,12 @@ interface BrowsingContext {
|
|||
|
||||
[SetterThrows] attribute boolean isActive;
|
||||
|
||||
/**
|
||||
* When set to true all channels in this browsing context or its children will report navigator.onLine = false,
|
||||
* and HTTP requests created from these browsing context will fail with NS_ERROR_OFFLINE.
|
||||
*/
|
||||
[SetterThrows] attribute boolean forceOffline;
|
||||
|
||||
/**
|
||||
* Sets whether this is an app tab. Non-same-origin link navigations from app
|
||||
* tabs may be forced to open in new contexts, rather than in the same context.
|
||||
|
|
|
@ -46,10 +46,10 @@ var _done = new Promise((resolve) => {
|
|||
|
||||
async function addVirtualAuthenticator() {
|
||||
let id = await SpecialPowers.spawnChrome([], () => {
|
||||
let webauthnTransport = Cc["@mozilla.org/webauthn/transport;1"].getService(
|
||||
Ci.nsIWebAuthnTransport
|
||||
let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
|
||||
Ci.nsIWebAuthnService
|
||||
);
|
||||
return webauthnTransport.addVirtualAuthenticator(
|
||||
return webauthnService.addVirtualAuthenticator(
|
||||
"ctap2",
|
||||
"internal",
|
||||
true,
|
||||
|
@ -61,10 +61,10 @@ async function addVirtualAuthenticator() {
|
|||
|
||||
SimpleTest.registerCleanupFunction(async () => {
|
||||
await SpecialPowers.spawnChrome([id], (authenticatorId) => {
|
||||
let webauthnTransport = Cc["@mozilla.org/webauthn/transport;1"].getService(
|
||||
Ci.nsIWebAuthnTransport
|
||||
let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
|
||||
Ci.nsIWebAuthnService
|
||||
);
|
||||
webauthnTransport.removeVirtualAuthenticator(authenticatorId);
|
||||
webauthnService.removeVirtualAuthenticator(authenticatorId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ void AsyncEventDispatcher::RunDOMEventWhenSafe(
|
|||
Composed::eDefault);
|
||||
return;
|
||||
}
|
||||
(new AsyncEventDispatcher(&aTarget, &aEvent, aOnlyChromeDispatch))
|
||||
(new AsyncEventDispatcher(&aTarget, do_AddRef(&aEvent), aOnlyChromeDispatch))
|
||||
->RunDOMEventWhenSafe();
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ class AsyncEventDispatcher : public CancelableRunnable {
|
|||
* destroyed).
|
||||
*/
|
||||
AsyncEventDispatcher(
|
||||
dom::EventTarget* aTarget, dom::Event* aEvent,
|
||||
dom::EventTarget* aTarget, already_AddRefed<dom::Event> aEvent,
|
||||
ChromeOnlyDispatch aOnlyChromeDispatch = ChromeOnlyDispatch::eNo)
|
||||
: CancelableRunnable("AsyncEventDispatcher"),
|
||||
mTarget(aTarget),
|
||||
|
@ -92,7 +92,7 @@ class AsyncEventDispatcher : public CancelableRunnable {
|
|||
mEventMessage(eUnidentifiedEvent),
|
||||
mOnlyChromeDispatch(aOnlyChromeDispatch) {
|
||||
MOZ_ASSERT(
|
||||
aEvent->IsSafeToBeDispatchedAsynchronously(),
|
||||
mEvent->IsSafeToBeDispatchedAsynchronously(),
|
||||
"The DOM event should be created without Widget*Event and "
|
||||
"Internal*Event "
|
||||
"because if it needs to be safe to be dispatched asynchronously");
|
||||
|
@ -192,8 +192,9 @@ class LoadBlockingAsyncEventDispatcher final : public AsyncEventDispatcher {
|
|||
mBlockedDoc->BlockOnload();
|
||||
}
|
||||
|
||||
LoadBlockingAsyncEventDispatcher(nsINode* aEventNode, dom::Event* aEvent)
|
||||
: AsyncEventDispatcher(aEventNode, aEvent),
|
||||
LoadBlockingAsyncEventDispatcher(nsINode* aEventNode,
|
||||
already_AddRefed<dom::Event> aEvent)
|
||||
: AsyncEventDispatcher(aEventNode, std::move(aEvent)),
|
||||
mBlockedDoc(aEventNode->OwnerDoc()) {
|
||||
mBlockedDoc->BlockOnload();
|
||||
}
|
||||
|
|
|
@ -7115,7 +7115,7 @@ void HTMLMediaElement::DispatchEncrypted(const nsTArray<uint8_t>& aInitData,
|
|||
}
|
||||
|
||||
RefPtr<AsyncEventDispatcher> asyncDispatcher =
|
||||
new AsyncEventDispatcher(this, event);
|
||||
new AsyncEventDispatcher(this, event.forget());
|
||||
asyncDispatcher->PostDOMEvent();
|
||||
}
|
||||
|
||||
|
|
|
@ -135,7 +135,7 @@ void MediaTrackList::CreateAndDispatchTrackEventRunner(
|
|||
TrackEvent::Constructor(this, aEventName, eventInit);
|
||||
|
||||
RefPtr<AsyncEventDispatcher> asyncDispatcher =
|
||||
new AsyncEventDispatcher(this, event);
|
||||
new AsyncEventDispatcher(this, event.forget());
|
||||
asyncDispatcher->PostDOMEvent();
|
||||
}
|
||||
|
||||
|
|
|
@ -549,7 +549,7 @@ void MediaKeySession::DispatchKeyMessage(MediaKeyMessageType aMessageType,
|
|||
RefPtr<MediaKeyMessageEvent> event(
|
||||
MediaKeyMessageEvent::Constructor(this, aMessageType, aMessage));
|
||||
RefPtr<AsyncEventDispatcher> asyncDispatcher =
|
||||
new AsyncEventDispatcher(this, event);
|
||||
new AsyncEventDispatcher(this, event.forget());
|
||||
asyncDispatcher->PostDOMEvent();
|
||||
}
|
||||
|
||||
|
@ -557,9 +557,9 @@ void MediaKeySession::DispatchKeyError(uint32_t aSystemCode) {
|
|||
EME_LOG("MediaKeySession[%p,'%s'] DispatchKeyError() systemCode=%u.", this,
|
||||
NS_ConvertUTF16toUTF8(mSessionId).get(), aSystemCode);
|
||||
|
||||
RefPtr<MediaKeyError> event(new MediaKeyError(this, aSystemCode));
|
||||
auto event = MakeRefPtr<MediaKeyError>(this, aSystemCode);
|
||||
RefPtr<AsyncEventDispatcher> asyncDispatcher =
|
||||
new AsyncEventDispatcher(this, event);
|
||||
new AsyncEventDispatcher(this, event.forget());
|
||||
asyncDispatcher->PostDOMEvent();
|
||||
}
|
||||
|
||||
|
|
|
@ -500,7 +500,7 @@ void MediaController::HandlePositionStateChanged(const PositionState& aState) {
|
|||
init.mPosition = aState.mLastReportedPlaybackPosition;
|
||||
RefPtr<PositionStateEvent> event =
|
||||
PositionStateEvent::Constructor(this, u"positionstatechange"_ns, init);
|
||||
DispatchAsyncEvent(event);
|
||||
DispatchAsyncEvent(event.forget());
|
||||
}
|
||||
|
||||
void MediaController::HandleMetadataChanged(
|
||||
|
@ -522,13 +522,14 @@ void MediaController::DispatchAsyncEvent(const nsAString& aName) {
|
|||
RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
|
||||
event->InitEvent(aName, false, false);
|
||||
event->SetTrusted(true);
|
||||
DispatchAsyncEvent(event);
|
||||
DispatchAsyncEvent(event.forget());
|
||||
}
|
||||
|
||||
void MediaController::DispatchAsyncEvent(Event* aEvent) {
|
||||
MOZ_ASSERT(aEvent);
|
||||
void MediaController::DispatchAsyncEvent(already_AddRefed<Event> aEvent) {
|
||||
RefPtr<Event> event = aEvent;
|
||||
MOZ_ASSERT(event);
|
||||
nsAutoString eventType;
|
||||
aEvent->GetType(eventType);
|
||||
event->GetType(eventType);
|
||||
if (!mIsActive && !eventType.EqualsLiteral("deactivated")) {
|
||||
LOG("Only 'deactivated' can be dispatched on a deactivated controller, not "
|
||||
"'%s'",
|
||||
|
@ -537,7 +538,7 @@ void MediaController::DispatchAsyncEvent(Event* aEvent) {
|
|||
}
|
||||
LOG("Dispatch event %s", NS_ConvertUTF16toUTF8(eventType).get());
|
||||
RefPtr<AsyncEventDispatcher> asyncDispatcher =
|
||||
new AsyncEventDispatcher(this, aEvent);
|
||||
new AsyncEventDispatcher(this, event.forget());
|
||||
asyncDispatcher->PostDOMEvent();
|
||||
}
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ class MediaController final : public DOMEventTargetHelper,
|
|||
void UpdateDeactivationTimerIfNeeded();
|
||||
|
||||
void DispatchAsyncEvent(const nsAString& aName);
|
||||
void DispatchAsyncEvent(Event* aEvent);
|
||||
void DispatchAsyncEvent(already_AddRefed<Event> aEvent);
|
||||
|
||||
bool IsMainController() const;
|
||||
void ForceToBecomeMainControllerIfNeeded();
|
||||
|
|
|
@ -25,15 +25,36 @@ static void FillLinearRamp(double aBufferStartTime, Span<float> aBuffer,
|
|||
|
||||
static void FillExponentialRamp(double aBufferStartTime, Span<float> aBuffer,
|
||||
double t0, float v0, double t1, float v1) {
|
||||
float ratio = v1 / v0;
|
||||
if (v0 == 0.f || ratio < 0.f) {
|
||||
MOZ_ASSERT(aBuffer.Length() >= 1);
|
||||
double fullRatio = static_cast<double>(v1) / v0;
|
||||
if (v0 == 0.f || fullRatio < 0.0) {
|
||||
std::fill_n(aBuffer.Elements(), aBuffer.Length(), v0);
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < aBuffer.Length(); ++i) {
|
||||
double exponent =
|
||||
(aBufferStartTime - t0 + static_cast<double>(i)) / (t1 - t0);
|
||||
aBuffer[i] = v0 * fdlibm_powf(v1 / v0, static_cast<float>(exponent));
|
||||
|
||||
double tDelta = t1 - t0;
|
||||
// Calculate the value for the first tick from the curve initial value.
|
||||
// v(t) = v0 * (v1/v0)^((t-t0)/(t1-t0))
|
||||
double exponent = (aBufferStartTime - t0) / tDelta;
|
||||
// The power function can amplify rounding error in the exponent by
|
||||
// ((t−t0)/(t1−t0)) ln (v1/v0). The single precision exponent argument for
|
||||
// powf() would be sufficient when max(v1/v0,v0/v1) <= e, where e is Euler's
|
||||
// number, but fdlibm's single precision powf() is not expected to provide
|
||||
// speed advantages over double precision pow().
|
||||
double v = v0 * fdlibm_pow(fullRatio, exponent);
|
||||
aBuffer[0] = static_cast<float>(v);
|
||||
if (aBuffer.Length() == 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the inter-tick ratio to calculate values at other ticks.
|
||||
// v(t+1) = (v1/v0)^(1/(t1-t0)) * v(t)
|
||||
// Double precision is used so that accumulation of rounding error is not
|
||||
// significant.
|
||||
double tickRatio = fdlibm_pow(fullRatio, 1.0 / tDelta);
|
||||
for (size_t i = 1; i < aBuffer.Length(); ++i) {
|
||||
v *= tickRatio;
|
||||
aBuffer[i] = static_cast<float>(v);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,13 +53,6 @@ inline float ConvertDecibelsToLinear(float aDecibels) {
|
|||
return fdlibm_powf(10.0f, 0.05f * aDecibels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a decibel to a linear value.
|
||||
*/
|
||||
inline float ConvertDecibelToLinear(float aDecibel) {
|
||||
return fdlibm_powf(10.0f, 0.05f * aDecibel);
|
||||
}
|
||||
|
||||
inline void FixNaN(double& aDouble) {
|
||||
if (std::isnan(aDouble) || std::isinf(aDouble)) {
|
||||
aDouble = 0.0;
|
||||
|
|
|
@ -507,10 +507,16 @@ int32_t WebrtcGmpVideoEncoder::SetRates_g(RefPtr<WebrtcGmpVideoEncoder> aThis,
|
|||
void WebrtcGmpVideoEncoder::Terminated() {
|
||||
GMP_LOG_DEBUG("GMP Encoder Terminated: %p", (void*)this);
|
||||
|
||||
mGMP->Close();
|
||||
GMPVideoEncoderProxy* gmp(mGMP);
|
||||
mGMP = nullptr;
|
||||
mHost = nullptr;
|
||||
mInitting = false;
|
||||
|
||||
if (gmp) {
|
||||
// Do this last, since this could cause us to be destroyed
|
||||
gmp->Close();
|
||||
}
|
||||
|
||||
// Could now notify that it's dead
|
||||
}
|
||||
|
||||
|
@ -947,10 +953,16 @@ int32_t WebrtcGmpVideoDecoder::ReleaseGmp() {
|
|||
void WebrtcGmpVideoDecoder::Terminated() {
|
||||
GMP_LOG_DEBUG("GMP Decoder Terminated: %p", (void*)this);
|
||||
|
||||
mGMP->Close();
|
||||
GMPVideoDecoderProxy* gmp(mGMP);
|
||||
mGMP = nullptr;
|
||||
mHost = nullptr;
|
||||
mInitting = false;
|
||||
|
||||
if (gmp) {
|
||||
// Do this last, since this could cause us to be destroyed
|
||||
gmp->Close();
|
||||
}
|
||||
|
||||
// Could now notify that it's dead
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import requests
|
|||
|
||||
THIRDPARTY_USED_IN_FIREFOX = [
|
||||
"abseil-cpp",
|
||||
"google_benchmark",
|
||||
"pffft",
|
||||
"rnnoise",
|
||||
]
|
||||
|
|
|
@ -603,7 +603,7 @@ nsresult UDPSocket::DispatchReceivedData(const nsACString& aRemoteAddress,
|
|||
udpEvent->SetTrusted(true);
|
||||
|
||||
RefPtr<AsyncEventDispatcher> asyncDispatcher =
|
||||
new AsyncEventDispatcher(this, udpEvent);
|
||||
new AsyncEventDispatcher(this, udpEvent.forget());
|
||||
|
||||
return asyncDispatcher->PostDOMEvent();
|
||||
}
|
||||
|
|
|
@ -427,13 +427,19 @@ void RequestResolver::ResolveOrReject() {
|
|||
} else {
|
||||
MOZ_ASSERT(mProxy);
|
||||
|
||||
promise = mProxy->WorkerPromise();
|
||||
// The worker ref might have been notified already before we are run.
|
||||
MutexAutoLock lock(mProxy->Lock());
|
||||
if (!mProxy->CleanedUp()) {
|
||||
promise = mProxy->WorkerPromise();
|
||||
|
||||
// Only clean up for worker case.
|
||||
autoCleanup.emplace(mProxy);
|
||||
// Only clean up for worker case.
|
||||
autoCleanup.emplace(mProxy);
|
||||
}
|
||||
}
|
||||
|
||||
MOZ_ASSERT(promise);
|
||||
if (!promise) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mType == Type::Estimate) {
|
||||
if (NS_SUCCEEDED(mResultCode)) {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
#include "mozilla/dom/BindingDeclarations.h"
|
||||
#include "mozilla/dom/WebAuthenticationBinding.h"
|
||||
#include "nsCycleCollectionParticipant.h"
|
||||
#include "nsIWebAuthnController.h"
|
||||
#include "nsIWebAuthnAttObj.h"
|
||||
#include "nsWrapperCache.h"
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
#include "nsTArray.h"
|
||||
|
||||
class nsIWebAuthnAttObj;
|
||||
class nsIWebAuthnTransport;
|
||||
class nsIWebAuthnService;
|
||||
|
||||
extern "C" {
|
||||
|
||||
nsresult authrs_transport_constructor(nsIWebAuthnTransport** result);
|
||||
nsresult authrs_service_constructor(nsIWebAuthnService** result);
|
||||
|
||||
nsresult authrs_webauthn_att_obj_constructor(
|
||||
const nsTArray<uint8_t>& attestation, bool anonymize,
|
||||
|
|
|
@ -2,20 +2,20 @@
|
|||
* 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/. */
|
||||
|
||||
#include "AuthrsTransport.h"
|
||||
#include "AuthrsService.h"
|
||||
#include "AuthrsBridge_ffi.h"
|
||||
#include "nsIWebAuthnController.h"
|
||||
#include "nsIWebAuthnService.h"
|
||||
#include "nsCOMPtr.h"
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
already_AddRefed<nsIWebAuthnTransport> NewAuthrsTransport() {
|
||||
nsCOMPtr<nsIWebAuthnTransport> transport;
|
||||
nsresult rv = authrs_transport_constructor(getter_AddRefs(transport));
|
||||
already_AddRefed<nsIWebAuthnService> NewAuthrsService() {
|
||||
nsCOMPtr<nsIWebAuthnService> webauthnService;
|
||||
nsresult rv = authrs_service_constructor(getter_AddRefs(webauthnService));
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return nullptr;
|
||||
}
|
||||
return transport.forget();
|
||||
return webauthnService.forget();
|
||||
}
|
||||
|
||||
} // namespace mozilla::dom
|
|
@ -6,11 +6,11 @@
|
|||
#define DOM_WEBAUTHN_AUTHRS_BRIDGE_H_
|
||||
|
||||
#include "mozilla/AlreadyAddRefed.h"
|
||||
#include "nsIWebAuthnController.h"
|
||||
#include "nsIWebAuthnService.h"
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
already_AddRefed<nsIWebAuthnTransport> NewAuthrsTransport();
|
||||
already_AddRefed<nsIWebAuthnService> NewAuthrsService();
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
#include "mozilla/dom/WebAuthnTransactionChild.h"
|
||||
#include "mozilla/ipc/BackgroundParent.h"
|
||||
#include "nsIWebAuthnController.h"
|
||||
#include "nsIWebAuthnArgs.h"
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
|
|
|
@ -1,436 +0,0 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
#include "json/json.h"
|
||||
#include "mozilla/dom/WebAuthnController.h"
|
||||
#include "mozilla/dom/PWebAuthnTransactionParent.h"
|
||||
#include "mozilla/dom/WebAuthnUtil.h"
|
||||
#include "mozilla/ipc/BackgroundParent.h"
|
||||
#include "mozilla/ClearOnShutdown.h"
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/StaticPrefs_security.h"
|
||||
#include "mozilla/Unused.h"
|
||||
#include "nsEscape.h"
|
||||
#include "nsIObserver.h"
|
||||
#include "nsIObserverService.h"
|
||||
#include "nsIThread.h"
|
||||
#include "nsServiceManagerUtils.h"
|
||||
#include "nsTextFormatter.h"
|
||||
#include "mozilla/Telemetry.h"
|
||||
|
||||
#include "AuthrsTransport.h"
|
||||
#include "CtapArgs.h"
|
||||
#include "WebAuthnEnumStrings.h"
|
||||
|
||||
#ifdef MOZ_WIDGET_ANDROID
|
||||
# include "mozilla/dom/AndroidWebAuthnTokenManager.h"
|
||||
#endif
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
/***********************************************************************
|
||||
* Statics
|
||||
**********************************************************************/
|
||||
|
||||
namespace {
|
||||
static mozilla::LazyLogModule gWebAuthnControllerLog("webauthncontroller");
|
||||
StaticRefPtr<WebAuthnController> gWebAuthnController;
|
||||
static nsIThread* gWebAuthnBackgroundThread;
|
||||
} // namespace
|
||||
|
||||
/***********************************************************************
|
||||
* WebAuthnController Implementation
|
||||
**********************************************************************/
|
||||
|
||||
NS_IMPL_ISUPPORTS(WebAuthnController, nsIWebAuthnController);
|
||||
|
||||
WebAuthnController::WebAuthnController() : mTransactionParent(nullptr) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
// Create on the main thread to make sure ClearOnShutdown() works.
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
}
|
||||
|
||||
// static
|
||||
void WebAuthnController::Initialize() {
|
||||
if (!XRE_IsParentProcess()) {
|
||||
return;
|
||||
}
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!gWebAuthnController);
|
||||
gWebAuthnController = new WebAuthnController();
|
||||
ClearOnShutdown(&gWebAuthnController);
|
||||
}
|
||||
|
||||
// static
|
||||
WebAuthnController* WebAuthnController::Get() {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
return gWebAuthnController;
|
||||
}
|
||||
|
||||
void WebAuthnController::AbortTransaction(
|
||||
const nsresult& aError = NS_ERROR_DOM_NOT_ALLOWED_ERR) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::AbortTransaction"));
|
||||
if (mTransactionParent && mTransactionId.isSome()) {
|
||||
Unused << mTransactionParent->SendAbort(mTransactionId.ref(), aError);
|
||||
}
|
||||
ClearTransaction();
|
||||
}
|
||||
|
||||
void WebAuthnController::MaybeClearTransaction(
|
||||
PWebAuthnTransactionParent* aParent) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::MaybeClearTransaction"));
|
||||
// Only clear if we've been requested to do so by our current transaction
|
||||
// parent.
|
||||
if (mTransactionParent == aParent) {
|
||||
ClearTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
void WebAuthnController::ClearTransaction() {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::ClearTransaction"));
|
||||
mTransactionParent = nullptr;
|
||||
mPendingClientData.reset();
|
||||
mTransactionId.reset();
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIWebAuthnTransport> WebAuthnController::GetTransportImpl() {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
|
||||
if (mTransportImpl) {
|
||||
return mTransportImpl;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIWebAuthnTransport> transport(
|
||||
do_GetService("@mozilla.org/webauthn/transport;1"));
|
||||
transport->SetController(this);
|
||||
return transport;
|
||||
}
|
||||
|
||||
void WebAuthnController::Register(
|
||||
PWebAuthnTransactionParent* aTransactionParent,
|
||||
const uint64_t& aTransactionId, const WebAuthnMakeCredentialInfo& aInfo) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::Register"));
|
||||
|
||||
if (!gWebAuthnBackgroundThread) {
|
||||
gWebAuthnBackgroundThread = NS_GetCurrentThread();
|
||||
MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!");
|
||||
}
|
||||
|
||||
// Abort ongoing transaction, if any.
|
||||
AbortTransaction(NS_ERROR_DOM_ABORT_ERR);
|
||||
|
||||
mTransportImpl = GetTransportImpl();
|
||||
if (!mTransportImpl) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
nsresult rv = mTransportImpl->Reset();
|
||||
if (NS_FAILED(rv)) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(aTransactionId > 0);
|
||||
mTransactionParent = aTransactionParent;
|
||||
mTransactionId = Some(aTransactionId);
|
||||
mPendingClientData = Some(aInfo.ClientDataJSON());
|
||||
|
||||
RefPtr<CtapRegisterArgs> args(new CtapRegisterArgs(aInfo));
|
||||
rv = mTransportImpl->MakeCredential(mTransactionId.ref(),
|
||||
aInfo.BrowsingContextId(), args);
|
||||
if (NS_FAILED(rv)) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnController::FinishRegister(uint64_t aTransactionId,
|
||||
nsICtapRegisterResult* aResult) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::FinishRegister"));
|
||||
nsCOMPtr<nsIRunnable> r(
|
||||
NewRunnableMethod<uint64_t, RefPtr<nsICtapRegisterResult>>(
|
||||
"WebAuthnController::RunFinishRegister", this,
|
||||
&WebAuthnController::RunFinishRegister, aTransactionId, aResult));
|
||||
|
||||
if (!gWebAuthnBackgroundThread) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
|
||||
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
|
||||
}
|
||||
return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
|
||||
}
|
||||
|
||||
void WebAuthnController::RunFinishRegister(
|
||||
uint64_t aTransactionId, const RefPtr<nsICtapRegisterResult>& aResult) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::RunFinishRegister"));
|
||||
if (mTransactionId.isNothing() || mPendingClientData.isNothing() ||
|
||||
aTransactionId != mTransactionId.ref()) {
|
||||
// The previous transaction was likely cancelled from the prompt.
|
||||
return;
|
||||
}
|
||||
|
||||
nsresult status;
|
||||
nsresult rv = aResult->GetStatus(&status);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
if (NS_FAILED(status)) {
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPRegisterAbort"_ns, 1);
|
||||
AbortTransaction(status);
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> attObj;
|
||||
rv = aResult->GetAttestationObject(attObj);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> credentialId;
|
||||
rv = aResult->GetCredentialId(credentialId);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<nsString> transports;
|
||||
rv = aResult->GetTransports(transports);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<WebAuthnExtensionResult> extensions;
|
||||
bool credPropsRk;
|
||||
rv = aResult->GetCredPropsRk(&credPropsRk);
|
||||
if (rv != NS_ERROR_NOT_AVAILABLE) {
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
extensions.AppendElement(WebAuthnExtensionResultCredProps(credPropsRk));
|
||||
}
|
||||
|
||||
WebAuthnMakeCredentialResult result(mPendingClientData.extract(), attObj,
|
||||
credentialId, transports, extensions);
|
||||
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPRegisterFinish"_ns, 1);
|
||||
Unused << mTransactionParent->SendConfirmRegister(aTransactionId, result);
|
||||
ClearTransaction();
|
||||
}
|
||||
|
||||
void WebAuthnController::Sign(PWebAuthnTransactionParent* aTransactionParent,
|
||||
const uint64_t& aTransactionId,
|
||||
const WebAuthnGetAssertionInfo& aInfo) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::Sign"));
|
||||
MOZ_ASSERT(aTransactionId > 0);
|
||||
|
||||
if (!gWebAuthnBackgroundThread) {
|
||||
gWebAuthnBackgroundThread = NS_GetCurrentThread();
|
||||
MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!");
|
||||
}
|
||||
|
||||
// Abort ongoing transaction, if any.
|
||||
AbortTransaction(NS_ERROR_DOM_ABORT_ERR);
|
||||
|
||||
mTransportImpl = GetTransportImpl();
|
||||
if (!mTransportImpl) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
nsresult rv = mTransportImpl->Reset();
|
||||
if (NS_FAILED(rv)) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
mTransactionParent = aTransactionParent;
|
||||
mTransactionId = Some(aTransactionId);
|
||||
mPendingClientData = Some(aInfo.ClientDataJSON());
|
||||
|
||||
RefPtr<CtapSignArgs> args(new CtapSignArgs(aInfo));
|
||||
rv = mTransportImpl->GetAssertion(mTransactionId.ref(),
|
||||
aInfo.BrowsingContextId(), args.get());
|
||||
if (NS_FAILED(rv)) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnController::FinishSign(uint64_t aTransactionId,
|
||||
nsICtapSignResult* aResult) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::FinishSign"));
|
||||
nsCOMPtr<nsIRunnable> r(
|
||||
NewRunnableMethod<uint64_t, RefPtr<nsICtapSignResult>>(
|
||||
"WebAuthnController::RunFinishSign", this,
|
||||
&WebAuthnController::RunFinishSign, aTransactionId, aResult));
|
||||
|
||||
if (!gWebAuthnBackgroundThread) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
|
||||
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
|
||||
}
|
||||
return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
|
||||
}
|
||||
|
||||
void WebAuthnController::RunFinishSign(
|
||||
uint64_t aTransactionId, const RefPtr<nsICtapSignResult>& aResult) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::RunFinishSign"));
|
||||
if (mTransactionId.isNothing() || mPendingClientData.isNothing() ||
|
||||
aTransactionId != mTransactionId.ref()) {
|
||||
return;
|
||||
}
|
||||
|
||||
nsresult status;
|
||||
nsresult rv = aResult->GetStatus(&status);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
if (NS_FAILED(status)) {
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPSignAbort"_ns, 1);
|
||||
AbortTransaction(status);
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> credentialId;
|
||||
rv = aResult->GetCredentialId(credentialId);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> signature;
|
||||
rv = aResult->GetSignature(signature);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> authenticatorData;
|
||||
rv = aResult->GetAuthenticatorData(authenticatorData);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> userHandle;
|
||||
Unused << aResult->GetUserHandle(userHandle); // optional
|
||||
|
||||
nsTArray<WebAuthnExtensionResult> extensions;
|
||||
bool usedAppId;
|
||||
rv = aResult->GetUsedAppId(&usedAppId);
|
||||
if (rv != NS_ERROR_NOT_AVAILABLE) {
|
||||
if (NS_FAILED(rv)) {
|
||||
AbortTransaction();
|
||||
return;
|
||||
}
|
||||
extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId));
|
||||
}
|
||||
|
||||
WebAuthnGetAssertionResult result(mPendingClientData.extract(), credentialId,
|
||||
signature, authenticatorData, extensions,
|
||||
userHandle);
|
||||
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPSignFinish"_ns, 1);
|
||||
Unused << mTransactionParent->SendConfirmSign(aTransactionId, result);
|
||||
ClearTransaction();
|
||||
}
|
||||
|
||||
void WebAuthnController::Cancel(PWebAuthnTransactionParent* aTransactionParent,
|
||||
const Tainted<uint64_t>& aTransactionId) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::Cancel (IPC)"));
|
||||
// The last transaction ID also suffers from the issue described in Bug
|
||||
// 1696159. A content process could cancel another content processes
|
||||
// transaction by guessing the last transaction ID.
|
||||
if (mTransactionParent != aTransactionParent || mTransactionId.isNothing() ||
|
||||
!MOZ_IS_VALID(aTransactionId, mTransactionId.ref() == aTransactionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mTransportImpl) {
|
||||
mTransportImpl->Reset();
|
||||
}
|
||||
|
||||
ClearTransaction();
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnController::Cancel(uint64_t aTransactionId) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::Cancel (XPCOM)"));
|
||||
|
||||
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t>(
|
||||
"WebAuthnController::Cancel", this, &WebAuthnController::RunCancel,
|
||||
aTransactionId));
|
||||
|
||||
if (!gWebAuthnBackgroundThread) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
|
||||
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
|
||||
}
|
||||
return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
|
||||
}
|
||||
|
||||
void WebAuthnController::RunCancel(uint64_t aTransactionId) {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
|
||||
("WebAuthnController::RunCancel (XPCOM)"));
|
||||
|
||||
if (mTransactionId.isNothing() || mTransactionId.ref() != aTransactionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
AbortTransaction();
|
||||
}
|
||||
|
||||
} // namespace mozilla::dom
|
|
@ -1,83 +0,0 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
#ifndef mozilla_dom_WebAuthnController_h
|
||||
#define mozilla_dom_WebAuthnController_h
|
||||
|
||||
#include "nsIWebAuthnController.h"
|
||||
#include "mozilla/dom/PWebAuthnTransaction.h"
|
||||
#include "mozilla/Tainting.h"
|
||||
|
||||
/*
|
||||
* Parent process manager for WebAuthn API transactions. Handles process
|
||||
* transactions from all content processes, make sure only one transaction is
|
||||
* live at any time. Manages access to hardware and software based key systems.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
class WebAuthnController final : public nsIWebAuthnController {
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIWEBAUTHNCONTROLLER
|
||||
|
||||
// Main thread only
|
||||
static void Initialize();
|
||||
|
||||
// IPDL Background thread only
|
||||
static WebAuthnController* Get();
|
||||
|
||||
// IPDL Background thread only
|
||||
void Register(PWebAuthnTransactionParent* aTransactionParent,
|
||||
const uint64_t& aTransactionId,
|
||||
const WebAuthnMakeCredentialInfo& aInfo);
|
||||
|
||||
// IPDL Background thread only
|
||||
void Sign(PWebAuthnTransactionParent* aTransactionParent,
|
||||
const uint64_t& aTransactionId,
|
||||
const WebAuthnGetAssertionInfo& aInfo);
|
||||
|
||||
// IPDL Background thread only
|
||||
void Cancel(PWebAuthnTransactionParent* aTransactionParent,
|
||||
const Tainted<uint64_t>& aTransactionId);
|
||||
|
||||
// IPDL Background thread only
|
||||
void MaybeClearTransaction(PWebAuthnTransactionParent* aParent);
|
||||
|
||||
private:
|
||||
WebAuthnController();
|
||||
~WebAuthnController() = default;
|
||||
|
||||
// All of the private functions and members are to be
|
||||
// accessed on the IPDL background thread only.
|
||||
|
||||
nsCOMPtr<nsIWebAuthnTransport> GetTransportImpl();
|
||||
nsCOMPtr<nsIWebAuthnTransport> mTransportImpl;
|
||||
|
||||
void AbortTransaction(const nsresult& aError);
|
||||
void ClearTransaction();
|
||||
void RunCancel(uint64_t aTransactionId);
|
||||
void RunFinishRegister(uint64_t aTransactionId,
|
||||
const RefPtr<nsICtapRegisterResult>& aResult);
|
||||
void RunFinishSign(uint64_t aTransactionId,
|
||||
const RefPtr<nsICtapSignResult>& aResult);
|
||||
|
||||
// Using a raw pointer here, as the lifetime of the IPC object is managed by
|
||||
// the PBackground protocol code. This means we cannot be left holding an
|
||||
// invalid IPC protocol object after the transaction is finished.
|
||||
PWebAuthnTransactionParent* mTransactionParent;
|
||||
|
||||
// The current transaction ID.
|
||||
Maybe<uint64_t> mTransactionId;
|
||||
|
||||
// Client data associated with mTransactionId.
|
||||
Maybe<nsCString> mPendingClientData;
|
||||
};
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
||||
#endif // mozilla_dom_U2FTokenManager_h
|
88
dom/webauthn/WebAuthnPromiseHolder.cpp
Normal file
88
dom/webauthn/WebAuthnPromiseHolder.cpp
Normal file
|
@ -0,0 +1,88 @@
|
|||
/* 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/. */
|
||||
|
||||
#include "mozilla/AppShutdown.h"
|
||||
#include "WebAuthnPromiseHolder.h"
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
NS_IMPL_ISUPPORTS(WebAuthnRegisterPromiseHolder, nsIWebAuthnRegisterPromise);
|
||||
|
||||
already_AddRefed<WebAuthnRegisterPromise>
|
||||
WebAuthnRegisterPromiseHolder::Ensure() {
|
||||
return mRegisterPromise.Ensure(__func__);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnRegisterPromiseHolder::Resolve(nsICtapRegisterResult* aResult) {
|
||||
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
|
||||
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
|
||||
}
|
||||
|
||||
// Resolve the promise on its owning thread if Disconnect() has not been
|
||||
// called.
|
||||
RefPtr<nsICtapRegisterResult> result(aResult);
|
||||
mEventTarget->Dispatch(NS_NewRunnableFunction(
|
||||
"WebAuthnRegisterPromiseHolder::Resolve",
|
||||
[self = RefPtr{this}, result]() {
|
||||
self->mRegisterPromise.ResolveIfExists(result, __func__);
|
||||
}));
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnRegisterPromiseHolder::Reject(nsresult aResult) {
|
||||
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
|
||||
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
|
||||
}
|
||||
|
||||
// Reject the promise on its owning thread if Disconnect() has not been
|
||||
// called.
|
||||
mEventTarget->Dispatch(NS_NewRunnableFunction(
|
||||
"WebAuthnRegisterPromiseHolder::Reject",
|
||||
[self = RefPtr{this}, aResult]() {
|
||||
self->mRegisterPromise.RejectIfExists(aResult, __func__);
|
||||
}));
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(WebAuthnSignPromiseHolder, nsIWebAuthnSignPromise);
|
||||
|
||||
already_AddRefed<WebAuthnSignPromise> WebAuthnSignPromiseHolder::Ensure() {
|
||||
return mSignPromise.Ensure(__func__);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnSignPromiseHolder::Resolve(nsICtapSignResult* aResult) {
|
||||
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
|
||||
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
|
||||
}
|
||||
|
||||
// Resolve the promise on its owning thread if Disconnect() has not been
|
||||
// called.
|
||||
RefPtr<nsICtapSignResult> result(aResult);
|
||||
mEventTarget->Dispatch(NS_NewRunnableFunction(
|
||||
"WebAuthnSignPromiseHolder::Resolve", [self = RefPtr{this}, result]() {
|
||||
self->mSignPromise.ResolveIfExists(result, __func__);
|
||||
}));
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnSignPromiseHolder::Reject(nsresult aResult) {
|
||||
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
|
||||
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
|
||||
}
|
||||
|
||||
// Reject the promise on its owning thread if Disconnect() has not been
|
||||
// called.
|
||||
mEventTarget->Dispatch(NS_NewRunnableFunction(
|
||||
"WebAuthnSignPromiseHolder::Reject", [self = RefPtr{this}, aResult]() {
|
||||
self->mSignPromise.RejectIfExists(aResult, __func__);
|
||||
}));
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
} // namespace mozilla::dom
|
69
dom/webauthn/WebAuthnPromiseHolder.h
Normal file
69
dom/webauthn/WebAuthnPromiseHolder.h
Normal file
|
@ -0,0 +1,69 @@
|
|||
/* 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/. */
|
||||
|
||||
#ifndef mozilla_dom_WebAuthnPromiseHolder_h
|
||||
#define mozilla_dom_WebAuthnPromiseHolder_h
|
||||
|
||||
#include "mozilla/MozPromise.h"
|
||||
#include "nsIWebAuthnResult.h"
|
||||
#include "nsIWebAuthnPromise.h"
|
||||
#include "nsIThread.h"
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
/*
|
||||
* WebAuthnRegisterPromiseHolder and WebAuthnSignPromiseHolder wrap a
|
||||
* MozPromiseHolder with an XPCOM interface that allows the contained promise
|
||||
* to be resolved, rejected, or disconnected safely from any thread.
|
||||
*
|
||||
* Calls to Resolve(), Reject(), and Disconnect() are dispatched to the serial
|
||||
* event target with wich the promise holder was initialized. At most one of
|
||||
* these calls will be processed; the first call to reach the event target
|
||||
* wins. Once the promise is initialized with Ensure() the program MUST call
|
||||
* at least one of Resolve(), Reject(), or Disconnect().
|
||||
*/
|
||||
|
||||
typedef MozPromise<RefPtr<nsICtapRegisterResult>, nsresult, true>
|
||||
WebAuthnRegisterPromise;
|
||||
|
||||
typedef MozPromise<RefPtr<nsICtapSignResult>, nsresult, true>
|
||||
WebAuthnSignPromise;
|
||||
|
||||
class WebAuthnRegisterPromiseHolder final : public nsIWebAuthnRegisterPromise {
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIWEBAUTHNREGISTERPROMISE
|
||||
|
||||
explicit WebAuthnRegisterPromiseHolder(nsISerialEventTarget* aEventTarget)
|
||||
: mEventTarget(aEventTarget) {}
|
||||
|
||||
already_AddRefed<WebAuthnRegisterPromise> Ensure();
|
||||
|
||||
private:
|
||||
~WebAuthnRegisterPromiseHolder() = default;
|
||||
|
||||
nsCOMPtr<nsISerialEventTarget> mEventTarget;
|
||||
MozPromiseHolder<WebAuthnRegisterPromise> mRegisterPromise;
|
||||
};
|
||||
|
||||
class WebAuthnSignPromiseHolder final : public nsIWebAuthnSignPromise {
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIWEBAUTHNSIGNPROMISE
|
||||
|
||||
explicit WebAuthnSignPromiseHolder(nsISerialEventTarget* aEventTarget)
|
||||
: mEventTarget(aEventTarget) {}
|
||||
|
||||
already_AddRefed<WebAuthnSignPromise> Ensure();
|
||||
|
||||
private:
|
||||
~WebAuthnSignPromiseHolder() = default;
|
||||
|
||||
nsCOMPtr<nsISerialEventTarget> mEventTarget;
|
||||
MozPromiseHolder<WebAuthnSignPromise> mSignPromise;
|
||||
};
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
||||
#endif // mozilla_dom_WebAuthnPromiseHolder_h
|
|
@ -5,11 +5,12 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "mozilla/dom/WebAuthnTransactionParent.h"
|
||||
#include "mozilla/dom/WebAuthnController.h"
|
||||
#include "mozilla/ipc/PBackgroundParent.h"
|
||||
#include "mozilla/ipc/BackgroundParent.h"
|
||||
#include "mozilla/StaticPrefs_security.h"
|
||||
|
||||
#include "CtapArgs.h"
|
||||
#include "nsIWebAuthnService.h"
|
||||
#include "nsThreadUtils.h"
|
||||
|
||||
#ifdef MOZ_WIDGET_ANDROID
|
||||
|
@ -56,9 +57,98 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister(
|
|||
}
|
||||
#endif
|
||||
|
||||
WebAuthnController* ctrl = WebAuthnController::Get();
|
||||
if (ctrl) {
|
||||
ctrl->Register(this, aTransactionId, aTransactionInfo);
|
||||
// If there's an ongoing transaction, abort it.
|
||||
if (mTransactionId.isSome()) {
|
||||
mRegisterPromiseRequest.DisconnectIfExists();
|
||||
mSignPromiseRequest.DisconnectIfExists();
|
||||
Unused << SendAbort(mTransactionId.ref(), NS_ERROR_DOM_ABORT_ERR);
|
||||
}
|
||||
mTransactionId = Some(aTransactionId);
|
||||
|
||||
RefPtr<WebAuthnRegisterPromiseHolder> promiseHolder =
|
||||
new WebAuthnRegisterPromiseHolder(GetCurrentSerialEventTarget());
|
||||
|
||||
PWebAuthnTransactionParent* parent = this;
|
||||
RefPtr<WebAuthnRegisterPromise> promise = promiseHolder->Ensure();
|
||||
promise
|
||||
->Then(
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[this, parent, aTransactionId,
|
||||
clientData = aTransactionInfo.ClientDataJSON()](
|
||||
const WebAuthnRegisterPromise::ResolveValueType& aValue) {
|
||||
mRegisterPromiseRequest.Complete();
|
||||
nsTArray<uint8_t> attObj;
|
||||
nsresult rv = aValue->GetAttestationObject(attObj);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPRegisterAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId,
|
||||
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> credentialId;
|
||||
rv = aValue->GetCredentialId(credentialId);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPRegisterAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId,
|
||||
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<nsString> transports;
|
||||
rv = aValue->GetTransports(transports);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPRegisterAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId,
|
||||
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<WebAuthnExtensionResult> extensions;
|
||||
bool credPropsRk;
|
||||
rv = aValue->GetCredPropsRk(&credPropsRk);
|
||||
if (rv != NS_ERROR_NOT_AVAILABLE) {
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
Telemetry::ScalarAdd(
|
||||
Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPRegisterAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId,
|
||||
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
return;
|
||||
}
|
||||
extensions.AppendElement(
|
||||
WebAuthnExtensionResultCredProps(credPropsRk));
|
||||
}
|
||||
|
||||
WebAuthnMakeCredentialResult result(
|
||||
clientData, attObj, credentialId, transports, extensions);
|
||||
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPRegisterFinish"_ns, 1);
|
||||
Unused << parent->SendConfirmRegister(aTransactionId, result);
|
||||
},
|
||||
[this, parent, aTransactionId](
|
||||
const WebAuthnRegisterPromise::RejectValueType aValue) {
|
||||
mRegisterPromiseRequest.Complete();
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPRegisterAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId, aValue);
|
||||
})
|
||||
->Track(mRegisterPromiseRequest);
|
||||
|
||||
nsCOMPtr<nsIWebAuthnService> webauthnService(
|
||||
do_GetService("@mozilla.org/webauthn/service;1"));
|
||||
|
||||
RefPtr<CtapRegisterArgs> args(new CtapRegisterArgs(aTransactionInfo));
|
||||
|
||||
nsresult rv = webauthnService->MakeCredential(
|
||||
aTransactionId, aTransactionInfo.BrowsingContextId(), args,
|
||||
promiseHolder);
|
||||
if (NS_FAILED(rv)) {
|
||||
promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
}
|
||||
|
||||
return IPC_OK();
|
||||
|
@ -98,8 +188,100 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestSign(
|
|||
}
|
||||
#endif
|
||||
|
||||
WebAuthnController* ctrl = WebAuthnController::Get();
|
||||
ctrl->Sign(this, aTransactionId, aTransactionInfo);
|
||||
if (mTransactionId.isSome()) {
|
||||
mRegisterPromiseRequest.DisconnectIfExists();
|
||||
mSignPromiseRequest.DisconnectIfExists();
|
||||
Unused << SendAbort(mTransactionId.ref(), NS_ERROR_DOM_ABORT_ERR);
|
||||
}
|
||||
mTransactionId = Some(aTransactionId);
|
||||
|
||||
RefPtr<WebAuthnSignPromiseHolder> promiseHolder =
|
||||
new WebAuthnSignPromiseHolder(GetCurrentSerialEventTarget());
|
||||
|
||||
PWebAuthnTransactionParent* parent = this;
|
||||
RefPtr<WebAuthnSignPromise> promise = promiseHolder->Ensure();
|
||||
promise
|
||||
->Then(
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[this, parent, aTransactionId,
|
||||
clientData = aTransactionInfo.ClientDataJSON()](
|
||||
const WebAuthnSignPromise::ResolveValueType& aValue) {
|
||||
mSignPromiseRequest.Complete();
|
||||
nsTArray<uint8_t> credentialId;
|
||||
nsresult rv = aValue->GetCredentialId(credentialId);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPSignAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId,
|
||||
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> signature;
|
||||
rv = aValue->GetSignature(signature);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPSignAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId,
|
||||
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> authenticatorData;
|
||||
rv = aValue->GetAuthenticatorData(authenticatorData);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPSignAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId,
|
||||
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> userHandle;
|
||||
Unused << aValue->GetUserHandle(userHandle); // optional
|
||||
|
||||
nsTArray<WebAuthnExtensionResult> extensions;
|
||||
bool usedAppId;
|
||||
rv = aValue->GetUsedAppId(&usedAppId);
|
||||
if (rv != NS_ERROR_NOT_AVAILABLE) {
|
||||
if (NS_FAILED(rv)) {
|
||||
Telemetry::ScalarAdd(
|
||||
Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPSignAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId,
|
||||
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
return;
|
||||
}
|
||||
extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId));
|
||||
}
|
||||
|
||||
WebAuthnGetAssertionResult result(clientData, credentialId,
|
||||
signature, authenticatorData,
|
||||
extensions, userHandle);
|
||||
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPSignFinish"_ns, 1);
|
||||
Unused << parent->SendConfirmSign(aTransactionId, result);
|
||||
},
|
||||
[this, parent,
|
||||
aTransactionId](const WebAuthnSignPromise::RejectValueType aValue) {
|
||||
mSignPromiseRequest.Complete();
|
||||
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
|
||||
u"CTAPSignAbort"_ns, 1);
|
||||
Unused << parent->SendAbort(aTransactionId, aValue);
|
||||
})
|
||||
->Track(mSignPromiseRequest);
|
||||
|
||||
RefPtr<CtapSignArgs> args(new CtapSignArgs(aTransactionInfo));
|
||||
|
||||
nsCOMPtr<nsIWebAuthnService> webauthnService(
|
||||
do_GetService("@mozilla.org/webauthn/service;1"));
|
||||
nsresult rv = webauthnService->GetAssertion(
|
||||
aTransactionId, aTransactionInfo.BrowsingContextId(), args,
|
||||
promiseHolder);
|
||||
if (NS_FAILED(rv)) {
|
||||
promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
}
|
||||
|
||||
return IPC_OK();
|
||||
}
|
||||
|
@ -115,7 +297,7 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestCancel(
|
|||
mgr->Cancel(this, aTransactionId);
|
||||
}
|
||||
}
|
||||
// fall through in case WebAuthnController was used.
|
||||
// fall through in case the virtual token was used.
|
||||
#endif
|
||||
|
||||
// Bug 1819414 will reroute requests on Android through WebAuthnController and
|
||||
|
@ -125,12 +307,22 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestCancel(
|
|||
if (mgr) {
|
||||
mgr->Cancel(this, aTransactionId);
|
||||
}
|
||||
// fall through in case WebAuthnController was used.
|
||||
// fall through in case the virtual token was used.
|
||||
#endif
|
||||
|
||||
WebAuthnController* ctrl = WebAuthnController::Get();
|
||||
if (ctrl) {
|
||||
ctrl->Cancel(this, aTransactionId);
|
||||
if (mTransactionId.isNothing() ||
|
||||
!MOZ_IS_VALID(aTransactionId, mTransactionId.ref() == aTransactionId)) {
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
mRegisterPromiseRequest.DisconnectIfExists();
|
||||
mSignPromiseRequest.DisconnectIfExists();
|
||||
mTransactionId.reset();
|
||||
|
||||
nsCOMPtr<nsIWebAuthnService> webauthnService(
|
||||
do_GetService("@mozilla.org/webauthn/service;1"));
|
||||
if (webauthnService) {
|
||||
webauthnService->Reset();
|
||||
}
|
||||
|
||||
return IPC_OK();
|
||||
|
@ -167,7 +359,7 @@ void WebAuthnTransactionParent::ActorDestroy(ActorDestroyReason aWhy) {
|
|||
mgr->MaybeClearTransaction(this);
|
||||
}
|
||||
}
|
||||
// fall through in case WebAuthnController was used.
|
||||
// fall through in case the virtual token was used.
|
||||
#endif
|
||||
|
||||
// Bug 1819414 will reroute requests on Android through WebAuthnController and
|
||||
|
@ -177,13 +369,12 @@ void WebAuthnTransactionParent::ActorDestroy(ActorDestroyReason aWhy) {
|
|||
if (mgr) {
|
||||
mgr->MaybeClearTransaction(this);
|
||||
}
|
||||
// fall through in case WebAuthnController was used.
|
||||
// fall through in case the virtual token was used.
|
||||
#endif
|
||||
|
||||
WebAuthnController* ctrl = WebAuthnController::Get();
|
||||
if (ctrl) {
|
||||
ctrl->MaybeClearTransaction(this);
|
||||
}
|
||||
mRegisterPromiseRequest.DisconnectIfExists();
|
||||
mSignPromiseRequest.DisconnectIfExists();
|
||||
mTransactionId.reset();
|
||||
}
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
|
|
@ -8,15 +8,17 @@
|
|||
#define mozilla_dom_WebAuthnTransactionParent_h
|
||||
|
||||
#include "mozilla/dom/PWebAuthnTransactionParent.h"
|
||||
#include "mozilla/dom/WebAuthnPromiseHolder.h"
|
||||
|
||||
/*
|
||||
* Parent process IPC implementation for WebAuthn and U2F API. Receives
|
||||
* authentication data to be either registered or signed by a key, passes
|
||||
* information to U2FTokenManager.
|
||||
* Parent process IPC implementation for WebAuthn.
|
||||
*/
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
class WebAuthnRegisterPromiseHolder;
|
||||
class WebAuthnSignPromiseHolder;
|
||||
|
||||
class WebAuthnTransactionParent final : public PWebAuthnTransactionParent {
|
||||
public:
|
||||
NS_INLINE_DECL_REFCOUNTING(WebAuthnTransactionParent);
|
||||
|
@ -39,6 +41,10 @@ class WebAuthnTransactionParent final : public PWebAuthnTransactionParent {
|
|||
|
||||
private:
|
||||
~WebAuthnTransactionParent() = default;
|
||||
|
||||
Maybe<uint64_t> mTransactionId;
|
||||
MozPromiseRequestHolder<WebAuthnRegisterPromise> mRegisterPromiseRequest;
|
||||
MozPromiseRequestHolder<WebAuthnSignPromise> mSignPromiseRequest;
|
||||
};
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
#include "mozilla/ipc/BackgroundParent.h"
|
||||
#include "mozilla/ClearOnShutdown.h"
|
||||
#include "mozilla/Unused.h"
|
||||
#include "nsIWebAuthnController.h"
|
||||
#include "nsIWebAuthnAttObj.h"
|
||||
#include "nsTextFormatter.h"
|
||||
#include "nsWindowsHelpers.h"
|
||||
#include "AuthrsBridge_ffi.h"
|
||||
|
|
|
@ -25,9 +25,8 @@ use base64::Engine;
|
|||
use cstr::cstr;
|
||||
use moz_task::{get_main_thread, RunnableBuilder};
|
||||
use nserror::{
|
||||
nsresult, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR, NS_ERROR_FAILURE,
|
||||
NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_ERROR_NULL_POINTER,
|
||||
NS_OK,
|
||||
nsresult, NS_ERROR_DOM_ABORT_ERR, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR,
|
||||
NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NULL_POINTER, NS_OK,
|
||||
};
|
||||
use nsstring::{nsACString, nsCString, nsString};
|
||||
use serde::Serialize;
|
||||
|
@ -40,24 +39,24 @@ use std::sync::{Arc, Mutex};
|
|||
use thin_vec::{thin_vec, ThinVec};
|
||||
use xpcom::interfaces::{
|
||||
nsICredentialParameters, nsICtapRegisterArgs, nsICtapRegisterResult, nsICtapSignArgs,
|
||||
nsICtapSignResult, nsIObserverService, nsIWebAuthnAttObj, nsIWebAuthnController,
|
||||
nsIWebAuthnTransport,
|
||||
nsICtapSignResult, nsIObserverService, nsIWebAuthnAttObj, nsIWebAuthnRegisterPromise,
|
||||
nsIWebAuthnService, nsIWebAuthnSignPromise,
|
||||
};
|
||||
use xpcom::{xpcom_method, RefPtr};
|
||||
|
||||
mod test_token;
|
||||
use test_token::TestTokenManager;
|
||||
|
||||
fn authrs_to_nserror(e: &AuthenticatorError) -> nsresult {
|
||||
fn authrs_to_nserror(e: AuthenticatorError) -> nsresult {
|
||||
match e {
|
||||
AuthenticatorError::CredentialExcluded => NS_ERROR_DOM_INVALID_STATE_ERR,
|
||||
_ => NS_ERROR_DOM_NOT_ALLOWED_ERR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_cancels_prompts(e: &AuthenticatorError) -> bool {
|
||||
match e {
|
||||
AuthenticatorError::CredentialExcluded | AuthenticatorError::PinError(_) => false,
|
||||
fn should_cancel_prompts<T>(result: &Result<T, AuthenticatorError>) -> bool {
|
||||
match result {
|
||||
Err(AuthenticatorError::CredentialExcluded) | Err(AuthenticatorError::PinError(_)) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +116,7 @@ fn send_prompt(
|
|||
})
|
||||
)
|
||||
.or(Err(NS_ERROR_FAILURE))?;
|
||||
RunnableBuilder::new("AuthrsTransport::send_prompt", move || {
|
||||
RunnableBuilder::new("AuthrsService::send_prompt", move || {
|
||||
if let Ok(obs_svc) = xpcom::components::Observer::service::<nsIObserverService>() {
|
||||
unsafe {
|
||||
obs_svc.NotifyObservers(
|
||||
|
@ -136,39 +135,29 @@ fn cancel_prompts(tid: u64) -> Result<(), nsresult> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
type RegisterResultOrError = Result<RegisterResult, AuthenticatorError>;
|
||||
|
||||
#[xpcom(implement(nsICtapRegisterResult), atomic)]
|
||||
pub struct CtapRegisterResult {
|
||||
result: RegisterResultOrError,
|
||||
result: RegisterResult,
|
||||
}
|
||||
|
||||
impl CtapRegisterResult {
|
||||
xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>);
|
||||
fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
let mut out = ThinVec::new();
|
||||
let make_cred_res = self.result.as_ref().or(Err(NS_ERROR_FAILURE))?;
|
||||
serde_cbor::to_writer(&mut out, &make_cred_res.att_obj).or(Err(NS_ERROR_FAILURE))?;
|
||||
serde_cbor::to_writer(&mut out, &self.result.att_obj).or(Err(NS_ERROR_FAILURE))?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>);
|
||||
fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
let mut out = ThinVec::new();
|
||||
if let Ok(make_cred_res) = &self.result {
|
||||
if let Some(credential_data) = &make_cred_res.att_obj.auth_data.credential_data {
|
||||
out.extend_from_slice(&credential_data.credential_id);
|
||||
return Ok(out);
|
||||
}
|
||||
}
|
||||
Err(NS_ERROR_FAILURE)
|
||||
let Some(credential_data) = &self.result.att_obj.auth_data.credential_data else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
Ok(credential_data.credential_id.as_slice().into())
|
||||
}
|
||||
|
||||
xpcom_method!(get_transports => GetTransports() -> ThinVec<nsString>);
|
||||
fn get_transports(&self) -> Result<ThinVec<nsString>, nsresult> {
|
||||
if self.result.is_err() {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
// The list that we return here might be included in a future GetAssertion request as a
|
||||
// hint as to which transports to try. We currently only support the USB transport. If
|
||||
// that changes, we will need a mechanism to track which transport was used for a
|
||||
|
@ -178,22 +167,11 @@ impl CtapRegisterResult {
|
|||
|
||||
xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool);
|
||||
fn get_cred_props_rk(&self) -> Result<bool, nsresult> {
|
||||
let result = self.result.as_ref().or(Err(NS_ERROR_FAILURE))?;
|
||||
let cred_props = result
|
||||
.extensions
|
||||
.cred_props
|
||||
.as_ref()
|
||||
.ok_or(NS_ERROR_NOT_AVAILABLE)?;
|
||||
let Some(cred_props) = &self.result.extensions.cred_props else {
|
||||
return Err(NS_ERROR_NOT_AVAILABLE);
|
||||
};
|
||||
Ok(cred_props.rk)
|
||||
}
|
||||
|
||||
xpcom_method!(get_status => GetStatus() -> nsresult);
|
||||
fn get_status(&self) -> Result<nsresult, nsresult> {
|
||||
match &self.result {
|
||||
Ok(_) => Ok(NS_OK),
|
||||
Err(e) => Ok(authrs_to_nserror(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[xpcom(implement(nsIWebAuthnAttObj), atomic)]
|
||||
|
@ -229,132 +207,65 @@ impl WebAuthnAttObj {
|
|||
|
||||
xpcom_method!(get_public_key_algorithm => GetPublicKeyAlgorithm() -> i32);
|
||||
fn get_public_key_algorithm(&self) -> Result<i32, nsresult> {
|
||||
if let Some(credential_data) = &self.att_obj.auth_data.credential_data {
|
||||
// safe to cast to i32 by inspection of defined values
|
||||
Ok(credential_data.credential_public_key.alg as i32)
|
||||
} else {
|
||||
Err(NS_ERROR_FAILURE)
|
||||
}
|
||||
let Some(credential_data) = &self.att_obj.auth_data.credential_data else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
// safe to cast to i32 by inspection of defined values
|
||||
Ok(credential_data.credential_public_key.alg as i32)
|
||||
}
|
||||
}
|
||||
|
||||
type SignResultOrError = Result<SignResult, AuthenticatorError>;
|
||||
|
||||
#[xpcom(implement(nsICtapSignResult), atomic)]
|
||||
pub struct CtapSignResult {
|
||||
result: SignResultOrError,
|
||||
result: SignResult,
|
||||
}
|
||||
|
||||
impl CtapSignResult {
|
||||
xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>);
|
||||
fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
let rv = NS_ERROR_FAILURE;
|
||||
let inner = self.result.as_ref().or(Err(rv))?;
|
||||
let cred = inner.assertion.credentials.as_ref().ok_or(rv)?;
|
||||
let Some(cred) = &self.result.assertion.credentials else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
Ok(cred.id.as_slice().into())
|
||||
}
|
||||
|
||||
xpcom_method!(get_signature => GetSignature() -> ThinVec<u8>);
|
||||
fn get_signature(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
let inner = self.result.as_ref().or(Err(NS_ERROR_FAILURE))?;
|
||||
Ok(inner.assertion.signature.as_slice().into())
|
||||
Ok(self.result.assertion.signature.as_slice().into())
|
||||
}
|
||||
|
||||
xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>);
|
||||
fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
let inner = self.result.as_ref().or(Err(NS_ERROR_FAILURE))?;
|
||||
Ok(inner.assertion.auth_data.to_vec().into())
|
||||
Ok(self.result.assertion.auth_data.to_vec().into())
|
||||
}
|
||||
|
||||
xpcom_method!(get_user_handle => GetUserHandle() -> ThinVec<u8>);
|
||||
fn get_user_handle(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
let rv = NS_ERROR_NOT_AVAILABLE;
|
||||
let inner = self.result.as_ref().or(Err(rv))?;
|
||||
let user = &inner.assertion.user.as_ref().ok_or(rv)?;
|
||||
let Some(user) = &self.result.assertion.user else {
|
||||
return Err(NS_ERROR_NOT_AVAILABLE);
|
||||
};
|
||||
Ok(user.id.as_slice().into())
|
||||
}
|
||||
|
||||
xpcom_method!(get_user_name => GetUserName() -> nsACString);
|
||||
fn get_user_name(&self) -> Result<nsCString, nsresult> {
|
||||
let rv = NS_ERROR_NOT_AVAILABLE;
|
||||
let inner = self.result.as_ref().or(Err(rv))?;
|
||||
let user = inner.assertion.user.as_ref().ok_or(rv)?;
|
||||
let name = user.name.as_ref().ok_or(rv)?;
|
||||
let Some(user) = &self.result.assertion.user else {
|
||||
return Err(NS_ERROR_NOT_AVAILABLE);
|
||||
};
|
||||
let Some(name) = &user.name else {
|
||||
return Err(NS_ERROR_NOT_AVAILABLE);
|
||||
};
|
||||
Ok(nsCString::from(name))
|
||||
}
|
||||
|
||||
xpcom_method!(get_used_app_id => GetUsedAppId() -> bool);
|
||||
fn get_used_app_id(&self) -> Result<bool, nsresult> {
|
||||
let inner = self.result.as_ref().or(Err(NS_ERROR_FAILURE))?;
|
||||
let app_id = inner.extensions.app_id.ok_or(NS_ERROR_NOT_AVAILABLE)?;
|
||||
Ok(app_id)
|
||||
}
|
||||
|
||||
xpcom_method!(get_status => GetStatus() -> nsresult);
|
||||
fn get_status(&self) -> Result<nsresult, nsresult> {
|
||||
match &self.result {
|
||||
Ok(_) => Ok(NS_OK),
|
||||
Err(e) => Ok(authrs_to_nserror(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Controller wraps a raw pointer to an nsIWebAuthnController. The AuthrsTransport struct holds a
|
||||
/// Controller which we need to initialize from the SetController XPCOM method. Since XPCOM
|
||||
/// methods take &self, Controller must have interior mutability.
|
||||
#[derive(Clone)]
|
||||
struct Controller(RefCell<*const nsIWebAuthnController>);
|
||||
|
||||
/// Our implementation of nsIWebAuthnController in WebAuthnController.cpp has the property that its
|
||||
/// XPCOM methods are safe to call from any thread, hence a raw pointer to an nsIWebAuthnController
|
||||
/// is Send.
|
||||
unsafe impl Send for Controller {}
|
||||
|
||||
impl Controller {
|
||||
fn init(&self, ptr: *const nsIWebAuthnController) -> Result<(), nsresult> {
|
||||
self.0.replace(ptr);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_register(&self, tid: u64, result: RegisterResultOrError) -> Result<(), nsresult> {
|
||||
if (*self.0.borrow()).is_null() {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
let wrapped_result = CtapRegisterResult::allocate(InitCtapRegisterResult { result })
|
||||
.query_interface::<nsICtapRegisterResult>()
|
||||
.ok_or(NS_ERROR_FAILURE)?;
|
||||
unsafe {
|
||||
(**(self.0.borrow())).FinishRegister(tid, wrapped_result.coerce());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_sign(&self, tid: u64, result: SignResultOrError) -> Result<(), nsresult> {
|
||||
if (*self.0.borrow()).is_null() {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
let wrapped_result = CtapSignResult::allocate(InitCtapSignResult { result })
|
||||
.query_interface::<nsICtapSignResult>()
|
||||
.ok_or(NS_ERROR_FAILURE)?;
|
||||
unsafe {
|
||||
(**(self.0.borrow())).FinishSign(tid, wrapped_result.coerce());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel(&self, tid: u64) -> Result<(), nsresult> {
|
||||
if (*self.0.borrow()).is_null() {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
unsafe {
|
||||
(**(self.0.borrow())).Cancel(tid);
|
||||
}
|
||||
Ok(())
|
||||
self.result.extensions.app_id.ok_or(NS_ERROR_NOT_AVAILABLE)
|
||||
}
|
||||
}
|
||||
|
||||
// A transaction may create a channel to ask a user for additional input, e.g. a PIN. The Sender
|
||||
// component of this channel is sent to an AuthrsTransport in a StatusUpdate. AuthrsTransport
|
||||
// component of this channel is sent to an AuthrsServide in a StatusUpdate. AuthrsService
|
||||
// caches the sender along with the expected (u64) transaction ID, which is used as a consistency
|
||||
// check in callbacks.
|
||||
type PinReceiver = Option<(u64, Sender<Pin>)>;
|
||||
|
@ -365,8 +276,8 @@ fn status_callback(
|
|||
tid: u64,
|
||||
origin: &String,
|
||||
browsing_context_id: u64,
|
||||
pin_receiver: Arc<Mutex<PinReceiver>>, /* Shared with an AuthrsTransport */
|
||||
selection_receiver: Arc<Mutex<SelectionReceiver>>, /* Shared with an AuthrsTransport */
|
||||
pin_receiver: Arc<Mutex<PinReceiver>>, /* Shared with an AuthrsService */
|
||||
selection_receiver: Arc<Mutex<SelectionReceiver>>, /* Shared with an AuthrsService */
|
||||
) -> Result<(), nsresult> {
|
||||
let origin = Some(origin.as_str());
|
||||
let browsing_context_id = Some(browsing_context_id);
|
||||
|
@ -477,6 +388,62 @@ fn status_callback(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RegisterPromise(RefPtr<nsIWebAuthnRegisterPromise>);
|
||||
|
||||
impl RegisterPromise {
|
||||
fn resolve_or_reject(&self, result: Result<RegisterResult, nsresult>) -> Result<(), nsresult> {
|
||||
match result {
|
||||
Ok(result) => {
|
||||
let wrapped_result =
|
||||
CtapRegisterResult::allocate(InitCtapRegisterResult { result })
|
||||
.query_interface::<nsICtapRegisterResult>()
|
||||
.ok_or(NS_ERROR_FAILURE)?;
|
||||
unsafe { self.0.Resolve(wrapped_result.coerce()) };
|
||||
}
|
||||
Err(result) => {
|
||||
unsafe { self.0.Reject(result) };
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SignPromise(RefPtr<nsIWebAuthnSignPromise>);
|
||||
|
||||
impl SignPromise {
|
||||
fn resolve_or_reject(&self, result: Result<SignResult, nsresult>) -> Result<(), nsresult> {
|
||||
match result {
|
||||
Ok(result) => {
|
||||
let wrapped_result = CtapSignResult::allocate(InitCtapSignResult { result })
|
||||
.query_interface::<nsICtapSignResult>()
|
||||
.ok_or(NS_ERROR_FAILURE)?;
|
||||
unsafe { self.0.Resolve(wrapped_result.coerce()) };
|
||||
}
|
||||
Err(result) => {
|
||||
unsafe { self.0.Reject(result) };
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum TransactionPromise {
|
||||
Register(RegisterPromise),
|
||||
Sign(SignPromise),
|
||||
}
|
||||
|
||||
impl TransactionPromise {
|
||||
fn reject(&self, err: nsresult) -> Result<(), nsresult> {
|
||||
match self {
|
||||
TransactionPromise::Register(promise) => promise.resolve_or_reject(Err(err)),
|
||||
TransactionPromise::Sign(promise) => promise.resolve_or_reject(Err(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TransactionArgs {
|
||||
Register(/* timeout */ u64, RegisterArgs),
|
||||
// Bug 1838932 - we'll need to cache SignArgs once we support conditional mediation
|
||||
|
@ -487,83 +454,67 @@ struct TransactionState {
|
|||
tid: u64,
|
||||
browsing_context_id: u64,
|
||||
pending_args: Option<TransactionArgs>,
|
||||
promise: TransactionPromise,
|
||||
}
|
||||
|
||||
// AuthrsTransport provides an nsIWebAuthnTransport interface to an AuthenticatorService. This
|
||||
// allows an nsIWebAuthnController to dispatch MakeCredential and GetAssertion requests to USB HID
|
||||
// tokens. The AuthrsTransport struct also keeps 1) a pointer to the active nsIWebAuthnController,
|
||||
// which is used to prompt the user for input and to return results to the controller; and
|
||||
// 2) a channel through which to receive a pin callback.
|
||||
#[xpcom(implement(nsIWebAuthnTransport), atomic)]
|
||||
pub struct AuthrsTransport {
|
||||
// AuthrsService provides an nsIWebAuthnService built on top of authenticator-rs.
|
||||
#[xpcom(implement(nsIWebAuthnService), atomic)]
|
||||
pub struct AuthrsService {
|
||||
usb_token_manager: RefCell<StateMachine>, // interior mutable for use in XPCOM methods
|
||||
test_token_manager: TestTokenManager,
|
||||
controller: Controller,
|
||||
pin_receiver: Arc<Mutex<PinReceiver>>,
|
||||
selection_receiver: Arc<Mutex<SelectionReceiver>>,
|
||||
transaction: Arc<Mutex<Option<TransactionState>>>,
|
||||
}
|
||||
|
||||
impl AuthrsTransport {
|
||||
xpcom_method!(get_controller => GetController() -> *const nsIWebAuthnController);
|
||||
fn get_controller(&self) -> Result<RefPtr<nsIWebAuthnController>, nsresult> {
|
||||
Err(NS_ERROR_NOT_IMPLEMENTED)
|
||||
}
|
||||
|
||||
// # Safety
|
||||
//
|
||||
// This will mutably borrow the controller pointer through a RefCell. The caller must ensure
|
||||
// that at most one WebAuthn transaction is active at any given time.
|
||||
xpcom_method!(set_controller => SetController(aController: *const nsIWebAuthnController));
|
||||
fn set_controller(&self, controller: *const nsIWebAuthnController) -> Result<(), nsresult> {
|
||||
self.controller.init(controller)
|
||||
}
|
||||
|
||||
impl AuthrsService {
|
||||
xpcom_method!(pin_callback => PinCallback(aTransactionId: u64, aPin: *const nsACString));
|
||||
fn pin_callback(&self, transaction_id: u64, pin: &nsACString) -> Result<(), nsresult> {
|
||||
let mut guard = self.pin_receiver.lock().or(Err(NS_ERROR_FAILURE))?;
|
||||
match guard.take() {
|
||||
Some((tid, channel)) if tid == transaction_id => channel
|
||||
.send(Pin::new(&pin.to_string()))
|
||||
.or(Err(NS_ERROR_FAILURE)),
|
||||
// Either we weren't expecting a pin, or the controller is confused
|
||||
// about which transaction is active. Neither is recoverable, so it's
|
||||
// OK to drop the PinReceiver here.
|
||||
_ => Err(NS_ERROR_FAILURE),
|
||||
let Some((tid, channel)) = self.pin_receiver.lock().unwrap().take() else {
|
||||
// We weren't expecting a pin.
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
if tid != transaction_id {
|
||||
// The browser is confused about which transaction is active.
|
||||
// This shouldn't happen
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
channel
|
||||
.send(Pin::new(&pin.to_string()))
|
||||
.or(Err(NS_ERROR_FAILURE))
|
||||
}
|
||||
|
||||
xpcom_method!(selection_callback => SelectionCallback(aTransactionId: u64, aSelection: u64));
|
||||
fn selection_callback(&self, transaction_id: u64, selection: u64) -> Result<(), nsresult> {
|
||||
let mut guard = self.selection_receiver.lock().or(Err(NS_ERROR_FAILURE))?;
|
||||
match guard.take() {
|
||||
Some((tid, channel)) if tid == transaction_id => channel
|
||||
.send(Some(selection as usize))
|
||||
.or(Err(NS_ERROR_FAILURE)),
|
||||
// Either we weren't expecting a selection, or the controller is confused
|
||||
// about which transaction is active. Neither is recoverable, so it's
|
||||
// OK to drop the SelectionReceiver here.
|
||||
_ => Err(NS_ERROR_FAILURE),
|
||||
let Some((tid, channel)) = self.selection_receiver.lock().unwrap().take() else {
|
||||
// We weren't expecting a selection.
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
if tid != transaction_id {
|
||||
// The browser is confused about which transaction is active.
|
||||
// This shouldn't happen
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
channel
|
||||
.send(Some(selection as usize))
|
||||
.or(Err(NS_ERROR_FAILURE))
|
||||
}
|
||||
|
||||
// # Safety
|
||||
//
|
||||
// This will mutably borrow usb_token_manager through a RefCell. The caller must ensure that at
|
||||
// most one WebAuthn transaction is active at any given time.
|
||||
xpcom_method!(make_credential => MakeCredential(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsICtapRegisterArgs));
|
||||
xpcom_method!(make_credential => MakeCredential(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsICtapRegisterArgs, aPromise: *const nsIWebAuthnRegisterPromise));
|
||||
fn make_credential(
|
||||
&self,
|
||||
tid: u64,
|
||||
browsing_context_id: u64,
|
||||
args: *const nsICtapRegisterArgs,
|
||||
args: &nsICtapRegisterArgs,
|
||||
promise: &nsIWebAuthnRegisterPromise,
|
||||
) -> Result<(), nsresult> {
|
||||
self.reset()?;
|
||||
|
||||
if args.is_null() {
|
||||
return Err(NS_ERROR_NULL_POINTER);
|
||||
}
|
||||
let args = unsafe { &*args };
|
||||
let promise = RegisterPromise(RefPtr::new(promise));
|
||||
|
||||
let mut origin = nsString::new();
|
||||
unsafe { args.GetOrigin(&mut *origin) }.to_result()?;
|
||||
|
@ -691,6 +642,7 @@ impl AuthrsTransport {
|
|||
tid,
|
||||
browsing_context_id,
|
||||
pending_args: Some(TransactionArgs::Register(timeout_ms as u64, info)),
|
||||
promise: TransactionPromise::Register(promise),
|
||||
});
|
||||
|
||||
if none_attestation
|
||||
|
@ -729,61 +681,61 @@ impl AuthrsTransport {
|
|||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
let browsing_context_id = state.browsing_context_id;
|
||||
let (timeout_ms, info) = match state.pending_args.take() {
|
||||
Some(TransactionArgs::Register(timeout_ms, info)) => (timeout_ms, info),
|
||||
_ => return Err(NS_ERROR_FAILURE),
|
||||
let Some(TransactionArgs::Register(timeout_ms, info)) = state.pending_args.take() else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
|
||||
let (status_tx, status_rx) = channel::<StatusUpdate>();
|
||||
let pin_receiver = self.pin_receiver.clone();
|
||||
let selection_receiver = self.selection_receiver.clone();
|
||||
let status_origin = info.origin.clone();
|
||||
RunnableBuilder::new(
|
||||
"AuthrsTransport::MakeCredential::StatusReceiver",
|
||||
move || {
|
||||
let _ = status_callback(
|
||||
status_rx,
|
||||
tid,
|
||||
&status_origin,
|
||||
browsing_context_id,
|
||||
pin_receiver,
|
||||
selection_receiver,
|
||||
);
|
||||
},
|
||||
)
|
||||
RunnableBuilder::new("AuthrsService::MakeCredential::StatusReceiver", move || {
|
||||
let _ = status_callback(
|
||||
status_rx,
|
||||
tid,
|
||||
&status_origin,
|
||||
browsing_context_id,
|
||||
pin_receiver,
|
||||
selection_receiver,
|
||||
);
|
||||
})
|
||||
.may_block(true)
|
||||
.dispatch_background_task()?;
|
||||
|
||||
let controller = self.controller.clone();
|
||||
let callback_transaction = self.transaction.clone();
|
||||
let callback_origin = info.origin.clone();
|
||||
let state_callback = StateCallback::<RegisterResultOrError>::new(Box::new(move |result| {
|
||||
let result = match result {
|
||||
Ok(mut make_cred_res) => {
|
||||
let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new(
|
||||
Box::new(move |mut result| {
|
||||
let mut guard = callback_transaction.lock().unwrap();
|
||||
let Some(state) = guard.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let TransactionPromise::Register(ref promise) = state.promise else {
|
||||
return;
|
||||
};
|
||||
if let Ok(inner) = result.as_mut() {
|
||||
// Tokens always provide attestation, but the user may have asked we not
|
||||
// include the attestation statement in the response.
|
||||
if force_none_attestation {
|
||||
make_cred_res.att_obj.anonymize();
|
||||
inner.att_obj.anonymize();
|
||||
}
|
||||
Ok(make_cred_res)
|
||||
}
|
||||
Err(e @ AuthenticatorError::CredentialExcluded) => {
|
||||
if let Err(AuthenticatorError::CredentialExcluded) = result {
|
||||
let _ = send_prompt(
|
||||
BrowserPromptType::AlreadyRegistered,
|
||||
tid,
|
||||
Some(&callback_origin),
|
||||
Some(browsing_context_id),
|
||||
);
|
||||
Err(e)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
// Some errors are accompanied by prompts that should persist after the
|
||||
// operation terminates.
|
||||
if result.is_ok() || error_cancels_prompts(&result.as_ref().unwrap_err()) {
|
||||
let _ = cancel_prompts(tid);
|
||||
}
|
||||
let _ = controller.finish_register(tid, result);
|
||||
}));
|
||||
if should_cancel_prompts(&result) {
|
||||
// Some errors are accompanied by prompts that should persist after the
|
||||
// operation terminates.
|
||||
let _ = cancel_prompts(tid);
|
||||
}
|
||||
let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror));
|
||||
}),
|
||||
);
|
||||
|
||||
// The authenticator crate provides an `AuthenticatorService` which can dispatch a request
|
||||
// in parallel to any number of transports. We only support the USB transport in production
|
||||
|
@ -792,13 +744,13 @@ impl AuthrsTransport {
|
|||
if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") {
|
||||
self.usb_token_manager.borrow_mut().register(
|
||||
timeout_ms,
|
||||
info.into(),
|
||||
info,
|
||||
status_tx,
|
||||
state_callback,
|
||||
);
|
||||
} else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") {
|
||||
self.test_token_manager
|
||||
.register(timeout_ms, info.into(), status_tx, state_callback);
|
||||
.register(timeout_ms, info, status_tx, state_callback);
|
||||
} else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
|
@ -810,19 +762,17 @@ impl AuthrsTransport {
|
|||
//
|
||||
// This will mutably borrow usb_token_manager through a RefCell. The caller must ensure that at
|
||||
// most one WebAuthn transaction is active at any given time.
|
||||
xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsICtapSignArgs));
|
||||
xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsICtapSignArgs, aPromise: *const nsIWebAuthnSignPromise));
|
||||
fn get_assertion(
|
||||
&self,
|
||||
tid: u64,
|
||||
browsing_context_id: u64,
|
||||
args: *const nsICtapSignArgs,
|
||||
args: &nsICtapSignArgs,
|
||||
promise: &nsIWebAuthnSignPromise,
|
||||
) -> Result<(), nsresult> {
|
||||
self.reset()?;
|
||||
|
||||
if args.is_null() {
|
||||
return Err(NS_ERROR_NULL_POINTER);
|
||||
}
|
||||
let args = unsafe { &*args };
|
||||
let promise = SignPromise(RefPtr::new(promise));
|
||||
|
||||
let mut origin = nsString::new();
|
||||
unsafe { args.GetOrigin(&mut *origin) }.to_result()?;
|
||||
|
@ -869,7 +819,7 @@ impl AuthrsTransport {
|
|||
let pin_receiver = self.pin_receiver.clone();
|
||||
let selection_receiver = self.selection_receiver.clone();
|
||||
let status_origin = origin.to_string();
|
||||
RunnableBuilder::new("AuthrsTransport::GetAssertion::StatusReceiver", move || {
|
||||
RunnableBuilder::new("AuthrsService::GetAssertion::StatusReceiver", move || {
|
||||
let _ = status_callback(
|
||||
status_rx,
|
||||
tid,
|
||||
|
@ -888,9 +838,16 @@ impl AuthrsTransport {
|
|||
None
|
||||
};
|
||||
|
||||
let controller = self.controller.clone();
|
||||
let state_callback =
|
||||
StateCallback::<SignResultOrError>::new(Box::new(move |mut result| {
|
||||
let callback_transaction = self.transaction.clone();
|
||||
let state_callback = StateCallback::<Result<SignResult, AuthenticatorError>>::new(
|
||||
Box::new(move |mut result| {
|
||||
let mut guard = callback_transaction.lock().unwrap();
|
||||
let Some(state) = guard.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let TransactionPromise::Sign(ref promise) = state.promise else {
|
||||
return;
|
||||
};
|
||||
if uniq_allowed_cred.is_some() {
|
||||
// In CTAP 2.0, but not CTAP 2.1, the assertion object's credential field
|
||||
// "May be omitted if the allowList has exactly one credential." If we had
|
||||
|
@ -899,13 +856,14 @@ impl AuthrsTransport {
|
|||
inner.assertion.credentials = uniq_allowed_cred;
|
||||
}
|
||||
}
|
||||
// Some errors are accompanied by prompts that should persist after the
|
||||
// operation terminates.
|
||||
if result.is_ok() || error_cancels_prompts(&result.as_ref().unwrap_err()) {
|
||||
if should_cancel_prompts(&result) {
|
||||
// Some errors are accompanied by prompts that should persist after the
|
||||
// operation terminates.
|
||||
let _ = cancel_prompts(tid);
|
||||
}
|
||||
let _ = controller.finish_sign(tid, result);
|
||||
}));
|
||||
let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror));
|
||||
}),
|
||||
);
|
||||
|
||||
let info = SignArgs {
|
||||
client_data_hash: client_data_hash_arr,
|
||||
|
@ -934,19 +892,20 @@ impl AuthrsTransport {
|
|||
tid,
|
||||
browsing_context_id,
|
||||
pending_args: None,
|
||||
promise: TransactionPromise::Sign(promise),
|
||||
});
|
||||
|
||||
// As in `register`, we are intentionally avoiding `AuthenticatorService` here.
|
||||
if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") {
|
||||
self.usb_token_manager.borrow_mut().sign(
|
||||
timeout_ms as u64,
|
||||
info.into(),
|
||||
info,
|
||||
status_tx,
|
||||
state_callback,
|
||||
);
|
||||
} else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") {
|
||||
self.test_token_manager
|
||||
.sign(timeout_ms as u64, info.into(), status_tx, state_callback);
|
||||
.sign(timeout_ms as u64, info, status_tx, state_callback);
|
||||
} else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
|
@ -956,32 +915,38 @@ impl AuthrsTransport {
|
|||
|
||||
xpcom_method!(cancel => Cancel(aTransactionId: u64));
|
||||
fn cancel(&self, tid: u64) -> Result<(), nsresult> {
|
||||
let mut guard = self.transaction.lock().unwrap();
|
||||
if guard.as_ref().map_or(false, |state| state.tid == tid) {
|
||||
self.reset_helper()?;
|
||||
self.controller.cancel(tid)?;
|
||||
{
|
||||
let mut guard = self.transaction.lock().unwrap();
|
||||
let Some(state) = guard.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
if state.tid != tid {
|
||||
return Ok(());
|
||||
}
|
||||
state.promise.reject(NS_ERROR_DOM_NOT_ALLOWED_ERR)?;
|
||||
*guard = None;
|
||||
}
|
||||
self.reset_helper()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
xpcom_method!(reset => Reset());
|
||||
fn reset(&self) -> Result<(), nsresult> {
|
||||
if let Some(transaction) = self.transaction.lock().unwrap().take() {
|
||||
self.reset_helper()?;
|
||||
cancel_prompts(transaction.tid)?;
|
||||
{
|
||||
if let Some(state) = self.transaction.lock().unwrap().take() {
|
||||
cancel_prompts(state.tid)?;
|
||||
state.promise.reject(NS_ERROR_DOM_ABORT_ERR)?;
|
||||
}
|
||||
}
|
||||
self.reset_helper()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset_helper(&self) -> Result<(), nsresult> {
|
||||
drop(self.pin_receiver.lock().or(Err(NS_ERROR_FAILURE))?.take());
|
||||
drop(
|
||||
self.selection_receiver
|
||||
.lock()
|
||||
.or(Err(NS_ERROR_FAILURE))?
|
||||
.take(),
|
||||
);
|
||||
// NOTE: Caller must not hold the lock on self.transaction as
|
||||
// the USB token manager may try to take it in cancel().
|
||||
drop(self.pin_receiver.lock().unwrap().take());
|
||||
drop(self.selection_receiver.lock().unwrap().take());
|
||||
self.usb_token_manager.borrow_mut().cancel();
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1108,13 +1073,10 @@ impl AuthrsTransport {
|
|||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn authrs_transport_constructor(
|
||||
result: *mut *const nsIWebAuthnTransport,
|
||||
) -> nsresult {
|
||||
let wrapper = AuthrsTransport::allocate(InitAuthrsTransport {
|
||||
pub extern "C" fn authrs_service_constructor(result: *mut *const nsIWebAuthnService) -> nsresult {
|
||||
let wrapper = AuthrsService::allocate(InitAuthrsService {
|
||||
usb_token_manager: RefCell::new(StateMachine::new()),
|
||||
test_token_manager: TestTokenManager::new(),
|
||||
controller: Controller(RefCell::new(std::ptr::null())),
|
||||
pin_receiver: Arc::new(Mutex::new(None)),
|
||||
selection_receiver: Arc::new(Mutex::new(None)),
|
||||
transaction: Arc::new(Mutex::new(None)),
|
||||
|
@ -1154,7 +1116,7 @@ pub extern "C" fn authrs_transport_constructor(
|
|||
}
|
||||
|
||||
unsafe {
|
||||
RefPtr::new(wrapper.coerce::<nsIWebAuthnTransport>()).forget(&mut *result);
|
||||
RefPtr::new(wrapper.coerce::<nsIWebAuthnService>()).forget(&mut *result);
|
||||
}
|
||||
NS_OK
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
Classes = [
|
||||
{
|
||||
'cid': '{ebe8a51d-bd54-4838-b031-cd2289990e14}',
|
||||
'contract_ids': ['@mozilla.org/webauthn/transport;1'],
|
||||
'headers': ['/dom/webauthn/AuthrsTransport.h'],
|
||||
'constructor': 'mozilla::dom::NewAuthrsTransport',
|
||||
'contract_ids': ['@mozilla.org/webauthn/service;1'],
|
||||
'headers': ['/dom/webauthn/AuthrsService.h'],
|
||||
'constructor': 'mozilla::dom::NewAuthrsService',
|
||||
},
|
||||
]
|
||||
|
|
|
@ -13,7 +13,13 @@ XPCOM_MANIFESTS += [
|
|||
"components.conf",
|
||||
]
|
||||
|
||||
XPIDL_SOURCES += ["nsIWebAuthnController.idl"]
|
||||
XPIDL_SOURCES += [
|
||||
"nsIWebAuthnArgs.idl",
|
||||
"nsIWebAuthnAttObj.idl",
|
||||
"nsIWebAuthnPromise.idl",
|
||||
"nsIWebAuthnResult.idl",
|
||||
"nsIWebAuthnService.idl",
|
||||
]
|
||||
|
||||
XPIDL_MODULE = "dom_webauthn"
|
||||
|
||||
|
@ -23,9 +29,9 @@ EXPORTS.mozilla.dom += [
|
|||
"AuthenticatorResponse.h",
|
||||
"PublicKeyCredential.h",
|
||||
"U2FTokenTransport.h",
|
||||
"WebAuthnController.h",
|
||||
"WebAuthnManager.h",
|
||||
"WebAuthnManagerBase.h",
|
||||
"WebAuthnPromiseHolder.h",
|
||||
"WebAuthnTransactionChild.h",
|
||||
"WebAuthnTransactionParent.h",
|
||||
"WebAuthnUtil.h",
|
||||
|
@ -36,12 +42,12 @@ UNIFIED_SOURCES += [
|
|||
"AuthenticatorAssertionResponse.cpp",
|
||||
"AuthenticatorAttestationResponse.cpp",
|
||||
"AuthenticatorResponse.cpp",
|
||||
"AuthrsTransport.cpp",
|
||||
"AuthrsService.cpp",
|
||||
"CtapArgs.cpp",
|
||||
"PublicKeyCredential.cpp",
|
||||
"WebAuthnController.cpp",
|
||||
"WebAuthnManager.cpp",
|
||||
"WebAuthnManagerBase.cpp",
|
||||
"WebAuthnPromiseHolder.cpp",
|
||||
"WebAuthnTransactionChild.cpp",
|
||||
"WebAuthnTransactionParent.cpp",
|
||||
"WebAuthnUtil.cpp",
|
||||
|
|
102
dom/webauthn/nsIWebAuthnArgs.idl
Normal file
102
dom/webauthn/nsIWebAuthnArgs.idl
Normal file
|
@ -0,0 +1,102 @@
|
|||
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* 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/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
typedef long COSEAlgorithmIdentifier;
|
||||
|
||||
// The nsICtapRegisterArgs interface encapsulates the arguments to the CTAP
|
||||
// authenticatorMakeCredential command as defined in
|
||||
// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential
|
||||
// It is essentially a shim that allows data to be copied from an IPDL-defined
|
||||
// WebAuthnMakeCredentialInfo C++ struct to an authenticator-rs defined
|
||||
// RegisterArgsCtap2 Rust struct.
|
||||
//
|
||||
[uuid(2fc8febe-a277-11ed-bda2-8f6495a5e75c)]
|
||||
interface nsICtapRegisterArgs : nsISupports {
|
||||
// TODO(Bug 1820035) The origin is only used for prompt callbacks. Refactor and remove.
|
||||
readonly attribute AString origin;
|
||||
|
||||
readonly attribute Array<octet> clientDataHash;
|
||||
|
||||
// A PublicKeyCredentialRpEntity
|
||||
readonly attribute AString rpId;
|
||||
[must_use] readonly attribute AString rpName;
|
||||
|
||||
// A PublicKeyCredentialUserEntity
|
||||
[must_use] readonly attribute Array<octet> userId;
|
||||
[must_use] readonly attribute AString userName;
|
||||
[must_use] readonly attribute AString userDisplayName;
|
||||
|
||||
// The spec defines this as a sequence<PublicKeyCredentialParameters>.
|
||||
// We require type = "public-key" and only serialize the alg fields.
|
||||
[must_use] readonly attribute Array<COSEAlgorithmIdentifier> coseAlgs;
|
||||
|
||||
// The spec defines this as a sequence<PublicKeyCredentialDescriptor>.
|
||||
// We only include the ID field, as the transport field is optional and we
|
||||
// can assume that the type is "public-key".
|
||||
readonly attribute Array<Array<octet> > excludeList;
|
||||
|
||||
// CTAP2 passes extensions in a CBOR map of extension identifier ->
|
||||
// WebAuthn AuthenticationExtensionsClientInputs. That's not feasible here.
|
||||
// So we define a getter for each supported extension input and use the
|
||||
// return code to signal presence.
|
||||
[must_use] readonly attribute bool credProps;
|
||||
[must_use] readonly attribute bool hmacCreateSecret;
|
||||
[must_use] readonly attribute bool minPinLength;
|
||||
|
||||
// Options.
|
||||
[must_use] readonly attribute AString residentKey;
|
||||
[must_use] readonly attribute AString userVerification;
|
||||
[must_use] readonly attribute AString authenticatorAttachment;
|
||||
|
||||
// This is the WebAuthn PublicKeyCredentialCreationOptions timeout.
|
||||
// Arguably we don't need to pass it through since WebAuthnController can
|
||||
// cancel transactions.
|
||||
readonly attribute uint32_t timeoutMS;
|
||||
|
||||
// This is the WebAuthn PublicKeyCredentialCreationOptions attestation.
|
||||
// We might overwrite the provided value with "none" if the user declines the
|
||||
// consent popup.
|
||||
[must_use] readonly attribute AString attestationConveyancePreference;
|
||||
};
|
||||
|
||||
// The nsICtapSignArgs interface encapsulates the arguments to the CTAP
|
||||
// authenticatorGetAssertion command as defined in
|
||||
// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetAssertion
|
||||
// It is essentially a shim that allows data to be copied from an IPDL-defined
|
||||
// WebAuthnGetAssertionInfo C++ struct to an authenticator-rs defined
|
||||
// SignArgsCtap2 Rust struct.
|
||||
//
|
||||
[uuid(2e621cf4-a277-11ed-ae00-bf41a54ef553)]
|
||||
interface nsICtapSignArgs : nsISupports {
|
||||
// TODO(Bug 1820035) The origin is only used for prompt callbacks. Refactor and remove.
|
||||
readonly attribute AString origin;
|
||||
|
||||
// The spec only asks for the ID field of a PublicKeyCredentialRpEntity here
|
||||
readonly attribute AString rpId;
|
||||
|
||||
readonly attribute Array<octet> clientDataHash;
|
||||
|
||||
// The spec defines this as a sequence<PublicKeyCredentialDescriptor>.
|
||||
// We only include the ID field, as the transport field is optional and we
|
||||
// can assume that the type is "public-key".
|
||||
readonly attribute Array<Array<octet> > allowList;
|
||||
|
||||
// CTAP2 passes extensions in a CBOR map of extension identifier ->
|
||||
// WebAuthn AuthenticationExtensionsClientInputs. That's not feasible here.
|
||||
// So we define a getter for each supported extension input and use the
|
||||
// return code to signal presence.
|
||||
[must_use] readonly attribute bool hmacCreateSecret;
|
||||
[must_use] readonly attribute AString appId;
|
||||
|
||||
// Options
|
||||
[must_use] readonly attribute AString userVerification;
|
||||
|
||||
// This is the WebAuthn PublicKeyCredentialCreationOptions timeout.
|
||||
// Arguably we don't need to pass it through since WebAuthnController can
|
||||
// cancel transactions.
|
||||
readonly attribute unsigned long timeoutMS;
|
||||
};
|
20
dom/webauthn/nsIWebAuthnAttObj.idl
Normal file
20
dom/webauthn/nsIWebAuthnAttObj.idl
Normal file
|
@ -0,0 +1,20 @@
|
|||
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* 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/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
#include "nsIWebAuthnArgs.idl"
|
||||
|
||||
[uuid(91e41be0-ed73-4a10-b55e-3312319bfddf)]
|
||||
interface nsIWebAuthnAttObj : nsISupports {
|
||||
// The serialied attestation object as defined in
|
||||
// https://www.w3.org/TR/webauthn-2/#sctn-attestation
|
||||
readonly attribute Array<octet> attestationObject;
|
||||
|
||||
readonly attribute Array<octet> authenticatorData;
|
||||
|
||||
readonly attribute Array<octet> publicKey;
|
||||
|
||||
readonly attribute COSEAlgorithmIdentifier publicKeyAlgorithm;
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue