| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.h" |
| |
| #include "third_party/blink/renderer/bindings/core/v8/script_event_listener.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" |
| #include "third_party/blink/renderer/core/css/css_computed_style_declaration.h" |
| #include "third_party/blink/renderer/core/dom/attribute.h" |
| #include "third_party/blink/renderer/core/dom/attribute_collection.h" |
| #include "third_party/blink/renderer/core/dom/character_data.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/document_type.h" |
| #include "third_party/blink/renderer/core/dom/dom_node_ids.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/node.h" |
| #include "third_party/blink/renderer/core/dom/pseudo_element.h" |
| #include "third_party/blink/renderer/core/dom/qualified_name.h" |
| #include "third_party/blink/renderer/core/dom/shadow_root.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/html/forms/html_input_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_option_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_text_area_element.h" |
| #include "third_party/blink/renderer/core/html/html_frame_owner_element.h" |
| #include "third_party/blink/renderer/core/html/html_image_element.h" |
| #include "third_party/blink/renderer/core/html/html_link_element.h" |
| #include "third_party/blink/renderer/core/html/html_template_element.h" |
| #include "third_party/blink/renderer/core/input_type_names.h" |
| #include "third_party/blink/renderer/core/inspector/dom_traversal_utils.h" |
| #include "third_party/blink/renderer/core/inspector/identifiers_factory.h" |
| #include "third_party/blink/renderer/core/inspector/inspector_dom_agent.h" |
| #include "third_party/blink/renderer/core/inspector/inspector_dom_debugger_agent.h" |
| #include "third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h" |
| #include "third_party/blink/renderer/core/inspector/thread_debugger.h" |
| #include "third_party/blink/renderer/core/layout/layout_embedded_content.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_text.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" |
| #include "v8/include/v8-inspector.h" |
| |
| namespace blink { |
| using protocol::Maybe; |
| using protocol::Response; |
| |
| namespace { |
| |
| std::unique_ptr<protocol::DOM::Rect> LegacyBuildRectForPhysicalRect( |
| const PhysicalRect& rect) { |
| return protocol::DOM::Rect::create() |
| .setX(rect.X()) |
| .setY(rect.Y()) |
| .setWidth(rect.Width()) |
| .setHeight(rect.Height()) |
| .build(); |
| } |
| |
| } // namespace |
| |
| struct LegacyDOMSnapshotAgent::VectorStringHashTraits |
| : public WTF::GenericHashTraits<Vector<String>> { |
| static unsigned GetHash(const Vector<String>& vec) { |
| unsigned h = DefaultHash<size_t>::Hash::GetHash(vec.size()); |
| for (wtf_size_t i = 0; i < vec.size(); i++) { |
| h = WTF::HashInts(h, DefaultHash<String>::Hash::GetHash(vec[i])); |
| } |
| return h; |
| } |
| |
| static bool Equal(const Vector<String>& a, const Vector<String>& b) { |
| if (a.size() != b.size()) |
| return false; |
| for (wtf_size_t i = 0; i < a.size(); i++) { |
| if (a[i] != b[i]) |
| return false; |
| } |
| return true; |
| } |
| |
| static void ConstructDeletedValue(Vector<String>& vec, bool) { |
| new (NotNull, &vec) Vector<String>(WTF::kHashTableDeletedValue); |
| } |
| |
| static bool IsDeletedValue(const Vector<String>& vec) { |
| return vec.IsHashTableDeletedValue(); |
| } |
| |
| static bool IsEmptyValue(const Vector<String>& vec) { return vec.IsEmpty(); } |
| |
| static const bool kEmptyValueIsZero = false; |
| static const bool safe_to_compare_to_empty_or_deleted = false; |
| static const bool kHasIsEmptyValueFunction = true; |
| }; |
| |
| LegacyDOMSnapshotAgent::LegacyDOMSnapshotAgent( |
| InspectorDOMDebuggerAgent* dom_debugger_agent, |
| OriginUrlMap* origin_url_map) |
| : origin_url_map_(origin_url_map), |
| dom_debugger_agent_(dom_debugger_agent) {} |
| |
| LegacyDOMSnapshotAgent::~LegacyDOMSnapshotAgent() = default; |
| |
| Response LegacyDOMSnapshotAgent::GetSnapshot( |
| Document* document, |
| std::unique_ptr<protocol::Array<String>> style_filter, |
| protocol::Maybe<bool> include_event_listeners, |
| protocol::Maybe<bool> include_paint_order, |
| protocol::Maybe<bool> include_user_agent_shadow_tree, |
| std::unique_ptr<protocol::Array<protocol::DOMSnapshot::DOMNode>>* dom_nodes, |
| std::unique_ptr<protocol::Array<protocol::DOMSnapshot::LayoutTreeNode>>* |
| layout_tree_nodes, |
| std::unique_ptr<protocol::Array<protocol::DOMSnapshot::ComputedStyle>>* |
| computed_styles) { |
| // Setup snapshot. |
| dom_nodes_ = |
| std::make_unique<protocol::Array<protocol::DOMSnapshot::DOMNode>>(); |
| layout_tree_nodes_ = std::make_unique< |
| protocol::Array<protocol::DOMSnapshot::LayoutTreeNode>>(); |
| computed_styles_ = |
| std::make_unique<protocol::Array<protocol::DOMSnapshot::ComputedStyle>>(); |
| computed_styles_map_ = std::make_unique<ComputedStylesMap>(); |
| css_property_filter_ = std::make_unique<CSSPropertyFilter>(); |
| |
| // Look up the CSSPropertyIDs for each entry in |style_filter|. |
| for (const String& entry : *style_filter) { |
| CSSPropertyID property_id = |
| cssPropertyID(document->GetExecutionContext(), entry); |
| if (property_id == CSSPropertyID::kInvalid) |
| continue; |
| css_property_filter_->emplace_back(entry, property_id); |
| } |
| |
| if (include_paint_order.fromMaybe(false)) |
| paint_order_map_ = InspectorDOMSnapshotAgent::BuildPaintLayerTree(document); |
| |
| // Actual traversal. |
| VisitNode(document, include_event_listeners.fromMaybe(false), |
| include_user_agent_shadow_tree.fromMaybe(false)); |
| |
| // Extract results from state and reset. |
| *dom_nodes = std::move(dom_nodes_); |
| *layout_tree_nodes = std::move(layout_tree_nodes_); |
| *computed_styles = std::move(computed_styles_); |
| computed_styles_map_.reset(); |
| css_property_filter_.reset(); |
| paint_order_map_.reset(); |
| return Response::Success(); |
| } |
| |
| int LegacyDOMSnapshotAgent::VisitNode(Node* node, |
| bool include_event_listeners, |
| bool include_user_agent_shadow_tree) { |
| // Update layout tree before traversal of document so that we inspect a |
| // current and consistent state of all trees. No need to do this if paint |
| // order was calculated, since layout trees were already updated during |
| // TraversePaintLayerTree(). |
| if (node->IsDocumentNode() && !paint_order_map_) |
| node->GetDocument().UpdateStyleAndLayoutTree(); |
| |
| String node_value; |
| switch (node->getNodeType()) { |
| case Node::kTextNode: |
| case Node::kAttributeNode: |
| case Node::kCommentNode: |
| case Node::kCdataSectionNode: |
| case Node::kDocumentFragmentNode: |
| node_value = node->nodeValue(); |
| break; |
| default: |
| break; |
| } |
| |
| // Create DOMNode object and add it to the result array before traversing |
| // children, so that parents appear before their children in the array. |
| std::unique_ptr<protocol::DOMSnapshot::DOMNode> owned_value = |
| protocol::DOMSnapshot::DOMNode::create() |
| .setNodeType(static_cast<int>(node->getNodeType())) |
| .setNodeName(node->nodeName()) |
| .setNodeValue(node_value) |
| .setBackendNodeId(IdentifiersFactory::IntIdForNode(node)) |
| .build(); |
| if (origin_url_map_ && |
| origin_url_map_->Contains(owned_value->getBackendNodeId())) { |
| String origin_url = origin_url_map_->at(owned_value->getBackendNodeId()); |
| // In common cases, it is implicit that a child node would have the same |
| // origin url as its parent, so no need to mark twice. |
| if (!node->parentNode() || origin_url_map_->at(DOMNodeIds::IdForNode( |
| node->parentNode())) != origin_url) { |
| owned_value->setOriginURL( |
| origin_url_map_->at(owned_value->getBackendNodeId())); |
| } |
| } |
| protocol::DOMSnapshot::DOMNode* value = owned_value.get(); |
| int index = static_cast<int>(dom_nodes_->size()); |
| dom_nodes_->emplace_back(std::move(owned_value)); |
| |
| int layoutNodeIndex = |
| VisitLayoutTreeNode(node->GetLayoutObject(), node, index); |
| if (layoutNodeIndex != -1) |
| value->setLayoutNodeIndex(layoutNodeIndex); |
| |
| if (node->WillRespondToMouseClickEvents()) |
| value->setIsClickable(true); |
| |
| if (include_event_listeners && node->GetDocument().GetFrame()) { |
| ScriptState* script_state = |
| ToScriptStateForMainWorld(node->GetDocument().GetFrame()); |
| if (script_state->ContextIsValid()) { |
| ScriptState::Scope scope(script_state); |
| v8::Local<v8::Context> context = script_state->GetContext(); |
| V8EventListenerInfoList event_information; |
| InspectorDOMDebuggerAgent::CollectEventListeners( |
| script_state->GetIsolate(), node, v8::Local<v8::Value>(), node, true, |
| &event_information); |
| if (!event_information.IsEmpty()) { |
| value->setEventListeners( |
| dom_debugger_agent_->BuildObjectsForEventListeners( |
| event_information, context, v8_inspector::StringView())); |
| } |
| } |
| } |
| |
| auto* element = DynamicTo<Element>(node); |
| if (element) { |
| value->setAttributes(BuildArrayForElementAttributes(element)); |
| |
| if (auto* frame_owner = DynamicTo<HTMLFrameOwnerElement>(node)) { |
| if (LocalFrame* frame = |
| DynamicTo<LocalFrame>(frame_owner->ContentFrame())) |
| value->setFrameId(IdentifiersFactory::FrameId(frame)); |
| |
| if (Document* doc = frame_owner->contentDocument()) { |
| value->setContentDocumentIndex(VisitNode( |
| doc, include_event_listeners, include_user_agent_shadow_tree)); |
| } |
| } |
| |
| if (node->parentNode() && node->parentNode()->IsDocumentNode()) { |
| LocalFrame* frame = node->GetDocument().GetFrame(); |
| if (frame) |
| value->setFrameId(IdentifiersFactory::FrameId(frame)); |
| } |
| |
| if (auto* textarea_element = DynamicTo<HTMLTextAreaElement>(*element)) |
| value->setTextValue(textarea_element->value()); |
| |
| if (auto* input_element = DynamicTo<HTMLInputElement>(*element)) { |
| value->setInputValue(input_element->value()); |
| if ((input_element->type() == input_type_names::kRadio) || |
| (input_element->type() == input_type_names::kCheckbox)) { |
| value->setInputChecked(input_element->checked()); |
| } |
| } |
| |
| if (auto* option_element = DynamicTo<HTMLOptionElement>(*element)) |
| value->setOptionSelected(option_element->Selected()); |
| |
| if (element->GetPseudoId()) { |
| value->setPseudoType( |
| InspectorDOMAgent::ProtocolPseudoElementType(element->GetPseudoId())); |
| } |
| value->setPseudoElementIndexes( |
| VisitPseudoElements(element, index, include_event_listeners, |
| include_user_agent_shadow_tree)); |
| |
| auto* image_element = DynamicTo<HTMLImageElement>(node); |
| if (image_element) |
| value->setCurrentSourceURL(image_element->currentSrc()); |
| } else if (auto* document = DynamicTo<Document>(node)) { |
| value->setDocumentURL(InspectorDOMAgent::DocumentURLString(document)); |
| value->setBaseURL(InspectorDOMAgent::DocumentBaseURLString(document)); |
| if (document->ContentLanguage()) |
| value->setContentLanguage(document->ContentLanguage().Utf8().c_str()); |
| if (document->EncodingName()) |
| value->setDocumentEncoding(document->EncodingName().Utf8().c_str()); |
| value->setFrameId(IdentifiersFactory::FrameId(document->GetFrame())); |
| if (document->View() && document->View()->LayoutViewport()) { |
| auto offset = document->View()->LayoutViewport()->GetScrollOffset(); |
| value->setScrollOffsetX(offset.Width()); |
| value->setScrollOffsetY(offset.Height()); |
| } |
| } else if (auto* doc_type = DynamicTo<DocumentType>(node)) { |
| value->setPublicId(doc_type->publicId()); |
| value->setSystemId(doc_type->systemId()); |
| } |
| if (node->IsInShadowTree()) { |
| value->setShadowRootType( |
| InspectorDOMAgent::GetShadowRootType(node->ContainingShadowRoot())); |
| } |
| |
| if (node->IsContainerNode()) { |
| value->setChildNodeIndexes(VisitContainerChildren( |
| node, include_event_listeners, include_user_agent_shadow_tree)); |
| } |
| return index; |
| } |
| |
| std::unique_ptr<protocol::Array<int>> |
| LegacyDOMSnapshotAgent::VisitContainerChildren( |
| Node* container, |
| bool include_event_listeners, |
| bool include_user_agent_shadow_tree) { |
| auto children = std::make_unique<protocol::Array<int>>(); |
| |
| if (!blink::dom_traversal_utils::HasChildren(*container, |
| include_user_agent_shadow_tree)) |
| return nullptr; |
| |
| Node* child = blink::dom_traversal_utils::FirstChild( |
| *container, include_user_agent_shadow_tree); |
| while (child) { |
| children->emplace_back(VisitNode(child, include_event_listeners, |
| include_user_agent_shadow_tree)); |
| child = blink::dom_traversal_utils::NextSibling( |
| *child, include_user_agent_shadow_tree); |
| } |
| |
| return children; |
| } |
| |
| std::unique_ptr<protocol::Array<int>> |
| LegacyDOMSnapshotAgent::VisitPseudoElements( |
| Element* parent, |
| int index, |
| bool include_event_listeners, |
| bool include_user_agent_shadow_tree) { |
| if (!parent->GetPseudoElement(kPseudoIdFirstLetter) && |
| !parent->GetPseudoElement(kPseudoIdBefore) && |
| !parent->GetPseudoElement(kPseudoIdAfter)) { |
| return nullptr; |
| } |
| |
| auto pseudo_elements = std::make_unique<protocol::Array<int>>(); |
| for (PseudoId pseudo_id : |
| {kPseudoIdFirstLetter, kPseudoIdBefore, kPseudoIdAfter}) { |
| if (Node* pseudo_node = parent->GetPseudoElement(pseudo_id)) { |
| pseudo_elements->emplace_back(VisitNode(pseudo_node, |
| include_event_listeners, |
| include_user_agent_shadow_tree)); |
| } |
| } |
| return pseudo_elements; |
| } |
| |
| std::unique_ptr<protocol::Array<protocol::DOMSnapshot::NameValue>> |
| LegacyDOMSnapshotAgent::BuildArrayForElementAttributes(Element* element) { |
| AttributeCollection attributes = element->Attributes(); |
| if (attributes.IsEmpty()) |
| return nullptr; |
| auto attributes_value = |
| std::make_unique<protocol::Array<protocol::DOMSnapshot::NameValue>>(); |
| for (const auto& attribute : attributes) { |
| attributes_value->emplace_back(protocol::DOMSnapshot::NameValue::create() |
| .setName(attribute.GetName().ToString()) |
| .setValue(attribute.Value()) |
| .build()); |
| } |
| return attributes_value; |
| } |
| |
| int LegacyDOMSnapshotAgent::VisitLayoutTreeNode(LayoutObject* layout_object, |
| Node* node, |
| int node_index) { |
| if (!layout_object) |
| return -1; |
| |
| if (node->GetPseudoId()) { |
| // For pseudo elements, visit the children of the layout object. |
| for (LayoutObject* child = layout_object->SlowFirstChild(); child; |
| child = child->NextSibling()) { |
| if (child->IsAnonymous()) |
| VisitLayoutTreeNode(child, node, node_index); |
| } |
| } |
| |
| auto layout_tree_node = |
| protocol::DOMSnapshot::LayoutTreeNode::create() |
| .setDomNodeIndex(node_index) |
| .setBoundingBox(LegacyBuildRectForPhysicalRect( |
| InspectorDOMSnapshotAgent::RectInDocument(layout_object))) |
| .build(); |
| |
| int style_index = GetStyleIndexForNode(node); |
| if (style_index != -1) |
| layout_tree_node->setStyleIndex(style_index); |
| |
| if (layout_object->Style() && layout_object->IsStackingContext()) |
| layout_tree_node->setIsStackingContext(true); |
| |
| if (paint_order_map_) { |
| PaintLayer* paint_layer = layout_object->EnclosingLayer(); |
| |
| // We visited all PaintLayers when building |paint_order_map_|. |
| DCHECK(paint_order_map_->Contains(paint_layer)); |
| |
| if (int paint_order = paint_order_map_->at(paint_layer)) |
| layout_tree_node->setPaintOrder(paint_order); |
| } |
| |
| if (layout_object->IsText()) { |
| LayoutText* layout_text = ToLayoutText(layout_object); |
| layout_tree_node->setLayoutText(layout_text->GetText()); |
| Vector<LayoutText::TextBoxInfo> text_boxes = layout_text->GetTextBoxInfo(); |
| if (!text_boxes.IsEmpty()) { |
| auto inline_text_nodes = std::make_unique< |
| protocol::Array<protocol::DOMSnapshot::InlineTextBox>>(); |
| for (const auto& text_box : text_boxes) { |
| inline_text_nodes->emplace_back( |
| protocol::DOMSnapshot::InlineTextBox::create() |
| .setStartCharacterIndex(text_box.dom_start_offset) |
| .setNumCharacters(text_box.dom_length) |
| .setBoundingBox(LegacyBuildRectForPhysicalRect( |
| InspectorDOMSnapshotAgent::TextFragmentRectInDocument( |
| layout_object, text_box))) |
| .build()); |
| } |
| layout_tree_node->setInlineTextNodes(std::move(inline_text_nodes)); |
| } |
| } |
| |
| int index = static_cast<int>(layout_tree_nodes_->size()); |
| layout_tree_nodes_->emplace_back(std::move(layout_tree_node)); |
| return index; |
| } |
| |
| int LegacyDOMSnapshotAgent::GetStyleIndexForNode(Node* node) { |
| auto* computed_style_info = |
| MakeGarbageCollected<CSSComputedStyleDeclaration>(node, true); |
| |
| Vector<String> style; |
| bool all_properties_empty = true; |
| for (const auto& pair : *css_property_filter_) { |
| String value = computed_style_info->GetPropertyValue(pair.second); |
| if (!value.IsEmpty()) |
| all_properties_empty = false; |
| style.push_back(value); |
| } |
| |
| // -1 means an empty style. |
| if (all_properties_empty) |
| return -1; |
| |
| ComputedStylesMap::iterator it = computed_styles_map_->find(style); |
| if (it != computed_styles_map_->end()) |
| return it->value; |
| |
| // It's a distinct style, so append to |computedStyles|. |
| auto style_properties = |
| std::make_unique<protocol::Array<protocol::DOMSnapshot::NameValue>>(); |
| |
| for (wtf_size_t i = 0; i < style.size(); i++) { |
| if (style[i].IsEmpty()) |
| continue; |
| style_properties->emplace_back( |
| protocol::DOMSnapshot::NameValue::create() |
| .setName((*css_property_filter_)[i].first) |
| .setValue(style[i]) |
| .build()); |
| } |
| |
| wtf_size_t index = static_cast<wtf_size_t>(computed_styles_->size()); |
| computed_styles_->emplace_back(protocol::DOMSnapshot::ComputedStyle::create() |
| .setProperties(std::move(style_properties)) |
| .build()); |
| computed_styles_map_->insert(std::move(style), index); |
| return index; |
| } |
| |
| } // namespace blink |