parserRemoveChild should unload subframes
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 13 Feb 2017 04:21:03 +0000 (04:21 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 13 Feb 2017 04:21:03 +0000 (04:21 +0000)
https://bugs.webkit.org/show_bug.cgi?id=168151

Reviewed by Darin Adler.

Source/WebCore:

Fix the bug that the adoption agency algorithm does not unload subframes as it disconnects nodes.

Also moved calls to nodeWillBeRemoved inside NoEventDispatchAssertion to expand on r211965.

Tests: fast/parser/adoption-agency-clear-focus-range.html
       fast/parser/adoption-agency-unload-iframe-1.html
       fast/parser/adoption-agency-unload-iframe-2.html

* dom/ContainerNode.cpp:
(WebCore::ContainerNode::takeAllChildrenFrom): Rewritten using idioms used in removeChildren and parserAppendChild.

Disconnect all subframes first since this can synchronously dispatch an unload event. Then update DOM ranges,
the focused element, and other states in the document.

Second, use the regular removeBetween, notifyChildNodeRemoved, childrenChanged sequence of calls to disconnect nodes
instead of a single call to removeDetachedChildren to properly disconnect child nodes since those nodes may have
already come live due to execution of synchronous scripts prior to the adoption agency algorithm has run, or in
response to the unload event we just dispatched.

Third, append these nodes using parserAppendChild to avoid dispatching mutation events.

(WebCore::willRemoveChild): Removed the call to nodeWillBeRemoved. It's now called within NoEventDispatchAssertion
in each call site of willRemoveChild and willRemoveChildren.
(WebCore::willRemoveChildren): Ditto.
(WebCore::ContainerNode::removeChild): Call nodeWillBeRemoved inside NoEventDispatchAssertion.
(WebCore::ContainerNode::replaceAllChildren): Call nodeWillBeRemoved inside NoEventDispatchAssertion.
(WebCore::ContainerNode::parserRemoveChild): Disconnect subframes and update document's states.

* html/parser/HTMLConstructionSite.cpp:
(WebCore::executeTakeAllChildrenAndReparentTask): Add a release assert that new parent does not already have a parent.

LayoutTests:

Add two W3C-style testharness tests for unloading iframes inside the adoption agency algorithm.

Also added a test to make sure ContainerNode::takeAllChildrenFrom adjusts the focused element and DOM ranges.

* fast/css/stylesheet-candidate-nodes-crash-expected.txt: Rebaselined. The difference comes from the fact
iframe now is unloaded in parserRemoveChild as expected and then reloaded in parserAppendChild inside
insertErrorMessageBlock as opposed to after the parser had completed as if the iframe had never been detached.
* fast/parser/adoption-agency-clear-focus-range-expected.txt: Added.
* fast/parser/adoption-agency-clear-focus-range.html: Added.
* fast/parser/adoption-agency-unload-iframe-1-expected.txt: Added.
* fast/parser/adoption-agency-unload-iframe-1.html: Added.
* fast/parser/adoption-agency-unload-iframe-2-expected.txt: Added.
* fast/parser/adoption-agency-unload-iframe-2.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@212218 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/fast/css/stylesheet-candidate-nodes-crash-expected.txt
LayoutTests/fast/parser/adoption-agency-clear-focus-range-expected.txt [new file with mode: 0644]
LayoutTests/fast/parser/adoption-agency-clear-focus-range.html [new file with mode: 0644]
LayoutTests/fast/parser/adoption-agency-unload-iframe-1-expected.txt [new file with mode: 0644]
LayoutTests/fast/parser/adoption-agency-unload-iframe-1.html [new file with mode: 0644]
LayoutTests/fast/parser/adoption-agency-unload-iframe-2-expected.txt [new file with mode: 0644]
LayoutTests/fast/parser/adoption-agency-unload-iframe-2.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/ContainerNode.cpp
Source/WebCore/html/parser/HTMLConstructionSite.cpp

index ee95bdf..70dd924 100644 (file)
@@ -1,5 +1,26 @@
 2017-02-12  Ryosuke Niwa  <rniwa@webkit.org>
 
+        parserRemoveChild should unload subframes
+        https://bugs.webkit.org/show_bug.cgi?id=168151
+
+        Reviewed by Darin Adler.
+
+        Add two W3C-style testharness tests for unloading iframes inside the adoption agency algorithm.
+
+        Also added a test to make sure ContainerNode::takeAllChildrenFrom adjusts the focused element and DOM ranges.
+
+        * fast/css/stylesheet-candidate-nodes-crash-expected.txt: Rebaselined. The difference comes from the fact
+        iframe now is unloaded in parserRemoveChild as expected and then reloaded in parserAppendChild inside
+        insertErrorMessageBlock as opposed to after the parser had completed as if the iframe had never been detached.
+        * fast/parser/adoption-agency-clear-focus-range-expected.txt: Added.
+        * fast/parser/adoption-agency-clear-focus-range.html: Added.
+        * fast/parser/adoption-agency-unload-iframe-1-expected.txt: Added.
+        * fast/parser/adoption-agency-unload-iframe-1.html: Added.
+        * fast/parser/adoption-agency-unload-iframe-2-expected.txt: Added.
+        * fast/parser/adoption-agency-unload-iframe-2.html: Added.
+
+2017-02-12  Ryosuke Niwa  <rniwa@webkit.org>
+
         REGRESSION (r179497): Crash inside setAttributeNode
         https://bugs.webkit.org/show_bug.cgi?id=168161
         <rdar://problem/30451581>
index 7ef22e9..d32ffd0 100644 (file)
@@ -1 +1,7 @@
+This page contains the following errors:
+
+error on line 29 at column 9: Double hyphen within comment
+error on line 32 at column 1: Comment not terminated
+Below is a rendering of the page up to the first error.
+
 PASS
diff --git a/LayoutTests/fast/parser/adoption-agency-clear-focus-range-expected.txt b/LayoutTests/fast/parser/adoption-agency-clear-focus-range-expected.txt
new file mode 100644 (file)
index 0000000..61d5fe0
--- /dev/null
@@ -0,0 +1,3 @@
+Active Element must be moved to body: PASS
+range.startContainer must be moved to p: PASS
+
diff --git a/LayoutTests/fast/parser/adoption-agency-clear-focus-range.html b/LayoutTests/fast/parser/adoption-agency-clear-focus-range.html
new file mode 100644 (file)
index 0000000..1f4de8b
--- /dev/null
@@ -0,0 +1,84 @@
+<script>
+
+function runTest() {
+    document.write(`<a><p><span tabindex="0">dummy</span><iframe onload="didLoadIframe()"></iframe><script> document.body.innerHTML = ''; <\/script>`);
+}
+
+let callCount = 0;
+let span;
+let range;
+function didLoadIframe() {
+    callCount++;
+    if (callCount == 1)
+        document.write('</a>');
+    else if (callCount == 2) {
+        // Test ContainerNode::takeAllChildrenFrom adjusts focused node and update ranges.
+        span = document.querySelector('span');
+        span.focus();
+        range = new Range;
+        range.selectNode(span.firstChild);
+    } else if (callCount == 3) {
+        setTimeout(() => {
+            let activeElementResult = (document.activeElement == document.body) ? 'PASS' : `FAIL - it was set on ${document.activeElement}`;
+            let rangeStartResult = (range.startContainer == document.querySelector('p')) ? 'PASS' : `FAIL - it was set on ${range.startContainer}`;
+
+            document.documentElement.innerHTML = `<body>
+            Active Element must be moved to body: ${activeElementResult}<br>
+            range.startContainer must be moved to p: ${rangeStartResult}<br>`;
+
+            if (window.testRunner)
+                testRunner.notifyDone();
+        }, 0);
+    }
+}
+
+/*
+
+ 0. The outer document.write creates the following tree:
+    body
+      + a
+        + p
+          + span
+          + iframe
+
+ 1. iframe's load event fires, and invokes the inner document.write,
+    which forces <script> from the outer document.write to be parsed:
+    body
+      + a
+        + p
+          + span
+          + iframe
+          + script
+
+   1.a. The script runs, and invokes document.body.innerHTML = '':
+        body
+
+   1.b. The adoption agency algorithm is involved, and p is removed from a
+        while both of them are detached from body.
+
+   1.c. The adoption agency algorithm inserts p back into body along with iframe.
+        body
+          + p
+            + span
+            + iframe
+            + script
+
+ 2. iframe's load event fires for the second time per step 1.c.
+
+    2.a. the focus is set on span and a range is created inside span.
+
+    2.b. The adoption agency algorithm now takes all children of p
+         and inserts them back under "a", and then inserts "p" under body.
+
+3. iframe's load event fires for the third time per step 2.b.
+
+*/
+
+if (window.testRunner) {
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+}
+
+runTest();
+
+</script>
diff --git a/LayoutTests/fast/parser/adoption-agency-unload-iframe-1-expected.txt b/LayoutTests/fast/parser/adoption-agency-unload-iframe-1-expected.txt
new file mode 100644 (file)
index 0000000..477d3dc
--- /dev/null
@@ -0,0 +1,3 @@
+
+PASS An iframe removed by the adoption agency algorithm must be unloaded 
+
diff --git a/LayoutTests/fast/parser/adoption-agency-unload-iframe-1.html b/LayoutTests/fast/parser/adoption-agency-unload-iframe-1.html
new file mode 100644 (file)
index 0000000..4938ad2
--- /dev/null
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<head>
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+</head>
+<body>
+<table><a><p><script>
+
+let oldBody = document.body;
+oldBody.remove();
+
+let a = oldBody.querySelector('a');
+document.documentElement.appendChild(a);
+/* html
+     + a
+       + p
+         + script */
+
+let iframe = document.createElement('iframe');
+a.firstChild.appendChild(iframe);
+/* html
+     + a
+       + p
+         + script
+         + iframe */
+
+let oldGlobal = iframe.contentWindow;
+
+window.onload = () => {
+    document.documentElement.appendChild(document.createElement('body'));
+    let test = async_test('An iframe removed by the adoption agency algorithm must be unloaded');
+    test.step(() => {
+        assert_not_equals(oldGlobal, iframe.contentWindow);
+    });
+    test.done();
+}
+
+</script></a></p>
diff --git a/LayoutTests/fast/parser/adoption-agency-unload-iframe-2-expected.txt b/LayoutTests/fast/parser/adoption-agency-unload-iframe-2-expected.txt
new file mode 100644 (file)
index 0000000..477d3dc
--- /dev/null
@@ -0,0 +1,3 @@
+
+PASS An iframe removed by the adoption agency algorithm must be unloaded 
+
diff --git a/LayoutTests/fast/parser/adoption-agency-unload-iframe-2.html b/LayoutTests/fast/parser/adoption-agency-unload-iframe-2.html
new file mode 100644 (file)
index 0000000..255b9d0
--- /dev/null
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<head>
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+</head>
+<body>
+<b><p><script>
+
+let oldBody = document.body;
+oldBody.remove();
+
+let b = oldBody.querySelector('b');
+document.documentElement.appendChild(b);
+/* html
+     + b
+       + p
+         + script */
+
+let iframe = document.createElement('iframe');
+b.firstChild.appendChild(iframe);
+/* html
+     + b
+       + p
+         + script
+         + iframe */
+
+let oldGlobal = iframe.contentWindow;
+
+window.onload = () => {
+    document.documentElement.appendChild(document.createElement('body'));
+    let test = async_test('An iframe removed by the adoption agency algorithm must be unloaded');
+    test.step(() => {
+        assert_not_equals(oldGlobal, iframe.contentWindow);
+    });
+    test.done();
+}
+
+</script></b>
index 5cba6ee..e05a3c3 100644 (file)
@@ -1,5 +1,43 @@
 2017-02-12  Ryosuke Niwa  <rniwa@webkit.org>
 
+        parserRemoveChild should unload subframes
+        https://bugs.webkit.org/show_bug.cgi?id=168151
+
+        Reviewed by Darin Adler.
+
+        Fix the bug that the adoption agency algorithm does not unload subframes as it disconnects nodes.
+
+        Also moved calls to nodeWillBeRemoved inside NoEventDispatchAssertion to expand on r211965.
+
+        Tests: fast/parser/adoption-agency-clear-focus-range.html
+               fast/parser/adoption-agency-unload-iframe-1.html
+               fast/parser/adoption-agency-unload-iframe-2.html
+
+        * dom/ContainerNode.cpp:
+        (WebCore::ContainerNode::takeAllChildrenFrom): Rewritten using idioms used in removeChildren and parserAppendChild.
+
+        Disconnect all subframes first since this can synchronously dispatch an unload event. Then update DOM ranges,
+        the focused element, and other states in the document.
+
+        Second, use the regular removeBetween, notifyChildNodeRemoved, childrenChanged sequence of calls to disconnect nodes
+        instead of a single call to removeDetachedChildren to properly disconnect child nodes since those nodes may have
+        already come live due to execution of synchronous scripts prior to the adoption agency algorithm has run, or in
+        response to the unload event we just dispatched.
+
+        Third, append these nodes using parserAppendChild to avoid dispatching mutation events.
+
+        (WebCore::willRemoveChild): Removed the call to nodeWillBeRemoved. It's now called within NoEventDispatchAssertion
+        in each call site of willRemoveChild and willRemoveChildren.
+        (WebCore::willRemoveChildren): Ditto.
+        (WebCore::ContainerNode::removeChild): Call nodeWillBeRemoved inside NoEventDispatchAssertion.
+        (WebCore::ContainerNode::replaceAllChildren): Call nodeWillBeRemoved inside NoEventDispatchAssertion.
+        (WebCore::ContainerNode::parserRemoveChild): Disconnect subframes and update document's states.
+
+        * html/parser/HTMLConstructionSite.cpp:
+        (WebCore::executeTakeAllChildrenAndReparentTask): Add a release assert that new parent does not already have a parent. 
+
+2017-02-12  Ryosuke Niwa  <rniwa@webkit.org>
+
         REGRESSION (r179497): Crash inside setAttributeNode
         https://bugs.webkit.org/show_bug.cgi?id=168161
         <rdar://problem/30451581>
index 4d5cf1e..1d999c0 100644 (file)
@@ -130,21 +130,27 @@ void ContainerNode::takeAllChildrenFrom(ContainerNode* oldParent)
             mutation.willRemoveChild(child);
     }
 
