Skip to content

Commit

Permalink
Bug 1755104 - Make EditorEventListener::Focus check whether the eve…
Browse files Browse the repository at this point in the history
…nt target still has focus even after flushing the pending things r=m_kato

When the `focus` event listener of editors which is in the system group runs,
a preceding `focus` event listener may have already blurred the focused element,
but it may have not been applied to the DOM tree yet.  In this case, checking
whether the editor still has focus or has already blurred without flushing the
pending things does not make sense.  Therefore, this patch makes the `Focus`
do it first.

Note that this patch adds 3 crash tests, but only the `<textarea>` case crashes
without this patch.  The others are only for detecting new regressions.

Differential Revision: https://phabricator.services.mozilla.com/D139089
  • Loading branch information
masayuki-nakano committed Feb 21, 2022
1 parent f9fa9e0 commit 9874524
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 45 deletions.
104 changes: 59 additions & 45 deletions editor/libeditor/EditorEventListener.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1089,68 +1089,82 @@ nsresult EditorEventListener::Focus(InternalFocusEvent* aFocusEvent) {
return NS_OK;
}

RefPtr<EditorBase> editorBase(mEditorBase);

// Spell check a textarea the first time that it is focused.
SpellCheckIfNeeded();
if (DetachedFromEditor()) {
return NS_OK;
}

