1 /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 // SPDX-FileCopyrightText: 2008 litl, LLC
4 // SPDX-FileCopyrightText: 2009 Red Hat, Inc.
5 // SPDX-FileCopyrightText: 2017 Philip Chimento <philip.chimento@gmail.com>
6 // SPDX-FileCopyrightText: 2020 Evan Welsh <contact@evanwelsh.com>
7
8 #include <config.h>
9
10 #include <stddef.h> // for size_t
11
12 #include <glib.h>
13
14 #include <js/CallArgs.h> // for CallArgs, CallArgsFromVp
15 #include <js/CharacterEncoding.h> // for JS_EncodeStringToUTF8
16 #include <js/Class.h>
17 #include <js/CompilationAndEvaluation.h>
18 #include <js/CompileOptions.h>
19 #include <js/Id.h>
20 #include <js/PropertyDescriptor.h> // for JSPROP_PERMANENT, JSPROP_RE...
21 #include <js/PropertySpec.h>
22 #include <js/Realm.h> // for GetObjectRealmOrNull, SetRealmPrivate
23 #include <js/RealmOptions.h>
24 #include <js/RootingAPI.h>
25 #include <js/SourceText.h>
26 #include <js/TypeDecls.h>
27 #include <js/Utility.h> // for UniqueChars
28 #include <jsapi.h> // for AutoSaveExceptionState, ...
29
30 #include "gjs/atoms.h"
31 #include "gjs/context-private.h"
32 #include "gjs/engine.h"
33 #include "gjs/global.h"
34 #include "gjs/internal.h"
35 #include "gjs/jsapi-util.h"
36 #include "gjs/native.h"
37
38 namespace mozilla {
39 union Utf8Unit;
40 }
41
42 class GjsBaseGlobal {
base(JSContext * cx,const JSClass * clasp,JS::RealmCreationOptions options)43 static JSObject* base(JSContext* cx, const JSClass* clasp,
44 JS::RealmCreationOptions options) {
45 JS::RealmBehaviors behaviors;
46 JS::RealmOptions compartment_options(options, behaviors);
47
48 JS::RootedObject global(
49 cx, JS_NewGlobalObject(cx, clasp, nullptr, JS::FireOnNewGlobalHook,
50 compartment_options));
51
52 if (!global)
53 return nullptr;
54
55 JSAutoRealm ac(cx, global);
56
57 if (!JS_InitReflectParse(cx, global) ||
58 !JS_DefineDebuggerObject(cx, global))
59 return nullptr;
60
61 return global;
62 }
63
64 protected:
create(JSContext * cx,const JSClass * clasp,JS::RealmCreationOptions options=JS::RealmCreationOptions ())65 [[nodiscard]] static JSObject* create(
66 JSContext* cx, const JSClass* clasp,
67 JS::RealmCreationOptions options = JS::RealmCreationOptions()) {
68 options.setNewCompartmentAndZone();
69 return base(cx, clasp, options);
70 }
71
create_with_compartment(JSContext * cx,JS::HandleObject existing,const JSClass * clasp,JS::RealmCreationOptions options=JS::RealmCreationOptions ())72 [[nodiscard]] static JSObject* create_with_compartment(
73 JSContext* cx, JS::HandleObject existing, const JSClass* clasp,
74 JS::RealmCreationOptions options = JS::RealmCreationOptions()) {
75 options.setExistingCompartment(existing);
76 return base(cx, clasp, options);
77 }
78
79 GJS_JSAPI_RETURN_CONVENTION
run_bootstrap(JSContext * cx,const char * bootstrap_script,JS::HandleObject global)80 static bool run_bootstrap(JSContext* cx, const char* bootstrap_script,
81 JS::HandleObject global) {
82 GjsAutoChar uri = g_strdup_printf(
83 "resource:///org/gnome/gjs/modules/script/_bootstrap/%s.js",
84 bootstrap_script);
85
86 JSAutoRealm ar(cx, global);
87
88 JS::CompileOptions options(cx);
89 options.setFileAndLine(uri, 1).setSourceIsLazy(true);
90
91 char* script;
92 size_t script_len;
93 if (!gjs_load_internal_source(cx, uri, &script, &script_len))
94 return false;
95
96 JS::SourceText<mozilla::Utf8Unit> source;
97 if (!source.init(cx, script, script_len,
98 JS::SourceOwnership::TakeOwnership))
99 return false;
100
101 JS::RootedScript compiled_script(cx, JS::Compile(cx, options, source));
102 if (!compiled_script)
103 return false;
104
105 JS::RootedValue ignored(cx);
106 return JS::CloneAndExecuteScript(cx, compiled_script, &ignored);
107 }
108
109 GJS_JSAPI_RETURN_CONVENTION
load_native_module(JSContext * m_cx,unsigned argc,JS::Value * vp)110 static bool load_native_module(JSContext* m_cx, unsigned argc,
111 JS::Value* vp) {
112 JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
113
114 // This function should never be directly exposed to user code, so we
115 // can be strict.
116 g_assert(argc == 1);
117 g_assert(argv[0].isString());
118
119 JS::RootedString str(m_cx, argv[0].toString());
120 JS::UniqueChars id(JS_EncodeStringToUTF8(m_cx, str));
121
122 if (!id)
123 return false;
124
125 JS::RootedObject native_obj(m_cx);
126
127 if (!gjs_load_native_module(m_cx, id.get(), &native_obj)) {
128 gjs_throw(m_cx, "Failed to load native module: %s", id.get());
129 return false;
130 }
131
132 argv.rval().setObject(*native_obj);
133 return true;
134 }
135 };
136
137 const JSClassOps defaultclassops = JS::DefaultGlobalClassOps;
138
139 class GjsGlobal : GjsBaseGlobal {
140 static constexpr JSClass klass = {
141 // Jasmine depends on the class name "GjsGlobal" to detect GJS' global
142 // object.
143 "GjsGlobal",
144 JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
145 static_cast<uint32_t>(GjsGlobalSlot::LAST)),
146 &defaultclassops,
147 };
148
149 // clang-format off
150 static constexpr JSPropertySpec static_props[] = {
151 JS_STRING_SYM_PS(toStringTag, "GjsGlobal", JSPROP_READONLY),
152 JS_PS_END};
153 // clang-format on
154
155 static constexpr JSFunctionSpec static_funcs[] = {
156 JS_FS_END};
157
158 public:
create(JSContext * cx)159 [[nodiscard]] static JSObject* create(JSContext* cx) {
160 return GjsBaseGlobal::create(cx, &klass);
161 }
162
create_with_compartment(JSContext * cx,JS::HandleObject cmp_global)163 [[nodiscard]] static JSObject* create_with_compartment(
164 JSContext* cx, JS::HandleObject cmp_global) {
165 return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass);
166 }
167
168 GJS_JSAPI_RETURN_CONVENTION
define_properties(JSContext * cx,JS::HandleObject global,const char * realm_name,const char * bootstrap_script)169 static bool define_properties(JSContext* cx, JS::HandleObject global,
170 const char* realm_name,
171 const char* bootstrap_script) {
172 const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
173 if (!JS_DefinePropertyById(cx, global, atoms.window(), global,
174 JSPROP_READONLY | JSPROP_PERMANENT) ||
175 !JS_DefineFunctions(cx, global, GjsGlobal::static_funcs) ||
176 !JS_DefineProperties(cx, global, GjsGlobal::static_props))
177 return false;
178
179 JS::Realm* realm = JS::GetObjectRealmOrNull(global);
180 g_assert(realm && "Global object must be associated with a realm");
181 // const_cast is allowed here if we never free the realm data
182 JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
183
184 JS::RootedObject native_registry(cx, JS::NewMapObject(cx));
185 if (!native_registry)
186 return false;
187
188 gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
189 JS::ObjectValue(*native_registry));
190
191 JS::RootedObject module_registry(cx, JS::NewMapObject(cx));
192 if (!module_registry)
193 return false;
194
195 gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
196 JS::ObjectValue(*module_registry));
197
198 JS::Value v_importer =
199 gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS);
200 g_assert(((void) "importer should be defined before passing null "
201 "importer to GjsGlobal::define_properties",
202 v_importer.isObject()));
203 JS::RootedObject root_importer(cx, &v_importer.toObject());
204
205 // Wrapping is a no-op if the importer is already in the same realm.
206 if (!JS_WrapObject(cx, &root_importer) ||
207 !JS_DefinePropertyById(cx, global, atoms.imports(), root_importer,
208 GJS_MODULE_PROP_FLAGS))
209 return false;
210
211 if (bootstrap_script) {
212 if (!run_bootstrap(cx, bootstrap_script, global))
213 return false;
214 }
215
216 return true;
217 }
218 };
219
220 class GjsDebuggerGlobal : GjsBaseGlobal {
221 static constexpr JSClass klass = {
222 "GjsDebuggerGlobal",
223 JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
224 static_cast<uint32_t>(GjsDebuggerGlobalSlot::LAST)),
225 &defaultclassops,
226 };
227
228 static constexpr JSFunctionSpec static_funcs[] = {
229 JS_FN("loadNative", &load_native_module, 1, 0), JS_FS_END};
230
231 public:
create(JSContext * cx)232 [[nodiscard]] static JSObject* create(JSContext* cx) {
233 JS::RealmCreationOptions options;
234 options.setToSourceEnabled(true); // debugger uses uneval()
235 return GjsBaseGlobal::create(cx, &klass, options);
236 }
237
create_with_compartment(JSContext * cx,JS::HandleObject cmp_global)238 [[nodiscard]] static JSObject* create_with_compartment(
239 JSContext* cx, JS::HandleObject cmp_global) {
240 return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass);
241 }
242
define_properties(JSContext * cx,JS::HandleObject global,const char * realm_name,const char * bootstrap_script)243 static bool define_properties(JSContext* cx, JS::HandleObject global,
244 const char* realm_name,
245 const char* bootstrap_script) {
246 const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
247 if (!JS_DefinePropertyById(cx, global, atoms.window(), global,
248 JSPROP_READONLY | JSPROP_PERMANENT) ||
249 !JS_DefineFunctions(cx, global, GjsDebuggerGlobal::static_funcs))
250 return false;
251
252 JS::Realm* realm = JS::GetObjectRealmOrNull(global);
253 g_assert(realm && "Global object must be associated with a realm");
254 // const_cast is allowed here if we never free the realm data
255 JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
256
257 if (bootstrap_script) {
258 if (!run_bootstrap(cx, bootstrap_script, global))
259 return false;
260 }
261
262 return true;
263 }
264 };
265
266 class GjsInternalGlobal : GjsBaseGlobal {
267 static constexpr JSFunctionSpec static_funcs[] = {
268 JS_FN("compileModule", gjs_internal_compile_module, 2, 0),
269 JS_FN("compileInternalModule", gjs_internal_compile_internal_module, 2,
270 0),
271 JS_FN("getRegistry", gjs_internal_get_registry, 1, 0),
272 JS_FN("loadResourceOrFile", gjs_internal_load_resource_or_file, 1, 0),
273 JS_FN("loadResourceOrFileAsync",
274 gjs_internal_load_resource_or_file_async, 1, 0),
275 JS_FN("parseURI", gjs_internal_parse_uri, 1, 0),
276 JS_FN("resolveRelativeResourceOrFile",
277 gjs_internal_resolve_relative_resource_or_file, 2, 0),
278 JS_FN("setGlobalModuleLoader", gjs_internal_set_global_module_loader, 2,
279 0),
280 JS_FN("setModulePrivate", gjs_internal_set_module_private, 2, 0),
281 JS_FN("uriExists", gjs_internal_uri_exists, 1, 0),
282 JS_FS_END};
283
284 static constexpr JSClass klass = {
285 "GjsInternalGlobal",
286 JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
287 static_cast<uint32_t>(GjsInternalGlobalSlot::LAST)),
288 &defaultclassops,
289 };
290
291 public:
create(JSContext * cx)292 [[nodiscard]] static JSObject* create(JSContext* cx) {
293 return GjsBaseGlobal::create(cx, &klass);
294 }
295
create_with_compartment(JSContext * cx,JS::HandleObject cmp_global)296 [[nodiscard]] static JSObject* create_with_compartment(
297 JSContext* cx, JS::HandleObject cmp_global) {
298 return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass);
299 }
300
define_properties(JSContext * cx,JS::HandleObject global,const char * realm_name,const char * bootstrap_script)301 static bool define_properties(JSContext* cx, JS::HandleObject global,
302 const char* realm_name,
303 const char* bootstrap_script
304 [[maybe_unused]]) {
305 JS::Realm* realm = JS::GetObjectRealmOrNull(global);
306 g_assert(realm && "Global object must be associated with a realm");
307 // const_cast is allowed here if we never free the realm data
308 JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
309
310 JSAutoRealm ar(cx, global);
311 JS::RootedObject native_registry(cx, JS::NewMapObject(cx));
312 if (!native_registry)
313 return false;
314
315 gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
316 JS::ObjectValue(*native_registry));
317
318 JS::RootedObject module_registry(cx, JS::NewMapObject(cx));
319 if (!module_registry)
320 return false;
321
322 gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
323 JS::ObjectValue(*module_registry));
324
325 return JS_DefineFunctions(cx, global, static_funcs);
326 }
327 };
328
329 /**
330 * gjs_create_global_object:
331 * @cx: a #JSContext
332 *
333 * Creates a global object, and initializes it with the default API.
334 *
335 * Returns: the created global object on success, nullptr otherwise, in which
336 * case an exception is pending on @cx
337 */
gjs_create_global_object(JSContext * cx,GjsGlobalType global_type,JS::HandleObject current_global)338 JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type,
339 JS::HandleObject current_global) {
340 if (current_global) {
341 switch (global_type) {
342 case GjsGlobalType::DEFAULT:
343 return GjsGlobal::create_with_compartment(cx, current_global);
344 case GjsGlobalType::DEBUGGER:
345 return GjsDebuggerGlobal::create_with_compartment(
346 cx, current_global);
347 case GjsGlobalType::INTERNAL:
348 return GjsInternalGlobal::create_with_compartment(
349 cx, current_global);
350 default:
351 return nullptr;
352 }
353 }
354
355 switch (global_type) {
356 case GjsGlobalType::DEFAULT:
357 return GjsGlobal::create(cx);
358 case GjsGlobalType::DEBUGGER:
359 return GjsDebuggerGlobal::create(cx);
360 case GjsGlobalType::INTERNAL:
361 return GjsInternalGlobal::create(cx);
362 default:
363 return nullptr;
364 }
365 }
366
367 /**
368 * gjs_global_is_type:
369 *
370 * @param cx the current #JSContext
371 * @param type the global type to test for
372 *
373 * @returns whether the current global is the same type as #type
374 */
gjs_global_is_type(JSContext * cx,GjsGlobalType type)375 bool gjs_global_is_type(JSContext* cx, GjsGlobalType type) {
376 JSObject* global = JS::CurrentGlobalOrNull(cx);
377
378 g_assert(global && "gjs_global_is_type called before a realm was entered.");
379
380 JS::Value global_type =
381 gjs_get_global_slot(global, GjsBaseGlobalSlot::GLOBAL_TYPE);
382
383 g_assert(global_type.isInt32());
384
385 return static_cast<GjsGlobalType>(global_type.toInt32()) == type;
386 }
387
gjs_global_get_type(JSContext * cx)388 GjsGlobalType gjs_global_get_type(JSContext* cx) {
389 auto global = JS::CurrentGlobalOrNull(cx);
390
391 g_assert(global &&
392 "gjs_global_get_type called before a realm was entered.");
393
394 JS::Value global_type =
395 gjs_get_global_slot(global, GjsBaseGlobalSlot::GLOBAL_TYPE);
396
397 g_assert(global_type.isInt32());
398
399 return static_cast<GjsGlobalType>(global_type.toInt32());
400 }
401
gjs_global_get_type(JSObject * global)402 GjsGlobalType gjs_global_get_type(JSObject* global) {
403 JS::Value global_type =
404 gjs_get_global_slot(global, GjsBaseGlobalSlot::GLOBAL_TYPE);
405
406 g_assert(global_type.isInt32());
407
408 return static_cast<GjsGlobalType>(global_type.toInt32());
409 }
410
411 /**
412 * gjs_global_registry_set:
413 *
414 * @brief This function inserts a module object into a global registry.
415 * Global registries are JS Map objects for easy reuse and access
416 * within internal JS. This function will assert if a module has
417 * already been inserted at the given key.
418
419 * @param cx the current #JSContext
420 * @param registry a JS Map object
421 * @param key a module identifier, typically a string or symbol
422 * @param module a module object
423 */
gjs_global_registry_set(JSContext * cx,JS::HandleObject registry,JS::PropertyKey key,JS::HandleObject module)424 bool gjs_global_registry_set(JSContext* cx, JS::HandleObject registry,
425 JS::PropertyKey key, JS::HandleObject module) {
426 JS::RootedValue v_key(cx);
427 if (!JS_IdToValue(cx, key, &v_key))
428 return false;
429
430 bool has_key;
431 if (!JS::MapHas(cx, registry, v_key, &has_key))
432 return false;
433
434 g_assert(!has_key && "Module key already exists in the registry");
435
436 JS::RootedValue v_value(cx, JS::ObjectValue(*module));
437
438 return JS::MapSet(cx, registry, v_key, v_value);
439 }
440
441 /**
442 * gjs_global_registry_get:
443 *
444 * @brief This function inserts a module object into a global registry.
445 * Global registries are JS Map objects for easy reuse and access
446 * within internal JS. This function will assert if a module has
447 * already been inserted at the given key.
448
449 * @param cx the current #JSContext
450 * @param registry a JS Map object
451 * @param key a module identifier, typically a string or symbol
452 * @param module a module object
453 */
gjs_global_registry_get(JSContext * cx,JS::HandleObject registry,JS::PropertyKey key,JS::MutableHandleObject module_out)454 bool gjs_global_registry_get(JSContext* cx, JS::HandleObject registry,
455 JS::PropertyKey key,
456 JS::MutableHandleObject module_out) {
457 JS::RootedValue v_key(cx), v_value(cx);
458 if (!JS_IdToValue(cx, key, &v_key) ||
459 !JS::MapGet(cx, registry, v_key, &v_value))
460 return false;
461
462 g_assert((v_value.isUndefined() || v_value.isObject()) &&
463 "Invalid value in module registry");
464
465 if (v_value.isObject()) {
466 module_out.set(&v_value.toObject());
467 return true;
468 }
469
470 module_out.set(nullptr);
471 return true;
472 }
473
474 /**
475 * gjs_define_global_properties:
476 * @cx: a #JSContext
477 * @global: a JS global object that has not yet been passed to this function
478 * @realm_name: (nullable): name of the realm, for debug output
479 * @bootstrap_script: (nullable): name of a bootstrap script (found at
480 * resource://org/gnome/gjs/modules/script/_bootstrap/@bootstrap_script) or
481 * %NULL for none
482 *
483 * Defines properties on the global object such as 'window' and 'imports', and
484 * runs a bootstrap JS script on the global object to define any properties
485 * that can be defined from JS.
486 * This function completes the initialization of a new global object, but it
487 * is separate from gjs_create_global_object() because all globals share the
488 * same root importer.
489 * The code creating the main global for the JS context needs to create the
490 * root importer in between calling gjs_create_global_object() and
491 * gjs_define_global_properties().
492 *
493 * The caller of this function should be in the realm for @global.
494 * If the root importer object belongs to a different realm, this function will
495 * create a wrapper for it.
496 *
497 * Returns: true on success, false otherwise, in which case an exception is
498 * pending on @cx
499 */
gjs_define_global_properties(JSContext * cx,JS::HandleObject global,GjsGlobalType global_type,const char * realm_name,const char * bootstrap_script)500 bool gjs_define_global_properties(JSContext* cx, JS::HandleObject global,
501 GjsGlobalType global_type,
502 const char* realm_name,
503 const char* bootstrap_script) {
504 gjs_set_global_slot(global.get(), GjsBaseGlobalSlot::GLOBAL_TYPE,
505 JS::Int32Value(static_cast<uint32_t>(global_type)));
506
507 switch (global_type) {
508 case GjsGlobalType::DEFAULT:
509 return GjsGlobal::define_properties(cx, global, realm_name,
510 bootstrap_script);
511 case GjsGlobalType::DEBUGGER:
512 return GjsDebuggerGlobal::define_properties(cx, global, realm_name,
513 bootstrap_script);
514 case GjsGlobalType::INTERNAL:
515 return GjsInternalGlobal::define_properties(cx, global, realm_name,
516 bootstrap_script);
517 }
518
519 // Global type does not handle define_properties
520 g_assert_not_reached();
521 }
522
set_global_slot(JSObject * global,uint32_t slot,JS::Value value)523 void detail::set_global_slot(JSObject* global, uint32_t slot, JS::Value value) {
524 JS_SetReservedSlot(global, JSCLASS_GLOBAL_SLOT_COUNT + slot, value);
525 }
526
get_global_slot(JSObject * global,uint32_t slot)527 JS::Value detail::get_global_slot(JSObject* global, uint32_t slot) {
528 return JS_GetReservedSlot(global, JSCLASS_GLOBAL_SLOT_COUNT + slot);
529 }
530
531 decltype(GjsGlobal::klass) constexpr GjsGlobal::klass;
532 decltype(GjsGlobal::static_funcs) constexpr GjsGlobal::static_funcs;
533 decltype(GjsGlobal::static_props) constexpr GjsGlobal::static_props;
534
535 decltype(GjsDebuggerGlobal::klass) constexpr GjsDebuggerGlobal::klass;
536 decltype(
537 GjsDebuggerGlobal::static_funcs) constexpr GjsDebuggerGlobal::static_funcs;
538
539 decltype(GjsInternalGlobal::klass) constexpr GjsInternalGlobal::klass;
540 decltype(
541 GjsInternalGlobal::static_funcs) constexpr GjsInternalGlobal::static_funcs;
542