Bug 1013743, MutationObserver should observe only the subtree it is attached to, r=wchen
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Tue, 21 Apr 2015 17:58:29 -0700
changeset 259369 25db40a4dd8ec36cecf48b46774f3f9b88ed3331
parent 259368 d3e10d7ea4773ce4b3ab9b1c7e81c0a13d6f219e
child 259370 73917a03e7347846cbc9a9f5da14a4c50f04fcde
push id8007
push userraliiev@mozilla.com
push dateMon, 11 May 2015 19:23:16 +0000
treeherdermozilla-aurora@e2ce1aac996e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswchen
bugs1013743
milestone40.0a1
Bug 1013743, MutationObserver should observe only the subtree it is attached to, r=wchen
dom/base/nsDOMMutationObserver.cpp
dom/base/nsDOMMutationObserver.h
dom/base/test/test_mutationobservers.html
--- a/dom/base/nsDOMMutationObserver.cpp
+++ b/dom/base/nsDOMMutationObserver.cpp
@@ -59,16 +59,23 @@ NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ns
                                       mPreviousSibling, mNextSibling,
                                       mAddedNodes, mRemovedNodes,
                                       mAddedAnimations, mRemovedAnimations,
                                       mChangedAnimations,
                                       mNext, mOwner)
 
 // Observer
 
+bool
+nsMutationReceiverBase::IsObservable(nsIContent* aContent)
+{
+  return !aContent->ChromeOnlyAccess() &&
+    (Observer()->IsChrome() || !aContent->IsInAnonymousSubtree());
+}
+
 NS_IMPL_ADDREF(nsMutationReceiver)
 NS_IMPL_RELEASE(nsMutationReceiver)
 
 NS_INTERFACE_MAP_BEGIN(nsMutationReceiver)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
   NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
 NS_INTERFACE_MAP_END
 
