Bug 1421504 - EditorBase should move children carefully r?m_kato draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Wed, 29 Nov 2017 17:57:00 +0900
changeset 705524 02eebc3277dd7a707722c5a9ecfe42fdace8eaec
parent 705491 1937b394c31d7d5fc92c8a3353cc83c65f873c54
child 705789 93e80ca698d592f7dcd55b951a3836615eb3638a
push id91494
push usermasayuki@d-toybox.com
push dateThu, 30 Nov 2017 08:47:59 +0000
reviewersm_kato
bugs1421504
milestone59.0a1
Bug 1421504 - EditorBase should move children carefully r?m_kato While moving children of a container to another container, mutation observer may move children before moving and/or move node immediately after the insertion point. Therefore, EditorBase should store all children which should be moved with a local variable. Then, move one by one carefully. E.g., if a child before being moved is moved to different container, it shouldn't be moved because JS already handles it as expected for the web app. If next sibling of the insertion point has been moved, EditorBase should stop moving the remaining children because it may move children to odd position and it may cause dataloss. This patch creates EditorBase::MoveChildren() and it moves children carefully with above checks. Additionally, making its callers simpler, this patch also creates EditorBase::MovePreviousSiblings() and MoveAllChildren(). MozReview-Commit-ID: FJDdSqXIlqD
editor/libeditor/EditorBase.cpp
editor/libeditor/EditorBase.h
editor/libeditor/EditorDOMPoint.h
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -1864,16 +1864,129 @@ EditorBase::MoveNode(nsIContent* aNode,
   nsCOMPtr<nsINode> kungFuDeathGrip = aNode;
 
   nsresult rv = DeleteNode(aNode);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return InsertNode(*aNode, *aParent, aOffset);
 }
 
