dom/xbl/nsXBLProtoImpl.cpp
author Mozilla Releng Treescript <release+treescript@mozilla.org>
Tue, 15 Oct 2019 00:58:32 +0000
changeset 558723 25d95d2e7866acd7dd134cb9dde03bdef7470819
parent 509549 f0a91d36587266d7454a450c6044d573664fbed5
permissions -rw-r--r--
No bug - Tagging e1479d4d94da4261b90f09b0246f8de6d49756bb with DEVEDITION_71_0b1_BUILD1 a=release CLOSED TREE DONTBUILD

/* -*- 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/DebugOnly.h"

#include "nsXBLProtoImpl.h"
#include "nsIContent.h"
#include "mozilla/dom/Document.h"
#include "nsContentUtils.h"
#include "nsIXPConnect.h"
#include "nsIServiceManager.h"
#include "nsXBLPrototypeBinding.h"
#include "nsXBLProtoImplProperty.h"
#include "nsIURI.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/XULElementBinding.h"
#include "xpcpublic.h"
#include "js/CharacterEncoding.h"

using namespace mozilla;
using namespace mozilla::dom;
using js::AssertSameCompartment;

nsresult nsXBLProtoImpl::InstallImplementation(
    nsXBLPrototypeBinding* aPrototypeBinding, nsXBLBinding* aBinding) {
  // This function is called to install a concrete implementation on a bound
  // element using this prototype implementation as a guide.  The prototype
  // implementation is compiled lazily, so for the first bound element that
  // needs a concrete implementation, we also build the prototype
  // implementation.
  if (!mMembers &&
      !mFields)    // Constructor and destructor also live in mMembers
    return NS_OK;  // Nothing to do, so let's not waste time.

  // If the way this gets the script context changes, fix
  // nsXBLProtoImplAnonymousMethod::Execute
  Document* document = aBinding->GetBoundElement()->OwnerDoc();

  // This sometimes gets called when we have no outer window and if we don't
  // catch this, we get leaks during crashtests and reftests.
  if (NS_WARN_IF(!document->GetWindow())) {
    return NS_OK;
  }

  // |propertyHolder| (below) can be an existing object, so in theory we might
  // hit something that could end up running script. We never want that to
  // happen here, so we use an AutoJSAPI instead of an AutoEntryScript.
  dom::AutoJSAPI jsapi;
  if (NS_WARN_IF(!jsapi.Init(document->GetScopeObject()))) {
    return NS_OK;
  }
  JSContext* cx = jsapi.cx();

  // InitTarget objects gives us back the JS object that represents the bound
  // element and the class object in the bound document that represents the
  // concrete version of this implementation. This function also has the side
  // effect of building up the prototype implementation if it has not been built
  // already.
  JS::Rooted<JSObject*> targetClassObject(cx, nullptr);
  bool targetObjectIsNew = false;
  nsresult rv =
      InitTargetObjects(aPrototypeBinding, aBinding->GetBoundElement(),
                        &targetClassObject, &targetObjectIsNew);
  NS_ENSURE_SUCCESS(rv, rv);  // kick out if we were unable to properly
                              // intialize our target objects
  MOZ_ASSERT(targetClassObject);

  // If the prototype already existed, we don't need to install anything. return
  // early.
  if (!targetObjectIsNew) return NS_OK;

  // We want to define the canonical set of members in a safe place. If we're
  // using a separate XBL scope, we want to define them there first (so that
  // they'll be available for Xray lookups, among other things), and then copy
  // the properties to the content-side prototype as needed. We don't need to
  // bother about the field accessors here, since we don't use/support those
  // for in-content bindings.

  // First, start by entering the realm of the XBL scope. This may or may
  // not be the same realm as globalObject.
  JS::Rooted<JSObject*> globalObject(
      cx, JS::GetNonCCWObjectGlobal(targetClassObject));
  JS::Rooted<JSObject*> scopeObject(cx,
                                    xpc::GetXBLScopeOrGlobal(cx, globalObject));
  NS_ENSURE_TRUE(scopeObject, NS_ERROR_OUT_OF_MEMORY);
  MOZ_ASSERT(JS_IsGlobalObject(scopeObject));
  JSAutoRealm ar(cx, scopeObject);

  // Determine the appropriate property holder.
  //
  // Note: If |targetIsNew| is false, we'll early-return above. However, that
  // only tells us if the content-side object is new, which may be the case even
  // if we've already set up the binding on the XBL side. For example, if we
  // apply a binding #foo to a <span> when we've already applied it to a <div>,
  // we'll end up with a different content prototype, but we'll already have a
  // property holder called |foo| in the XBL scope. Check for that to avoid
  // wasteful and weird property holder duplication.
  const nsString& className = aPrototypeBinding->ClassName();
  const char16_t* classNameChars = className.get();
  const size_t classNameLen = className.Length();

  JS::Rooted<JSObject*> propertyHolder(cx);
  JS::Rooted<JS::PropertyDescriptor> existingHolder(cx);
  if (scopeObject != globalObject &&
      !JS_GetOwnUCPropertyDescriptor(cx, scopeObject, classNameChars,
                                     classNameLen, &existingHolder)) {
    return NS_ERROR_FAILURE;
  }
  bool propertyHolderIsNew =
      !existingHolder.object() || !existingHolder.value().isObject();

  if (!propertyHolderIsNew) {
    propertyHolder = &existingHolder.value().toObject();
  } else if (scopeObject != globalObject) {
    // This is just a property holder, so it doesn't need any special JSClass.
    propertyHolder = JS_NewObjectWithGivenProto(cx, nullptr, nullptr);
    NS_ENSURE_TRUE(propertyHolder, NS_ERROR_OUT_OF_MEMORY);

    // Define it as a property on the scopeObject, using the same name used on
    // the content side.
    bool ok =
        JS_DefineUCProperty(cx, scopeObject, classNameChars, classNameLen,
                            propertyHolder, JSPROP_PERMANENT | JSPROP_READONLY);
    NS_ENSURE_TRUE(ok, NS_ERROR_UNEXPECTED);
  } else {
    propertyHolder = targetClassObject;
  }

  // Walk our member list and install each one in turn on the XBL scope object.
  if (propertyHolderIsNew) {
    for (nsXBLProtoImplMember* curr = mMembers; curr; curr = curr->GetNext())
      curr->InstallMember(cx, propertyHolder);
  }

  // Now, if we're using a separate XBL scope, enter the compartment of the
  // bound node and copy exposable properties to the prototype there. This
  // rewraps them appropriately, which should result in cross-compartment
  // function wrappers.
  if (propertyHolder != targetClassObject) {
    AssertSameCompartment(propertyHolder, scopeObject);
    AssertSameCompartment(targetClassObject, globalObject);
    bool inContentXBLScope = xpc::IsInContentXBLScope(scopeObject);
    for (nsXBLProtoImplMember* curr = mMembers; curr; curr = curr->GetNext()) {
      if (!inContentXBLScope || curr->ShouldExposeToUntrustedContent()) {
        JS::Rooted<jsid> id(cx);
        JS::TwoByteChars chars(curr->GetName(), NS_strlen(curr->GetName()));
        bool ok = JS_CharsToId(cx, chars, &id);
        NS_ENSURE_TRUE(ok, NS_ERROR_UNEXPECTED);

        bool found;
        ok = JS_HasPropertyById(cx, propertyHolder, id, &found);
        NS_ENSURE_TRUE(ok, NS_ERROR_UNEXPECTED);
        if (!found) {
          // Some members don't install anything in InstallMember (e.g.,
          // nsXBLProtoImplAnonymousMethod). We need to skip copying in
          // those cases.
          continue;
        }

        ok = JS_CopyPropertyFrom(cx, id, targetClassObject, propertyHolder);
        NS_ENSURE_TRUE(ok, NS_ERROR_UNEXPECTED);
      }
    }
  }

  // From here on out, work in the scope of the bound element.
  JSAutoRealm ar2(cx, targetClassObject);

  // Install all of our field accessors.
  for (nsXBLProtoImplField* curr = mFields; curr; curr = curr->GetNext())
    curr->InstallAccessors(cx, targetClassObject);

  return NS_OK;
}

nsresult nsXBLProtoImpl::InitTargetObjects(
    nsXBLPrototypeBinding* aBinding, nsIContent* aBoundElement,
    JS::MutableHandle<JSObject*> aTargetClassObject, bool* aTargetIsNew) {
  nsresult rv = NS_OK;

  if (!mPrecompiledMemberHolder) {
    rv = CompilePrototypeMembers(
        aBinding);  // This is the first time we've ever installed this binding
                    // on an element. We need to go ahead and compile all
                    // methods and properties on a class in our prototype
                    // binding.
    if (NS_FAILED(rv)) return rv;

    MOZ_ASSERT(mPrecompiledMemberHolder);
  }

  Document* ownerDoc = aBoundElement->OwnerDoc();
  nsIGlobalObject* sgo;

  if (!(sgo = ownerDoc->GetScopeObject())) {
    return NS_ERROR_UNEXPECTED;
  }

  // Because our prototype implementation has a class, we need to build up a
  // corresponding class for the concrete implementation in the bound document.
  AutoJSContext cx;
  JS::Rooted<JSObject*> global(cx, sgo->GetGlobalJSObject());
  JS::Rooted<JS::Value> v(cx);

  JSAutoRealm ar(cx, global);
  // Make sure the interface object is created before the prototype object
  // so that XULElement is hidden from content. See bug 909340.
  bool defineOnGlobal = dom::XULElement_Binding::ConstructorEnabled(cx, global);
  dom::XULElement_Binding::GetConstructorObjectHandle(cx, defineOnGlobal);

  rv = nsContentUtils::WrapNative(cx, aBoundElement, &v,
                                  /* aAllowWrapping = */ false);
  NS_ENSURE_SUCCESS(rv, rv);

  JS::Rooted<JSObject*> value(cx, &v.toObject());

  // We passed aAllowWrapping = false to nsContentUtils::WrapNative so we
  // should not have a wrapper.
  MOZ_ASSERT(!js::IsWrapper(value));

  JSAutoRealm ar2(cx, value);

  // All of the above code was just obtaining the bound element's script object
  // and its immediate concrete base class.  We need to alter the object so that
  // our concrete class is interposed between the object and its base class.  We
  // become the new base class of the object, and the object's old base class
  // becomes the new class' base class.
  rv = aBinding->InitClass(mClassName, cx, value, aTargetClassObject,
                           aTargetIsNew);
  if (NS_FAILED(rv)) {
    return rv;
  }

  aBoundElement->PreserveWrapper(aBoundElement);

  return rv;
}