@@ -106,18 +113,17 @@ nsMutationReceiver::Disconnect(bool aRem
 void
 nsMutationReceiver::AttributeWillChange(nsIDocument* aDocument,
                                         mozilla::dom::Element* aElement,
                                         int32_t aNameSpaceID,
                                         nsIAtom* aAttribute,
                                         int32_t aModType)
 {
   if (nsAutoMutationBatch::IsBatching() ||
-      !ObservesAttr(aElement, aNameSpaceID, aAttribute) ||
-      aElement->ChromeOnlyAccess()) {
+      !ObservesAttr(RegisterTarget(), aElement, aNameSpaceID, aAttribute)) {
     return;
   }
 
   nsDOMMutationRecord* m =
     Observer()->CurrentRecord(nsGkAtoms::attributes);
 
   NS_ASSERTION(!m->mTarget || m->mTarget == aElement,
                "Wrong target!");
@@ -142,24 +148,26 @@ nsMutationReceiver::AttributeWillChange(
 }
 
 void
 nsMutationReceiver::CharacterDataWillChange(nsIDocument *aDocument,
                                             nsIContent* aContent,
                                             CharacterDataChangeInfo* aInfo)
 {
   if (nsAutoMutationBatch::IsBatching() ||
-      !CharacterData() || !(Subtree() || aContent == Target()) ||
-      aContent->ChromeOnlyAccess()) {
+      !CharacterData() ||
+      (!Subtree() && aContent != Target()) ||
+      (Subtree() && RegisterTarget()->SubtreeRoot() != aContent->SubtreeRoot()) ||
+      !IsObservable(aContent)) {
     return;
   }
-  
+
   nsDOMMutationRecord* m =
     Observer()->CurrentRecord(nsGkAtoms::characterData);
- 
+
   NS_ASSERTION(!m->mTarget || m->mTarget == aContent,
                "Wrong target!");
 
   if (!m->mTarget) {
     m->mTarget = aContent;
   }
   if (CharacterDataOldValue() && m->mPrevValue.IsVoid()) { 
     aContent->GetText()->AppendTo(m->mPrevValue);
@@ -168,18 +176,21 @@ nsMutationReceiver::CharacterDataWillCha
 
 void
 nsMutationReceiver::ContentAppended(nsIDocument* aDocument,
                                     nsIContent* aContainer,
                                     nsIContent* aFirstNewContent,
                                     int32_t aNewIndexInContainer)
 {
   nsINode* parent = NODE_FROM(aContainer, aDocument);
-  bool wantsChildList = ChildList() && (Subtree() || parent == Target());
-  if (!wantsChildList || aFirstNewContent->ChromeOnlyAccess()) {
+  bool wantsChildList =
+    ChildList() &&
+    ((Subtree() && RegisterTarget()->SubtreeRoot() == parent->SubtreeRoot()) ||
+     parent == Target());
+  if (!wantsChildList || !IsObservable(aFirstNewContent)) {
     return;
   }
 
   if (nsAutoMutationBatch::IsBatching()) {
     if (parent == nsAutoMutationBatch::GetBatchTarget()) {
       nsAutoMutationBatch::UpdateObserver(Observer(), wantsChildList);
     }
     return;
@@ -206,18 +217,21 @@ nsMutationReceiver::ContentAppended(nsID
 
 void
 nsMutationReceiver::ContentInserted(nsIDocument* aDocument,
                                     nsIContent* aContainer,
                                     nsIContent* aChild,
                                     int32_t aIndexInContainer)
 {
   nsINode* parent = NODE_FROM(aContainer, aDocument);
-  bool wantsChildList = ChildList() && (Subtree() || parent == Target());
-  if (!wantsChildList || aChild->ChromeOnlyAccess()) {
+  bool wantsChildList =
+    ChildList() &&
+    ((Subtree() && RegisterTarget()->SubtreeRoot() == parent->SubtreeRoot()) ||
+     parent == Target());
+  if (!wantsChildList || !IsObservable(aChild)) {
     return;
   }
 
   if (nsAutoMutationBatch::IsBatching()) {
     if (parent == nsAutoMutationBatch::GetBatchTarget()) {
       nsAutoMutationBatch::UpdateObserver(Observer(), wantsChildList);
     }
     return;
@@ -238,21 +252,24 @@ nsMutationReceiver::ContentInserted(nsID
 
 void
 nsMutationReceiver::ContentRemoved(nsIDocument* aDocument,
                                    nsIContent* aContainer,
                                    nsIContent* aChild,
                                    int32_t aIndexInContainer,
                                    nsIContent* aPreviousSibling)
 {
-  if (aChild->ChromeOnlyAccess()) {
+  if (!IsObservable(aChild)) {
     return;
   }
 
   nsINode* parent = NODE_FROM(aContainer, aDocument);
+  if (Subtree() && parent->SubtreeRoot() != RegisterTarget()->SubtreeRoot()) {
+    return;
+  }
   if (nsAutoMutationBatch::IsBatching()) {
     if (nsAutoMutationBatch::IsRemovalDone()) {
       // This can happen for example if HTML parser parses to
       // context node, but needs to move elements around.
       return;
     }
     if (nsAutoMutationBatch::GetBatchTarget() != parent) {
       return;
@@ -260,17 +277,17 @@ nsMutationReceiver::ContentRemoved(nsIDo
 
     bool wantsChildList = ChildList() && (Subtree() || parent == Target());
     if (wantsChildList || Subtree()) {
       nsAutoMutationBatch::NodeRemoved(aChild);
       nsAutoMutationBatch::UpdateObserver(Observer(), wantsChildList);
     }
 
     return;
-  }                                                                   
+  }
 
   if (Subtree()) {
     // Try to avoid creating transient observer if the node
     // already has an observer observing the same set of nodes.
     nsMutationReceiver* orig = GetParent() ? GetParent() : this;
     if (Observer()->GetReceiverFor(aChild, false, false) != orig) {
       bool transientExists = false;
       nsCOMArray<nsMutationReceiver>* transientReceivers = nullptr;
@@ -709,18 +726,19 @@ nsDOMMutationObserver::Constructor(const
                                    mozilla::ErrorResult& aRv)
 {
   nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aGlobal.GetAsSupports());
   if (!window) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
   MOZ_ASSERT(window->IsInnerWindow());
+  bool isChrome = nsContentUtils::IsChromeDoc(window->GetExtantDoc());
   nsRefPtr<nsDOMMutationObserver> observer =
-    new nsDOMMutationObserver(window.forget(), aCb);
+    new nsDOMMutationObserver(window.forget(), aCb, isChrome);
   return observer.forget();
 }
 
 void
 nsDOMMutationObserver::HandleMutation()
 {
   NS_ASSERTION(nsContentUtils::IsSafeToRunScript(), "Whaat!");
   NS_ASSERTION(mCurrentMutations.IsEmpty(),
--- a/dom/base/nsDOMMutationObserver.h
+++ b/dom/base/nsDOMMutationObserver.h
@@ -243,24 +243,30 @@ protected:
 
   void AddObserver()
   {
     AddMutationObserver();
     mRegisterTarget->SetMayHaveDOMMutationObserver();
     mRegisterTarget->OwnerDoc()->SetMayHaveDOMMutationObservers();
   }
 
-  bool ObservesAttr(mozilla::dom::Element* aElement,
+  bool IsObservable(nsIContent* aContent);
+
+  bool ObservesAttr(nsINode* aRegisterTarget,
+                    mozilla::dom::Element* aElement,
                     int32_t aNameSpaceID,
                     nsIAtom* aAttr)
   {
     if (mParent) {
-      return mParent->ObservesAttr(aElement, aNameSpaceID, aAttr);
+      return mParent->ObservesAttr(aRegisterTarget, aElement, aNameSpaceID, aAttr);
     }
-    if (!Attributes() || (!Subtree() && aElement != Target())) {
+    if (!Attributes() ||
+        (!Subtree() && aElement != Target()) ||
+        (Subtree() && aRegisterTarget->SubtreeRoot() != aElement->SubtreeRoot()) ||
+        !IsObservable(aElement)) {
       return false;
     }
     if (AllAttributes()) {
       return true;
     }
 
     if (aNameSpaceID != kNameSpaceID_None) {
       return false;
@@ -442,19 +448,20 @@ private:
 { 0x0c3b91f8, 0xcc3b, 0x4b08, \
   { 0x9e, 0xab, 0x07, 0x47, 0xa9, 0xe4, 0x65, 0xb4 } }
 
 class nsDOMMutationObserver final : public nsISupports,
                                     public nsWrapperCache
 {
 public:
   nsDOMMutationObserver(already_AddRefed<nsPIDOMWindow>&& aOwner,
-                        mozilla::dom::MutationCallback& aCb)
+                        mozilla::dom::MutationCallback& aCb,
+                        bool aChrome)
   : mOwner(aOwner), mLastPendingMutation(nullptr), mPendingMutationCount(0),
-    mCallback(&aCb), mWaitingForRun(false), mId(++sCount)
+    mCallback(&aCb), mWaitingForRun(false), mIsChrome(aChrome), mId(++sCount)
   {
   }
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(nsDOMMutationObserver)
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_MUTATION_OBSERVER_IID)
 
   static already_AddRefed<nsDOMMutationObserver>
   Constructor(const mozilla::dom::GlobalObject& aGlobal,
@@ -466,16 +473,21 @@ public:
     return mozilla::dom::MutationObserverBinding::Wrap(aCx, this, aGivenProto);
   }
 
   nsISupports* GetParentObject() const
   {
     return mOwner;
   }
 
+  bool IsChrome()
+  {
+    return mIsChrome;
+  }
+
   void Observe(nsINode& aTarget,
                const mozilla::dom::MutationObserverInit& aOptions,
                mozilla::ErrorResult& aRv);
 
   void Disconnect();
 
   void TakeRecords(nsTArray<nsRefPtr<nsDOMMutationRecord> >& aRetVal);
 
@@ -566,16 +578,17 @@ protected:
   // the microtask.
   nsRefPtr<nsDOMMutationRecord>                      mFirstPendingMutation;
   nsDOMMutationRecord*                               mLastPendingMutation;
   uint32_t                                           mPendingMutationCount;
 
   nsRefPtr<mozilla::dom::MutationCallback>           mCallback;
 
   bool                                               mWaitingForRun;
+  bool                                               mIsChrome;
 
   uint64_t                                           mId;
 
   static uint64_t                                    sCount;
   static nsAutoTArray<nsRefPtr<nsDOMMutationObserver>, 4>* sScheduledMutationObservers;
   static nsDOMMutationObserver*                      sCurrentObserver;
 
   static uint32_t                                    sMutationLevel;
--- a/dom/base/test/test_mutationobservers.html
+++ b/dom/base/test/test_mutationobservers.html
@@ -15,16 +15,18 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="content" style="display: none">
                                 
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 641821 **/
 
+SimpleTest.requestFlakyTimeout("requestFlakyTimeout is silly. (But make sure marquee has time to initialize itself.)");
+
 var div = document.createElement("div");
 
 var M;
 if ("MozMutationObserver" in window) {
   M = window.MozMutationObserver;
 } else if ("WebKitMutationObserver" in window) {
   M = window.WebKitMutationObserver;
 } else {
@@ -575,32 +577,100 @@ function testTakeRecords() {
   div.setAttribute("foo", "bar");
   is(mutationEventCount, 1, "Should have got only one mutation event!");
 }
 
 function testExpandos() {
   var m2 = new M(function(records, observer) {
     is(observer.expandoProperty, true);
     observer.disconnect();
-    then(testStyleCreate);
+    then(testOutsideShadowDOM);
   });
   m2.expandoProperty = true;
   m2.observe(div, { attributes: true });
   m2 = null;
   if (SpecialPowers) {
     // Run GC several times to see if the expando property disappears.
 
     SpecialPowers.gc();
     SpecialPowers.gc();
     SpecialPowers.gc();
     SpecialPowers.gc();
   }
   div.setAttribute("foo", "bar2");
 }
 
+function testOutsideShadowDOM() {
+  var m = new M(function(records, observer) {
+    is(records.length, 1);
+    is(records[0].type, "attributes", "Should have got attributes");
+    observer.disconnect();
+    then(testInsideShadowDOM);
+  });
+  m.observe(div, {
+      attributes: true,
+      childList: true,
+      characterData: true,
+      subtree: true
+    })
+  var sr = div.createShadowRoot();
+  sr.innerHTML = "<div" + ">text</" + "div>";
+  sr.firstChild.setAttribute("foo", "bar");
+  sr.firstChild.firstChild.data = "text2";
+  sr.firstChild.appendChild(document.createElement("div"));
+  div.setAttribute("foo", "bar");
+}
+
+function testInsideShadowDOM() {
+  var m = new M(function(records, observer) {
+    is(records.length, 4);
+    is(records[0].type, "childList");
+    is(records[1].type, "attributes");
+    is(records[2].type, "characterData");
+    is(records[3].type, "childList");
+    observer.disconnect();
+    then(testMarquee);
+  });
+  var sr = div.createShadowRoot();
+  m.observe(sr, {
+      attributes: true,
+      childList: true,
+      characterData: true,
+      subtree: true
+    });
+
+  sr.innerHTML = "<div" + ">text</" + "div>";
+  sr.firstChild.setAttribute("foo", "bar");
+  sr.firstChild.firstChild.data = "text2";
+  sr.firstChild.appendChild(document.createElement("div"));
+  div.setAttribute("foo", "bar2");
+
+}
+
+function testMarquee() {
+  var m = new M(function(records, observer) {
+    is(records.length, 1);
+    is(records[0].type, "attributes");
+    is(records[0].attributeName, "ok");
+    is(records[0].oldValue, null);
+    observer.disconnect();
+    then(testStyleCreate);
+  });
+  var marquee = document.createElement("marquee");
+  m.observe(marquee, {
+    attributes: true,
+    attributeOldValue: true,
+    childList: true,
+    characterData: true,
+    subtree: true
+  });
+  document.body.appendChild(marquee);
+  setTimeout(function() {marquee.setAttribute("ok", "ok")}, 500);
+}
+
 function testStyleCreate() {
   m = new M(function(records, observer) {
     is(records.length, 1, "number of records");
     is(records[0].type, "attributes", "record.type");
     is(records[0].attributeName, "style", "record.attributeName");
     is(records[0].oldValue, null, "record.oldValue");
     isnot(div.getAttribute("style"), null, "style attribute after creation");
     observer.disconnect();