Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/dom/xbl/nsXBLProtoImplField.cpp
Line
Count
Source (jump to first uncovered line)
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 "nsAtom.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/dom/BindingUtils.h"
21
#include "mozilla/dom/ElementBinding.h"
22
#include "mozilla/dom/ScriptSettings.h"
23
#include "nsGlobalWindow.h"
24
#include "xpcpublic.h"
25
#include "WrapperFactory.h"
26
27
using namespace mozilla;
28
using namespace mozilla::dom;
29
30
nsXBLProtoImplField::nsXBLProtoImplField(const char16_t* aName, const char16_t* aReadOnly)
31
  : mNext(nullptr),
32
    mFieldText(nullptr),
33
    mFieldTextLength(0),
34
    mLineNumber(0)
35
0
{
36
0
  MOZ_COUNT_CTOR(nsXBLProtoImplField);
37
0
  mName = NS_xstrdup(aName);  // XXXbz make more sense to use a stringbuffer?
38
0
39
0
  mJSAttributes = JSPROP_ENUMERATE;
40
0
  if (aReadOnly) {
41
0
    nsAutoString readOnly; readOnly.Assign(aReadOnly);
42
0
    if (readOnly.LowerCaseEqualsLiteral("true"))
43
0
      mJSAttributes |= JSPROP_READONLY;
44
0
  }
45
0
}
46
47
48
nsXBLProtoImplField::nsXBLProtoImplField(const bool aIsReadOnly)
49
  : mNext(nullptr),
50
    mName(nullptr),
51
    mFieldText(nullptr),
52
    mFieldTextLength(0),
53
    mLineNumber(0)
