Пройдите дерево DOM

Поскольку большинство (все?) PHP-библиотек, выполняющих очистку HTML, таких как HTMLPurifier, сильно зависят от регулярных выражений, я подумал, что попытка написать HTML-очиститель, использующий DOMDocument и связанные с ним классы, будет полезным экспериментом. Пока я нахожусь на очень ранней стадии с этим, проект пока показывает некоторое обещание.

Моя идея вращается вокруг класса, который использует DOMDocument для обхода всех узлов в предоставленной разметке, сравнения их с белым списком и удаления чего-либо, чего нет в белом списке. (первая реализация очень проста, только удаление узлов на основе их типа, но я надеюсь стать более сложным и проанализировать атрибуты узла, связываются ли ссылки с элементами в другом домене и т. д. в будущем).

У меня вопрос, как мне пройти по дереву DOM? Насколько я понимаю, объекты DOM* имеют атрибут childNodes, так что мне нужно повторять по всему дереву? Кроме того, ранние эксперименты с DOMNodeLists показали, что вы должны быть очень осторожны с порядком удаления объектов, иначе вы можете оставить элементы позади или вызвать исключения.

Если у кого-то есть опыт работы с деревом DOM в PHP, я буду признателен за любые ваши отзывы по этой теме.

РЕДАКТИРОВАТЬ: я построил следующий метод для моего класса очистки HTML. Он рекурсивно обходит дерево DOM и проверяет, есть ли найденные элементы в белом списке. Если они не, они удалены.

Проблема, с которой я столкнулся, заключалась в том, что при удалении узла индексы всех последующих узлов в DOMNodeList меняются. Простая работа снизу вверх позволяет избежать этой проблемы. Это все еще очень простой подход в настоящее время, но я думаю, что он обещает. Он, безусловно, работает намного быстрее, чем HTMLPurifier, хотя, по общему признанию, Purifier делает намного больше вещей.

/**
 * Recursivly remove elements from the DOM that aren't whitelisted
 * @param DOMNode $elem
 * @return array List of elements removed from the DOM
 * @throws Exception If removal of a node failed than an exception is thrown
 */
private function cleanNodes (DOMNode $elem)
{
    $removed    = array ();
    if (in_array ($elem -> nodeName, $this -> whiteList))
    {
        if ($elem -> hasChildNodes ())
        {
            /*
             * Iterate over the element's children. The reason we go backwards is because
             * going forwards will cause indexes to change when elements get removed
             */
            $children   = $elem -> childNodes;
            $index      = $children -> length;
            while (--$index >= 0)
            {
                $removed = array_merge ($removed, $this -> cleanNodes ($children -> item ($index)));
            }
        }
    }
    else
    {
        // The element is not on the whitelist, so remove it
        if ($elem -> parentNode -> removeChild ($elem))
        {
            $removed [] = $elem;
        }
        else
        {
            throw new Exception ('Failed to remove node from DOM');
        }
    }
    return ($removed);
}

1 ответ

Решение

Для начала вы можете взглянуть на этот пользовательский RecursiveDomIterator:

Код:

class RecursiveDOMIterator implements RecursiveIterator
{
    /**
     * Current Position in DOMNodeList
     * @var Integer
     */
    protected $_position;

    /**
     * The DOMNodeList with all children to iterate over
     * @var DOMNodeList
     */
    protected $_nodeList;

    /**
     * @param DOMNode $domNode
     * @return void
     */
    public function __construct(DOMNode $domNode)
    {
        $this->_position = 0;
        $this->_nodeList = $domNode->childNodes;
    }

    /**
     * Returns the current DOMNode
     * @return DOMNode
     */
    public function current()
    {
        return $this->_nodeList->item($this->_position);
    }

    /**
     * Returns an iterator for the current iterator entry
     * @return RecursiveDOMIterator
     */
    public function getChildren()
    {
        return new self($this->current());
    }

    /**
     * Returns if an iterator can be created for the current entry.
     * @return Boolean
     */
    public function hasChildren()
    {
        return $this->current()->hasChildNodes();
    }

    /**
     * Returns the current position
     * @return Integer
     */
    public function key()
    {
        return $this->_position;
    }

    /**
     * Moves the current position to the next element.
     * @return void
     */
    public function next()
    {
        $this->_position++;
    }

    /**
     * Rewind the Iterator to the first element
     * @return void
     */
    public function rewind()
    {
        $this->_position = 0;
    }

    /**
     * Checks if current position is valid
     * @return Boolean
     */
    public function valid()
    {
        return $this->_position < $this->_nodeList->length;
    }
}

Вы можете использовать это в сочетании с RecursiveIteratorIterator, Примеры использования находятся на странице.

В целом, было бы проще использовать XPath для поиска узлов, занесенных в черный список, вместо обхода дерева DOM. Также имейте в виду, что DOM уже неплохо предотвращает XSS, автоматически экранируя сущности xml в nodeValues.

Еще одна вещь, о которой вы должны знать, это то, что любые манипуляции с DOMDocument немедленно влияют на любой DOMNodeList, который вы можете получить из запросов XPath, и это может привести к пропуску узлов при манипулировании ими. См. Пример замены DOMNode классами DOM PHP.

Другие вопросы по тегам