gecko-dev/editor/libeditor/HTMLEditorDataTransfer.cpp

2403 строки
84 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 "nsIEditRules.h"
#include "nsIFile.h"
#include "nsIInputStream.h"
#include "nsIMIMEService.h"
#include "nsNameSpaceManager.h"
#include "nsINode.h"
#include "nsIParserUtils.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);
uint32_t childOffset = range->StartOffset();
nsCOMPtr<nsIDOMNode> nodeToInsert;
docfrag->GetFirstChild(getter_AddRefs(nodeToInsert));
while (nodeToInsert) {
rv = InsertNode(nodeToInsert, parent,
static_cast<int32_t>(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 nsString& 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) &&
!IsVisibleBRElement(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 (!IsVisibleBRElement(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 nsString& fragUcs2Str = NS_ConvertUTF8toUTF16(fragmentUTF8);
const nsString& 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(ePasteNoFormatting, 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 nsString& 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;
// Always enable the paste command when inside of a HTML or XHTML document.
nsCOMPtr<nsIDocument> doc = GetDocument();
if (doc && doc->IsHTMLOrXHTML()) {
*aCanPaste = true;
return NS_OK;
}
// 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;
// '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);
}
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* aStartContainer,
int32_t aStartOffset,
nsINode* aEndContainer,
int32_t aEndOffset)
{
// If no info was provided about the boundary between context and stream,
// then assume all is stream.
if (!aStartContainer) {
aStartContainer = &aFragment;
aStartOffset = 0;
aEndContainer = &aFragment;
aEndOffset = aFragment.Length();
}
RefPtr<nsRange> docFragRange;
nsresult rv = nsRange::CreateRange(aStartContainer, aStartOffset,
aEndContainer, 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.
// Postprocess list to remove any descendants of this node so that we don't
// insert them twice.
uint32_t removedCount = 0;
uint32_t originalLength = aNodeArray.Length();
for (uint32_t i = 0; i < originalLength; i++) {
uint32_t idx = aStartOrEnd == StartOrEnd::start ?
(i - removedCount) : (originalLength - i - 1);
OwningNonNull<nsINode> endpoint = aNodeArray[idx];
if (endpoint == replaceNode ||
EditorUtils::IsDescendantOf(endpoint, replaceNode)) {
aNodeArray.RemoveElementAt(idx);
removedCount++;
}
}
// Now replace the removed nodes with the structural parent
if (aStartOrEnd == StartOrEnd::end) {
aNodeArray.AppendElement(*replaceNode);
} else {
aNodeArray.InsertElementAt(0, *replaceNode);
}
}
} // namespace mozilla