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