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