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