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