/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ /* vim: set ts=2 sw=2 et tw=79: */ /* 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/. */ #ifndef mozilla_dom_BindingUtils_h__ #define mozilla_dom_BindingUtils_h__ #include "mozilla/dom/DOMJSClass.h" #include "mozilla/dom/DOMJSProxyHandler.h" #include "mozilla/dom/NonRefcountedDOMObject.h" #include "mozilla/dom/workers/Workers.h" #include "mozilla/ErrorResult.h" #include "jsapi.h" #include "jsfriendapi.h" #include "jswrapper.h" #include "nsIXPConnect.h" #include "qsObjectHelper.h" #include "xpcpublic.h" #include "nsTraceRefcnt.h" #include "nsWrapperCacheInlines.h" #include "mozilla/Likely.h" // nsGlobalWindow implements nsWrapperCache, but doesn't always use it. Don't // try to use it without fixing that first. class nsGlobalWindow; namespace mozilla { namespace dom { bool ThrowErrorMessage(JSContext* aCx, const ErrNum aErrorNumber, ...); template inline bool Throw(JSContext* cx, nsresult rv) { using mozilla::dom::workers::exceptions::ThrowDOMExceptionForNSResult; // XXX Introduce exception machinery. if (mainThread) { xpc::Throw(cx, rv); } else { if (!JS_IsExceptionPending(cx)) { ThrowDOMExceptionForNSResult(cx, rv); } } return false; } template inline bool ThrowMethodFailedWithDetails(JSContext* cx, ErrorResult& rv, const char* /* ifaceName */, const char* /* memberName */) { if (rv.IsTypeError()) { rv.ReportTypeError(cx); return false; } return Throw(cx, rv.ErrorCode()); } // Returns true if the JSClass is used for DOM objects. inline bool IsDOMClass(const JSClass* clasp) { return clasp->flags & JSCLASS_IS_DOMJSCLASS; } inline bool IsDOMClass(const js::Class* clasp) { return IsDOMClass(Jsvalify(clasp)); } // Returns true if the JSClass is used for DOM interface and interface // prototype objects. inline bool IsDOMIfaceAndProtoClass(const JSClass* clasp) { return clasp->flags & JSCLASS_IS_DOMIFACEANDPROTOJSCLASS; } inline bool IsDOMIfaceAndProtoClass(const js::Class* clasp) { return IsDOMIfaceAndProtoClass(Jsvalify(clasp)); } // It's ok for eRegularDOMObject and eProxyDOMObject to be the same, but // eNonDOMObject should always be different from the other two. This enum // shouldn't be used to differentiate between non-proxy and proxy bindings. enum DOMObjectSlot { eNonDOMObject = -1, eRegularDOMObject = DOM_OBJECT_SLOT, eProxyDOMObject = DOM_PROXY_OBJECT_SLOT }; template inline T* UnwrapDOMObject(JSObject* obj, DOMObjectSlot slot) { MOZ_ASSERT(slot != eNonDOMObject, "Don't pass non-DOM objects to this function"); #ifdef DEBUG if (IsDOMClass(js::GetObjectClass(obj))) { MOZ_ASSERT(slot == eRegularDOMObject); } else { MOZ_ASSERT(IsDOMProxy(obj)); MOZ_ASSERT(slot == eProxyDOMObject); } #endif JS::Value val = js::GetReservedSlot(obj, slot); // XXXbz/khuey worker code tries to unwrap interface objects (which have // nothing here). That needs to stop. // XXX We don't null-check UnwrapObject's result; aren't we going to crash // anyway? if (val.isUndefined()) { return NULL; } return static_cast(val.toPrivate()); } // Only use this with a new DOM binding object (either proxy or regular). inline const DOMClass* GetDOMClass(JSObject* obj) { js::Class* clasp = js::GetObjectClass(obj); if (IsDOMClass(clasp)) { return &DOMJSClass::FromJSClass(clasp)->mClass; } MOZ_ASSERT(IsDOMProxy(obj)); js::BaseProxyHandler* handler = js::GetProxyHandler(obj); return &static_cast(handler)->mClass; } inline DOMObjectSlot GetDOMClass(JSObject* obj, const DOMClass*& result) { js::Class* clasp = js::GetObjectClass(obj); if (IsDOMClass(clasp)) { result = &DOMJSClass::FromJSClass(clasp)->mClass; return eRegularDOMObject; } if (js::IsObjectProxyClass(clasp) || js::IsFunctionProxyClass(clasp)) { js::BaseProxyHandler* handler = js::GetProxyHandler(obj); if (handler->family() == ProxyFamily()) { result = &static_cast(handler)->mClass; return eProxyDOMObject; } } return eNonDOMObject; } inline bool UnwrapDOMObjectToISupports(JSObject* obj, nsISupports*& result) { const DOMClass* clasp; DOMObjectSlot slot = GetDOMClass(obj, clasp); if (slot == eNonDOMObject || !clasp->mDOMObjectIsISupports) { return false; } result = UnwrapDOMObject(obj, slot); return true; } inline bool IsDOMObject(JSObject* obj) { js::Class* clasp = js::GetObjectClass(obj); return IsDOMClass(clasp) || IsDOMProxy(obj, clasp); } // Some callers don't want to set an exception when unwrapping fails // (for example, overload resolution uses unwrapping to tell what sort // of thing it's looking at). // U must be something that a T* can be assigned to (e.g. T* or an nsRefPtr). template inline nsresult UnwrapObject(JSContext* cx, JSObject* obj, U& value) { /* First check to see whether we have a DOM object */ const DOMClass* domClass; DOMObjectSlot slot = GetDOMClass(obj, domClass); if (slot == eNonDOMObject) { /* Maybe we have a security wrapper or outer window? */ if (!js::IsWrapper(obj)) { /* Not a DOM object, not a wrapper, just bail */ return NS_ERROR_XPC_BAD_CONVERT_JS; } obj = xpc::Unwrap(cx, obj, false); if (!obj) { return NS_ERROR_XPC_SECURITY_MANAGER_VETO; } MOZ_ASSERT(!js::IsWrapper(obj)); slot = GetDOMClass(obj, domClass); if (slot == eNonDOMObject) { /* We don't have a DOM object */ return NS_ERROR_XPC_BAD_CONVERT_JS; } } /* This object is a DOM object. Double-check that it is safely castable to T by checking whether it claims to inherit from the class identified by protoID. */ if (domClass->mInterfaceChain[PrototypeTraits::Depth] == PrototypeID) { value = UnwrapDOMObject(obj, slot); return NS_OK; } /* It's the wrong sort of DOM object */ return NS_ERROR_XPC_BAD_CONVERT_JS; } inline bool IsArrayLike(JSContext* cx, JSObject* obj) { MOZ_ASSERT(obj); // For simplicity, check for security wrappers up front. In case we // have a security wrapper, don't forget to enter the compartment of // the underlying object after unwrapping. Maybe ac; if (js::IsWrapper(obj)) { obj = xpc::Unwrap(cx, obj, false); if (!obj) { // Let's say it's not return false; } ac.construct(cx, obj); } // XXXbz need to detect platform objects (including listbinding // ones) with indexGetters here! return JS_IsArrayObject(cx, obj) || JS_IsTypedArrayObject(obj); } inline bool IsPlatformObject(JSContext* cx, JSObject* obj) { // XXXbz Should be treating list-binding objects as platform objects // too? The one consumer so far wants non-array-like platform // objects, so listbindings that have an indexGetter should test // false from here. Maybe this function should have a different // name? MOZ_ASSERT(obj); // Fast-path the common case JSClass* clasp = js::GetObjectJSClass(obj); if (IsDOMClass(clasp)) { return true; } // Now for simplicity check for security wrappers before anything else if (js::IsWrapper(obj)) { obj = xpc::Unwrap(cx, obj, false); if (!obj) { // Let's say it's not return false; } clasp = js::GetObjectJSClass(obj); } return IS_WRAPPER_CLASS(js::Valueify(clasp)) || IsDOMClass(clasp) || JS_IsArrayBufferObject(obj); } // U must be something that a T* can be assigned to (e.g. T* or an nsRefPtr). template inline nsresult UnwrapObject(JSContext* cx, JSObject* obj, U& value) { return UnwrapObject( PrototypeIDMap::PrototypeID), T>(cx, obj, value); } // The items in the protoAndIfaceArray are indexed by the prototypes::id::ID and // constructors::id::ID enums, in that order. The end of the prototype objects // should be the start of the interface objects. MOZ_STATIC_ASSERT((size_t)constructors::id::_ID_Start == (size_t)prototypes::id::_ID_Count, "Overlapping or discontiguous indexes."); const size_t kProtoAndIfaceCacheCount = constructors::id::_ID_Count; inline void AllocateProtoAndIfaceCache(JSObject* obj) { MOZ_ASSERT(js::GetObjectClass(obj)->flags & JSCLASS_DOM_GLOBAL); MOZ_ASSERT(js::GetReservedSlot(obj, DOM_PROTOTYPE_SLOT).isUndefined()); // Important: The () at the end ensure zero-initialization JSObject** protoAndIfaceArray = new JSObject*[kProtoAndIfaceCacheCount](); js::SetReservedSlot(obj, DOM_PROTOTYPE_SLOT, JS::PrivateValue(protoAndIfaceArray)); } inline void TraceProtoAndIfaceCache(JSTracer* trc, JSObject* obj) { MOZ_ASSERT(js::GetObjectClass(obj)->flags & JSCLASS_DOM_GLOBAL); if (!HasProtoAndIfaceArray(obj)) return; JSObject** protoAndIfaceArray = GetProtoAndIfaceArray(obj); for (size_t i = 0; i < kProtoAndIfaceCacheCount; ++i) { JSObject* proto = protoAndIfaceArray[i]; if (proto) { JS_CALL_OBJECT_TRACER(trc, proto, "protoAndIfaceArray[i]"); } } } inline void DestroyProtoAndIfaceCache(JSObject* obj) { MOZ_ASSERT(js::GetObjectClass(obj)->flags & JSCLASS_DOM_GLOBAL); JSObject** protoAndIfaceArray = GetProtoAndIfaceArray(obj); delete [] protoAndIfaceArray; } /** * Add constants to an object. */ bool DefineConstants(JSContext* cx, JSObject* obj, ConstantSpec* cs); struct JSNativeHolder { JSNative mNative; const NativePropertyHooks* mPropertyHooks; }; /* * Create a DOM interface object (if constructorClass is non-null) and/or a * DOM interface prototype object (if protoClass is non-null). * * global is used as the parent of the interface object and the interface * prototype object * protoProto is the prototype to use for the interface prototype object. * protoClass is the JSClass to use for the interface prototype object. * This is null if we should not create an interface prototype * object. * protoCache a pointer to a JSObject pointer where we should cache the * interface prototype object. This must be null if protoClass is and * vice versa. * constructorClass is the JSClass to use for the interface object. * This is null if we should not create an interface object or * if it should be a function object. * constructor holds the JSNative to back the interface object which should be a * Function, unless constructorClass is non-null in which case it is * ignored. If this is null and constructorClass is also null then * we should not create an interface object at all. * ctorNargs is the length of the constructor function; 0 if no constructor * constructorCache a pointer to a JSObject pointer where we should cache the * interface object. This must be null if both constructorClass * and constructor are null, and non-null otherwise. * domClass is the DOMClass of instance objects for this class. This can be * null if this is not a concrete proto. * properties contains the methods, attributes and constants to be defined on * objects in any compartment. * chromeProperties contains the methods, attributes and constants to be defined * on objects in chrome compartments. This must be null if the * interface doesn't have any ChromeOnly properties or if the * object is being created in non-chrome compartment. * * At least one of protoClass, constructorClass or constructor should be * non-null. If constructorClass or constructor are non-null, the resulting * interface object will be defined on the given global with property name * |name|, which must also be non-null. */ void CreateInterfaceObjects(JSContext* cx, JSObject* global, JSObject* protoProto, JSClass* protoClass, JSObject** protoCache, JSClass* constructorClass, JSNativeHolder* constructor, unsigned ctorNargs, JSObject** constructorCache, const DOMClass* domClass, const NativeProperties* regularProperties, const NativeProperties* chromeOnlyProperties, const char* name); /* * Define the unforgeable attributes on an object. */ bool DefineUnforgeableAttributes(JSContext* cx, JSObject* obj, Prefable* props); // If *vp is an object and *vp and obj are not in the same compartment, wrap *vp // into the compartment of obj (typically by replacing it with an Xray or // cross-compartment wrapper around the original object). inline bool MaybeWrapValue(JSContext* cx, JSObject* obj, JS::Value* vp) { MOZ_ASSERT(js::GetObjectCompartment(obj) == js::GetContextCompartment(cx)); if (vp->isObject() && js::GetObjectCompartment(&vp->toObject()) != js::GetObjectCompartment(obj)) { return JS_WrapValue(cx, vp); } return true; } #ifdef _MSC_VER #define HAS_MEMBER_CHECK(_name) \ template static yes& Check(char (*)[(&V::_name == 0) + 1]) #else #define HAS_MEMBER_CHECK(_name) \ template static yes& Check(char (*)[sizeof(&V::_name) + 1]) #endif #define HAS_MEMBER(_name) \ template \ class Has##_name##Member { \ typedef char yes[1]; \ typedef char no[2]; \ HAS_MEMBER_CHECK(_name); \ template static no& Check(...); \ \ public: \ static bool const Value = sizeof(Check(nullptr)) == sizeof(yes); \ }; HAS_MEMBER(AddRef) HAS_MEMBER(Release) HAS_MEMBER(QueryInterface) template struct IsRefCounted { static bool const Value = HasAddRefMember::Value && HasReleaseMember::Value; }; template struct IsISupports { static bool const Value = IsRefCounted::Value && HasQueryInterfaceMember::Value; }; HAS_MEMBER(WrapObject) // HasWrapObject::Value will be true if T has a WrapObject member but it's // not nsWrapperCache::WrapObject. template struct HasWrapObject { private: typedef char yes[1]; typedef char no[2]; typedef JSObject* (nsWrapperCache::*WrapObject)(JSContext*, JSObject*, bool*); template struct SFINAE; template static no& Check(SFINAE*); template static yes& Check(...); public: static bool const Value = HasWrapObjectMember::Value && sizeof(Check(nullptr)) == sizeof(yes); }; // Create a JSObject wrapping "value", for cases when "value" is a // non-wrapper-cached object using WebIDL bindings. "value" must implement a // WrapObject() method taking a JSContext and a scope. template inline bool WrapNewBindingObject(JSContext* cx, JSObject* scope, T* value, JS::Value* vp) { JSObject* obj = value->GetWrapperPreserveColor(); if (obj) { xpc_UnmarkNonNullGrayObject(obj); if (js::GetObjectCompartment(obj) == js::GetObjectCompartment(scope)) { *vp = JS::ObjectValue(*obj); return true; } } if (!obj) { bool triedToWrap; obj = value->WrapObject(cx, scope, &triedToWrap); if (!obj) { // At this point, obj is null, so just return false. We could // try to communicate triedToWrap to the caller, but in practice // callers seem to be testing JS_IsExceptionPending(cx) to // figure out whether WrapObject() threw instead. return false; } } #ifdef DEBUG // Some sanity asserts about our object. Specifically: // 1) If our class claims we're nsISupports, we better be nsISupports // XXXbz ideally, we could assert that reinterpret_cast to nsISupports // does the right thing, but I don't see a way to do it. :( // 2) If our class doesn't claim we're nsISupports we better be // reinterpret_castable to nsWrapperCache. const DOMClass* clasp = nullptr; DOMObjectSlot slot = GetDOMClass(obj, clasp); MOZ_ASSERT(slot != eNonDOMObject, "Totally unexpected object here"); MOZ_ASSERT(clasp, "What happened here?"); MOZ_ASSERT_IF(clasp->mDOMObjectIsISupports, IsISupports::Value); // MOZ_ASSERT_IF(!clasp->mDOMObjectIsISupports, // reinterpret_cast( // static_cast( // reinterpret_cast(1))) == 1); #endif // When called via XrayWrapper, we end up here while running in the // chrome compartment. But the obj we have would be created in // whatever the content compartment is. So at this point we need to // make sure it's correctly wrapped for the compartment of |scope|. // cx should already be in the compartment of |scope| here. MOZ_ASSERT(js::IsObjectInContextCompartment(scope, cx)); *vp = JS::ObjectValue(*obj); return JS_WrapValue(cx, vp); } // Create a JSObject wrapping "value", if there isn't one already, and store it // in *vp. "value" must be a concrete class that implements a GetWrapper() // which can return its existing wrapper, if any, and a WrapObject() which will // try to create a wrapper. Typically, this is done by having "value" inherit // from nsWrapperCache. template inline bool WrapNewBindingNonWrapperCachedObject(JSContext* cx, JSObject* scope, T* value, JS::Value* vp) { // We try to wrap in the compartment of the underlying object of "scope" JSObject* obj; { // scope for the JSAutoCompartment so that we restore the compartment // before we call JS_WrapValue. Maybe ac; if (js::IsWrapper(scope)) { scope = xpc::Unwrap(cx, scope, false); if (!scope) return false; ac.construct(cx, scope); } obj = value->WrapObject(cx, scope); } // We can end up here in all sorts of compartments, per above. Make // sure to JS_WrapValue! *vp = JS::ObjectValue(*obj); return JS_WrapValue(cx, vp); } // Helper for smart pointers (nsAutoPtr/nsRefPtr/nsCOMPtr). template