// Copyright (c) 2009 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 "config.h" #include "AtomicString.h" #include "DOMWindow.h" #include "Document.h" #include "Event.h" #include "EventListener.h" #include "EventNames.h" #include "EventTarget.h" #include "HTMLFrameOwnerElement.h" #include "markup.h" #include "MutationEvent.h" #include "Node.h" #include "NodeList.h" #include "PlatformString.h" #include "Text.h" #include "XPathResult.h" #include #include #include #include #undef LOG #include "base/string_util.h" #include "base/values.h" #include "webkit/glue/devtools/dom_agent_impl.h" #include "webkit/glue/glue_util.h" using namespace WebCore; const char DomAgentImpl::kExactTagNames[] = "//*[name() == '%s')]"; const char DomAgentImpl::kPartialTagNames[] = "//*[contains(name(), '%s')]"; const char DomAgentImpl::kStartOfTagNames[] = "//*[starts-with(name(), '%s')]"; const char DomAgentImpl::kPartialTagNamesAndAttributeValues[] = "//*[contains(name(), '%s') or contains(@*, '%s')]"; const char DomAgentImpl::kPartialAttributeValues[] = "//*[contains(@*, '%s')]"; const char DomAgentImpl::kPlainText[] = "//text()[contains(., '%s')] | //comment()[contains(., '%s')]"; // static PassRefPtr DomAgentImpl::EventListenerWrapper::Create( DomAgentImpl* dom_agent_impl) { return adoptRef(new EventListenerWrapper(dom_agent_impl)); } DomAgentImpl::EventListenerWrapper::EventListenerWrapper( DomAgentImpl* dom_agent_impl) : dom_agent_impl_(dom_agent_impl) { } void DomAgentImpl::EventListenerWrapper::handleEvent( Event* event, bool isWindowEvent) { dom_agent_impl_->handleEvent(event, isWindowEvent); } DomAgentImpl::DomAgentImpl(DomAgentDelegate* delegate) : delegate_(delegate), last_node_id_(1), document_element_requested_(false) { event_listener_ = EventListenerWrapper::Create(this); } DomAgentImpl::~DomAgentImpl() { SetDocument(NULL); } void DomAgentImpl::SetDocument(Document* doc) { if (doc == GetMainFrameDocument()) { return; } ListHashSet > copy = documents_; for (ListHashSet >::iterator it = copy.begin(); it != copy.end(); ++it) { StopListening((*it).get()); } ASSERT(documents_.size() == 0); if (doc) { StartListening(doc); if (document_element_requested_) { GetDocumentElement(); document_element_requested_ = false; } } else { DiscardBindings(); } } void DomAgentImpl::StartListening(Document* doc) { if (documents_.contains(doc)) return; doc->addEventListener(eventNames().DOMContentLoadedEvent, event_listener_, false); doc->addEventListener(eventNames().DOMNodeInsertedEvent, event_listener_, false); doc->addEventListener(eventNames().DOMNodeRemovedEvent, event_listener_, false); doc->addEventListener(eventNames().DOMNodeRemovedFromDocumentEvent, event_listener_, true); doc->addEventListener(eventNames().DOMAttrModifiedEvent, event_listener_, false); documents_.add(doc); } void DomAgentImpl::StopListening(Document* doc) { doc->removeEventListener(eventNames().DOMContentLoadedEvent, event_listener_.get(), false); doc->removeEventListener(eventNames().DOMNodeInsertedEvent, event_listener_.get(), false); doc->removeEventListener(eventNames().DOMNodeRemovedEvent, event_listener_.get(), false); doc->removeEventListener(eventNames().DOMNodeRemovedFromDocumentEvent, event_listener_.get(), true); doc->removeEventListener(eventNames().DOMAttrModifiedEvent, event_listener_.get(), false); documents_.remove(doc); } int DomAgentImpl::Bind(Node* node) { HashMap::iterator it = node_to_id_.find(node); if (it != node_to_id_.end()) return it->second; int id = last_node_id_++; node_to_id_.set(node, id); id_to_node_.set(id, node); return id; } void DomAgentImpl::Unbind(Node* node) { if (node->isFrameOwnerElement()) { const HTMLFrameOwnerElement* frame_owner = static_cast(node); StopListening(frame_owner->contentDocument()); } HashMap::iterator it = node_to_id_.find(node); if (it != node_to_id_.end()) { id_to_node_.remove(id_to_node_.find(it->second)); children_requested_.remove(children_requested_.find(it->second)); node_to_id_.remove(it); } } void DomAgentImpl::PushDocumentElementToClient() { Element* doc_elem = GetMainFrameDocument()->documentElement(); if (!node_to_id_.contains(doc_elem)) { OwnPtr value(BuildValueForNode(doc_elem, 0)); delegate_->SetDocumentElement(*value.get()); } } void DomAgentImpl::PushChildNodesToClient(int element_id) { Node* node = GetNodeForId(element_id); if (!node || (node->nodeType() != Node::ELEMENT_NODE)) return; if (children_requested_.contains(element_id)) return; Element* element = static_cast(node); OwnPtr children(BuildValueForElementChildren(element, 1)); children_requested_.add(element_id); delegate_->SetChildNodes(element_id, *children.get()); } void DomAgentImpl::DiscardBindings() { node_to_id_.clear(); id_to_node_.clear(); children_requested_.clear(); } Node* DomAgentImpl::GetNodeForId(int id) { HashMap::iterator it = id_to_node_.find(id); if (it != id_to_node_.end()) { return it->second; } return NULL; } int DomAgentImpl::GetIdForNode(Node* node) { if (node == NULL) { return 0; } HashMap::iterator it = node_to_id_.find(node); if (it != node_to_id_.end()) { return it->second; } return 0; } void DomAgentImpl::handleEvent(Event* event, bool isWindowEvent) { AtomicString type = event->type(); Node* node = event->target()->toNode(); // Remove mapping entry if necessary. if (type == eventNames().DOMNodeRemovedFromDocumentEvent) { Unbind(node); return; } if (type == eventNames().DOMAttrModifiedEvent) { int id = GetIdForNode(node); if (!id) { // Node is not mapped yet -> ignore the event. return; } Element* element = static_cast(node); OwnPtr attributesValue(BuildValueForElementAttributes(element)); delegate_->AttributesUpdated(id, *attributesValue.get()); } else if (type == eventNames().DOMNodeInsertedEvent) { if (IsWhitespace(node)) { return; } Node* parent = static_cast(event)->relatedNode(); int parent_id = GetIdForNode(parent); if (!parent_id) { // Parent is not mapped yet -> ignore the event. return; } if (!children_requested_.contains(parent_id)) { // No children are mapped yet -> only notify on changes of hasChildren. delegate_->HasChildrenUpdated(parent_id, true); } else { // Children have been requested -> return value of a new child. int prev_id = GetIdForNode(InnerPreviousSibling(node)); OwnPtr value(BuildValueForNode(node, 0)); delegate_->ChildNodeInserted(parent_id, prev_id, *value.get()); } } else if (type == eventNames().DOMNodeRemovedEvent) { if (IsWhitespace(node)) { return; } Node* parent = static_cast(event)->relatedNode(); int parent_id = GetIdForNode(parent); if (!parent_id) { // Parent is not mapped yet -> ignore the event. return; } if (!children_requested_.contains(parent_id)) { // No children are mapped yet -> only notify on changes of hasChildren. if (InnerChildNodeCount(parent) == 1) delegate_->HasChildrenUpdated(parent_id, false); } else { int id = GetIdForNode(node); delegate_->ChildNodeRemoved(parent_id, id); } } else if (type == eventNames().DOMContentLoadedEvent) { // Re-push document once it is loaded. DiscardBindings(); PushDocumentElementToClient(); } } void DomAgentImpl::GetDocumentElement() { Document* main_document = GetMainFrameDocument(); if (main_document) { PushDocumentElementToClient(); } else { document_element_requested_ = true; } } void DomAgentImpl::GetChildNodes(int call_id, int element_id) { PushChildNodesToClient(element_id); delegate_->DidGetChildNodes(call_id); } int DomAgentImpl::PushNodePathToClient(Node* node_to_select) { ASSERT(node_to_select); // Invalid input // Return id in case the node is known. int result = GetIdForNode(node_to_select); if (result) return result; Element* element = InnerParentElement(node_to_select); ASSERT(element); // Node is detached or is a document itself // If we are sending information to the client that is currently being // created. Send root node first. PushDocumentElementToClient(); Vector path; while (element && !GetIdForNode(element)) { path.append(element); element = InnerParentElement(element); } // element is known to the client ASSERT(element); path.append(element); for (int i = path.size() - 1; i >= 0; --i) { int node_id = GetIdForNode(path.at(i)); ASSERT(node_id); PushChildNodesToClient(node_id); } return GetIdForNode(node_to_select); } void DomAgentImpl::SetAttribute( int call_id, int element_id, const String& name, const String& value) { Node* node = GetNodeForId(element_id); if (node && (node->nodeType() == Node::ELEMENT_NODE)) { Element* element = static_cast(node); ExceptionCode ec = 0; element->setAttribute(name, value, ec); delegate_->DidApplyDomChange(call_id, ec == 0); } else { delegate_->DidApplyDomChange(call_id, false); } } void DomAgentImpl::RemoveAttribute( int call_id, int element_id, const String& name) { Node* node = GetNodeForId(element_id); if (node && (node->nodeType() == Node::ELEMENT_NODE)) { Element* element = static_cast(node); ExceptionCode ec = 0; element->removeAttribute(name, ec); delegate_->DidApplyDomChange(call_id, ec == 0); } else { delegate_->DidApplyDomChange(call_id, false); } } void DomAgentImpl::SetTextNodeValue( int call_id, int element_id, const String& value) { Node* node = GetNodeForId(element_id); if (node && (node->nodeType() == Node::TEXT_NODE)) { Text* text_node = static_cast(node); ExceptionCode ec = 0; // TODO(pfeldman): Add error handling text_node->replaceWholeText(value, ec); delegate_->DidApplyDomChange(call_id, ec == 0); } else { delegate_->DidApplyDomChange(call_id, false); } } void DomAgentImpl::PerformSearch(int call_id, const String& query) { String tag_name_query = query; String attribute_name_query = query; bool start_tag_found = tag_name_query.startsWith("<", true); bool end_tag_found = tag_name_query.endsWith(">", true); if (start_tag_found || end_tag_found) { int tag_name_query_length = tag_name_query.length(); int start = start_tag_found ? 1 : 0; int end = end_tag_found ? tag_name_query_length - 1 : tag_name_query_length; tag_name_query = tag_name_query.substring(start, end - start); } Vector xpath_queries; xpath_queries.append(String::format(kPlainText, query.utf8().data(), query.utf8().data())); if (tag_name_query.length() && start_tag_found && end_tag_found) { xpath_queries.append(String::format(kExactTagNames, tag_name_query.utf8().data())); } else if (tag_name_query.length() && start_tag_found) { xpath_queries.append(String::format(kStartOfTagNames, tag_name_query.utf8().data())); } else if (tag_name_query.length() && end_tag_found) { // FIXME(pfeldman): we should have a matchEndOfTagNames search function if // endTagFound is true but not startTagFound. // This requires ends-with() support in XPath, WebKit only supports // starts-with() and contains(). xpath_queries.append(String::format(kPartialTagNames, tag_name_query.utf8().data())); } else if (query == "//*" || query == "*") { // These queries will match every node. Matching everything isn't useful // and can be slow for large pages, so limit the search functions list to // plain text and attribute matching. xpath_queries.append(String::format(kPartialAttributeValues, query.utf8().data())); } else { // TODO(pfeldman): Add more patterns. xpath_queries.append(String::format(kPartialTagNamesAndAttributeValues, tag_name_query.utf8().data(), query.utf8().data())); } ExceptionCode ec = 0; Vector search_documents; Document* main_document = GetMainFrameDocument(); search_documents.append(main_document); // Find all frames, iframes and object elements to search their documents. RefPtr node_list = main_document->querySelectorAll( "iframe, frame, object", ec); if (ec) { ListValue list; delegate_->DidPerformSearch(call_id, list); return; } for (unsigned int i = 0; i < node_list->length(); ++i) { Node* node = node_list->item(i); if (node->isFrameOwnerElement()) { const HTMLFrameOwnerElement* frame_owner = static_cast(node); if (frame_owner->contentDocument()) { search_documents.append(search_documents); } } } HashSet node_ids; for (Vector::iterator it = search_documents.begin(); it != search_documents.end(); ++it) { for (Vector::iterator qit = xpath_queries.begin(); qit != xpath_queries.end(); ++qit) { String query = *qit; RefPtr result = (*it)->evaluate(query, *it, NULL, XPathResult::UNORDERED_NODE_ITERATOR_TYPE, 0, ec); if (ec) { ListValue list; delegate_->DidPerformSearch(call_id, list); return; } Node* node = result->iterateNext(ec); while (node && !ec) { node_ids.add(PushNodePathToClient(node)); node = result->iterateNext(ec); } } } ListValue list; for (HashSet::iterator it = node_ids.begin(); it != node_ids.end(); ++it) { list.Append(Value::CreateIntegerValue(*it)); } delegate_->DidPerformSearch(call_id, list); } ListValue* DomAgentImpl::BuildValueForNode(Node* node, int depth) { OwnPtr value(new ListValue()); int id = Bind(node); String nodeName; String nodeValue; switch (node->nodeType()) { case Node::TEXT_NODE: case Node::COMMENT_NODE: nodeValue = node->nodeValue(); break; case Node::ATTRIBUTE_NODE: case Node::DOCUMENT_NODE: case Node::DOCUMENT_FRAGMENT_NODE: break; case Node::ELEMENT_NODE: default: { nodeName = node->nodeName(); break; } } value->Append(Value::CreateIntegerValue(id)); value->Append(Value::CreateIntegerValue(node->nodeType())); value->Append(Value::CreateStringValue( webkit_glue::StringToStdWString(nodeName))); value->Append(Value::CreateStringValue( webkit_glue::StringToStdWString(nodeValue))); if (node->nodeType() == Node::ELEMENT_NODE) { Element* element = static_cast(node); value->Append(BuildValueForElementAttributes(element)); int nodeCount = InnerChildNodeCount(element); value->Append(Value::CreateIntegerValue(nodeCount)); OwnPtr children(BuildValueForElementChildren(element, depth)); if (children->GetSize() > 0) { value->Append(children.release()); } } return value.release(); } ListValue* DomAgentImpl::BuildValueForElementAttributes(Element* element) { OwnPtr attributes_value(new ListValue()); // Go through all attributes and serialize them. const NamedNodeMap* attr_map = element->attributes(true); if (!attr_map) { return attributes_value.release(); } unsigned num_attrs = attr_map->length(); for (unsigned i = 0; i < num_attrs; ++i) { // Add attribute pair const Attribute *attribute = attr_map->attributeItem(i); OwnPtr name(Value::CreateStringValue( webkit_glue::StringToStdWString(attribute->name().toString()))); OwnPtr value(Value::CreateStringValue( webkit_glue::StringToStdWString(attribute->value()))); attributes_value->Append(name.release()); attributes_value->Append(value.release()); } return attributes_value.release(); } ListValue* DomAgentImpl::BuildValueForElementChildren( Element* element, int depth) { OwnPtr children(new ListValue()); if (depth == 0) { // Special case the_only text child. if (InnerChildNodeCount(element) == 1) { Node *child = InnerFirstChild(element); if (child->nodeType() == Node::TEXT_NODE) { children->Append(BuildValueForNode(child, 0)); } } return children.release(); } else if (depth > 0) { depth--; } for (Node *child = InnerFirstChild(element); child != NULL; child = InnerNextSibling(child)) { children->Append(BuildValueForNode(child, depth)); } return children.release(); } Node* DomAgentImpl::InnerFirstChild(Node* node) { if (node->isFrameOwnerElement()) { HTMLFrameOwnerElement* frame_owner = static_cast(node); Document* doc = frame_owner->contentDocument(); if (doc) { StartListening(doc); return doc->firstChild(); } } node = node->firstChild(); while (IsWhitespace(node)) { node = node->nextSibling(); } return node; } Node* DomAgentImpl::InnerNextSibling(Node* node) { do { node = node->nextSibling(); } while (IsWhitespace(node)); return node; } Node* DomAgentImpl::InnerPreviousSibling(Node* node) { do { node = node->previousSibling(); } while (IsWhitespace(node)); return node; } int DomAgentImpl::InnerChildNodeCount(Node* node) { int count = 0; Node* child = InnerFirstChild(node); while (child) { count++; child = InnerNextSibling(child); } return count; } Element* DomAgentImpl::InnerParentElement(Node* node) { Element* element = node->parentElement(); if (!element) { return node->ownerDocument()->ownerElement(); } return element; } bool DomAgentImpl::IsWhitespace(Node* node) { return node != NULL && node->nodeType() == Node::TEXT_NODE && node->nodeValue().stripWhiteSpace().length() == 0; } Document* DomAgentImpl::GetMainFrameDocument() { ListHashSet >::iterator it = documents_.begin(); if (it != documents_.end()) { return it->get(); } return NULL; }