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(); |
} |