nsresult nsXBLProtoImpl::CompilePrototypeMembers(
    nsXBLPrototypeBinding* aBinding) {
  // We want to pre-compile our implementation's members against a "prototype
  // context". Then when we actually bind the prototype to a real xbl instance,
  // we'll clone the pre-compiled JS into the real instance's context.
  AutoJSAPI jsapi;
  if (NS_WARN_IF(!jsapi.Init(xpc::CompilationScope()))) return NS_ERROR_FAILURE;
  JSContext* cx = jsapi.cx();

  mPrecompiledMemberHolder = JS_NewObjectWithGivenProto(cx, nullptr, nullptr);
  if (!mPrecompiledMemberHolder) return NS_ERROR_OUT_OF_MEMORY;

  // Now that we have a class object installed, we walk our member list and
  // compile each of our properties and methods in turn.
  JS::Rooted<JSObject*> rootedHolder(cx, mPrecompiledMemberHolder);
  for (nsXBLProtoImplMember* curr = mMembers; curr; curr = curr->GetNext()) {
    nsresult rv = curr->CompileMember(jsapi, mClassName, rootedHolder);
    if (NS_FAILED(rv)) {
      DestroyMembers();
      return rv;
    }
  }

  return NS_OK;
}

bool nsXBLProtoImpl::LookupMember(
    JSContext* aCx, nsString& aName, JS::Handle<jsid> aNameAsId,
    JS::MutableHandle<JS::PropertyDescriptor> aDesc,
    JS::Handle<JSObject*> aClassObject) {
  for (nsXBLProtoImplMember* m = mMembers; m; m = m->GetNext()) {
    if (aName.Equals(m->GetName())) {
      return JS_GetPropertyDescriptorById(aCx, aClassObject, aNameAsId, aDesc);
    }
  }
  return true;
}

