HTML Purifier. Расширяем возможности

в 9:56, , рубрики: php, yii, Блог компании SmartProgress, метки: ,

HTML Purifier. Расширяем возможности
Буквально пару абзацев я уделю внимание особенностям взаимодействия этой библиотеки с фреймворком Yii, остальное же в полной мере универсально и будет интересно всем, кто использует или планирует использовать эту библиотеку

Если вы уже хорошо знакомы с Purifier то можете смело начинать читать отсюда

Немного о HTML Purifier

Если вы не слышали о такой прекрасной библиотеке (а поиск на Хабре говорит о не такой уж большой популярности) как HTML Purifier, то советую обязательно к ней присмотреться, особенно если ваши пользователи генерируют контент в html формате. Это может быть рядовой пользователь, модератор или даже администратор.
Что же делает эта библиотека?
Согласно конфигурации она очищает любой html код от всех вредоносных, невалидных, запрещенных (вашей конфигурацией) частей кода, в том числе отдельные атрибуты.

Меньше слов, больше кода

Думаю пару примеров скажут сами за себя.

        $config = HTMLPurifier_Config::createDefault();
        $config->set('Attr.AllowedClasses',array('header')); // или Attr.ForbiddenClasses имеются ввиду CSS классы
        $config->set('AutoFormat.AutoParagraph',true); // авто добавление <p> в тексте при переносе
        $config->set('AutoFormat.RemoveEmpty',true); // удаляет пустые теги, есть исключения*
        $config->set('HTML.Doctype','HTML 4.01 Strict'); // обратите внимание как заменился тег <strike>
        $purifier = new HTMLPurifier($config);
        $clean_html = $purifier->purify($html);

* — Исключения RemoveEmpty

Исходный html:

        <p invalidAttribute="value">О, я хочу безумно <strike>жить</strike>:</p>
        <p>Всё сущее - <invalidTag>увековечить</invalidTag>,</p>
        <p class="header error">Безличное - вочеловечить,</p>
        Несбывшееся - воплотить!
        <script type="text/javascript">alert("hacked by Alexander Blok");</script>

Результат применения функции purify

        <p>О, я хочу безумно <span style="text-decoration:line-through;">жить</span>:</p>
        <p>Всё сущее - увековечить,</p>
        <p class="header">Безличное - вочеловечить,</p>
        <p>Несбывшееся - воплотить!</p>

Количество настроек впечатляет и дает возможность из коробки получить те плюшки, которые нужны именно вам.

«Перламутровые пуговицы»

Но не было бы этого поста, если бы как обычно, нам не захотелось чего то особенного, а именно две вещи:

  1. Заменить все ссылки на внешние сайты нашей ссылкой вида site.ru/redirect?url=link
  2. Добавить ко всем ссылкам пользователей атрибут target=_blank

Задачи не показались слишком сложными, по первой есть неплохая статья в доках, а вторая вообще плевая — конфиг HTML.TargetBlank делает работу за нас.

Задача 1 — замена внешних ссылок

У Purifier есть замечательный класс HTMLPurifier_URIFilter и не менее замечательные примеры реализации возможностей этого фильтра
Я взял за основу файл DisableExternalResources и быстро переписал его под свои нужды, а именно замена внешней ссылки на внутреннюю.

Файл фильтра

Небольшое описание:
В функции prepare мы получаем хост нашего сайта, делим по точкам, и разворачиваем массив.
В итоге получает array('ru', 'site', 'subdomen').
В функции filter мы делаем то же самое с ссылкой пользователя и сравниваем хост, если он одинаковый, то ничего не меняем и возвращаем true, если же нет, то создаем новый объект URI, с нашим адресом и вставляем пользовательскую ссылку в GET параметр.
Важно Метод filter не должен возвращать ничего, кроме true или false. Не пытайтесь заменить ссылку вернув её через return.

<?php
class HTMLPurifier_URIFilter_MakeRedirect extends HTMLPurifier_URIFilter
{
    /**
     * @type string
     */
    public $name = 'MakeRedirect';

    /**
     * @type array
     */
    protected $ourHostParts = false;

    /**
     * @param HTMLPurifier_Config $config
     * @return void
     */
    public function prepare($config)
    {
        $our_host = $config->getDefinition('URI')->host;
        if ($our_host !== null) {
            $this->ourHostParts = array_reverse(explode('.', $our_host));
        }
    }

