Bug 1515646 - Add FindWithName and FindChildWithName to BrowsingContext. r=peterv
authorAndreas Farre <farre@mozilla.com>
Fri, 15 Feb 2019 09:59:21 +0000
changeset 459515 9243ddacadcca22365599b018d279a026cc51b8c
parent 459514 9be01bc279250018ca2d84eb3b0621ffe4489b12
child 459516 c5f15b5682b546a9b770fc2bcb355de32d79b089
push id111964
push usercsabou@mozilla.com
push dateFri, 15 Feb 2019 18:54:44 +0000
treeherdermozilla-inbound@db3c4f905082 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspeterv
bugs1515646
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1515646 - Add FindWithName and FindChildWithName to BrowsingContext. r=peterv This implements the step of choosing a browsing context with FindWithName, which should be equivalent to calling nsIDocShellTreeItem.findItemWithName passing null for 'aRequestor' and 'aOriginalRequestor' and false for 'aSkipTabGroup'. Differential Revision: https://phabricator.services.mozilla.com/D15190
docshell/base/BrowsingContext.cpp
docshell/base/BrowsingContext.h
docshell/base/nsDocShell.h
dom/base/nsGlobalWindowOuter.cpp
dom/chrome-webidl/BrowsingContext.webidl
--- a/docshell/base/BrowsingContext.cpp
+++ b/docshell/base/BrowsingContext.cpp
@@ -216,26 +216,26 @@ void BrowsingContext::Detach() {
   RefPtr<BrowsingContext> kungFuDeathGrip(this);
 
   BrowsingContextMap<RefPtr>::Ptr p;
   if (sCachedBrowsingContexts && (p = sCachedBrowsingContexts->lookup(Id()))) {
     MOZ_DIAGNOSTIC_ASSERT(!mParent || !mParent->mChildren.Contains(this));
     MOZ_DIAGNOSTIC_ASSERT(!mGroup || !mGroup->Toplevels().Contains(this));
     sCachedBrowsingContexts->remove(p);
   } else {
-    auto* children = mParent ? &mParent->mChildren : &mGroup->Toplevels();
+    Children& children = mParent ? mParent->mChildren : mGroup->Toplevels();
 
     // TODO(farre): This assert looks extremely fishy, I know, but
     // what we're actually saying is this: if we're detaching, but our
     // parent doesn't have any children, it is because we're being
     // detached by the cycle collector destroying docshells out of
     // order.
-    MOZ_DIAGNOSTIC_ASSERT(children->IsEmpty() || children->Contains(this));
+    MOZ_DIAGNOSTIC_ASSERT(children.IsEmpty() || children.Contains(this));
 
-    children->RemoveElement(this);
+    children.RemoveElement(this);
   }
 
   Group()->Unregister(this);
 
   if (!XRE_IsContentProcess()) {
     return;
   }
 
@@ -287,16 +287,170 @@ void BrowsingContext::SetOpener(Browsing
     return;
   }
 
   auto cc = ContentChild::GetSingleton();
   MOZ_DIAGNOSTIC_ASSERT(cc);
   cc->SendSetOpenerBrowsingContext(this, aOpener);
 }
 