void nsXBLProtoImpl::Trace(const TraceCallbacks& aCallbacks, void* aClosure) {
  // If we don't have a class object then we either didn't compile members
  // or we only have fields, in both cases there are no cycles through our
  // members.
  if (!mPrecompiledMemberHolder) {
    return;
  }

  nsXBLProtoImplMember* member;
  for (member = mMembers; member; member = member->GetNext()) {
    member->Trace(aCallbacks, aClosure);
  }
}

void nsXBLProtoImpl::UnlinkJSObjects() {
  if (mPrecompiledMemberHolder) {
    DestroyMembers();
  }
}

nsXBLProtoImplField* nsXBLProtoImpl::FindField(
    const nsString& aFieldName) const {
  for (nsXBLProtoImplField* f = mFields; f; f = f->GetNext()) {
    if (aFieldName.Equals(f->GetName())) {
      return f;
    }
  }

  return nullptr;
}

bool nsXBLProtoImpl::ResolveAllFields(JSContext* cx,
                                      JS::Handle<JSObject*> obj) const {
  for (nsXBLProtoImplField* f = mFields; f; f = f->GetNext()) {
    nsDependentString name(f->GetName());
    bool dummy;
    if (!::JS_HasUCProperty(cx, obj, name.get(), name.Length(), &dummy)) {
      return false;
    }
  }

  return true;
}

