script/
devtools.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::collections::HashMap;
6use std::rc::Rc;
7use std::str;
8
9use base::id::PipelineId;
10use devtools_traits::{
11    AttrModification, AutoMargins, ComputedNodeLayout, CssDatabaseProperty, EvaluateJSReply,
12    NodeInfo, NodeStyle, RuleModification, TimelineMarker, TimelineMarkerType,
13};
14use ipc_channel::ipc::IpcSender;
15use js::conversions::jsstr_to_string;
16use js::jsval::UndefinedValue;
17use js::rust::ToString;
18use markup5ever::{LocalName, ns};
19use servo_config::pref;
20use style::attr::AttrValue;
21use uuid::Uuid;
22
23use crate::document_collection::DocumentCollection;
24use crate::dom::bindings::codegen::Bindings::CSSRuleListBinding::CSSRuleListMethods;
25use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
26use crate::dom::bindings::codegen::Bindings::CSSStyleRuleBinding::CSSStyleRuleMethods;
27use crate::dom::bindings::codegen::Bindings::CSSStyleSheetBinding::CSSStyleSheetMethods;
28use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
29use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
30use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
31use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
32use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants;
33use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
34use crate::dom::bindings::conversions::{ConversionResult, FromJSValConvertible};
35use crate::dom::bindings::inheritance::Castable;
36use crate::dom::bindings::root::DomRoot;
37use crate::dom::bindings::str::DOMString;
38use crate::dom::css::cssstyledeclaration::ENABLED_LONGHAND_PROPERTIES;
39use crate::dom::css::cssstylerule::CSSStyleRule;
40use crate::dom::document::AnimationFrameCallback;
41use crate::dom::element::Element;
42use crate::dom::globalscope::GlobalScope;
43use crate::dom::html::htmlscriptelement::SourceCode;
44use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
45use crate::dom::types::HTMLElement;
46use crate::realms::enter_realm;
47use crate::script_module::ScriptFetchOptions;
48use crate::script_runtime::{CanGc, IntroductionType};
49
50#[allow(unsafe_code)]
51pub(crate) fn handle_evaluate_js(
52    global: &GlobalScope,
53    eval: String,
54    reply: IpcSender<EvaluateJSReply>,
55    can_gc: CanGc,
56) {
57    // global.get_cx() returns a valid `JSContext` pointer, so this is safe.
58    let result = unsafe {
59        let cx = GlobalScope::get_cx();
60        let _ac = enter_realm(global);
61        rooted!(in(*cx) let mut rval = UndefinedValue());
62        let source_code = SourceCode::Text(Rc::new(DOMString::from_string(eval)));
63        // TODO: run code with SpiderMonkey Debugger API, like Firefox does
64        // <https://searchfox.org/mozilla-central/rev/f6a806c38c459e0e0d797d264ca0e8ad46005105/devtools/server/actors/webconsole/eval-with-debugger.js#270>
65        _ = global.evaluate_script_on_global_with_result(
66            &source_code,
67            "<eval>",
68            rval.handle_mut(),
69            1,
70            ScriptFetchOptions::default_classic_script(global),
71            global.api_base_url(),
72            can_gc,
73            Some(IntroductionType::DEBUGGER_EVAL),
74        );
75
76        if rval.is_undefined() {
77            EvaluateJSReply::VoidValue
78        } else if rval.is_boolean() {
79            EvaluateJSReply::BooleanValue(rval.to_boolean())
80        } else if rval.is_double() || rval.is_int32() {
81            EvaluateJSReply::NumberValue(
82                match FromJSValConvertible::from_jsval(*cx, rval.handle(), ()) {
83                    Ok(ConversionResult::Success(v)) => v,
84                    _ => unreachable!(),
85                },
86            )
87        } else if rval.is_string() {
88            let jsstr = std::ptr::NonNull::new(rval.to_string()).unwrap();
89            EvaluateJSReply::StringValue(jsstr_to_string(*cx, jsstr))
90        } else if rval.is_null() {
91            EvaluateJSReply::NullValue
92        } else {
93            assert!(rval.is_object());
94
95            let jsstr = std::ptr::NonNull::new(ToString(*cx, rval.handle())).unwrap();
96            let class_name = jsstr_to_string(*cx, jsstr);
97
98            EvaluateJSReply::ActorValue {
99                class: class_name,
100                uuid: Uuid::new_v4().to_string(),
101            }
102        }
103    };
104    reply.send(result).unwrap();
105}
106
107pub(crate) fn handle_get_root_node(
108    documents: &DocumentCollection,
109    pipeline: PipelineId,
110    reply: IpcSender<Option<NodeInfo>>,
111    can_gc: CanGc,
112) {
113    let info = documents
114        .find_document(pipeline)
115        .map(|document| document.upcast::<Node>().summarize(can_gc));
116    reply.send(info).unwrap();
117}
118
119pub(crate) fn handle_get_document_element(
120    documents: &DocumentCollection,
121    pipeline: PipelineId,
122    reply: IpcSender<Option<NodeInfo>>,
123    can_gc: CanGc,
124) {
125    let info = documents
126        .find_document(pipeline)
127        .and_then(|document| document.GetDocumentElement())
128        .map(|element| element.upcast::<Node>().summarize(can_gc));
129    reply.send(info).unwrap();
130}
131
132fn find_node_by_unique_id(
133    documents: &DocumentCollection,
134    pipeline: PipelineId,
135    node_id: &str,
136) -> Option<DomRoot<Node>> {
137    documents.find_document(pipeline).and_then(|document| {
138        document
139            .upcast::<Node>()
140            .traverse_preorder(ShadowIncluding::Yes)
141            .find(|candidate| candidate.unique_id(pipeline) == node_id)
142    })
143}
144
145pub(crate) fn handle_get_children(
146    documents: &DocumentCollection,
147    pipeline: PipelineId,
148    node_id: String,
149    reply: IpcSender<Option<Vec<NodeInfo>>>,
150    can_gc: CanGc,
151) {
152    match find_node_by_unique_id(documents, pipeline, &node_id) {
153        None => reply.send(None).unwrap(),
154        Some(parent) => {
155            let is_whitespace = |node: &NodeInfo| {
156                node.node_type == NodeConstants::TEXT_NODE &&
157                    node.node_value.as_ref().is_none_or(|v| v.trim().is_empty())
158            };
159
160            let inline: Vec<_> = parent
161                .children()
162                .map(|child| {
163                    let window = child.owner_window();
164                    let Some(elem) = child.downcast::<Element>() else {
165                        return false;
166                    };
167                    let computed_style = window.GetComputedStyle(elem, None);
168                    let display = computed_style.Display();
169                    display == "inline"
170                })
171                .collect();
172
173            let mut children = vec![];
174            if let Some(shadow_root) = parent.downcast::<Element>().and_then(Element::shadow_root) {
175                if !shadow_root.is_user_agent_widget() ||
176                    pref!(inspector_show_servo_internal_shadow_roots)
177                {
178                    children.push(shadow_root.upcast::<Node>().summarize(can_gc));
179                }
180            }
181            let children_iter = parent.children().enumerate().filter_map(|(i, child)| {
182                // Filter whitespace only text nodes that are not inline level
183                // https://firefox-source-docs.mozilla.org/devtools-user/page_inspector/how_to/examine_and_edit_html/index.html#whitespace-only-text-nodes
184                let prev_inline = i > 0 && inline[i - 1];
185                let next_inline = i < inline.len() - 1 && inline[i + 1];
186
187                let info = child.summarize(can_gc);
188                if !is_whitespace(&info) {
189                    return Some(info);
190                }
191
192                (prev_inline && next_inline).then_some(info)
193            });
194            children.extend(children_iter);
195
196            reply.send(Some(children)).unwrap();
197        },
198    };
199}
200
201pub(crate) fn handle_get_attribute_style(
202    documents: &DocumentCollection,
203    pipeline: PipelineId,
204    node_id: String,
205    reply: IpcSender<Option<Vec<NodeStyle>>>,
206    can_gc: CanGc,
207) {
208    let node = match find_node_by_unique_id(documents, pipeline, &node_id) {
209        None => return reply.send(None).unwrap(),
210        Some(found_node) => found_node,
211    };
212
213    let Some(elem) = node.downcast::<HTMLElement>() else {
214        // the style attribute only works on html elements
215        reply.send(None).unwrap();
216        return;
217    };
218    let style = elem.Style(can_gc);
219
220    let msg = (0..style.Length())
221        .map(|i| {
222            let name = style.Item(i);
223            NodeStyle {
224                name: name.to_string(),
225                value: style.GetPropertyValue(name.clone()).to_string(),
226                priority: style.GetPropertyPriority(name).to_string(),
227            }
228        })
229        .collect();
230
231    reply.send(Some(msg)).unwrap();
232}
233
234#[cfg_attr(crown, allow(crown::unrooted_must_root))]
235pub(crate) fn handle_get_stylesheet_style(
236    documents: &DocumentCollection,
237    pipeline: PipelineId,
238    node_id: String,
239    selector: String,
240    stylesheet: usize,
241    reply: IpcSender<Option<Vec<NodeStyle>>>,
242    can_gc: CanGc,
243) {
244    let msg = (|| {
245        let node = find_node_by_unique_id(documents, pipeline, &node_id)?;
246
247        let document = documents.find_document(pipeline)?;
248        let _realm = enter_realm(document.window());
249        let owner = node.stylesheet_list_owner();
250
251        let stylesheet = owner.stylesheet_at(stylesheet)?;
252        let list = stylesheet.GetCssRules(can_gc).ok()?;
253
254        let styles = (0..list.Length())
255            .filter_map(move |i| {
256                let rule = list.Item(i, can_gc)?;
257                let style = rule.downcast::<CSSStyleRule>()?;
258                if selector != style.SelectorText() {
259                    return None;
260                };
261                Some(style.Style(can_gc))
262            })
263            .flat_map(|style| {
264                (0..style.Length()).map(move |i| {
265                    let name = style.Item(i);
266                    NodeStyle {
267                        name: name.to_string(),
268                        value: style.GetPropertyValue(name.clone()).to_string(),
269                        priority: style.GetPropertyPriority(name).to_string(),
270                    }
271                })
272            })
273            .collect();
274
275        Some(styles)
276    })();
277
278    reply.send(msg).unwrap();
279}
280
281#[cfg_attr(crown, allow(crown::unrooted_must_root))]
282pub(crate) fn handle_get_selectors(
283    documents: &DocumentCollection,
284    pipeline: PipelineId,
285    node_id: String,
286    reply: IpcSender<Option<Vec<(String, usize)>>>,
287    can_gc: CanGc,
288) {
289    let msg = (|| {
290        let node = find_node_by_unique_id(documents, pipeline, &node_id)?;
291
292        let document = documents.find_document(pipeline)?;
293        let _realm = enter_realm(document.window());
294        let owner = node.stylesheet_list_owner();
295
296        let rules = (0..owner.stylesheet_count())
297            .filter_map(|i| {
298                let stylesheet = owner.stylesheet_at(i)?;
299                let list = stylesheet.GetCssRules(can_gc).ok()?;
300                let elem = node.downcast::<Element>()?;
301
302                Some((0..list.Length()).filter_map(move |j| {
303                    let rule = list.Item(j, can_gc)?;
304                    let style = rule.downcast::<CSSStyleRule>()?;
305                    let selector = style.SelectorText();
306                    elem.Matches(selector.clone()).ok()?.then_some(())?;
307                    Some((selector.into(), i))
308                }))
309            })
310            .flatten()
311            .collect();
312
313        Some(rules)
314    })();
315
316    reply.send(msg).unwrap();
317}
318
319pub(crate) fn handle_get_computed_style(
320    documents: &DocumentCollection,
321    pipeline: PipelineId,
322    node_id: String,
323    reply: IpcSender<Option<Vec<NodeStyle>>>,
324) {
325    let node = match find_node_by_unique_id(documents, pipeline, &node_id) {
326        None => return reply.send(None).unwrap(),
327        Some(found_node) => found_node,
328    };
329
330    let window = node.owner_window();
331    let elem = node
332        .downcast::<Element>()
333        .expect("This should be an element");
334    let computed_style = window.GetComputedStyle(elem, None);
335
336    let msg = (0..computed_style.Length())
337        .map(|i| {
338            let name = computed_style.Item(i);
339            NodeStyle {
340                name: name.to_string(),
341                value: computed_style.GetPropertyValue(name.clone()).to_string(),
342                priority: computed_style.GetPropertyPriority(name).to_string(),
343            }
344        })
345        .collect();
346
347    reply.send(Some(msg)).unwrap();
348}
349
350pub(crate) fn handle_get_layout(
351    documents: &DocumentCollection,
352    pipeline: PipelineId,
353    node_id: String,
354    reply: IpcSender<Option<ComputedNodeLayout>>,
355    can_gc: CanGc,
356) {
357    let node = match find_node_by_unique_id(documents, pipeline, &node_id) {
358        None => return reply.send(None).unwrap(),
359        Some(found_node) => found_node,
360    };
361
362    let elem = node
363        .downcast::<Element>()
364        .expect("should be getting layout of element");
365    let rect = elem.GetBoundingClientRect(can_gc);
366    let width = rect.Width() as f32;
367    let height = rect.Height() as f32;
368
369    let window = node.owner_window();
370    let elem = node
371        .downcast::<Element>()
372        .expect("should be getting layout of element");
373    let computed_style = window.GetComputedStyle(elem, None);
374
375    reply
376        .send(Some(ComputedNodeLayout {
377            display: String::from(computed_style.Display()),
378            position: String::from(computed_style.Position()),
379            z_index: String::from(computed_style.ZIndex()),
380            box_sizing: String::from(computed_style.BoxSizing()),
381            auto_margins: determine_auto_margins(&node),
382            margin_top: String::from(computed_style.MarginTop()),
383            margin_right: String::from(computed_style.MarginRight()),
384            margin_bottom: String::from(computed_style.MarginBottom()),
385            margin_left: String::from(computed_style.MarginLeft()),
386            border_top_width: String::from(computed_style.BorderTopWidth()),
387            border_right_width: String::from(computed_style.BorderRightWidth()),
388            border_bottom_width: String::from(computed_style.BorderBottomWidth()),
389            border_left_width: String::from(computed_style.BorderLeftWidth()),
390            padding_top: String::from(computed_style.PaddingTop()),
391            padding_right: String::from(computed_style.PaddingRight()),
392            padding_bottom: String::from(computed_style.PaddingBottom()),
393            padding_left: String::from(computed_style.PaddingLeft()),
394            width,
395            height,
396        }))
397        .unwrap();
398}
399
400pub(crate) fn handle_get_xpath(
401    documents: &DocumentCollection,
402    pipeline: PipelineId,
403    node_id: String,
404    reply: IpcSender<String>,
405) {
406    let Some(node) = find_node_by_unique_id(documents, pipeline, &node_id) else {
407        return reply.send(Default::default()).unwrap();
408    };
409
410    let selector = node
411        .inclusive_ancestors(ShadowIncluding::Yes)
412        .filter_map(|ancestor| {
413            let Some(element) = ancestor.downcast::<Element>() else {
414                // TODO: figure out how to handle shadow roots here
415                return None;
416            };
417
418            let mut result = "/".to_owned();
419            if *element.namespace() != ns!(html) {
420                result.push_str(element.namespace());
421                result.push(':');
422            }
423
424            result.push_str(element.local_name());
425
426            let would_node_also_match_selector = |sibling: &Node| {
427                let Some(sibling) = sibling.downcast::<Element>() else {
428                    return false;
429                };
430                sibling.namespace() == element.namespace() &&
431                    sibling.local_name() == element.local_name()
432            };
433
434            let matching_elements_before = ancestor
435                .preceding_siblings()
436                .filter(|node| would_node_also_match_selector(node))
437                .count();
438            let matching_elements_after = ancestor
439                .following_siblings()
440                .filter(|node| would_node_also_match_selector(node))
441                .count();
442
443            if matching_elements_before + matching_elements_after != 0 {
444                // Need to add an index (note that XPath uses 1-based indexing)
445                result.push_str(&format!("[{}]", matching_elements_before + 1));
446            }
447
448            Some(result)
449        })
450        .collect::<Vec<_>>()
451        .into_iter()
452        .rev()
453        .collect::<Vec<_>>()
454        .join("");
455
456    reply.send(selector).unwrap();
457}
458
459fn determine_auto_margins(node: &Node) -> AutoMargins {
460    let style = node.style().unwrap();
461    let margin = style.get_margin();
462    AutoMargins {
463        top: margin.margin_top.is_auto(),
464        right: margin.margin_right.is_auto(),
465        bottom: margin.margin_bottom.is_auto(),
466        left: margin.margin_left.is_auto(),
467    }
468}
469
470pub(crate) fn handle_modify_attribute(
471    documents: &DocumentCollection,
472    pipeline: PipelineId,
473    node_id: String,
474    modifications: Vec<AttrModification>,
475    can_gc: CanGc,
476) {
477    let Some(document) = documents.find_document(pipeline) else {
478        return warn!("document for pipeline id {} is not found", &pipeline);
479    };
480    let _realm = enter_realm(document.window());
481
482    let node = match find_node_by_unique_id(documents, pipeline, &node_id) {
483        None => {
484            return warn!(
485                "node id {} for pipeline id {} is not found",
486                &node_id, &pipeline
487            );
488        },
489        Some(found_node) => found_node,
490    };
491
492    let elem = node
493        .downcast::<Element>()
494        .expect("should be getting layout of element");
495
496    for modification in modifications {
497        match modification.new_value {
498            Some(string) => {
499                elem.set_attribute(
500                    &LocalName::from(modification.attribute_name),
501                    AttrValue::String(string),
502                    can_gc,
503                );
504            },
505            None => elem.RemoveAttribute(DOMString::from(modification.attribute_name), can_gc),
506        }
507    }
508}
509
510pub(crate) fn handle_modify_rule(
511    documents: &DocumentCollection,
512    pipeline: PipelineId,
513    node_id: String,
514    modifications: Vec<RuleModification>,
515    can_gc: CanGc,
516) {
517    let Some(document) = documents.find_document(pipeline) else {
518        return warn!("Document for pipeline id {} is not found", &pipeline);
519    };
520    let _realm = enter_realm(document.window());
521
522    let Some(node) = find_node_by_unique_id(documents, pipeline, &node_id) else {
523        return warn!(
524            "Node id {} for pipeline id {} is not found",
525            &node_id, &pipeline
526        );
527    };
528
529    let elem = node
530        .downcast::<HTMLElement>()
531        .expect("This should be an HTMLElement");
532    let style = elem.Style(can_gc);
533
534    for modification in modifications {
535        let _ = style.SetProperty(
536            modification.name.into(),
537            modification.value.into(),
538            modification.priority.into(),
539            can_gc,
540        );
541    }
542}
543
544pub(crate) fn handle_wants_live_notifications(global: &GlobalScope, send_notifications: bool) {
545    global.set_devtools_wants_updates(send_notifications);
546}
547
548pub(crate) fn handle_set_timeline_markers(
549    documents: &DocumentCollection,
550    pipeline: PipelineId,
551    marker_types: Vec<TimelineMarkerType>,
552    reply: IpcSender<Option<TimelineMarker>>,
553) {
554    match documents.find_window(pipeline) {
555        None => reply.send(None).unwrap(),
556        Some(window) => window.set_devtools_timeline_markers(marker_types, reply),
557    }
558}
559
560pub(crate) fn handle_drop_timeline_markers(
561    documents: &DocumentCollection,
562    pipeline: PipelineId,
563    marker_types: Vec<TimelineMarkerType>,
564) {
565    if let Some(window) = documents.find_window(pipeline) {
566        window.drop_devtools_timeline_markers(marker_types);
567    }
568}
569
570pub(crate) fn handle_request_animation_frame(
571    documents: &DocumentCollection,
572    id: PipelineId,
573    actor_name: String,
574) {
575    if let Some(doc) = documents.find_document(id) {
576        doc.request_animation_frame(AnimationFrameCallback::DevtoolsFramerateTick { actor_name });
577    }
578}
579
580pub(crate) fn handle_reload(documents: &DocumentCollection, id: PipelineId, can_gc: CanGc) {
581    if let Some(win) = documents.find_window(id) {
582        win.Location().reload_without_origin_check(can_gc);
583    }
584}
585
586pub(crate) fn handle_get_css_database(reply: IpcSender<HashMap<String, CssDatabaseProperty>>) {
587    let database: HashMap<_, _> = ENABLED_LONGHAND_PROPERTIES
588        .iter()
589        .map(|l| {
590            (
591                l.name().into(),
592                CssDatabaseProperty {
593                    is_inherited: l.inherited(),
594                    values: vec![], // TODO: Get allowed values for each property
595                    supports: vec![],
596                    subproperties: vec![l.name().into()],
597                },
598            )
599        })
600        .collect();
601    let _ = reply.send(database);
602}
603
604pub(crate) fn handle_highlight_dom_node(
605    documents: &DocumentCollection,
606    id: PipelineId,
607    node_id: Option<String>,
608) {
609    let node = node_id.and_then(|node_id| {
610        let node = find_node_by_unique_id(documents, id, &node_id);
611        if node.is_none() {
612            log::warn!("Node id {node_id} for pipeline id {id} is not found",);
613        }
614        node
615    });
616
617    if let Some(window) = documents.find_window(id) {
618        window.Document().highlight_dom_node(node.as_deref());
619    }
620}