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