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 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::globalscope::GlobalScope;
32use crate::dom::window::Window;
33use crate::realms::enter_auto_realm;
34use crate::script_module::{
35 ModuleScript, ModuleSource, ModuleTree, RethrowError, ScriptFetchOptions,
36};
37use crate::unminify::unminify_js;
38
39/// <https://html.spec.whatwg.org/multipage/#classic-script>
40#[derive(JSTraceable, MallocSizeOf)]
41pub(crate) struct ClassicScript {
42 /// On script parsing success this will be <https://html.spec.whatwg.org/multipage/#concept-script-record>
43 /// On failure <https://html.spec.whatwg.org/multipage/#concept-script-error-to-rethrow>
44 #[ignore_malloc_size_of = "mozjs"]
45 pub record: Result<RootedTraceableBox<Heap<*mut JSScript>>, RethrowError>,
46 /// <https://html.spec.whatwg.org/multipage/#concept-script-script-fetch-options>
47 fetch_options: ScriptFetchOptions,
48 /// <https://html.spec.whatwg.org/multipage/#concept-script-base-url>
49 #[no_trace]
50 url: ServoUrl,
51 /// <https://html.spec.whatwg.org/multipage/#muted-errors>
52 muted_errors: ErrorReporting,
53}
54
55#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
56pub(crate) enum ErrorReporting {
57 Muted,
58 Unmuted,
59}
60
61impl From<bool> for ErrorReporting {
62 fn from(boolean: bool) -> Self {
63 if boolean {
64 ErrorReporting::Muted
65 } else {
66 ErrorReporting::Unmuted
67 }
68 }
69}
70
71pub(crate) enum RethrowErrors {
72 Yes,
73 No,
74}
75
76impl GlobalScope {
77 /// <https://html.spec.whatwg.org/multipage/#creating-a-classic-script>
78 #[expect(clippy::too_many_arguments)]
79 #[expect(unsafe_code)]
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 source = if self.unminified_js_dir().is_some() {
92 let mut script_source = ModuleSource {
93 source,
94 unminified_dir: self.unminified_js_dir(),
95 external,
96 url: url.clone(),
97 };
98 unminify_js(&mut script_source);
99 transform_str_to_source_text(&script_source.source)
100 } else {
101 transform_str_to_source_text(&source)
102 };
103
104 // TODO Step 1. If mutedErrors is true, then set baseURL to about:blank.
105
106 // TODO Step 2. If scripting is disabled for settings, then set source to the empty string.
107
108 // TODO Step 4. Set script's settings object to settings.
109
110 // TODO Step 9. Record classic script creation time given script and sourceURLForWindowScripts.
111
112 let options = fill_compile_options(
113 cx,
114 url.as_str(),
115 introduction_type,
116 muted_errors,
117 true, // noScriptRval
118 line_number,
119 );
120
121 // Step 10. Let result be ParseScript(source, settings's realm, script).
122 rooted!(&in(cx) let compiled_script = unsafe { Compile1(cx, options.ptr, &mut source) });
123
124 // Step 11. If result is a list of errors, then:
125 let record = if compiled_script.get().is_null() {
126 // Step 11.1. Set script's parse error and its error to rethrow to result[0].
127 // Step 11.2. Return script.
128 Err(RethrowError::from_pending_exception(cx))
129 } else {
130 Ok(RootedTraceableBox::from_box(Heap::boxed(
131 compiled_script.get(),
132 )))
133 };
134
135 // Step 3. Let script be a new classic script that this algorithm will subsequently initialize.
136 // Step 5. Set script's base URL to baseURL.
137 // Step 6. Set script's fetch options to options.
138 // Step 7. Set script's muted errors to mutedErrors.
139 // Step 12. Set script's record to result.
140 // Step 13. Return script.
141 ClassicScript {
142 record,
143 url,
144 fetch_options,
145 muted_errors,
146 }
147 }
148
149 /// <https://html.spec.whatwg.org/multipage/#run-a-classic-script>
150 #[expect(unsafe_code)]
151 pub(crate) fn run_a_classic_script(
152 &self,
153 cx: &mut JSContext,
154 script: ClassicScript,
155 rethrow_errors: RethrowErrors,
156 ) -> ErrorResult {
157 // TODO Step 1. Let settings be the settings object of script.
158
159 // Step 2. Check if we can run script with settings. If this returns "do not run", then return NormalCompletion(empty).
160 if !self.can_run_script() {
161 return Ok(());
162 }
163
164 // TODO Step 3. Record classic script execution start time given script.
165
166 let mut realm = enter_auto_realm(cx, self);
167 let cx = &mut realm.current_realm();
168
169 // Step 4. Prepare to run script given settings.
170 // Once dropped this will run "Step 9. Clean up after running script" steps
171 run_a_script::<DomTypeHolder, _, _>(cx, self, |cx| {
172 // Step 5. Let evaluationStatus be null.
173 let mut result = false;
174
175 match script.record {
176 // Step 6. If script's error to rethrow is not null, then set evaluationStatus to ThrowCompletion(script's error to rethrow).
177 Err(error_to_rethrow) => unsafe {
178 JS_SetPendingException(
179 cx,
180 error_to_rethrow.handle(),
181 ExceptionStackBehavior::Capture,
182 )
183 },
184 // Step 7. Otherwise, set evaluationStatus to ScriptEvaluation(script's record).
185 Ok(compiled_script) => {
186 rooted!(&in(cx) let mut rval = UndefinedValue());
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 rval.handle_mut(),
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 rethrow_errors {
204 RethrowErrors::Yes => {
205 match script.muted_errors {
206 // Step 8.1. If rethrow errors is true and script's muted errors is false, then:
207 // Rethrow evaluationStatus.[[Value]].
208 ErrorReporting::Unmuted => return Err(Error::JSFailed),
209 // Step 8.2. If rethrow errors is true and script's muted errors is true, then:
210 ErrorReporting::Muted => {
211 unsafe { JS_ClearPendingException(cx) };
212 // Throw a "NetworkError" DOMException.
213 return Err(Error::Network(None));
214 },
215 }
216 },
217 // Step 8.3. Otherwise, rethrow errors is false. Perform the following steps:
218 RethrowErrors::No => {
219 // Report an exception given by evaluationStatus.[[Value]] for script's
220 // settings object's global object.
221 match script.muted_errors {
222 ErrorReporting::Unmuted => {
223 report_pending_exception(cx);
224 },
225 ErrorReporting::Muted => {
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 },
236 }
237 return Err(Error::JSFailed);
238 },
239 }
240 }
241
242 maybe_resume_unwind();
243
244 // Step 10. If evaluationStatus is a normal completion, then return evaluationStatus.
245 if result {
246 return Ok(());
247 }
248
249 // Step 11. If we've reached this point, evaluationStatus was left as null because the script
250 // was aborted prematurely during evaluation. Return ThrowCompletion(a new QuotaExceededError).
251 Err(Error::QuotaExceeded {
252 quota: None,
253 requested: None,
254 })
255 })
256 }
257
258 /// <https://html.spec.whatwg.org/multipage/#run-a-module-script>
259 pub(crate) fn run_a_module_script(
260 &self,
261 cx: &mut JSContext,
262 module_tree: Rc<ModuleTree>,
263 _rethrow_errors: bool,
264 ) {
265 // Step 1. Let settings be the settings object of script.
266 // NOTE(pylbrecht): "settings" is `self` here.
267
268 // Step 2. Check if we can run script with settings. If this returns "do not run", then
269 // return a promise resolved with undefined.
270 if !self.can_run_script() {
271 return;
272 }
273
274 // Step 3. Record module script execution start time given script.
275 // TODO
276
277 // Step 4. Prepare to run script given settings.
278 run_a_script::<DomTypeHolder, _, _>(cx, self, |cx| {
279 // Step 6. If script's error to rethrow is not null, then set evaluationPromise to a
280 // promise rejected with script's error to rethrow.
281 {
282 let module_error = module_tree.get_rethrow_error().borrow();
283 if module_error.is_some() {
284 module_tree.report_error(cx, self);
285 return;
286 }
287 }
288
289 // Step 7.1. Otherwise: Let record be script's record.
290 let record = module_tree.get_record().map(|record| record.handle());
291
292 if let Some(record) = record {
293 // Step 7.2. Set evaluationPromise to record.Evaluate().
294 rooted!(&in(cx) let mut rval = UndefinedValue());
295 let evaluated = module_tree.execute_module(cx, self, record, rval.handle_mut());
296
297 // Step 8. If preventErrorReporting is false, then upon rejection of evaluationPromise
298 // with reason, report an exception given by reason for script's settings object's
299 // global object.
300 if let Err(exception) = evaluated {
301 module_tree.set_rethrow_error(exception);
302 module_tree.report_error(cx, self);
303 }
304 }
305 });
306 }
307
308 /// <https://html.spec.whatwg.org/multipage/#check-if-we-can-run-script>
309 pub(crate) fn can_run_script(&self) -> bool {
310 // Step 1 If the global object specified by settings is a Window object
311 // whose Document object is not fully active, then return "do not run".
312 //
313 // Step 2 If scripting is disabled for settings, then return "do not run".
314 //
315 // An user agent can also disable scripting
316 //
317 // Either settings's global object is not a Window object,
318 // or settings's global object's associated Document's active sandboxing flag set
319 // does not have its sandboxed scripts browsing context flag set.
320 if let Some(window) = self.downcast::<Window>() {
321 let doc = window.Document();
322 doc.is_fully_active() &&
323 !doc.has_active_sandboxing_flag(
324 SandboxingFlagSet::SANDBOXED_SCRIPTS_BROWSING_CONTEXT_FLAG,
325 )
326 } else {
327 true
328 }
329 }
330}
331
332pub(crate) fn fill_compile_options(
333 cx: &mut JSContext,
334 filename: &str,
335 introduction_type: Option<&'static CStr>,
336 muted_errors: ErrorReporting,
337 no_script_rval: bool,
338 line_number: u32,
339) -> CompileOptionsWrapper {
340 let muted_errors = match muted_errors {
341 ErrorReporting::Muted => true,
342 ErrorReporting::Unmuted => false,
343 };
344
345 // TODO: pass filename as CString to avoid allocation
346 // See https://github.com/servo/servo/issues/42126
347 let mut options = CompileOptionsWrapper::new(cx, cformat!("{filename}"), line_number);
348 if let Some(introduction_type) = introduction_type {
349 options.set_introduction_type(introduction_type);
350 }
351
352 // https://searchfox.org/firefox-main/rev/46fa95cd7f10222996ec267947ab94c5107b1475/js/public/CompileOptions.h#284
353 options.set_muted_errors(muted_errors);
354
355 // https://searchfox.org/firefox-main/rev/46fa95cd7f10222996ec267947ab94c5107b1475/js/public/CompileOptions.h#518
356 options.set_is_run_once(true);
357 options.set_no_script_rval(no_script_rval);
358
359 options
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}