Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 : /* This Source Code Form is subject to the terms of the Mozilla Public
4 : * License, v. 2.0. If a copy of the MPL was not distributed with this
5 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 :
7 : #include "nsIAtom.h"
8 : #include "nsIContent.h"
9 : #include "nsString.h"
10 : #include "nsJSUtils.h"
11 : #include "jsapi.h"
12 : #include "js/CharacterEncoding.h"
13 : #include "nsUnicharUtils.h"
14 : #include "nsReadableUtils.h"
15 : #include "nsXBLProtoImplField.h"
16 : #include "nsIScriptContext.h"
17 : #include "nsIURI.h"
18 : #include "nsXBLSerialize.h"
19 : #include "nsXBLPrototypeBinding.h"
20 : #include "mozilla/AddonPathService.h"
21 : #include "mozilla/dom/BindingUtils.h"
22 : #include "mozilla/dom/ElementBinding.h"
23 : #include "mozilla/dom/ScriptSettings.h"
24 : #include "nsGlobalWindow.h"
25 : #include "xpcpublic.h"
26 : #include "WrapperFactory.h"
27 :
28 : using namespace mozilla;
29 : using namespace mozilla::dom;
30 :
31 0 : nsXBLProtoImplField::nsXBLProtoImplField(const char16_t* aName, const char16_t* aReadOnly)
32 : : mNext(nullptr),
33 : mFieldText(nullptr),
34 : mFieldTextLength(0),
35 0 : mLineNumber(0)
36 : {
37 0 : MOZ_COUNT_CTOR(nsXBLProtoImplField);
38 0 : mName = NS_strdup(aName); // XXXbz make more sense to use a stringbuffer?
39 :
40 0 : mJSAttributes = JSPROP_ENUMERATE;
41 0 : if (aReadOnly) {
42 0 : nsAutoString readOnly; readOnly.Assign(aReadOnly);
43 0 : if (readOnly.LowerCaseEqualsLiteral("true"))
44 0 : mJSAttributes |= JSPROP_READONLY;
45 : }
46 0 : }
47 :
48 :
49 331 : nsXBLProtoImplField::nsXBLProtoImplField(const bool aIsReadOnly)
50 : : mNext(nullptr),
51 : mFieldText(nullptr),
52 : mFieldTextLength(0),
53 331 : mLineNumber(0)
54 : {
55 331 : MOZ_COUNT_CTOR(nsXBLProtoImplField);
56 :
57 331 : mJSAttributes = JSPROP_ENUMERATE;
58 331 : if (aIsReadOnly)
59 56 : mJSAttributes |= JSPROP_READONLY;
60 331 : }
61 :
62 0 : nsXBLProtoImplField::~nsXBLProtoImplField()
63 : {
64 0 : MOZ_COUNT_DTOR(nsXBLProtoImplField);
65 0 : if (mFieldText)
66 0 : free(mFieldText);
67 0 : free(mName);
68 0 : NS_CONTENT_DELETE_LIST_MEMBER(nsXBLProtoImplField, this, mNext);
69 0 : }
70 :
71 : void
72 0 : nsXBLProtoImplField::AppendFieldText(const nsAString& aText)
73 : {
74 0 : if (mFieldText) {
75 0 : nsDependentString fieldTextStr(mFieldText, mFieldTextLength);
76 0 : nsAutoString newFieldText = fieldTextStr + aText;
77 0 : char16_t* temp = mFieldText;
78 0 : mFieldText = ToNewUnicode(newFieldText);
79 0 : mFieldTextLength = newFieldText.Length();
80 0 : free(temp);
81 : }
82 : else {
83 0 : mFieldText = ToNewUnicode(aText);
84 0 : mFieldTextLength = aText.Length();
85 : }
86 0 : }
87 :
88 : // XBL fields are represented on elements inheriting that field a bit trickily.
89 : // When setting up the XBL prototype object, we install accessors for the fields
90 : // on the prototype object. Those accessors, when used, will then (via
91 : // InstallXBLField below) reify a property for the field onto the actual XBL-backed
92 : // element.
93 : //
94 : // The accessor property is a plain old property backed by a getter function and
95 : // a setter function. These properties are backed by the FieldGetter and
96 : // FieldSetter natives; they're created by InstallAccessors. The precise field to be
97 : // reified is identified using two extra slots on the getter/setter functions.
98 : // XBLPROTO_SLOT stores the XBL prototype object that provides the field.
99 : // FIELD_SLOT stores the name of the field, i.e. its JavaScript property name.
100 : //
101 : // This two-step field installation process -- creating an accessor on the
102 : // prototype, then have that reify an own property on the actual element -- is
103 : // admittedly convoluted. Better would be for XBL-backed elements to be proxies
104 : // that could resolve fields onto themselves. But given that XBL bindings are
105 : // associated with elements mutably -- you can add/remove/change -moz-binding
106 : // whenever you want, alas -- doing so would require all elements to be proxies,
107 : // which isn't performant now. So we do this two-step instead.
108 : static const uint32_t XBLPROTO_SLOT = 0;
109 : static const uint32_t FIELD_SLOT = 1;
110 :
111 : bool
112 336 : ValueHasISupportsPrivate(JS::Handle<JS::Value> v)
113 : {
114 336 : if (!v.isObject()) {
115 0 : return false;
116 : }
117 :
118 336 : const DOMJSClass* domClass = GetDOMClass(&v.toObject());
119 336 : if (domClass) {
120 336 : return domClass->mDOMObjectIsISupports;
121 : }
122 :
123 0 : const JSClass* clasp = ::JS_GetClass(&v.toObject());
124 : const uint32_t HAS_PRIVATE_NSISUPPORTS =
125 0 : JSCLASS_HAS_PRIVATE | JSCLASS_PRIVATE_IS_NSISUPPORTS;
126 0 : return (clasp->flags & HAS_PRIVATE_NSISUPPORTS) == HAS_PRIVATE_NSISUPPORTS;
127 : }
128 :
129 : #ifdef DEBUG
130 : static bool
131 112 : ValueHasISupportsPrivate(JSContext* cx, const JS::Value& aVal)
132 : {
133 224 : JS::Rooted<JS::Value> v(cx, aVal);
134 224 : return ValueHasISupportsPrivate(v);
135 : }
136 : #endif
137 :
138 : // Define a shadowing property on |this| for the XBL field defined by the
139 : // contents of the callee's reserved slots. If the property was defined,
140 : // *installed will be true, and idp will be set to the property name that was
141 : // defined.
142 : static bool
143 112 : InstallXBLField(JSContext* cx,
144 : JS::Handle<JSObject*> callee, JS::Handle<JSObject*> thisObj,
145 : JS::MutableHandle<jsid> idp, bool* installed)
146 : {
147 112 : *installed = false;
148 :
149 : // First ensure |this| is a reasonable XBL bound node.
150 : //
151 : // FieldAccessorGuard already determined whether |thisObj| was acceptable as
152 : // |this| in terms of not throwing a TypeError. Assert this for good measure.
153 112 : MOZ_ASSERT(ValueHasISupportsPrivate(cx, JS::ObjectValue(*thisObj)));
154 :
155 : // But there are some cases where we must accept |thisObj| but not install a
156 : // property on it, or otherwise touch it. Hence this split of |this|-vetting
157 : // duties.
158 224 : nsCOMPtr<nsISupports> native = xpc::UnwrapReflectorToISupports(thisObj);
159 112 : if (!native) {
160 : // Looks like whatever |thisObj| is it's not our nsIContent. It might well
161 : // be the proto our binding installed, however, where the private is the
162 : // nsXBLDocumentInfo, so just baul out quietly. Do NOT throw an exception
163 : // here.
164 : //
165 : // We could make this stricter by checking the class maybe, but whatever.
166 0 : return true;
167 : }
168 :
169 224 : nsCOMPtr<nsIContent> xblNode = do_QueryInterface(native);
170 112 : if (!xblNode) {
171 0 : xpc::Throw(cx, NS_ERROR_UNEXPECTED);
172 0 : return false;
173 : }
174 :
175 : // Now that |this| is okay, actually install the field.
176 :
177 : // Because of the possibility (due to XBL binding inheritance, because each
178 : // XBL binding lives in its own global object) that |this| might be in a
179 : // different compartment from the callee (not to mention that this method can
180 : // be called with an arbitrary |this| regardless of how insane XBL is), and
181 : // because in this method we've entered |this|'s compartment (see in
182 : // Field[GS]etter where we attempt a cross-compartment call), we must enter
183 : // the callee's compartment to access its reserved slots.
184 : nsXBLPrototypeBinding* protoBinding;
185 224 : nsAutoJSString fieldName;
186 : {
187 224 : JSAutoCompartment ac(cx, callee);
188 :
189 224 : JS::Rooted<JSObject*> xblProto(cx);
190 112 : xblProto = &js::GetFunctionNativeReserved(callee, XBLPROTO_SLOT).toObject();
191 :
192 224 : JS::Rooted<JS::Value> name(cx, js::GetFunctionNativeReserved(callee, FIELD_SLOT));
193 112 : if (!fieldName.init(cx, name.toString())) {
194 0 : return false;
195 : }
196 :
197 112 : MOZ_ALWAYS_TRUE(JS_ValueToId(cx, name, idp));
198 :
199 : // If a separate XBL scope is being used, the callee is not same-compartment
200 : // with the xbl prototype, and the object is a cross-compartment wrapper.
201 112 : xblProto = js::UncheckedUnwrap(xblProto);
202 224 : JSAutoCompartment ac2(cx, xblProto);
203 112 : JS::Value slotVal = ::JS_GetReservedSlot(xblProto, 0);
204 112 : protoBinding = static_cast<nsXBLPrototypeBinding*>(slotVal.toPrivate());
205 112 : MOZ_ASSERT(protoBinding);
206 : }
207 :
208 112 : nsXBLProtoImplField* field = protoBinding->FindField(fieldName);
209 112 : MOZ_ASSERT(field);
210 :
211 112 : nsresult rv = field->InstallField(thisObj, protoBinding->DocURI(), installed);
212 112 : if (NS_SUCCEEDED(rv)) {
213 112 : return true;
214 : }
215 :
216 0 : if (!::JS_IsExceptionPending(cx)) {
217 0 : xpc::Throw(cx, rv);
218 : }
219 0 : return false;
220 : }
221 :
222 : bool
223 68 : FieldGetterImpl(JSContext *cx, const JS::CallArgs& args)
224 : {
225 68 : JS::Handle<JS::Value> thisv = args.thisv();
226 68 : MOZ_ASSERT(ValueHasISupportsPrivate(thisv));
227 :
228 136 : JS::Rooted<JSObject*> thisObj(cx, &thisv.toObject());
229 :
230 : // We should be in the compartment of |this|. If we got here via nativeCall,
231 : // |this| is not same-compartment with |callee|, and it's possible via
232 : // asymmetric security semantics that |args.calleev()| is actually a security
233 : // wrapper. In this case, we know we want to do an unsafe unwrap, and
234 : // InstallXBLField knows how to handle cross-compartment pointers.
235 68 : bool installed = false;
236 136 : JS::Rooted<JSObject*> callee(cx, js::UncheckedUnwrap(&args.calleev().toObject()));
237 136 : JS::Rooted<jsid> id(cx);
238 68 : if (!InstallXBLField(cx, callee, thisObj, &id, &installed)) {
239 0 : return false;
240 : }
241 :
242 68 : if (!installed) {
243 0 : args.rval().setUndefined();
244 0 : return true;
245 : }
246 :
247 68 : return JS_GetPropertyById(cx, thisObj, id, args.rval());
248 : }
249 :
250 : static bool
251 68 : FieldGetter(JSContext *cx, unsigned argc, JS::Value *vp)
252 : {
253 68 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
254 : return JS::CallNonGenericMethod<ValueHasISupportsPrivate, FieldGetterImpl>
255 68 : (cx, args);
256 : }
257 :
258 : bool
259 44 : FieldSetterImpl(JSContext *cx, const JS::CallArgs& args)
260 : {
261 44 : JS::Handle<JS::Value> thisv = args.thisv();
262 44 : MOZ_ASSERT(ValueHasISupportsPrivate(thisv));
263 :
264 88 : JS::Rooted<JSObject*> thisObj(cx, &thisv.toObject());
265 :
266 : // We should be in the compartment of |this|. If we got here via nativeCall,
267 : // |this| is not same-compartment with |callee|, and it's possible via
268 : // asymmetric security semantics that |args.calleev()| is actually a security
269 : // wrapper. In this case, we know we want to do an unsafe unwrap, and
270 : // InstallXBLField knows how to handle cross-compartment pointers.
271 44 : bool installed = false;
272 88 : JS::Rooted<JSObject*> callee(cx, js::UncheckedUnwrap(&args.calleev().toObject()));
273 88 : JS::Rooted<jsid> id(cx);
274 44 : if (!InstallXBLField(cx, callee, thisObj, &id, &installed)) {
275 0 : return false;
276 : }
277 :
278 44 : if (installed) {
279 44 : if (!::JS_SetPropertyById(cx, thisObj, id, args.get(0))) {
280 0 : return false;
281 : }
282 : }
283 44 : args.rval().setUndefined();
284 44 : return true;
285 : }
286 :
287 : static bool
288 44 : FieldSetter(JSContext *cx, unsigned argc, JS::Value *vp)
289 : {
290 44 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
291 : return JS::CallNonGenericMethod<ValueHasISupportsPrivate, FieldSetterImpl>
292 44 : (cx, args);
293 : }
294 :
295 : nsresult
296 282 : nsXBLProtoImplField::InstallAccessors(JSContext* aCx,
297 : JS::Handle<JSObject*> aTargetClassObject)
298 : {
299 282 : MOZ_ASSERT(js::IsObjectInContextCompartment(aTargetClassObject, aCx));
300 564 : JS::Rooted<JSObject*> globalObject(aCx, JS_GetGlobalForObject(aCx, aTargetClassObject));
301 564 : JS::Rooted<JSObject*> scopeObject(aCx, xpc::GetXBLScopeOrGlobal(aCx, globalObject));
302 282 : NS_ENSURE_TRUE(scopeObject, NS_ERROR_OUT_OF_MEMORY);
303 :
304 : // Don't install it if the field is empty; see also InstallField which also must
305 : // implement the not-empty requirement.
306 282 : if (IsEmpty()) {
307 2 : return NS_OK;
308 : }
309 :
310 : // Install a getter/setter pair which will resolve the field onto the actual
311 : // object, when invoked.
312 :
313 : // Get the field name as an id.
314 560 : JS::Rooted<jsid> id(aCx);
315 280 : JS::TwoByteChars chars(mName, NS_strlen(mName));
316 280 : if (!JS_CharsToId(aCx, chars, &id))
317 0 : return NS_ERROR_OUT_OF_MEMORY;
318 :
319 : // Properties/Methods have historically taken precendence over fields. We
320 : // install members first, so just bounce here if the property is already
321 : // defined.
322 280 : bool found = false;
323 280 : if (!JS_AlreadyHasOwnPropertyById(aCx, aTargetClassObject, id, &found))
324 0 : return NS_ERROR_FAILURE;
325 280 : if (found)
326 1 : return NS_OK;
327 :
328 : // FieldGetter and FieldSetter need to run in the XBL scope so that they can
329 : // see through any SOWs on their targets.
330 :
331 : // First, enter the XBL scope, and compile the functions there.
332 558 : JSAutoCompartment ac(aCx, scopeObject);
333 558 : JS::Rooted<JS::Value> wrappedClassObj(aCx, JS::ObjectValue(*aTargetClassObject));
334 279 : if (!JS_WrapValue(aCx, &wrappedClassObj))
335 0 : return NS_ERROR_OUT_OF_MEMORY;
336 :
337 : JS::Rooted<JSObject*> get(aCx,
338 558 : JS_GetFunctionObject(js::NewFunctionByIdWithReserved(aCx, FieldGetter,
339 837 : 0, 0, id)));
340 279 : if (!get) {
341 0 : return NS_ERROR_OUT_OF_MEMORY;
342 : }
343 279 : js::SetFunctionNativeReserved(get, XBLPROTO_SLOT, wrappedClassObj);
344 279 : js::SetFunctionNativeReserved(get, FIELD_SLOT,
345 558 : JS::StringValue(JSID_TO_STRING(id)));
346 :
347 : JS::Rooted<JSObject*> set(aCx,
348 558 : JS_GetFunctionObject(js::NewFunctionByIdWithReserved(aCx, FieldSetter,
349 837 : 1, 0, id)));
350 279 : if (!set) {
351 0 : return NS_ERROR_OUT_OF_MEMORY;
352 : }
353 279 : js::SetFunctionNativeReserved(set, XBLPROTO_SLOT, wrappedClassObj);
354 279 : js::SetFunctionNativeReserved(set, FIELD_SLOT,
355 558 : JS::StringValue(JSID_TO_STRING(id)));
356 :
357 : // Now, re-enter the class object's scope, wrap the getters/setters, and define
358 : // them there.
359 558 : JSAutoCompartment ac2(aCx, aTargetClassObject);
360 279 : if (!JS_WrapObject(aCx, &get) || !JS_WrapObject(aCx, &set)) {
361 0 : return NS_ERROR_OUT_OF_MEMORY;
362 : }
363 :
364 558 : if (!::JS_DefinePropertyById(aCx, aTargetClassObject, id, JS::UndefinedHandleValue,
365 : AccessorAttributes(),
366 279 : JS_DATA_TO_FUNC_PTR(JSNative, get.get()),
367 279 : JS_DATA_TO_FUNC_PTR(JSNative, set.get()))) {
368 0 : return NS_ERROR_OUT_OF_MEMORY;
369 : }
370 :
371 279 : return NS_OK;
372 : }
373 :
374 : nsresult
375 112 : nsXBLProtoImplField::InstallField(JS::Handle<JSObject*> aBoundNode,
376 : nsIURI* aBindingDocURI,
377 : bool* aDidInstall) const
378 : {
379 112 : NS_PRECONDITION(aBoundNode,
380 : "uh-oh, bound node should NOT be null or bad things will "
381 : "happen");
382 :
383 112 : *aDidInstall = false;
384 :
385 : // Empty fields are treated as not actually present.
386 112 : if (IsEmpty()) {
387 0 : return NS_OK;
388 : }
389 :
390 224 : nsAutoMicroTask mt;
391 :
392 224 : nsAutoCString uriSpec;
393 112 : nsresult rv = aBindingDocURI->GetSpec(uriSpec);
394 112 : if (NS_WARN_IF(NS_FAILED(rv))) {
395 0 : return rv;
396 : }
397 :
398 112 : nsIGlobalObject* globalObject = xpc::WindowGlobalOrNull(aBoundNode);
399 112 : if (!globalObject) {
400 0 : return NS_OK;
401 : }
402 :
403 : // We are going to run script via EvaluateString, so we need a script entry
404 : // point, but as this is XBL related it does not appear in the HTML spec.
405 : // We need an actual JSContext to do GetScopeForXBLExecution, and it needs to
406 : // be in the compartment of globalObject. But we want our XBL execution scope
407 : // to be our entry global.
408 224 : AutoJSAPI jsapi;
409 112 : if (!jsapi.Init(globalObject)) {
410 0 : return NS_ERROR_UNEXPECTED;
411 : }
412 112 : MOZ_ASSERT(!::JS_IsExceptionPending(jsapi.cx()),
413 : "Shouldn't get here when an exception is pending!");
414 :
415 112 : JSAddonId* addonId = MapURIToAddonID(aBindingDocURI);
416 :
417 : // Note: the UNWRAP_OBJECT may mutate boundNode; don't use it after that call.
418 224 : JS::Rooted<JSObject*> boundNode(jsapi.cx(), aBoundNode);
419 112 : Element* boundElement = nullptr;
420 112 : rv = UNWRAP_OBJECT(Element, &boundNode, boundElement);
421 112 : if (NS_WARN_IF(NS_FAILED(rv))) {
422 0 : return rv;
423 : }
424 :
425 : // First, enter the xbl scope, build the element's scope chain, and use
426 : // that as the scope chain for the evaluation.
427 224 : JS::Rooted<JSObject*> scopeObject(jsapi.cx(),
428 448 : xpc::GetScopeForXBLExecution(jsapi.cx(), aBoundNode, addonId));
429 112 : NS_ENSURE_TRUE(scopeObject, NS_ERROR_OUT_OF_MEMORY);
430 :
431 224 : AutoEntryScript aes(scopeObject, "XBL <field> initialization", true);
432 112 : JSContext* cx = aes.cx();
433 :
434 224 : JS::Rooted<JS::Value> result(cx);
435 224 : JS::CompileOptions options(cx);
436 112 : options.setFileAndLine(uriSpec.get(), mLineNumber)
437 112 : .setVersion(JSVERSION_LATEST);
438 224 : JS::AutoObjectVector scopeChain(cx);
439 112 : if (!nsJSUtils::GetScopeChainForElement(cx, boundElement, scopeChain)) {
440 0 : return NS_ERROR_OUT_OF_MEMORY;
441 : }
442 112 : rv = NS_OK;
443 : {
444 224 : nsJSUtils::ExecutionContext exec(cx, scopeObject);
445 112 : exec.SetScopeChain(scopeChain);
446 224 : exec.CompileAndExec(options, nsDependentString(mFieldText,
447 224 : mFieldTextLength));
448 112 : rv = exec.ExtractReturnValue(&result);
449 : }
450 :
451 112 : if (NS_FAILED(rv)) {
452 0 : return rv;
453 : }
454 :
455 112 : if (rv == NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW) {
456 : // Report the exception now, before we try using the JSContext for
457 : // the JS_DefineUCProperty call. Note that this reports in our current
458 : // compartment, which is the XBL scope.
459 0 : aes.ReportException();
460 : }
461 :
462 : // Now, enter the node's compartment, wrap the eval result, and define it on
463 : // the bound node.
464 224 : JSAutoCompartment ac2(cx, aBoundNode);
465 224 : nsDependentString name(mName);
466 448 : if (!JS_WrapValue(cx, &result) ||
467 448 : !::JS_DefineUCProperty(cx, aBoundNode,
468 112 : reinterpret_cast<const char16_t*>(mName),
469 224 : name.Length(), result, mJSAttributes)) {
470 0 : return NS_ERROR_OUT_OF_MEMORY;
471 : }
472 :
473 112 : *aDidInstall = true;
474 112 : return NS_OK;
475 : }
476 :
477 : nsresult
478 331 : nsXBLProtoImplField::Read(nsIObjectInputStream* aStream)
479 : {
480 662 : nsAutoString name;
481 331 : nsresult rv = aStream->ReadString(name);
482 331 : NS_ENSURE_SUCCESS(rv, rv);
483 331 : mName = ToNewUnicode(name);
484 :
485 331 : rv = aStream->Read32(&mLineNumber);
486 331 : NS_ENSURE_SUCCESS(rv, rv);
487 :
488 662 : nsAutoString fieldText;
489 331 : rv = aStream->ReadString(fieldText);
490 331 : NS_ENSURE_SUCCESS(rv, rv);
491 331 : mFieldTextLength = fieldText.Length();
492 331 : if (mFieldTextLength)
493 328 : mFieldText = ToNewUnicode(fieldText);
494 :
495 331 : return NS_OK;
496 : }
497 :
498 : nsresult
499 0 : nsXBLProtoImplField::Write(nsIObjectOutputStream* aStream)
500 : {
501 0 : XBLBindingSerializeDetails type = XBLBinding_Serialize_Field;
502 :
503 0 : if (mJSAttributes & JSPROP_READONLY) {
504 0 : type |= XBLBinding_Serialize_ReadOnly;
505 : }
506 :
507 0 : nsresult rv = aStream->Write8(type);
508 0 : NS_ENSURE_SUCCESS(rv, rv);
509 0 : rv = aStream->WriteWStringZ(mName);
510 0 : NS_ENSURE_SUCCESS(rv, rv);
511 0 : rv = aStream->Write32(mLineNumber);
512 0 : NS_ENSURE_SUCCESS(rv, rv);
513 :
514 0 : return aStream->WriteWStringZ(mFieldText ? mFieldText : u"");
515 : }
|