Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 /* | 1 /* |
| 2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org) | 2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org) |
| 3 * (C) 1999 Antti Koivisto (koivisto@kde.org) | 3 * (C) 1999 Antti Koivisto (koivisto@kde.org) |
| 4 * (C) 2001 Dirk Mueller (mueller@kde.org) | 4 * (C) 2001 Dirk Mueller (mueller@kde.org) |
| 5 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2013 Apple Inc. All rights | 5 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2013 Apple Inc. All rights |
| 6 * reserved. | 6 * reserved. |
| 7 * | 7 * |
| 8 * This library is free software; you can redistribute it and/or | 8 * This library is free software; you can redistribute it and/or |
| 9 * modify it under the terms of the GNU Library General Public | 9 * modify it under the terms of the GNU Library General Public |
| 10 * License as published by the Free Software Foundation; either | 10 * License as published by the Free Software Foundation; either |
| (...skipping 420 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 431 targets, exceptionState)) | 431 targets, exceptionState)) |
| 432 return oldChild; | 432 return oldChild; |
| 433 | 433 |
| 434 if (next) | 434 if (next) |
| 435 insertNodeVector(targets, next, AdoptAndInsertBefore()); | 435 insertNodeVector(targets, next, AdoptAndInsertBefore()); |
| 436 else | 436 else |
| 437 insertNodeVector(targets, nullptr, AdoptAndAppendChild()); | 437 insertNodeVector(targets, nullptr, AdoptAndAppendChild()); |
| 438 return oldChild; | 438 return oldChild; |
| 439 } | 439 } |
| 440 | 440 |
| 441 void ContainerNode::willRemoveChild(Node& child) { | |
| 442 DCHECK_EQ(child.parentNode(), this); | |
| 443 ChildListMutationScope(*this).willRemoveChild(child); | |
| 444 child.notifyMutationObserversNodeWillDetach(); | |
| 445 dispatchChildRemovalEvents(child); | |
| 446 ChildFrameDisconnector(child).disconnect(); | |
| 447 if (document() != child.document()) { | |
| 448 // |child| was moved another document by DOM mutation event handler. | |
| 449 return; | |
| 450 } | |
| 451 | |
| 452 // |nodeWillBeRemoved()| must be run after |ChildFrameDisconnector|, because | |
| 453 // |ChildFrameDisconnector| can run script which may cause state that is to | |
| 454 // be invalidated by removing the node. | |
| 455 ScriptForbiddenScope scriptForbiddenScope; | |
| 456 EventDispatchForbiddenScope assertNoEventDispatch; | |
| 457 // e.g. mutation event listener can create a new range. | |
| 458 document().nodeWillBeRemoved(child); | |
| 459 } | |
| 460 | |
| 461 void ContainerNode::willRemoveChildren() { | |
| 462 NodeVector children; | |
| 463 getChildNodes(*this, children); | |
| 464 | |
| 465 ChildListMutationScope mutation(*this); | |
| 466 for (const auto& node : children) { | |
| 467 DCHECK(node); | |
| 468 Node& child = *node; | |
| 469 mutation.willRemoveChild(child); | |
| 470 child.notifyMutationObserversNodeWillDetach(); | |
| 471 dispatchChildRemovalEvents(child); | |
| 472 } | |
| 473 | |
| 474 ChildFrameDisconnector(*this).disconnect( | |
| 475 ChildFrameDisconnector::DescendantsOnly); | |
| 476 } | |
| 477 | |
| 478 DEFINE_TRACE(ContainerNode) { | 441 DEFINE_TRACE(ContainerNode) { |
| 479 visitor->trace(m_firstChild); | 442 visitor->trace(m_firstChild); |
| 480 visitor->trace(m_lastChild); | 443 visitor->trace(m_lastChild); |
| 481 Node::trace(visitor); | 444 Node::trace(visitor); |
| 482 } | 445 } |
| 483 | 446 |
| 484 DEFINE_TRACE_WRAPPERS(ContainerNode) { | 447 DEFINE_TRACE_WRAPPERS(ContainerNode) { |
| 485 visitor->traceWrappersWithManualWriteBarrier(m_firstChild); | 448 visitor->traceWrappersWithManualWriteBarrier(m_firstChild); |
| 486 visitor->traceWrappersWithManualWriteBarrier(m_lastChild); | 449 visitor->traceWrappersWithManualWriteBarrier(m_lastChild); |
| 487 Node::traceWrappers(visitor); | 450 Node::traceWrappers(visitor); |
| 488 } | 451 } |
| 489 | 452 |
| 490 Node* ContainerNode::removeChild(Node* oldChild, | 453 Node* ContainerNode::removeChild(Node* oldChild, |
| 491 ExceptionState& exceptionState) { | 454 ExceptionState& exceptionState) { |
| 492 // NotFoundError: Raised if oldChild is not a child of this node. | 455 if (!oldChild || oldChild->parentNode() != this) { |
| 493 // FIXME: We should never really get PseudoElements in here, but editing will | |
| 494 // sometimes attempt to remove them still. We should fix that and enable this | |
| 495 // DCHECK. DCHECK(!oldChild->isPseudoElement()) | |
| 496 if (!oldChild || oldChild->parentNode() != this || | |
| 497 oldChild->isPseudoElement()) { | |
| 498 exceptionState.throwDOMException( | 456 exceptionState.throwDOMException( |
| 499 NotFoundError, "The node to be removed is not a child of this node."); | 457 NotFoundError, "The node to be removed is not a child of this node."); |
| 500 return nullptr; | 458 return nullptr; |
| 501 } | 459 } |
| 502 | 460 |
| 503 Node* child = oldChild; | 461 // TODO(esprehn): We should never really get PseudoElements in here, but |
| 462 // editing will sometimes attempt to remove them still. We should fix that and | |
| 463 // turn this into an assert. | |
| 464 if (oldChild->isPseudoElement()) { | |
| 465 exceptionState.throwDOMException(NotFoundError, | |
| 466 "Pseudo elements cannot be removed."); | |
| 467 return nullptr; | |
| 468 } | |
| 504 | 469 |
| 505 document().removeFocusedElementOfSubtree(child); | 470 // MutationObserver, no script here. |
| 471 ChildListMutationScope(*this).willRemoveChild(*oldChild); | |
| 472 oldChild->notifyMutationObserversNodeWillDetach(); | |
| 506 | 473 |
| 507 // Events fired when blurring currently focused node might have moved this | 474 // Run script in 'DOMNodeRemoved' and 'DOMNodeRemovedFromDocument' event |
| 508 // child into a different parent. | 475 // handlers. |
| 509 if (child->parentNode() != this) { | 476 dispatchChildRemovalEvents(*oldChild); |
| 477 | |
| 478 if (oldChild->parentNode() != this) { | |
| 479 exceptionState.throwDOMException(NotFoundError, | |
| 480 "The node to be removed is no longer a " | |
| 481 "child of this node. Perhaps it was moved " | |
| 482 "in response to a mutation?"); | |
| 483 return nullptr; | |
| 484 } | |
| 485 | |
| 486 // Run script in 'unload' events for <iframe>, <object>, <embed>, etc. | |
| 487 ChildFrameDisconnector(*oldChild).disconnect(); | |
| 488 | |
| 489 // Forbid loading frames inside the steps below that run script since we'd | |
| 490 // never disconnect them. | |
| 491 SubframeLoadingDisabler subframeLoadingDisabler(*oldChild); | |
| 492 | |
| 493 if (oldChild->parentNode() != this) { | |
| 494 exceptionState.throwDOMException(NotFoundError, | |
| 495 "The node to be removed is no longer a " | |
| 496 "child of this node. Perhaps it was moved " | |
| 497 "in an 'unload' event handler?"); | |
| 498 return nullptr; | |
| 499 } | |
| 500 | |
| 501 // Run script in blur events. | |
| 502 document().removeFocusedElementOfSubtree(oldChild); | |
| 503 | |
| 504 if (oldChild->parentNode() != this) { | |
| 510 exceptionState.throwDOMException(NotFoundError, | 505 exceptionState.throwDOMException(NotFoundError, |
| 511 "The node to be removed is no longer a " | 506 "The node to be removed is no longer a " |
| 512 "child of this node. Perhaps it was moved " | 507 "child of this node. Perhaps it was moved " |
| 513 "in a 'blur' event handler?"); | 508 "in a 'blur' event handler?"); |
| 514 return nullptr; | 509 return nullptr; |
| 515 } | 510 } |
| 516 | 511 |
| 517 willRemoveChild(*child); | |
| 518 | |
| 519 // Mutation events might have moved this child into a different parent. | |
| 520 if (child->parentNode() != this) { | |
| 521 exceptionState.throwDOMException(NotFoundError, | |
| 522 "The node to be removed is no longer a " | |
| 523 "child of this node. Perhaps it was moved " | |
| 524 "in response to a mutation?"); | |
| 525 return nullptr; | |
| 526 } | |
| 527 | |
| 528 { | 512 { |
| 513 // Script may run inside the UpdateSuspendScope as a result of plugins. | |
| 529 HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates; | 514 HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates; |
| 530 DocumentOrderedMap::RemoveScope treeRemoveScope; | 515 DocumentOrderedMap::RemoveScope treeRemoveScope; |
| 531 | 516 |
| 532 Node* prev = child->previousSibling(); | 517 Node* prev = oldChild->previousSibling(); |
| 533 Node* next = child->nextSibling(); | 518 Node* next = oldChild->nextSibling(); |
| 534 removeBetween(prev, next, *child); | 519 |
| 535 notifyNodeRemoved(*child); | 520 { |
| 536 childrenChanged(ChildrenChange::forRemoval(*child, prev, next, | 521 EventDispatchForbiddenScope forbidEvents; |
| 522 ScriptForbiddenScope forbidScript; | |
| 523 | |
| 524 if (document() == oldChild->document()) | |
| 525 document().nodeWillBeRemoved(*oldChild); | |
| 526 | |
| 527 // Actually remove the node from the tree and then call removedFrom on it | |
| 528 // and all descendants. | |
| 529 removeBetween(prev, next, *oldChild); | |
| 530 notifyNodeRemoved(*oldChild); | |
| 531 } | |
| 532 | |
| 533 // Script could run inside childrenChanged, enable iframe loading again. | |
|
dcheng
2016/11/07 20:50:57
Nit: it's not clear to me what "enable iframe load
esprehn
2016/11/07 22:42:33
There is one on the stack, see above subframeLoadi
| |
| 534 // TODO(esprehn): Can script run inside childrenChanged for Removal? It | |
| 535 // looks like it can only happen for Insertion. | |
| 536 childrenChanged(ChildrenChange::forRemoval(*oldChild, prev, next, | |
| 537 ChildrenChangeSourceAPI)); | 537 ChildrenChangeSourceAPI)); |
| 538 } | 538 } |
| 539 | |
| 540 // Run script in DOMSubtreeModified event handlers. | |
| 539 dispatchSubtreeModifiedEvent(); | 541 dispatchSubtreeModifiedEvent(); |
| 540 return child; | 542 return oldChild; |
| 541 } | 543 } |
| 542 | 544 |
| 543 void ContainerNode::removeBetween(Node* previousChild, | 545 void ContainerNode::removeBetween(Node* previousChild, |
| 544 Node* nextChild, | 546 Node* nextChild, |
| 545 Node& oldChild) { | 547 Node& oldChild) { |
| 548 ScriptForbiddenScope scriptForbiddenScope; | |
| 546 EventDispatchForbiddenScope assertNoEventDispatch; | 549 EventDispatchForbiddenScope assertNoEventDispatch; |
| 547 | 550 |
| 548 DCHECK_EQ(oldChild.parentNode(), this); | 551 DCHECK_EQ(oldChild.parentNode(), this); |
| 549 | 552 |
| 550 AttachContext context; | 553 AttachContext context; |
| 551 context.clearInvalidation = true; | 554 context.clearInvalidation = true; |
| 552 if (!oldChild.needsAttach()) | 555 if (!oldChild.needsAttach()) |
| 553 oldChild.detachLayoutTree(context); | 556 oldChild.detachLayoutTree(context); |
| 554 | 557 |
| 555 if (nextChild) | 558 if (nextChild) |
| 556 nextChild->setPreviousSibling(previousChild); | 559 nextChild->setPreviousSibling(previousChild); |
| 557 if (previousChild) | 560 if (previousChild) |
| 558 previousChild->setNextSibling(nextChild); | 561 previousChild->setNextSibling(nextChild); |
| 559 if (m_firstChild == &oldChild) | 562 if (m_firstChild == &oldChild) |
| 560 setFirstChild(nextChild); | 563 setFirstChild(nextChild); |
| 561 if (m_lastChild == &oldChild) | 564 if (m_lastChild == &oldChild) |
| 562 setLastChild(previousChild); | 565 setLastChild(previousChild); |
| 563 | 566 |
| 564 oldChild.setPreviousSibling(nullptr); | 567 oldChild.setPreviousSibling(nullptr); |
| 565 oldChild.setNextSibling(nullptr); | 568 oldChild.setNextSibling(nullptr); |
| 566 oldChild.setParentOrShadowHostNode(nullptr); | 569 oldChild.setParentOrShadowHostNode(nullptr); |
| 567 | 570 |
| 568 document().adoptIfNeeded(oldChild); | 571 document().adoptIfNeeded(oldChild); |
| 569 } | 572 } |
| 570 | 573 |
| 574 // TODO(esprehn): This function skips MutationEvents but runs script in lots of | |
| 575 // other ways, perhaps we should change to dispatching those too and just call | |
| 576 // removeChild() inside the parser? | |
| 571 void ContainerNode::parserRemoveChild(Node& oldChild) { | 577 void ContainerNode::parserRemoveChild(Node& oldChild) { |
| 572 DCHECK_EQ(oldChild.parentNode(), this); | 578 DCHECK_EQ(oldChild.parentNode(), this); |
| 573 DCHECK(!oldChild.isDocumentFragment()); | 579 DCHECK(!oldChild.isDocumentFragment()); |
| 574 | 580 |
| 575 // This may cause arbitrary Javascript execution via onunload handlers. | 581 // MutationObserver, no script here. |
| 576 if (oldChild.connectedSubframeCount()) | 582 ChildListMutationScope(*this).willRemoveChild(oldChild); |
| 577 ChildFrameDisconnector(oldChild).disconnect(); | 583 oldChild.notifyMutationObserversNodeWillDetach(); |
| 584 | |
| 585 // Run script in 'unload' events for <iframe>, <object>, <embed>, etc. | |
| 586 ChildFrameDisconnector(oldChild).disconnect(); | |
| 587 | |
| 588 // Forbid loading frames inside the steps below that run script since we'd | |
| 589 // never disconnect them. | |
| 590 SubframeLoadingDisabler subframeLoadingDisabler(oldChild); | |
|
dcheng
2016/11/07 20:50:57
I'd like to understand why this needs to be scoped
esprehn
2016/11/07 22:42:32
Anything that finds a ScriptForbiddenScope bypass
| |
| 578 | 591 |
| 579 if (oldChild.parentNode() != this) | 592 if (oldChild.parentNode() != this) |
| 580 return; | 593 return; |
| 581 | 594 |
| 582 ChildListMutationScope(*this).willRemoveChild(oldChild); | 595 // TODO(esprehn): Why don't we call removeFocusedElementOfSubtree? This means |
| 583 oldChild.notifyMutationObserversNodeWillDetach(); | 596 // focus doesn't get updated when the parser moves children around. |
| 584 | 597 |
| 585 HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates; | 598 { |
| 586 DocumentOrderedMap::RemoveScope treeRemoveScope; | 599 // Script may run inside the UpdateSuspendScope as a result of plugins. |
| 600 HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates; | |
| 601 DocumentOrderedMap::RemoveScope treeRemoveScope; | |
| 587 | 602 |
| 588 Node* prev = oldChild.previousSibling(); | 603 Node* prev = oldChild.previousSibling(); |
| 589 Node* next = oldChild.nextSibling(); | 604 Node* next = oldChild.nextSibling(); |
| 590 removeBetween(prev, next, oldChild); | |
| 591 | 605 |
| 592 notifyNodeRemoved(oldChild); | 606 { |
| 593 childrenChanged(ChildrenChange::forRemoval(oldChild, prev, next, | 607 EventDispatchForbiddenScope forbidEvents; |
| 594 ChildrenChangeSourceParser)); | 608 ScriptForbiddenScope forbidScript; |
| 609 | |
| 610 // TODO(esprehn): Why don't we call document().nodeWillBeRemoved(oldChild) | |
| 611 // here? This means the parser can corrupt ranges, selection, node | |
| 612 // iterators and more. | |
| 613 | |
| 614 // Actually remove the node from the tree and then call removedFrom on it. | |
| 615 // No script can run here. | |
| 616 removeBetween(prev, next, oldChild); | |
| 617 notifyNodeRemoved(oldChild); | |
| 618 } | |
| 619 | |
| 620 // Script could run inside childrenChanged, enable iframe loading again. | |
| 621 // TODO(esprehn): Can script run inside childrenChanged for Removal? It | |
| 622 // looks like it can only happen for Insertion. | |
| 623 subframeLoadingDisabler.release(); | |
| 624 childrenChanged(ChildrenChange::forRemoval(oldChild, prev, next, | |
| 625 ChildrenChangeSourceParser)); | |
| 626 } | |
| 595 } | 627 } |
| 596 | 628 |
| 597 // This differs from other remove functions because it forcibly removes all the | 629 // This differs from other remove functions because it forcibly removes all the |
| 598 // children, regardless of read-only status or event exceptions, e.g. | 630 // children, regardless of read-only status or event exceptions, e.g. |
| 599 void ContainerNode::removeChildren(SubtreeModificationAction action) { | 631 void ContainerNode::removeChildren(SubtreeModificationAction action) { |
| 600 if (!m_firstChild) | 632 if (!m_firstChild) |
| 601 return; | 633 return; |
| 602 | 634 |
| 603 // Do any prep work needed before actually starting to detach | 635 // TODO(esprehn): We shouldn't allow removing PseudoElements here. |
| 604 // and remove... e.g. stop loading frames, fire unload events. | |
| 605 willRemoveChildren(); | |
| 606 | 636 |
| 607 { | 637 { |
| 608 // Removing focus can cause frames to load, either via events (focusout, | 638 NodeVector children; |
| 609 // blur) or widget updates (e.g., for <embed>). | 639 getChildNodes(*this, children); |
| 610 SubframeLoadingDisabler disabler(*this); | |
| 611 | 640 |
| 612 // Exclude this node when looking for removed focusedElement since only | 641 // This must be inside a block scope before the below steps so |
| 613 // children will be removed. | 642 // MutationRecords are visible in takeRecords() inside those event |
| 614 // This must be later than willRemoveChildren, which might change focus | 643 // handlers. |
| 615 // state of a child. | 644 ChildListMutationScope mutation(*this); |
| 616 document().removeFocusedElementOfSubtree(this, true); | 645 for (const auto& oldChild : children) { |
| 646 // MutationObserver, no script here. | |
| 647 mutation.willRemoveChild(*oldChild); | |
| 648 oldChild->notifyMutationObserversNodeWillDetach(); | |
| 617 | 649 |
| 618 // Removing a node from a selection can cause widget updates. | 650 // TODO(esprehn): removeChild() exposes the MutationRecords to the |
| 619 document().nodeChildrenWillBeRemoved(*this); | 651 // child removal event handlers, but this function doesn't because we |
| 652 // batch the records in the ChildListMutationScope above. Should we | |
| 653 // make these consistent? | |
| 654 | |
| 655 // Run script in 'DOMNodeRemoved' and 'DOMNodeRemovedFromDocument' event | |
| 656 // handlers. | |
| 657 dispatchChildRemovalEvents(*oldChild); | |
| 658 } | |
| 620 } | 659 } |
| 621 | 660 |
| 661 // Run script in 'unload' events for <iframe>, <object>, <embed>, etc. | |
| 662 ChildFrameDisconnector(*this).disconnect( | |
| 663 ChildFrameDisconnector::DescendantsOnly); | |
| 664 | |
| 665 // Forbid loading frames inside the steps below that run script since we'd | |
| 666 // never disconnect them. | |
| 667 SubframeLoadingDisabler subframeLoadingDisabler(*this); | |
| 668 | |
| 669 // Exclude this node when looking for removed focusedElement since only | |
| 670 // children will be removed. | |
| 671 document().removeFocusedElementOfSubtree(this, true); | |
|
dcheng
2016/11/07 20:50:57
From reading this, perhaps we should just scope Su
esprehn
2016/11/07 22:42:32
That wouldn't have prevented the recent UXSS, sinc
dcheng
2016/11/08 01:51:12
Sorry, one more question, because I'd really like
| |
| 672 | |
| 622 { | 673 { |
| 674 // Script may run inside the UpdateSuspendScope as a result of plugins. | |
| 623 HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates; | 675 HTMLFrameOwnerElement::UpdateSuspendScope suspendWidgetHierarchyUpdates; |
| 624 DocumentOrderedMap::RemoveScope treeRemoveScope; | 676 DocumentOrderedMap::RemoveScope treeRemoveScope; |
| 677 | |
| 625 { | 678 { |
| 626 EventDispatchForbiddenScope assertNoEventDispatch; | 679 EventDispatchForbiddenScope assertNoEventDispatch; |
| 627 ScriptForbiddenScope forbidScript; | 680 ScriptForbiddenScope forbidScript; |
| 628 | 681 |
| 629 while (Node* child = m_firstChild) { | 682 document().nodeChildrenWillBeRemoved(*this); |
| 630 removeBetween(0, child->nextSibling(), *child); | 683 |
| 631 notifyNodeRemoved(*child); | 684 // Remove each node from the tree and then call removedFrom on it and all |
| 685 // descendants. | |
| 686 while (Node* oldChild = m_firstChild) { | |
| 687 removeBetween(0, oldChild->nextSibling(), *oldChild); | |
| 688 notifyNodeRemoved(*oldChild); | |
| 632 } | 689 } |
| 633 } | 690 } |
| 634 | 691 |
| 692 // Script could run inside childrenChanged, enable iframe loading again. | |
| 693 // TODO(esprehn): Can script run inside childrenChanged for Removal? It | |
| 694 // looks like it can only happen for Insertion. | |
| 695 subframeLoadingDisabler.release(); | |
| 635 ChildrenChange change = {AllChildrenRemoved, nullptr, nullptr, nullptr, | 696 ChildrenChange change = {AllChildrenRemoved, nullptr, nullptr, nullptr, |
| 636 ChildrenChangeSourceAPI}; | 697 ChildrenChangeSourceAPI}; |
| 637 childrenChanged(change); | 698 childrenChanged(change); |
| 638 } | 699 } |
| 639 | 700 |
| 701 // Run script in DOMSubtreeModified event handlers. | |
| 640 if (action == DispatchSubtreeModifiedEvent) | 702 if (action == DispatchSubtreeModifiedEvent) |
| 641 dispatchSubtreeModifiedEvent(); | 703 dispatchSubtreeModifiedEvent(); |
| 642 } | 704 } |
| 643 | 705 |
| 644 Node* ContainerNode::appendChild(Node* newChild, | 706 Node* ContainerNode::appendChild(Node* newChild, |
| 645 ExceptionState& exceptionState) { | 707 ExceptionState& exceptionState) { |
| 646 // Make sure adding the new child is ok | 708 // Make sure adding the new child is ok |
| 647 if (!checkAcceptChild(newChild, 0, exceptionState)) | 709 if (!checkAcceptChild(newChild, 0, exceptionState)) |
| 648 return newChild; | 710 return newChild; |
| 649 DCHECK(newChild); | 711 DCHECK(newChild); |
| (...skipping 818 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1468 return true; | 1530 return true; |
| 1469 | 1531 |
| 1470 if (node->isElementNode() && toElement(node)->shadow()) | 1532 if (node->isElementNode() && toElement(node)->shadow()) |
| 1471 return true; | 1533 return true; |
| 1472 | 1534 |
| 1473 return false; | 1535 return false; |
| 1474 } | 1536 } |
| 1475 #endif | 1537 #endif |
| 1476 | 1538 |
| 1477 } // namespace blink | 1539 } // namespace blink |
| OLD | NEW |