Bug 1760160 - Make ContentCacheInChild stop storing content if editable element has already been blurred r=m_kato

It's designed for caching content information of focused editor.  However, at
sending focus notification to the main process, the editor may have already
been blurred but `IMEContentObserver` may have not known it yet.  In this edge
case, only query selection succeeds since IMEContentObserver still has a cache,
but query the others failed because of root content node check of
`IMEContentObserver::HandleQueryContentEvent`.  If a content process meets this
case, it should not send focus notification and stop storing the content since
IME shouldn't get focus nor query non-editable content.

On the other hand, the reported testcase reproduces this with a fuzzing API
called **in** the content process. Therefore, I have no idea how to reproduce
it without the API. That's the reason why this patch does not contain new tests.

Differential Revision: https://phabricator.services.mozilla.com/D141821
This commit is contained in:
Masayuki Nakano 2022-04-07 00:58:49 +00:00
parent 281f40ed71
commit 91e18f14d8
5 changed files with 65 additions and 13 deletions

View File

@ -430,6 +430,8 @@ nsresult ContentEventHandler::Init(WidgetQueryContentEvent* aEvent) {
aEvent->EmplaceReply();
aEvent->mReply->mContentsRoot = mRootContent.get();
aEvent->mReply->mIsEditableContent =
mRootContent && mRootContent->IsEditable();
nsRect r;
nsIFrame* frame = nsCaret::GetGeometry(mSelection, &r);

View File

@ -607,6 +607,12 @@ nsresult IMEContentObserver::HandleQueryContentEvent(
}
aEvent->mReply->mContentsRoot = mRootContent;
aEvent->mReply->mWritingMode = mSelectionData.GetWritingMode();
// The selection cache in IMEContentObserver must always have been in
// an editing host (or an editable annoymous <div> element). Therefore,
// we set mIsEditableContent to true here even though it's already been
// blurred or changed its editable state but the selection cache has not
// been invalidated yet.
aEvent->mReply->mIsEditableContent = true;
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p HandleQueryContentEvent(aEvent={ "
"mMessage=%s, mReply=%s })",

View File

@ -133,6 +133,20 @@ bool ContentCacheInChild::CacheSelection(nsIWidget* aWidget,
sContentCacheLog, LogLevel::Error,
("0x%p CacheSelection(), FAILED, couldn't retrieve the selected text",
this));
}
// ContentCache should store only editable content. Therefore, if current
// selection root is not editable, we don't need to store the selection, i.e.,
// let's treat it as there is no selection. However, if we already have
// previously editable text, let's store the selection even if it becomes
// uneditable because not doing so would create odd situation. E.g., IME may
// fail only querying selection after succeeded querying text.
else if (NS_WARN_IF(mText.isNothing() &&
!querySelectedTextEvent.mReply->mIsEditableContent)) {
MOZ_LOG(sContentCacheLog, LogLevel::Error,
("0x%p CacheSelection(), FAILED, editable content had already been "
"blurred",
this));
return false;
} else {
mSelection.emplace(querySelectedTextEvent);
}
@ -193,6 +207,16 @@ bool ContentCacheInChild::CacheEditorRect(
this));
return false;
}
// ContentCache should store only editable content. Therefore, if current
// selection root is not editable, we don't need to store the editor rect,
// i.e., let's treat it as there is no focused editor.
if (NS_WARN_IF(!queryEditorRectEvent.mReply->mIsEditableContent)) {
MOZ_LOG(sContentCacheLog, LogLevel::Error,
("0x%p CacheText(), FAILED, editable content had already been "
"blurred",
this));
return false;
}
mEditorRect = queryEditorRectEvent.mReply->mRect;
MOZ_LOG(sContentCacheLog, LogLevel::Info,
("0x%p CacheEditorRect(), Succeeded, mEditorRect=%s", this,
@ -215,6 +239,16 @@ bool ContentCacheInChild::CacheText(nsIWidget* aWidget,
MOZ_LOG(sContentCacheLog, LogLevel::Error,
("0x%p CacheText(), FAILED, couldn't retrieve whole text", this));
mText.reset();
}
// ContentCache should store only editable content. Therefore, if current
// selection root is not editable, we don't need to store the text, i.e.,
// let's treat it as there is no editable text.
else if (NS_WARN_IF(!queryTextContentEvent.mReply->mIsEditableContent)) {
MOZ_LOG(sContentCacheLog, LogLevel::Error,
("0x%p CacheText(), FAILED, editable content had already been "
"blurred",
this));
mText.reset();
} else {
mText = Some(nsString(queryTextContentEvent.mReply->DataRef()));
MOZ_LOG(sContentCacheLog, LogLevel::Info,
@ -241,8 +275,18 @@ bool ContentCacheInChild::CacheText(nsIWidget* aWidget,
mLastCommit.reset();
}
const bool selectionCached = CacheSelection(aWidget, aNotification);
return selectionCached || queryTextContentEvent.Succeeded();
// If we fail to get editable text content, it must mean that there is no
// focused element anymore or focused element is not editable. In this case,
// we should not get selection of non-editable content
if (MOZ_UNLIKELY(queryTextContentEvent.Failed() ||
!queryTextContentEvent.mReply->mIsEditableContent)) {
mSelection.reset();
mCaret.reset();
mTextRectArray.reset();
return false;
}
return CacheSelection(aWidget, aNotification);
}
bool ContentCacheInChild::QueryCharRect(nsIWidget* aWidget, uint32_t aOffset,

View File

@ -755,7 +755,8 @@ nsresult PuppetWidget::NotifyIMEOfFocusChange(
bool gotFocus = aIMENotification.mMessage == NOTIFY_IME_OF_FOCUS;
if (gotFocus) {
// When IME gets focus, we should initialize all information of the
// content.
// content, however, it may fail to get it because the editor may have
// already been blurred.
if (NS_WARN_IF(!mContentCache.CacheAll(this, &aIMENotification))) {
return NS_ERROR_FAILURE;
}

View File

@ -1146,7 +1146,7 @@ class WidgetQueryContentEvent : public WidgetGUIEvent {
struct Reply final {
EventMessage const mEventMessage;
void* mContentsRoot;
void* mContentsRoot = nullptr;
Maybe<OffsetAndData<uint32_t>> mOffsetAndData;
// mTentativeCaretOffset is used by only eQueryCharacterAtPoint.
// This is the offset where caret would be if user clicked at the mRefPoint.
@ -1158,7 +1158,7 @@ class WidgetQueryContentEvent : public WidgetGUIEvent {
// to the owner window, not the <xul:panel>.
mozilla::LayoutDeviceIntRect mRect;
// The return widget has the caret. This is set at all query events.
nsIWidget* mFocusedWidget;
nsIWidget* mFocusedWidget = nullptr;
// mozilla::WritingMode value at the end (focus) of the selection
mozilla::WritingMode mWritingMode;
// Used by eQuerySelectionAsTransferable
@ -1168,17 +1168,14 @@ class WidgetQueryContentEvent : public WidgetGUIEvent {
// Used by eQueryTextRectArray
CopyableTArray<mozilla::LayoutDeviceIntRect> mRectArray;
// true if selection is reversed (end < start)
bool mReversed;
bool mReversed = false;
// true if DOM element under mouse belongs to widget
bool mWidgetIsHit;
bool mWidgetIsHit = false;
// true if mContentRoot is focused editable content
bool mIsEditableContent = false;
Reply() = delete;
explicit Reply(EventMessage aEventMessage)
: mEventMessage(aEventMessage),
mContentsRoot(nullptr),
mFocusedWidget(nullptr),
mReversed(false),
mWidgetIsHit(false) {}
explicit Reply(EventMessage aEventMessage) : mEventMessage(aEventMessage) {}
// Don't allow to copy/move because of `mEventMessage`.
Reply(const Reply& aOther) = delete;
@ -1279,6 +1276,8 @@ class WidgetQueryContentEvent : public WidgetGUIEvent {
aStream << ", mWritingMode=" << ToString(aReply.mWritingMode).c_str();
}
aStream << ", mContentsRoot=0x" << aReply.mContentsRoot
<< ", mIsEditableContent="
<< (aReply.mIsEditableContent ? "true" : "false")
<< ", mFocusedWidget=0x" << aReply.mFocusedWidget;
if (aReply.mEventMessage == eQueryTextContent) {
aStream << ", mFontRanges={ Length()=" << aReply.mFontRanges.Length()