Prevent check failures in AXSelection resulting from DOM mutations
When AXSelection::Select is called to set the caret in an unfocused
input, FrameSelection::DidSetSelectionDeprecated focuses the node by
default. If this focus event happens to trigger a DOM mutation, the DOM
tree version associated with the selection will not match the version
associated with the document. As a result, when AXSelection::Select
goes to cache the selection range, the DCHECK for this condition in
SelectionTemplate<Strategy>::AssertValid fails.
We can stop this condition from happening by using the same logic in
FrameSelection::SetSelection, namely call SetSelectionDeprecated and
if true, then call DidSetSelectionDeprecated -- but only after we have
cached the selection range.
If instead the DOM mutation is a consequence of the selection itself,
there will be a similar check failures, both for the DOM tree version
and the fact that layout is needed. Therefore, if a layout is pending
immediately after we dispatch the selection start, update the layout,
the AXPositions, and our stored DOM tree and Style versions.
AX-Relnotes: N/A
Change-Id: Ifbd63744d77db10b8895905a130b0b901cad3e0c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2346329
Commit-Queue: Joanmarie Diggs <jdiggs@igalia.com>
Reviewed-by: Yoshifumi Inoue <yosin@chromium.org>
Reviewed-by: Nektarios Paisios <nektar@chromium.org>
Cr-Commit-Position: refs/heads/master@{#803824}
diff --git a/content/browser/accessibility/accessibility_auralinux_browsertest.cc b/content/browser/accessibility/accessibility_auralinux_browsertest.cc
index 657d4b4..3c9e3f4 100644
--- a/content/browser/accessibility/accessibility_auralinux_browsertest.cc
+++ b/content/browser/accessibility/accessibility_auralinux_browsertest.cc
@@ -43,6 +43,13 @@
return result;
}
+static bool IsAtkObjectEditable(AtkObject* object) {
+ AtkStateSet* state_set = atk_object_ref_state_set(object);
+ bool result = atk_state_set_contains_state(state_set, ATK_STATE_EDITABLE);
+ g_object_unref(state_set);
+ return result;
+}
+
} // namespace
class AccessibilityAuraLinuxBrowserTest : public AccessibilityBrowserTest {
@@ -1565,6 +1572,170 @@
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
+ SelectionTriggersReparentingOnSelectionStart) {
+ LoadInitialAccessibilityTreeFromHtml(
+ R"HTML(<!DOCTYPE html>
+ <html>
+ <head>
+ <script>
+ document.onselectstart = () => {
+ var tomove = document.getElementById("tomove");
+ document.getElementById("div").appendChild(tomove);
+ }
+ </script>
+ </head>
+ <body>
+ <div id="tomove">Move me</div>
+ <p id="paragraph">hello world</p>
+ <div id="div"></div>
+ </body>
+ </html>)HTML");
+
+ AtkObject* document = GetRendererAccessible();
+ AtkObject* paragraph = atk_object_ref_accessible_child(document, 1);
+ ASSERT_EQ(atk_object_get_role(paragraph), ATK_ROLE_PARAGRAPH);
+
+ AccessibilityNotificationWaiter waiter(
+ shell()->web_contents(), ui::kAXModeComplete,
+ ax::mojom::Event::kTextSelectionChanged);
+
+ EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(paragraph), 0, 0, 5));
+ waiter.WaitForNotification();
+
+ gchar* selected =
+ atk_text_get_selection(ATK_TEXT(paragraph), 0, nullptr, nullptr);
+ EXPECT_STREQ(selected, "hello");
+ g_free(selected);
+
+ g_object_unref(paragraph);
+}
+
+IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
+ SelectionTriggersAnchorDeletionOnSelectionStart) {
+ LoadInitialAccessibilityTreeFromHtml(
+ R"HTML(<!DOCTYPE html>
+ <html>
+ <head>
+ <script>
+ document.onselectstart = () => {
+ document.getElementById("p").removeChild(p.childNodes[0]);
+ }
+ </script>
+ </head>
+ <body>
+ <p id="p"><span>hello</span> <span>world</span></p>
+ <button id="button">ok</button>
+ </body>
+ </html>)HTML");
+
+ AtkObject* document = GetRendererAccessible();
+ AtkObject* paragraph = atk_object_ref_accessible_child(document, 0);
+ ASSERT_EQ(atk_object_get_role(paragraph), ATK_ROLE_PARAGRAPH);
+
+ AtkObject* button = atk_object_ref_accessible_child(document, 1);
+ ASSERT_EQ(atk_object_get_role(button), ATK_ROLE_PUSH_BUTTON);
+
+ AccessibilityNotificationWaiter waiter(
+ shell()->web_contents(), ui::kAXModeComplete, ax::mojom::Event::kFocus);
+
+ EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(paragraph), 0, 0, 11));
+ atk_component_grab_focus(ATK_COMPONENT(button));
+ waiter.WaitForNotification();
+
+ gchar* selected =
+ atk_text_get_selection(ATK_TEXT(paragraph), 0, nullptr, nullptr);
+ EXPECT_STREQ(selected, nullptr);
+ g_free(selected);
+
+ g_object_unref(paragraph);
+ g_object_unref(button);
+}
+
+IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
+ SelectionTriggersFocusDeletionOnSelectionStart) {
+ LoadInitialAccessibilityTreeFromHtml(
+ R"HTML(<!DOCTYPE html>
+ <head>
+ <script>
+ document.onselectstart = () => {
+ document.getElementById("p").removeChild(p.childNodes[2]);
+ }
+ </script>
+ </head>
+ <body>
+ <p id="p"><span>hello</span> <span>world</span></p>
+ <button id="button">ok</button>
+ </body>
+ </html>)HTML");
+
+ AtkObject* document = GetRendererAccessible();
+ AtkObject* paragraph = atk_object_ref_accessible_child(document, 0);
+ ASSERT_EQ(atk_object_get_role(paragraph), ATK_ROLE_PARAGRAPH);
+
+ AtkObject* button = atk_object_ref_accessible_child(document, 1);
+ ASSERT_EQ(atk_object_get_role(button), ATK_ROLE_PUSH_BUTTON);
+
+ AccessibilityNotificationWaiter waiter(
+ shell()->web_contents(), ui::kAXModeComplete, ax::mojom::Event::kFocus);
+
+ EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(paragraph), 0, 0, 11));
+ atk_component_grab_focus(ATK_COMPONENT(button));
+ waiter.WaitForNotification();
+
+ gchar* selected =
+ atk_text_get_selection(ATK_TEXT(paragraph), 0, nullptr, nullptr);
+ EXPECT_STREQ(selected, nullptr);
+ g_free(selected);
+
+ g_object_unref(paragraph);
+ g_object_unref(button);
+}
+
+IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
+ SelectionTriggersReparentingOnFocus) {
+ LoadInitialAccessibilityTreeFromHtml(
+ R"HTML(<!DOCTYPE html>
+ <html>
+ <head>
+ <script>
+ function go() {
+ var edit = document.getElementById("edit");
+ document.getElementById("search").appendChild(edit);
+ }
+ </script>
+ </head>
+ <body>
+ <span id="edit" tabindex="0" contenteditable onfocusin="go()">foo</span>
+ <div id="search" role="search"></div>
+ </body>
+ </html>)HTML");
+
+ AtkObject* document = GetRendererAccessible();
+ AtkObject* section = atk_object_ref_accessible_child(document, 0);
+ AtkObject* edit = atk_object_ref_accessible_child(section, 0);
+ ASSERT_TRUE(IsAtkObjectEditable(edit));
+ ASSERT_FALSE(IsAtkObjectFocused(edit));
+
+ AccessibilityNotificationWaiter waiter(
+ shell()->web_contents(), ui::kAXModeComplete,
+ ax::mojom::Event::kTextSelectionChanged);
+
+ EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(edit), 0, 1, 2));
+ waiter.WaitForNotification();
+
+ // When the unfocused contenteditable span has its selection set, focus will
+ // be set. That will trigger the script in the source to move that span to
+ // a different parent, causing focus to be removed and the selection cleared.
+ ASSERT_FALSE(IsAtkObjectFocused(edit));
+ gchar* selected = atk_text_get_selection(ATK_TEXT(edit), 0, nullptr, nullptr);
+ EXPECT_STREQ(selected, nullptr);
+ g_free(selected);
+
+ g_object_unref(edit);
+ g_object_unref(section);
+}
+
+IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestFocusInputTextFields) {
auto verify_selection = [](AtkObject* object, const char* selection) {
gchar* selected_text =
diff --git a/third_party/blink/renderer/core/editing/frame_selection.cc b/third_party/blink/renderer/core/editing/frame_selection.cc
index 2333dc6a..3da05862 100644
--- a/third_party/blink/renderer/core/editing/frame_selection.cc
+++ b/third_party/blink/renderer/core/editing/frame_selection.cc
@@ -349,6 +349,17 @@
TaskType::kMiscPlatformAPI);
}
+void FrameSelection::SetSelectionForAccessibility(
+ const SelectionInDOMTree& selection,
+ const SetSelectionOptions& options) {
+ ClearDocumentCachedRange();
+
+ const bool did_set = SetSelectionDeprecated(selection, options);
+ CacheRangeOfDocument(CreateRange(selection.ComputeRange()));
+ if (did_set)
+ DidSetSelectionDeprecated(selection, options);
+}
+
void FrameSelection::NodeChildrenWillBeRemoved(ContainerNode& container) {
if (!container.InActiveDocument())
return;
diff --git a/third_party/blink/renderer/core/editing/frame_selection.h b/third_party/blink/renderer/core/editing/frame_selection.h
index 69cebad..43d64d19 100644
--- a/third_party/blink/renderer/core/editing/frame_selection.h
+++ b/third_party/blink/renderer/core/editing/frame_selection.h
@@ -166,6 +166,8 @@
const SetSelectionOptions&);
void DidSetSelectionDeprecated(const SelectionInDOMTree&,
const SetSelectionOptions&);
+ void SetSelectionForAccessibility(const SelectionInDOMTree&,
+ const SetSelectionOptions&);
// Call this after doing user-triggered selections to make it easy to delete
// the frame you entirely selected.
diff --git a/third_party/blink/renderer/modules/accessibility/ax_position.h b/third_party/blink/renderer/modules/accessibility/ax_position.h
index 58c23bf..e6d1726 100644
--- a/third_party/blink/renderer/modules/accessibility/ax_position.h
+++ b/third_party/blink/renderer/modules/accessibility/ax_position.h
@@ -191,6 +191,8 @@
uint64_t dom_tree_version_;
uint64_t style_version_;
#endif
+
+ friend class AXSelection;
};
MODULES_EXPORT bool operator==(const AXPosition&, const AXPosition&);
diff --git a/third_party/blink/renderer/modules/accessibility/ax_selection.cc b/third_party/blink/renderer/modules/accessibility/ax_selection.cc
index ad1745a..3a1f49d9 100644
--- a/third_party/blink/renderer/modules/accessibility/ax_selection.cc
+++ b/third_party/blink/renderer/modules/accessibility/ax_selection.cc
@@ -7,6 +7,7 @@
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
@@ -329,6 +330,23 @@
return selection_builder.Build();
}
+void AXSelection::UpdateSelectionIfNecessary() {
+ Document* document = base_.ContainerObject()->GetDocument();
+ DCHECK(document);
+
+ LocalFrameView* view = document->View();
+ if (!view || !view->LayoutPending())
+ return;
+
+ document->UpdateStyleAndLayout(DocumentUpdateReason::kSelection);
+#if DCHECK_IS_ON()
+ base_.dom_tree_version_ = extent_.dom_tree_version_ = dom_tree_version_ =
+ document->DomTreeVersion();
+ base_.style_version_ = extent_.style_version_ = style_version_ =
+ document->StyleVersion();
+#endif // DCHECK_IS_ON()
+}
+
bool AXSelection::Select(const AXSelectionBehavior selection_behavior) {
if (!IsValid()) {
NOTREACHED() << "Trying to select an invalid accessibility selection.";
@@ -354,9 +372,9 @@
return true;
}
- const SelectionInDOMTree selection = AsSelection(selection_behavior);
- DCHECK(selection.AssertValid());
- Document* document = selection.Base().GetDocument();
+ const SelectionInDOMTree old_selection = AsSelection(selection_behavior);
+ DCHECK(old_selection.AssertValid());
+ Document* document = old_selection.Base().GetDocument();
if (!document) {
NOTREACHED() << "Valid DOM selections should have an attached document.";
return false;
@@ -374,46 +392,36 @@
// See the following section in the Selection API Specification:
// https://w3c.github.io/selection-api/#selectstart-event
- if (DispatchSelectStart(selection.Extent().ComputeContainerNode()) !=
+ if (DispatchSelectStart(old_selection.Base().ComputeContainerNode()) !=
DispatchEventResult::kNotCanceled) {
return false;
}
+ // If the anchor or focus is removed during the "selectstart" event, do
+ // not proceed.
+ if (base_.ContainerObject()->IsDetached() ||
+ extent_.ContainerObject()->IsDetached())
+ return false;
+
+ UpdateSelectionIfNecessary();
+
+ // Dispatching the "selectstart" event could potentially change the document
+ // associated with the current frame.
+ if (!frame_selection.IsAvailable())
+ return false;
+
+ // Re-retrieve the SelectionInDOMTree in case a DOM mutation took place.
+ // That way it will also have the updated DOM tree and Style versions,
+ // and the SelectionTemplate checks for each won't fail.
+ const SelectionInDOMTree selection = AsSelection(selection_behavior);
+
SetSelectionOptions::Builder options_builder;
options_builder.SetIsDirectional(true)
.SetShouldCloseTyping(true)
.SetShouldClearTypingStyle(true)
.SetSetSelectionBy(SetSelectionBy::kUser);
- frame_selection.ClearDocumentCachedRange();
- frame_selection.SetSelection(selection, options_builder.Build());
-
- // Cache the newly created document range. This doesn't affect the already
- // applied selection. Note that DOM's |Range| object has a start and an end
- // container that need to be in DOM order. See the DOM specification for more
- // information: https://dom.spec.whatwg.org/#interface-range
- Range* range = Range::Create(*document);
- if (selection.Extent().IsNull()) {
- DCHECK(selection.Base().IsNotNull())
- << "AX selections converted to DOM selections should have at least one "
- "endpoint non-null.\n"
- << *this << '\n'
- << selection;
- range->setStart(selection.Base().ComputeContainerNode(),
- selection.Base().ComputeOffsetInContainerNode());
- range->setEnd(selection.Base().ComputeContainerNode(),
- selection.Base().ComputeOffsetInContainerNode());
- } else if (selection.Base() < selection.Extent()) {
- range->setStart(selection.Base().ComputeContainerNode(),
- selection.Base().ComputeOffsetInContainerNode());
- range->setEnd(selection.Extent().ComputeContainerNode(),
- selection.Extent().ComputeOffsetInContainerNode());
- } else {
- range->setStart(selection.Extent().ComputeContainerNode(),
- selection.Extent().ComputeOffsetInContainerNode());
- range->setEnd(selection.Base().ComputeContainerNode(),
- selection.Base().ComputeOffsetInContainerNode());
- }
- frame_selection.CacheRangeOfDocument(range);
+ frame_selection.SetSelectionForAccessibility(selection,
+ options_builder.Build());
return true;
}
diff --git a/third_party/blink/renderer/modules/accessibility/ax_selection.h b/third_party/blink/renderer/modules/accessibility/ax_selection.h
index 8f3a5e4..713e6b8 100644
--- a/third_party/blink/renderer/modules/accessibility/ax_selection.h
+++ b/third_party/blink/renderer/modules/accessibility/ax_selection.h
@@ -94,6 +94,10 @@
AXSelection();
+ // If a layout is pending, update the style and layout along with the DOM
+ // tree and style versions of the AXSelection and associated AXPositions.
+ void UpdateSelectionIfNecessary();
+
// Determines whether this selection is targeted to the contents of a text
// field, and returns the start and end text offsets, as well as its
// direction. |start| should always be less than equal to |end|.