blob: 2c51503b789c1b26db56b995352d43268ad61ed5 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/inspector/dom_patch_support.h"
#include <memory>
#include "base/memory/scoped_refptr.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/core/dom/attribute.h"
#include "third_party/blink/renderer/core/dom/context_features.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_fragment.h"
#include "third_party/blink/renderer/core/dom/document_init.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/node_traversal.h"
#include "third_party/blink/renderer/core/dom/xml_document.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/html/html_document.h"
#include "third_party/blink/renderer/core/html/html_head_element.h"
#include "third_party/blink/renderer/core/html/parser/html_document_parser.h"
#include "third_party/blink/renderer/core/inspector/add_string_to_digestor.h"
#include "third_party/blink/renderer/core/inspector/dom_editor.h"
#include "third_party/blink/renderer/core/inspector/inspector_history.h"
#include "third_party/blink/renderer/core/xml/parser/xml_document_parser.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/crypto.h"
#include "third_party/blink/renderer/platform/wtf/deque.h"
#include "third_party/blink/renderer/platform/wtf/hash_traits.h"
#include "third_party/blink/renderer/platform/wtf/text/base64.h"
#include "third_party/blink/renderer/platform/wtf/text/cstring.h"
namespace blink {
DOMPatchSupport::DOMPatchSupport(DOMEditor* dom_editor, Document& document)
: dom_editor_(dom_editor), document_(document) {}
void DOMPatchSupport::PatchDocument(const String& markup) {
Document* new_document = nullptr;
DocumentInit init = DocumentInit::Create();
if (GetDocument().IsHTMLDocument())
new_document = HTMLDocument::Create(init);
else if (GetDocument().IsSVGDocument())
new_document = XMLDocument::CreateSVG(init);
else if (GetDocument().IsXHTMLDocument())
new_document = XMLDocument::CreateXHTML(init);
else if (GetDocument().IsXMLDocument())
new_document = XMLDocument::Create(init);
DCHECK(new_document);
new_document->SetContextFeatures(GetDocument().GetContextFeatures());
if (!GetDocument().IsHTMLDocument()) {
DocumentParser* parser = XMLDocumentParser::Create(*new_document, nullptr);
parser->Append(markup);
parser->Finish();
parser->Detach();
// Avoid breakage on non-well-formed documents.
if (!static_cast<XMLDocumentParser*>(parser)->WellFormed())
return;
}
new_document->SetContent(markup);
Digest* old_info = CreateDigest(GetDocument().documentElement(), nullptr);
Digest* new_info =
CreateDigest(new_document->documentElement(), &unused_nodes_map_);
if (!InnerPatchNode(old_info, new_info, IGNORE_EXCEPTION_FOR_TESTING)) {
// Fall back to rewrite.
GetDocument().write(markup);
GetDocument().close();
}
}
Node* DOMPatchSupport::PatchNode(Node* node,
const String& markup,
ExceptionState& exception_state) {
// Don't parse <html> as a fragment.
if (node->IsDocumentNode() ||
(node->parentNode() && node->parentNode()->IsDocumentNode())) {
PatchDocument(markup);
return nullptr;
}
Node* previous_sibling = node->previousSibling();
DocumentFragment* fragment = DocumentFragment::Create(GetDocument());
Node* target_node = node->ParentElementOrShadowRoot()
? node->ParentElementOrShadowRoot()
: GetDocument().documentElement();
// Use the document BODY as the context element when editing immediate shadow
// root children, as it provides an equivalent parsing context.
if (target_node->IsShadowRoot())
target_node = GetDocument().body();
Element* target_element = ToElement(target_node);
// FIXME: This code should use one of createFragment* in Serialization.h
if (GetDocument().IsHTMLDocument())
fragment->ParseHTML(markup, target_element);
else
fragment->ParseXML(markup, target_element);
// Compose the old list.
ContainerNode* parent_node = node->parentNode();
HeapVector<Member<Digest>> old_list;
for (Node* child = parent_node->firstChild(); child;
child = child->nextSibling())
old_list.push_back(CreateDigest(child, nullptr));
// Compose the new list.
String markup_copy = markup.DeprecatedLower();
HeapVector<Member<Digest>> new_list;
for (Node* child = parent_node->firstChild(); child != node;
child = child->nextSibling())
new_list.push_back(CreateDigest(child, nullptr));
for (Node* child = fragment->firstChild(); child;
child = child->nextSibling()) {
if (IsHTMLHeadElement(*child) && !child->hasChildren() &&
markup_copy.Find("</head>") == kNotFound) {
// HTML5 parser inserts empty <head> tag whenever it parses <body>
continue;
}
if (IsHTMLBodyElement(*child) && !child->hasChildren() &&
markup_copy.Find("</body>") == kNotFound) {
// HTML5 parser inserts empty <body> tag whenever it parses </head>
continue;
}
new_list.push_back(CreateDigest(child, &unused_nodes_map_));
}
for (Node* child = node->nextSibling(); child; child = child->nextSibling())
new_list.push_back(CreateDigest(child, nullptr));
if (!InnerPatchChildren(parent_node, old_list, new_list, exception_state)) {
// Fall back to total replace.
if (!dom_editor_->ReplaceChild(parent_node, fragment, node,
exception_state))
return nullptr;
}
return previous_sibling ? previous_sibling->nextSibling()
: parent_node->firstChild();
}
bool DOMPatchSupport::InnerPatchNode(Digest* old_digest,
Digest* new_digest,
ExceptionState& exception_state) {
if (old_digest->sha1_ == new_digest->sha1_)
return true;
Node* old_node = old_digest->node_;
Node* new_node = new_digest->node_;
if (new_node->getNodeType() != old_node->getNodeType() ||
new_node->nodeName() != old_node->nodeName())
return dom_editor_->ReplaceChild(old_node->parentNode(), new_node, old_node,
exception_state);
if (old_node->nodeValue() != new_node->nodeValue()) {
if (!dom_editor_->SetNodeValue(old_node, new_node->nodeValue(),
exception_state))
return false;
}
if (!old_node->IsElementNode())
return true;
// Patch attributes
Element* old_element = ToElement(old_node);
Element* new_element = ToElement(new_node);
if (old_digest->attrs_sha1_ != new_digest->attrs_sha1_) {
// FIXME: Create a function in Element for removing all properties. Take in
// account whether did/willModifyAttribute are important.
while (old_element->AttributesWithoutUpdate().size()) {
const Attribute& attribute = old_element->AttributesWithoutUpdate().at(0);
if (!dom_editor_->RemoveAttribute(
old_element, attribute.GetName().ToString(), exception_state))
return false;
}
// FIXME: Create a function in Element for copying properties.
// cloneDataFromElement() is close but not enough for this case.
for (auto& attribute : new_element->AttributesWithoutUpdate()) {
if (!dom_editor_->SetAttribute(old_element,
attribute.GetName().ToString(),
attribute.Value(), exception_state))
return false;
}
}
bool result = InnerPatchChildren(old_element, old_digest->children_,
new_digest->children_, exception_state);
unused_nodes_map_.erase(new_digest->sha1_);
return result;
}
std::pair<DOMPatchSupport::ResultMap, DOMPatchSupport::ResultMap>
DOMPatchSupport::Diff(const HeapVector<Member<Digest>>& old_list,
const HeapVector<Member<Digest>>& new_list) {
ResultMap new_map(new_list.size());
ResultMap old_map(old_list.size());
for (wtf_size_t i = 0; i < old_map.size(); ++i) {
old_map[i].first = nullptr;
old_map[i].second = 0;
}
for (wtf_size_t i = 0; i < new_map.size(); ++i) {
new_map[i].first = nullptr;
new_map[i].second = 0;
}
// Trim head and tail.
for (wtf_size_t i = 0; i < old_list.size() && i < new_list.size() &&
old_list[i]->sha1_ == new_list[i]->sha1_;
++i) {
old_map[i].first = old_list[i].Get();
old_map[i].second = i;
new_map[i].first = new_list[i].Get();
new_map[i].second = i;
}
for (wtf_size_t i = 0; i < old_list.size() && i < new_list.size() &&
old_list[old_list.size() - i - 1]->sha1_ ==
new_list[new_list.size() - i - 1]->sha1_;
++i) {
wtf_size_t old_index = old_list.size() - i - 1;
wtf_size_t new_index = new_list.size() - i - 1;
old_map[old_index].first = old_list[old_index].Get();
old_map[old_index].second = new_index;
new_map[new_index].first = new_list[new_index].Get();
new_map[new_index].second = old_index;
}
typedef HashMap<String, Vector<wtf_size_t>> DiffTable;
DiffTable new_table;
DiffTable old_table;
for (wtf_size_t i = 0; i < new_list.size(); ++i) {
new_table.insert(new_list[i]->sha1_, Vector<wtf_size_t>())
.stored_value->value.push_back(i);
}
for (wtf_size_t i = 0; i < old_list.size(); ++i) {
old_table.insert(old_list[i]->sha1_, Vector<wtf_size_t>())
.stored_value->value.push_back(i);
}
for (auto& new_it : new_table) {
if (new_it.value.size() != 1)
continue;
DiffTable::iterator old_it = old_table.find(new_it.key);
if (old_it == old_table.end() || old_it->value.size() != 1)
continue;
new_map[new_it.value[0]] =
std::make_pair(new_list[new_it.value[0]].Get(), old_it->value[0]);
old_map[old_it->value[0]] =
std::make_pair(old_list[old_it->value[0]].Get(), new_it.value[0]);
}
for (wtf_size_t i = 0; new_list.size() > 0 && i < new_list.size() - 1; ++i) {
if (!new_map[i].first || new_map[i + 1].first)
continue;
wtf_size_t j = new_map[i].second + 1;
if (j < old_map.size() && !old_map[j].first &&
new_list[i + 1]->sha1_ == old_list[j]->sha1_) {
new_map[i + 1] = std::make_pair(new_list[i + 1].Get(), j);
old_map[j] = std::make_pair(old_list[j].Get(), i + 1);
}
}
for (wtf_size_t i = new_list.size() - 1; new_list.size() > 0 && i > 0; --i) {
if (!new_map[i].first || new_map[i - 1].first || new_map[i].second <= 0)
continue;
wtf_size_t j = new_map[i].second - 1;
if (!old_map[j].first && new_list[i - 1]->sha1_ == old_list[j]->sha1_) {
new_map[i - 1] = std::make_pair(new_list[i - 1].Get(), j);
old_map[j] = std::make_pair(old_list[j].Get(), i - 1);
}
}
return std::make_pair(old_map, new_map);
}
bool DOMPatchSupport::InnerPatchChildren(
ContainerNode* parent_node,
const HeapVector<Member<Digest>>& old_list,
const HeapVector<Member<Digest>>& new_list,
ExceptionState& exception_state) {
std::pair<ResultMap, ResultMap> result_maps = Diff(old_list, new_list);
ResultMap& old_map = result_maps.first;
ResultMap& new_map = result_maps.second;
Digest* old_head = nullptr;
Digest* old_body = nullptr;
// 1. First strip everything except for the nodes that retain. Collect pending
// merges.
HeapHashMap<Member<Digest>, Member<Digest>> merges;
HashSet<wtf_size_t, WTF::IntHash<wtf_size_t>,
WTF::UnsignedWithZeroKeyHashTraits<wtf_size_t>>
used_new_ordinals;
for (wtf_size_t i = 0; i < old_list.size(); ++i) {
if (old_map[i].first) {
if (used_new_ordinals.insert(old_map[i].second).is_new_entry)
continue;
old_map[i].first = nullptr;
old_map[i].second = 0;
}
// Always match <head> and <body> tags with each other - we can't remove
// them from the DOM upon patching.
if (IsHTMLHeadElement(*old_list[i]->node_)) {
old_head = old_list[i].Get();
continue;
}
if (IsHTMLBodyElement(*old_list[i]->node_)) {
old_body = old_list[i].Get();
continue;
}
// Check if this change is between stable nodes. If it is, consider it as
// "modified".
if (!unused_nodes_map_.Contains(old_list[i]->sha1_) &&
(!i || old_map[i - 1].first) &&
(i == old_map.size() - 1 || old_map[i + 1].first)) {
wtf_size_t anchor_candidate = i ? old_map[i - 1].second + 1 : 0;
wtf_size_t anchor_after = (i == old_map.size() - 1)
? anchor_candidate + 1
: old_map[i + 1].second;
if (anchor_after - anchor_candidate == 1 &&
anchor_candidate < new_list.size())
merges.Set(new_list[anchor_candidate].Get(), old_list[i].Get());
else {
if (!RemoveChildAndMoveToNew(old_list[i].Get(), exception_state))
return false;
}
} else {
if (!RemoveChildAndMoveToNew(old_list[i].Get(), exception_state))
return false;
}
}
// Mark retained nodes as used, do not reuse node more than once.
HashSet<wtf_size_t, WTF::IntHash<wtf_size_t>,
WTF::UnsignedWithZeroKeyHashTraits<wtf_size_t>>
used_old_ordinals;
for (wtf_size_t i = 0; i < new_list.size(); ++i) {
if (!new_map[i].first)
continue;
wtf_size_t old_ordinal = new_map[i].second;
if (used_old_ordinals.Contains(old_ordinal)) {
// Do not map node more than once
new_map[i].first = nullptr;
new_map[i].second = 0;
continue;
}
used_old_ordinals.insert(old_ordinal);
MarkNodeAsUsed(new_map[i].first);
}
// Mark <head> and <body> nodes for merge.
if (old_head || old_body) {
for (wtf_size_t i = 0; i < new_list.size(); ++i) {
if (old_head && IsHTMLHeadElement(*new_list[i]->node_))
merges.Set(new_list[i].Get(), old_head);
if (old_body && IsHTMLBodyElement(*new_list[i]->node_))
merges.Set(new_list[i].Get(), old_body);
}
}
// 2. Patch nodes marked for merge.
for (auto& merge : merges) {
if (!InnerPatchNode(merge.value, merge.key, exception_state))
return false;
}
// 3. Insert missing nodes.
for (wtf_size_t i = 0; i < new_map.size(); ++i) {
if (new_map[i].first || merges.Contains(new_list[i].Get()))
continue;
if (!InsertBeforeAndMarkAsUsed(parent_node, new_list[i].Get(),
NodeTraversal::ChildAt(*parent_node, i),
exception_state))
return false;
}
// 4. Then put all nodes that retained into their slots (sort by new index).
for (wtf_size_t i = 0; i < old_map.size(); ++i) {
if (!old_map[i].first)
continue;
Node* node = old_map[i].first->node_;
Node* anchor_node = NodeTraversal::ChildAt(*parent_node, old_map[i].second);
if (node == anchor_node)
continue;
if (IsHTMLBodyElement(*node) || IsHTMLHeadElement(*node)) {
// Never move head or body, move the rest of the nodes around them.
continue;
}
if (!dom_editor_->InsertBefore(parent_node, node, anchor_node,
exception_state))
return false;
}
return true;
}
DOMPatchSupport::Digest* DOMPatchSupport::CreateDigest(
Node* node,
UnusedNodesMap* unused_nodes_map) {
Digest* digest = MakeGarbageCollected<Digest>(node);
std::unique_ptr<WebCryptoDigestor> digestor =
CreateDigestor(kHashAlgorithmSha1);
DigestValue digest_result;
Node::NodeType node_type = node->getNodeType();
digestor->Consume(reinterpret_cast<const unsigned char*>(&node_type),
sizeof(node_type));
AddStringToDigestor(digestor.get(), node->nodeName());
AddStringToDigestor(digestor.get(), node->nodeValue());
if (node->IsElementNode()) {
Element& element = ToElement(*node);
Node* child = element.firstChild();
while (child) {
Digest* child_info = CreateDigest(child, unused_nodes_map);
AddStringToDigestor(digestor.get(), child_info->sha1_);
child = child->nextSibling();
digest->children_.push_back(child_info);
}
AttributeCollection attributes = element.AttributesWithoutUpdate();
if (!attributes.IsEmpty()) {
std::unique_ptr<WebCryptoDigestor> attrs_digestor =
CreateDigestor(kHashAlgorithmSha1);
for (auto& attribute : attributes) {
AddStringToDigestor(attrs_digestor.get(),
attribute.GetName().ToString());
AddStringToDigestor(attrs_digestor.get(),
attribute.Value().GetString());
}
FinishDigestor(attrs_digestor.get(), digest_result);
digest->attrs_sha1_ =
Base64Encode(reinterpret_cast<const char*>(digest_result.data()), 10);
AddStringToDigestor(digestor.get(), digest->attrs_sha1_);
digest_result.clear();
}
}
FinishDigestor(digestor.get(), digest_result);
digest->sha1_ =
Base64Encode(reinterpret_cast<const char*>(digest_result.data()), 10);
if (unused_nodes_map)
unused_nodes_map->insert(digest->sha1_, digest);
return digest;
}
bool DOMPatchSupport::InsertBeforeAndMarkAsUsed(
ContainerNode* parent_node,
Digest* digest,
Node* anchor,
ExceptionState& exception_state) {
bool result = dom_editor_->InsertBefore(parent_node, digest->node_, anchor,
exception_state);
MarkNodeAsUsed(digest);
return result;
}
bool DOMPatchSupport::RemoveChildAndMoveToNew(Digest* old_digest,
ExceptionState& exception_state) {
Node* old_node = old_digest->node_;
if (!dom_editor_->RemoveChild(old_node->parentNode(), old_node,
exception_state))
return false;
// Diff works within levels. In order not to lose the node identity when user
// prepends their HTML with "<div>" (i.e. all nodes are shifted to the next
// nested level), prior to dropping the original node on the floor, check
// whether new DOM has a digest with matching sha1. If it does, replace it
// with the original DOM chunk. Chances are high that it will get merged back
// into the original DOM during the further patching.
UnusedNodesMap::iterator it = unused_nodes_map_.find(old_digest->sha1_);
if (it != unused_nodes_map_.end()) {
Digest* new_digest = it->value;
Node* new_node = new_digest->node_;
if (!dom_editor_->ReplaceChild(new_node->parentNode(), old_node, new_node,
exception_state))
return false;
new_digest->node_ = old_node;
MarkNodeAsUsed(new_digest);
return true;
}
for (wtf_size_t i = 0; i < old_digest->children_.size(); ++i) {
if (!RemoveChildAndMoveToNew(old_digest->children_[i].Get(),
exception_state))
return false;
}
return true;
}
void DOMPatchSupport::MarkNodeAsUsed(Digest* digest) {
HeapDeque<Member<Digest>> queue;
queue.push_back(digest);
while (!queue.IsEmpty()) {
Digest* first = queue.TakeFirst();
unused_nodes_map_.erase(first->sha1_);
for (wtf_size_t i = 0; i < first->children_.size(); ++i)
queue.push_back(first->children_[i].Get());
}
}
void DOMPatchSupport::Digest::Trace(blink::Visitor* visitor) {
visitor->Trace(node_);
visitor->Trace(children_);
}
} // namespace blink