nsCOMPtr<nsINode> eventTargetNode =
nsCOMPtr<nsINode> originalEventTargetNode =
nsINode::FromEventTargetOrNull(aFocusEvent->GetOriginalDOMEventTarget());
if (NS_WARN_IF(!eventTargetNode)) {
if (MOZ_UNLIKELY(NS_WARN_IF(!originalEventTargetNode))) {
return NS_ERROR_UNEXPECTED;
}

// If the target is a document node but it's not editable, we should ignore
// it because actual focused element's event is going to come.
if (eventTargetNode->IsDocument() && !eventTargetNode->IsInDesignMode()) {
// If the target is a document node but it's not editable, we should
// ignore it because actual focused element's event is going to come.
if (originalEventTargetNode->IsDocument()) {
if (!originalEventTargetNode->IsInDesignMode()) {
return NS_OK;
}
}
// We should not receive focus events whose target is not a content node
// unless the node is a document node.
else if (MOZ_UNLIKELY(NS_WARN_IF(!originalEventTargetNode->IsContent()))) {
return NS_OK;
}

if (eventTargetNode->IsContent()) {
nsIContent* content =
eventTargetNode->AsContent()->FindFirstNonChromeOnlyAccessContent();
// XXX If the focus event target is a form control in contenteditable
// element, perhaps, the parent HTML editor should do nothing by this
// handler. However, FindSelectionRoot() returns the root element of the
// contenteditable editor. So, the editableRoot value is invalid for
// the plain text editor, and it will be set to the wrong limiter of
// the selection. However, fortunately, actual bugs are not found yet.
nsCOMPtr<nsIContent> editableRoot = editorBase->FindSelectionRoot(content);

// make sure that the element is really focused in case an earlier
// listener in the chain changed the focus.
if (editableRoot) {
nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
if (NS_WARN_IF(!focusManager)) {
return NS_OK;
}
RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
if (MOZ_UNLIKELY(NS_WARN_IF(!focusManager))) {
return NS_OK;
}

nsIContent* focusedContent = focusManager->GetFocusedElement();
if (!focusedContent) {
return NS_OK;
}
auto CanKeepHandlingFocusEvent = [&]() -> bool {
if (this->DetachedFromEditor()) {
return false;
}
// If the event target is document mode, we only need to handle the focus
// event when the document is still in designMode. Otherwise, the
// mode has been disabled by somebody while we're handling the focus event.
if (originalEventTargetNode->IsDocument()) {
return originalEventTargetNode->IsInDesignMode();
}
MOZ_ASSERT(originalEventTargetNode->IsContent());
// If nobody has focus, the focus event target has been blurred by somebody
// else. So the editor shouldn't initialize itself to start to handle
// anything.
if (!focusManager->GetFocusedElement()) {
return false;
}
const nsIContent* const exposedTargetContent =
originalEventTargetNode->AsContent()
->FindFirstNonChromeOnlyAccessContent();
const nsIContent* const exposedFocusedContent =
focusManager->GetFocusedElement()
->FindFirstNonChromeOnlyAccessContent();
return exposedTargetContent && exposedFocusedContent &&
exposedTargetContent == exposedFocusedContent;
};

nsCOMPtr<nsIContent> originalTargetAsContent =
do_QueryInterface(aFocusEvent->GetOriginalDOMEventTarget());
RefPtr<PresShell> presShell = GetPresShell();
if (MOZ_UNLIKELY(NS_WARN_IF(!presShell))) {
return NS_OK;
}
// Let's update the layout information right now because there are some
// pending notifications and flushing them may cause destroying the editor.
presShell->FlushPendingNotifications(FlushType::Layout);
if (MOZ_UNLIKELY(!CanKeepHandlingFocusEvent())) {
return NS_OK;
}

if (!SameCOMIdentity(
focusedContent->FindFirstNonChromeOnlyAccessContent(),
originalTargetAsContent->FindFirstNonChromeOnlyAccessContent())) {
return NS_OK;
}
}
// Spell check a textarea the first time that it is focused.
SpellCheckIfNeeded();
if (MOZ_UNLIKELY(!CanKeepHandlingFocusEvent())) {
return NS_OK;
}

editorBase->OnFocus(*eventTargetNode);
RefPtr<EditorBase> editorBase(mEditorBase);
editorBase->OnFocus(*originalEventTargetNode);
if (DetachedFromEditorOrDefaultPrevented(aFocusEvent)) {
return NS_OK;
}

RefPtr<nsPresContext> presContext = GetPresContext();
if (NS_WARN_IF(!presContext)) {
if (MOZ_UNLIKELY(NS_WARN_IF(!presContext))) {
return NS_OK;
}
nsCOMPtr<nsIContent> focusedContent = editorBase->GetFocusedContent();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html class="test-wait">
<meta charset="utf-8">
<script>
addEventListener("load", () => {
const editingHost = document.querySelector("div[contenteditable]");
editingHost.addEventListener("focus", () => {
document.execCommand("insertText", false, "def");
editingHost.parentElement.setAttribute("hidden", "hidden");
setTimeout(() => document.documentElement.removeAttribute("class"), 0);
});
editingHost.focus();
});
</script>
<div><div contenteditable>abc</div></div>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html class="test-wait">
<meta charset="utf-8">
<script>
addEventListener("load", () => {
const parentDocument = document;
const iframe = parentDocument.querySelector("iframe");
iframe.contentDocument.designMode = "on";
iframe.contentWindow.addEventListener("focus", () => {
iframe.contentDocument.execCommand("insertText", false, "def");
iframe.parentElement.setAttribute("hidden", "hidden");
setTimeout(() => parentDocument.documentElement.removeAttribute("class"), 0);
});
iframe.contentWindow.focus();
});
</script>
<div><iframe srcdoc="<div>abc</div>"></iframe></div>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html class="test-wait">
<meta charset="utf-8">
<script>
addEventListener("load", () => {
const textarea = document.querySelector("textarea");
textarea.addEventListener("focus", () => {
textarea.select();
textarea.parentElement.setAttribute("hidden", "hidden");
setTimeout(() => document.documentElement.removeAttribute("class"), 0);
});
textarea.focus();
});
</script>
<div><textarea>abc</textarea></div>
</html>
3 changes: 3 additions & 0 deletions testing/web-platform/tests/lint.ignore
Original file line number Diff line number Diff line change
Expand Up @@ -813,8 +813,11 @@ CSS-COLLIDING-REF-NAME: css/css-break/background-image-001-ref.html

# Ported crashtests from Mozilla
SET TIMEOUT: editing/crashtests/backcolor-in-nested-editing-host-td-from-DOMAttrModified.html
SET TIMEOUT: editing/crashtests/contenteditable-will-be-blurred-by-focus-event-listener.html
SET TIMEOUT: editing/crashtests/designMode-document-will-be-blurred-by-focus-event-listener.html
SET TIMEOUT: editing/crashtests/inserthtml-after-temporarily-removing-document-element.html
SET TIMEOUT: editing/crashtests/inserthtml-in-text-adopted-to-other-document.html
SET TIMEOUT: editing/crashtests/insertorderedlist-in-text-adopted-to-other-document.html
SET TIMEOUT: editing/crashtests/make-editable-div-inline-and-set-contenteditable-of-input-to-false.html
SET TIMEOUT: editing/crashtests/outdent-across-svg-boundary.html
SET TIMEOUT: editing/crashtests/textarea-will-be-blurred-by-focus-event-listener.html

0 comments on commit 9874524

Please sign in to comment.