-    // FIXME: We need to do notifyMutationObserversNodeWillDetach() for each child,
-    // probably inside removeDetachedChildrenInContainer.
+    disconnectSubframesIfNeeded(*oldParent, DescendantsOnly);
+    {
+        NoEventDispatchAssertion assertNoEventDispatch;
+
+        oldParent->document().nodeChildrenWillBeRemoved(*oldParent);
 
-    oldParent->removeDetachedChildren();
+        WidgetHierarchyUpdatesSuspensionScope suspendWidgetHierarchyUpdates;
+        while (RefPtr<Node> child = oldParent->m_firstChild) {
+            oldParent->removeBetween(nullptr, child->nextSibling(), *child);
+            notifyChildNodeRemoved(*oldParent, *child);
+        }
+        ChildChange change = { AllChildrenRemoved, nullptr, nullptr, ChildChangeSourceParser };
+        childrenChanged(change);
+    }
 
+    // FIXME: assert that we don't dispatch events here since this container node is still disconnected.
     for (auto& child : children) {
-        destroyRenderTreeIfNeeded(child);
-
-        // FIXME: We need a no mutation event version of adoptNode.
-        auto adoptedChild = document().adoptNode(child).releaseReturnValue();
-        parserAppendChild(adoptedChild);
-        // FIXME: Together with adoptNode above, the tree scope might get updated recursively twice
-        // (if the document changed or oldParent was in a shadow tree, AND *this is in a shadow tree).
-        // Can we do better?
-        treeScope().adoptIfNeeded(adoptedChild);
+        RELEASE_ASSERT(!child->parentNode() && &child->treeScope() == &treeScope());
+        ASSERT(!ensurePreInsertionValidity(child, nullptr).hasException());
+        treeScope().adoptIfNeeded(child);
+        parserAppendChild(child);
     }
 }
 
