diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /editor/libeditor/HTMLEditorDataTransfer.cpp | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'editor/libeditor/HTMLEditorDataTransfer.cpp')
-rw-r--r-- | editor/libeditor/HTMLEditorDataTransfer.cpp | 2409 |
1 files changed, 2409 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp new file mode 100644 index 000000000..b9cd8adb9 --- /dev/null +++ b/editor/libeditor/HTMLEditorDataTransfer.cpp @@ -0,0 +1,2409 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 et tw=78: */ +/* 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 <string.h> + +#include "HTMLEditUtils.h" +#include "TextEditUtils.h" +#include "WSRunObject.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/DocumentFragment.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Base64.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/Preferences.h" +#include "mozilla/SelectionState.h" +#include "nsAString.h" +#include "nsCOMPtr.h" +#include "nsCRTGlue.h" // for CRLF +#include "nsComponentManagerUtils.h" +#include "nsIScriptError.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsDependentSubstring.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIClipboard.h" +#include "nsIContent.h" +#include "nsIContentFilter.h" +#include "nsIDOMComment.h" +#include "nsIDOMDocument.h" +#include "nsIDOMDocumentFragment.h" +#include "nsIDOMElement.h" +#include "nsIDOMHTMLAnchorElement.h" +#include "nsIDOMHTMLEmbedElement.h" +#include "nsIDOMHTMLFrameElement.h" +#include "nsIDOMHTMLIFrameElement.h" +#include "nsIDOMHTMLImageElement.h" +#include "nsIDOMHTMLInputElement.h" +#include "nsIDOMHTMLLinkElement.h" +#include "nsIDOMHTMLObjectElement.h" +#include "nsIDOMHTMLScriptElement.h" +#include "nsIDOMNode.h" +#include "nsIDocument.h" +#include "nsIEditor.h" +#include "nsIEditorIMESupport.h" +#include "nsIEditorMailSupport.h" +#include "nsIEditRules.h" +#include "nsIFile.h" +#include "nsIInputStream.h" +#include "nsIMIMEService.h" +#include "nsNameSpaceManager.h" +#include "nsINode.h" +#include "nsIParserUtils.h" +#include "nsIPlaintextEditor.h" +#include "nsISupportsImpl.h" +#include "nsISupportsPrimitives.h" +#include "nsISupportsUtils.h" +#include "nsITransferable.h" +#include "nsIURI.h" +#include "nsIVariant.h" +#include "nsLinebreakConverter.h" +#include "nsLiteralString.h" +#include "nsNetUtil.h" +#include "nsRange.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsStringIterator.h" +#include "nsSubstringTuple.h" +#include "nsTreeSanitizer.h" +#include "nsXPCOM.h" +#include "nscore.h" +#include "nsContentUtils.h" + +class nsIAtom; +class nsILoadContext; +class nsISupports; + +namespace mozilla { + +using namespace dom; + +#define kInsertCookie "_moz_Insert Here_moz_" + +// some little helpers +static bool FindIntegerAfterString(const char* aLeadingString, + nsCString& aCStr, int32_t& foundNumber); +static nsresult RemoveFragComments(nsCString& theStr); +static void RemoveBodyAndHead(nsINode& aNode); +static nsresult FindTargetNode(nsIDOMNode* aStart, + nsCOMPtr<nsIDOMNode>& aResult); + +nsresult +HTMLEditor::LoadHTML(const nsAString& aInputString) +{ + NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED); + + // force IME commit; set up rules sniffing and batching + ForceCompositionEnd(); + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::loadHTML, nsIEditor::eNext); + + // Get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + TextRulesInfo ruleInfo(EditAction::loadHTML); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel) { + return NS_OK; // rules canceled the operation + } + + if (!handled) { + // Delete Selection, but only if it isn't collapsed, see bug #106269 + if (!selection->Collapsed()) { + rv = DeleteSelection(eNone, eStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Get the first range in the selection, for context: + RefPtr<nsRange> range = selection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER); + + // create fragment for pasted html + nsCOMPtr<nsIDOMDocumentFragment> docfrag; + rv = range->CreateContextualFragment(aInputString, getter_AddRefs(docfrag)); + NS_ENSURE_SUCCESS(rv, rv); + // put the fragment into the document + nsCOMPtr<nsIDOMNode> parent; + rv = range->GetStartContainer(getter_AddRefs(parent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + int32_t childOffset; + rv = range->GetStartOffset(&childOffset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMNode> nodeToInsert; + docfrag->GetFirstChild(getter_AddRefs(nodeToInsert)); + while (nodeToInsert) { + rv = InsertNode(nodeToInsert, parent, childOffset++); + NS_ENSURE_SUCCESS(rv, rv); + docfrag->GetFirstChild(getter_AddRefs(nodeToInsert)); + } + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +NS_IMETHODIMP +HTMLEditor::InsertHTML(const nsAString& aInString) +{ + const nsAFlatString& empty = EmptyString(); + + return InsertHTMLWithContext(aInString, empty, empty, empty, + nullptr, nullptr, 0, true); +} + +nsresult +HTMLEditor::InsertHTMLWithContext(const nsAString& aInputString, + const nsAString& aContextStr, + const nsAString& aInfoStr, + const nsAString& aFlavor, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestNode, + int32_t aDestOffset, + bool aDeleteSelection) +{ + return DoInsertHTMLWithContext(aInputString, aContextStr, aInfoStr, + aFlavor, aSourceDoc, aDestNode, aDestOffset, aDeleteSelection, + /* trusted input */ true, /* clear style */ false); +} + +nsresult +HTMLEditor::DoInsertHTMLWithContext(const nsAString& aInputString, + const nsAString& aContextStr, + const nsAString& aInfoStr, + const nsAString& aFlavor, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestNode, + int32_t aDestOffset, + bool aDeleteSelection, + bool aTrustedInput, + bool aClearStyle) +{ + NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED); + + // Prevent the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + // force IME commit; set up rules sniffing and batching + ForceCompositionEnd(); + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::htmlPaste, nsIEditor::eNext); + + // Get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + // create a dom document fragment that represents the structure to paste + nsCOMPtr<nsIDOMNode> fragmentAsNode, streamStartParent, streamEndParent; + int32_t streamStartOffset = 0, streamEndOffset = 0; + + nsresult rv = CreateDOMFragmentFromPaste(aInputString, aContextStr, aInfoStr, + address_of(fragmentAsNode), + address_of(streamStartParent), + address_of(streamEndParent), + &streamStartOffset, + &streamEndOffset, + aTrustedInput); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMNode> targetNode; + int32_t targetOffset=0; + + if (!aDestNode) { + // if caller didn't provide the destination/target node, + // fetch the paste insertion point from our selection + rv = GetStartNodeAndOffset(selection, getter_AddRefs(targetNode), &targetOffset); + NS_ENSURE_SUCCESS(rv, rv); + if (!targetNode || !IsEditable(targetNode)) { + return NS_ERROR_FAILURE; + } + } else { + targetNode = aDestNode; + targetOffset = aDestOffset; + } + + bool doContinue = true; + + rv = DoContentFilterCallback(aFlavor, aSourceDoc, aDeleteSelection, + (nsIDOMNode **)address_of(fragmentAsNode), + (nsIDOMNode **)address_of(streamStartParent), + &streamStartOffset, + (nsIDOMNode **)address_of(streamEndParent), + &streamEndOffset, + (nsIDOMNode **)address_of(targetNode), + &targetOffset, &doContinue); + + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(doContinue, NS_OK); + + // if we have a destination / target node, we want to insert there + // rather than in place of the selection + // ignore aDeleteSelection here if no aDestNode since deletion will + // also occur later; this block is intended to cover the various + // scenarios where we are dropping in an editor (and may want to delete + // the selection before collapsing the selection in the new destination) + if (aDestNode) { + if (aDeleteSelection) { + // Use an auto tracker so that our drop point is correctly + // positioned after the delete. + AutoTrackDOMPoint tracker(mRangeUpdater, &targetNode, &targetOffset); + rv = DeleteSelection(eNone, eStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = selection->Collapse(targetNode, targetOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + // we need to recalculate various things based on potentially new offsets + // this is work to be completed at a later date (probably by jfrancis) + + // make a list of what nodes in docFrag we need to move + nsTArray<OwningNonNull<nsINode>> nodeList; + nsCOMPtr<nsINode> fragmentAsNodeNode = do_QueryInterface(fragmentAsNode); + NS_ENSURE_STATE(fragmentAsNodeNode || !fragmentAsNode); + nsCOMPtr<nsINode> streamStartParentNode = + do_QueryInterface(streamStartParent); + NS_ENSURE_STATE(streamStartParentNode || !streamStartParent); + nsCOMPtr<nsINode> streamEndParentNode = + do_QueryInterface(streamEndParent); + NS_ENSURE_STATE(streamEndParentNode || !streamEndParent); + CreateListOfNodesToPaste(*static_cast<DocumentFragment*>(fragmentAsNodeNode.get()), + nodeList, + streamStartParentNode, streamStartOffset, + streamEndParentNode, streamEndOffset); + + if (nodeList.IsEmpty()) { + // We aren't inserting anything, but if aDeleteSelection is set, we do want + // to delete everything. + if (aDeleteSelection) { + return DeleteSelection(eNone, eStrip); + } + return NS_OK; + } + + // Are there any table elements in the list? + // node and offset for insertion + nsCOMPtr<nsIDOMNode> parentNode; + int32_t offsetOfNewNode; + + // check for table cell selection mode + bool cellSelectionMode = false; + nsCOMPtr<nsIDOMElement> cell; + rv = GetFirstSelectedCell(nullptr, getter_AddRefs(cell)); + if (NS_SUCCEEDED(rv) && cell) { + cellSelectionMode = true; + } + + if (cellSelectionMode) { + // do we have table content to paste? If so, we want to delete + // the selected table cells and replace with new table elements; + // but if not we want to delete _contents_ of cells and replace + // with non-table elements. Use cellSelectionMode bool to + // indicate results. + if (!HTMLEditUtils::IsTableElement(nodeList[0])) { + cellSelectionMode = false; + } + } + + if (!cellSelectionMode) { + rv = DeleteSelectionAndPrepareToCreateNode(); + NS_ENSURE_SUCCESS(rv, rv); + + if (aClearStyle) { + // pasting does not inherit local inline styles + nsCOMPtr<nsINode> tmpNode = selection->GetAnchorNode(); + int32_t tmpOffset = static_cast<int32_t>(selection->AnchorOffset()); + rv = ClearStyle(address_of(tmpNode), &tmpOffset, nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Delete whole cells: we will replace with new table content. + + // Braces for artificial block to scope AutoSelectionRestorer. + // Save current selection since DeleteTableCell() perturbs it. + { + AutoSelectionRestorer selectionRestorer(selection, this); + rv = DeleteTableCell(1); + NS_ENSURE_SUCCESS(rv, rv); + } + // collapse selection to beginning of deleted table content + selection->CollapseToStart(); + } + + // give rules a chance to handle or cancel + TextRulesInfo ruleInfo(EditAction::insertElement); + bool cancel, handled; + rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel) { + return NS_OK; // rules canceled the operation + } + + if (!handled) { + // The rules code (WillDoAction above) might have changed the selection. + // refresh our memory... + rv = GetStartNodeAndOffset(selection, getter_AddRefs(parentNode), &offsetOfNewNode); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parentNode, NS_ERROR_FAILURE); + + // Adjust position based on the first node we are going to insert. + NormalizeEOLInsertPosition(nodeList[0], address_of(parentNode), + &offsetOfNewNode); + + // if there are any invisible br's after our insertion point, remove them. + // this is because if there is a br at end of what we paste, it will make + // the invisible br visible. + WSRunObject wsObj(this, parentNode, offsetOfNewNode); + if (wsObj.mEndReasonNode && + TextEditUtils::IsBreak(wsObj.mEndReasonNode) && + !IsVisBreak(wsObj.mEndReasonNode)) { + rv = DeleteNode(wsObj.mEndReasonNode); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Remember if we are in a link. + bool bStartedInLink = IsInLink(parentNode); + + // Are we in a text node? If so, split it. + if (IsTextNode(parentNode)) { + nsCOMPtr<nsIContent> parentContent = do_QueryInterface(parentNode); + NS_ENSURE_STATE(parentContent || !parentNode); + offsetOfNewNode = SplitNodeDeep(*parentContent, *parentContent, + offsetOfNewNode); + NS_ENSURE_STATE(offsetOfNewNode != -1); + nsCOMPtr<nsIDOMNode> temp; + rv = parentNode->GetParentNode(getter_AddRefs(temp)); + NS_ENSURE_SUCCESS(rv, rv); + parentNode = temp; + } + + // build up list of parents of first node in list that are either + // lists or tables. First examine front of paste node list. + nsTArray<OwningNonNull<Element>> startListAndTableArray; + GetListAndTableParents(StartOrEnd::start, nodeList, + startListAndTableArray); + + // remember number of lists and tables above us + int32_t highWaterMark = -1; + if (!startListAndTableArray.IsEmpty()) { + highWaterMark = DiscoverPartialListsAndTables(nodeList, + startListAndTableArray); + } + + // if we have pieces of tables or lists to be inserted, let's force the paste + // to deal with table elements right away, so that it doesn't orphan some + // table or list contents outside the table or list. + if (highWaterMark >= 0) { + ReplaceOrphanedStructure(StartOrEnd::start, nodeList, + startListAndTableArray, highWaterMark); + } + + // Now go through the same process again for the end of the paste node list. + nsTArray<OwningNonNull<Element>> endListAndTableArray; + GetListAndTableParents(StartOrEnd::end, nodeList, endListAndTableArray); + highWaterMark = -1; + + // remember number of lists and tables above us + if (!endListAndTableArray.IsEmpty()) { + highWaterMark = DiscoverPartialListsAndTables(nodeList, + endListAndTableArray); + } + + // don't orphan partial list or table structure + if (highWaterMark >= 0) { + ReplaceOrphanedStructure(StartOrEnd::end, nodeList, + endListAndTableArray, highWaterMark); + } + + // Loop over the node list and paste the nodes: + nsCOMPtr<nsIDOMNode> parentBlock, lastInsertNode, insertedContextParent; + nsCOMPtr<nsINode> parentNodeNode = do_QueryInterface(parentNode); + NS_ENSURE_STATE(parentNodeNode || !parentNode); + if (IsBlockNode(parentNodeNode)) { + parentBlock = parentNode; + } else { + parentBlock = GetBlockNodeParent(parentNode); + } + + int32_t listCount = nodeList.Length(); + for (int32_t j = 0; j < listCount; j++) { + bool bDidInsert = false; + nsCOMPtr<nsIDOMNode> curNode = nodeList[j]->AsDOMNode(); + + NS_ENSURE_TRUE(curNode, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(curNode != fragmentAsNode, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(!TextEditUtils::IsBody(curNode), NS_ERROR_FAILURE); + + if (insertedContextParent) { + // if we had to insert something higher up in the paste hierarchy, we want to + // skip any further paste nodes that descend from that. Else we will paste twice. + if (EditorUtils::IsDescendantOf(curNode, insertedContextParent)) { + continue; + } + } + + // give the user a hand on table element insertion. if they have + // a table or table row on the clipboard, and are trying to insert + // into a table or table row, insert the appropriate children instead. + if (HTMLEditUtils::IsTableRow(curNode) && + HTMLEditUtils::IsTableRow(parentNode) && + (HTMLEditUtils::IsTable(curNode) || + HTMLEditUtils::IsTable(parentNode))) { + nsCOMPtr<nsIDOMNode> child; + curNode->GetFirstChild(getter_AddRefs(child)); + while (child) { + rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true); + if (NS_FAILED(rv)) { + break; + } + + bDidInsert = true; + lastInsertNode = child; + offsetOfNewNode++; + + curNode->GetFirstChild(getter_AddRefs(child)); + } + } + // give the user a hand on list insertion. if they have + // a list on the clipboard, and are trying to insert + // into a list or list item, insert the appropriate children instead, + // ie, merge the lists instead of pasting in a sublist. + else if (HTMLEditUtils::IsList(curNode) && + (HTMLEditUtils::IsList(parentNode) || + HTMLEditUtils::IsListItem(parentNode))) { + nsCOMPtr<nsIDOMNode> child, tmp; + curNode->GetFirstChild(getter_AddRefs(child)); + while (child) { + if (HTMLEditUtils::IsListItem(child) || + HTMLEditUtils::IsList(child)) { + // Check if we are pasting into empty list item. If so + // delete it and paste into parent list instead. + if (HTMLEditUtils::IsListItem(parentNode)) { + bool isEmpty; + rv = IsEmptyNode(parentNode, &isEmpty, true); + if (NS_SUCCEEDED(rv) && isEmpty) { + int32_t newOffset; + nsCOMPtr<nsIDOMNode> listNode = GetNodeLocation(parentNode, &newOffset); + if (listNode) { + DeleteNode(parentNode); + parentNode = listNode; + offsetOfNewNode = newOffset; + } + } + } + rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true); + if (NS_FAILED(rv)) { + break; + } + + bDidInsert = true; + lastInsertNode = child; + offsetOfNewNode++; + } else { + curNode->RemoveChild(child, getter_AddRefs(tmp)); + } + curNode->GetFirstChild(getter_AddRefs(child)); + } + } else if (parentBlock && HTMLEditUtils::IsPre(parentBlock) && + HTMLEditUtils::IsPre(curNode)) { + // Check for pre's going into pre's. + nsCOMPtr<nsIDOMNode> child; + curNode->GetFirstChild(getter_AddRefs(child)); + while (child) { + rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true); + if (NS_FAILED(rv)) { + break; + } + + bDidInsert = true; + lastInsertNode = child; + offsetOfNewNode++; + + curNode->GetFirstChild(getter_AddRefs(child)); + } + } + + if (!bDidInsert || NS_FAILED(rv)) { + // try to insert + rv = InsertNodeAtPoint(curNode, address_of(parentNode), &offsetOfNewNode, true); + if (NS_SUCCEEDED(rv)) { + bDidInsert = true; + lastInsertNode = curNode; + } + + // Assume failure means no legal parent in the document hierarchy, + // try again with the parent of curNode in the paste hierarchy. + nsCOMPtr<nsIDOMNode> parent; + while (NS_FAILED(rv) && curNode) { + curNode->GetParentNode(getter_AddRefs(parent)); + if (parent && !TextEditUtils::IsBody(parent)) { + rv = InsertNodeAtPoint(parent, address_of(parentNode), &offsetOfNewNode, true); + if (NS_SUCCEEDED(rv)) { + bDidInsert = true; + insertedContextParent = parent; + lastInsertNode = GetChildAt(parentNode, offsetOfNewNode); + } + } + curNode = parent; + } + } + if (lastInsertNode) { + parentNode = GetNodeLocation(lastInsertNode, &offsetOfNewNode); + offsetOfNewNode++; + } + } + + // Now collapse the selection to the end of what we just inserted: + if (lastInsertNode) { + // set selection to the end of what we just pasted. + nsCOMPtr<nsIDOMNode> selNode, tmp, highTable; + int32_t selOffset; + + // but don't cross tables + if (!HTMLEditUtils::IsTable(lastInsertNode)) { + nsCOMPtr<nsINode> lastInsertNode_ = do_QueryInterface(lastInsertNode); + NS_ENSURE_STATE(lastInsertNode_ || !lastInsertNode); + selNode = GetAsDOMNode(GetLastEditableLeaf(*lastInsertNode_)); + tmp = selNode; + while (tmp && tmp != lastInsertNode) { + if (HTMLEditUtils::IsTable(tmp)) { + highTable = tmp; + } + nsCOMPtr<nsIDOMNode> parent = tmp; + tmp->GetParentNode(getter_AddRefs(parent)); + tmp = parent; + } + if (highTable) { + selNode = highTable; + } + } + if (!selNode) { + selNode = lastInsertNode; + } + if (IsTextNode(selNode) || + (IsContainer(selNode) && !HTMLEditUtils::IsTable(selNode))) { + rv = GetLengthOfDOMNode(selNode, (uint32_t&)selOffset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // We need to find a container for selection. Look up. + tmp = selNode; + selNode = GetNodeLocation(tmp, &selOffset); + // selNode might be null in case a mutation listener removed + // the stuff we just inserted from the DOM. + NS_ENSURE_STATE(selNode); + ++selOffset; // want to be *after* last leaf node in paste + } + + // make sure we don't end up with selection collapsed after an invisible break node + WSRunObject wsRunObj(this, selNode, selOffset); + nsCOMPtr<nsINode> visNode; + int32_t outVisOffset=0; + WSType visType; + nsCOMPtr<nsINode> selNode_(do_QueryInterface(selNode)); + wsRunObj.PriorVisibleNode(selNode_, selOffset, address_of(visNode), + &outVisOffset, &visType); + if (visType == WSType::br) { + // we are after a break. Is it visible? Despite the name, + // PriorVisibleNode does not make that determination for breaks. + // It also may not return the break in visNode. We have to pull it + // out of the WSRunObject's state. + if (!IsVisBreak(wsRunObj.mStartReasonNode)) { + // don't leave selection past an invisible break; + // reset {selNode,selOffset} to point before break + selNode = GetNodeLocation(GetAsDOMNode(wsRunObj.mStartReasonNode), &selOffset); + // we want to be inside any inline style prior to break + WSRunObject wsRunObj(this, selNode, selOffset); + selNode_ = do_QueryInterface(selNode); + wsRunObj.PriorVisibleNode(selNode_, selOffset, address_of(visNode), + &outVisOffset, &visType); + if (visType == WSType::text || visType == WSType::normalWS) { + selNode = GetAsDOMNode(visNode); + selOffset = outVisOffset; // PriorVisibleNode already set offset to _after_ the text or ws + } else if (visType == WSType::special) { + // prior visible thing is an image or some other non-text thingy. + // We want to be right after it. + selNode = GetNodeLocation(GetAsDOMNode(wsRunObj.mStartReasonNode), &selOffset); + ++selOffset; + } + } + } + selection->Collapse(selNode, selOffset); + + // if we just pasted a link, discontinue link style + nsCOMPtr<nsIDOMNode> link; + if (!bStartedInLink && IsInLink(selNode, address_of(link))) { + // so, if we just pasted a link, I split it. Why do that instead of just + // nudging selection point beyond it? Because it might have ended in a BR + // that is not visible. If so, the code above just placed selection + // inside that. So I split it instead. + nsCOMPtr<nsIContent> linkContent = do_QueryInterface(link); + NS_ENSURE_STATE(linkContent || !link); + nsCOMPtr<nsIContent> selContent = do_QueryInterface(selNode); + NS_ENSURE_STATE(selContent || !selNode); + nsCOMPtr<nsIContent> leftLink; + SplitNodeDeep(*linkContent, *selContent, selOffset, + EmptyContainers::no, getter_AddRefs(leftLink)); + if (leftLink) { + selNode = GetNodeLocation(GetAsDOMNode(leftLink), &selOffset); + selection->Collapse(selNode, selOffset+1); + } + } + } + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +NS_IMETHODIMP +HTMLEditor::AddInsertionListener(nsIContentFilter* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER); + + // don't let a listener be added more than once + if (!mContentFilters.Contains(aListener)) { + mContentFilters.AppendElement(*aListener); + } + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::RemoveInsertionListener(nsIContentFilter* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_FAILURE); + + mContentFilters.RemoveElement(aListener); + + return NS_OK; +} + +nsresult +HTMLEditor::DoContentFilterCallback(const nsAString& aFlavor, + nsIDOMDocument* sourceDoc, + bool aWillDeleteSelection, + nsIDOMNode** aFragmentAsNode, + nsIDOMNode** aFragStartNode, + int32_t* aFragStartOffset, + nsIDOMNode** aFragEndNode, + int32_t* aFragEndOffset, + nsIDOMNode** aTargetNode, + int32_t* aTargetOffset, + bool* aDoContinue) +{ + *aDoContinue = true; + + for (auto& listener : mContentFilters) { + if (!*aDoContinue) { + break; + } + listener->NotifyOfInsertion(aFlavor, nullptr, sourceDoc, + aWillDeleteSelection, aFragmentAsNode, + aFragStartNode, aFragStartOffset, + aFragEndNode, aFragEndOffset, aTargetNode, + aTargetOffset, aDoContinue); + } + + return NS_OK; +} + +bool +HTMLEditor::IsInLink(nsIDOMNode* aNode, + nsCOMPtr<nsIDOMNode>* outLink) +{ + NS_ENSURE_TRUE(aNode, false); + if (outLink) { + *outLink = nullptr; + } + nsCOMPtr<nsIDOMNode> tmp, node = aNode; + while (node) { + if (HTMLEditUtils::IsLink(node)) { + if (outLink) { + *outLink = node; + } + return true; + } + tmp = node; + tmp->GetParentNode(getter_AddRefs(node)); + } + return false; +} + +nsresult +HTMLEditor::StripFormattingNodes(nsIContent& aNode, + bool aListOnly) +{ + if (aNode.TextIsOnlyWhitespace()) { + nsCOMPtr<nsINode> parent = aNode.GetParentNode(); + if (parent) { + if (!aListOnly || HTMLEditUtils::IsList(parent)) { + ErrorResult rv; + parent->RemoveChild(aNode, rv); + return rv.StealNSResult(); + } + return NS_OK; + } + } + + if (!aNode.IsHTMLElement(nsGkAtoms::pre)) { + nsCOMPtr<nsIContent> child = aNode.GetLastChild(); + while (child) { + nsCOMPtr<nsIContent> previous = child->GetPreviousSibling(); + nsresult rv = StripFormattingNodes(*child, aListOnly); + NS_ENSURE_SUCCESS(rv, rv); + child = previous.forget(); + } + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::PrepareTransferable(nsITransferable** aTransferable) +{ + return NS_OK; +} + +nsresult +HTMLEditor::PrepareHTMLTransferable(nsITransferable** aTransferable) +{ + // Create generic Transferable for getting the data + nsresult rv = CallCreateInstance("@mozilla.org/widget/transferable;1", aTransferable); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the nsITransferable interface for getting the data from the clipboard + if (aTransferable) { + nsCOMPtr<nsIDocument> destdoc = GetDocument(); + nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr; + (*aTransferable)->Init(loadContext); + + // Create the desired DataFlavor for the type of data + // we want to get out of the transferable + // This should only happen in html editors, not plaintext + if (!IsPlaintextEditor()) { + (*aTransferable)->AddDataFlavor(kNativeHTMLMime); + (*aTransferable)->AddDataFlavor(kHTMLMime); + (*aTransferable)->AddDataFlavor(kFileMime); + + switch (Preferences::GetInt("clipboard.paste_image_type", 1)) { + case 0: // prefer JPEG over PNG over GIF encoding + (*aTransferable)->AddDataFlavor(kJPEGImageMime); + (*aTransferable)->AddDataFlavor(kJPGImageMime); + (*aTransferable)->AddDataFlavor(kPNGImageMime); + (*aTransferable)->AddDataFlavor(kGIFImageMime); + break; + case 1: // prefer PNG over JPEG over GIF encoding (default) + default: + (*aTransferable)->AddDataFlavor(kPNGImageMime); + (*aTransferable)->AddDataFlavor(kJPEGImageMime); + (*aTransferable)->AddDataFlavor(kJPGImageMime); + (*aTransferable)->AddDataFlavor(kGIFImageMime); + break; + case 2: // prefer GIF over JPEG over PNG encoding + (*aTransferable)->AddDataFlavor(kGIFImageMime); + (*aTransferable)->AddDataFlavor(kJPEGImageMime); + (*aTransferable)->AddDataFlavor(kJPGImageMime); + (*aTransferable)->AddDataFlavor(kPNGImageMime); + break; + } + } + (*aTransferable)->AddDataFlavor(kUnicodeMime); + (*aTransferable)->AddDataFlavor(kMozTextInternal); + } + + return NS_OK; +} + +bool +FindIntegerAfterString(const char* aLeadingString, + nsCString& aCStr, + int32_t& foundNumber) +{ + // first obtain offsets from cfhtml str + int32_t numFront = aCStr.Find(aLeadingString); + if (numFront == -1) { + return false; + } + numFront += strlen(aLeadingString); + + int32_t numBack = aCStr.FindCharInSet(CRLF, numFront); + if (numBack == -1) { + return false; + } + + nsAutoCString numStr(Substring(aCStr, numFront, numBack-numFront)); + nsresult errorCode; + foundNumber = numStr.ToInteger(&errorCode); + return true; +} + +nsresult +RemoveFragComments(nsCString& aStr) +{ + // remove the StartFragment/EndFragment comments from the str, if present + int32_t startCommentIndx = aStr.Find("<!--StartFragment"); + if (startCommentIndx >= 0) { + int32_t startCommentEnd = aStr.Find("-->", false, startCommentIndx); + if (startCommentEnd > startCommentIndx) { + aStr.Cut(startCommentIndx, (startCommentEnd + 3) - startCommentIndx); + } + } + int32_t endCommentIndx = aStr.Find("<!--EndFragment"); + if (endCommentIndx >= 0) { + int32_t endCommentEnd = aStr.Find("-->", false, endCommentIndx); + if (endCommentEnd > endCommentIndx) { + aStr.Cut(endCommentIndx, (endCommentEnd + 3) - endCommentIndx); + } + } + return NS_OK; +} + +nsresult +HTMLEditor::ParseCFHTML(nsCString& aCfhtml, + char16_t** aStuffToPaste, + char16_t** aCfcontext) +{ + // First obtain offsets from cfhtml str. + int32_t startHTML, endHTML, startFragment, endFragment; + if (!FindIntegerAfterString("StartHTML:", aCfhtml, startHTML) || + startHTML < -1) { + return NS_ERROR_FAILURE; + } + if (!FindIntegerAfterString("EndHTML:", aCfhtml, endHTML) || + endHTML < -1) { + return NS_ERROR_FAILURE; + } + if (!FindIntegerAfterString("StartFragment:", aCfhtml, startFragment) || + startFragment < 0) { + return NS_ERROR_FAILURE; + } + if (!FindIntegerAfterString("EndFragment:", aCfhtml, endFragment) || + startFragment < 0) { + return NS_ERROR_FAILURE; + } + + // The StartHTML and EndHTML markers are allowed to be -1 to include everything. + // See Reference: MSDN doc entitled "HTML Clipboard Format" + // http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx#unknown_854 + if (startHTML == -1) { + startHTML = aCfhtml.Find("<!--StartFragment-->"); + if (startHTML == -1) { + return NS_OK; + } + } + if (endHTML == -1) { + const char endFragmentMarker[] = "<!--EndFragment-->"; + endHTML = aCfhtml.Find(endFragmentMarker); + if (endHTML == -1) { + return NS_OK; + } + endHTML += ArrayLength(endFragmentMarker) - 1; + } + + // create context string + nsAutoCString contextUTF8(Substring(aCfhtml, startHTML, startFragment - startHTML) + + NS_LITERAL_CSTRING("<!--" kInsertCookie "-->") + + Substring(aCfhtml, endFragment, endHTML - endFragment)); + + // validate startFragment + // make sure it's not in the middle of a HTML tag + // see bug #228879 for more details + int32_t curPos = startFragment; + while (curPos > startHTML) { + if (aCfhtml[curPos] == '>') { + // working backwards, the first thing we see is the end of a tag + // so StartFragment is good, so do nothing. + break; + } + if (aCfhtml[curPos] == '<') { + // if we are at the start, then we want to see the '<' + if (curPos != startFragment) { + // working backwards, the first thing we see is the start of a tag + // so StartFragment is bad, so we need to update it. + NS_ERROR("StartFragment byte count in the clipboard looks bad, see bug #228879"); + startFragment = curPos - 1; + } + break; + } + curPos--; + } + + // create fragment string + nsAutoCString fragmentUTF8(Substring(aCfhtml, startFragment, endFragment-startFragment)); + + // remove the StartFragment/EndFragment comments from the fragment, if present + RemoveFragComments(fragmentUTF8); + + // remove the StartFragment/EndFragment comments from the context, if present + RemoveFragComments(contextUTF8); + + // convert both strings to usc2 + const nsAFlatString& fragUcs2Str = NS_ConvertUTF8toUTF16(fragmentUTF8); + const nsAFlatString& cntxtUcs2Str = NS_ConvertUTF8toUTF16(contextUTF8); + + // translate platform linebreaks for fragment + int32_t oldLengthInChars = fragUcs2Str.Length() + 1; // +1 to include null terminator + int32_t newLengthInChars = 0; + *aStuffToPaste = nsLinebreakConverter::ConvertUnicharLineBreaks(fragUcs2Str.get(), + nsLinebreakConverter::eLinebreakAny, + nsLinebreakConverter::eLinebreakContent, + oldLengthInChars, &newLengthInChars); + NS_ENSURE_TRUE(*aStuffToPaste, NS_ERROR_FAILURE); + + // translate platform linebreaks for context + oldLengthInChars = cntxtUcs2Str.Length() + 1; // +1 to include null terminator + newLengthInChars = 0; + *aCfcontext = nsLinebreakConverter::ConvertUnicharLineBreaks(cntxtUcs2Str.get(), + nsLinebreakConverter::eLinebreakAny, + nsLinebreakConverter::eLinebreakContent, + oldLengthInChars, &newLengthInChars); + // it's ok for context to be empty. frag might be whole doc and contain all its context. + + // we're done! + return NS_OK; +} + +static nsresult +ImgFromData(const nsACString& aType, const nsACString& aData, nsString& aOutput) +{ + nsAutoCString data64; + nsresult rv = Base64Encode(aData, data64); + NS_ENSURE_SUCCESS(rv, rv); + + aOutput.AssignLiteral("<IMG src=\"data:"); + AppendUTF8toUTF16(aType, aOutput); + aOutput.AppendLiteral(";base64,"); + if (!AppendASCIItoUTF16(data64, aOutput, fallible_t())) { + return NS_ERROR_OUT_OF_MEMORY; + } + aOutput.AppendLiteral("\" alt=\"\" >"); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(HTMLEditor::BlobReader, nsIEditorBlobListener) + +HTMLEditor::BlobReader::BlobReader(BlobImpl* aBlob, + HTMLEditor* aHTMLEditor, + bool aIsSafe, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) + : mBlob(aBlob) + , mHTMLEditor(aHTMLEditor) + , mIsSafe(aIsSafe) + , mSourceDoc(aSourceDoc) + , mDestinationNode(aDestinationNode) + , mDestOffset(aDestOffset) + , mDoDeleteSelection(aDoDeleteSelection) +{ + MOZ_ASSERT(mBlob); + MOZ_ASSERT(mHTMLEditor); + MOZ_ASSERT(mDestinationNode); +} + +NS_IMETHODIMP +HTMLEditor::BlobReader::OnResult(const nsACString& aResult) +{ + nsString blobType; + mBlob->GetType(blobType); + + NS_ConvertUTF16toUTF8 type(blobType); + nsAutoString stuffToPaste; + nsresult rv = ImgFromData(type, aResult, stuffToPaste); + NS_ENSURE_SUCCESS(rv, rv); + + AutoEditBatch beginBatching(mHTMLEditor); + rv = mHTMLEditor->DoInsertHTMLWithContext(stuffToPaste, EmptyString(), + EmptyString(), + NS_LITERAL_STRING(kFileMime), + mSourceDoc, + mDestinationNode, mDestOffset, + mDoDeleteSelection, + mIsSafe, false); + return rv; +} + +NS_IMETHODIMP +HTMLEditor::BlobReader::OnError(const nsAString& aError) +{ + nsCOMPtr<nsINode> destNode = do_QueryInterface(mDestinationNode); + const nsPromiseFlatString& flat = PromiseFlatString(aError); + const char16_t* error = flat.get(); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Editor"), + destNode->OwnerDoc(), + nsContentUtils::eDOM_PROPERTIES, + "EditorFileDropFailed", + &error, 1); + return NS_OK; +} + +nsresult +HTMLEditor::InsertObject(const nsACString& aType, + nsISupports* aObject, + bool aIsSafe, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) +{ + nsresult rv; + + if (nsCOMPtr<BlobImpl> blob = do_QueryInterface(aObject)) { + RefPtr<BlobReader> br = new BlobReader(blob, this, aIsSafe, aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection); + nsCOMPtr<nsIEditorUtils> utils = + do_GetService("@mozilla.org/editor-utils;1"); + NS_ENSURE_TRUE(utils, NS_ERROR_FAILURE); + + nsCOMPtr<nsINode> node = do_QueryInterface(aDestinationNode); + MOZ_ASSERT(node); + + nsCOMPtr<nsIDOMBlob> domBlob = Blob::Create(node->GetOwnerGlobal(), blob); + NS_ENSURE_TRUE(domBlob, NS_ERROR_FAILURE); + + return utils->SlurpBlob(domBlob, node->OwnerDoc()->GetWindow(), br); + } + + nsAutoCString type(aType); + + // Check to see if we can insert an image file + bool insertAsImage = false; + nsCOMPtr<nsIFile> fileObj; + if (type.EqualsLiteral(kFileMime)) { + fileObj = do_QueryInterface(aObject); + if (fileObj) { + // Accept any image type fed to us + if (nsContentUtils::IsFileImage(fileObj, type)) { + insertAsImage = true; + } else { + // Reset type. + type.AssignLiteral(kFileMime); + } + } + } + + if (type.EqualsLiteral(kJPEGImageMime) || + type.EqualsLiteral(kJPGImageMime) || + type.EqualsLiteral(kPNGImageMime) || + type.EqualsLiteral(kGIFImageMime) || + insertAsImage) { + nsCString imageData; + if (insertAsImage) { + rv = nsContentUtils::SlurpFileToString(fileObj, imageData); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsCOMPtr<nsIInputStream> imageStream = do_QueryInterface(aObject); + NS_ENSURE_TRUE(imageStream, NS_ERROR_FAILURE); + + rv = NS_ConsumeStream(imageStream, UINT32_MAX, imageData); + NS_ENSURE_SUCCESS(rv, rv); + + rv = imageStream->Close(); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsAutoString stuffToPaste; + rv = ImgFromData(type, imageData, stuffToPaste); + NS_ENSURE_SUCCESS(rv, rv); + + AutoEditBatch beginBatching(this); + rv = DoInsertHTMLWithContext(stuffToPaste, EmptyString(), EmptyString(), + NS_LITERAL_STRING(kFileMime), + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + aIsSafe, false); + } + + return NS_OK; +} + +nsresult +HTMLEditor::InsertFromTransferable(nsITransferable* transferable, + nsIDOMDocument* aSourceDoc, + const nsAString& aContextStr, + const nsAString& aInfoStr, + bool havePrivateHTMLFlavor, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) +{ + nsresult rv = NS_OK; + nsAutoCString bestFlavor; + nsCOMPtr<nsISupports> genericDataObj; + uint32_t len = 0; + if (NS_SUCCEEDED( + transferable->GetAnyTransferData(bestFlavor, + getter_AddRefs(genericDataObj), + &len))) { + AutoTransactionsConserveSelection dontSpazMySelection(this); + nsAutoString flavor; + flavor.AssignWithConversion(bestFlavor); + nsAutoString stuffToPaste; + bool isSafe = IsSafeToInsertData(aSourceDoc); + + if (bestFlavor.EqualsLiteral(kFileMime) || + bestFlavor.EqualsLiteral(kJPEGImageMime) || + bestFlavor.EqualsLiteral(kJPGImageMime) || + bestFlavor.EqualsLiteral(kPNGImageMime) || + bestFlavor.EqualsLiteral(kGIFImageMime)) { + rv = InsertObject(bestFlavor, genericDataObj, isSafe, + aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection); + } else if (bestFlavor.EqualsLiteral(kNativeHTMLMime)) { + // note cf_html uses utf8, hence use length = len, not len/2 as in flavors below + nsCOMPtr<nsISupportsCString> textDataObj = do_QueryInterface(genericDataObj); + if (textDataObj && len > 0) { + nsAutoCString cfhtml; + textDataObj->GetData(cfhtml); + NS_ASSERTION(cfhtml.Length() <= (len), "Invalid length!"); + nsXPIDLString cfcontext, cffragment, cfselection; // cfselection left emtpy for now + + rv = ParseCFHTML(cfhtml, getter_Copies(cffragment), getter_Copies(cfcontext)); + if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) { + AutoEditBatch beginBatching(this); + // If we have our private HTML flavor, we will only use the fragment + // from the CF_HTML. The rest comes from the clipboard. + if (havePrivateHTMLFlavor) { + rv = DoInsertHTMLWithContext(cffragment, + aContextStr, aInfoStr, flavor, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } else { + rv = DoInsertHTMLWithContext(cffragment, + cfcontext, cfselection, flavor, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + + } + } else { + // In some platforms (like Linux), the clipboard might return data + // requested for unknown flavors (for example: + // application/x-moz-nativehtml). In this case, treat the data + // to be pasted as mere HTML to get the best chance of pasting it + // correctly. + bestFlavor.AssignLiteral(kHTMLMime); + // Fall through the next case + } + } + } + if (bestFlavor.EqualsLiteral(kHTMLMime) || + bestFlavor.EqualsLiteral(kUnicodeMime) || + bestFlavor.EqualsLiteral(kMozTextInternal)) { + nsCOMPtr<nsISupportsString> textDataObj = do_QueryInterface(genericDataObj); + if (textDataObj && len > 0) { + nsAutoString text; + textDataObj->GetData(text); + NS_ASSERTION(text.Length() <= (len/2), "Invalid length!"); + stuffToPaste.Assign(text.get(), len / 2); + } else { + nsCOMPtr<nsISupportsCString> textDataObj(do_QueryInterface(genericDataObj)); + if (textDataObj && len > 0) { + nsAutoCString text; + textDataObj->GetData(text); + NS_ASSERTION(text.Length() <= len, "Invalid length!"); + stuffToPaste.Assign(NS_ConvertUTF8toUTF16(Substring(text, 0, len))); + } + } + + if (!stuffToPaste.IsEmpty()) { + AutoEditBatch beginBatching(this); + if (bestFlavor.EqualsLiteral(kHTMLMime)) { + rv = DoInsertHTMLWithContext(stuffToPaste, + aContextStr, aInfoStr, flavor, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } else { + rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection); + } + } + } + } + + // Try to scroll the selection into view if the paste succeeded + if (NS_SUCCEEDED(rv)) { + ScrollSelectionIntoView(false); + } + return rv; +} + +static void +GetStringFromDataTransfer(nsIDOMDataTransfer* aDataTransfer, + const nsAString& aType, + int32_t aIndex, + nsAString& aOutputString) +{ + nsCOMPtr<nsIVariant> variant; + DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(aType, aIndex, getter_AddRefs(variant)); + if (variant) { + variant->GetAsAString(aOutputString); + } +} + +nsresult +HTMLEditor::InsertFromDataTransfer(DataTransfer* aDataTransfer, + int32_t aIndex, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) +{ + ErrorResult rv; + RefPtr<DOMStringList> types = aDataTransfer->MozTypesAt(aIndex, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + bool hasPrivateHTMLFlavor = types->Contains(NS_LITERAL_STRING(kHTMLContext)); + + bool isText = IsPlaintextEditor(); + bool isSafe = IsSafeToInsertData(aSourceDoc); + + uint32_t length = types->Length(); + for (uint32_t t = 0; t < length; t++) { + nsAutoString type; + types->Item(t, type); + + if (!isText) { + if (type.EqualsLiteral(kFileMime) || + type.EqualsLiteral(kJPEGImageMime) || + type.EqualsLiteral(kJPGImageMime) || + type.EqualsLiteral(kPNGImageMime) || + type.EqualsLiteral(kGIFImageMime)) { + nsCOMPtr<nsIVariant> variant; + DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(type, aIndex, getter_AddRefs(variant)); + if (variant) { + nsCOMPtr<nsISupports> object; + variant->GetAsISupports(getter_AddRefs(object)); + return InsertObject(NS_ConvertUTF16toUTF8(type), object, isSafe, + aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection); + } + } else if (type.EqualsLiteral(kNativeHTMLMime)) { + // Windows only clipboard parsing. + nsAutoString text; + GetStringFromDataTransfer(aDataTransfer, type, aIndex, text); + NS_ConvertUTF16toUTF8 cfhtml(text); + + nsXPIDLString cfcontext, cffragment, cfselection; // cfselection left emtpy for now + + nsresult rv = ParseCFHTML(cfhtml, getter_Copies(cffragment), getter_Copies(cfcontext)); + if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) { + AutoEditBatch beginBatching(this); + + if (hasPrivateHTMLFlavor) { + // If we have our private HTML flavor, we will only use the fragment + // from the CF_HTML. The rest comes from the clipboard. + nsAutoString contextString, infoString; + GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLContext), aIndex, contextString); + GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLInfo), aIndex, infoString); + return DoInsertHTMLWithContext(cffragment, + contextString, infoString, type, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } else { + return DoInsertHTMLWithContext(cffragment, + cfcontext, cfselection, type, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } + } + } else if (type.EqualsLiteral(kHTMLMime)) { + nsAutoString text, contextString, infoString; + GetStringFromDataTransfer(aDataTransfer, type, aIndex, text); + GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLContext), aIndex, contextString); + GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLInfo), aIndex, infoString); + + AutoEditBatch beginBatching(this); + if (type.EqualsLiteral(kHTMLMime)) { + return DoInsertHTMLWithContext(text, + contextString, infoString, type, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } + } + } + + if (type.EqualsLiteral(kTextMime) || + type.EqualsLiteral(kMozTextInternal)) { + nsAutoString text; + GetStringFromDataTransfer(aDataTransfer, type, aIndex, text); + + AutoEditBatch beginBatching(this); + return InsertTextAt(text, aDestinationNode, aDestOffset, aDoDeleteSelection); + } + } + + return NS_OK; +} + +bool +HTMLEditor::HavePrivateHTMLFlavor(nsIClipboard* aClipboard) +{ + // check the clipboard for our special kHTMLContext flavor. If that is there, we know + // we have our own internal html format on clipboard. + + NS_ENSURE_TRUE(aClipboard, false); + bool bHavePrivateHTMLFlavor = false; + + const char* flavArray[] = { kHTMLContext }; + + if (NS_SUCCEEDED( + aClipboard->HasDataMatchingFlavors(flavArray, + ArrayLength(flavArray), + nsIClipboard::kGlobalClipboard, + &bHavePrivateHTMLFlavor))) { + return bHavePrivateHTMLFlavor; + } + + return false; +} + + +NS_IMETHODIMP +HTMLEditor::Paste(int32_t aSelectionType) +{ + if (!FireClipboardEvent(ePaste, aSelectionType)) { + return NS_OK; + } + + // Get Clipboard Service + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the nsITransferable interface for getting the data from the clipboard + nsCOMPtr<nsITransferable> trans; + rv = PrepareHTMLTransferable(getter_AddRefs(trans)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE); + // Get the Data from the clipboard + rv = clipboard->GetData(trans, aSelectionType); + NS_ENSURE_SUCCESS(rv, rv); + if (!IsModifiable()) { + return NS_OK; + } + + // also get additional html copy hints, if present + nsAutoString contextStr, infoStr; + + // If we have our internal html flavor on the clipboard, there is special + // context to use instead of cfhtml context. + bool bHavePrivateHTMLFlavor = HavePrivateHTMLFlavor(clipboard); + if (bHavePrivateHTMLFlavor) { + nsCOMPtr<nsISupports> contextDataObj, infoDataObj; + uint32_t contextLen, infoLen; + nsCOMPtr<nsISupportsString> textDataObj; + + nsCOMPtr<nsITransferable> contextTrans = + do_CreateInstance("@mozilla.org/widget/transferable;1"); + NS_ENSURE_TRUE(contextTrans, NS_ERROR_NULL_POINTER); + contextTrans->Init(nullptr); + contextTrans->AddDataFlavor(kHTMLContext); + clipboard->GetData(contextTrans, aSelectionType); + contextTrans->GetTransferData(kHTMLContext, getter_AddRefs(contextDataObj), &contextLen); + + nsCOMPtr<nsITransferable> infoTrans = + do_CreateInstance("@mozilla.org/widget/transferable;1"); + NS_ENSURE_TRUE(infoTrans, NS_ERROR_NULL_POINTER); + infoTrans->Init(nullptr); + infoTrans->AddDataFlavor(kHTMLInfo); + clipboard->GetData(infoTrans, aSelectionType); + infoTrans->GetTransferData(kHTMLInfo, getter_AddRefs(infoDataObj), &infoLen); + + if (contextDataObj) { + nsAutoString text; + textDataObj = do_QueryInterface(contextDataObj); + textDataObj->GetData(text); + NS_ASSERTION(text.Length() <= (contextLen/2), "Invalid length!"); + contextStr.Assign(text.get(), contextLen / 2); + } + + if (infoDataObj) { + nsAutoString text; + textDataObj = do_QueryInterface(infoDataObj); + textDataObj->GetData(text); + NS_ASSERTION(text.Length() <= (infoLen/2), "Invalid length!"); + infoStr.Assign(text.get(), infoLen / 2); + } + } + + // handle transferable hooks + nsCOMPtr<nsIDOMDocument> domdoc; + GetDocument(getter_AddRefs(domdoc)); + if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, trans)) { + return NS_OK; + } + + return InsertFromTransferable(trans, nullptr, contextStr, infoStr, bHavePrivateHTMLFlavor, + nullptr, 0, true); +} + +NS_IMETHODIMP +HTMLEditor::PasteTransferable(nsITransferable* aTransferable) +{ + // Use an invalid value for the clipboard type as data comes from aTransferable + // and we don't currently implement a way to put that in the data transfer yet. + if (!FireClipboardEvent(ePaste, nsIClipboard::kGlobalClipboard)) { + return NS_OK; + } + + // handle transferable hooks + nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument(); + if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, aTransferable)) { + return NS_OK; + } + + nsAutoString contextStr, infoStr; + return InsertFromTransferable(aTransferable, nullptr, contextStr, infoStr, false, + nullptr, 0, true); +} + +/** + * HTML PasteNoFormatting. Ignore any HTML styles and formating in paste source. + */ +NS_IMETHODIMP +HTMLEditor::PasteNoFormatting(int32_t aSelectionType) +{ + if (!FireClipboardEvent(ePaste, aSelectionType)) { + return NS_OK; + } + + ForceCompositionEnd(); + + // Get Clipboard Service + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the nsITransferable interface for getting the data from the clipboard. + // use TextEditor::PrepareTransferable() to force unicode plaintext data. + nsCOMPtr<nsITransferable> trans; + rv = TextEditor::PrepareTransferable(getter_AddRefs(trans)); + if (NS_SUCCEEDED(rv) && trans) { + // Get the Data from the clipboard + if (NS_SUCCEEDED(clipboard->GetData(trans, aSelectionType)) && + IsModifiable()) { + const nsAFlatString& empty = EmptyString(); + rv = InsertFromTransferable(trans, nullptr, empty, empty, false, nullptr, 0, + true); + } + } + + return rv; +} + +// The following arrays contain the MIME types that we can paste. The arrays +// are used by CanPaste() and CanPasteTransferable() below. + +static const char* textEditorFlavors[] = { kUnicodeMime }; +static const char* textHtmlEditorFlavors[] = { kUnicodeMime, kHTMLMime, + kJPEGImageMime, kJPGImageMime, + kPNGImageMime, kGIFImageMime }; + +NS_IMETHODIMP +HTMLEditor::CanPaste(int32_t aSelectionType, + bool* aCanPaste) +{ + NS_ENSURE_ARG_POINTER(aCanPaste); + *aCanPaste = false; + + // can't paste if readonly + if (!IsModifiable()) { + return NS_OK; + } + + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + bool haveFlavors; + + // Use the flavors depending on the current editor mask + if (IsPlaintextEditor()) { + rv = clipboard->HasDataMatchingFlavors(textEditorFlavors, + ArrayLength(textEditorFlavors), + aSelectionType, &haveFlavors); + } else { + rv = clipboard->HasDataMatchingFlavors(textHtmlEditorFlavors, + ArrayLength(textHtmlEditorFlavors), + aSelectionType, &haveFlavors); + } + NS_ENSURE_SUCCESS(rv, rv); + + *aCanPaste = haveFlavors; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable, + bool* aCanPaste) +{ + NS_ENSURE_ARG_POINTER(aCanPaste); + + // can't paste if readonly + if (!IsModifiable()) { + *aCanPaste = false; + return NS_OK; + } + + // If |aTransferable| is null, assume that a paste will succeed. + if (!aTransferable) { + *aCanPaste = true; + return NS_OK; + } + + // Peek in |aTransferable| to see if it contains a supported MIME type. + + // Use the flavors depending on the current editor mask + const char ** flavors; + unsigned length; + if (IsPlaintextEditor()) { + flavors = textEditorFlavors; + length = ArrayLength(textEditorFlavors); + } else { + flavors = textHtmlEditorFlavors; + length = ArrayLength(textHtmlEditorFlavors); + } + + for (unsigned int i = 0; i < length; i++, flavors++) { + nsCOMPtr<nsISupports> data; + uint32_t dataLen; + nsresult rv = aTransferable->GetTransferData(*flavors, + getter_AddRefs(data), + &dataLen); + if (NS_SUCCEEDED(rv) && data) { + *aCanPaste = true; + return NS_OK; + } + } + + *aCanPaste = false; + return NS_OK; +} + +/** + * HTML PasteAsQuotation: Paste in a blockquote type=cite. + */ +NS_IMETHODIMP +HTMLEditor::PasteAsQuotation(int32_t aSelectionType) +{ + if (IsPlaintextEditor()) { + return PasteAsPlaintextQuotation(aSelectionType); + } + + nsAutoString citation; + return PasteAsCitedQuotation(citation, aSelectionType); +} + +NS_IMETHODIMP +HTMLEditor::PasteAsCitedQuotation(const nsAString& aCitation, + int32_t aSelectionType) +{ + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertQuotation, + nsIEditor::eNext); + + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // give rules a chance to handle or cancel + TextRulesInfo ruleInfo(EditAction::insertElement); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel || handled) { + return NS_OK; // rules canceled the operation + } + + nsCOMPtr<Element> newNode = + DeleteSelectionAndCreateElement(*nsGkAtoms::blockquote); + NS_ENSURE_TRUE(newNode, NS_ERROR_NULL_POINTER); + + // Try to set type=cite. Ignore it if this fails. + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("cite"), true); + + // Set the selection to the underneath the node we just inserted: + rv = selection->Collapse(newNode, 0); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensure that the inserted <blockquote> has a frame to make it IsEditable. + FlushFrames(); + + return Paste(aSelectionType); +} + +/** + * Paste a plaintext quotation. + */ +NS_IMETHODIMP +HTMLEditor::PasteAsPlaintextQuotation(int32_t aSelectionType) +{ + // Get Clipboard Service + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Create generic Transferable for getting the data + nsCOMPtr<nsITransferable> trans = + do_CreateInstance("@mozilla.org/widget/transferable;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDocument> destdoc = GetDocument(); + nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr; + trans->Init(loadContext); + + // We only handle plaintext pastes here + trans->AddDataFlavor(kUnicodeMime); + + // Get the Data from the clipboard + clipboard->GetData(trans, aSelectionType); + + // Now we ask the transferable for the data + // it still owns the data, we just have a pointer to it. + // If it can't support a "text" output of the data the call will fail + nsCOMPtr<nsISupports> genericDataObj; + uint32_t len = 0; + nsAutoCString flav; + rv = trans->GetAnyTransferData(flav, getter_AddRefs(genericDataObj), &len); + NS_ENSURE_SUCCESS(rv, rv); + + if (flav.EqualsLiteral(kUnicodeMime)) { + nsCOMPtr<nsISupportsString> textDataObj = do_QueryInterface(genericDataObj); + if (textDataObj && len > 0) { + nsAutoString stuffToPaste; + textDataObj->GetData(stuffToPaste); + NS_ASSERTION(stuffToPaste.Length() <= (len/2), "Invalid length!"); + AutoEditBatch beginBatching(this); + rv = InsertAsPlaintextQuotation(stuffToPaste, true, 0); + } + } + + return rv; +} + +NS_IMETHODIMP +HTMLEditor::InsertTextWithQuotations(const nsAString& aStringToInsert) +{ + AutoEditBatch beginBatching(this); + // The whole operation should be undoable in one transaction: + BeginTransaction(); + + // We're going to loop over the string, collecting up a "hunk" + // that's all the same type (quoted or not), + // Whenever the quotedness changes (or we reach the string's end) + // we will insert the hunk all at once, quoted or non. + + static const char16_t cite('>'); + bool curHunkIsQuoted = (aStringToInsert.First() == cite); + + nsAString::const_iterator hunkStart, strEnd; + aStringToInsert.BeginReading(hunkStart); + aStringToInsert.EndReading(strEnd); + + // In the loop below, we only look for DOM newlines (\n), + // because we don't have a FindChars method that can look + // for both \r and \n. \r is illegal in the dom anyway, + // but in debug builds, let's take the time to verify that + // there aren't any there: +#ifdef DEBUG + nsAString::const_iterator dbgStart (hunkStart); + if (FindCharInReadable('\r', dbgStart, strEnd)) { + NS_ASSERTION(false, + "Return characters in DOM! InsertTextWithQuotations may be wrong"); + } +#endif /* DEBUG */ + + // Loop over lines: + nsresult rv = NS_OK; + nsAString::const_iterator lineStart (hunkStart); + // We will break from inside when we run out of newlines. + for (;;) { + // Search for the end of this line (dom newlines, see above): + bool found = FindCharInReadable('\n', lineStart, strEnd); + bool quoted = false; + if (found) { + // if there's another newline, lineStart now points there. + // Loop over any consecutive newline chars: + nsAString::const_iterator firstNewline (lineStart); + while (*lineStart == '\n') { + ++lineStart; + } + quoted = (*lineStart == cite); + if (quoted == curHunkIsQuoted) { + continue; + } + // else we're changing state, so we need to insert + // from curHunk to lineStart then loop around. + + // But if the current hunk is quoted, then we want to make sure + // that any extra newlines on the end do not get included in + // the quoted section: blank lines flaking a quoted section + // should be considered unquoted, so that if the user clicks + // there and starts typing, the new text will be outside of + // the quoted block. + if (curHunkIsQuoted) { + lineStart = firstNewline; + + // 'firstNewline' points to the first '\n'. We want to + // ensure that this first newline goes into the hunk + // since quoted hunks can be displayed as blocks + // (and the newline should become invisible in this case). + // So the next line needs to start at the next character. + lineStart++; + } + } + + // If no newline found, lineStart is now strEnd and we can finish up, + // inserting from curHunk to lineStart then returning. + const nsAString &curHunk = Substring(hunkStart, lineStart); + nsCOMPtr<nsIDOMNode> dummyNode; + if (curHunkIsQuoted) { + rv = InsertAsPlaintextQuotation(curHunk, false, + getter_AddRefs(dummyNode)); + } else { + rv = InsertText(curHunk); + } + if (!found) { + break; + } + curHunkIsQuoted = quoted; + hunkStart = lineStart; + } + + EndTransaction(); + + return rv; +} + +NS_IMETHODIMP +HTMLEditor::InsertAsQuotation(const nsAString& aQuotedText, + nsIDOMNode** aNodeInserted) +{ + if (IsPlaintextEditor()) { + return InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted); + } + + nsAutoString citation; + return InsertAsCitedQuotation(aQuotedText, citation, false, + aNodeInserted); +} + +// Insert plaintext as a quotation, with cite marks (e.g. "> "). +// This differs from its corresponding method in TextEditor +// in that here, quoted material is enclosed in a <pre> tag +// in order to preserve the original line wrapping. +NS_IMETHODIMP +HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText, + bool aAddCites, + nsIDOMNode** aNodeInserted) +{ + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertQuotation, + nsIEditor::eNext); + + // give rules a chance to handle or cancel + TextRulesInfo ruleInfo(EditAction::insertElement); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel || handled) { + return NS_OK; // rules canceled the operation + } + + // Wrap the inserted quote in a <span> so we can distinguish it. If we're + // inserting into the <body>, we use a <span> which is displayed as a block + // and sized to the screen using 98 viewport width units. + // We could use 100vw, but 98vw avoids a horizontal scroll bar where possible. + // All this is done to wrap overlong lines to the screen and not to the + // container element, the width-restricted body. + nsCOMPtr<Element> newNode = + DeleteSelectionAndCreateElement(*nsGkAtoms::span); + + // If this succeeded, then set selection inside the pre + // so the inserted text will end up there. + // If it failed, we don't care what the return value was, + // but we'll fall through and try to insert the text anyway. + if (newNode) { + // Add an attribute on the pre node so we'll know it's a quotation. + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::mozquote, + NS_LITERAL_STRING("true"), true); + // Allow wrapping on spans so long lines get wrapped to the screen. + nsCOMPtr<nsINode> parent = newNode->GetParentNode(); + if (parent && parent->IsHTMLElement(nsGkAtoms::body)) { + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::style, + NS_LITERAL_STRING("white-space: pre-wrap; display: block; width: 98vw;"), + true); + } else { + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::style, + NS_LITERAL_STRING("white-space: pre-wrap;"), true); + } + + // and set the selection inside it: + selection->Collapse(newNode, 0); + } + + // Ensure that the inserted <span> has a frame to make it IsEditable. + FlushFrames(); + + if (aAddCites) { + rv = TextEditor::InsertAsQuotation(aQuotedText, aNodeInserted); + } else { + rv = TextEditor::InsertText(aQuotedText); + } + // Note that if !aAddCites, aNodeInserted isn't set. + // That's okay because the routines that use aAddCites + // don't need to know the inserted node. + + if (aNodeInserted && NS_SUCCEEDED(rv)) { + *aNodeInserted = GetAsDOMNode(newNode); + NS_IF_ADDREF(*aNodeInserted); + } + + // Set the selection to just after the inserted node: + if (NS_SUCCEEDED(rv) && newNode) { + nsCOMPtr<nsINode> parent = newNode->GetParentNode(); + int32_t offset = parent ? parent->IndexOf(newNode) : -1; + if (parent) { + selection->Collapse(parent, offset + 1); + } + } + return rv; +} + +NS_IMETHODIMP +HTMLEditor::StripCites() +{ + return TextEditor::StripCites(); +} + +NS_IMETHODIMP +HTMLEditor::Rewrap(bool aRespectNewlines) +{ + return TextEditor::Rewrap(aRespectNewlines); +} + +NS_IMETHODIMP +HTMLEditor::InsertAsCitedQuotation(const nsAString& aQuotedText, + const nsAString& aCitation, + bool aInsertHTML, + nsIDOMNode** aNodeInserted) +{ + // Don't let anyone insert html into a "plaintext" editor: + if (IsPlaintextEditor()) { + NS_ASSERTION(!aInsertHTML, "InsertAsCitedQuotation: trying to insert html into plaintext editor"); + return InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted); + } + + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertQuotation, + nsIEditor::eNext); + + // give rules a chance to handle or cancel + TextRulesInfo ruleInfo(EditAction::insertElement); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel || handled) { + return NS_OK; // rules canceled the operation + } + + nsCOMPtr<Element> newNode = + DeleteSelectionAndCreateElement(*nsGkAtoms::blockquote); + NS_ENSURE_TRUE(newNode, NS_ERROR_NULL_POINTER); + + // Try to set type=cite. Ignore it if this fails. + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("cite"), true); + + if (!aCitation.IsEmpty()) { + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::cite, aCitation, true); + } + + // Set the selection inside the blockquote so aQuotedText will go there: + selection->Collapse(newNode, 0); + + // Ensure that the inserted <blockquote> has a frame to make it IsEditable. + FlushFrames(); + + if (aInsertHTML) { + rv = LoadHTML(aQuotedText); + } else { + rv = InsertText(aQuotedText); // XXX ignore charset + } + + if (aNodeInserted && NS_SUCCEEDED(rv)) { + *aNodeInserted = GetAsDOMNode(newNode); + NS_IF_ADDREF(*aNodeInserted); + } + + // Set the selection to just after the inserted node: + if (NS_SUCCEEDED(rv) && newNode) { + nsCOMPtr<nsINode> parent = newNode->GetParentNode(); + int32_t offset = parent ? parent->IndexOf(newNode) : -1; + if (parent) { + selection->Collapse(parent, offset + 1); + } + } + return rv; +} + + +void RemoveBodyAndHead(nsINode& aNode) +{ + nsCOMPtr<nsIContent> body, head; + // find the body and head nodes if any. + // look only at immediate children of aNode. + for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild(); + child; + child = child->GetNextSibling()) { + if (child->IsHTMLElement(nsGkAtoms::body)) { + body = child; + } else if (child->IsHTMLElement(nsGkAtoms::head)) { + head = child; + } + } + if (head) { + ErrorResult ignored; + aNode.RemoveChild(*head, ignored); + } + if (body) { + nsCOMPtr<nsIContent> child = body->GetFirstChild(); + while (child) { + ErrorResult ignored; + aNode.InsertBefore(*child, body, ignored); + child = body->GetFirstChild(); + } + + ErrorResult ignored; + aNode.RemoveChild(*body, ignored); + } +} + +/** + * This function finds the target node that we will be pasting into. aStart is + * the context that we're given and aResult will be the target. Initially, + * *aResult must be nullptr. + * + * The target for a paste is found by either finding the node that contains + * the magical comment node containing kInsertCookie or, failing that, the + * firstChild of the firstChild (until we reach a leaf). + */ +nsresult FindTargetNode(nsIDOMNode *aStart, nsCOMPtr<nsIDOMNode> &aResult) +{ + NS_ENSURE_TRUE(aStart, NS_OK); + + nsCOMPtr<nsIDOMNode> child, tmp; + + nsresult rv = aStart->GetFirstChild(getter_AddRefs(child)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!child) { + // If the current result is nullptr, then aStart is a leaf, and is the + // fallback result. + if (!aResult) { + aResult = aStart; + } + return NS_OK; + } + + do { + // Is this child the magical cookie? + nsCOMPtr<nsIDOMComment> comment = do_QueryInterface(child); + if (comment) { + nsAutoString data; + rv = comment->GetData(data); + NS_ENSURE_SUCCESS(rv, rv); + + if (data.EqualsLiteral(kInsertCookie)) { + // Yes it is! Return an error so we bubble out and short-circuit the + // search. + aResult = aStart; + + // Note: it doesn't matter if this fails. + aStart->RemoveChild(child, getter_AddRefs(tmp)); + + return NS_SUCCESS_EDITOR_FOUND_TARGET; + } + } + + rv = FindTargetNode(child, aResult); + NS_ENSURE_SUCCESS(rv, rv); + + if (rv == NS_SUCCESS_EDITOR_FOUND_TARGET) { + return NS_SUCCESS_EDITOR_FOUND_TARGET; + } + + rv = child->GetNextSibling(getter_AddRefs(tmp)); + NS_ENSURE_SUCCESS(rv, rv); + + child = tmp; + } while (child); + + return NS_OK; +} + +nsresult +HTMLEditor::CreateDOMFragmentFromPaste(const nsAString& aInputString, + const nsAString& aContextStr, + const nsAString& aInfoStr, + nsCOMPtr<nsIDOMNode>* outFragNode, + nsCOMPtr<nsIDOMNode>* outStartNode, + nsCOMPtr<nsIDOMNode>* outEndNode, + int32_t* outStartOffset, + int32_t* outEndOffset, + bool aTrustedInput) +{ + NS_ENSURE_TRUE(outFragNode && outStartNode && outEndNode, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDocument> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + // if we have context info, create a fragment for that + nsresult rv = NS_OK; + nsCOMPtr<nsIDOMNode> contextLeaf; + RefPtr<DocumentFragment> contextAsNode; + if (!aContextStr.IsEmpty()) { + rv = ParseFragment(aContextStr, nullptr, doc, getter_AddRefs(contextAsNode), + aTrustedInput); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(contextAsNode, NS_ERROR_FAILURE); + + rv = StripFormattingNodes(*contextAsNode); + NS_ENSURE_SUCCESS(rv, rv); + + RemoveBodyAndHead(*contextAsNode); + + rv = FindTargetNode(contextAsNode, contextLeaf); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIContent> contextLeafAsContent = do_QueryInterface(contextLeaf); + + // create fragment for pasted html + nsIAtom* contextAtom; + if (contextLeafAsContent) { + contextAtom = contextLeafAsContent->NodeInfo()->NameAtom(); + if (contextLeafAsContent->IsHTMLElement(nsGkAtoms::html)) { + contextAtom = nsGkAtoms::body; + } + } else { + contextAtom = nsGkAtoms::body; + } + RefPtr<DocumentFragment> fragment; + rv = ParseFragment(aInputString, + contextAtom, + doc, + getter_AddRefs(fragment), + aTrustedInput); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(fragment, NS_ERROR_FAILURE); + + RemoveBodyAndHead(*fragment); + + if (contextAsNode) { + // unite the two trees + nsCOMPtr<nsIDOMNode> junk; + contextLeaf->AppendChild(fragment, getter_AddRefs(junk)); + fragment = contextAsNode; + } + + rv = StripFormattingNodes(*fragment, true); + NS_ENSURE_SUCCESS(rv, rv); + + // If there was no context, then treat all of the data we did get as the + // pasted data. + if (contextLeaf) { + *outEndNode = *outStartNode = contextLeaf; + } else { + *outEndNode = *outStartNode = fragment; + } + + *outFragNode = fragment.forget(); + *outStartOffset = 0; + + // get the infoString contents + if (!aInfoStr.IsEmpty()) { + int32_t sep = aInfoStr.FindChar((char16_t)','); + nsAutoString numstr1(Substring(aInfoStr, 0, sep)); + nsAutoString numstr2(Substring(aInfoStr, sep+1, aInfoStr.Length() - (sep+1))); + + // Move the start and end children. + nsresult err; + int32_t num = numstr1.ToInteger(&err); + + nsCOMPtr<nsIDOMNode> tmp; + while (num--) { + (*outStartNode)->GetFirstChild(getter_AddRefs(tmp)); + NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE); + tmp.swap(*outStartNode); + } + + num = numstr2.ToInteger(&err); + while (num--) { + (*outEndNode)->GetLastChild(getter_AddRefs(tmp)); + NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE); + tmp.swap(*outEndNode); + } + } + + nsCOMPtr<nsINode> node = do_QueryInterface(*outEndNode); + *outEndOffset = node->Length(); + return NS_OK; +} + + +nsresult +HTMLEditor::ParseFragment(const nsAString& aFragStr, + nsIAtom* aContextLocalName, + nsIDocument* aTargetDocument, + DocumentFragment** aFragment, + bool aTrustedInput) +{ + nsAutoScriptBlockerSuppressNodeRemoved autoBlocker; + + RefPtr<DocumentFragment> fragment = + new DocumentFragment(aTargetDocument->NodeInfoManager()); + nsresult rv = nsContentUtils::ParseFragmentHTML(aFragStr, + fragment, + aContextLocalName ? + aContextLocalName : nsGkAtoms::body, + kNameSpaceID_XHTML, + false, + true); + if (!aTrustedInput) { + nsTreeSanitizer sanitizer(aContextLocalName ? + nsIParserUtils::SanitizerAllowStyle : + nsIParserUtils::SanitizerAllowComments); + sanitizer.Sanitize(fragment); + } + fragment.forget(aFragment); + return rv; +} + +void +HTMLEditor::CreateListOfNodesToPaste( + DocumentFragment& aFragment, + nsTArray<OwningNonNull<nsINode>>& outNodeList, + nsINode* aStartNode, + int32_t aStartOffset, + nsINode* aEndNode, + int32_t aEndOffset) +{ + // If no info was provided about the boundary between context and stream, + // then assume all is stream. + if (!aStartNode) { + aStartNode = &aFragment; + aStartOffset = 0; + aEndNode = &aFragment; + aEndOffset = aFragment.Length(); + } + + RefPtr<nsRange> docFragRange; + nsresult rv = nsRange::CreateRange(aStartNode, aStartOffset, + aEndNode, aEndOffset, + getter_AddRefs(docFragRange)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, ); + + // Now use a subtree iterator over the range to create a list of nodes + TrivialFunctor functor; + DOMSubtreeIterator iter; + rv = iter.Init(*docFragRange); + NS_ENSURE_SUCCESS(rv, ); + iter.AppendList(functor, outNodeList); +} + +void +HTMLEditor::GetListAndTableParents(StartOrEnd aStartOrEnd, + nsTArray<OwningNonNull<nsINode>>& aNodeList, + nsTArray<OwningNonNull<Element>>& outArray) +{ + MOZ_ASSERT(aNodeList.Length()); + + // Build up list of parents of first (or last) node in list that are either + // lists, or tables. + int32_t idx = aStartOrEnd == StartOrEnd::end ? aNodeList.Length() - 1 : 0; + + for (nsCOMPtr<nsINode> node = aNodeList[idx]; node; + node = node->GetParentNode()) { + if (HTMLEditUtils::IsList(node) || HTMLEditUtils::IsTable(node)) { + outArray.AppendElement(*node->AsElement()); + } + } +} + +int32_t +HTMLEditor::DiscoverPartialListsAndTables( + nsTArray<OwningNonNull<nsINode>>& aPasteNodes, + nsTArray<OwningNonNull<Element>>& aListsAndTables) +{ + int32_t ret = -1; + int32_t listAndTableParents = aListsAndTables.Length(); + + // Scan insertion list for table elements (other than table). + for (auto& curNode : aPasteNodes) { + if (HTMLEditUtils::IsTableElement(curNode) && + !curNode->IsHTMLElement(nsGkAtoms::table)) { + nsCOMPtr<Element> table = curNode->GetParentElement(); + while (table && !table->IsHTMLElement(nsGkAtoms::table)) { + table = table->GetParentElement(); + } + if (table) { + int32_t idx = aListsAndTables.IndexOf(table); + if (idx == -1) { + return ret; + } + ret = idx; + if (ret == listAndTableParents - 1) { + return ret; + } + } + } + if (HTMLEditUtils::IsListItem(curNode)) { + nsCOMPtr<Element> list = curNode->GetParentElement(); + while (list && !HTMLEditUtils::IsList(list)) { + list = list->GetParentElement(); + } + if (list) { + int32_t idx = aListsAndTables.IndexOf(list); + if (idx == -1) { + return ret; + } + ret = idx; + if (ret == listAndTableParents - 1) { + return ret; + } + } + } + } + return ret; +} + +nsINode* +HTMLEditor::ScanForListAndTableStructure( + StartOrEnd aStartOrEnd, + nsTArray<OwningNonNull<nsINode>>& aNodes, + Element& aListOrTable) +{ + // Look upward from first/last paste node for a piece of this list/table + int32_t idx = aStartOrEnd == StartOrEnd::end ? aNodes.Length() - 1 : 0; + bool isList = HTMLEditUtils::IsList(&aListOrTable); + + for (nsCOMPtr<nsINode> node = aNodes[idx]; node; + node = node->GetParentNode()) { + if ((isList && HTMLEditUtils::IsListItem(node)) || + (!isList && HTMLEditUtils::IsTableElement(node) && + !node->IsHTMLElement(nsGkAtoms::table))) { + nsCOMPtr<Element> structureNode = node->GetParentElement(); + if (isList) { + while (structureNode && !HTMLEditUtils::IsList(structureNode)) { + structureNode = structureNode->GetParentElement(); + } + } else { + while (structureNode && + !structureNode->IsHTMLElement(nsGkAtoms::table)) { + structureNode = structureNode->GetParentElement(); + } + } + if (structureNode == &aListOrTable) { + if (isList) { + return structureNode; + } + return node; + } + } + } + return nullptr; +} + +void +HTMLEditor::ReplaceOrphanedStructure( + StartOrEnd aStartOrEnd, + nsTArray<OwningNonNull<nsINode>>& aNodeArray, + nsTArray<OwningNonNull<Element>>& aListAndTableArray, + int32_t aHighWaterMark) +{ + OwningNonNull<Element> curNode = aListAndTableArray[aHighWaterMark]; + + // Find substructure of list or table that must be included in paste. + nsCOMPtr<nsINode> replaceNode = + ScanForListAndTableStructure(aStartOrEnd, aNodeArray, curNode); + + if (!replaceNode) { + return; + } + + // If we found substructure, paste it instead of its descendants. + // Only replace with the substructure if all the nodes in the list are + // descendants. + bool shouldReplaceNodes = true; + for (uint32_t i = 0; i < aNodeArray.Length(); i++) { + uint32_t idx = aStartOrEnd == StartOrEnd::start ? + i : (aNodeArray.Length() - i - 1); + OwningNonNull<nsINode> endpoint = aNodeArray[idx]; + if (!EditorUtils::IsDescendantOf(endpoint, replaceNode)) { + shouldReplaceNodes = false; + break; + } + } + + if (shouldReplaceNodes) { + // Now replace the removed nodes with the structural parent + aNodeArray.Clear(); + if (aStartOrEnd == StartOrEnd::end) { + aNodeArray.AppendElement(*replaceNode); + } else { + aNodeArray.InsertElementAt(0, *replaceNode); + } + } +} + +} // namespace mozilla |