Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(291)

Unified Diff: third_party/WebKit/Source/core/dom/ContainerNode.cpp

Issue 2478573002: Make removeChild, parserRemoveChild and removeChildren more consistent. (Closed)
Patch Set: SubframeLoadingDisabler (oldChild) Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « third_party/WebKit/Source/core/dom/ContainerNode.h ('k') | third_party/WebKit/Source/core/dom/Document.cpp » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: third_party/WebKit/Source/core/dom/ContainerNode.cpp
diff --git a/third_party/WebKit/Source/core/dom/ContainerNode.cpp b/third_party/WebKit/Source/core/dom/ContainerNode.cpp
index a2ce787cc8e9cc6fcfb7bfad0ca1587b978746c2..d144a58419750cbeeec3b5f912afe5d72813fa79 100644
--- a/third_party/WebKit/Source/core/dom/ContainerNode.cpp
+++ b/third_party/WebKit/Source/core/dom/ContainerNode.cpp
@@ -438,43 +438,6 @@ Node* ContainerNode::replaceChild(Node* newChild,
return oldChild;
}
-void ContainerNode::willRemoveChild(Node& child) {
- DCHECK_EQ(child.parentNode(), this);
- ChildListMutationScope(*this).willRemoveChild(child);
- child.notifyMutationObserversNodeWillDetach();
- dispatchChildRemovalEvents(child);
- ChildFrameDisconnector(child).disconnect();
- if (document() != child.document()) {
- // |child| was moved another document by DOM mutation event handler.
- return;
- }
-
- // |nodeWillBeRemoved()| must be run after |ChildFrameDisconnector|, because
- // |ChildFrameDisconnector| can run script which may cause state that is to
- // be invalidated by removing the node.
- ScriptForbiddenScope scriptForbiddenScope;
- EventDispatchForbiddenScope assertNoEventDispatch;
- // e.g. mutation event listener can create a new range.
- document().nodeWillBeRemoved(child);
-}
-
-void ContainerNode::willRemoveChildren() {
- NodeVector children;
- getChildNodes(*this, children);
-
- ChildListMutationScope mutation(*this);
- for (const auto& node : children) {
- DCHECK(node);
- Node& child = *node;
- mutation.willRemoveChild(child);
- child.notifyMutationObserversNodeWillDetach();
- dispatchChildRemovalEvents(child);
- }
-
- ChildFrameDisconnector(*this).disconnect(
- ChildFrameDisconnector::DescendantsOnly);
-}
-
DEFINE_TRACE(ContainerNode) {
visitor->trace(m_firstChild);
visitor->trace(m_lastChild);
@@ -489,60 +452,100 @@ DEFINE_TRACE_WRAPPERS(ContainerNode) {
Node* ContainerNode::removeChild(Node* oldChild,
ExceptionState& exceptionState) {
- // NotFoundError: Raised if oldChild is not a child of this node.
- // FIXME: We should never really get PseudoElements in here, but editing will
- // sometimes attempt to remove them still. We should fix that and enable this
- // DCHECK. DCHECK(!oldChild->isPseudoElement())
- if (!oldChild || oldChild->parentNode() != this ||
- oldChild->isPseudoElement()) {
+ if (!oldChild || oldChild->parentNode() != this) {
exceptionState.throwDOMException(
NotFoundError, "The node to be removed is not a child of this node.");
return nullptr;
}
- Node* child = oldChild;
+ // TODO(esprehn): We should never really get PseudoElements in here, but
+ // editing will sometimes attempt to remove them still. We should fix that and
+ // turn this into an assert.
+ if (oldChild->isPseudoElement()) {
+ exceptionState.throwDOMException(NotFoundError,
+ "Pseudo elements cannot be removed.");
+ return nullptr;
+ }
+
+ // MutationObserver, no script here.
+ ChildListMutationScope(*this).willRemoveChild(*oldChild);
+ oldChild->notifyMutationObserversNodeWillDetach();
- document().removeFocusedElementOfSubtree(child);
+ // Run script in 'DOMNodeRemoved' and 'DOMNodeRemovedFromDocument' event
+ // handlers.
+ dispatchChildRemovalEvents(*oldChild);
- // Events fired when blurring currently focused node might have moved this
- // child into a different parent.
- if (child->parentNode() != this) {
+ if (oldChild->parentNode() != this) {
exceptionState.throwDOMException(NotFoundError,
"The node to be removed is no longer a "
"child of this node. Perhaps it was moved "
- "in a 'blur' event handler?");
+ "in response to a mutation?");
return nullptr;
}
- willRemoveChild(*child);
+ // Run script in 'unload' events for <iframe>, <object>, <embed>, etc.
+ ChildFrameDisconnector(*oldChild).disconnect();
- // Mutation events might have moved this child into a different parent.
- if (child->parentNode() != this) {
+ // Forbid loading frames inside the steps below that run script since we'd
+ // never disconnect them.
+ SubframeLoadingDisabler subframeLoadingDisabler(*oldChild);
+
+ if (oldChild->parentNode() != this) {
exceptionState.throwDOMException(NotFoundError,
"The node to be removed is no longer a "
"child of this node. Perhaps it was moved "
- "in response to a mutation?");
+ "in an 'unload' event handler?");
+ return nullptr;
+ }
+
+ // Run script in blur events.
+ document().removeFocusedElementOfSubtree(oldChild);
+
+ if (oldChild->parentNode() != this) {
+ exceptionState.throwDOMException(NotFoundError,
+ "The node to be removed is no longer a "
+ "child of this node. Perhaps it was moved "
+ "in a 'blur' event handler?");
return nullptr;
}
{
+ // Script may run inside the UpdateSuspendScope as a result of plugins.
HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates;
DocumentOrderedMap::RemoveScope treeRemoveScope;
- Node* prev = child->previousSibling();
- Node* next = child->nextSibling();
- removeBetween(prev, next, *child);
- notifyNodeRemoved(*child);
- childrenChanged(ChildrenChange::forRemoval(*child, prev, next,
+ Node* prev = oldChild->previousSibling();
+ Node* next = oldChild->nextSibling();
+
+ {
+ EventDispatchForbiddenScope forbidEvents;
+ ScriptForbiddenScope forbidScript;
+
+ if (document() == oldChild->document())
+ document().nodeWillBeRemoved(*oldChild);
+
+ // Actually remove the node from the tree and then call removedFrom on it
+ // and all descendants.
+ removeBetween(prev, next, *oldChild);
+ notifyNodeRemoved(*oldChild);
+ }
+
+ // Script could run inside childrenChanged, enable iframe loading again.
dcheng 2016/11/07 20:50:57 Nit: it's not clear to me what "enable iframe load
esprehn 2016/11/07 22:42:33 There is one on the stack, see above subframeLoadi
+ // TODO(esprehn): Can script run inside childrenChanged for Removal? It
+ // looks like it can only happen for Insertion.
+ childrenChanged(ChildrenChange::forRemoval(*oldChild, prev, next,
ChildrenChangeSourceAPI));
}
+
+ // Run script in DOMSubtreeModified event handlers.
dispatchSubtreeModifiedEvent();
- return child;
+ return oldChild;
}
void ContainerNode::removeBetween(Node* previousChild,
Node* nextChild,
Node& oldChild) {
+ ScriptForbiddenScope scriptForbiddenScope;
EventDispatchForbiddenScope assertNoEventDispatch;
DCHECK_EQ(oldChild.parentNode(), this);
@@ -568,30 +571,59 @@ void ContainerNode::removeBetween(Node* previousChild,
document().adoptIfNeeded(oldChild);
}
+// TODO(esprehn): This function skips MutationEvents but runs script in lots of
+// other ways, perhaps we should change to dispatching those too and just call
+// removeChild() inside the parser?
void ContainerNode::parserRemoveChild(Node& oldChild) {
DCHECK_EQ(oldChild.parentNode(), this);
DCHECK(!oldChild.isDocumentFragment());
- // This may cause arbitrary Javascript execution via onunload handlers.
- if (oldChild.connectedSubframeCount())
- ChildFrameDisconnector(oldChild).disconnect();
+ // MutationObserver, no script here.
+ ChildListMutationScope(*this).willRemoveChild(oldChild);
+ oldChild.notifyMutationObserversNodeWillDetach();
+
+ // Run script in 'unload' events for <iframe>, <object>, <embed>, etc.
+ ChildFrameDisconnector(oldChild).disconnect();
+
+ // Forbid loading frames inside the steps below that run script since we'd
+ // never disconnect them.
+ SubframeLoadingDisabler subframeLoadingDisabler(oldChild);
dcheng 2016/11/07 20:50:57 I'd like to understand why this needs to be scoped
esprehn 2016/11/07 22:42:32 Anything that finds a ScriptForbiddenScope bypass
if (oldChild.parentNode() != this)
return;
- ChildListMutationScope(*this).willRemoveChild(oldChild);
- oldChild.notifyMutationObserversNodeWillDetach();
+ // TODO(esprehn): Why don't we call removeFocusedElementOfSubtree? This means
+ // focus doesn't get updated when the parser moves children around.
+
+ {
+ // Script may run inside the UpdateSuspendScope as a result of plugins.
+ HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates;
+ DocumentOrderedMap::RemoveScope treeRemoveScope;
- HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates;
- DocumentOrderedMap::RemoveScope treeRemoveScope;
+ Node* prev = oldChild.previousSibling();
+ Node* next = oldChild.nextSibling();
- Node* prev = oldChild.previousSibling();
- Node* next = oldChild.nextSibling();
- removeBetween(prev, next, oldChild);
+ {
+ EventDispatchForbiddenScope forbidEvents;
+ ScriptForbiddenScope forbidScript;
- notifyNodeRemoved(oldChild);
- childrenChanged(ChildrenChange::forRemoval(oldChild, prev, next,
- ChildrenChangeSourceParser));
+ // TODO(esprehn): Why don't we call document().nodeWillBeRemoved(oldChild)
+ // here? This means the parser can corrupt ranges, selection, node
+ // iterators and more.
+
+ // Actually remove the node from the tree and then call removedFrom on it.
+ // No script can run here.
+ removeBetween(prev, next, oldChild);
+ notifyNodeRemoved(oldChild);
+ }
+
+ // Script could run inside childrenChanged, enable iframe loading again.
+ // TODO(esprehn): Can script run inside childrenChanged for Removal? It
+ // looks like it can only happen for Insertion.
+ subframeLoadingDisabler.release();
+ childrenChanged(ChildrenChange::forRemoval(oldChild, prev, next,
+ ChildrenChangeSourceParser));
+ }
}
// This differs from other remove functions because it forcibly removes all the
@@ -600,43 +632,73 @@ void ContainerNode::removeChildren(SubtreeModificationAction action) {
if (!m_firstChild)
return;
- // Do any prep work needed before actually starting to detach
- // and remove... e.g. stop loading frames, fire unload events.
- willRemoveChildren();
+ // TODO(esprehn): We shouldn't allow removing PseudoElements here.
{
- // Removing focus can cause frames to load, either via events (focusout,
- // blur) or widget updates (e.g., for <embed>).
- SubframeLoadingDisabler disabler(*this);
+ NodeVector children;
+ getChildNodes(*this, children);
+
+ // This must be inside a block scope before the below steps so
+ // MutationRecords are visible in takeRecords() inside those event
+ // handlers.
+ ChildListMutationScope mutation(*this);
+ for (const auto& oldChild : children) {
+ // MutationObserver, no script here.
+ mutation.willRemoveChild(*oldChild);
+ oldChild->notifyMutationObserversNodeWillDetach();
+
+ // TODO(esprehn): removeChild() exposes the MutationRecords to the
+ // child removal event handlers, but this function doesn't because we
+ // batch the records in the ChildListMutationScope above. Should we
+ // make these consistent?
+
+ // Run script in 'DOMNodeRemoved' and 'DOMNodeRemovedFromDocument' event
+ // handlers.
+ dispatchChildRemovalEvents(*oldChild);
+ }
+ }
+
+ // Run script in 'unload' events for <iframe>, <object>, <embed>, etc.
+ ChildFrameDisconnector(*this).disconnect(
+ ChildFrameDisconnector::DescendantsOnly);
- // Exclude this node when looking for removed focusedElement since only
- // children will be removed.
- // This must be later than willRemoveChildren, which might change focus
- // state of a child.
- document().removeFocusedElementOfSubtree(this, true);
+ // Forbid loading frames inside the steps below that run script since we'd
+ // never disconnect them.
+ SubframeLoadingDisabler subframeLoadingDisabler(*this);
- // Removing a node from a selection can cause widget updates.
- document().nodeChildrenWillBeRemoved(*this);
- }
+ // Exclude this node when looking for removed focusedElement since only
+ // children will be removed.
+ document().removeFocusedElementOfSubtree(this, true);
dcheng 2016/11/07 20:50:57 From reading this, perhaps we should just scope Su
esprehn 2016/11/07 22:42:32 That wouldn't have prevented the recent UXSS, sinc
dcheng 2016/11/08 01:51:12 Sorry, one more question, because I'd really like
{
+ // Script may run inside the UpdateSuspendScope as a result of plugins.
HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates;
DocumentOrderedMap::RemoveScope treeRemoveScope;
+
{
EventDispatchForbiddenScope assertNoEventDispatch;
ScriptForbiddenScope forbidScript;
- while (Node* child = m_firstChild) {
- removeBetween(0, child->nextSibling(), *child);
- notifyNodeRemoved(*child);
+ document().nodeChildrenWillBeRemoved(*this);
+
+ // Remove each node from the tree and then call removedFrom on it and all
+ // descendants.
+ while (Node* oldChild = m_firstChild) {
+ removeBetween(0, oldChild->nextSibling(), *oldChild);
+ notifyNodeRemoved(*oldChild);
}
}
+ // Script could run inside childrenChanged, enable iframe loading again.
+ // TODO(esprehn): Can script run inside childrenChanged for Removal? It
+ // looks like it can only happen for Insertion.
+ subframeLoadingDisabler.release();
ChildrenChange change = {AllChildrenRemoved, nullptr, nullptr, nullptr,
ChildrenChangeSourceAPI};
childrenChanged(change);
}
+ // Run script in DOMSubtreeModified event handlers.
if (action == DispatchSubtreeModifiedEvent)
dispatchSubtreeModifiedEvent();
}
« no previous file with comments | « third_party/WebKit/Source/core/dom/ContainerNode.h ('k') | third_party/WebKit/Source/core/dom/Document.cpp » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698