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