    /**
     * @param HTMLPurifier_URI $uri Reference
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool
     */
    public function filter(&$uri, $config, $context)
    {
        if (is_null($uri->host)) {
            return true;
        }
        if ($this->ourHostParts === false) {
            return false;
        }
        $host_parts = array_reverse(explode('.', $uri->host));
        foreach ($this->ourHostParts as $i => $x) {
            if (!isset($host_parts[$i]) || $host_parts[$i] != $this->ourHostParts[$i]) {
                $path = Yii::app()->createUrl('site/redirect'); // Немного Yii, можно заменить на любой ваш url manager или просто вписать относительный путь до файла/action, который занимается редиректом
                $query = 'url='.urlencode($uri->toString());
                $uri = new HTMLPurifier_URI('http', 
                                              null, 
                                              Yii::app()->request->getServerName(), // return $_SERVER['SERVER_NAME']
                                              null, 
                                              $path, 
                                              $query, 
                                              null);
                break;
            }
        }
        return true;
    }
}

Применим фильтр

Для этого как подсказывает документация нам нужно обратится к объекту HTMLPurifier_Config.

        $config = HTMLPurifier_Config::createDefault();
        $uri = $config->getDefinition('URI');
        $uri->addFilter(new HTMLPurifier_URIFilter_MakeRedirect(), $config);
        $purifier = new HTMLPurifier($config);
        $clean_html = $purifier->purify($html);

Абзац для счастливых пользователей Yii

Я один из них (и ничуть не жалею). Yii из коробки поддерживает Purifier, но не все так гладко.
Пример из документации:

$p = new CHtmlPurifier(); // обертка от Yii
$p->options = array('URI.AllowedSchemes'=>array('http' => true, 'https' => true,)); // Передача конфига в формате массива
$text = $p->purify($text); 

Оттуда же мы узнаем:

         /**
	 * @var mixed the options to be passed to HTML Purifier instance.
	 * This can be a HTMLPurifier_Config object,  an array of directives (Namespace.Directive => Value)
	 * or the filename of an ini file.
	 * @see http://htmlpurifier.org/live/configdoc/plain.html
	 */
	private $_options=null;

Вроде бы все отлично, можно передать вместо массива объект HTMLPurifier_Config, пробуем:

        $purifier = new CHtmlPurifier();
        $config = HTMLPurifier_Config::createDefault();
        $config->set('AutoFormat.RemoveEmpty', true);
        $uri = $config->getDefinition('URI');
        $uri->addFilter(new HTMLPurifier_URIFilter_MakeRedirect(), $config);
        $purifier->options = $config;
        $clean_html = $purifier->purify($html);

        Warning
        Base directory /framework/vendors/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer does not exist,
        please create or change using %Cache.SerializerPath

Тут мы не расстраиваемся и лезем в маны Goggle CHtmlPurifier и узнаем что необходимо установить параметр Cache.SerializerPath со значением Yii::app()->getRuntimePath(), это даст пуриферу использовать эту папку для хранения кеша
Делаем:

$purifier = new CHtmlPurifier();
        $config = HTMLPurifier_Config::createDefault();
        $config->set('AutoFormat.RemoveEmpty', true);
        $config->set('Cache.SerializerPath',Yii::app()->getRuntimePath()); // <--
        $uri = $config->getDefinition('URI');
        $uri->addFilter(new HTMLPurifier_URIFilter_MakeRedirect(), $config);
        $purifier->options = $config;
        $clean_html = $purifier->purify($html);

Cannot set directive after finalization invoked on line 127 in file /framework/web/widgets/CHtmlPurifier.php

Теперь пуриферу не нравится, что мы определяем параметр дважды. А делает это сам CHtmlPurifier в методе createNewHtmlPurifierInstance()

protected function createNewHtmlPurifierInstance()
	{
		$this->_purifier=new HTMLPurifier($this->getOptions());
		$this->_purifier->config->set('Cache.SerializerPath',Yii::app()->getRuntimePath());
		return $this->_purifier;
	}

Тут, признаюсь, я потратил не мало времени в поисках красивого решения, но увы. Ничего более красивого, кроме как создать класс GHtmlPurifier и унаследовать его от класса CHtmlPurifier, переписав метод createNewHtmlPurifierInstance(), я не нашел.
Новый файл положил в папку protected/components/ и код наконец заработал.

        $htmlpurifier = new GHtmlPurifier();
        $config = HTMLPurifier_Config::createDefault();
        $config->set('Cache.SerializerPath',Yii::app()->getRuntimePath());
        $uri = $config->getDefinition('URI');
        $uri->addFilter(new HTMLPurifier_URIFilter_MakeRedirect(), $config);
        $htmlpurifier->options = $config;
        return $htmlpurifier->purify($text);

