diff -r 000000000000 -r 6474c204b198 dom/xbl/nsXBLProtoImplField.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/xbl/nsXBLProtoImplField.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,494 @@ +/* -*- 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 "js/CharacterEncoding.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 "mozilla/dom/ScriptSettings.h" +#include "nsGlobalWindow.h" +#include "xpcpublic.h" +#include "WrapperFactory.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsXBLProtoImplField::nsXBLProtoImplField(const char16_t* aName, const char16_t* 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; + char16_t* 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(JS::Handle v) +{ + if (!v.isObject()) { + return false; + } + + const DOMClass* domClass = GetDOMClass(&v.toObject()); + if (domClass) { + return domClass->mDOMObjectIsISupports; + } + + const 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; +} + +#ifdef DEBUG +static bool +ValueHasISupportsPrivate(JSContext* cx, const JS::Value& aVal) +{ + JS::Rooted v(cx, aVal); + return ValueHasISupportsPrivate(v); +} +#endif + +// 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 bool +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(cx, 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)); + + // 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); + + nsresult rv = field->InstallField(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) +{ + JS::Handle 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)) { + return false; + } + args.rval().set(v); + return true; +} + +static bool +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) +{ + JS::Handle 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) { + if (!::JS_SetPropertyById(cx, thisObj, id, args.get(0))) { + return false; + } + } + args.rval().setUndefined(); + return true; +} + +static bool +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::GetXBLScopeOrGlobal(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)) + 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. + bool 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) || !JS_WrapId(aCx, &id)) + 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) || !JS_WrapObject(aCx, &set) || + !JS_WrapId(aCx, &id)) + { + 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(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); + + nsIGlobalObject* globalObject = xpc::WindowGlobalOrNull(aBoundNode); + if (!globalObject) { + return NS_OK; + } + + // We are going to run script via EvaluateString, so we need a script entry + // point, but as this is XBL related it does not appear in the HTML spec. + AutoEntryScript entryScript(globalObject, true); + JSContext* cx = entryScript.cx(); + + NS_ASSERTION(!::JS_IsExceptionPending(cx), + "Shouldn't get here when an exception is pending!"); + + // First, enter the xbl scope, wrap the node, and use that as the scope for + // the evaluation. + JS::Rooted scopeObject(cx, xpc::GetXBLScopeOrGlobal(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)) + return NS_ERROR_OUT_OF_MEMORY; + + JS::Rooted result(cx); + JS::CompileOptions options(cx); + options.setFileAndLine(uriSpec.get(), mLineNumber) + .setVersion(JSVERSION_LATEST); + nsJSUtils::EvaluateOptions evalOptions; + rv = nsJSUtils::EvaluateString(cx, nsDependentString(mFieldText, + mFieldTextLength), + wrappedNode, options, evalOptions, + &result); + 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) || + !::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(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(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 : MOZ_UTF16("")); +}