1 /*
2 * Copyright (C) 2012 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31 #include "config.h"
32 #include "core/inspector/DOMPatchSupport.h"
33
34 #include "HTMLNames.h"
35 #include "bindings/v8/ExceptionState.h"
36 #include "bindings/v8/ExceptionStatePlaceholder.h"
37 #include "core/dom/Attribute.h"
38 #include "core/dom/ContextFeatures.h"
39 #include "core/dom/Document.h"
40 #include "core/dom/DocumentFragment.h"
41 #include "core/dom/Node.h"
42 #include "core/html/HTMLDocument.h"
43 #include "core/html/parser/HTMLDocumentParser.h"
44 #include "core/inspector/DOMEditor.h"
45 #include "core/inspector/InspectorHistory.h"
46 #include "core/xml/parser/XMLDocumentParser.h"
47 #include "wtf/Deque.h"
48 #include "wtf/HashTraits.h"
49 #include "wtf/RefPtr.h"
50 #include "wtf/SHA1.h"
51 #include "wtf/text/Base64.h"
52 #include "wtf/text/CString.h"
53
54 using namespace std;
55
56 namespace WebCore {
57
58 using HTMLNames::bodyTag;
59 using HTMLNames::headTag;
60 using HTMLNames::htmlTag;
61
62 struct DOMPatchSupport::Digest {
DigestWebCore::DOMPatchSupport::Digest63 explicit Digest(Node* node) : m_node(node) { }
64
65 String m_sha1;
66 String m_attrsSHA1;
67 Node* m_node;
68 Vector<OwnPtr<Digest> > m_children;
69 };
70
patchDocument(Document & document,const String & markup)71 void DOMPatchSupport::patchDocument(Document& document, const String& markup)
72 {
73 InspectorHistory history;
74 DOMEditor domEditor(&history);
75 DOMPatchSupport patchSupport(&domEditor, document);
76 patchSupport.patchDocument(markup);
77 }
78
DOMPatchSupport(DOMEditor * domEditor,Document & document)79 DOMPatchSupport::DOMPatchSupport(DOMEditor* domEditor, Document& document)
80 : m_domEditor(domEditor)
81 , m_document(document)
82 {
83 }
84
patchDocument(const String & markup)85 void DOMPatchSupport::patchDocument(const String& markup)
86 {
87 RefPtr<Document> newDocument;
88 if (m_document.isHTMLDocument())
89 newDocument = HTMLDocument::create();
90 else if (m_document.isXHTMLDocument())
91 newDocument = HTMLDocument::createXHTML();
92 else if (m_document.isSVGDocument())
93 newDocument = Document::create();
94
95 ASSERT(newDocument);
96 newDocument->setContextFeatures(m_document.contextFeatures());
97 RefPtr<DocumentParser> parser;
98 if (m_document.isHTMLDocument())
99 parser = HTMLDocumentParser::create(toHTMLDocument(newDocument.get()), false);
100 else
101 parser = XMLDocumentParser::create(newDocument.get(), 0);
102 parser->insert(markup); // Use insert() so that the parser will not yield.
103 parser->finish();
104 parser->detach();
105
106 OwnPtr<Digest> oldInfo = createDigest(m_document.documentElement(), 0);
107 OwnPtr<Digest> newInfo = createDigest(newDocument->documentElement(), &m_unusedNodesMap);
108
109 if (!innerPatchNode(oldInfo.get(), newInfo.get(), IGNORE_EXCEPTION)) {
110 // Fall back to rewrite.
111 m_document.write(markup);
112 m_document.close();
113 }
114 }
115
patchNode(Node * node,const String & markup,ExceptionState & exceptionState)116 Node* DOMPatchSupport::patchNode(Node* node, const String& markup, ExceptionState& exceptionState)
117 {
118 // Don't parse <html> as a fragment.
119 if (node->isDocumentNode() || (node->parentNode() && node->parentNode()->isDocumentNode())) {
120 patchDocument(markup);
121 return 0;
122 }
123
124 Node* previousSibling = node->previousSibling();
125 // FIXME: This code should use one of createFragment* in markup.h
126 RefPtr<DocumentFragment> fragment = DocumentFragment::create(m_document);
127 if (m_document.isHTMLDocument())
128 fragment->parseHTML(markup, node->parentElement() ? node->parentElement() : m_document.documentElement());
129 else
130 fragment->parseXML(markup, node->parentElement() ? node->parentElement() : m_document.documentElement());
131
132 // Compose the old list.
133 ContainerNode* parentNode = node->parentNode();
134 Vector<OwnPtr<Digest> > oldList;
135 for (Node* child = parentNode->firstChild(); child; child = child->nextSibling())
136 oldList.append(createDigest(child, 0));
137
138 // Compose the new list.
139 String markupCopy = markup.lower();
140 Vector<OwnPtr<Digest> > newList;
141 for (Node* child = parentNode->firstChild(); child != node; child = child->nextSibling())
142 newList.append(createDigest(child, 0));
143 for (Node* child = fragment->firstChild(); child; child = child->nextSibling()) {
144 if (child->hasTagName(headTag) && !child->firstChild() && markupCopy.find("</head>") == kNotFound)
145 continue; // HTML5 parser inserts empty <head> tag whenever it parses <body>
146 if (child->hasTagName(bodyTag) && !child->firstChild() && markupCopy.find("</body>") == kNotFound)
147 continue; // HTML5 parser inserts empty <body> tag whenever it parses </head>
148 newList.append(createDigest(child, &m_unusedNodesMap));
149 }
150 for (Node* child = node->nextSibling(); child; child = child->nextSibling())
151 newList.append(createDigest(child, 0));
152
153 if (!innerPatchChildren(parentNode, oldList, newList, exceptionState)) {
154 // Fall back to total replace.
155 if (!m_domEditor->replaceChild(parentNode, fragment.release(), node, exceptionState))
156 return 0;
157 }
158 return previousSibling ? previousSibling->nextSibling() : parentNode->firstChild();
159 }
160
innerPatchNode(Digest * oldDigest,Digest * newDigest,ExceptionState & exceptionState)161 bool DOMPatchSupport::innerPatchNode(Digest* oldDigest, Digest* newDigest, ExceptionState& exceptionState)
162 {
163 if (oldDigest->m_sha1 == newDigest->m_sha1)
164 return true;
165
166 Node* oldNode = oldDigest->m_node;
167 Node* newNode = newDigest->m_node;
168
169 if (newNode->nodeType() != oldNode->nodeType() || newNode->nodeName() != oldNode->nodeName())
170 return m_domEditor->replaceChild(oldNode->parentNode(), newNode, oldNode, exceptionState);
171
172 if (oldNode->nodeValue() != newNode->nodeValue()) {
173 if (!m_domEditor->setNodeValue(oldNode, newNode->nodeValue(), exceptionState))
174 return false;
175 }
176
177 if (oldNode->nodeType() != Node::ELEMENT_NODE)
178 return true;
179
180 // Patch attributes
181 Element* oldElement = toElement(oldNode);
182 Element* newElement = toElement(newNode);
183 if (oldDigest->m_attrsSHA1 != newDigest->m_attrsSHA1) {
184 // FIXME: Create a function in Element for removing all properties. Take in account whether did/willModifyAttribute are important.
185 if (oldElement->hasAttributesWithoutUpdate()) {
186 while (oldElement->attributeCount()) {
187 const Attribute* attribute = oldElement->attributeItem(0);
188 if (!m_domEditor->removeAttribute(oldElement, attribute->localName(), exceptionState))
189 return false;
190 }
191 }
192
193 // FIXME: Create a function in Element for copying properties. cloneDataFromElement() is close but not enough for this case.
194 if (newElement->hasAttributesWithoutUpdate()) {
195 size_t numAttrs = newElement->attributeCount();
196 for (size_t i = 0; i < numAttrs; ++i) {
197 const Attribute* attribute = newElement->attributeItem(i);
198 if (!m_domEditor->setAttribute(oldElement, attribute->name().localName(), attribute->value(), exceptionState))
199 return false;
200 }
201 }
202 }
203
204 bool result = innerPatchChildren(oldElement, oldDigest->m_children, newDigest->m_children, exceptionState);
205 m_unusedNodesMap.remove(newDigest->m_sha1);
206 return result;
207 }
208
209 pair<DOMPatchSupport::ResultMap, DOMPatchSupport::ResultMap>
diff(const Vector<OwnPtr<Digest>> & oldList,const Vector<OwnPtr<Digest>> & newList)210 DOMPatchSupport::diff(const Vector<OwnPtr<Digest> >& oldList, const Vector<OwnPtr<Digest> >& newList)
211 {
212 ResultMap newMap(newList.size());
213 ResultMap oldMap(oldList.size());
214
215 for (size_t i = 0; i < oldMap.size(); ++i) {
216 oldMap[i].first = 0;
217 oldMap[i].second = 0;
218 }
219
220 for (size_t i = 0; i < newMap.size(); ++i) {
221 newMap[i].first = 0;
222 newMap[i].second = 0;
223 }
224
225 // Trim head and tail.
226 for (size_t i = 0; i < oldList.size() && i < newList.size() && oldList[i]->m_sha1 == newList[i]->m_sha1; ++i) {
227 oldMap[i].first = oldList[i].get();
228 oldMap[i].second = i;
229 newMap[i].first = newList[i].get();
230 newMap[i].second = i;
231 }
232 for (size_t i = 0; i < oldList.size() && i < newList.size() && oldList[oldList.size() - i - 1]->m_sha1 == newList[newList.size() - i - 1]->m_sha1; ++i) {
233 size_t oldIndex = oldList.size() - i - 1;
234 size_t newIndex = newList.size() - i - 1;
235 oldMap[oldIndex].first = oldList[oldIndex].get();
236 oldMap[oldIndex].second = newIndex;
237 newMap[newIndex].first = newList[newIndex].get();
238 newMap[newIndex].second = oldIndex;
239 }
240
241 typedef HashMap<String, Vector<size_t> > DiffTable;
242 DiffTable newTable;
243 DiffTable oldTable;
244
245 for (size_t i = 0; i < newList.size(); ++i) {
246 DiffTable::iterator it = newTable.add(newList[i]->m_sha1, Vector<size_t>()).iterator;
247 it->value.append(i);
248 }
249
250 for (size_t i = 0; i < oldList.size(); ++i) {
251 DiffTable::iterator it = oldTable.add(oldList[i]->m_sha1, Vector<size_t>()).iterator;
252 it->value.append(i);
253 }
254
255 for (DiffTable::iterator newIt = newTable.begin(); newIt != newTable.end(); ++newIt) {
256 if (newIt->value.size() != 1)
257 continue;
258
259 DiffTable::iterator oldIt = oldTable.find(newIt->key);
260 if (oldIt == oldTable.end() || oldIt->value.size() != 1)
261 continue;
262
263 newMap[newIt->value[0]] = make_pair(newList[newIt->value[0]].get(), oldIt->value[0]);
264 oldMap[oldIt->value[0]] = make_pair(oldList[oldIt->value[0]].get(), newIt->value[0]);
265 }
266
267 for (size_t i = 0; newList.size() > 0 && i < newList.size() - 1; ++i) {
268 if (!newMap[i].first || newMap[i + 1].first)
269 continue;
270
271 size_t j = newMap[i].second + 1;
272 if (j < oldMap.size() && !oldMap[j].first && newList[i + 1]->m_sha1 == oldList[j]->m_sha1) {
273 newMap[i + 1] = make_pair(newList[i + 1].get(), j);
274 oldMap[j] = make_pair(oldList[j].get(), i + 1);
275 }
276 }
277
278 for (size_t i = newList.size() - 1; newList.size() > 0 && i > 0; --i) {
279 if (!newMap[i].first || newMap[i - 1].first || newMap[i].second <= 0)
280 continue;
281
282 size_t j = newMap[i].second - 1;
283 if (!oldMap[j].first && newList[i - 1]->m_sha1 == oldList[j]->m_sha1) {
284 newMap[i - 1] = make_pair(newList[i - 1].get(), j);
285 oldMap[j] = make_pair(oldList[j].get(), i - 1);
286 }
287 }
288
289 #ifdef DEBUG_DOM_PATCH_SUPPORT
290 dumpMap(oldMap, "OLD");
291 dumpMap(newMap, "NEW");
292 #endif
293
294 return make_pair(oldMap, newMap);
295 }
296
innerPatchChildren(ContainerNode * parentNode,const Vector<OwnPtr<Digest>> & oldList,const Vector<OwnPtr<Digest>> & newList,ExceptionState & exceptionState)297 bool DOMPatchSupport::innerPatchChildren(ContainerNode* parentNode, const Vector<OwnPtr<Digest> >& oldList, const Vector<OwnPtr<Digest> >& newList, ExceptionState& exceptionState)
298 {
299 pair<ResultMap, ResultMap> resultMaps = diff(oldList, newList);
300 ResultMap& oldMap = resultMaps.first;
301 ResultMap& newMap = resultMaps.second;
302
303 Digest* oldHead = 0;
304 Digest* oldBody = 0;
305
306 // 1. First strip everything except for the nodes that retain. Collect pending merges.
307 HashMap<Digest*, Digest*> merges;
308 HashSet<size_t, WTF::IntHash<size_t>, WTF::UnsignedWithZeroKeyHashTraits<size_t> > usedNewOrdinals;
309 for (size_t i = 0; i < oldList.size(); ++i) {
310 if (oldMap[i].first) {
311 if (usedNewOrdinals.add(oldMap[i].second).isNewEntry)
312 continue;
313 oldMap[i].first = 0;
314 oldMap[i].second = 0;
315 }
316
317 // Always match <head> and <body> tags with each other - we can't remove them from the DOM
318 // upon patching.
319 if (oldList[i]->m_node->hasTagName(headTag)) {
320 oldHead = oldList[i].get();
321 continue;
322 }
323 if (oldList[i]->m_node->hasTagName(bodyTag)) {
324 oldBody = oldList[i].get();
325 continue;
326 }
327
328 // Check if this change is between stable nodes. If it is, consider it as "modified".
329 if (!m_unusedNodesMap.contains(oldList[i]->m_sha1) && (!i || oldMap[i - 1].first) && (i == oldMap.size() - 1 || oldMap[i + 1].first)) {
330 size_t anchorCandidate = i ? oldMap[i - 1].second + 1 : 0;
331 size_t anchorAfter = (i == oldMap.size() - 1) ? anchorCandidate + 1 : oldMap[i + 1].second;
332 if (anchorAfter - anchorCandidate == 1 && anchorCandidate < newList.size())
333 merges.set(newList[anchorCandidate].get(), oldList[i].get());
334 else {
335 if (!removeChildAndMoveToNew(oldList[i].get(), exceptionState))
336 return false;
337 }
338 } else {
339 if (!removeChildAndMoveToNew(oldList[i].get(), exceptionState))
340 return false;
341 }
342 }
343
344 // Mark retained nodes as used, do not reuse node more than once.
345 HashSet<size_t, WTF::IntHash<size_t>, WTF::UnsignedWithZeroKeyHashTraits<size_t> > usedOldOrdinals;
346 for (size_t i = 0; i < newList.size(); ++i) {
347 if (!newMap[i].first)
348 continue;
349 size_t oldOrdinal = newMap[i].second;
350 if (usedOldOrdinals.contains(oldOrdinal)) {
351 // Do not map node more than once
352 newMap[i].first = 0;
353 newMap[i].second = 0;
354 continue;
355 }
356 usedOldOrdinals.add(oldOrdinal);
357 markNodeAsUsed(newMap[i].first);
358 }
359
360 // Mark <head> and <body> nodes for merge.
361 if (oldHead || oldBody) {
362 for (size_t i = 0; i < newList.size(); ++i) {
363 if (oldHead && newList[i]->m_node->hasTagName(headTag))
364 merges.set(newList[i].get(), oldHead);
365 if (oldBody && newList[i]->m_node->hasTagName(bodyTag))
366 merges.set(newList[i].get(), oldBody);
367 }
368 }
369
370 // 2. Patch nodes marked for merge.
371 for (HashMap<Digest*, Digest*>::iterator it = merges.begin(); it != merges.end(); ++it) {
372 if (!innerPatchNode(it->value, it->key, exceptionState))
373 return false;
374 }
375
376 // 3. Insert missing nodes.
377 for (size_t i = 0; i < newMap.size(); ++i) {
378 if (newMap[i].first || merges.contains(newList[i].get()))
379 continue;
380 if (!insertBeforeAndMarkAsUsed(parentNode, newList[i].get(), parentNode->childNode(i), exceptionState))
381 return false;
382 }
383
384 // 4. Then put all nodes that retained into their slots (sort by new index).
385 for (size_t i = 0; i < oldMap.size(); ++i) {
386 if (!oldMap[i].first)
387 continue;
388 RefPtr<Node> node = oldMap[i].first->m_node;
389 Node* anchorNode = parentNode->childNode(oldMap[i].second);
390 if (node.get() == anchorNode)
391 continue;
392 if (node->hasTagName(bodyTag) || node->hasTagName(headTag))
393 continue; // Never move head or body, move the rest of the nodes around them.
394
395 if (!m_domEditor->insertBefore(parentNode, node.release(), anchorNode, exceptionState))
396 return false;
397 }
398 return true;
399 }
400
addStringToSHA1(SHA1 & sha1,const String & string)401 static void addStringToSHA1(SHA1& sha1, const String& string)
402 {
403 CString cString = string.utf8();
404 sha1.addBytes(reinterpret_cast<const uint8_t*>(cString.data()), cString.length());
405 }
406
createDigest(Node * node,UnusedNodesMap * unusedNodesMap)407 PassOwnPtr<DOMPatchSupport::Digest> DOMPatchSupport::createDigest(Node* node, UnusedNodesMap* unusedNodesMap)
408 {
409 Digest* digest = new Digest(node);
410
411 SHA1 sha1;
412
413 Node::NodeType nodeType = node->nodeType();
414 sha1.addBytes(reinterpret_cast<const uint8_t*>(&nodeType), sizeof(nodeType));
415 addStringToSHA1(sha1, node->nodeName());
416 addStringToSHA1(sha1, node->nodeValue());
417
418 if (node->nodeType() == Node::ELEMENT_NODE) {
419 Node* child = node->firstChild();
420 while (child) {
421 OwnPtr<Digest> childInfo = createDigest(child, unusedNodesMap);
422 addStringToSHA1(sha1, childInfo->m_sha1);
423 child = child->nextSibling();
424 digest->m_children.append(childInfo.release());
425 }
426 Element* element = toElement(node);
427
428 if (element->hasAttributesWithoutUpdate()) {
429 size_t numAttrs = element->attributeCount();
430 SHA1 attrsSHA1;
431 for (size_t i = 0; i < numAttrs; ++i) {
432 const Attribute* attribute = element->attributeItem(i);
433 addStringToSHA1(attrsSHA1, attribute->name().toString());
434 addStringToSHA1(attrsSHA1, attribute->value());
435 }
436 Vector<uint8_t, 20> attrsHash;
437 attrsSHA1.computeHash(attrsHash);
438 digest->m_attrsSHA1 = base64Encode(reinterpret_cast<const char*>(attrsHash.data()), 10);
439 addStringToSHA1(sha1, digest->m_attrsSHA1);
440 }
441 }
442
443 Vector<uint8_t, 20> hash;
444 sha1.computeHash(hash);
445 digest->m_sha1 = base64Encode(reinterpret_cast<const char*>(hash.data()), 10);
446 if (unusedNodesMap)
447 unusedNodesMap->add(digest->m_sha1, digest);
448 return adoptPtr(digest);
449 }
450
insertBeforeAndMarkAsUsed(ContainerNode * parentNode,Digest * digest,Node * anchor,ExceptionState & exceptionState)451 bool DOMPatchSupport::insertBeforeAndMarkAsUsed(ContainerNode* parentNode, Digest* digest, Node* anchor, ExceptionState& exceptionState)
452 {
453 bool result = m_domEditor->insertBefore(parentNode, digest->m_node, anchor, exceptionState);
454 markNodeAsUsed(digest);
455 return result;
456 }
457
removeChildAndMoveToNew(Digest * oldDigest,ExceptionState & exceptionState)458 bool DOMPatchSupport::removeChildAndMoveToNew(Digest* oldDigest, ExceptionState& exceptionState)
459 {
460 RefPtr<Node> oldNode = oldDigest->m_node;
461 if (!m_domEditor->removeChild(oldNode->parentNode(), oldNode.get(), exceptionState))
462 return false;
463
464 // Diff works within levels. In order not to lose the node identity when user
465 // prepends his HTML with "<div>" (i.e. all nodes are shifted to the next nested level),
466 // prior to dropping the original node on the floor, check whether new DOM has a digest
467 // with matching sha1. If it does, replace it with the original DOM chunk. Chances are
468 // high that it will get merged back into the original DOM during the further patching.
469 UnusedNodesMap::iterator it = m_unusedNodesMap.find(oldDigest->m_sha1);
470 if (it != m_unusedNodesMap.end()) {
471 Digest* newDigest = it->value;
472 Node* newNode = newDigest->m_node;
473 if (!m_domEditor->replaceChild(newNode->parentNode(), oldNode, newNode, exceptionState))
474 return false;
475 newDigest->m_node = oldNode.get();
476 markNodeAsUsed(newDigest);
477 return true;
478 }
479
480 for (size_t i = 0; i < oldDigest->m_children.size(); ++i) {
481 if (!removeChildAndMoveToNew(oldDigest->m_children[i].get(), exceptionState))
482 return false;
483 }
484 return true;
485 }
486
markNodeAsUsed(Digest * digest)487 void DOMPatchSupport::markNodeAsUsed(Digest* digest)
488 {
489 Deque<Digest*> queue;
490 queue.append(digest);
491 while (!queue.isEmpty()) {
492 Digest* first = queue.takeFirst();
493 m_unusedNodesMap.remove(first->m_sha1);
494 for (size_t i = 0; i < first->m_children.size(); ++i)
495 queue.append(first->m_children[i].get());
496 }
497 }
498
499 #ifdef DEBUG_DOM_PATCH_SUPPORT
nodeName(Node * node)500 static String nodeName(Node* node)
501 {
502 if (node->document().isXHTMLDocument())
503 return node->nodeName();
504 return node->nodeName().lower();
505 }
506
dumpMap(const ResultMap & map,const String & name)507 void DOMPatchSupport::dumpMap(const ResultMap& map, const String& name)
508 {
509 fprintf(stderr, "\n\n");
510 for (size_t i = 0; i < map.size(); ++i)
511 fprintf(stderr, "%s[%lu]: %s (%p) - [%lu]\n", name.utf8().data(), i, map[i].first ? nodeName(map[i].first->m_node).utf8().data() : "", map[i].first, map[i].second);
512 }
513 #endif
514
515 } // namespace WebCore
516
517