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 servo_url::ServoUrl;
21
22use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
23use crate::dom::bindings::error::{Error, ErrorResult};
24use crate::dom::bindings::inheritance::Castable;
25use crate::dom::bindings::settings_stack::AutoEntryScript;
26use crate::dom::bindings::str::DOMString;
27use crate::dom::globalscope::GlobalScope;
28use crate::dom::window::Window;
29use crate::script_module::{ModuleScript, ModuleSource, RethrowError, ScriptFetchOptions};
30use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
31use crate::unminify::unminify_js;
32
33/// <https://html.spec.whatwg.org/multipage/#classic-script>
34#[derive(JSTraceable, MallocSizeOf)]
35pub(crate) struct ClassicScript {
36    /// On script parsing success this will be <https://html.spec.whatwg.org/multipage/#concept-script-record>
37    /// On failure <https://html.spec.whatwg.org/multipage/#concept-script-error-to-rethrow>
38    #[no_trace]
39    #[ignore_malloc_size_of = "mozjs"]
40    pub record: Result<NonNull<JSScript>, RethrowError>,
41    /// <https://html.spec.whatwg.org/multipage/#concept-script-script-fetch-options>
42    fetch_options: ScriptFetchOptions,
43    /// <https://html.spec.whatwg.org/multipage/#concept-script-base-url>
44    #[no_trace]
45    url: ServoUrl,
46    /// <https://html.spec.whatwg.org/multipage/#muted-errors>
47    muted_errors: ErrorReporting,
48}
49
50#[derive(JSTraceable, MallocSizeOf)]
51pub(crate) enum ErrorReporting {
52    Muted,
53    Unmuted,
54}
55
56impl From<bool> for ErrorReporting {
57    fn from(boolean: bool) -> Self {
58        if boolean {
59            ErrorReporting::Muted
60        } else {
61            ErrorReporting::Unmuted
62        }
63    }
64}
65
66pub(crate) enum RethrowErrors {
67    Yes,
68    No,
69}
70
71impl GlobalScope {
72    /// <https://html.spec.whatwg.org/multipage/#creating-a-classic-script>
73    #[expect(clippy::too_many_arguments)]
74    pub(crate) fn create_a_classic_script(
75        &self,
76        source: Cow<'_, str>,
77        url: ServoUrl,
78        fetch_options: ScriptFetchOptions,
79        muted_errors: ErrorReporting,
80        introduction_type: Option<&'static CStr>,
81        line_number: u32,
82        external: bool,
83    ) -> ClassicScript {
84        let cx = GlobalScope::get_cx();
85
86        let mut script_source = ModuleSource {
87            source: Rc::new(DOMString::from(source)),
88            unminified_dir: self.unminified_js_dir(),
89            external,
90            url: url.clone(),
91        };
92        unminify_js(&mut script_source);
93
94        rooted!(in(*cx) let mut compiled_script = std::ptr::null_mut::<JSScript>());
95
96        // TODO Step 1. If mutedErrors is true, then set baseURL to about:blank.
97
98        // TODO Step 2. If scripting is disabled for settings, then set source to the empty string.
99
100        // TODO Step 4. Set script's settings object to settings.
101
102        // TODO Step 9. Record classic script creation time given script and sourceURLForWindowScripts.
103
104        // Step 10. Let result be ParseScript(source, settings's realm, script).
105        compiled_script.set(compile_script(
106            cx,
107            &script_source.source.str(),
108            url.as_str(),
109            line_number,
110            introduction_type,
111        ));
112
113        // Step 11. If result is a list of errors, then:
114        let record = if compiled_script.get().is_null() {
115            // Step 11.1. Set script's parse error and its error to rethrow to result[0].
116            // Step 11.2. Return script.
117            Err(RethrowError::from_pending_exception(cx))
118        } else {
119            Ok(NonNull::new(*compiled_script).expect("Can't be null"))
120        };
121
122        // Step 3. Let script be a new classic script that this algorithm will subsequently initialize.
123        // Step 5. Set script's base URL to baseURL.
124        // Step 6. Set script's fetch options to options.
125        // Step 7. Set script's muted errors to mutedErrors.
126        // Step 12. Set script's record to result.
127        // Step 13. Return script.
128        ClassicScript {
129            record,
130            url,
131            fetch_options,
132            muted_errors,
133        }
134    }
135
136    /// <https://html.spec.whatwg.org/multipage/#run-a-classic-script>
137    #[expect(unsafe_code)]
138    pub(crate) fn run_a_classic_script(
139        &self,
140        script: ClassicScript,
141        rethrow_errors: RethrowErrors,
142        can_gc: CanGc,
143    ) -> ErrorResult {
144        let cx = GlobalScope::get_cx();
145        // TODO Step 1. Let settings be the settings object of script.
146
147        // Step 2. Check if we can run script with settings. If this returns "do not run", then return NormalCompletion(empty).
148        if !self.can_run_script() {
149            return Ok(());
150        }
151
152        // TODO Step 3. Record classic script execution start time given script.
153
154        // Step 4. Prepare to run script given settings.
155        // Once dropped this will run "Step 9. Clean up after running script" steps
156        let _aes = AutoEntryScript::new(self);
157
158        // Step 5. Let evaluationStatus be null.
159        rooted!(in(*cx) let mut evaluation_status = UndefinedValue());
160        let mut result = false;
161
162        match script.record {
163            // Step 6. If script's error to rethrow is not null, then set evaluationStatus to ThrowCompletion(script's error to rethrow).
164            Err(error_to_rethrow) => unsafe {
165                JS_SetPendingException(
166                    *cx,
167                    error_to_rethrow.handle(),
168                    ExceptionStackBehavior::Capture,
169                )
170            },
171            // Step 7. Otherwise, set evaluationStatus to ScriptEvaluation(script's record).
172            Ok(compiled_script) => {
173                rooted!(in(*cx) let mut rval = UndefinedValue());
174                result = evaluate_script(
175                    cx,
176                    compiled_script,
177                    script.url,
178                    script.fetch_options,
179                    rval.handle_mut(),
180                );
181            },
182        }
183
184        unsafe { JS_GetPendingException(*cx, evaluation_status.handle_mut()) };
185
186        // Step 8. If evaluationStatus is an abrupt completion, then:
187        if !evaluation_status.is_undefined() {
188            warn!("Error evaluating script");
189
190            match (rethrow_errors, script.muted_errors) {
191                // Step 8.1. If rethrow errors is true and script's muted errors is false, then:
192                (RethrowErrors::Yes, ErrorReporting::Unmuted) => {
193                    // Rethrow evaluationStatus.[[Value]].
194                    return Err(Error::JSFailed);
195                },
196                // Step 8.2. If rethrow errors is true and script's muted errors is true, then:
197                (RethrowErrors::Yes, ErrorReporting::Muted) => {
198                    unsafe { JS_ClearPendingException(*cx) };
199                    // Throw a "NetworkError" DOMException.
200                    return Err(Error::Network(None));
201                },
202                // Step 8.3. Otherwise, rethrow errors is false. Perform the following steps:
203                _ => {
204                    unsafe { JS_ClearPendingException(*cx) };
205                    // Report an exception given by evaluationStatus.[[Value]] for script's settings object's global object.
206                    self.report_an_exception(cx, evaluation_status.handle(), can_gc);
207
208                    // Return evaluationStatus.
209                    return Err(Error::JSFailed);
210                },
211            }
212        }
213
214        maybe_resume_unwind();
215
216        // Step 10. If evaluationStatus is a normal completion, then return evaluationStatus.
217        if result {
218            return Ok(());
219        }
220
221        // Step 11. If we've reached this point, evaluationStatus was left as null because the script
222        // was aborted prematurely during evaluation. Return ThrowCompletion(a new QuotaExceededError).
223        Err(Error::QuotaExceeded {
224            quota: None,
225            requested: None,
226        })
227    }
228
229    /// <https://html.spec.whatwg.org/multipage/#check-if-we-can-run-script>
230    fn can_run_script(&self) -> bool {
231        // Step 1 If the global object specified by settings is a Window object
232        // whose Document object is not fully active, then return "do not run".
233        //
234        // Step 2 If scripting is disabled for settings, then return "do not run".
235        //
236        // An user agent can also disable scripting
237        //
238        // Either settings's global object is not a Window object,
239        // or settings's global object's associated Document's active sandboxing flag set
240        // does not have its sandboxed scripts browsing context flag set.
241        if let Some(window) = self.downcast::<Window>() {
242            let doc = window.Document();
243            doc.is_fully_active() ||
244                !doc.has_active_sandboxing_flag(
245                    SandboxingFlagSet::SANDBOXED_SCRIPTS_BROWSING_CONTEXT_FLAG,
246                )
247        } else {
248            true
249        }
250    }
251}
252
253#[expect(unsafe_code)]
254pub(crate) fn compile_script(
255    cx: SafeJSContext,
256    text: &str,
257    filename: &str,
258    line_number: u32,
259    introduction_type: Option<&'static CStr>,
260) -> *mut JSScript {
261    let mut options = unsafe { CompileOptionsWrapper::new_raw(*cx, filename, line_number) };
262    if let Some(introduction_type) = introduction_type {
263        options.set_introduction_type(introduction_type);
264    }
265
266    debug!("Compiling script");
267    unsafe { Compile1(*cx, options.ptr, &mut transform_str_to_source_text(text)) }
268}
269
270/// <https://tc39.es/ecma262/#sec-runtime-semantics-scriptevaluation>
271#[expect(unsafe_code)]
272pub(crate) fn evaluate_script(
273    cx: SafeJSContext,
274    compiled_script: NonNull<JSScript>,
275    url: ServoUrl,
276    fetch_options: ScriptFetchOptions,
277    rval: MutableHandleValue,
278) -> bool {
279    rooted!(in(*cx) let record = compiled_script.as_ptr());
280    rooted!(in(*cx) let mut script_private = UndefinedValue());
281
282    unsafe { JS_GetScriptPrivate(*record, script_private.handle_mut()) };
283
284    // When `ScriptPrivate` for the compiled script is undefined,
285    // we need to set it so that it can be used in dynamic import context.
286    if script_private.is_undefined() {
287        debug!("Set script private for {}", url);
288        let module_script_data = Rc::new(ModuleScript::new(
289            url,
290            fetch_options,
291            // We can't initialize an module owner here because
292            // the executing context of script might be different
293            // from the dynamic import script's executing context.
294            None,
295        ));
296
297        unsafe {
298            SetScriptPrivate(
299                *record,
300                &PrivateValue(Rc::into_raw(module_script_data) as *const _),
301            );
302        }
303    }
304
305    unsafe { JS_ExecuteScript(*cx, record.handle(), rval) }
306}