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