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