+// FindWithName follows the rules for choosing a browsing context,
+// with the exception of sandboxing for iframes. The implementation
+// for arbitrarily choosing between two browsing contexts with the
+// same name is as follows:
+//
+// 1) The start browsing context, i.e. 'this'
+// 2) Descendants in insertion order
+// 3) The parent
+// 4) Siblings and their children, both in insertion order
+// 5) After this we iteratively follow the parent chain, repeating 3
+//    and 4 until
+// 6) If there is no parent, consider all other top level browsing
+//    contexts and their children, both in insertion order
+//
+// See
+// https://html.spec.whatwg.org/multipage/browsers.html#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name
+BrowsingContext* BrowsingContext::FindWithName(const nsAString& aName) {
+  BrowsingContext* found = nullptr;
+  if (aName.IsEmpty()) {
+    // You can't find a browsing context with an empty name.
+    found = nullptr;
+  } else if (BrowsingContext* special = FindWithSpecialName(aName)) {
+    found = special;
+  } else if (aName.LowerCaseEqualsLiteral("_blank")) {
+    // Just return null. Caller must handle creating a new window with
+    // a blank name.
+    found = nullptr;
+  } else if (BrowsingContext* child = FindWithNameInSubtree(aName, this)) {
+    found = child;
+  } else {
+    BrowsingContext* current = this;
+
+    do {
+      Children* siblings;
+      BrowsingContext* parent = current->mParent;
+
+      if (!parent) {
+        // We've reached the root of the tree, consider browsing
+        // contexts in the same browsing context group.
+        siblings = &mGroup->Toplevels();
+      } else if (parent->NameEquals(aName) && CanAccess(parent) &&
+                 parent->IsActive()) {
+        found = parent;
+        break;
+      } else {
+        siblings = &parent->mChildren;
+      }
+
+      for (BrowsingContext* sibling : *siblings) {
+        if (sibling == current) {
+          continue;
+        }
+
+        if (BrowsingContext* relative =
+                sibling->FindWithNameInSubtree(aName, this)) {
+          found = relative;
+          // Breaks the outer loop
+          parent = nullptr;
+          break;
+        }
+      }
+
+      current = parent;
+    } while (current);
+  }
+
+  // Helpers should perform access control checks, which means that we
+  // only need to assert that we can access found.
+  MOZ_DIAGNOSTIC_ASSERT(!found || CanAccess(found));
+
+  return found;
+}
+
+BrowsingContext* BrowsingContext::FindChildWithName(const nsAString& aName) {
+  if (aName.IsEmpty()) {
+    // You can't find a browsing context with the empty name.
+    return nullptr;
+  }
+
+  for (BrowsingContext* child : mChildren) {
+    if (child->NameEquals(aName) && CanAccess(child) && child->IsActive()) {
+      return child;
+    }
+  }
+
+  return nullptr;
+}
+
+BrowsingContext* BrowsingContext::FindWithSpecialName(const nsAString& aName) {
+  // TODO(farre): Neither BrowsingContext nor nsDocShell checks if the
+  // browsing context pointed to by a special name is active. Should
+  // it be? See Bug 1527913.
+  if (aName.LowerCaseEqualsLiteral("_self")) {
+    return this;
+  }
+
+  if (aName.LowerCaseEqualsLiteral("_parent")) {
+    return mParent && CanAccess(mParent.get()) ? mParent.get() : this;
+  }
+
+  if (aName.LowerCaseEqualsLiteral("_top")) {
+    BrowsingContext* top = TopLevelBrowsingContext();
+
+    return CanAccess(top) ? top : nullptr;
+  }
+
+  return nullptr;
+}
+
+BrowsingContext* BrowsingContext::FindWithNameInSubtree(
+    const nsAString& aName, BrowsingContext* aRequestingContext) {
+  MOZ_DIAGNOSTIC_ASSERT(!aName.IsEmpty());
+
+  if (NameEquals(aName) && aRequestingContext->CanAccess(this) && IsActive()) {
+    return this;
+  }
+
+  for (BrowsingContext* child : mChildren) {
+    if (BrowsingContext* found =
+            child->FindWithNameInSubtree(aName, aRequestingContext)) {
+      return found;
+    }
+  }
+
+  return nullptr;
+}
+
+bool BrowsingContext::CanAccess(BrowsingContext* aContext) {
+  // TODO(farre): Bouncing this to nsDocShell::CanAccessItem is
+  // temporary, we should implement a replacement for this in
+  // BrowsingContext. See Bug 151590.
+  return aContext && nsDocShell::CanAccessItem(aContext->mDocShell, mDocShell);
+}
+
+bool BrowsingContext::IsActive() const {
+  // TODO(farre): Mimicking the bahaviour from
+  // ItemIsActive(nsIDocShellTreeItem* aItem) is temporary, we should
+  // implement a replacement for this using mClosed only. See Bug
+  // 1527321.
+
+  if (!mDocShell) {
+    return mClosed;
+  }
+
+  if (nsCOMPtr<nsPIDOMWindowOuter> window = mDocShell->GetWindow()) {
+    auto* win = nsGlobalWindowOuter::Cast(window);
+    if (!win->GetClosedOuter()) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
 BrowsingContext::~BrowsingContext() {
   MOZ_DIAGNOSTIC_ASSERT(!mParent || !mParent->mChildren.Contains(this));
   MOZ_DIAGNOSTIC_ASSERT(!mGroup || !mGroup->Toplevels().Contains(this));
   MOZ_DIAGNOSTIC_ASSERT(!sCachedBrowsingContexts ||
                         !sCachedBrowsingContexts->has(Id()));
 
   if (sBrowsingContexts) {
     sBrowsingContexts->remove(Id());
@@ -491,32 +645,16 @@ void BrowsingContext::PostMessageMoz(JSC
                                      JS::Handle<JS::Value> aMessage,
                                      const WindowPostMessageOptions& aOptions,
                                      nsIPrincipal& aSubjectPrincipal,
                                      ErrorResult& aError) {
   PostMessageMoz(aCx, aMessage, aOptions.mTargetOrigin, aOptions.mTransfer,
                  aSubjectPrincipal, aError);
 }
 
-already_AddRefed<BrowsingContext> BrowsingContext::FindChildWithName(
-    const nsAString& aName) {
-  // FIXME https://bugzilla.mozilla.org/show_bug.cgi?id=1515646 will reimplement
-  //       this on top of the BC tree.
-  MOZ_ASSERT(mDocShell);
-  nsCOMPtr<nsIDocShellTreeItem> child;
-  mDocShell->FindChildWithName(aName, false, true, nullptr, nullptr,
-                               getter_AddRefs(child));
-  nsCOMPtr<nsIDocShell> childDS = do_QueryInterface(child);
-  RefPtr<BrowsingContext> bc;
-  if (childDS) {
-    childDS->GetBrowsingContext(getter_AddRefs(bc));
-  }
-  return bc.forget();
-}
-
 }  // namespace dom
 
 namespace ipc {
 
 void IPDLParamTraits<dom::BrowsingContext>::Write(
     IPC::Message* aMsg, IProtocol* aActor, dom::BrowsingContext* aParam) {
   uint64_t id = aParam ? aParam->Id() : 0;
   WriteIPDLParam(aMsg, aActor, id);
--- a/docshell/base/BrowsingContext.h
+++ b/docshell/base/BrowsingContext.h
@@ -125,32 +125,51 @@ class BrowsingContext : public nsWrapper
   // Determine if the current BrowsingContext was 'cached' by the logic in
   // CacheChildren.
   bool IsCached();
 
   // TODO(farre): We should sync changes from SetName to the parent
   // process. [Bug 1490303]
   void SetName(const nsAString& aName) { mName = aName; }
   const nsString& Name() const { return mName; }
+  void GetName(nsAString& aName) { aName = mName; }
   bool NameEquals(const nsAString& aName) { return mName.Equals(aName); }
 
   bool IsContent() const { return mType == Type::Content; }
 
   uint64_t Id() const { return mBrowsingContextId; }
 
   BrowsingContext* GetParent() { return mParent; }
 
   void GetChildren(nsTArray<RefPtr<BrowsingContext>>& aChildren);
 
   BrowsingContext* GetOpener() const { return mOpener; }
 
   void SetOpener(BrowsingContext* aOpener);
 
   BrowsingContextGroup* Group() { return mGroup; }
 
+  // Using the rules for choosing a browsing context we try to find
+  // the browsing context with the given name in the set of
+  // transitively reachable browsing contexts. Performs access control
+  // with regards to this.
+  // See
+  // https://html.spec.whatwg.org/multipage/browsers.html#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name.
+  //
+  // BrowsingContext::FindWithName(const nsAString&) is equivalent to
+  // calling nsIDocShellTreeItem::FindItemWithName(aName, nullptr,
+  // nullptr, false, <return value>).
+  BrowsingContext* FindWithName(const nsAString& aName);
+
+  // Find a browsing context in this context's list of
+  // children. Doesn't consider the special names, '_self', '_parent',
+  // '_top', or '_blank'. Performs access control with regard to
+  // 'this'.
+  BrowsingContext* FindChildWithName(const nsAString& aName);
+
   nsISupports* GetParentObject() const;
   JSObject* WrapObject(JSContext* aCx,
                        JS::Handle<JSObject*> aGivenProto) override;
 
   // This function would be called when its corresponding document is activated
   // by user gesture, and we would set the flag in the top level browsing
   // context.
   void NotifyUserGestureActivation();
@@ -199,27 +218,41 @@ class BrowsingContext : public nsWrapper
   void PostMessageMoz(JSContext* aCx, JS::Handle<JS::Value> aMessage,
                       const nsAString& aTargetOrigin,
                       const Sequence<JSObject*>& aTransfer,
                       nsIPrincipal& aSubjectPrincipal, ErrorResult& aError);
   void PostMessageMoz(JSContext* aCx, JS::Handle<JS::Value> aMessage,
                       const WindowPostMessageOptions& aOptions,
                       nsIPrincipal& aSubjectPrincipal, ErrorResult& aError);
 
-  already_AddRefed<BrowsingContext> FindChildWithName(const nsAString& aName);
-
   JSObject* WrapObject(JSContext* aCx);
 
  protected:
   virtual ~BrowsingContext();
   BrowsingContext(BrowsingContext* aParent, BrowsingContext* aOpener,
                   const nsAString& aName, uint64_t aBrowsingContextId,
                   Type aType);
 
  private:
+  // Find the special browsing context if aName is '_self', '_parent',
+  // '_top', but not '_blank'. The latter is handled in FindWithName
+  BrowsingContext* FindWithSpecialName(const nsAString& aName);
+
+  // Find a browsing context in the subtree rooted at 'this' Doesn't
+  // consider the special names, '_self', '_parent', '_top', or
+  // '_blank'. Performs access control with regard to
+  // 'aRequestingContext'.
+  BrowsingContext* FindWithNameInSubtree(const nsAString& aName,
+                                         BrowsingContext* aRequestingContext);
+
+  // Performs access control to check that 'this' can access 'aContext'.
+  bool CanAccess(BrowsingContext* aContext);
+
+  bool IsActive() const;
+
   friend class ::nsOuterWindowProxy;
   friend class ::nsGlobalWindowOuter;
   // Update the window proxy object that corresponds to this browsing context.
   // This should be called from the window proxy object's objectMoved hook, if
   // the object mWindowProxy points to was moved by the JS GC.
   void UpdateWindowProxy(JSObject* obj, JSObject* old) {
     if (mWindowProxy) {
       MOZ_ASSERT(mWindowProxy == old);
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -397,16 +397,17 @@ class nsDocShell final : public nsDocLoa
   nsresult InternalLoad(nsDocShellLoadState* aLoadState,
                         nsIDocShell** aDocShell, nsIRequest** aRequest);
 
  private:  // member functions
   friend class nsDSURIContentListener;
   friend class FramingChecker;
   friend class OnLinkClickEvent;
   friend class nsIDocShell;
+  friend class mozilla::dom::BrowsingContext;
 
   // It is necessary to allow adding a timeline marker wherever a docshell
   // instance is available. This operation happens frequently and needs to
   // be very fast, so instead of using a Map or having to search for some
   // docshell-specific markers storage, a pointer to an `ObservedDocShell` is
   // is stored on docshells directly.
   friend void mozilla::TimelineConsumers::AddConsumer(nsDocShell*);
   friend void mozilla::TimelineConsumers::RemoveConsumer(nsDocShell*);
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -3928,17 +3928,17 @@ Nullable<WindowProxyHolder> nsGlobalWind
   }
   return WindowProxyHolder(topBC);
 }
 
 already_AddRefed<BrowsingContext> nsGlobalWindowOuter::GetChildWindow(
     const nsAString& aName) {
   NS_ENSURE_TRUE(mBrowsingContext, nullptr);
 
-  return mBrowsingContext->FindChildWithName(aName);
+  return do_AddRef(mBrowsingContext->FindChildWithName(aName));
 }
 
 bool nsGlobalWindowOuter::DispatchCustomEvent(const nsAString& aEventName) {
   bool defaultActionEnabled = true;
   nsContentUtils::DispatchTrustedEvent(mDoc, ToSupports(this), aEventName,
                                        CanBubble::eYes, Cancelable::eYes,
                                        &defaultActionEnabled);
 
--- a/dom/chrome-webidl/BrowsingContext.webidl
+++ b/dom/chrome-webidl/BrowsingContext.webidl
@@ -4,16 +4,21 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 interface nsIDocShell;
 
 [Exposed=Window, ChromeOnly]
 interface BrowsingContext {
   static BrowsingContext? get(unsigned long long aId);
 
+  BrowsingContext? findChildWithName(DOMString name);
+  BrowsingContext? findWithName(DOMString name);
+
+  readonly attribute DOMString name;
+
   readonly attribute BrowsingContext? parent;
 
   sequence<BrowsingContext> getChildren();
 
   readonly attribute nsIDocShell? docShell;
 
   readonly attribute unsigned long long id;