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