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