/* 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 "HTMLEditor.h" #include "EditAction.h" #include "EditorDOMAPIWrapper.h" #include "EditorUtils.h" #include "HTMLEditorNestedClasses.h" #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/DebugOnly.h" #include "mozilla/IMEStateManager.h" #include "mozilla/Logging.h" #include "mozilla/Maybe.h" #include "mozilla/RefPtr.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/Element.h" #include "mozInlineSpellChecker.h" #include "nsContentUtils.h" #include "nsIContent.h" #include "nsIContentInlines.h" #include "nsIMutationObserver.h" #include "nsINode.h" #include "nsRange.h" #include "nsThreadUtils.h" namespace mozilla { using namespace dom; /****************************************************************************** * DOM mutation logger ******************************************************************************/ // - HTMLEditorMutation:3: Logging only mutations in editable containers which // is not expected. // - HTMLEditorMutation:4: Logging only mutations in editable containers which // is either expected or not expected. // - HTMLEditorMutation:5: Logging any mutations including in // non-editable containers. LazyLogModule gHTMLEditorMutationLog("HTMLEditorMutation"); LogLevel HTMLEditor::MutationLogLevelOf( nsIContent* aContent, const CharacterDataChangeInfo* aCharacterDataChangeInfo, DOMMutationType aDOMMutationType) const { // Should be called only when the "info" level is enabled at least since we // shouldn't add any new unnecessary calls in the hot paths when the logging // is disabled. MOZ_ASSERT(MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)); if (MOZ_UNLIKELY(!aContent->IsInComposedDoc())) { return LogLevel::Disabled; } Element* const containerElement = aContent->GetAsElementOrParentElement(); if (!containerElement || !containerElement->IsEditable()) { return MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Verbose) ? LogLevel::Verbose : LogLevel::Disabled; } if (!mRunningDOMAPIWrapper) { return LogLevel::Info; } switch (aDOMMutationType) { case DOMMutationType::ContentAppended: return mRunningDOMAPIWrapper->IsExpectedContentAppended(aContent) ? LogLevel::Debug : LogLevel::Info; case DOMMutationType::ContentInserted: return mRunningDOMAPIWrapper->IsExpectedContentInserted(aContent) ? LogLevel::Debug : LogLevel::Info; case DOMMutationType::ContentWillBeRemoved: return mRunningDOMAPIWrapper->IsExpectedContentWillBeRemoved(aContent) ? LogLevel::Debug : LogLevel::Info; case DOMMutationType::CharacterDataChanged: MOZ_ASSERT(aCharacterDataChangeInfo); return mRunningDOMAPIWrapper->IsExpectedCharacterDataChanged( aContent, *aCharacterDataChangeInfo) ? LogLevel::Debug : LogLevel::Info; default: MOZ_ASSERT_UNREACHABLE("Invalid DOMMutationType value"); return LogLevel::Disabled; } } // - HTMLEditorAttrMutation:3: Logging only mutations of editable element which // is not expected. // - HTMLEditorAttrMutation:4: Logging only mutations of editable element which // is either expected or not expected. // - HTMLEditorAttrMutation:5: Logging any mutations including non-editable // elements' attributes. LazyLogModule gHTMLEditorAttrMutationLog("HTMLEditorAttrMutation"); LogLevel HTMLEditor::AttrMutationLogLevelOf( Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, AttrModType aModType, const nsAttrValue* aOldValue) const { // Should be called only when the "info" level is enabled at least since we // shouldn't add any new unnecessary calls in the hot paths when the logging // is disabled. MOZ_ASSERT(MOZ_LOG_TEST(gHTMLEditorAttrMutationLog, LogLevel::Info)); if (MOZ_UNLIKELY(!aElement->IsInComposedDoc())) { return LogLevel::Disabled; } if (!aElement->IsEditable()) { return MOZ_LOG_TEST(gHTMLEditorAttrMutationLog, LogLevel::Verbose) ? LogLevel::Verbose : LogLevel::Disabled; } if (!mRunningDOMAPIWrapper) { return LogLevel::Info; } return mRunningDOMAPIWrapper->IsExpectedAttributeChanged( aElement, aNameSpaceID, aAttribute, aModType, aOldValue) ? LogLevel::Debug : LogLevel::Info; } void HTMLEditor::MaybeLogContentAppended(nsIContent* aFirstNewContent) const { const LogLevel logLevel = MutationLogLevelOf( aFirstNewContent, nullptr, DOMMutationType::ContentAppended); if (logLevel == LogLevel::Disabled) { return; } MOZ_LOG( gHTMLEditorMutationLog, logLevel, ("%p %s ContentAppended: %s (previousSibling=%s, nextSibling=%s)", this, logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse", NodeToString(aFirstNewContent).get(), NodeToString(aFirstNewContent ? aFirstNewContent->GetPreviousSibling() : nullptr) .get(), NodeToString(aFirstNewContent ? aFirstNewContent->GetNextSibling() : nullptr) .get())); } void HTMLEditor::MaybeLogContentInserted(nsIContent* aChild) const { const LogLevel logLevel = MutationLogLevelOf(aChild, nullptr, DOMMutationType::ContentInserted); if (logLevel == LogLevel::Disabled) { return; } MOZ_LOG(gHTMLEditorMutationLog, logLevel, ("%p %s ContentInserted: %s (previousSibling=%s, nextSibling=%s)", this, logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse", NodeToString(aChild).get(), NodeToString(aChild ? aChild->GetPreviousSibling() : nullptr).get(), NodeToString(aChild ? aChild->GetNextSibling() : nullptr).get())); } void HTMLEditor::MaybeLogContentWillBeRemoved(nsIContent* aChild) const { const LogLevel logLevel = MutationLogLevelOf( aChild, nullptr, DOMMutationType::ContentWillBeRemoved); if (logLevel == LogLevel::Disabled) { return; } MOZ_LOG( gHTMLEditorMutationLog, logLevel, ("%p %s ContentWillBeRemoved: %s (previousSibling=%s, nextSibling=%s)", this, logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse", NodeToString(aChild).get(), NodeToString(aChild ? aChild->GetPreviousSibling() : nullptr).get(), NodeToString(aChild ? aChild->GetNextSibling() : nullptr).get())); } void HTMLEditor::MaybeLogCharacterDataChanged( nsIContent* aContent, const CharacterDataChangeInfo& aInfo) const { const LogLevel logLevel = MutationLogLevelOf( aContent, &aInfo, DOMMutationType::CharacterDataChanged); if (logLevel == LogLevel::Disabled) { return; } nsAutoString data; aContent->GetCharacterDataBuffer()->AppendTo(data); MarkSelectionAndShrinkLongString shrunkenData( data, aInfo.mChangeStart, aInfo.mChangeStart + aInfo.mReplaceLength); MakeHumanFriendly(shrunkenData); MOZ_LOG( gHTMLEditorMutationLog, logLevel, ("%p %s CharacterDataChanged: %s, data=\"%s\", (length=%u), aInfo=%s", this, logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse", ToString(*aContent).c_str(), NS_ConvertUTF16toUTF8(shrunkenData).get(), aContent->Length(), ToString(aInfo).c_str())); } void HTMLEditor::MaybeLogAttributeChanged(Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, AttrModType aModType, const nsAttrValue* aOldValue) const { const LogLevel logLevel = AttrMutationLogLevelOf( aElement, aNameSpaceID, aAttribute, aModType, aOldValue); if (logLevel == LogLevel::Disabled) { return; } nsAutoString value; aElement->GetAttr(aAttribute, value); MOZ_LOG( gHTMLEditorAttrMutationLog, logLevel, ("%p %s AttributeChanged: %s of %s %s", this, logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse", nsAutoAtomCString(aAttribute).get(), NodeToString(aElement).get(), aModType == AttrModType::Removal ? "Removed" : nsPrintfCString("to \"%s\"", NS_ConvertUTF16toUTF8(value).get()) .get())); } /****************************************************************************** * mozilla::HTMLEditor - Start/end of a DOM API call to modify the DOM ******************************************************************************/ const AutoDOMAPIWrapperBase* HTMLEditor::OnDOMAPICallStart( const AutoDOMAPIWrapperBase& aWrapperBase) { const AutoDOMAPIWrapperBase* const prevRunner = mRunningDOMAPIWrapper; mRunningDOMAPIWrapper = &aWrapperBase; MOZ_LOG(gHTMLEditorMutationLog, LogLevel::Warning, (">>>> %p Calling DOM API: %s", this, ToString(*mRunningDOMAPIWrapper).c_str())); return prevRunner; } void HTMLEditor::OnDOMAPICallEnd(const AutoDOMAPIWrapperBase* aPrevWrapper) { MOZ_LOG(gHTMLEditorMutationLog, LogLevel::Warning, ("<<<< %p Called DOM API: %s", this, ToString(mRunningDOMAPIWrapper->Type()).c_str())); mRunningDOMAPIWrapper = aPrevWrapper; } /****************************************************************************** * mozilla::HTMLEditor - mutation observers/handlers ******************************************************************************/ void HTMLEditor::NotifyRootChanged() { MOZ_ASSERT(mPendingRootElementUpdatedRunner, "HTMLEditor::NotifyRootChanged() should be called via a runner"); mPendingRootElementUpdatedRunner = nullptr; nsCOMPtr kungFuDeathGrip(this); AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return; } RemoveEventListeners(); nsresult rv = InstallEventListeners(); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::InstallEventListeners() failed, but ignored"); return; } UpdateRootElement(); if (MOZ_LIKELY(mRootElement)) { rv = MaybeCollapseSelectionAtFirstEditableNode(false); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) " "failed, " "but ignored"); return; } // When this editor has focus, we need to reset the selection limiter to // new root. Otherwise, that is going to be done when this gets focus. nsCOMPtr node = GetFocusedNode(); if (node) { DebugOnly rvIgnored = InitializeSelection(*node); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "EditorBase::InitializeSelection() failed, but ignored"); } SyncRealTimeSpell(); } RefPtr newRootElement(mRootElement); IMEStateManager::OnUpdateHTMLEditorRootElement(*this, newRootElement); } // nsStubMutationObserver::ContentAppended override MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentAppended( nsIContent* aFirstNewContent, const ContentAppendInfo&) { DoContentInserted(aFirstNewContent, ContentNodeIs::Appended); } // nsStubMutationObserver::ContentInserted override MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentInserted( nsIContent* aChild, const ContentInsertInfo&) { DoContentInserted(aChild, ContentNodeIs::Inserted); } bool HTMLEditor::IsInObservedSubtree(nsIContent* aChild) { if (!aChild) { return false; } // FIXME(emilio, bug 1596856): This should probably work if the root is in the // same shadow tree as the child, probably? I don't know what the // contenteditable-in-shadow-dom situation is. if (Element* root = GetRoot()) { // To be super safe here, check both ChromeOnlyAccess and NAC / Shadow DOM. // That catches (also unbound) native anonymous content and ShadowDOM. if (root->ChromeOnlyAccess() != aChild->ChromeOnlyAccess() || root->IsInNativeAnonymousSubtree() != aChild->IsInNativeAnonymousSubtree() || root->IsInShadowTree() != aChild->IsInShadowTree()) { return false; } } return !aChild->ChromeOnlyAccess() && !aChild->IsInShadowTree() && !aChild->IsInNativeAnonymousSubtree(); } bool HTMLEditor::ShouldReplaceRootElement() const { if (!mRootElement) { // If we don't know what is our root element, we should find our root. return true; } // If we temporary set document root element to mRootElement, but there is // body element now, we should replace the root element by the body element. return mRootElement != GetBodyElement(); } void HTMLEditor::DoContentInserted(nsIContent* aChild, ContentNodeIs aContentNodeIs) { MOZ_ASSERT(aChild); nsINode* container = aChild->GetParentNode(); MOZ_ASSERT(container); if (!IsInObservedSubtree(aChild)) { return; } if (MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)) { if (aContentNodeIs == ContentNodeIs::Appended) { MaybeLogContentAppended(aChild); } else { MOZ_ASSERT(aContentNodeIs == ContentNodeIs::Inserted); MaybeLogContentInserted(aChild); } } // XXX Why do we need this? This method is a helper of mutation observer. // So, the callers of mutation observer should guarantee that this won't // be deleted at least during the call. RefPtr kungFuDeathGrip(this); // Do not create AutoEditActionDataSetter here because it grabs `Selection`, // but that appear in the profile. If you need to create to it in some cases, // you should do it in the minimum scope. if (ShouldReplaceRootElement()) { // Forget maybe disconnected root element right now because nobody should // work with it. mRootElement = nullptr; if (mPendingRootElementUpdatedRunner) { return; } mPendingRootElementUpdatedRunner = NewRunnableMethod( "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged); nsContentUtils::AddScriptRunner( do_AddRef(mPendingRootElementUpdatedRunner)); return; } // We don't need to handle our own modifications if (!GetTopLevelEditSubAction() && container->IsEditable()) { if (EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) { // Ignore insertion of the padding
element. return; } nsresult rv = RunOrScheduleOnModifyDocument(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::RunOrScheduleOnModifyDocument() failed, but ignored"); // Update spellcheck for only the newly-inserted node (bug 743819) if (mInlineSpellChecker) { nsIContent* endContent = aChild; if (aContentNodeIs == ContentNodeIs::Appended) { nsIContent* child = nullptr; for (child = aChild; child; child = child->GetNextSibling()) { if (child->InclusiveDescendantMayNeedSpellchecking(this)) { break; } } if (!child) { // No child needed spellchecking, return. return; } // Maybe more than 1 child was appended. endContent = container->GetLastChild(); } else if (!aChild->InclusiveDescendantMayNeedSpellchecking(this)) { return; } RefPtr range = nsRange::Create(aChild); range->SelectNodesInContainer(container, aChild, endContent); DebugOnly rvIgnored = mInlineSpellChecker->SpellCheckRange(range); NS_WARNING_ASSERTION( rvIgnored == NS_ERROR_NOT_INITIALIZED || NS_SUCCEEDED(rvIgnored), "mozInlineSpellChecker::SpellCheckRange() failed, but ignored"); } } } // nsStubMutationObserver::ContentWillBeRemoved override MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentWillBeRemoved( nsIContent* aChild, const ContentRemoveInfo&) { if (mLastCollapsibleWhiteSpaceAppendedTextNode == aChild) { mLastCollapsibleWhiteSpaceAppendedTextNode = nullptr; } if (!IsInObservedSubtree(aChild)) { return; } if (MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)) { MaybeLogContentWillBeRemoved(aChild); } // XXX Why do we need to do this? This method is a mutation observer's // method. Therefore, the caller should guarantee that this won't be // deleted during the call. RefPtr kungFuDeathGrip(this); // Do not create AutoEditActionDataSetter here because it grabs `Selection`, // but that appear in the profile. If you need to create to it in some cases, // you should do it in the minimum scope. // FYI: mRootElement may be the of the document or the root element. // Therefore, we don't need to check it across shadow DOM boundaries. if (mRootElement && mRootElement->IsInclusiveDescendantOf(aChild)) { // Forget the disconnected root element right now because nobody should work // with it. mRootElement = nullptr; if (mPendingRootElementUpdatedRunner) { return; } mPendingRootElementUpdatedRunner = NewRunnableMethod( "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged); nsContentUtils::AddScriptRunner( do_AddRef(mPendingRootElementUpdatedRunner)); return; } // We don't need to handle our own modifications if (!GetTopLevelEditSubAction() && aChild->GetParentNode()->IsEditable()) { if (aChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) { // Ignore removal of the padding
element for empty editor. return; } nsresult rv = RunOrScheduleOnModifyDocument(aChild); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::RunOrScheduleOnModifyDocument() failed, but ignored"); } } // nsStubMutationObserver::CharacterDataChanged override MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::CharacterDataChanged( nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { if (!IsInObservedSubtree(aContent)) { return; } if (MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)) { MaybeLogCharacterDataChanged(aContent, aInfo); } if (!mInlineSpellChecker || !aContent->IsEditable() || GetTopLevelEditSubAction() != EditSubAction::eNone) { return; } nsIContent* parent = aContent->GetParent(); if (!parent || !parent->InclusiveDescendantMayNeedSpellchecking(this)) { return; } RefPtr range = nsRange::Create(aContent); range->SelectNodesInContainer(parent, aContent, aContent); DebugOnly rvIgnored = mInlineSpellChecker->SpellCheckRange(range); } nsresult HTMLEditor::RunOrScheduleOnModifyDocument( const nsIContent* aContentWillBeRemoved /* = nullptr */) { if (mPendingDocumentModifiedRunner) { return NS_OK; // We've already posted same runnable into the queue. } mPendingDocumentModifiedRunner = new DocumentModifiedEvent(*this); nsContentUtils::AddScriptRunner(do_AddRef(mPendingDocumentModifiedRunner)); // Be aware, if OnModifyDocument() may be called synchronously, the // editor might have been destroyed here. return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; } // nsStubMutationObserver::AttributeChanged override MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::AttributeChanged( Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, AttrModType aModType, const nsAttrValue* aOldValue) { if (MOZ_LOG_TEST(gHTMLEditorAttrMutationLog, LogLevel::Info) && IsInObservedSubtree(aElement)) { MaybeLogAttributeChanged(aElement, aNameSpaceID, aAttribute, aModType, aOldValue); } } nsresult HTMLEditor::OnModifyDocument(const DocumentModifiedEvent& aRunner) { MOZ_ASSERT(mPendingDocumentModifiedRunner, "HTMLEditor::OnModifyDocument() should be called via a runner"); MOZ_ASSERT(&aRunner == mPendingDocumentModifiedRunner); mPendingDocumentModifiedRunner = nullptr; Maybe editActionData; if (!IsEditActionDataAvailable()) { editActionData.emplace(*this, EditAction::eCreatePaddingBRElementForEmptyEditor); if (NS_WARN_IF(!editActionData->CanHandle())) { return NS_ERROR_NOT_AVAILABLE; } } // EnsureNoPaddingBRElementForEmptyEditor() below may cause a flush, which // could destroy the editor nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker; // Delete our padding
element for empty editor, if we have one, since // the document might not be empty any more. nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return rv; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); // Try to recreate the padding
element for empty editor if needed. rv = MaybeCreatePaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed"); return rv; } } // namespace mozilla