dom/bindings/DOMJSProxyHandler.cpp
author James Teh <jteh@mozilla.com>
Thu, 07 Mar 2019 18:10:13 +0000
changeset 520910 c26d4a8d43c4d0d6f6e13135b8f35924123fe99e
parent 505383 6f3709b3878117466168c40affa7bca0b60cf75b
child 525643 92c43505695c3b09295fdaaeaf29bf77d72d2a2d
permissions -rw-r--r--
Bug 1527922: Ensure the Reload button is disabled when testing against blank tabs in the browser toolbar key nav tests. r=Gijs For a blank tab, the Reload button should be disabled. These tests depend on this. This seems to be true when setting the new tab page to blank in Firefox Options. However, when we open about:blank with BrowserTestUtils.withNewTab, this is unreliable. That is, sometimes the Reload button is enabled, sometimes it isn't. I don't understand why this happens. For the purposes of these tests, just force the Reload button to be disabled for new, blank tabs so we get consistent results. Differential Revision: https://phabricator.services.mozilla.com/D22449

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/dom/DOMJSProxyHandler.h"
#include "xpcpublic.h"
#include "xpcprivate.h"
#include "XPCWrapper.h"
#include "WrapperFactory.h"
#include "nsWrapperCacheInlines.h"
#include "mozilla/dom/BindingUtils.h"

#include "jsapi.h"

using namespace JS;

