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