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 html5ever::LocalName;
15use ipc_channel::ipc::IpcSender;
16use js::conversions::jsstr_to_string;
17use js::jsval::UndefinedValue;
18use js::rust::ToString;
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::cssstyledeclaration::ENABLED_LONGHAND_PROPERTIES;
39use crate::dom::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
400fn determine_auto_margins(node: &Node) -> AutoMargins {
401    let style = node.style().unwrap();
402    let margin = style.get_margin();
403    AutoMargins {
404        top: margin.margin_top.is_auto(),
405        right: margin.margin_right.is_auto(),
406        bottom: margin.margin_bottom.is_auto(),
407        left: margin.margin_left.is_auto(),
408    }
409}
410
411pub(crate) fn handle_modify_attribute(
412    documents: &DocumentCollection,
413    pipeline: PipelineId,
414    node_id: String,
415    modifications: Vec<AttrModification>,
416    can_gc: CanGc,
417) {
418    let Some(document) = documents.find_document(pipeline) else {
419        return warn!("document for pipeline id {} is not found", &pipeline);
420    };
421    let _realm = enter_realm(document.window());
422
423    let node = match find_node_by_unique_id(documents, pipeline, &node_id) {
424        None => {
425            return warn!(
426                "node id {} for pipeline id {} is not found",
427                &node_id, &pipeline
428            );
429        },
430        Some(found_node) => found_node,
431    };
432
433    let elem = node
434        .downcast::<Element>()
435        .expect("should be getting layout of element");
436
437    for modification in modifications {
438        match modification.new_value {
439            Some(string) => {
440                elem.set_attribute(
441                    &LocalName::from(modification.attribute_name),
442                    AttrValue::String(string),
443                    can_gc,
444                );
445            },
446            None => elem.RemoveAttribute(DOMString::from(modification.attribute_name), can_gc),
447        }
448    }
449}
450
451pub(crate) fn handle_modify_rule(
452    documents: &DocumentCollection,
453    pipeline: PipelineId,
454    node_id: String,
455    modifications: Vec<RuleModification>,
456    can_gc: CanGc,
457) {
458    let Some(document) = documents.find_document(pipeline) else {
459        return warn!("Document for pipeline id {} is not found", &pipeline);
460    };
461    let _realm = enter_realm(document.window());
462
463    let Some(node) = find_node_by_unique_id(documents, pipeline, &node_id) else {
464        return warn!(
465            "Node id {} for pipeline id {} is not found",
466            &node_id, &pipeline
467        );
468    };
469
470    let elem = node
471        .downcast::<HTMLElement>()
472        .expect("This should be an HTMLElement");
473    let style = elem.Style(can_gc);
474
475    for modification in modifications {
476        let _ = style.SetProperty(
477            modification.name.into(),
478            modification.value.into(),
479            modification.priority.into(),
480            can_gc,
481        );
482    }
483}
484
485pub(crate) fn handle_wants_live_notifications(global: &GlobalScope, send_notifications: bool) {
486    global.set_devtools_wants_updates(send_notifications);
487}
488
489pub(crate) fn handle_set_timeline_markers(
490    documents: &DocumentCollection,
491    pipeline: PipelineId,
492    marker_types: Vec<TimelineMarkerType>,
493    reply: IpcSender<Option<TimelineMarker>>,
494) {
495    match documents.find_window(pipeline) {
496        None => reply.send(None).unwrap(),
497        Some(window) => window.set_devtools_timeline_markers(marker_types, reply),
498    }
499}
500
501pub(crate) fn handle_drop_timeline_markers(
502    documents: &DocumentCollection,
503    pipeline: PipelineId,
504    marker_types: Vec<TimelineMarkerType>,
505) {
506    if let Some(window) = documents.find_window(pipeline) {
507        window.drop_devtools_timeline_markers(marker_types);
508    }
509}
510
511pub(crate) fn handle_request_animation_frame(
512    documents: &DocumentCollection,
513    id: PipelineId,
514    actor_name: String,
515) {
516    if let Some(doc) = documents.find_document(id) {
517        doc.request_animation_frame(AnimationFrameCallback::DevtoolsFramerateTick { actor_name });
518    }
519}
520
521pub(crate) fn handle_reload(documents: &DocumentCollection, id: PipelineId, can_gc: CanGc) {
522    if let Some(win) = documents.find_window(id) {
523        win.Location().reload_without_origin_check(can_gc);
524    }
525}
526
527pub(crate) fn handle_get_css_database(reply: IpcSender<HashMap<String, CssDatabaseProperty>>) {
528    let database: HashMap<_, _> = ENABLED_LONGHAND_PROPERTIES
529        .iter()
530        .map(|l| {
531            (
532                l.name().into(),
533                CssDatabaseProperty {
534                    is_inherited: l.inherited(),
535                    values: vec![], // TODO: Get allowed values for each property
536                    supports: vec![],
537                    subproperties: vec![l.name().into()],
538                },
539            )
540        })
541        .collect();
542    let _ = reply.send(database);
543}
544
545pub(crate) fn handle_highlight_dom_node(
546    documents: &DocumentCollection,
547    id: PipelineId,
548    node_id: Option<String>,
549) {
550    let node = node_id.and_then(|node_id| {
551        let node = find_node_by_unique_id(documents, id, &node_id);
552        if node.is_none() {
553            log::warn!("Node id {node_id} for pipeline id {id} is not found",);
554        }
555        node
556    });
557
558    if let Some(window) = documents.find_window(id) {
559        window.Document().highlight_dom_node(node.as_deref());
560    }
561}