@@ -486,11 +492,6 @@ static void willRemoveChild(ContainerNode& container, Node& child)
 
     if (is<ContainerNode>(child))
         disconnectSubframesIfNeeded(downcast<ContainerNode>(child), RootAndDescendants);
-
-    if (child.parentNode() != &container)
-        return;
-
-    child.document().nodeWillBeRemoved(child); // e.g. mutation event listener can create a new range.
 }
 
 static void willRemoveChildren(ContainerNode& container)
@@ -508,9 +509,6 @@ static void willRemoveChildren(ContainerNode& container)
     }
 
     disconnectSubframesIfNeeded(container, DescendantsOnly);
-
-    container.document().nodeChildrenWillBeRemoved(container);
-
 }
 
 void ContainerNode::disconnectDescendantFrames()
@@ -534,7 +532,7 @@ ExceptionOr<void> ContainerNode::removeChild(Node& oldChild)
 
     willRemoveChild(*this, child);
 
-    // Mutation events might have moved this child into a different parent.
+    // Mutation events in willRemoveChild might have moved this child into a different parent.
     if (child->parentNode() != this)
         return Exception { NOT_FOUND_ERR };
 
@@ -542,6 +540,8 @@ ExceptionOr<void> ContainerNode::removeChild(Node& oldChild)
         WidgetHierarchyUpdatesSuspensionScope suspendWidgetHierarchyUpdates;
         NoEventDispatchAssertion assertNoEventDispatch;
 
