| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/ui_devtools/dom_agent.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/containers/adapters.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/observer_list.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "components/ui_devtools/devtools_server.h" |
| #include "components/ui_devtools/root_element.h" |
| #include "components/ui_devtools/ui_devtools_features.h" |
| #include "components/ui_devtools/ui_element.h" |
| #include "ui/base/interaction/element_identifier.h" |
| |
| namespace { |
| |
| std::string CreateIdentifier() { |
| static int last_used_identifier; |
| return base::NumberToString(++last_used_identifier); |
| } |
| |
| } // namespace |
| |
| namespace ui_devtools { |
| |
| using ui_devtools::protocol::Array; |
| using ui_devtools::protocol::DOM::Node; |
| using ui_devtools::protocol::Response; |
| |
| namespace { |
| |
| bool FindMatchInStylesProperty(const std::string& query, |
| ui_devtools::UIElement* node) { |
| for (UIElement::ClassProperties& class_properties : |
| node->GetCustomPropertiesForMatchedStyle()) { |
| for (UIElement::UIProperty& ui_property : class_properties.properties_) { |
| protocol::String dataName = ui_property.name_; |
| protocol::String dataValue = ui_property.value_; |
| dataName = base::ToLowerASCII(dataName); |
| dataValue = base::ToLowerASCII(dataValue); |
| protocol::String style_property_match = dataName + ": " + dataValue + ";"; |
| if (style_property_match.find(query) != protocol::String::npos) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| bool FindMatchByID(const std::string& query, ui_devtools::UIElement* node) { |
| auto identifier = ui::ElementIdentifier::FromName(query.c_str()); |
| if (!identifier) |
| return false; |
| return node->FindMatchByElementID(identifier); |
| } |
| |
| bool FindMatchInDomProperties(const std::string& query, |
| const std::string& tag_name_query, |
| const std::string& attribute_query, |
| bool exact_attribute_match, |
| ui_devtools::UIElement* node) { |
| protocol::String node_name = node->GetTypeName(); |
| node_name = base::ToLowerASCII(node_name); |
| if (node_name.find(query) != protocol::String::npos || |
| node_name == tag_name_query) { |
| return true; |
| } |
| for (std::string& data : node->GetAttributes()) { |
| data = base::ToLowerASCII(data); |
| if (data.find(query) != protocol::String::npos) { |
| return true; |
| } |
| if (data.find(attribute_query) != protocol::String::npos && |
| (!exact_attribute_match || data.length() == attribute_query.length())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| struct DOMAgent::Query { |
| enum QueryType { Normal, Style, ID }; |
| Query(protocol::String query, |
| protocol::String tag_name_query, |
| protocol::String attribute_query, |
| bool exact_attribute_match, |
| QueryType query_type) |
| : query_(query), |
| tag_name_query_(tag_name_query), |
| attribute_query_(attribute_query), |
| exact_attribute_match_(exact_attribute_match), |
| query_type_(query_type) {} |
| protocol::String query_; |
| protocol::String tag_name_query_; |
| protocol::String attribute_query_; |
| bool exact_attribute_match_; |
| QueryType query_type_; |
| }; |
| |
| DOMAgent::DOMAgent() {} |
| |
| DOMAgent::~DOMAgent() { |
| Reset(); |
| } |
| |
| Response DOMAgent::disable() { |
| Reset(); |
| return Response::Success(); |
| } |
| |
| Response DOMAgent::getDocument(std::unique_ptr<Node>* out_root) { |
| element_root_->ResetNodeId(); |
| *out_root = BuildInitialTree(); |
| is_document_created_ = true; |
| return Response::Success(); |
| } |
| |
| Response DOMAgent::pushNodesByBackendIdsToFrontend( |
| std::unique_ptr<protocol::Array<int>> backend_node_ids, |
| std::unique_ptr<protocol::Array<int>>* result) { |
| *result = std::move(backend_node_ids); |
| return Response::Success(); |
| } |
| |
| void DOMAgent::OnUIElementAdded(UIElement* parent, UIElement* child) { |
| // When parent is null, only need to update |node_id_to_ui_element_|. |
| if (!parent) { |
| node_id_to_ui_element_[child->node_id()] = child; |
| return; |
| } |
| |
| DCHECK(node_id_to_ui_element_.count(parent->node_id())); |
| |
| auto* current_parent = parent; |
| while (current_parent) { |
| if (current_parent->is_updating()) { |
| // One of the parents is updating, so no need to update here. |
| return; |
| } |
| current_parent = current_parent->parent(); |
| } |
| |
| child->set_is_updating(true); |
| |
| const auto& children = parent->children(); |
| auto iter = base::ranges::find(children, child); |
| int prev_node_id = |
| (iter == children.begin()) ? 0 : (*std::prev(iter))->node_id(); |
| frontend()->childNodeInserted(parent->node_id(), prev_node_id, |
| BuildTreeForUIElement(child)); |
| |
| child->set_is_updating(false); |
| for (auto& observer : observers_) |
| observer.OnElementAdded(child); |
| } |
| |
| void DOMAgent::OnUIElementReordered(UIElement* parent, UIElement* child) { |
| DCHECK(node_id_to_ui_element_.count(parent->node_id())); |
| |
| const auto& children = parent->children(); |
| auto iter = base::ranges::find(children, child); |
| CHECK(iter != children.end()); |
| int prev_node_id = |
| (iter == children.begin()) ? 0 : (*std::prev(iter))->node_id(); |
| RemoveDomNode(child, false); |
| frontend()->childNodeInserted(parent->node_id(), prev_node_id, |
| BuildDomNodeFromUIElement(child)); |
| } |
| |
| void DOMAgent::OnUIElementRemoved(UIElement* ui_element) { |
| if (node_id_to_ui_element_.count(ui_element->node_id())) { |
| // Check if |ui_element| exists in DOM before asking the frontend to remove |
| // it. It is possible that this node is already removed along with its |
| // ancestor. This happens for example when a bubble closes. |
| // The DOM tree is first requested to be removed in |
| // |WidgetElement::OnWidgetDestroyed()| then in |
| // |ViewElement::OnChildViewRemoved()|. |
| RemoveDomNode(ui_element, true); |
| } |
| } |
| |
| void DOMAgent::OnUIElementBoundsChanged(UIElement* ui_element) { |
| for (auto& observer : observers_) |
| observer.OnElementBoundsChanged(ui_element); |
| } |
| |
| void DOMAgent::AddObserver(DOMAgentObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void DOMAgent::RemoveObserver(DOMAgentObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| UIElement* DOMAgent::GetElementFromNodeId(int node_id) const { |
| auto it = node_id_to_ui_element_.find(node_id); |
| if (it != node_id_to_ui_element_.end()) |
| return it->second; |
| return nullptr; |
| } |
| |
| int DOMAgent::GetParentIdOfNodeId(int node_id) const { |
| DCHECK(node_id_to_ui_element_.count(node_id)); |
| const UIElement* element = node_id_to_ui_element_.at(node_id); |
| if (element->parent() && element->parent() != element_root_.get()) |
| return element->parent()->node_id(); |
| return 0; |
| } |
| |
| std::unique_ptr<Node> DOMAgent::BuildNode( |
| const std::string& name, |
| std::unique_ptr<std::vector<std::string>> attributes, |
| std::unique_ptr<Array<Node>> children, |
| int node_ids) { |
| constexpr int kDomElementNodeType = 1; |
| std::unique_ptr<Node> node = Node::create() |
| .setNodeId(node_ids) |
| .setBackendNodeId(node_ids) |
| .setNodeName(name) |
| .setNodeType(kDomElementNodeType) |
| .setAttributes(std::move(attributes)) |
| .build(); |
| node->setChildNodeCount(static_cast<int>(children->size())); |
| node->setChildren(std::move(children)); |
| return node; |
| } |
| |
| std::unique_ptr<Node> DOMAgent::BuildDomNodeFromUIElement(UIElement* root) { |
| auto children = std::make_unique<protocol::Array<Node>>(); |
| for (ui_devtools::UIElement* it : root->children()) { |
| children->emplace_back(BuildDomNodeFromUIElement(it)); |
| } |
| |
| return BuildNode( |
| root->GetTypeName(), |
| std::make_unique<std::vector<std::string>>(root->GetAttributes()), |
| std::move(children), root->node_id()); |
| } |
| |
| std::unique_ptr<Node> DOMAgent::BuildInitialTree() { |
| auto children = std::make_unique<protocol::Array<Node>>(); |
| |
| element_root_ = std::make_unique<RootElement>(this); |
| element_root_->set_is_updating(true); |
| |
| for (auto* child : CreateChildrenForRoot()) { |
| children->emplace_back(BuildTreeForUIElement(child)); |
| element_root_->AddChild(child); |
| } |
| std::unique_ptr<Node> root_node = |
| BuildNode("root", nullptr, std::move(children), element_root_->node_id()); |
| element_root_->set_is_updating(false); |
| return root_node; |
| } |
| |
| void DOMAgent::OnElementBoundsChanged(UIElement* ui_element) { |
| for (auto& observer : observers_) |
| observer.OnElementBoundsChanged(ui_element); |
| } |
| |
| void DOMAgent::RemoveDomNode(UIElement* ui_element, bool update_node_id_map) { |
| for (ui_devtools::UIElement* child_element : ui_element->children()) { |
| RemoveDomNode(child_element, update_node_id_map); |
| } |
| frontend()->childNodeRemoved(ui_element->parent()->node_id(), |
| ui_element->node_id()); |
| if (update_node_id_map) { |
| node_id_to_ui_element_.erase(ui_element->node_id()); |
| } |
| } |
| |
| void DOMAgent::Reset() { |
| element_root_.reset(); |
| node_id_to_ui_element_.clear(); |
| observers_.Clear(); |
| is_document_created_ = false; |
| search_results_.clear(); |
| } |
| |
| DOMAgent::Query DOMAgent::PreprocessQuery(protocol::String query) { |
| constexpr char kSearchIDKeyword[] = "id:"; |
| bool exact_attribute_match = false; |
| if (query.find(kSearchIDKeyword) == 0) { |
| // remove whitespaces (if they exist) after the 'id:' keyword |
| base::TrimWhitespaceASCII(query.substr(strlen(kSearchIDKeyword)), |
| base::TrimPositions::TRIM_ALL, &query); |
| return Query(query, query, query, exact_attribute_match, |
| Query::QueryType::ID); |
| } |
| |
| // If it's not query by id, transform query to lower case. |
| query = base::ToLowerASCII(query); |
| |
| constexpr char kSearchStylesPanelKeyword[] = "style:"; |
| // Temporary Solution: search on styles panel if the keyword 'style:' |
| // exists in the beginning of the query. |
| if (query.find(kSearchStylesPanelKeyword) == 0) { |
| // remove whitespaces (if they exist) after the 'style:' keyword |
| base::TrimWhitespaceASCII(query.substr(strlen(kSearchStylesPanelKeyword)), |
| base::TrimPositions::TRIM_ALL, &query); |
| return Query(query, query, query, exact_attribute_match, |
| Query::QueryType::Style); |
| } |
| |
| // Preprocessing for normal dom search. |
| protocol::String tag_name_query = query; |
| protocol::String attribute_query = query; |
| unsigned query_length = query.length(); |
| bool start_tag_found = !query.find('<'); |
| bool end_tag_found = query.rfind('>') + 1 == query_length; |
| bool start_quote_found = !query.find('"'); |
| bool end_quote_found = query.rfind('"') + 1 == query_length; |
| exact_attribute_match = start_quote_found && end_quote_found; |
| |
| if (start_tag_found) |
| tag_name_query = tag_name_query.substr(1, tag_name_query.length() - 1); |
| if (end_tag_found) |
| tag_name_query = tag_name_query.substr(0, tag_name_query.length() - 1); |
| if (start_quote_found) |
| attribute_query = attribute_query.substr(1, attribute_query.length() - 1); |
| if (end_quote_found) |
| attribute_query = attribute_query.substr(0, attribute_query.length() - 1); |
| return Query(query, tag_name_query, attribute_query, exact_attribute_match, |
| Query::QueryType::Normal); |
| } |
| |
| void DOMAgent::SearchDomTree(const DOMAgent::Query& query_data, |
| std::vector<int>* result_collector) { |
| std::vector<UIElement*> stack; |
| // Root node from element_root() is not a real node from the DOM tree. |
| // The children of the root node are the 'actual' roots of the DOM tree. |
| std::vector<raw_ptr<UIElement, VectorExperimental>> root_list = |
| element_root()->children(); |
| DCHECK(root_list.size()); |
| // Children are accessed from bottom to top. So iterate backwards. |
| for (ui_devtools::UIElement* root : base::Reversed(root_list)) { |
| stack.push_back(root); |
| } |
| |
| // Manual plain text search. DFS traversal. |
| while (!stack.empty()) { |
| UIElement* node = stack.back(); |
| stack.pop_back(); |
| std::vector<raw_ptr<UIElement, VectorExperimental>> children_array = |
| node->children(); |
| // Children are accessed from bottom to top. So iterate backwards. |
| for (ui_devtools::UIElement* child : base::Reversed(children_array)) { |
| stack.push_back(child); |
| } |
| bool found_match = false; |
| if (query_data.query_type_ == Query::QueryType::Style) |
| found_match = FindMatchInStylesProperty(query_data.query_, node); |
| else if (query_data.query_type_ == Query::QueryType::ID) |
| found_match = FindMatchByID(query_data.query_, node); |
| else |
| found_match = FindMatchInDomProperties( |
| query_data.query_, query_data.tag_name_query_, |
| query_data.attribute_query_, query_data.exact_attribute_match_, node); |
| if (found_match) |
| result_collector->push_back(node->node_id()); |
| } |
| } |
| |
| // Code is based on InspectorDOMAgent::performSearch() from |
| // src/third_party/blink/renderer/core/inspector/inspector_dom_agent.cc |
| Response DOMAgent::performSearch( |
| const protocol::String& whitespace_trimmed_query, |
| protocol::Maybe<bool> optional_include_user_agent_shadow_dom, |
| protocol::String* search_id, |
| int* result_count) { |
| Query query_data = PreprocessQuery(whitespace_trimmed_query); |
| if (!query_data.query_.length()) |
| return Response::Success(); |
| |
| std::vector<int> result_collector; |
| SearchDomTree(query_data, &result_collector); |
| |
| *search_id = CreateIdentifier(); |
| *result_count = result_collector.size(); |
| search_results_.insert( |
| std::make_pair(*search_id, std::move(result_collector))); |
| return Response::Success(); |
| } |
| |
| Response DOMAgent::getSearchResults( |
| const protocol::String& search_id, |
| int from_index, |
| int to_index, |
| std::unique_ptr<protocol::Array<int>>* node_ids) { |
| SearchResults::iterator it = search_results_.find(search_id); |
| if (it == search_results_.end()) |
| return Response::ServerError("No search session with given id found"); |
| |
| int size = it->second.size(); |
| if (from_index < 0 || to_index > size || from_index >= to_index) |
| return Response::ServerError("Invalid search result range"); |
| |
| *node_ids = std::make_unique<protocol::Array<int>>(); |
| for (int i = from_index; i < to_index; ++i) |
| (*node_ids)->emplace_back((it->second)[i]); |
| return Response::Success(); |
| } |
| |
| Response DOMAgent::discardSearchResults(const protocol::String& search_id) { |
| search_results_.erase(search_id); |
| return Response::Success(); |
| } |
| |
| protocol::Response DOMAgent::dispatchMouseEvent( |
| int node_id, |
| std::unique_ptr<protocol::DOM::MouseEvent> event) { |
| if (!base::FeatureList::IsEnabled( |
| ui_devtools::kUIDebugToolsEnableSyntheticEvents)) |
| return Response::ServerError("Dispatch mouse events is not enabled."); |
| if (node_id_to_ui_element_.count(node_id) == 0) |
| return Response::ServerError("Element not found on node id"); |
| if (!node_id_to_ui_element_[node_id]->DispatchMouseEvent(event.get())) |
| return Response::ServerError("Failed to dispatch mouse event for node id"); |
| return Response::Success(); |
| } |
| |
| protocol::Response DOMAgent::dispatchKeyEvent( |
| int node_id, |
| std::unique_ptr<protocol::DOM::KeyEvent> event) { |
| if (!base::FeatureList::IsEnabled( |
| ui_devtools::kUIDebugToolsEnableSyntheticEvents)) |
| return Response::ServerError("Dispatch key events is not enabled."); |
| if (node_id_to_ui_element_.count(node_id) == 0) |
| return Response::ServerError("Element not found on node id"); |
| if (!node_id_to_ui_element_[node_id]->DispatchKeyEvent(event.get())) |
| return Response::ServerError("Failed to dispatch key event for node id"); |
| return Response::Success(); |
| } |
| |
| } // namespace ui_devtools |