Пройдите дерево 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.