+        document().nodeWillBeRemoved(child);
+
         Node* prev = child->previousSibling();
         Node* next = child->nextSibling();
         removeBetween(prev, next, child);
@@ -591,16 +591,18 @@ void ContainerNode::removeBetween(Node* previousChild, Node* nextChild, Node& ol
 
 void ContainerNode::parserRemoveChild(Node& oldChild)
 {
+    disconnectSubframesIfNeeded(*this, DescendantsOnly);
+
     NoEventDispatchAssertion assertNoEventDispatch;
 
+    document().nodeChildrenWillBeRemoved(*this);
+
     ASSERT(oldChild.parentNode() == this);
     ASSERT(!oldChild.isDocumentFragment());
 
     Node* prev = oldChild.previousSibling();
     Node* next = oldChild.nextSibling();
 
-    oldChild.updateAncestorConnectedSubframeCountForRemoval();
-
     ChildListMutationScope(*this).willRemoveChild(oldChild);
     oldChild.notifyMutationObserversNodeWillDetach();
 
@@ -643,6 +645,9 @@ void ContainerNode::replaceAllChildren(Ref<Node>&& node)
         WidgetHierarchyUpdatesSuspensionScope suspendWidgetHierarchyUpdates;
         {
             NoEventDispatchAssertion assertNoEventDispatch;
+
+            document().nodeChildrenWillBeRemoved(*this);
+
             while (RefPtr<Node> child = m_firstChild) {
                 removeBetween(nullptr, child->nextSibling(), *child);
                 notifyChildNodeRemoved(*this, *child);
@@ -686,6 +691,8 @@ void ContainerNode::removeChildren()
         WidgetHierarchyUpdatesSuspensionScope suspendWidgetHierarchyUpdates;
         NoEventDispatchAssertion assertNoEventDispatch;
 
+        document().nodeChildrenWillBeRemoved(*this);
+
         while (RefPtr<Node> child = m_firstChild) {
             removeBetween(0, child->nextSibling(), *child);
             notifyChildNodeRemoved(*this, *child);
index b3c7119..b9b29ce 100644 (file)
@@ -150,6 +150,7 @@ static inline void executeTakeAllChildrenAndReparentTask(HTMLConstructionSiteTas
     auto* furthestBlock = task.oldParent();
     task.parent->takeAllChildrenFrom(furthestBlock);
 
+    RELEASE_ASSERT(!task.parent->parentNode());
     furthestBlock->parserAppendChild(*task.parent);
 }