/* -*- 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 "jsfriendapi.h" #include "jswrapper.h" #include "nsIXPConnect.h" #include "qsObjectHelper.h" #include "xpcpublic.h" #include "nsTraceRefcnt.h" #include "nsWrapperCacheInlines.h" #include "mozilla/Likely.h" #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/CallbackObject.h" 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; } if (rv.IsJSException()) { rv.ReportJSException(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)); } MOZ_STATIC_ASSERT(DOM_OBJECT_SLOT == js::JSSLOT_PROXY_PRIVATE, "JSSLOT_PROXY_PRIVATE doesn't match DOM_OBJECT_SLOT. " "Expect bad things"); template inline T* UnwrapDOMObject(JSObject* obj) { MOZ_ASSERT(IsDOMClass(js::GetObjectClass(obj)) || IsDOMProxy(obj), "Don't pass non-DOM objects to this function"); JS::Value val = js::GetReservedSlot(obj, DOM_OBJECT_SLOT); return static_cast(val.toPrivate()); } inline const DOMClass* GetDOMClass(JSObject* obj) { js::Class* clasp = js::GetObjectClass(obj); if (IsDOMClass(clasp)) { return &DOMJSClass::FromJSClass(clasp)->mClass; } if (js::IsObjectProxyClass(clasp) || js::IsFunctionProxyClass(clasp)) { js::BaseProxyHandler* handler = js::GetProxyHandler(obj); if (handler->family() == ProxyFamily()) { return &static_cast(handler)->mClass; } } return nullptr; } inline bool UnwrapDOMObjectToISupports(JSObject* obj, nsISupports*& result) { const DOMClass* clasp = GetDOMClass(obj); if (!clasp || !clasp->mDOMObjectIsISupports) { return false; } result = UnwrapDOMObject(obj); 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 MOZ_ALWAYS_INLINE nsresult UnwrapObject(JSContext* cx, JSObject* obj, U& value) { /* First check to see whether we have a DOM object */ const DOMClass* domClass = GetDOMClass(obj); if (!domClass) { /* 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 = js::CheckedUnwrap(obj, /* stopAtOuter = */ false); if (!obj) { return NS_ERROR_XPC_SECURITY_MANAGER_VETO; } MOZ_ASSERT(!js::IsWrapper(obj)); domClass = GetDOMClass(obj); if (!domClass) { /* 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); return NS_OK; } /* It's the wrong sort of DOM object */ return NS_ERROR_XPC_BAD_CONVERT_JS; } inline bool IsNotDateOrRegExp(JSContext* cx, JSObject* obj) { MOZ_ASSERT(obj); return !JS_ObjectIsDate(cx, obj) && !JS_ObjectIsRegExp(cx, obj); } MOZ_ALWAYS_INLINE bool IsArrayLike(JSContext* cx, JSObject* obj) { return IsNotDateOrRegExp(cx, obj); } MOZ_ALWAYS_INLINE bool IsConvertibleToDictionary(JSContext* cx, JSObject* obj) { return IsNotDateOrRegExp(cx, obj); } MOZ_ALWAYS_INLINE bool IsConvertibleToDictionary(JSContext* cx, JS::Value val) { return val.isNullOrUndefined() || (val.isObject() && IsConvertibleToDictionary(cx, &val.toObject())); } MOZ_ALWAYS_INLINE bool IsConvertibleToCallbackInterface(JSContext* cx, JSObject* obj) { return IsNotDateOrRegExp(cx, 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_CallObjectTracer(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, const ConstantSpec* cs); struct JSNativeHolder { JSNative mNative; const NativePropertyHooks* mPropertyHooks; }; struct NamedConstructor { const char* mName; const JSNativeHolder mHolder; unsigned mNargs; }; /* * 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, const JSNativeHolder* constructor, unsigned ctorNargs, const NamedConstructor* namedConstructors, 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, const Prefable* props); bool DefineWebIDLBindingPropertiesOnXPCProto(JSContext* cx, JSObject* proto, const NativeProperties* properties); #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*, JS::Handle); template struct SFINAE; template static no& Check(SFINAE*); template static yes& Check(...); public: static bool const Value = HasWrapObjectMember::Value && sizeof(Check(nullptr)) == sizeof(yes); }; #ifdef DEBUG template ::Value> struct CheckWrapperCacheCast { static bool Check() { return reinterpret_cast( static_cast( reinterpret_cast(1))) == 1; } }; template struct CheckWrapperCacheCast { static bool Check() { return true; } }; #endif MOZ_ALWAYS_INLINE bool CouldBeDOMBinding(void*) { return true; } MOZ_ALWAYS_INLINE bool CouldBeDOMBinding(nsWrapperCache* aCache) { return aCache->IsDOMBinding(); } // The DOM_OBJECT_SLOT_SOW slot contains a JS::ObjectValue which points to the // cached system object wrapper (SOW) or JS::UndefinedValue if this class // doesn't need SOWs. inline const JS::Value& GetSystemOnlyWrapperSlot(JSObject* obj) { MOZ_ASSERT(IsDOMClass(js::GetObjectJSClass(obj)) && !(js::GetObjectJSClass(obj)->flags & JSCLASS_DOM_GLOBAL)); return js::GetReservedSlot(obj, DOM_OBJECT_SLOT_SOW); } inline void SetSystemOnlyWrapperSlot(JSObject* obj, const JS::Value& v) { MOZ_ASSERT(IsDOMClass(js::GetObjectJSClass(obj)) && !(js::GetObjectJSClass(obj)->flags & JSCLASS_DOM_GLOBAL)); js::SetReservedSlot(obj, DOM_OBJECT_SLOT_SOW, v); } inline bool GetSameCompartmentWrapperForDOMBinding(JSObject*& obj) { js::Class* clasp = js::GetObjectClass(obj); if (dom::IsDOMClass(clasp)) { if (!(clasp->flags & JSCLASS_DOM_GLOBAL)) { JS::Value v = GetSystemOnlyWrapperSlot(obj); if (v.isObject()) { obj = &v.toObject(); } } return true; } return IsDOMProxy(obj, clasp); } inline void SetSystemOnlyWrapper(JSObject* obj, nsWrapperCache* cache, JSObject& wrapper) { SetSystemOnlyWrapperSlot(obj, JS::ObjectValue(wrapper)); cache->SetHasSystemOnlyWrapper(); } // If *vp is a gcthing and is not in the compartment of cx, wrap *vp // into the compartment of cx (typically by replacing it with an Xray or // cross-compartment wrapper around the original object). MOZ_ALWAYS_INLINE bool MaybeWrapValue(JSContext* cx, JS::Value* vp) { if (vp->isString()) { JSString* str = vp->toString(); if (JS::GetGCThingZone(str) != js::GetContextZone(cx)) { return JS_WrapValue(cx, vp); } return true; } if (vp->isObject()) { JSObject* obj = &vp->toObject(); if (js::GetObjectCompartment(obj) != js::GetContextCompartment(cx)) { return JS_WrapValue(cx, vp); } // We're same-compartment, but even then we might need to wrap // objects specially. Check for that. if (GetSameCompartmentWrapperForDOMBinding(obj)) { // We're a new-binding object, and "obj" now points to the right thing *vp = JS::ObjectValue(*obj); return true; } if (!IS_SLIM_WRAPPER(obj)) { // We might need a SOW return JS_WrapValue(cx, vp); } // Fall through to returning true } return true; } static inline void WrapNewBindingForSameCompartment(JSContext* cx, JSObject* obj, void* value, JS::Value* vp) { *vp = JS::ObjectValue(*obj); } static inline void WrapNewBindingForSameCompartment(JSContext* cx, JSObject* obj, nsWrapperCache* value, JS::Value* vp) { if (value->HasSystemOnlyWrapper()) { *vp = GetSystemOnlyWrapperSlot(obj); MOZ_ASSERT(vp->isObject()); } else { *vp = JS::ObjectValue(*obj); } } // 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 // GetWrapperPreserveColor() 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 MOZ_ALWAYS_INLINE bool WrapNewBindingObject(JSContext* cx, JS::Handle scope, T* value, JS::Value* vp) { MOZ_ASSERT(value); JSObject* obj = value->GetWrapperPreserveColor(); bool couldBeDOMBinding = CouldBeDOMBinding(value); if (obj) { xpc_UnmarkNonNullGrayObject(obj); } else { // Inline this here while we have non-dom objects in wrapper caches. if (!couldBeDOMBinding) { return false; } obj = value->WrapObject(cx, scope); if (!obj) { // At this point, obj is null, so just return false. // Callers seem to be testing JS_IsExceptionPending(cx) to // figure out whether WrapObject() threw. return false; } } #ifdef DEBUG const DOMClass* clasp = GetDOMClass(obj); // clasp can be null if the cache contained a non-DOM object from a // different compartment than scope. if (clasp) { // 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. MOZ_ASSERT(clasp, "What happened here?"); MOZ_ASSERT_IF(clasp->mDOMObjectIsISupports, IsISupports::Value); MOZ_ASSERT(CheckWrapperCacheCast::Check()); } // 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)); #endif bool sameCompartment = js::GetObjectCompartment(obj) == js::GetContextCompartment(cx); if (sameCompartment && couldBeDOMBinding) { WrapNewBindingForSameCompartment(cx, obj, value, vp); return true; } *vp = JS::ObjectValue(*obj); return (sameCompartment && IS_SLIM_WRAPPER(obj)) || JS_WrapValue(cx, vp); } // 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 WrapNewBindingNonWrapperCachedObject(JSContext* cx, JS::Handle scopeArg, T* value, JS::Value* vp) { MOZ_ASSERT(value); // 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; // Maybe doesn't so much work, and in any case, adding // more Maybe (one for a Rooted and one for a Handle) adds more // code (and branches!) than just adding a single rooted. JS::Rooted scope(cx, scopeArg); if (js::IsWrapper(scope)) { scope = js::CheckedUnwrap(scope, /* stopAtOuter = */ false); if (!scope) return false; ac.construct(cx, scope); } obj = value->WrapObject(cx, scope); } if (!obj) { return false; } // 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); } // Create a JSObject wrapping "value", for cases when "value" is a // non-wrapper-cached owned object using WebIDL bindings. "value" must implement a // WrapObject() method taking a JSContext, a scope, and a boolean outparam that // is true if the JSObject took ownership template inline bool WrapNewBindingNonWrapperCachedOwnedObject(JSContext* cx, JS::Handle scopeArg, nsAutoPtr& value, JS::Value* vp) { // We do a runtime check on value, because otherwise we might in // fact end up wrapping a null and invoking methods on it later. if (!value) { NS_RUNTIMEABORT("Don't try to wrap null objects"); } // 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; // Maybe doesn't so much work, and in any case, adding // more Maybe (one for a Rooted and one for a Handle) adds more // code (and branches!) than just adding a single rooted. JS::Rooted scope(cx, scopeArg); if (js::IsWrapper(scope)) { scope = js::CheckedUnwrap(scope, /* stopAtOuter = */ false); if (!scope) return false; ac.construct(cx, scope); } bool tookOwnership = false; obj = value->WrapObject(cx, scope, &tookOwnership); if (obj) { MOZ_ASSERT(tookOwnership); } if (tookOwnership) { value.forget(); } } if (!obj) { return false; } // 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