| /* |
| * 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 |