/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 "nsIAtom.h" #include "nsIContent.h" #include "nsString.h" #include "nsJSUtils.h" #include "jsapi.h" #include "nsUnicharUtils.h" #include "nsReadableUtils.h" #include "nsXBLProtoImplField.h" #include "nsIScriptContext.h" #include "nsIURI.h" #include "nsXBLSerialize.h" #include "nsXBLPrototypeBinding.h" #include "mozilla/dom/BindingUtils.h" #include "xpcpublic.h" #include "WrapperFactory.h" using namespace mozilla; using namespace mozilla::dom; nsXBLProtoImplField::nsXBLProtoImplField(const PRUnichar* aName, const PRUnichar* aReadOnly) : mNext(nullptr), mFieldText(nullptr), mFieldTextLength(0), mLineNumber(0) { MOZ_COUNT_CTOR(nsXBLProtoImplField); mName = NS_strdup(aName); // XXXbz make more sense to use a stringbuffer? mJSAttributes = JSPROP_ENUMERATE; if (aReadOnly) { nsAutoString readOnly; readOnly.Assign(aReadOnly); if (readOnly.LowerCaseEqualsLiteral("true")) mJSAttributes |= JSPROP_READONLY; } } nsXBLProtoImplField::nsXBLProtoImplField(const bool aIsReadOnly) : mNext(nullptr), mFieldText(nullptr), mFieldTextLength(0), mLineNumber(0) { MOZ_COUNT_CTOR(nsXBLProtoImplField); mJSAttributes = JSPROP_ENUMERATE; if (aIsReadOnly) mJSAttributes |= JSPROP_READONLY; } nsXBLProtoImplField::~nsXBLProtoImplField() { MOZ_COUNT_DTOR(nsXBLProtoImplField); if (mFieldText) nsMemory::Free(mFieldText); NS_Free(mName); NS_CONTENT_DELETE_LIST_MEMBER(nsXBLProtoImplField, this, mNext); } void nsXBLProtoImplField::AppendFieldText(const nsAString& aText) { if (mFieldText) { nsDependentString fieldTextStr(mFieldText, mFieldTextLength); nsAutoString newFieldText = fieldTextStr + aText; PRUnichar* temp = mFieldText; mFieldText = ToNewUnicode(newFieldText); mFieldTextLength = newFieldText.Length(); nsMemory::Free(temp); } else { mFieldText = ToNewUnicode(aText); mFieldTextLength = aText.Length(); } } // XBL fields are represented on elements inheriting that field a bit trickily. // When setting up the XBL prototype object, we install accessors for the fields // on the prototype object. Those accessors, when used, will then (via // InstallXBLField below) reify a property for the field onto the actual XBL-backed // element. // // The accessor property is a plain old property backed by a getter function and // a setter function. These properties are backed by the FieldGetter and // FieldSetter natives; they're created by InstallAccessors. The precise field to be // reified is identified using two extra slots on the getter/setter functions. // XBLPROTO_SLOT stores the XBL prototype object that provides the field. // FIELD_SLOT stores the name of the field, i.e. its JavaScript property name. // // This two-step field installation process -- creating an accessor on the // prototype, then have that reify an own property on the actual element -- is // admittedly convoluted. Better would be for XBL-backed elements to be proxies // that could resolve fields onto themselves. But given that XBL bindings are // associated with elements mutably -- you can add/remove/change -moz-binding // whenever you want, alas -- doing so would require all elements to be proxies, // which isn't performant now. So we do this two-step instead. static const uint32_t XBLPROTO_SLOT = 0; static const uint32_t FIELD_SLOT = 1; bool ValueHasISupportsPrivate(const JS::Value &v) { if (!v.isObject()) { return false; } const DOMClass* domClass = GetDOMClass(&v.toObject()); if (domClass) { return domClass->mDOMObjectIsISupports; } JSClass* clasp = ::JS_GetClass(&v.toObject()); const uint32_t HAS_PRIVATE_NSISUPPORTS = JSCLASS_HAS_PRIVATE | JSCLASS_PRIVATE_IS_NSISUPPORTS; return (clasp->flags & HAS_PRIVATE_NSISUPPORTS) == HAS_PRIVATE_NSISUPPORTS; } // Define a shadowing property on |this| for the XBL field defined by the // contents of the callee's reserved slots. If the property was defined, // *installed will be true, and idp will be set to the property name that was // defined. static JSBool InstallXBLField(JSContext* cx, JS::Handle callee, JS::Handle thisObj, JS::MutableHandle idp, bool* installed) { *installed = false; // First ensure |this| is a reasonable XBL bound node. // // FieldAccessorGuard already determined whether |thisObj| was acceptable as // |this| in terms of not throwing a TypeError. Assert this for good measure. MOZ_ASSERT(ValueHasISupportsPrivate(JS::ObjectValue(*thisObj))); // But there are some cases where we must accept |thisObj| but not install a // property on it, or otherwise touch it. Hence this split of |this|-vetting // duties. nsISupports* native = nsContentUtils::XPConnect()->GetNativeOfWrapper(cx, thisObj); if (!native) { // Looks like whatever |thisObj| is it's not our nsIContent. It might well // be the proto our binding installed, however, where the private is the // nsXBLDocumentInfo, so just baul out quietly. Do NOT throw an exception // here. // // We could make this stricter by checking the class maybe, but whatever. return true; } nsCOMPtr xblNode = do_QueryInterface(native); if (!xblNode) { xpc::Throw(cx, NS_ERROR_UNEXPECTED); return false; } // Now that |this| is okay, actually install the field. // Because of the possibility (due to XBL binding inheritance, because each // XBL binding lives in its own global object) that |this| might be in a // different compartment from the callee (not to mention that this method can // be called with an arbitrary |this| regardless of how insane XBL is), and // because in this method we've entered |this|'s compartment (see in // Field[GS]etter where we attempt a cross-compartment call), we must enter // the callee's compartment to access its reserved slots. nsXBLPrototypeBinding* protoBinding; nsDependentJSString fieldName; { JSAutoCompartment ac(cx, callee); JS::Rooted xblProto(cx); xblProto = &js::GetFunctionNativeReserved(callee, XBLPROTO_SLOT).toObject(); JS::Rooted name(cx, js::GetFunctionNativeReserved(callee, FIELD_SLOT)); JSFlatString* fieldStr = JS_ASSERT_STRING_IS_FLAT(name.toString()); fieldName.init(fieldStr); MOZ_ALWAYS_TRUE(JS_ValueToId(cx, name, idp.address())); // If a separate XBL scope is being used, the callee is not same-compartment // with the xbl prototype, and the object is a cross-compartment wrapper. xblProto = js::UncheckedUnwrap(xblProto); JSAutoCompartment ac2(cx, xblProto); JS::Value slotVal = ::JS_GetReservedSlot(xblProto, 0); protoBinding = static_cast(slotVal.toPrivate()); MOZ_ASSERT(protoBinding); } nsXBLProtoImplField* field = protoBinding->FindField(fieldName); MOZ_ASSERT(field); // This mirrors code in nsXBLProtoImpl::InstallImplementation nsIScriptGlobalObject* global = xblNode->OwnerDoc()->GetScriptGlobalObject(); if (!global) { return true; } nsCOMPtr context = global->GetContext(); if (!context) { return true; } nsresult rv = field->InstallField(context, thisObj, protoBinding->DocURI(), installed); if (NS_SUCCEEDED(rv)) { return true; } if (!::JS_IsExceptionPending(cx)) { xpc::Throw(cx, rv); } return false; } bool FieldGetterImpl(JSContext *cx, JS::CallArgs args) { const JS::Value &thisv = args.thisv(); MOZ_ASSERT(ValueHasISupportsPrivate(thisv)); JS::Rooted thisObj(cx, &thisv.toObject()); // We should be in the compartment of |this|. If we got here via nativeCall, // |this| is not same-compartment with |callee|, and it's possible via // asymmetric security semantics that |args.calleev()| is actually a security // wrapper. In this case, we know we want to do an unsafe unwrap, and // InstallXBLField knows how to handle cross-compartment pointers. bool installed = false; JS::Rooted callee(cx, js::UncheckedUnwrap(&args.calleev().toObject())); JS::Rooted id(cx); if (!InstallXBLField(cx, callee, thisObj, &id, &installed)) { return false; } if (!installed) { args.rval().setUndefined(); return true; } JS::Rooted v(cx); if (!JS_GetPropertyById(cx, thisObj, id, v.address())) { return false; } args.rval().set(v); return true; } static JSBool FieldGetter(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); return JS::CallNonGenericMethod (cx, args); } bool FieldSetterImpl(JSContext *cx, JS::CallArgs args) { const JS::Value &thisv = args.thisv(); MOZ_ASSERT(ValueHasISupportsPrivate(thisv)); JS::Rooted thisObj(cx, &thisv.toObject()); // We should be in the compartment of |this|. If we got here via nativeCall, // |this| is not same-compartment with |callee|, and it's possible via // asymmetric security semantics that |args.calleev()| is actually a security // wrapper. In this case, we know we want to do an unsafe unwrap, and // InstallXBLField knows how to handle cross-compartment pointers. bool installed = false; JS::Rooted callee(cx, js::UncheckedUnwrap(&args.calleev().toObject())); JS::Rooted id(cx); if (!InstallXBLField(cx, callee, thisObj, &id, &installed)) { return false; } if (installed) { JS::Rooted v(cx, args.length() > 0 ? args[0] : JS::UndefinedValue()); if (!::JS_SetPropertyById(cx, thisObj, id, v.address())) { return false; } } args.rval().setUndefined(); return true; } static JSBool FieldSetter(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); return JS::CallNonGenericMethod (cx, args); } nsresult nsXBLProtoImplField::InstallAccessors(JSContext* aCx, JS::Handle aTargetClassObject) { MOZ_ASSERT(js::IsObjectInContextCompartment(aTargetClassObject, aCx)); JS::Rooted globalObject(aCx, JS_GetGlobalForObject(aCx, aTargetClassObject)); JS::Rooted scopeObject(aCx, xpc::GetXBLScope(aCx, globalObject)); NS_ENSURE_TRUE(scopeObject, NS_ERROR_OUT_OF_MEMORY); // Don't install it if the field is empty; see also InstallField which also must // implement the not-empty requirement. if (IsEmpty()) { return NS_OK; } // Install a getter/setter pair which will resolve the field onto the actual // object, when invoked. // Get the field name as an id. JS::Rooted id(aCx); JS::TwoByteChars chars(mName, NS_strlen(mName)); if (!JS_CharsToId(aCx, chars, id.address())) return NS_ERROR_OUT_OF_MEMORY; // Properties/Methods have historically taken precendence over fields. We // install members first, so just bounce here if the property is already // defined. JSBool found = false; if (!JS_AlreadyHasOwnPropertyById(aCx, aTargetClassObject, id, &found)) return NS_ERROR_FAILURE; if (found) return NS_OK; // FieldGetter and FieldSetter need to run in the XBL scope so that they can // see through any SOWs on their targets. // First, enter the XBL scope, and compile the functions there. JSAutoCompartment ac(aCx, scopeObject); JS::Rooted wrappedClassObj(aCx, JS::ObjectValue(*aTargetClassObject)); if (!JS_WrapValue(aCx, wrappedClassObj.address()) || !JS_WrapId(aCx, id.address())) return NS_ERROR_OUT_OF_MEMORY; JS::Rooted get(aCx, JS_GetFunctionObject(js::NewFunctionByIdWithReserved(aCx, FieldGetter, 0, 0, scopeObject, id))); if (!get) { return NS_ERROR_OUT_OF_MEMORY; } js::SetFunctionNativeReserved(get, XBLPROTO_SLOT, wrappedClassObj); js::SetFunctionNativeReserved(get, FIELD_SLOT, JS::StringValue(JSID_TO_STRING(id))); JS::Rooted set(aCx, JS_GetFunctionObject(js::NewFunctionByIdWithReserved(aCx, FieldSetter, 1, 0, scopeObject, id))); if (!set) { return NS_ERROR_OUT_OF_MEMORY; } js::SetFunctionNativeReserved(set, XBLPROTO_SLOT, wrappedClassObj); js::SetFunctionNativeReserved(set, FIELD_SLOT, JS::StringValue(JSID_TO_STRING(id))); // Now, re-enter the class object's scope, wrap the getters/setters, and define // them there. JSAutoCompartment ac2(aCx, aTargetClassObject); if (!JS_WrapObject(aCx, get.address()) || !JS_WrapObject(aCx, set.address()) || !JS_WrapId(aCx, id.address())) { return NS_ERROR_OUT_OF_MEMORY; } if (!::JS_DefinePropertyById(aCx, aTargetClassObject, id, JS::UndefinedValue(), JS_DATA_TO_FUNC_PTR(JSPropertyOp, get.get()), JS_DATA_TO_FUNC_PTR(JSStrictPropertyOp, set.get()), AccessorAttributes())) { return NS_ERROR_OUT_OF_MEMORY; } return NS_OK; } nsresult nsXBLProtoImplField::InstallField(nsIScriptContext* aContext, JS::Handle aBoundNode, nsIURI* aBindingDocURI, bool* aDidInstall) const { NS_PRECONDITION(aBoundNode, "uh-oh, bound node should NOT be null or bad things will " "happen"); *aDidInstall = false; // Empty fields are treated as not actually present. if (IsEmpty()) { return NS_OK; } nsAutoMicroTask mt; // EvaluateString and JS_DefineUCProperty can both trigger GC, so // protect |result| here. nsresult rv; nsAutoCString uriSpec; aBindingDocURI->GetSpec(uriSpec); AutoPushJSContext cx(aContext->GetNativeContext()); NS_ASSERTION(!::JS_IsExceptionPending(cx), "Shouldn't get here when an exception is pending!"); // compile the literal string nsCOMPtr context = aContext; JSAutoRequest ar(cx); // First, enter the xbl scope, wrap the node, and use that as the scope for // the evaluation. JS::Rooted scopeObject(cx, xpc::GetXBLScope(cx, aBoundNode)); NS_ENSURE_TRUE(scopeObject, NS_ERROR_OUT_OF_MEMORY); JSAutoCompartment ac(cx, scopeObject); JS::Rooted wrappedNode(cx, aBoundNode); if (!JS_WrapObject(cx, wrappedNode.address())) return NS_ERROR_OUT_OF_MEMORY; JS::Rooted result(cx); JS::CompileOptions options(cx); options.setFileAndLine(uriSpec.get(), mLineNumber) .setVersion(JSVERSION_LATEST) .setUserBit(true); // Flag us as XBL rv = context->EvaluateString(nsDependentString(mFieldText, mFieldTextLength), wrappedNode, options, /* aCoerceToString = */ false, result.address()); if (NS_FAILED(rv)) { return rv; } // Now, enter the node's compartment, wrap the eval result, and define it on // the bound node. JSAutoCompartment ac2(cx, aBoundNode); nsDependentString name(mName); if (!JS_WrapValue(cx, result.address()) || !::JS_DefineUCProperty(cx, aBoundNode, reinterpret_cast(mName), name.Length(), result, nullptr, nullptr, mJSAttributes)) { return NS_ERROR_OUT_OF_MEMORY; } *aDidInstall = true; return NS_OK; } nsresult nsXBLProtoImplField::Read(nsIScriptContext* aContext, nsIObjectInputStream* aStream) { nsAutoString name; nsresult rv = aStream->ReadString(name); NS_ENSURE_SUCCESS(rv, rv); mName = ToNewUnicode(name); rv = aStream->Read32(&mLineNumber); NS_ENSURE_SUCCESS(rv, rv); nsAutoString fieldText; rv = aStream->ReadString(fieldText); NS_ENSURE_SUCCESS(rv, rv); mFieldTextLength = fieldText.Length(); if (mFieldTextLength) mFieldText = ToNewUnicode(fieldText); return NS_OK; } nsresult nsXBLProtoImplField::Write(nsIScriptContext* aContext, nsIObjectOutputStream* aStream) { XBLBindingSerializeDetails type = XBLBinding_Serialize_Field; if (mJSAttributes & JSPROP_READONLY) { type |= XBLBinding_Serialize_ReadOnly; } nsresult rv = aStream->Write8(type); NS_ENSURE_SUCCESS(rv, rv); rv = aStream->WriteWStringZ(mName); NS_ENSURE_SUCCESS(rv, rv); rv = aStream->Write32(mLineNumber); NS_ENSURE_SUCCESS(rv, rv); return aStream->WriteWStringZ(mFieldText ? mFieldText : EmptyString().get()); }