/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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/HTMLEditor.h" #include "mozilla/ComposerCommandsUpdater.h" #include "mozilla/ContentIterator.h" #include "mozilla/DebugOnly.h" #include "mozilla/EditAction.h" #include "mozilla/EditorDOMPoint.h" #include "mozilla/EventStates.h" #include "mozilla/mozInlineSpellChecker.h" #include "mozilla/Telemetry.h" #include "mozilla/TextEvents.h" #include "nsCRT.h" #include "nsUnicharUtils.h" #include "HTMLEditorEventListener.h" #include "HTMLEditRules.h" #include "HTMLEditUtils.h" #include "HTMLURIRefObject.h" #include "TextEditUtils.h" #include "TypeInState.h" #include "nsHTMLDocument.h" #include "mozilla/dom/DocumentInlines.h" #include "nsISelectionController.h" #include "nsILinkHandler.h" #include "nsIInlineSpellChecker.h" #include "mozilla/css/Loader.h" #include "nsIContent.h" #include "nsIMutableArray.h" #include "nsContentUtils.h" #include "nsIDocumentEncoder.h" #include "nsGenericHTMLElement.h" #include "nsIPresShell.h" #include "nsPresContext.h" #include "nsFocusManager.h" #include "nsPIDOMWindow.h" // netwerk #include "nsIURI.h" #include "nsNetUtil.h" // Misc #include "mozilla/EditorUtils.h" #include "WSRunObject.h" #include "nsGkAtoms.h" #include "nsIWidget.h" #include "nsIFrame.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/DocumentFragment.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/EventTarget.h" #include "mozilla/dom/HTMLBodyElement.h" #include "nsElementTable.h" #include "nsTextFragment.h" #include "nsContentList.h" #include "mozilla/StyleSheet.h" #include "mozilla/StyleSheetInlines.h" namespace mozilla { using namespace dom; using namespace widget; const char16_t kNBSP = 160; static already_AddRefed GetLowerCaseNameAtom( const nsAString& aTagName) { if (aTagName.IsEmpty()) { return nullptr; } nsAutoString lowerTagName; nsContentUtils::ASCIIToLower(aTagName, lowerTagName); return NS_Atomize(lowerTagName); } // Some utilities to handle overloading of "A" tag for link and named anchor. static bool IsLinkTag(const nsAtom& aTagName) { return &aTagName == nsGkAtoms::href; } static bool IsNamedAnchorTag(const nsAtom& aTagName) { return &aTagName == nsGkAtoms::anchor; } class HTMLEditorPrefs final { public: static bool IsResizingUIEnabledByDefault() { EnsurePrefValues(); return sUserWantsToEnableResizingUIByDefault; } static bool IsInlineTableEditingUIEnabledByDefault() { EnsurePrefValues(); return sUserWantsToEnableInlineTableEditingUIByDefault; } static bool IsAbsolutePositioningUIEnabledByDefault() { EnsurePrefValues(); return sUserWantsToEnableAbsolutePositioningUIByDefault; } private: static bool sUserWantsToEnableResizingUIByDefault; static bool sUserWantsToEnableInlineTableEditingUIByDefault; static bool sUserWantsToEnableAbsolutePositioningUIByDefault; static void EnsurePrefValues() { static bool sInitialized = false; if (sInitialized) { return; } Preferences::AddBoolVarCache(&sUserWantsToEnableResizingUIByDefault, "editor.resizing.enabled_by_default"); Preferences::AddBoolVarCache( &sUserWantsToEnableInlineTableEditingUIByDefault, "editor.inline_table_editing.enabled_by_default"); Preferences::AddBoolVarCache( &sUserWantsToEnableAbsolutePositioningUIByDefault, "editor.positioning.enabled_by_default"); sInitialized = true; } }; bool HTMLEditorPrefs::sUserWantsToEnableResizingUIByDefault = false; bool HTMLEditorPrefs::sUserWantsToEnableInlineTableEditingUIByDefault = false; bool HTMLEditorPrefs::sUserWantsToEnableAbsolutePositioningUIByDefault = false; template EditorDOMPoint HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( nsIContent& aNode, const EditorDOMPoint& aPointToInsert, SplitAtEdges aSplitAtEdges); template EditorDOMPoint HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( nsIContent& aNode, const EditorRawDOMPoint& aPointToInsert, SplitAtEdges aSplitAtEdges); HTMLEditor::HTMLEditor() : mCRInParagraphCreatesParagraph(false), mCSSAware(false), mSelectedCellIndex(0), mHasShownResizers(false), mIsObjectResizingEnabled(HTMLEditorPrefs::IsResizingUIEnabledByDefault()), mIsResizing(false), mPreserveRatio(false), mResizedObjectIsAnImage(false), mIsAbsolutelyPositioningEnabled( HTMLEditorPrefs::IsAbsolutePositioningUIEnabledByDefault()), mResizedObjectIsAbsolutelyPositioned(false), mHasShownGrabber(false), mGrabberClicked(false), mIsMoving(false), mSnapToGridEnabled(false), mHasShownInlineTableEditor(false), mIsInlineTableEditingEnabled( HTMLEditorPrefs::IsInlineTableEditingUIEnabledByDefault()), mOriginalX(0), mOriginalY(0), mResizedObjectX(0), mResizedObjectY(0), mResizedObjectWidth(0), mResizedObjectHeight(0), mResizedObjectMarginLeft(0), mResizedObjectMarginTop(0), mResizedObjectBorderLeft(0), mResizedObjectBorderTop(0), mXIncrementFactor(0), mYIncrementFactor(0), mWidthIncrementFactor(0), mHeightIncrementFactor(0), mResizerUsedCount(0), mGrabberUsedCount(0), mInlineTableEditorUsedCount(0), mInfoXIncrement(20), mInfoYIncrement(20), mPositionedObjectX(0), mPositionedObjectY(0), mPositionedObjectWidth(0), mPositionedObjectHeight(0), mPositionedObjectMarginLeft(0), mPositionedObjectMarginTop(0), mPositionedObjectBorderLeft(0), mPositionedObjectBorderTop(0), mGridSize(0), mDefaultParagraphSeparator( Preferences::GetBool("editor.use_div_for_default_newlines", true) ? ParagraphSeparator::div : ParagraphSeparator::br) { mIsHTMLEditorClass = true; } HTMLEditor::~HTMLEditor() { if (mRules && mRules->AsHTMLEditRules()) { mRules->AsHTMLEditRules()->EndListeningToEditSubActions(); } mTypeInState = nullptr; if (mLinkHandler && IsInitialized()) { nsCOMPtr ps = GetPresShell(); if (ps && ps->GetPresContext()) { ps->GetPresContext()->SetLinkHandler(mLinkHandler); } } RemoveEventListeners(); HideAnonymousEditingUIs(); Telemetry::Accumulate(Telemetry::HTMLEDITORS_WITH_RESIZERS, mHasShownResizers ? 1 : 0); if (mHasShownResizers) { Telemetry::Accumulate(Telemetry::HTMLEDITORS_WHOSE_RESIZERS_USED_BY_USER, mResizerUsedCount); } Telemetry::Accumulate(Telemetry::HTMLEDITORS_WITH_ABSOLUTE_POSITIONER, mHasShownGrabber ? 1 : 0); if (mHasShownGrabber) { Telemetry::Accumulate( Telemetry::HTMLEDITORS_WHOSE_ABSOLUTE_POSITIONER_USED_BY_USER, mGrabberUsedCount); } Telemetry::Accumulate(Telemetry::HTMLEDITORS_WITH_INLINE_TABLE_EDITOR, mHasShownInlineTableEditor ? 1 : 0); if (mHasShownInlineTableEditor) { Telemetry::Accumulate( Telemetry::HTMLEDITORS_WHOSE_INLINE_TABLE_EDITOR_USED_BY_USER, mInlineTableEditorUsedCount); } } NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEditor) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLEditor, TextEditor) NS_IMPL_CYCLE_COLLECTION_UNLINK(mTypeInState) NS_IMPL_CYCLE_COLLECTION_UNLINK(mComposerCommandsUpdater) NS_IMPL_CYCLE_COLLECTION_UNLINK(mStyleSheets) tmp->HideAnonymousEditingUIs(); NS_IMPL_CYCLE_COLLECTION_UNLINK(mLinkHandler) NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLEditor, TextEditor) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTypeInState) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mComposerCommandsUpdater) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheets) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopLeftHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopRightHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLeftHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRightHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomLeftHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomRightHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActivatedHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingShadow) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingInfo) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizedObject) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbsolutelyPositionedObject) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGrabber) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPositioningShadow) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineEditedCell) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnBeforeButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveColumnButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnAfterButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowBeforeButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveRowButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowAfterButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLinkHandler) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_ADDREF_INHERITED(HTMLEditor, EditorBase) NS_IMPL_RELEASE_INHERITED(HTMLEditor, EditorBase) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLEditor) NS_INTERFACE_MAP_ENTRY(nsIHTMLEditor) NS_INTERFACE_MAP_ENTRY(nsIHTMLObjectResizer) NS_INTERFACE_MAP_ENTRY(nsIHTMLAbsPosEditor) NS_INTERFACE_MAP_ENTRY(nsIHTMLInlineTableEditor) NS_INTERFACE_MAP_ENTRY(nsITableEditor) NS_INTERFACE_MAP_ENTRY(nsIEditorStyleSheets) NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) NS_INTERFACE_MAP_ENTRY(nsIEditorMailSupport) NS_INTERFACE_MAP_END_INHERITING(TextEditor) nsresult HTMLEditor::Init(Document& aDoc, Element* aRoot, nsISelectionController* aSelCon, uint32_t aFlags, const nsAString& aInitialValue) { MOZ_ASSERT(aInitialValue.IsEmpty(), "Non-empty initial values not supported"); nsresult rulesRv = NS_OK; { // block to scope AutoEditInitRulesTrigger AutoEditInitRulesTrigger rulesTrigger(this, rulesRv); // Init the plaintext editor nsresult rv = TextEditor::Init(aDoc, aRoot, nullptr, aFlags, aInitialValue); if (NS_FAILED(rv)) { return rv; } // Init mutation observer aDoc.AddMutationObserverUnlessExists(this); if (!mRootElement) { UpdateRootElement(); } // disable Composer-only features if (IsMailEditor()) { SetAbsolutePositioningEnabled(false); SetSnapToGridEnabled(false); } // Init the HTML-CSS utils mCSSEditUtils = MakeUnique(this); // disable links nsCOMPtr presShell = GetPresShell(); NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); nsPresContext* context = presShell->GetPresContext(); NS_ENSURE_TRUE(context, NS_ERROR_NULL_POINTER); if (!IsPlaintextEditor() && !IsInteractionAllowed()) { mLinkHandler = context->GetLinkHandler(); context->SetLinkHandler(nullptr); } // init the type-in state mTypeInState = new TypeInState(); if (!IsInteractionAllowed()) { // ignore any errors from this in case the file is missing AddOverrideStyleSheetInternal( NS_LITERAL_STRING("resource://gre/res/EditorOverride.css")); } } NS_ENSURE_SUCCESS(rulesRv, rulesRv); return NS_OK; } void HTMLEditor::PreDestroy(bool aDestroyingFrames) { if (mDidPreDestroy) { return; } // FYI: Cannot create AutoEditActionDataSetter here. However, it does not // necessary for the methods called by the following code. RefPtr document = GetDocument(); if (document) { document->RemoveMutationObserver(this); } while (!mStyleSheetURLs.IsEmpty()) { DebugOnly rv = RemoveOverrideStyleSheetInternal(mStyleSheetURLs[0]); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to remove an override style sheet"); } // Clean up after our anonymous content -- we don't want these nodes to // stay around (which they would, since the frames have an owning reference). HideAnonymousEditingUIs(); EditorBase::PreDestroy(aDestroyingFrames); } NS_IMETHODIMP HTMLEditor::NotifySelectionChanged(Document* aDocument, Selection* aSelection, int16_t aReason) { if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } if (mTypeInState) { RefPtr typeInState = mTypeInState; typeInState->OnSelectionChange(*aSelection); // We used a class which derived from nsISelectionListener to call // HTMLEditor::RefereshEditingUI(). The lifetime of the class was // exactly same as mTypeInState. So, call it only when mTypeInState // is not nullptr. if ((aReason & (nsISelectionListener::MOUSEDOWN_REASON | nsISelectionListener::KEYPRESS_REASON | nsISelectionListener::SELECTALL_REASON)) && aSelection) { // the selection changed and we need to check if we have to // hide and/or redisplay resizing handles // FYI: This is an XPCOM method. So, the caller, Selection, guarantees // the lifetime of this instance. So, don't need to grab this with // local variable. DebugOnly rv = RefereshEditingUI(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RefereshEditingUI() failed"); } } if (mComposerCommandsUpdater) { RefPtr updater = mComposerCommandsUpdater; updater->OnSelectionChange(); } return EditorBase::NotifySelectionChanged(aDocument, aSelection, aReason); } void HTMLEditor::UpdateRootElement() { // Use the HTML documents body element as the editor root if we didn't // get a root element during initialization. mRootElement = GetBodyElement(); if (!mRootElement) { RefPtr doc = GetDocument(); if (doc) { // If there is no HTML body element, // we should use the document root element instead. mRootElement = doc->GetDocumentElement(); } // else leave it null, for lack of anything better. } } Element* HTMLEditor::FindSelectionRoot(nsINode* aNode) const { if (NS_WARN_IF(!aNode)) { return nullptr; } MOZ_ASSERT(aNode->IsDocument() || aNode->IsContent(), "aNode must be content or document node"); Document* doc = aNode->GetComposedDoc(); if (!doc) { return nullptr; } if (aNode->IsInUncomposedDoc() && (doc->HasFlag(NODE_IS_EDITABLE) || !aNode->IsContent())) { return doc->GetRootElement(); } // XXX If we have readonly flag, shouldn't return the element which has // contenteditable="true"? However, such case isn't there without chrome // permission script. if (IsReadonly()) { // We still want to allow selection in a readonly editor. return GetRoot(); } nsIContent* content = aNode->AsContent(); if (!content->HasFlag(NODE_IS_EDITABLE)) { // If the content is in read-write state but is not editable itself, // return it as the selection root. if (content->IsElement() && content->AsElement()->State().HasState(NS_EVENT_STATE_MOZ_READWRITE)) { return content->AsElement(); } return nullptr; } // For non-readonly editors we want to find the root of the editable subtree // containing aContent. return content->GetEditingHost(); } void HTMLEditor::CreateEventListeners() { // Don't create the handler twice if (!mEventListener) { mEventListener = new HTMLEditorEventListener(); } } nsresult HTMLEditor::InstallEventListeners() { if (NS_WARN_IF(!IsInitialized()) || NS_WARN_IF(!mEventListener)) { return NS_ERROR_NOT_INITIALIZED; } // NOTE: HTMLEditor doesn't need to initialize mEventTarget here because // the target must be document node and it must be referenced as weak pointer. HTMLEditorEventListener* listener = reinterpret_cast(mEventListener.get()); return listener->Connect(this); } void HTMLEditor::RemoveEventListeners() { if (!IsInitialized()) { return; } TextEditor::RemoveEventListeners(); } NS_IMETHODIMP HTMLEditor::SetFlags(uint32_t aFlags) { nsresult rv = TextEditor::SetFlags(aFlags); NS_ENSURE_SUCCESS(rv, rv); // Sets mCSSAware to correspond to aFlags. This toggles whether CSS is // used to style elements in the editor. Note that the editor is only CSS // aware by default in Composer and in the mail editor. mCSSAware = !NoCSS() && !IsMailEditor(); return NS_OK; } nsresult HTMLEditor::InitRules() { MOZ_ASSERT(IsEditActionDataAvailable()); if (!mRules) { // instantiate the rules for the html editor mRules = new HTMLEditRules(); } return mRules->Init(this); } NS_IMETHODIMP HTMLEditor::BeginningOfDocument() { AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } return MaybeCollapseSelectionAtFirstEditableNode(false); } void HTMLEditor::InitializeSelectionAncestorLimit(nsIContent& aAncestorLimit) { MOZ_ASSERT(IsEditActionDataAvailable()); // Hack for initializing selection. // HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode() will try to // collapse selection at first editable text node or inline element which // cannot have text nodes as its children. However, selection has already // set into the new editing host by user, we should not change it. For // solving this issue, we should do nothing if selection range is in active // editing host except it's not collapsed at start of the editing host since // aSelection.SetAncestorLimiter(aAncestorLimit) will collapse selection // at start of the new limiter if focus node of aSelection is outside of the // editing host. However, we need to check here if selection is already // collapsed at start of the editing host because it's possible JS to do it. // In such case, we should not modify selection with calling // MaybeCollapseSelectionAtFirstEditableNode(). // Basically, we should try to collapse selection at first editable node // in HTMLEditor. bool tryToCollapseSelectionAtFirstEditableNode = true; if (SelectionRefPtr()->RangeCount() == 1 && SelectionRefPtr()->IsCollapsed()) { Element* editingHost = GetActiveEditingHost(); nsRange* range = SelectionRefPtr()->GetRangeAt(0); if (range->GetStartContainer() == editingHost && !range->StartOffset()) { // JS or user operation has already collapsed selection at start of // the editing host. So, we don't need to try to change selection // in this case. tryToCollapseSelectionAtFirstEditableNode = false; } } EditorBase::InitializeSelectionAncestorLimit(aAncestorLimit); // XXX Do we need to check if we still need to change selection? E.g., // we could have already lost focus while we're changing the ancestor // limiter because it may causes "selectionchange" event. if (tryToCollapseSelectionAtFirstEditableNode) { MaybeCollapseSelectionAtFirstEditableNode(true); } } nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode( bool aIgnoreIfSelectionInEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); // Use editing host. If you use root element here, selection may be // moved to element, e.g., if there is a text node in