54
0
{
55
0
  MOZ_COUNT_CTOR(nsXBLProtoImplField);
56
0
57
0
  mJSAttributes = JSPROP_ENUMERATE;
58
0
  if (aIsReadOnly)
59
0
    mJSAttributes |= JSPROP_READONLY;
60
0
}
61
62
nsXBLProtoImplField::~nsXBLProtoImplField()
63
0
{
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
nsXBLProtoImplField::AppendFieldText(const nsAString& aText)
73
0
{
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
0
  }
82
0
  else {
83
0
    mFieldText = ToNewUnicode(aText);
84
0
    mFieldTextLength = aText.Length();
85
0
  }
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
ValueHasISupportsPrivate(JS::Handle<JS::Value> v)
113
0
{
114
0
  if (!v.isObject()) {
115
0
    return false;
116
0
  }
117
0
118
0
  const DOMJSClass* domClass = GetDOMClass(&v.toObject());
119
0
  if (domClass) {
120
0
    return domClass->mDOMObjectIsISupports;
121
0
  }
122
0
123
0
  const JSClass* clasp = ::JS_GetClass(&v.toObject());
124
0
  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
0
}
128
129
#ifdef DEBUG
130
static bool
131
ValueHasISupportsPrivate(JSContext* cx, const JS::Value& aVal)
132
{
133
    JS::Rooted<JS::Value> v(cx, aVal);
134
    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
InstallXBLField(JSContext* cx,
144
                JS::Handle<JSObject*> callee, JS::Handle<JSObject*> thisObj,
145
                JS::MutableHandle<jsid> idp, bool* installed)
146
0
{
147
0
  *installed = false;
148
0
149
0
  // First ensure |this| is a reasonable XBL bound node.
150
0
  //
151
0
  // FieldAccessorGuard already determined whether |thisObj| was acceptable as
152
0
  // |this| in terms of not throwing a TypeError.  Assert this for good measure.
153
0
  MOZ_ASSERT(ValueHasISupportsPrivate(cx, JS::ObjectValue(*thisObj)));
154
0
155
0
  // But there are some cases where we must accept |thisObj| but not install a
156
0
  // property on it, or otherwise touch it.  Hence this split of |this|-vetting
157
0
  // duties.
158
0
  nsCOMPtr<nsISupports> native = xpc::UnwrapReflectorToISupports(thisObj);
159
0
  if (!native) {
160
0
    // Looks like whatever |thisObj| is it's not our nsIContent.  It might well
161
0
    // be the proto our binding installed, however, where the private is the
162
0
    // nsXBLDocumentInfo, so just baul out quietly.  Do NOT throw an exception
163
0
    // here.
164
0
    //
165
0
    // We could make this stricter by checking the class maybe, but whatever.
166
0
    return true;
167
0
  }
168
0
169
0
  nsCOMPtr<nsIContent> xblNode = do_QueryInterface(native);
170
0
  if (!xblNode) {
171
0
    xpc::Throw(cx, NS_ERROR_UNEXPECTED);
172
0
    return false;
173
0
  }
174
0
175
0
  // Now that |this| is okay, actually install the field.
176
0
177
0
  // Because of the possibility (due to XBL binding inheritance, because each
178
0
  // XBL binding lives in its own global object) that |this| might be in a
179
0
  // different realm from the callee (not to mention that this method can
180
0
  // be called with an arbitrary |this| regardless of how insane XBL is), and
181
0
  // because in this method we've entered |this|'s realm (see in
182
0
  // Field[GS]etter where we attempt a cross-realm call), we must enter
183
0
  // the callee's realm to access its reserved slots.
184
0
  nsXBLPrototypeBinding* protoBinding;
185
0
  nsAutoJSString fieldName;
186
0
  {
187
0
    JSAutoRealm ar(cx, callee);
188
0
189
0
    JS::Rooted<JSObject*> xblProto(cx);
190
0
    xblProto = &js::GetFunctionNativeReserved(callee, XBLPROTO_SLOT).toObject();
191
0
192
0
    JS::Rooted<JS::Value> name(cx, js::GetFunctionNativeReserved(callee, FIELD_SLOT));
193
0
    if (!fieldName.init(cx, name.toString())) {
194
0
      return false;
195
0
    }
196
0
197
0
    MOZ_ALWAYS_TRUE(JS_ValueToId(cx, name, idp));
198
0
199
0
    // If a separate XBL scope is being used, the callee is not same-realm
200
0
    // with the xbl prototype, and the object is a cross-compartment wrapper.
201
0
    xblProto = js::UncheckedUnwrap(xblProto);
202
0
    JSAutoRealm ar2(cx, xblProto);
203
0
    JS::Value slotVal = ::JS_GetReservedSlot(xblProto, 0);
204
0
    protoBinding = static_cast<nsXBLPrototypeBinding*>(slotVal.toPrivate());
205
0
    MOZ_ASSERT(protoBinding);
206
0
  }
207
0
208
0
  nsXBLProtoImplField* field = protoBinding->FindField(fieldName);
209
0
  MOZ_ASSERT(field);
210
0
211
0
  nsresult rv = field->InstallField(thisObj, *protoBinding, installed);
212
0
  if (NS_SUCCEEDED(rv)) {
213
0
    return true;
214
0
  }
215
0
216
0
  if (!::JS_IsExceptionPending(cx)) {
217
0
    xpc::Throw(cx, rv);
218
0
  }
219
0
  return false;
220
0
}
221
222
bool
223
FieldGetterImpl(JSContext *cx, const JS::CallArgs& args)
224
0
{
225
0
  JS::Handle<JS::Value> thisv = args.thisv();
226
0
  MOZ_ASSERT(ValueHasISupportsPrivate(thisv));
227
0
228
0
  JS::Rooted<JSObject*> thisObj(cx, &thisv.toObject());
229
0
230
0
  // We should be in the realm of |this|. If we got here via nativeCall,
231
0
  // |this| is not same-compartment with |callee|, and it's possible via
232
0
  // asymmetric security semantics that |args.calleev()| is actually a security
233
0
  // wrapper. In this case, we know we want to do an unsafe unwrap, and
234
0
  // InstallXBLField knows how to handle cross-compartment pointers.
235
0
  bool installed = false;
236
0
  JS::Rooted<JSObject*> callee(cx, js::UncheckedUnwrap(&args.calleev().toObject()));
237
0
  JS::Rooted<jsid> id(cx);
238
0
  if (!InstallXBLField(cx, callee, thisObj, &id, &installed)) {
239
0
    return false;
240
0
  }
241
0
242
0
  if (!installed) {
243
0
    args.rval().setUndefined();
244
0
    return true;
245
0
  }
246
0
247
0
  return JS_GetPropertyById(cx, thisObj, id, args.rval());
248
0
}
249
250
static bool
251
FieldGetter(JSContext *cx, unsigned argc, JS::Value *vp)
252
0
{
253
0
  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
254
0
  return JS::CallNonGenericMethod<ValueHasISupportsPrivate, FieldGetterImpl>
255
0
                                 (cx, args);
256
0
}
257
258
bool
259
FieldSetterImpl(JSContext *cx, const JS::CallArgs& args)
260
0
{
261
0
  JS::Handle<JS::Value> thisv = args.thisv();
262
0
  MOZ_ASSERT(ValueHasISupportsPrivate(thisv));
263
0
264
0
  JS::Rooted<JSObject*> thisObj(cx, &thisv.toObject());
265
0
266
0
  // We should be in the realm of |this|. If we got here via nativeCall,
267
0
  // |this| is not same-compartment with |callee|, and it's possible via
268
0
  // asymmetric security semantics that |args.calleev()| is actually a security
269
0
  // wrapper. In this case, we know we want to do an unsafe unwrap, and
270
0
  // InstallXBLField knows how to handle cross-compartment pointers.
271
0
  bool installed = false;
272
0
  JS::Rooted<JSObject*> callee(cx, js::UncheckedUnwrap(&args.calleev().toObject()));
273
0
  JS::Rooted<jsid> id(cx);
274
0
  if (!InstallXBLField(cx, callee, thisObj, &id, &installed)) {
275
0
    return false;
276
0
  }
277
0
278
0
  if (installed) {
279
0
    if (!::JS_SetPropertyById(cx, thisObj, id, args.get(0))) {
280
0
      return false;
281
0
    }
282
0
  }
283
0
  args.rval().setUndefined();
284
0
  return true;
285
0
}
286
287
static bool
288
FieldSetter(JSContext *cx, unsigned argc, JS::Value *vp)
289
0
{
290
0
  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
291
0
  return JS::CallNonGenericMethod<ValueHasISupportsPrivate, FieldSetterImpl>
292
0
                                 (cx, args);
293
0
}
294
295
nsresult
296
nsXBLProtoImplField::InstallAccessors(JSContext* aCx,
297
                                      JS::Handle<JSObject*> aTargetClassObject)
298
0
{
299
0
  MOZ_ASSERT(js::IsObjectInContextCompartment(aTargetClassObject, aCx));
300
0
  JS::Rooted<JSObject*> globalObject(aCx, JS::GetNonCCWObjectGlobal(aTargetClassObject));
301
0
  JS::Rooted<JSObject*> scopeObject(aCx, xpc::GetXBLScopeOrGlobal(aCx, globalObject));
302
0
  NS_ENSURE_TRUE(scopeObject, NS_ERROR_OUT_OF_MEMORY);
303
0
304
0
  // Don't install it if the field is empty; see also InstallField which also must
305
0
  // implement the not-empty requirement.
306
0
  if (IsEmpty()) {
307
0
    return NS_OK;
308
0
  }
309
0
310
0
  // Install a getter/setter pair which will resolve the field onto the actual
311
0
  // object, when invoked.
312
0
313
0
  // Get the field name as an id.
314
0
  JS::Rooted<jsid> id(aCx);
315
0
  JS::TwoByteChars chars(mName, NS_strlen(mName));
316
0
  if (!JS_CharsToId(aCx, chars, &id))
317
0
    return NS_ERROR_OUT_OF_MEMORY;
318
0
319
0
  // Properties/Methods have historically taken precendence over fields. We
320
0
  // install members first, so just bounce here if the property is already
321
0
  // defined.
322
0
  bool found = false;
323
0
  if (!JS_AlreadyHasOwnPropertyById(aCx, aTargetClassObject, id, &found))
324
0
    return NS_ERROR_FAILURE;
325
0
  if (found)
326
0
    return NS_OK;
327
0
328
0
  // FieldGetter and FieldSetter need to run in the XBL scope so that they can
329
0
  // see through any SOWs on their targets.
330
0
331
0
  // First, enter the XBL scope, and compile the functions there.
332
0
  JSAutoRealm ar(aCx, scopeObject);
333
0
  JS::Rooted<JS::Value> wrappedClassObj(aCx, JS::ObjectValue(*aTargetClassObject));
334
0
  if (!JS_WrapValue(aCx, &wrappedClassObj))
335
0
    return NS_ERROR_OUT_OF_MEMORY;
336
0
337
0
  JS::Rooted<JSObject*> get(aCx,
338
0
    JS_GetFunctionObject(js::NewFunctionByIdWithReserved(aCx, FieldGetter,
339
0
                                                         0, 0, id)));
340
0
  if (!get) {
341
0
    return NS_ERROR_OUT_OF_MEMORY;
342
0
  }
343
0
  js::SetFunctionNativeReserved(get, XBLPROTO_SLOT, wrappedClassObj);
344
0
  js::SetFunctionNativeReserved(get, FIELD_SLOT,
345
0
                                JS::StringValue(JSID_TO_STRING(id)));
346
0
347
0
  JS::Rooted<JSObject*> set(aCx,
348
0
    JS_GetFunctionObject(js::NewFunctionByIdWithReserved(aCx, FieldSetter,
349
0
                                                          1, 0, id)));
350
0
  if (!set) {
351
0
    return NS_ERROR_OUT_OF_MEMORY;
352
0
  }
353
0
  js::SetFunctionNativeReserved(set, XBLPROTO_SLOT, wrappedClassObj);
354
0
  js::SetFunctionNativeReserved(set, FIELD_SLOT,
355
0
                                JS::StringValue(JSID_TO_STRING(id)));
356
0
357
0
  // Now, re-enter the class object's scope, wrap the getters/setters, and define
358
0
  // them there.
359
0
  JSAutoRealm ar2(aCx, aTargetClassObject);
360
0
  if (!JS_WrapObject(aCx, &get) || !JS_WrapObject(aCx, &set)) {
361
0
    return NS_ERROR_OUT_OF_MEMORY;
362
0
  }
363
0
364
0
  if (!::JS_DefinePropertyById(aCx, aTargetClassObject, id, get, set,
365
0
                               AccessorAttributes())) {
366
0
    return NS_ERROR_OUT_OF_MEMORY;
367
0
  }
368
0
369
0
  return NS_OK;
370
0
}
371
372
nsresult
373
nsXBLProtoImplField::InstallField(JS::Handle<JSObject*> aBoundNode,
374
                                  const nsXBLPrototypeBinding& aProtoBinding,
375
                                  bool* aDidInstall) const
376
0
{
377
0
  MOZ_ASSERT(aBoundNode,
378
0
             "uh-oh, bound node should NOT be null or bad things will happen");
379
0
380
0
  *aDidInstall = false;
381
0
382
0
  // Empty fields are treated as not actually present.
383
0
  if (IsEmpty()) {
384
0
    return NS_OK;
385
0
  }
386
0
387
0
  nsAutoMicroTask mt;
388
0
389
0
  nsAutoCString uriSpec;
390
0
  nsresult rv = aProtoBinding.DocURI()->GetSpec(uriSpec);
391
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
392
0
    return rv;
393
0
  }
394
0
395
0
  nsIGlobalObject* globalObject = xpc::WindowGlobalOrNull(aBoundNode);
396
0
  if (!globalObject) {
397
0
    return NS_OK;
398
0
  }
399
0
400
0
  // We are going to run script via EvaluateString, so we need a script entry
401
0
  // point, but as this is XBL related it does not appear in the HTML spec.
402
0
  // We need an actual JSContext to do GetXBLScopeOrGlobal, and it needs to
403
0
  // be in the realm of globalObject.  But we want our XBL execution scope
404
0
  // to be our entry global.
405
0
  AutoJSAPI jsapi;
406
0
  if (!jsapi.Init(globalObject)) {
407
0
    return NS_ERROR_UNEXPECTED;
408
0
  }
409
0
  MOZ_ASSERT(!::JS_IsExceptionPending(jsapi.cx()),
410
0
             "Shouldn't get here when an exception is pending!");
411
0
412
0
  // Note: the UNWRAP_OBJECT may mutate boundNode; don't use it after that call.
413
0
  JS::Rooted<JSObject*> boundNode(jsapi.cx(), aBoundNode);
414
0
  Element* boundElement = nullptr;
415
0
  rv = UNWRAP_OBJECT(Element, &boundNode, boundElement);
416
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
417
0
    return rv;
418
0
  }
419
0
420
0
  // First, enter the xbl scope, build the element's scope chain, and use
421
0
  // that as the scope chain for the evaluation.
422
0
  JS::Rooted<JSObject*> scopeObject(jsapi.cx(),
423
0
    xpc::GetXBLScopeOrGlobal(jsapi.cx(), aBoundNode));
424
0
  NS_ENSURE_TRUE(scopeObject, NS_ERROR_OUT_OF_MEMORY);
425
0
426
0
  AutoEntryScript aes(scopeObject, "XBL <field> initialization", true);
427
0
  JSContext* cx = aes.cx();
428
0
429
0
  JS::Rooted<JS::Value> result(cx);
430
0
  JS::CompileOptions options(cx);
431
0
  options.setFileAndLine(uriSpec.get(), mLineNumber);
432
0
  JS::AutoObjectVector scopeChain(cx);
433
0
  if (!nsJSUtils::GetScopeChainForXBL(cx, boundElement, aProtoBinding, scopeChain)) {
434
0
    return NS_ERROR_OUT_OF_MEMORY;
435
0
  }
436
0
  rv = NS_OK;
437
0
  {
438
0
    nsJSUtils::ExecutionContext exec(cx, scopeObject);
439
0
    exec.SetScopeChain(scopeChain);
440
0
    exec.CompileAndExec(options, nsDependentString(mFieldText,
441
0
                                                   mFieldTextLength));
442
0
    rv = exec.ExtractReturnValue(&result);
443
0
  }
444
0
445
0
  if (NS_FAILED(rv)) {
446
0
    return rv;
447
0
  }
448
0
449
0
  if (rv == NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW) {
450
0
    // Report the exception now, before we try using the JSContext for
451
0
    // the JS_DefineUCProperty call.  Note that this reports in our current
452
0
    // realm, which is the XBL scope.
453
0
    aes.ReportException();
454
0
  }
455
0
456
0
  // Now, enter the node's realm, wrap the eval result, and define it on
457
0
  // the bound node.
458
0
  JSAutoRealm ar2(cx, aBoundNode);
459
0
  nsDependentString name(mName);
460
0
  if (!JS_WrapValue(cx, &result) ||
461
0
      !::JS_DefineUCProperty(cx, aBoundNode,
462
0
                             reinterpret_cast<const char16_t*>(mName),
463
0
                             name.Length(), result, mJSAttributes)) {
464
0
    return NS_ERROR_OUT_OF_MEMORY;
465
0
  }
466
0
467
0
  *aDidInstall = true;
468
0
  return NS_OK;
469
0
}
470
471
nsresult
472
nsXBLProtoImplField::Read(nsIObjectInputStream* aStream)
473
0
{
474
0
  nsAutoString name;
475
0
  nsresult rv = aStream->ReadString(name);
476
0
  NS_ENSURE_SUCCESS(rv, rv);
477
0
  mName = ToNewUnicode(name);
478
0
479
0
  rv = aStream->Read32(&mLineNumber);
480
0
  NS_ENSURE_SUCCESS(rv, rv);
481
0
482
0
  nsAutoString fieldText;
483
0
  rv = aStream->ReadString(fieldText);
484
0
  NS_ENSURE_SUCCESS(rv, rv);
485
0
  mFieldTextLength = fieldText.Length();
486
0
  if (mFieldTextLength)
487
0
    mFieldText = ToNewUnicode(fieldText);
488
0
489
0
  return NS_OK;
490
0
}
491
492
nsresult
493
nsXBLProtoImplField::Write(nsIObjectOutputStream* aStream)
494
0
{
495
0
  XBLBindingSerializeDetails type = XBLBinding_Serialize_Field;
496
0
497
0
  if (mJSAttributes & JSPROP_READONLY) {
498
0
    type |= XBLBinding_Serialize_ReadOnly;
499
0
  }
500
0
501
0
  nsresult rv = aStream->Write8(type);
502
0
  NS_ENSURE_SUCCESS(rv, rv);
503
0
  rv = aStream->WriteWStringZ(mName);
504
0
  NS_ENSURE_SUCCESS(rv, rv);
505
0
  rv = aStream->Write32(mLineNumber);
506
0
  NS_ENSURE_SUCCESS(rv, rv);
507
0
508
0
  return aStream->WriteWStringZ(mFieldText ? mFieldText : u"");
509
0
}