script/dom/global_scope_script_execution.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::borrow::Cow;
6use std::ffi::CStr;
7use std::ptr::NonNull;
8use std::rc::Rc;
9
10use content_security_policy::sandboxing_directive::SandboxingFlagSet;
11use js::context::JSContext;
12use js::jsapi::{ExceptionStackBehavior, Heap, JSScript, SetScriptPrivate};
13use js::jsval::{PrivateValue, UndefinedValue};
14use js::panic::maybe_resume_unwind;
15use js::rust::wrappers2::{
16 Compile1, JS_ClearPendingException, JS_ExecuteScript, JS_GetScriptPrivate,
17 JS_IsExceptionPending, JS_SetPendingException,
18};
19use js::rust::{
20 CompileOptionsWrapper, HandleValue, MutableHandleValue, transform_str_to_source_text,
21};
22use script_bindings::cformat;
23use script_bindings::settings_stack::run_a_script;
24use script_bindings::trace::RootedTraceableBox;
25use servo_url::ServoUrl;
26
27use crate::DomTypeHolder;
28use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
29use crate::dom::bindings::error::{Error, ErrorInfo, ErrorResult, report_pending_exception};
30use crate::dom::bindings::inheritance::Castable;
31use crate::dom::bindings::str::DOMString;
32use crate::dom::globalscope::GlobalScope;
33use crate::dom::window::Window;
34use crate::realms::enter_auto_realm;
35use crate::script_module::{
36 ModuleScript, ModuleSource, ModuleTree, RethrowError, ScriptFetchOptions,
37};
38use crate::unminify::unminify_js;
39
40/// <https://html.spec.whatwg.org/multipage/#classic-script>
41#[derive(JSTraceable, MallocSizeOf)]
42pub(crate) struct ClassicScript {
43 /// On script parsing success this will be <https://html.spec.whatwg.org/multipage/#concept-script-record>
44 /// On failure <https://html.spec.whatwg.org/multipage/#concept-script-error-to-rethrow>
45 #[ignore_malloc_size_of = "mozjs"]
46 pub record: Result<RootedTraceableBox<Heap<*mut JSScript>>, RethrowError>,
47 /// <https://html.spec.whatwg.org/multipage/#concept-script-script-fetch-options>
48 fetch_options: ScriptFetchOptions,
49 /// <https://html.spec.whatwg.org/multipage/#concept-script-base-url>
50 #[no_trace]
51 url: ServoUrl,
52 /// <https://html.spec.whatwg.org/multipage/#muted-errors>
53 muted_errors: ErrorReporting,
54}
55
56#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
57pub(crate) enum ErrorReporting {
58 Muted,
59 Unmuted,
60}
61
62impl From<bool> for ErrorReporting {
63 fn from(boolean: bool) -> Self {
64 if boolean {
65 ErrorReporting::Muted
66 } else {
67 ErrorReporting::Unmuted
68 }
69 }
70}
71
72pub(crate) enum RethrowErrors {
73 Yes,
74 No,
75}
76
77impl GlobalScope {
78 /// <https://html.spec.whatwg.org/multipage/#creating-a-classic-script>
79 #[expect(clippy::too_many_arguments)]
80 pub(crate) fn create_a_classic_script(
81 &self,
82 cx: &mut JSContext,
83 source: Cow<'_, str>,
84 url: ServoUrl,
85 fetch_options: ScriptFetchOptions,
86 muted_errors: ErrorReporting,
87 introduction_type: Option<&'static CStr>,
88 line_number: u32,
89 external: bool,
90 ) -> ClassicScript {
91 let mut script_source = ModuleSource {
92 source: Rc::new(DOMString::from(source)),
93 unminified_dir: self.unminified_js_dir(),
94 external,
95 url: url.clone(),
96 };
97 unminify_js(&mut script_source);
98
99 rooted!(&in(cx) let mut compiled_script = std::ptr::null_mut::<JSScript>());
100
101 // TODO Step 1. If mutedErrors is true, then set baseURL to about:blank.
102
103 // TODO Step 2. If scripting is disabled for settings, then set source to the empty string.
104
105 // TODO Step 4. Set script's settings object to settings.
106
107 // TODO Step 9. Record classic script creation time given script and sourceURLForWindowScripts.
108
109 // Step 10. Let result be ParseScript(source, settings's realm, script).
110 compiled_script.set(compile_script(
111 cx,
112 &script_source.source.str(),
113 url.as_str(),
114 line_number,
115 introduction_type,
116 muted_errors,
117 true,
118 ));
119
120 // Step 11. If result is a list of errors, then:
121 let record = if compiled_script.get().is_null() {
122 // Step 11.1. Set script's parse error and its error to rethrow to result[0].
123 // Step 11.2. Return script.
124 Err(RethrowError::from_pending_exception(cx))
125 } else {
126 Ok(RootedTraceableBox::from_box(Heap::boxed(
127 compiled_script.get(),
128 )))
129 };
130
131 // Step 3. Let script be a new classic script that this algorithm will subsequently initialize.
132 // Step 5. Set script's base URL to baseURL.
133 // Step 6. Set script's fetch options to options.
134 // Step 7. Set script's muted errors to mutedErrors.
135 // Step 12. Set script's record to result.
136 // Step 13. Return script.
137 ClassicScript {
138 record,
139 url,
140 fetch_options,
141 muted_errors,
142 }
143 }
144
145 /// <https://html.spec.whatwg.org/multipage/#run-a-classic-script>
146 #[expect(unsafe_code)]
147 pub(crate) fn run_a_classic_script(
148 &self,
149 cx: &mut JSContext,
150 script: ClassicScript,
151 rethrow_errors: RethrowErrors,
152 ) -> ErrorResult {
153 // TODO Step 1. Let settings be the settings object of script.
154
155 // Step 2. Check if we can run script with settings. If this returns "do not run", then return NormalCompletion(empty).
156 if !self.can_run_script() {
157 return Ok(());
158 }
159
160 // TODO Step 3. Record classic script execution start time given script.
161
162 let mut realm = enter_auto_realm(cx, self);
163 let cx = &mut realm.current_realm();
164
165 // Step 4. Prepare to run script given settings.
166 // Once dropped this will run "Step 9. Clean up after running script" steps
167 run_a_script::<DomTypeHolder, _>(self, || {
168 // Step 5. Let evaluationStatus be null.
169 let mut result = false;
170
171 match script.record {
172 // Step 6. If script's error to rethrow is not null, then set evaluationStatus to ThrowCompletion(script's error to rethrow).
173 Err(error_to_rethrow) => unsafe {
174 JS_SetPendingException(
175 cx,
176 error_to_rethrow.handle(),
177 ExceptionStackBehavior::Capture,
178 )
179 },
180 // Step 7. Otherwise, set evaluationStatus to ScriptEvaluation(script's record).
181 Ok(compiled_script) => {
182 rooted!(&in(cx) let mut rval = UndefinedValue());
183 let script_ptr = NonNull::new(compiled_script.get())
184 .expect("Compiled script must not be null");
185 result = evaluate_script(
186 cx,
187 script_ptr,
188 script.url,
189 script.fetch_options,
190 rval.handle_mut(),
191 );
192 },
193 }
194
195 // Step 8. If evaluationStatus is an abrupt completion, then:
196 if unsafe { JS_IsExceptionPending(cx) } {
197 warn!("Error evaluating script");
198
199 match rethrow_errors {
200 RethrowErrors::Yes => {
201 match script.muted_errors {
202 // Step 8.1. If rethrow errors is true and script's muted errors is false, then:
203 // Rethrow evaluationStatus.[[Value]].
204 ErrorReporting::Unmuted => return Err(Error::JSFailed),
205 // Step 8.2. If rethrow errors is true and script's muted errors is true, then:
206 ErrorReporting::Muted => {
207 unsafe { JS_ClearPendingException(cx) };
208 // Throw a "NetworkError" DOMException.
209 return Err(Error::Network(None));
210 },
211 }
212 },
213 // Step 8.3. Otherwise, rethrow errors is false. Perform the following steps:
214 RethrowErrors::No => {
215 // Report an exception given by evaluationStatus.[[Value]] for script's
216 // settings object's global object.
217 match script.muted_errors {
218 ErrorReporting::Unmuted => {
219 report_pending_exception(cx);
220 },
221 ErrorReporting::Muted => {
222 unsafe { JS_ClearPendingException(cx) };
223 self.report_an_error(
224 cx,
225 ErrorInfo {
226 message: String::from("Script error."),
227 ..Default::default()
228 },
229 HandleValue::null(),
230 );
231 },
232 }
233 return Err(Error::JSFailed);
234 },
235 }
236 }
237
238 maybe_resume_unwind();
239
240 // Step 10. If evaluationStatus is a normal completion, then return evaluationStatus.
241 if result {
242 return Ok(());
243 }
244
245 // Step 11. If we've reached this point, evaluationStatus was left as null because the script
246 // was aborted prematurely during evaluation. Return ThrowCompletion(a new QuotaExceededError).
247 Err(Error::QuotaExceeded {
248 quota: None,
249 requested: None,
250 })
251 })
252 }
253
254 /// <https://html.spec.whatwg.org/multipage/#run-a-module-script>
255 pub(crate) fn run_a_module_script(
256 &self,
257 cx: &mut JSContext,
258 module_tree: Rc<ModuleTree>,
259 _rethrow_errors: bool,
260 ) {
261 // Step 1. Let settings be the settings object of script.
262 // NOTE(pylbrecht): "settings" is `self` here.
263
264 // Step 2. Check if we can run script with settings. If this returns "do not run", then
265 // return a promise resolved with undefined.
266 if !self.can_run_script() {
267 return;
268 }
269
270 // Step 3. Record module script execution start time given script.
271 // TODO
272
273 // Step 4. Prepare to run script given settings.
274 run_a_script::<DomTypeHolder, _>(self, || {
275 // Step 6. If script's error to rethrow is not null, then set evaluationPromise to a
276 // promise rejected with script's error to rethrow.
277 {
278 let module_error = module_tree.get_rethrow_error().borrow();
279 if module_error.is_some() {
280 module_tree.report_error(cx, self);
281 return;
282 }
283 }
284
285 // Step 7.1. Otherwise: Let record be script's record.
286 let record = module_tree.get_record().map(|record| record.handle());
287
288 if let Some(record) = record {
289 // Step 7.2. Set evaluationPromise to record.Evaluate().
290 rooted!(&in(cx) let mut rval = UndefinedValue());
291 let evaluated = module_tree.execute_module(cx, self, record, rval.handle_mut());
292
293 // Step 8. If preventErrorReporting is false, then upon rejection of evaluationPromise
294 // with reason, report an exception given by reason for script's settings object's
295 // global object.
296 if let Err(exception) = evaluated {
297 module_tree.set_rethrow_error(exception);
298 module_tree.report_error(cx, self);
299 }
300 }
301 });
302 }
303
304 /// <https://html.spec.whatwg.org/multipage/#check-if-we-can-run-script>
305 fn can_run_script(&self) -> bool {
306 // Step 1 If the global object specified by settings is a Window object
307 // whose Document object is not fully active, then return "do not run".
308 //
309 // Step 2 If scripting is disabled for settings, then return "do not run".
310 //
311 // An user agent can also disable scripting
312 //
313 // Either settings's global object is not a Window object,
314 // or settings's global object's associated Document's active sandboxing flag set
315 // does not have its sandboxed scripts browsing context flag set.
316 if let Some(window) = self.downcast::<Window>() {
317 let doc = window.Document();
318 doc.is_fully_active() ||
319 !doc.has_active_sandboxing_flag(
320 SandboxingFlagSet::SANDBOXED_SCRIPTS_BROWSING_CONTEXT_FLAG,
321 )
322 } else {
323 true
324 }
325 }
326}
327
328#[expect(unsafe_code)]
329pub(crate) fn compile_script(
330 cx: &mut JSContext,
331 text: &str,
332 filename: &str,
333 line_number: u32,
334 introduction_type: Option<&'static CStr>,
335 muted_errors: ErrorReporting,
336 no_script_rval: bool,
337) -> *mut JSScript {
338 let muted_errors = match muted_errors {
339 ErrorReporting::Muted => true,
340 ErrorReporting::Unmuted => false,
341 };
342
343 // TODO: pass filename as CString to avoid allocation
344 // See https://github.com/servo/servo/issues/42126
345 let filename = cformat!("{filename}");
346 let mut options = CompileOptionsWrapper::new(cx, filename, line_number);
347 if let Some(introduction_type) = introduction_type {
348 options.set_introduction_type(introduction_type);
349 }
350
351 // https://searchfox.org/firefox-main/rev/46fa95cd7f10222996ec267947ab94c5107b1475/js/public/CompileOptions.h#284
352 options.set_muted_errors(muted_errors);
353
354 // https://searchfox.org/firefox-main/rev/46fa95cd7f10222996ec267947ab94c5107b1475/js/public/CompileOptions.h#518
355 options.set_is_run_once(true);
356 options.set_no_script_rval(no_script_rval);
357
358 debug!("Compiling script");
359 unsafe { Compile1(cx, options.ptr, &mut transform_str_to_source_text(text)) }
360}
361
362/// <https://tc39.es/ecma262/#sec-runtime-semantics-scriptevaluation>
363#[expect(unsafe_code)]
364pub(crate) fn evaluate_script(
365 cx: &mut JSContext,
366 compiled_script: NonNull<JSScript>,
367 url: ServoUrl,
368 fetch_options: ScriptFetchOptions,
369 rval: MutableHandleValue,
370) -> bool {
371 rooted!(&in(cx) let record = compiled_script.as_ptr());
372 rooted!(&in(cx) let mut script_private = UndefinedValue());
373
374 unsafe { JS_GetScriptPrivate(*record, script_private.handle_mut()) };
375
376 // When `ScriptPrivate` for the compiled script is undefined,
377 // we need to set it so that it can be used in dynamic import context.
378 if script_private.is_undefined() {
379 debug!("Set script private for {}", url);
380 let module_script_data = Rc::new(ModuleScript::new(
381 url,
382 fetch_options,
383 // We can't initialize an module owner here because
384 // the executing context of script might be different
385 // from the dynamic import script's executing context.
386 None,
387 ));
388
389 unsafe {
390 SetScriptPrivate(
391 *record,
392 &PrivateValue(Rc::into_raw(module_script_data) as *const _),
393 );
394 }
395 }
396
397 unsafe { JS_ExecuteScript(cx, record.handle(), rval) }
398}