namespace mozilla {
namespace dom {

jsid s_length_id = JSID_VOID;

bool DefineStaticJSVals(JSContext* cx) {
  return AtomizeAndPinJSString(cx, s_length_id, "length");
}

const char DOMProxyHandler::family = 0;

js::DOMProxyShadowsResult DOMProxyShadows(JSContext* cx,
                                          JS::Handle<JSObject*> proxy,
                                          JS::Handle<jsid> id) {
  JS::Rooted<JSObject*> expando(cx, DOMProxyHandler::GetExpandoObject(proxy));
  JS::Value v = js::GetProxyPrivate(proxy);
  bool isOverrideBuiltins = !v.isObject() && !v.isUndefined();
  if (expando) {
    bool hasOwn;
    if (!JS_AlreadyHasOwnPropertyById(cx, expando, id, &hasOwn))
      return js::ShadowCheckFailed;

    if (hasOwn) {
      return isOverrideBuiltins ? js::ShadowsViaIndirectExpando
                                : js::ShadowsViaDirectExpando;
    }
  }

  if (!isOverrideBuiltins) {
    // Our expando, if any, didn't shadow, so we're not shadowing at all.
    return js::DoesntShadow;
  }

  bool hasOwn;
  if (!GetProxyHandler(proxy)->hasOwn(cx, proxy, id, &hasOwn))
    return js::ShadowCheckFailed;

  return hasOwn ? js::Shadows : js::DoesntShadowUnique;
}

// Store the information for the specialized ICs.
struct SetDOMProxyInformation {
  SetDOMProxyInformation() {
    js::SetDOMProxyInformation((const void*)&DOMProxyHandler::family,
                               DOMProxyShadows);
  }
};

SetDOMProxyInformation gSetDOMProxyInformation;

static inline void CheckExpandoObject(JSObject* proxy,
                                      const JS::Value& expando) {
#ifdef DEBUG
  JSObject* obj = &expando.toObject();
  MOZ_ASSERT(!js::gc::EdgeNeedsSweepUnbarriered(&obj));
  MOZ_ASSERT(js::GetObjectCompartment(proxy) == js::GetObjectCompartment(obj));

  // When we create an expando object in EnsureExpandoObject below, we preserve
  // the wrapper. The wrapper is released when the object is unlinked, but we
  // should never call these functions after that point.
  nsISupports* native = UnwrapDOMObject<nsISupports>(proxy);
  nsWrapperCache* cache;
  CallQueryInterface(native, &cache);
  MOZ_ASSERT(cache->PreservingWrapper());
#endif
}

static inline void CheckExpandoAndGeneration(
    JSObject* proxy, js::ExpandoAndGeneration* expandoAndGeneration) {
#ifdef DEBUG
  JS::Value value = expandoAndGeneration->expando;
  if (!value.isUndefined()) CheckExpandoObject(proxy, value);
#endif
}

static inline void CheckDOMProxy(JSObject* proxy) {
#ifdef DEBUG
  MOZ_ASSERT(IsDOMProxy(proxy), "expected a DOM proxy object");
  MOZ_ASSERT(!js::gc::EdgeNeedsSweepUnbarriered(&proxy));
  nsISupports* native = UnwrapDOMObject<nsISupports>(proxy);
  nsWrapperCache* cache;
  // QI to nsWrapperCache cannot GC for very non-obvious reasons; see
  // https://searchfox.org/mozilla-central/rev/55da592d85c2baf8d8818010c41d9738c97013d2/js/xpconnect/src/XPCWrappedJSClass.cpp#521,545-548
  JS::AutoSuppressGCAnalysis nogc;
  CallQueryInterface(native, &cache);
  MOZ_ASSERT(cache->GetWrapperPreserveColor() == proxy);
#endif
}

// static
JSObject* DOMProxyHandler::GetAndClearExpandoObject(JSObject* obj) {
  CheckDOMProxy(obj);

  JS::Value v = js::GetProxyPrivate(obj);
  if (v.isUndefined()) {
    return nullptr;
  }

  if (v.isObject()) {
    js::SetProxyPrivate(obj, UndefinedValue());
  } else {
    js::ExpandoAndGeneration* expandoAndGeneration =
        static_cast<js::ExpandoAndGeneration*>(v.toPrivate());
    v = expandoAndGeneration->expando;
    if (v.isUndefined()) {
      return nullptr;
    }
    // We have to expose v to active JS here.  The reason for that is that we
    // might be in the middle of a GC right now.  If our proxy hasn't been
    // traced yet, when it _does_ get traced it won't trace the expando, since
    // we're breaking that link.  But the Rooted we're presumably being placed
    // into is also not going to trace us, because Rooted marking is done at
    // the very beginning of the GC.  In that situation, we need to manually
    // mark the expando as live here.  JS::ExposeValueToActiveJS will do just
    // that for us.
    //
    // We don't need to do this in the non-expandoAndGeneration case, because
    // in that case our value is stored in a slot and slots will already mark
    // the old thing live when the value in the slot changes.
    JS::ExposeValueToActiveJS(v);
    expandoAndGeneration->expando = UndefinedValue();
  }

  CheckExpandoObject(obj, v);

  return &v.toObject();
}

// static
JSObject* DOMProxyHandler::EnsureExpandoObject(JSContext* cx,
                                               JS::Handle<JSObject*> obj) {
  CheckDOMProxy(obj);

  JS::Value v = js::GetProxyPrivate(obj);
  if (v.isObject()) {
    CheckExpandoObject(obj, v);
    return &v.toObject();
  }

  js::ExpandoAndGeneration* expandoAndGeneration;
  if (!v.isUndefined()) {
    expandoAndGeneration =
        static_cast<js::ExpandoAndGeneration*>(v.toPrivate());
    CheckExpandoAndGeneration(obj, expandoAndGeneration);
    if (expandoAndGeneration->expando.isObject()) {
      return &expandoAndGeneration->expando.toObject();
    }
  } else {
    expandoAndGeneration = nullptr;
  }

  JS::Rooted<JSObject*> expando(
      cx, JS_NewObjectWithGivenProto(cx, nullptr, nullptr));
  if (!expando) {
    return nullptr;
  }

  nsISupports* native = UnwrapDOMObject<nsISupports>(obj);
  nsWrapperCache* cache;
  CallQueryInterface(native, &cache);
  cache->PreserveWrapper(native);

  if (expandoAndGeneration) {
    expandoAndGeneration->expando.setObject(*expando);
    return expando;
  }

  js::SetProxyPrivate(obj, ObjectValue(*expando));

  return expando;
}

bool DOMProxyHandler::preventExtensions(JSContext* cx,
                                        JS::Handle<JSObject*> proxy,
                                        JS::ObjectOpResult& result) const {
  // always extensible per WebIDL
  return result.failCantPreventExtensions();
}

bool DOMProxyHandler::isExtensible(JSContext* cx, JS::Handle<JSObject*> proxy,
                                   bool* extensible) const {
  *extensible = true;
  return true;
}

bool BaseDOMProxyHandler::getOwnPropertyDescriptor(
    JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id,
    MutableHandle<PropertyDescriptor> desc) const {
  return getOwnPropDescriptor(cx, proxy, id, /* ignoreNamedProps = */ false,
                              desc);
}

bool DOMProxyHandler::defineProperty(JSContext* cx, JS::Handle<JSObject*> proxy,
                                     JS::Handle<jsid> id,
                                     Handle<PropertyDescriptor> desc,
                                     JS::ObjectOpResult& result,
                                     bool* defined) const {
  if (xpc::WrapperFactory::IsXrayWrapper(proxy)) {
    return result.succeed();
  }

  JS::Rooted<JSObject*> expando(cx, EnsureExpandoObject(cx, proxy));
  if (!expando) {
    return false;
  }

  if (!JS_DefinePropertyById(cx, expando, id, desc, result)) {
    return false;
  }
  *defined = true;
  return true;
}

bool DOMProxyHandler::set(JSContext* cx, Handle<JSObject*> proxy,
                          Handle<jsid> id, Handle<JS::Value> v,
                          Handle<JS::Value> receiver,
                          ObjectOpResult& result) const {
  MOZ_ASSERT(!xpc::WrapperFactory::IsXrayWrapper(proxy),
             "Should not have a XrayWrapper here");
  bool done;
  if (!setCustom(cx, proxy, id, v, &done)) {
    return false;
  }
  if (done) {
    return result.succeed();
  }

  // Make sure to ignore our named properties when checking for own
  // property descriptors for a set.
  JS::Rooted<PropertyDescriptor> ownDesc(cx);
  if (!getOwnPropDescriptor(cx, proxy, id, /* ignoreNamedProps = */ true,
                            &ownDesc)) {
    return false;
  }
  return js::SetPropertyIgnoringNamedGetter(cx, proxy, id, v, receiver, ownDesc,
                                            result);
}

bool DOMProxyHandler::delete_(JSContext* cx, JS::Handle<JSObject*> proxy,
                              JS::Handle<jsid> id,
                              JS::ObjectOpResult& result) const {
  JS::Rooted<JSObject*> expando(cx);
  if (!xpc::WrapperFactory::IsXrayWrapper(proxy) &&
      (expando = GetExpandoObject(proxy))) {
    return JS_DeletePropertyById(cx, expando, id, result);
  }

  return result.succeed();
}

bool BaseDOMProxyHandler::ownPropertyKeys(JSContext* cx,
                                          JS::Handle<JSObject*> proxy,
                                          JS::AutoIdVector& props) const {
  return ownPropNames(cx, proxy,
                      JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, props);
}

bool BaseDOMProxyHandler::getPrototypeIfOrdinary(
    JSContext* cx, JS::Handle<JSObject*> proxy, bool* isOrdinary,
    JS::MutableHandle<JSObject*> proto) const {
  *isOrdinary = true;
  proto.set(GetStaticPrototype(proxy));
  return true;
}

bool BaseDOMProxyHandler::getOwnEnumerablePropertyKeys(
    JSContext* cx, JS::Handle<JSObject*> proxy, JS::AutoIdVector& props) const {
  return ownPropNames(cx, proxy, JSITER_OWNONLY, props);
}

bool DOMProxyHandler::setCustom(JSContext* cx, JS::Handle<JSObject*> proxy,
                                JS::Handle<jsid> id, JS::Handle<JS::Value> v,
                                bool* done) const {
  *done = false;
  return true;
}

// static
JSObject* DOMProxyHandler::GetExpandoObject(JSObject* obj) {
  CheckDOMProxy(obj);

  JS::Value v = js::GetProxyPrivate(obj);
  if (v.isObject()) {
    CheckExpandoObject(obj, v);
    return &v.toObject();
  }

  if (v.isUndefined()) {
    return nullptr;
  }

  js::ExpandoAndGeneration* expandoAndGeneration =
      static_cast<js::ExpandoAndGeneration*>(v.toPrivate());
  CheckExpandoAndGeneration(obj, expandoAndGeneration);

  v = expandoAndGeneration->expando;
  return v.isUndefined() ? nullptr : &v.toObject();
}

void ShadowingDOMProxyHandler::trace(JSTracer* trc, JSObject* proxy) const {
  DOMProxyHandler::trace(trc, proxy);

  MOZ_ASSERT(IsDOMProxy(proxy), "expected a DOM proxy object");
  JS::Value v = js::GetProxyPrivate(proxy);
  MOZ_ASSERT(!v.isObject(), "Should not have expando object directly!");

  // The proxy's private slot is set when we allocate the proxy,
  // so it cannot be |undefined|.
  MOZ_ASSERT(!v.isUndefined());

  js::ExpandoAndGeneration* expandoAndGeneration =
      static_cast<js::ExpandoAndGeneration*>(v.toPrivate());
  JS::TraceEdge(trc, &expandoAndGeneration->expando,
                "Shadowing DOM proxy expando");
}

}  // namespace dom
}  // namespace mozilla