Задача 2 — добавление target=_blank

Не буду утруждать вас примерами нерабочего кода и скажу сразу, что HTML.TargetBlank работает только с внешними ссылками и его применение отпадает. А URI фильтры не могут получить доступ к тегу и его атрибутам.
Уже привыкший к хорошей документации по библиотеке, полез в маны, но увы, нужный раздел Advanced API был пуст и там красовалась надпись «Filed under Development».
Ничего не оставалось, как погрузится в исходники и найти как реализован модуль HTML.TargetBlank.
Вот он:

HTMLPurifier_AttrTransform_TargetBlank

/**
 * Adds target="blank" to all outbound links.  This transform is
 * only attached if Attr.TargetBlank is TRUE.  This works regardless
 * of whether or not Attr.AllowedFrameTargets
 */
class HTMLPurifier_AttrTransform_TargetBlank extends HTMLPurifier_AttrTransform
{
    private $parser;

    public function __construct() {
        $this->parser = new HTMLPurifier_URIParser();
    }

    public function transform($attr, $config, $context) {

        if (!isset($attr['href'])) {
            return $attr;
        }

        // XXX Kind of inefficient
        $url = $this->parser->parse($attr['href']);
        $scheme = $url->getSchemeObj($config, $context);

        if ($scheme->browsable && !$url->isBenign($config, $context)) {
            $attr['target'] = '_blank';
        }

        return $attr;

    }

}

Было решено создать собственный модуль, который не будет включать проверку на внешний адрес, а добавит target=_blank всем ссылкам, которые найдет.
Думаю с копированием и удалением пары строк в методе transform справится каждый. Поэтому листинг приводить не буду. Важно не забыть поменять название вашего модуля, я назвал его HTMLPurifier_AttrTransform_TargetBlankAll и положил в ту же папку /protected/components/.
Но этого оказалось не достаточно, модуль автоматически не подцепляется, и нам необходимо создать класс, который добавит модуль в нашу конфигурацию. В коде я добавил пару комментариев, что было понятно, что нужно изменить, если вы захотите написать свой собственный модуль.

HTMLPurifier_HTMLModule_TargetBlankAll.php

class HTMLPurifier_HTMLModule_TargetBlankAll extends HTMLPurifier_HTMLModule
{

    public $name = 'TargetBlankAll'; // Это имя будет использоваться в конфиге. Не забудьте его поменять

    public function setup($config) {
        $a = $this->addBlankElement('a'); // Указываем, что модуль должен применяться ко всем тегам A
        $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetBlankAll(); // Записываем наш конфиг в массив ПОСТфильтров
        // Так же есть массив ПРЕфильтров $a->attr_transform_pre[]
    }

}

Этот файл я так же сложил в папку /protected/components.
Теперь осталось добавить этот модуль в наш конфиг и наслаждаться результатом. Делается, это не совсем логично. Мы должны получить ссылку на объект HTML, причем обязательно с параметром $raw = true, что бы он инициализировался и сработал метод __construct() в класс HTMLPurifier_HTMLDefinition.
В методе __construct() инициализируется переменная $this->manager, которую мы будем использовать, для подключения нашего модуля.

        $htmlpurifier = new GHtmlPurifier();
        $config = HTMLPurifier_Config::createDefault();
        $config->set('Cache.SerializerPath',Yii::app()->getRuntimePath());
        $uri = $config->getDefinition('URI');
        $uri->addFilter(new HTMLPurifier_URIFilter_MakeRedirect(), $config);
        $html = $config->getHTMLDefinition(true); // Получаем ссылку на объект HTMLPurifier_HTMLDefinition
        $html->manager->addModule('TargetBlankAll'); // Добавляем модуль через манажер модулей
        $htmlpurifier->options = $config;
        return $htmlpurifier->purify($text);

Та-дам:

<a href="http://site.ru/">http://site.ru</a>
<a href="http://habrahabr.ru/">http://habrahabr.ru</a>

<a href="http://site.ru/" target="_blank">http://site.ru</a>
<a href="http://site.ru/redirect/?url=http%3A%2F%2Fhabrahabr.ru%2F" target="_blank">http://habrahabr.ru</a>

Обе задачи выполнены!


Надеюсь эта статья познакомила вас с этим прекрасным инструментом и поможет сделать ваш сайт одновременно интереснее и безопаснее, дав возможность вашим пользователя создавать интересный контент, используя все возможности html.

Это библиотека не отличается быстрой, поэтому не стоит использовать её для вывода данных на лету.

Автор: OneArt

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js