gecko-dev/editor/libeditor/HTMLEditorDataTransfer.cpp
Masayuki Nakano 7436f01057 Bug 564411 Move all methods/attributes of nsIEditorIMESupport to nsIEditor r=smaug
Doing QI from nsIEditor to nsIEditorIMESupport doesn't make sense because editor should always support all methods and attributes of nsIEditorIMESupport (it does NOT mean that all nsIEditor implementation need to support IME).

This patch moves all of them to nsIEditor for avoiding redundant QIs.

MozReview-Commit-ID: DzIKuGHG4iy

--HG--
extra : rebase_source : cc5e9a6ae4572ebe461d9770ffa5c23d33dc8526
2016-12-20 21:47:31 +09:00

2394 lines
83 KiB
C++

/* -*- 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 "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, CallerType::System, 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);
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)
{
// 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;
}
}
// 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);
}
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);
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);
MOZ_ASSERT_IF(contextLeaf, contextLeafAsContent);
// 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
IgnoredErrorResult ignored;
contextLeafAsContent->AppendChild(*fragment, ignored);
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