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