mediawiki-extensions-Discus.../includes/TreeWalker.php
C. Scott Ananian 25272e7a4a Don't refer directly to PHP dom extension classes; avoid nonstandard behavior
These changes ensure that DiscussionTools is independent of DOM
library choice, and will not break if/when Parsoid switches to an
alternate (more standards-compliant) DOM library.

We run `phan` against the Dodo standards-compliant DOM library,
so this ends up flagging uses of non-standard PHP extensions to
the DOM.  These will be suppressed for now with a "Nonstandard DOM"
comment that can be grepped for, since they will eventually
will need to be rewritten or worked around.

Most frequent issues:

* Node::nodeValue and Node::textContent and Element::getAttribute()
can return null in a spec-compliant implementation.  Add `?? ''` to
make spec-compliant results consistent w/ what PHP returns.

* DOMXPath doesn't accept anything except DOMDocument.  These uses
should be replaced with DOMCompat::querySelectorAll() or similar
(which end up using DOMXPath under the covers for DOMDocument any way,
but are implemented more efficiently in a spec-compliant
implementation).

* A couple of times we have code like:
  `while ($node->firstChild!==null) { $node = $node->firstChild; }`
and phan's analysis isn't strong enough to determine that $node is still
non-null after the while.  This same issue should appear with DOMDocument
but phan doesn't complain for some reason.

One apparently legit issue:

* Node::insertBefore() is once called in a funny way which leans on
the fact that the second option is optional in PHP.  This seems to be
a workaround for an ancient PHP bug, and can probably be safely
removed.

Bug: T287611
Bug: T217867
Change-Id: I3c4f41c3819770f85d68157c9f690d650b7266a3
2021-07-30 18:15:40 -04:00

147 lines
3.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace MediaWiki\Extension\DiscussionTools;
use Exception;
use Throwable;
use Wikimedia\Parsoid\DOM\Node;
/**
* Partial implementation of W3 DOM4 TreeWalker interface.
*
* See also:
* - https://dom.spec.whatwg.org/#interface-treewalker
*
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
*/
class TreeWalker {
public $root;
public $whatToShow;
public $currentNode;
public $filter;
private $isActive = false;
/**
* See https://dom.spec.whatwg.org/#interface-treewalker
*
* @param Node $root
* @param int $whatToShow
* @param callable|null $filter
*/
public function __construct(
Node $root,
int $whatToShow = NodeFilter::SHOW_ALL,
callable $filter = null
) {
$this->currentNode = $root;
$this->filter = $filter;
$this->root = $root;
$this->whatToShow = $whatToShow;
}
/**
* See https://dom.spec.whatwg.org/#dom-treewalker-nextnode
*
* @return Node|null The current node
*/
public function nextNode(): ?Node {
$node = $this->currentNode;
$result = NodeFilter::FILTER_ACCEPT;
while ( true ) {
while ( $result !== NodeFilter::FILTER_REJECT && $node->firstChild !== null ) {
$node = $node->firstChild;
$result = $this->filterNode( $node );
if ( $result === NodeFilter::FILTER_ACCEPT ) {
$this->currentNode = $node;
return $node;
}
}
$sibling = null;
$temp = $node;
while ( $temp !== null ) {
if ( $temp === $this->root ) {
return null;
}
$sibling = $temp->nextSibling;
if ( $sibling !== null ) {
$node = $sibling;
break;
}
$temp = $temp->parentNode;
}
'@phan-var Node $node';
$result = $this->filterNode( $node );
if ( $result === NodeFilter::FILTER_ACCEPT ) {
$this->currentNode = $node;
return $node;
}
}
}
/**
* Filters a node.
*
* @internal
*
* @see https://dom.spec.whatwg.org/#concept-node-filter
*
* @param Node $node The node to check.
* @return int Returns one of NodeFilter's FILTER_* constants.
* - NodeFilter::FILTER_ACCEPT
* - NodeFilter::FILTER_REJECT
* - NodeFilter::FILTER_SKIP
* @throws Exception
*/
private function filterNode( Node $node ): int {
if ( $this->isActive ) {
throw new Exception( 'InvalidStateError' );
}
// Let n be nodes nodeType attribute value minus 1.
$n = $node->nodeType - 1;
// If the nth bit (where 0 is the least significant bit) of whatToShow
// is not set, return FILTER_SKIP.
if ( !( ( 1 << $n ) & $this->whatToShow ) ) {
return NodeFilter::FILTER_SKIP;
}
// If filter is null, return FILTER_ACCEPT.
if ( !$this->filter ) {
return NodeFilter::FILTER_ACCEPT;
}
$this->isActive = true;
try {
// Let $result be the return value of call a user object's operation
// with traverser's filter, "acceptNode", and Node. If this throws
// an exception, then unset traverser's active flag and rethrow the
// exception.
$result = $this->filter instanceof NodeFilter
? $this->filter->acceptNode( $node )
: ( $this->filter )( $node );
} catch ( Throwable $e ) {
$this->isActive = false;
throw $e;
}
$this->isActive = false;
return $result;
}
}