script/dom/
console.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::convert::TryFrom;
6use std::ptr::{self, NonNull};
7use std::slice;
8
9use devtools_traits::{
10    ConsoleArgument, ConsoleLogLevel, ConsoleMessage, ConsoleMessageFields,
11    ScriptToDevtoolsControlMsg, StackFrame, get_time_stamp,
12};
13use embedder_traits::EmbedderMsg;
14use js::conversions::jsstr_to_string;
15use js::jsapi::{self, ESClass, PropertyDescriptor};
16use js::jsval::{Int32Value, UndefinedValue};
17use js::rust::wrappers::{
18    GetBuiltinClass, GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById,
19    JS_IdToValue, JS_Stringify, JS_ValueToSource,
20};
21use js::rust::{
22    CapturedJSStack, HandleObject, HandleValue, IdVector, ToString, describe_scripted_caller,
23};
24use script_bindings::conversions::get_dom_class;
25
26use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::str::DOMString;
29use crate::dom::globalscope::GlobalScope;
30use crate::dom::workerglobalscope::WorkerGlobalScope;
31use crate::script_runtime::JSContext;
32
33/// The maximum object depth logged by console methods.
34const MAX_LOG_DEPTH: usize = 10;
35/// The maximum elements in an object logged by console methods.
36const MAX_LOG_CHILDREN: usize = 15;
37
38/// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
39#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
40pub(crate) struct Console;
41
42impl Console {
43    #[expect(unsafe_code)]
44    fn build_message(
45        level: ConsoleLogLevel,
46        arguments: Vec<ConsoleArgument>,
47        stacktrace: Option<Vec<StackFrame>>,
48    ) -> ConsoleMessage {
49        let cx = GlobalScope::get_cx();
50        let caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
51
52        ConsoleMessage {
53            fields: ConsoleMessageFields {
54                level,
55                filename: caller.filename,
56                line_number: caller.line,
57                column_number: caller.col,
58                time_stamp: get_time_stamp(),
59            },
60            arguments,
61            stacktrace,
62        }
63    }
64
65    /// Helper to send a message that only consists of a single string
66    fn send_string_message(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
67        let prefix = global.current_group_label().unwrap_or_default();
68        let formatted_message = format!("{prefix}{message}");
69
70        Self::send_to_embedder(global, level.clone(), formatted_message);
71
72        let console_message = Self::build_message(level, vec![message.into()], None);
73
74        Self::send_to_devtools(global, console_message);
75    }
76
77    fn method(
78        global: &GlobalScope,
79        level: ConsoleLogLevel,
80        messages: Vec<HandleValue>,
81        include_stacktrace: IncludeStackTrace,
82    ) {
83        let cx = GlobalScope::get_cx();
84
85        let arguments = messages
86            .iter()
87            .map(|msg| console_argument_from_handle_value(cx, *msg))
88            .collect();
89        let stacktrace = (include_stacktrace == IncludeStackTrace::Yes)
90            .then_some(get_js_stack(*GlobalScope::get_cx()));
91        let console_message = Self::build_message(level.clone(), arguments, stacktrace);
92
93        Console::send_to_devtools(global, console_message);
94
95        let prefix = global.current_group_label().unwrap_or_default();
96        let msgs = stringify_handle_values(&messages);
97        let formatted_message = format!("{prefix}{msgs}");
98
99        Self::send_to_embedder(global, level, formatted_message);
100    }
101
102    fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
103        if let Some(chan) = global.devtools_chan() {
104            let worker_id = global
105                .downcast::<WorkerGlobalScope>()
106                .map(|worker| worker.worker_id());
107            let devtools_message =
108                ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
109            chan.send(devtools_message).unwrap();
110        }
111    }
112
113    fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
114        global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
115            global.webview_id(),
116            level,
117            message,
118        ));
119    }
120
121    // Directly logs a DOMString, without processing the message
122    pub(crate) fn internal_warn(global: &GlobalScope, message: DOMString) {
123        Console::send_string_message(global, ConsoleLogLevel::Warn, String::from(message.clone()));
124    }
125}
126
127#[expect(unsafe_code)]
128unsafe fn handle_value_to_string(cx: *mut jsapi::JSContext, value: HandleValue) -> DOMString {
129    rooted!(in(cx) let mut js_string = std::ptr::null_mut::<jsapi::JSString>());
130    match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
131        Some(js_str) => {
132            js_string.set(js_str.as_ptr());
133            DOMString::from_string(unsafe { jsstr_to_string(cx, js_str) })
134        },
135        None => "<error converting value to string>".into(),
136    }
137}
138
139#[expect(unsafe_code)]
140fn console_argument_from_handle_value(cx: JSContext, handle_value: HandleValue) -> ConsoleArgument {
141    if handle_value.is_string() {
142        let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
143        let dom_string = unsafe { jsstr_to_string(*cx, js_string) };
144        return ConsoleArgument::String(dom_string);
145    }
146
147    if handle_value.is_int32() {
148        let integer = handle_value.to_int32();
149        return ConsoleArgument::Integer(integer);
150    }
151
152    if handle_value.is_number() {
153        let number = handle_value.to_number();
154        return ConsoleArgument::Number(number);
155    }
156
157    // FIXME: Handle more complex argument types here
158    let stringified_value = stringify_handle_value(handle_value);
159    ConsoleArgument::String(stringified_value.into())
160}
161
162#[expect(unsafe_code)]
163fn stringify_handle_value(message: HandleValue) -> DOMString {
164    let cx = GlobalScope::get_cx();
165    unsafe {
166        if message.is_string() {
167            let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
168            return DOMString::from_string(jsstr_to_string(*cx, jsstr));
169        }
170        unsafe fn stringify_object_from_handle_value(
171            cx: *mut jsapi::JSContext,
172            value: HandleValue,
173            parents: Vec<u64>,
174        ) -> DOMString {
175            rooted!(in(cx) let mut obj = value.to_object());
176            let mut object_class = ESClass::Other;
177            if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
178                return DOMString::from("/* invalid */");
179            }
180            let mut ids = unsafe { IdVector::new(cx) };
181            if !unsafe {
182                GetPropertyKeys(
183                    cx,
184                    obj.handle(),
185                    jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
186                    ids.handle_mut(),
187                )
188            } {
189                return DOMString::from("/* invalid */");
190            }
191            let truncate = ids.len() > MAX_LOG_CHILDREN;
192            if object_class != ESClass::Array && object_class != ESClass::Object {
193                if truncate {
194                    return DOMString::from("…");
195                } else {
196                    return unsafe { handle_value_to_string(cx, value) };
197                }
198            }
199
200            let mut explicit_keys = object_class == ESClass::Object;
201            let mut props = Vec::with_capacity(ids.len());
202            for id in ids.iter().take(MAX_LOG_CHILDREN) {
203                rooted!(in(cx) let id = *id);
204                rooted!(in(cx) let mut desc = PropertyDescriptor::default());
205
206                let mut is_none = false;
207                if !unsafe {
208                    JS_GetOwnPropertyDescriptorById(
209                        cx,
210                        obj.handle(),
211                        id.handle(),
212                        desc.handle_mut(),
213                        &mut is_none,
214                    )
215                } {
216                    return DOMString::from("/* invalid */");
217                }
218
219                rooted!(in(cx) let mut property = UndefinedValue());
220                if !unsafe {
221                    JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut())
222                } {
223                    return DOMString::from("/* invalid */");
224                }
225
226                if !explicit_keys {
227                    if id.is_int() {
228                        if let Ok(id_int) = usize::try_from(id.to_int()) {
229                            explicit_keys = props.len() != id_int;
230                        } else {
231                            explicit_keys = false;
232                        }
233                    } else {
234                        explicit_keys = false;
235                    }
236                }
237                let value_string = stringify_inner(
238                    unsafe { JSContext::from_ptr(cx) },
239                    property.handle(),
240                    parents.clone(),
241                );
242                if explicit_keys {
243                    let key = if id.is_string() || id.is_symbol() || id.is_int() {
244                        rooted!(in(cx) let mut key_value = UndefinedValue());
245                        let raw_id: jsapi::HandleId = id.handle().into();
246                        if !unsafe { JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) } {
247                            return DOMString::from("/* invalid */");
248                        }
249                        unsafe { handle_value_to_string(cx, key_value.handle()) }
250                    } else {
251                        return DOMString::from("/* invalid */");
252                    };
253                    props.push(format!("{}: {}", key, value_string,));
254                } else {
255                    props.push(value_string.to_string());
256                }
257            }
258            if truncate {
259                props.push("…".to_string());
260            }
261            if object_class == ESClass::Array {
262                DOMString::from(format!("[{}]", itertools::join(props, ", ")))
263            } else {
264                DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
265            }
266        }
267        fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
268            if parents.len() >= MAX_LOG_DEPTH {
269                return DOMString::from("...");
270            }
271            let value_bits = value.asBits_;
272            if parents.contains(&value_bits) {
273                return DOMString::from("[circular]");
274            }
275            if value.is_undefined() {
276                // This produces a better value than "(void 0)" from JS_ValueToSource.
277                return DOMString::from("undefined");
278            } else if !value.is_object() {
279                return unsafe { handle_value_to_string(*cx, value) };
280            }
281            parents.push(value_bits);
282
283            if value.is_object() {
284                if let Some(repr) = maybe_stringify_dom_object(cx, value) {
285                    return repr;
286                }
287            }
288            unsafe { stringify_object_from_handle_value(*cx, value, parents) }
289        }
290        stringify_inner(cx, message, Vec::new())
291    }
292}
293
294#[expect(unsafe_code)]
295fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
296    // The standard object serialization is not effective for DOM objects,
297    // since their properties generally live on the prototype object.
298    // Instead, fall back to the output of JSON.stringify combined
299    // with the class name extracted from the output of toString().
300    rooted!(in(*cx) let obj = value.to_object());
301    let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
302    if !is_dom_class {
303        return None;
304    }
305    rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
306    let Some(class_name) = NonNull::new(class_name.get()) else {
307        return Some("<error converting DOM object to string>".into());
308    };
309    let class_name = unsafe {
310        jsstr_to_string(*cx, class_name)
311            .replace("[object ", "")
312            .replace("]", "")
313    };
314    let mut repr = format!("{} ", class_name);
315    rooted!(in(*cx) let mut value = value.get());
316
317    #[expect(unsafe_code)]
318    unsafe extern "C" fn stringified(
319        string: *const u16,
320        len: u32,
321        data: *mut std::ffi::c_void,
322    ) -> bool {
323        let s = data as *mut String;
324        let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
325        unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
326        true
327    }
328
329    rooted!(in(*cx) let space = Int32Value(2));
330    let stringify_result = unsafe {
331        JS_Stringify(
332            *cx,
333            value.handle_mut(),
334            HandleObject::null(),
335            space.handle(),
336            Some(stringified),
337            &mut repr as *mut String as *mut _,
338        )
339    };
340    if !stringify_result {
341        return Some("<error converting DOM object to string>".into());
342    }
343    Some(repr.into())
344}
345
346fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
347    DOMString::from(itertools::join(
348        messages.iter().copied().map(stringify_handle_value),
349        " ",
350    ))
351}
352
353#[derive(Debug, Eq, PartialEq)]
354enum IncludeStackTrace {
355    Yes,
356    No,
357}
358
359impl consoleMethods<crate::DomTypeHolder> for Console {
360    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/log>
361    fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
362        Console::method(
363            global,
364            ConsoleLogLevel::Log,
365            messages,
366            IncludeStackTrace::No,
367        );
368    }
369
370    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/clear>
371    fn Clear(global: &GlobalScope) {
372        if let Some(chan) = global.devtools_chan() {
373            let worker_id = global
374                .downcast::<WorkerGlobalScope>()
375                .map(|worker| worker.worker_id());
376            let devtools_message =
377                ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
378            if let Err(error) = chan.send(devtools_message) {
379                log::warn!("Error sending clear message to devtools: {error:?}");
380            }
381        }
382    }
383
384    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
385    fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
386        Console::method(
387            global,
388            ConsoleLogLevel::Debug,
389            messages,
390            IncludeStackTrace::No,
391        );
392    }
393
394    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/info>
395    fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
396        Console::method(
397            global,
398            ConsoleLogLevel::Info,
399            messages,
400            IncludeStackTrace::No,
401        );
402    }
403
404    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/warn>
405    fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
406        Console::method(
407            global,
408            ConsoleLogLevel::Warn,
409            messages,
410            IncludeStackTrace::No,
411        );
412    }
413
414    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/error>
415    fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
416        Console::method(
417            global,
418            ConsoleLogLevel::Error,
419            messages,
420            IncludeStackTrace::No,
421        );
422    }
423
424    /// <https://console.spec.whatwg.org/#trace>
425    fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
426        Console::method(
427            global,
428            ConsoleLogLevel::Trace,
429            messages,
430            IncludeStackTrace::Yes,
431        );
432    }
433
434    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/assert>
435    fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
436        if !condition {
437            let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
438
439            Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
440        }
441    }
442
443    /// <https://console.spec.whatwg.org/#time>
444    fn Time(global: &GlobalScope, label: DOMString) {
445        if let Ok(()) = global.time(label.clone()) {
446            let message = format!("{label}: timer started");
447            Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
448        }
449    }
450
451    /// <https://console.spec.whatwg.org/#timelog>
452    fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
453        if let Ok(delta) = global.time_log(&label) {
454            let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
455
456            Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
457        }
458    }
459
460    /// <https://console.spec.whatwg.org/#timeend>
461    fn TimeEnd(global: &GlobalScope, label: DOMString) {
462        if let Ok(delta) = global.time_end(&label) {
463            let message = format!("{label}: {delta}ms");
464
465            Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
466        }
467    }
468
469    /// <https://console.spec.whatwg.org/#group>
470    fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
471        global.push_console_group(stringify_handle_values(&messages));
472    }
473
474    /// <https://console.spec.whatwg.org/#groupcollapsed>
475    fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
476        global.push_console_group(stringify_handle_values(&messages));
477    }
478
479    /// <https://console.spec.whatwg.org/#groupend>
480    fn GroupEnd(global: &GlobalScope) {
481        global.pop_console_group();
482    }
483
484    /// <https://console.spec.whatwg.org/#count>
485    fn Count(global: &GlobalScope, label: DOMString) {
486        let count = global.increment_console_count(&label);
487        let message = format!("{label}: {count}");
488
489        Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
490    }
491
492    /// <https://console.spec.whatwg.org/#countreset>
493    fn CountReset(global: &GlobalScope, label: DOMString) {
494        if global.reset_console_count(&label).is_err() {
495            Self::internal_warn(
496                global,
497                DOMString::from(format!("Counter “{label}” doesn’t exist.")),
498            )
499        }
500    }
501}
502
503#[expect(unsafe_code)]
504fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
505    const MAX_FRAME_COUNT: u32 = 128;
506
507    let mut frames = vec![];
508    rooted!(in(cx) let mut handle =  ptr::null_mut());
509    let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
510    let Some(captured_js_stack) = captured_js_stack else {
511        return frames;
512    };
513
514    captured_js_stack.for_each_stack_frame(|frame| {
515        rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
516
517        // Get function name
518        unsafe {
519            jsapi::GetSavedFrameFunctionDisplayName(
520                cx,
521                ptr::null_mut(),
522                frame.into(),
523                result.handle_mut().into(),
524                jsapi::SavedFrameSelfHosted::Include,
525            );
526        }
527        let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
528            unsafe { jsstr_to_string(cx, nonnull_result) }
529        } else {
530            "<anonymous>".into()
531        };
532
533        // Get source file name
534        result.set(ptr::null_mut());
535        unsafe {
536            jsapi::GetSavedFrameSource(
537                cx,
538                ptr::null_mut(),
539                frame.into(),
540                result.handle_mut().into(),
541                jsapi::SavedFrameSelfHosted::Include,
542            );
543        }
544        let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
545            unsafe { jsstr_to_string(cx, nonnull_result) }
546        } else {
547            "<anonymous>".into()
548        };
549
550        // get line/column number
551        let mut line_number = 0;
552        unsafe {
553            jsapi::GetSavedFrameLine(
554                cx,
555                ptr::null_mut(),
556                frame.into(),
557                &mut line_number,
558                jsapi::SavedFrameSelfHosted::Include,
559            );
560        }
561
562        let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
563        unsafe {
564            jsapi::GetSavedFrameColumn(
565                cx,
566                ptr::null_mut(),
567                frame.into(),
568                &mut column_number,
569                jsapi::SavedFrameSelfHosted::Include,
570            );
571        }
572        let frame = StackFrame {
573            filename,
574            function_name,
575            line_number,
576            column_number: column_number.value_,
577        };
578
579        frames.push(frame);
580    });
581
582    frames
583}