void nsXBLProtoImpl::UndefineFields(JSContext* cx,
                                    JS::Handle<JSObject*> obj) const {
  for (nsXBLProtoImplField* f = mFields; f; f = f->GetNext()) {
    nsDependentString name(f->GetName());

    const char16_t* s = name.get();
    bool hasProp;
    if (::JS_AlreadyHasOwnUCProperty(cx, obj, s, name.Length(), &hasProp) &&
        hasProp) {
      JS::ObjectOpResult ignored;
      ::JS_DeleteUCProperty(cx, obj, s, name.Length(), ignored);
    }
  }
}

void nsXBLProtoImpl::DestroyMembers() {
  MOZ_ASSERT(mPrecompiledMemberHolder);

  delete mMembers;
  mMembers = nullptr;
  mConstructor = nullptr;
  mDestructor = nullptr;
}

nsresult nsXBLProtoImpl::Read(nsIObjectInputStream* aStream,
                              nsXBLPrototypeBinding* aBinding) {
  AssertInCompilationScope();
  AutoJSContext cx;
  // Set up a class object first so that deserialization is possible
  mPrecompiledMemberHolder = JS_NewObjectWithGivenProto(cx, nullptr, nullptr);
  if (!mPrecompiledMemberHolder) return NS_ERROR_OUT_OF_MEMORY;

  nsXBLProtoImplField* previousField = nullptr;
  nsXBLProtoImplMember* previousMember = nullptr;

  do {
    XBLBindingSerializeDetails type;
    nsresult rv = aStream->Read8(&type);
    NS_ENSURE_SUCCESS(rv, rv);
    if (type == XBLBinding_Serialize_NoMoreItems) break;

    switch (type & XBLBinding_Serialize_Mask) {
      case XBLBinding_Serialize_Field: {
        nsXBLProtoImplField* field =
            new nsXBLProtoImplField(type & XBLBinding_Serialize_ReadOnly);
        rv = field->Read(aStream);
        if (NS_FAILED(rv)) {
          delete field;
          return rv;
        }

        if (previousField) {
          previousField->SetNext(field);
        } else {
          mFields = field;
        }
        previousField = field;

        break;
      }
      case XBLBinding_Serialize_GetterProperty:
      case XBLBinding_Serialize_SetterProperty:
      case XBLBinding_Serialize_GetterSetterProperty: {
        nsAutoString name;
        nsresult rv = aStream->ReadString(name);
        NS_ENSURE_SUCCESS(rv, rv);

        nsXBLProtoImplProperty* prop = new nsXBLProtoImplProperty(
            name.get(), type & XBLBinding_Serialize_ReadOnly);
        rv = prop->Read(aStream, type & XBLBinding_Serialize_Mask);
        if (NS_FAILED(rv)) {
          delete prop;
          return rv;
        }

        previousMember = AddMember(prop, previousMember);
        break;
      }
      case XBLBinding_Serialize_Method: {
        nsAutoString name;
        rv = aStream->ReadString(name);
        NS_ENSURE_SUCCESS(rv, rv);

        nsXBLProtoImplMethod* method = new nsXBLProtoImplMethod(name.get());
        rv = method->Read(aStream);
        if (NS_FAILED(rv)) {
          delete method;
          return rv;
        }

        previousMember = AddMember(method, previousMember);
        break;
      }
      case XBLBinding_Serialize_Constructor: {
        nsAutoString name;
        rv = aStream->ReadString(name);
        NS_ENSURE_SUCCESS(rv, rv);

        mConstructor = new nsXBLProtoImplAnonymousMethod(name.get());
        rv = mConstructor->Read(aStream);
        if (NS_FAILED(rv)) {
          delete mConstructor;
          mConstructor = nullptr;
          return rv;
        }

        previousMember = AddMember(mConstructor, previousMember);
        break;
      }
      case XBLBinding_Serialize_Destructor: {
        nsAutoString name;
        rv = aStream->ReadString(name);
        NS_ENSURE_SUCCESS(rv, rv);

        mDestructor = new nsXBLProtoImplAnonymousMethod(name.get());
        rv = mDestructor->Read(aStream);
        if (NS_FAILED(rv)) {
          delete mDestructor;
          mDestructor = nullptr;
          return rv;
        }

        previousMember = AddMember(mDestructor, previousMember);
        break;
      }
      default:
        NS_ERROR("Unexpected binding member type");
        break;
    }
  } while (1);

  return NS_OK;
}

