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