Update On Thu Oct 5 20:53:07 CEST 2023

This commit is contained in:
github-action[bot] 2023-10-05 20:53:08 +02:00
parent 10755d3d3c
commit 9122d956af
1215 changed files with 74741 additions and 38293 deletions

View file

@ -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
View file

@ -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"

View 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"

View file

@ -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",
],
)

View file

@ -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("**"):

View file

@ -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"]

View 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())

View file

@ -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);
}
});
}

View 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)

View file

@ -0,0 +1 @@
comtypes==1.2.0

View file

@ -0,0 +1,9 @@
[DEFAULT]
subsuite = "a11y"
skip-if = [
"os != 'win'",
"headless",
]
support-files = ["head.js"]
["browser_role.js"]

View 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 }
);

View 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 }
);

View file

@ -0,0 +1,11 @@
[DEFAULT]
subsuite = "a11y"
skip-if = [
"os != 'win'",
"headless",
]
support-files = ["head.js"]
["browser_controlType.js"]
["browser_elementFromPoint.js"]

View 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 }
);

View file

@ -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"
);
}
);

View 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 }
);

View file

@ -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;

View file

@ -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);

View file

@ -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>

View file

@ -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") {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -222,6 +222,7 @@ serp:
`ad_sidebar`,
`ad_sitelink`,
`incontent_searchbox`,
`non_ads_link`,
`refined_search_buttons`,
`shopping_tab`.
type: string

View file

@ -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"]

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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();
});
}

View file

@ -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">

View file

@ -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 dont 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

View file

@ -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,

View file

@ -1,6 +1,6 @@
[package]
name = "terminal_size"
version = "0.2.999"
version = "0.3.999"
edition = "2018"
license = "MIT OR Apache-2.0"

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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"]

View file

@ -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`
);
}
}

View file

@ -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.");
}

View file

@ -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
});

View file

@ -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."
);
});

View file

@ -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"

View file

@ -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");

View file

@ -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}")`
);
});

View file

@ -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

View file

@ -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,

View file

@ -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);

View file

@ -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: {

View file

@ -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;

View file

@ -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
************************

View file

@ -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

View file

@ -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,

View file

@ -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();
});

View file

@ -23,6 +23,7 @@ types.addDictType("target-configuration.configuration", {
reloadOnTouchSimulationToggle: "nullable:boolean",
restoreFocus: "nullable:boolean",
serviceWorkersTestingEnabled: "nullable:boolean",
setTabOffline: "nullable:boolean",
touchEventsOverride: "nullable:string",
});

View file

@ -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

View file

@ -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();

View file

@ -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);

View file

@ -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

View file

@ -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",

View file

@ -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();

View file

@ -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();
}

View file

@ -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 {

View file

@ -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();
}

View file

@ -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.

View file

@ -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);

View file

@ -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.

View file

@ -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);
});
});
}

View file

@ -142,7 +142,7 @@ void AsyncEventDispatcher::RunDOMEventWhenSafe(
Composed::eDefault);
return;
}
(new AsyncEventDispatcher(&aTarget, &aEvent, aOnlyChromeDispatch))
(new AsyncEventDispatcher(&aTarget, do_AddRef(&aEvent), aOnlyChromeDispatch))
->RunDOMEventWhenSafe();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();

View file

@ -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
// ((tt0)/(t1t0)) 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);
}
}

View file

@ -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;

View file

@ -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
}

View file

@ -14,7 +14,6 @@ import requests
THIRDPARTY_USED_IN_FIREFOX = [
"abseil-cpp",
"google_benchmark",
"pffft",
"rnnoise",
]

View file

@ -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();
}

View file

@ -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)) {

View file

@ -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 {

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -9,7 +9,7 @@
#include "mozilla/dom/WebAuthnTransactionChild.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "nsIWebAuthnController.h"
#include "nsIWebAuthnArgs.h"
namespace mozilla::dom {

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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
}

View file

@ -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',
},
]

View file

@ -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",

View 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;
};

View 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