Skip to main content

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}