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