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