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