Skip to main content

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