Skip to main content

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