nsresult nsXBLProtoImpl::Write(nsIObjectOutputStream* aStream,
                               nsXBLPrototypeBinding* aBinding) {
  nsresult rv;

  if (!mPrecompiledMemberHolder) {
    rv = CompilePrototypeMembers(aBinding);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  rv = aStream->WriteUtf8Z(mClassName.get());
  NS_ENSURE_SUCCESS(rv, rv);

  for (nsXBLProtoImplField* curr = mFields; curr; curr = curr->GetNext()) {
    rv = curr->Write(aStream);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  for (nsXBLProtoImplMember* curr = mMembers; curr; curr = curr->GetNext()) {
    if (curr == mConstructor) {
      rv = mConstructor->Write(aStream, XBLBinding_Serialize_Constructor);
    } else if (curr == mDestructor) {
      rv = mDestructor->Write(aStream, XBLBinding_Serialize_Destructor);
    } else {
      rv = curr->Write(aStream);
    }
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return aStream->Write8(XBLBinding_Serialize_NoMoreItems);
}

void NS_NewXBLProtoImpl(nsXBLPrototypeBinding* aBinding,
                        const char16_t* aClassName, nsXBLProtoImpl** aResult) {
  nsXBLProtoImpl* impl = new nsXBLProtoImpl();
  if (aClassName) {
    impl->mClassName = aClassName;
  } else {
    nsCString spec;
    nsresult rv = aBinding->BindingURI()->GetSpec(spec);
    // XXX: should handle this better
    MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
    impl->mClassName = NS_ConvertUTF8toUTF16(spec);
  }

  aBinding->SetImplementation(impl);
  *aResult = impl;
}