+void
+EditorBase::MoveAllChildren(nsINode& aContainer,
+                            const EditorRawDOMPoint& aPointToInsert,
+                            ErrorResult& aError)
+{
+  if (!aContainer.HasChildren()) {
+    return;
+  }
+  nsIContent* firstChild = aContainer.GetFirstChild();
+  if (NS_WARN_IF(!firstChild)) {
+    aError.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+  nsIContent* lastChild = aContainer.GetLastChild();
+  if (NS_WARN_IF(!lastChild)) {
+    aError.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+  return MoveChildren(*firstChild, *lastChild, aPointToInsert, aError);
+}
+
+void
+EditorBase::MovePreviousSiblings(nsIContent& aChild,
+                                 const EditorRawDOMPoint& aPointToInsert,
+                                 ErrorResult& aError)
+{
+  if (NS_WARN_IF(!aChild.GetParentNode())) {
+    aError.Throw(NS_ERROR_INVALID_ARG);
+    return;
+  }
+  nsIContent* firstChild = aChild.GetParentNode()->GetFirstChild();
+  if (NS_WARN_IF(!firstChild)) {
+    aError.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+  nsIContent* lastChild =
+    &aChild == firstChild ? firstChild : aChild.GetPreviousSibling();
+  if (NS_WARN_IF(!lastChild)) {
+    aError.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+  return MoveChildren(*firstChild, *lastChild, aPointToInsert, aError);
+}
+
+void
+EditorBase::MoveChildren(nsIContent& aFirstChild,
+                         nsIContent& aLastChild,
+                         const EditorRawDOMPoint& aPointToInsert,
+                         ErrorResult& aError)
+{
+  nsCOMPtr<nsINode> oldContainer = aFirstChild.GetParentNode();
+  if (NS_WARN_IF(oldContainer != aLastChild.GetParentNode()) ||
+      NS_WARN_IF(!aPointToInsert.IsSet()) ||
+      NS_WARN_IF(!aPointToInsert.Container()->IsContainerNode())) {
+    aError.Throw(NS_ERROR_INVALID_ARG);
+    return;
+  }
+
+  // First, store all children which should be moved to the new container.
+  AutoTArray<nsCOMPtr<nsIContent>, 10> children;
+  for (nsIContent* child = &aFirstChild;
+       child;
+       child = child->GetNextSibling()) {
+    children.AppendElement(child);
+    if (child == &aLastChild) {
+      break;
+    }
+  }
+
+  if (NS_WARN_IF(children.LastElement() != &aLastChild)) {
+    aError.Throw(NS_ERROR_INVALID_ARG);
+    return;
+  }
+
+  nsCOMPtr<nsINode> newContainer = aPointToInsert.Container();
+  nsCOMPtr<nsIContent> nextNode = aPointToInsert.GetChildAtOffset();
+  for (size_t i = children.Length(); i > 0; --i) {
+    nsCOMPtr<nsIContent>& child = children[i - 1];
+    if (child->GetParentNode() != oldContainer) {
+      // If the child has been moved to different container, we shouldn't
+      // touch it.
+      continue;
+    }
+    oldContainer->RemoveChild(*child, aError);
+    if (NS_WARN_IF(aError.Failed())) {
+      return;
+    }
+    if (nextNode) {
+      // If we're not appending the children to the new container, we should
+      // check if referring next node of insertion point is still in the new
+      // container.
+      EditorRawDOMPoint pointToInsert(nextNode);
+      if (NS_WARN_IF(!pointToInsert.IsSet()) ||
+          NS_WARN_IF(pointToInsert.Container() != newContainer)) {
+        // The next node of insertion point has been moved by mutation observer.
+        // Let's stop moving the remaining nodes.
+        // XXX Or should we move remaining children after the last moved child?
+        aError.Throw(NS_ERROR_FAILURE);
+        return;
+      }
+    }
+    newContainer->InsertBefore(*child, nextNode, aError);
+    if (NS_WARN_IF(aError.Failed())) {
+      return;
+    }
+    // If the child was inserted or appended properly, the following children
+    // should be inserted before it.  Otherwise, keep using current position.
+    if (child->GetParentNode() == newContainer) {
+      nextNode = child;
+    }
+  }
+}
+
 NS_IMETHODIMP
 EditorBase::AddEditorObserver(nsIEditorObserver* aObserver)
 {
   // we don't keep ownership of the observers.  They must
   // remove themselves as observers before they are destroyed.
 
   NS_ENSURE_TRUE(aObserver, NS_ERROR_NULL_POINTER);
 
@@ -2975,16 +3088,18 @@ EditorBase::SplitNodeImpl(const EditorDO
   }
 
   nsCOMPtr<nsINode> parent = aStartOfRightNode.Container()->GetParentNode();
   if (NS_WARN_IF(!parent)) {
     aError.Throw(NS_ERROR_FAILURE);
     return;
   }
 
+  // Fix the child before mutation observer may touch the DOM tree.
+  nsIContent* firstChildOfRightNode = aStartOfRightNode.GetChildAtOffset();
   parent->InsertBefore(aNewLeftNode, aStartOfRightNode.Container(),
                        aError);
   if (NS_WARN_IF(aError.Failed())) {
     return;
   }
 
   // At this point, the existing right node has all the children.  Move all
   // the children which are before aStartOfRightNode.
@@ -2999,34 +3114,31 @@ EditorBase::SplitNodeImpl(const EditorDO
                                  leftText);
       rightAsText->DeleteData(0, aStartOfRightNode.Offset());
       // Fix left node
       leftAsText->GetAsText()->SetData(leftText);
     } else {
       MOZ_DIAGNOSTIC_ASSERT(!rightAsText && !leftAsText);
       // Otherwise it's an interior node, so shuffle around the children. Go
       // through list backwards so deletes don't interfere with the iteration.
-      // FYI: It's okay to use raw pointer for caching existing right node since
-      //      it's already grabbed by aStartOfRightNode.
-      nsINode* existingRightNode = aStartOfRightNode.Container();
-      nsCOMPtr<nsINodeList> childNodes = existingRightNode->ChildNodes();
-      // XXX This is wrong loop range if some children has already gone.
-      //     This will be fixed by a later patch.
-      for (int32_t i = aStartOfRightNode.Offset() - 1; i >= 0; i--) {
-        nsCOMPtr<nsIContent> childNode = childNodes->Item(i);
-        MOZ_RELEASE_ASSERT(childNode);
-        existingRightNode->RemoveChild(*childNode, aError);
-        if (NS_WARN_IF(aError.Failed())) {
-          break;
-        }
-        nsCOMPtr<nsIContent> firstChild = aNewLeftNode.GetFirstChild();
-        aNewLeftNode.InsertBefore(*childNode, firstChild, aError);
+      if (!firstChildOfRightNode) {
+        MoveAllChildren(*aStartOfRightNode.Container(),
+                        EditorRawDOMPoint(&aNewLeftNode, 0), aError);
         NS_WARNING_ASSERTION(!aError.Failed(),
-          "Failed to insert a child which is removed from the right node into "
-          "the left node");
+          "Failed to move all children from the right node to the left node");
+      } else if (NS_WARN_IF(aStartOfRightNode.Container() !=
+                              firstChildOfRightNode->GetParentNode())) {
+          // firstChildOfRightNode has been moved by mutation observer.
+          // In this case, we what should we do?  Use offset?  But we cannot
+          // check if the offset is still expected.
+      } else {
+        MovePreviousSiblings(*firstChildOfRightNode,
+                             EditorRawDOMPoint(&aNewLeftNode, 0), aError);
+        NS_WARNING_ASSERTION(!aError.Failed(),
+          "Failed to move some children from the right node to the left node");
       }
     }
   }
 
   // XXX Why do we ignore an error while moving nodes from the right node to
   //     the left node?
   aError.SuppressException();
 
@@ -4079,17 +4191,17 @@ EditorBase::SplitNodeDeep(nsIContent& aM
     nsIContent* currentRightNode = atStartOfRightNode.Container()->AsContent();
 
     // If the split point is middle of the node or the node is not a text node
     // and we're allowed to create empty element node, split it.
     if ((aSplitAtEdges == SplitAtEdges::eAllowToCreateEmptyContainer &&
          !atStartOfRightNode.Container()->GetAsText()) ||
         (!atStartOfRightNode.IsStartOfContainer() &&
          !atStartOfRightNode.IsEndOfContainer())) {
-      ErrorResult error;
+      IgnoredErrorResult error;
       nsCOMPtr<nsIContent> newLeftNode =
         SplitNode(atStartOfRightNode.AsRaw(), error);
       if (NS_WARN_IF(error.Failed())) {
         return SplitNodeResult(NS_ERROR_FAILURE);
       }
 
       if (currentRightNode == &aMostAncestorToSplit) {
         // Actually, we split aMostAncestorToSplit.
--- a/editor/libeditor/EditorBase.h
+++ b/editor/libeditor/EditorBase.h
@@ -377,16 +377,72 @@ public:
    */
   already_AddRefed<nsIContent>
   SplitNode(const EditorRawDOMPoint& aStartOfRightNode,
             ErrorResult& aResult);
 
   nsresult JoinNodes(nsINode& aLeftNode, nsINode& aRightNode);
   nsresult MoveNode(nsIContent* aNode, nsINode* aParent, int32_t aOffset);
 
+  /**
+   * MoveAllChildren() moves all children of aContainer to before
+   * aPointToInsert.GetChildAtOffset().
+   * See explanation of MoveChildren() for the detail of the behavior.
+   *
+   * @param aContainer          The container node whose all children should
+   *                            be moved.
+   * @param aPointToInsert      The insertion point.  The container must not
+   *                            be a data node like a text node.
+   * @param aError              The result.  If this succeeds to move children,
+   *                            returns NS_OK.  Otherwise, an error.
+   */
+  void MoveAllChildren(nsINode& aContainer,
+                       const EditorRawDOMPoint& aPointToInsert,
+                       ErrorResult& aError);
+
+  /**
+   * MovePreviousSiblings() moves all siblings before aChild (i.e., aChild
+   * won't be moved) to before aPointToInsert.GetChildAtOffset().
+   * See explanation of MoveChildren() for the detail of the behavior.
+   *
+   * @param aChild              The node which is next sibling of the last
+   *                            node to be moved.
+   * @param aPointToInsert      The insertion point.  The container must not
+   *                            be a data node like a text node.
+   * @param aError              The result.  If this succeeds to move children,
+   *                            returns NS_OK.  Otherwise, an error.
+   */
+  void MovePreviousSiblings(nsIContent& aChild,
+                            const EditorRawDOMPoint& aPointToInsert,
+                            ErrorResult& aError);
+
+  /**
+   * MoveChildren() moves all children between aFirstChild and aLastChild to
+   * before aPointToInsert.GetChildAtOffset().
+   * If some children are moved to different container while this method
+   * moves other children, they are just ignored.
+   * If the child node referred by aPointToInsert is moved to different
+   * container while this method moves children, returns error.
+   *
+   * @param aFirstChild         The first child which should be moved to
+   *                            aPointToInsert.
+   * @param aLastChild          The last child which should be moved.  This
+   *                            must be a sibling of aFirstChild and it should
+   *                            be positioned after aFirstChild in the DOM tree
+   *                            order.
+   * @param aPointToInsert      The insertion point.  The container must not
+   *                            be a data node like a text node.
+   * @param aError              The result.  If this succeeds to move children,
+   *                            returns NS_OK.  Otherwise, an error.
+   */
+  void MoveChildren(nsIContent& aFirstChild,
+                    nsIContent& aLastChild,
+                    const EditorRawDOMPoint& aPointToInsert,
+                    ErrorResult& aError);
+
   nsresult CloneAttribute(nsAtom* aAttribute, Element* aDestElement,
                           Element* aSourceElement);
   nsresult RemoveAttribute(Element* aElement, nsAtom* aAttribute);
   virtual nsresult RemoveAttributeOrEquivalent(Element* aElement,
                                                nsAtom* aAttribute,
                                                bool aSuppressTransaction) = 0;
   nsresult SetAttribute(Element* aElement, nsAtom* aAttribute,
                         const nsAString& aValue);
--- a/editor/libeditor/EditorDOMPoint.h
+++ b/editor/libeditor/EditorDOMPoint.h
@@ -672,16 +672,17 @@ ImplCycleCollectionTraverse(nsCycleColle
  */
 class MOZ_STACK_CLASS AutoEditorDOMPointOffsetInvalidator final
 {
 public:
   explicit AutoEditorDOMPointOffsetInvalidator(EditorDOMPoint& aPoint)
     : mPoint(aPoint)
   {
     MOZ_ASSERT(aPoint.IsSetAndValid());
+    MOZ_ASSERT(mPoint.Container()->IsContainerNode());
     mChild = mPoint.GetChildAtOffset();
   }
 
   ~AutoEditorDOMPointOffsetInvalidator()
   {
     InvalidateOffset();
   }