Сервис постановки и достижения целей
92,66
рейтинг
18 ноября 2013 в 13:56

Разработка → 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
SmartProgress
рейтинг 92,66
Сервис постановки и достижения целей
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Комментарии (35)

  • +3
    Помогает кактус-то?
    • +2
      Только в сочетании с пальмами за окном)
    • 0
      Поясните, пожалуйста, чем оно кактус. Мужики-то не знают.
      • +1
        эм. А что это за растение в таком случае? :)
      • 0
        Суккуленты навсегда останутся в памяти благодарных потомков как «Кактус у компа, жрущий всякую вредную бяку излучений».
        • +1
          Вот оно что, оказывается. А я подумал, что вы про источник пищи и острых ощущений для грызунов.
    • –2
      Еще как! Главное правильно его готовить.

      А если он встроен в движок (как например в Cotonti, который я использую ), то ему цены нет. Тем более, что тут задаются различные профили для админов, авторов и тех кто пишет комментарии на сайт. У всех разные ограничения и соответственно уровень «зачистки» текстов различный. Очень удобно.
      • +2
        Это щас такая совсем беспалевная реклама очередной супер-CMS была?
        • 0
          С какой целью интересуетесь?

          Я стараюсь делиться информацией о полезных / удобных программах и инструментах, каковым в данном случае считаю Cotonti.
          Если форма подачи способствовала раздражению вашей аллергии на рекламу — извините, не хотел причинить страдания.
          • 0
            Ну просто явный коммент немного не в тему, имхо, с целью вставить ссылку на свой проект.
            • –2
              Вы не ответили на вопрос. :)
              Я не просто так спросил. Т.к. на мой взгляд ваш вопрос был «Ну просто явный коммент на потроллить».

              ps: О применимости HTML Purifiera в рамках какой-либо CMS писал ниже.
  • –2
    <p class="header error">Безличное - вочеловечить,</p>

    <p class="header">Безличное - вочеловечить,</p>

    Куда делся второй css-класс?
    • +2
      Мы в конфиге специально определили, что разрешен только класс header
      $config->set('Attr.AllowedClasses',array('header')); // или Attr.ForbiddenClasses имеются ввиду CSS классы
      
      • –3
        Зачем?
        • 0
          Как зачем? Чтобы не было возможности в статье убить всю вёрстку сайта поставив те классы тегам, которые активно используются в вёрстке этого самого сайта. К примеру если у вас в статье у некоторых элементов будут 9px border-ы, часть элементов будет летать, потому что position: fixed, ну и т.д.

          Можно просто определить список классов, которые можно использовать внутри. Зачем? Ну тут долго расписывать, бывает нужно. Правда чаще, нужно разработчику, чтобы из-за 1-ой 2-ух оригинальных идей в оформлении не городить огромные конструкции из кода, шаблонов и пр.
  • –6
    Как по мне намного лучше использовать валидатор какой-то и править руками. Особенно удручает:

    очищает любой html код от всех вредоносных, невалидных, запрещенных (вашей конфигурацией) частей кода, в том числе отдельные атрибуты


    Зачем вообще в коде невалидные части?

    А вот если переделать это в валидатор польза будет, например нашло ошибку и сразу бы бросало Exception. Вот тогда тим лид бы смог выставить правила верстки и заставить джунов верстать по этим правилом (е.г. если ошибка в темплейте то сайт вообще не отобразится)
    • +5
      Сидит себе content-manager Галя, копипастит разного рода тексты с других сайтов, с Word-а и ещё каких-нибудь источников. И тут система не даёт ей сохранить статью на сайте, мотивируя это тем, что:

      1. Галя, вы используете неподходящие к системе классы в тегах
      2. Галя, вы используете неподходящие теги
      3. Галя, вы используете неподходящие аттрибуты в тегах
      4. Галя, вы…
      А ну быстро всё исправили1111

      Галя повела бровями и послала систему к чёрту, т.е. набрала телефон разработчика.
      Как то так я вижу ваш валидатор. Вы такой вариант предлагаете? :)
      • –4
        Я писал джун девелопер а не Галя контент менеджер. Это немного не то.
        Если Галя будет копипастить с Ворда, а потом дать еще какому-то куску коду это порезать как оно будет выглядеть на фронтенде? Кто потом это править будет? Научите Галю какому-там маркдауну например.

        • +2
          Вы правда не понимаете? Это штука для фильтрации пользовательского ввода, когда пользователи заведомо злые и им надо резрешить некоторые тэги. Форумы, чаты, комментарии в блогах. При чем тут тимлиды и джуниоры.
        • +1
          Я, честно говоря, не понял причём тут «джун девелопер», поэтому опустил этот момент. Речь ведь идёт о библиотеке, назначением которой является — приведение любого стороннего HTML-кода в нормальный (согласно конфигу) и безопасный вид. Т.е. в 99% случаев речь идёт о тех моментах на сайте, которые выглядят как куча текста с картинками, видео, таблицами и кнопкой «изменить» под ними. Кнопка, в свою очередь, позволяет всю эту кашу дополнить\изменить\удалить. Т.е. мы на фронтенде изначально.

          Научите Шалю какому-там маркдауну

          Боюсь такое возможно только в далёкой-далёкой галактике. Либо у Гали будет гонорары как у вашего «джун девелопера», а то и выше. При этом Галя всё равно будет вас прокликать всеми правдами и не правдами. Т.к. накликать мышкой в CKEditor-е статью в 10 раз быстрее, чем написать её в «каком-то там маркдауне». А большего и не нужно.
        • +1
          Гале надо сделать за день двадцать-тридцать текстов, и её от вашего мардауна будет тошнить уже к обеду. Поэтому для неё мы делаем визуальный редактор, а дальше текст в порядок приводит сервер.
          • –6
            Ааа ну извиняюсь тогда =) Я что-то про такой кейс и думать забыл, давно не делал говно копи-паста сайты)
            • +1
              А причём тут говно-копипаста сайты? Хабр тоже говнокопипаст сайт? :) Любой сайт, наполнение которого предполагает заполнение сайта текстами должен сопровождаться использованием подобных библиотек\инструментов. Либо это не сайт, а одна сплошная дыра.
            • 0
              Епам такой епам :)
      • 0
        Да. Особое зло — это копипаста из Word, с его «включенными» стилями по полстраницы на абзац. Вычищать такое руками, или даже полуавтоматически — ад.

        Решал подобную задачу недавно На сайте было около сотни отдельных страниц, каждая на несколько экранов. Про встроенные стили не говорю — как раз приведенный случай. В результате решил за счет HTMLPurifier'а очень быстро. Подключил его, настроил соотв. образом. Далее все свелось к нажатию 2-х кнопок — открыть страницу и пересохранить. Всё!
  • 0
    А он может таблицу привести отсюда:
    <table cellspacing="0" cellpadding="0" width="615" border="1">
        <tbody>
            <tr>
                <td valign="top" width="492">
                    Наименование МО
                </td>
                <td valign="top" width="123">
                    Код ОКАТО
                </td>
            </tr>
            <tr>
                <td valign="top" width="492">
                    Городской округ ....
                </td>
                <td valign="bottom" width="123">
                    99 401 000 000
                </td>
            </tr>
            ...
        </tbody>
    </table>
    

    вот сюда:
    <table>
        <tbody>
            <tr>
                <td>
                    Наименование МО
                </td>
                <td>
                    Код ОКАТО
                </td>
            </tr>
            <tr>
                <td>
                    Городской округ ....
                </td>
                <td>
                    99 401 000 000
                </td>
            </tr>
            ...
        </tbody>
    </table>
    

    Ну то есть убрать мусор и оставить голую структуру таблицы?
    • +3
      Легко. Просто разрешить ему теги tbody, thead, td, tr, th, table. И не разрешать никаких аттрибутов. Получится как раз то, что вы хотите.
      • +1
        В данном случае даже проще — просто запретить атрибуты.
        • 0
          Если мне не изменяет память, то запрещены все аттрибуты по умолчанию. Даже href, src и пр. Но я могу ошибаться, точно не помню. Не так давно добавлял в конфиг rolspan и colspan, в конфиг 2х-летней давности. Просто за всё это время эти аттрибуты не пригодились, а изначально я про них попросту забыл :)
  • 0
    Мне вот всегда было интересно узнать о преимуществах HTML Purifier в сравнении с Jevix. Нет, я конечно понимаю, что Purifier легко масштабируется при помощи фильтров. Но такую плевую задачку как подмена ссылки и добавление target="_blank" можно решить пропустив текст через функцию после обработки Jevix'ом.
    • 0
      Думаю тут все достаточно очевидно. Из описания недостатков на главной Jevix:
      Недостатки
      Jevix разделяет строки br-ами. Делать абзацы (p) он не умеет, и, в существующей архитектурной концепции вряд ли научится
      Автор больше не развивает проект, перешёл на Python и постепенно забывает синтаксис PHP. Однако PULL-реквесты приветствуются
  • 0
    К сожалению Purifier слишком медленный и прожорливый. В данный момент используем связку Tidy + Htmlawed. Tidy работает как демон, в случае если tidy поперхнулся, то кидаем на htmlawed с простеньким конфигом.
  • 0
    Использую HTML Purifier c Drupal, очень довольна! Позволяет отказаться от десятка мелких модулей, которые делают rel=nofollow, безопасную вставку всяких iframe'ов (карты, видео) и т.д. Настроить можно из UI.

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

    Drupal это дело кеширует.

    Надо будет попробовать подцепить кастомный компонент по типу вашего TargetBlankAll.
  • +1
    А зачем было писать свой велосипед, если есть URI.Munge?
    htmlpurifier.org/live/configdoc/plain.html#URI.Munge
    • 0
      Ох тыж ёмоё…
      На самом деле было интересно покопаться, да и самое не тривиальное это добавление собственного модуля.
      Спасибо за наводку!

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка