Улучшения в htmlspecialchars() в версии 5.4

Original author: Nikita Popov
  • Translation
Вокруг новых фич в PHP 5.4 было много разговоров, как например про traits, короткий синтаксис массивов.

Но одни особенно важные изменения, которые часто забывают для PHP 5.4, героически переписал cataphract (Artefacto на StackOverflow) большую часть htmlspecialchars.

Изменения о которых идет речь, относятся не только к htmlspecialchars, но еще и к htmlentities, htmlspecialchars_decode, html_entity_decode, get_html_translation_table.

Вот краткий обзор наиболее важных изменений:
  • UTF-8 кодировка по-умолчанию
  • Улучшенная обработка ошибок (ENT_SUBSTITUTE)
  • Обработка Doctype (ENT_HTML401, …)




UTF-8 кодировка по умолчанию



Как вы знаете, третий аргумент для htmlspecialchars это кодировка. Большинство людей просто упускают этот аргумент, таким образом, получая кодировку по умолчанию. Это значение было ISO-8859-1 до PHP 5.4. Новая версия исправляет это, сделав UTF-8 по умолчанию.

Улучшенная обработка ошибок


Обработка ошибок в htmlspecialchars до 5.4 была… хм, назовем ее «неинтуитивной»:

Если вы указали строку содержащую «некорректную кодовую последовательность» (для Unicode это «некоректно закодированя строка») htmlspecialchars вернет пустую строку. Ну, ладно, пока все хорошо. Забавно то, что она дополнительно выдаст ошибку, но только если отображения ошибок было отключено. Чудесно, не так ли?

В основном это означало, что на вашем dev-компьютере, вы не увидите никаких ошибок, но на production среде журнал ошибок будет заполнен вместе с ними. Удивительно.

В PHP 5.4 к счастью, такое поведение уже история. Ошибки больше не будут генерироваться.

Кроме того есть две опции, которые позволяют указать альтернативу возвращаемой пустой строке:

  • ENT_IGNORE: Этот вариант (который на самом деле не новый, он был и в PHP 5.3) просто отбросит всю некорректную последовательность кода. Это плохо по двум причинам: во-первых, вы не увидите недопустимых символов. Во-вторых, это накладывает определенный риск безопасности.
  • ENT_SUBSTITUTE: Это новая альтернативная опция. Вместо того чтобы просто удалять символы, они будут заменены на символ замены Юникод � (U + FFFD).


Давайте посмотрим на различные поведения (демо):

<?php // "\80" некоректно UTF-8 в этом контексте
var_dump(htmlspecialchars("a\x80b"));                 // string(0) ""
var_dump(htmlspecialchars("a\x80b", ENT_IGNORE));     // string(2) "ab"
var_dump(htmlspecialchars("a\x80b", ENT_SUBSTITUTE)); // string(5) "a�b"


Очевидно, последний вариант предпочтительней. В реальном приложении это будет выглядеть так:

<?php

// это в bootstrap, чтобы небыло сообщения об ошибке в 5.3
if (!defined('ENT_SUBSTITUTE')) {
    define('ENT_SUBSTITUTE', 0);          // если хотите пустые строки в 5.3
    // или
    define('ENT_SUBSTITUTE', ENT_IGNORE); // если хотите удаления символов в 5.3
}

// не забудьте кодировку в 5.3
$escaped = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');


Обработка Doctype



В PHP 5.4 есть четыре дополнительных флага для указания doctype который нужно использовать:

  • ENT_HTML401 (HTML 4.01) => используется по умолчанию
  • ENT_HTML5 (HTML 5)
  • ENT_XML1 (XML 1)
  • ENT_XHTML (XHTML)


В зависимости какой doctype вы укажите htmlspecialchars (и другие родственные функции) будут использовать разные таблицы сущностей.

Пример (демо):
<?php
var_dump(htmlspecialchars("'", ENT_HTML401)); // string(6) "&#039;"
var_dump(htmlspecialchars("'", ENT_HTML5));   // string(6) "&apos;"


Таким образом, для HTML 5 сущность &apos; будет возвращена, а для HTML 4.01 — потому что он не поддерживает &apos; — числовой код &#039;.
Разница становится более очевидной при использовании htmlentities, потому что там различий больше.
Вы можете легко убедиться в этом, когда посмотрите на сырые таблицы перевода.

Чтобы сделать это, можно использовать get_html_translation_table функцию. Вот пример для XML 1 doctype (демо):


<?php
var_dump(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES | ENT_XML1));


Результат выполнения:
array(5) {
["""]=>
string(6) "&quot;"
["&"]=>
string(5) "&amp;"
["'"]=>
string(6) "&apos;"
["<"]=>
string(4) "&lt;"
[">"]=>
string(4) "&gt;"
}


Это соответствует нашим ожиданиям: XML сам по себе определяет только пять основных сущностей.
А теперь попробуем то же самое для HTML 5 (демо), и мы увидим нечто вроде этого:

array(1510) {
[" "]=>
string(5) "&Tab;"
["
"]=>
string(9) "&NewLine;"
["!"]=>
string(6) "&excl;"
["""]=>
string(6) "&quot;"
["#"]=>
string(5) "&num;"
["$"]=>
string(8) "&dollar;"
["%"]=>
string(8) "&percnt;"
["&"]=>
string(5) "&amp;"
["'"]=>
string(6) "&apos;"
// ...
}


HTML 5 определяет большое количество сущностей — 1510, если быть точным. Вы также можете попробовать указать HTML 4.01 и XHTML, они оба определяют 253 сущности.

Также затронутым от выбранного типа документа является еще одний новый флаг обработки ошибок, который я не упомянул выше: ENT_DISALLOWED. Этот флаг будет заменять символы на Unicode символы замены, которые формально являются корректными последовательностями кода, но недопустимы в данном DOCTYPE.

Таким образом, вы можете гарантировать, что возвращаемая строка будет всегда хорошо сформирована в отношении кодирования (в данном типе документа). Хотя я не уверен, много ли дает использование этого флага. Браузер обрабатывает недействительные символы изящно любом случае, так что это мне кажется ненужным (хотя я наверное не прав).


Это еще не все


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

<?php
htmlspecialchars("<\x80The End\xef\xbf\xbf>", ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED | ENT_SUBSTITUTE, 'UTF-8');
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 30

    +7
    Пожалуй, со времен введения нового драйвера для MySQL (в версии 5.3, если не ошибаюсь) апдейты касающиеся кодировок, и в особенности более глубокой поддержки UTF-8, действительно стОящие внимания, с точки зрения полезности для конечного разработчика. Отличная новость! Это вам не автозагрузка классов и прочая сомнительной пользы ерунда (IMHO).
      +16
      Насчёт автозагрузки классов я готов поспорить :)
        +5
        все улучшения связанные с кодировкой в php это пожалуй самое ожидаемое в его обновлениях
        0
        0
        Если вы ранее не использовали SPL это ничего не значит
        +7
        Надеюсь, когда нибудь utf-8 станет классикой, не придется мучиться с iconv и всем таким. Поэтому, новость радостная.
          +1
          > когда нибудь utf-8 станет классикой

          Классикой в смысле «вымрет за ненадобностью»? ;)

          Потому что вместо iconv на смену придет нормализованные и ненормализованные формы, каноникализация и т.д. и т.п., что ни один из языков (кроме, вроде, Perl'а) не поддерживает в полном объеме (если только нет полноценной привязки к ICU)
            0
            Увы, чтобы без заморочек выгрузить простейшую таблицу в csv приходится извращаться с перекодировкой в CP1251, чтобы Excel понял… Либо ставить параллельно с M$ Office ещё Open/LibreOffice.
              +1
              Используй utf 16 + BOM, 2007-й офис открывает такие файлы нормально и символы не потеряешь
            0
            > Надеюсь, когда нибудь utf-8 станет классикой

            Всем нам ещё очень много кода придется перебрать и конвертировать в utf-8.
              +2
              Хочется, чтобы то, что пишется сейчас, использовали в большинстве своем utf-8, так нет, сколько натыкаюсь на недокодеров, руки у них к cp1251 тянутся.
                0
                А в чем проблема? Если не предполагается работать с языками, отличными от русского и английского, не вижу проблем в cp1251. По крайней мере, памяти потребляется в два раза меньше, и строковые операции соответственно в два раза быстрее, на высоконагруженных проектах это может иметь значение.
              0
              Жаль, где-нибудь в php.ini нельзя присвоить значения по умолчанию для флагов, придётся писать обёртку.
                –6
                > Как вы знаете, третий аргумент для htmlspecialchars это кодировка. Большинство людей просто упускают этот аргумент, таким образом, получая кодировку по умолчанию. Это значение было ISO-8859-1 до PHP 5.4. Новая версия исправляет это, сделав UTF-8 по умолчанию.

                На сайтах с win1251 (я знаю, такие до сих пор есть) символы кириллицы будут поняты как неправильный UTF код и функция видимо будет возвращать пустую строку.

                Я лично считаю, что дефолтная кодировка ISO это как раз хорошо, так как с ней функция только меняет 5 символов на сущности, а теперь она будет еще анализировать правильность UTF символов, и работать намного медленнее (и я не представляю ситуации где эта проверка нужна).

                И еще, хотелось бы сказать всем, кто кричит о поддержке кодировок — идите на свой Питон, Яву или что у вас там есть. PHP всегда работал с бинарными строками, и работал замечательно, и никакой дополнительной поддержки НЕ требется. Все виду русских и нерусских букв давно поддерживаются. Бинарные строки без кодировок работают быстрее. Используйте utf-8 и mb_* функции для строк, флаг u для регулярок с русскими буквами, и никаких проблем (кроме ф-й из раздела preg-*) у вас никогда не будет. Если же вы настолько тупые, что этого не понимаете, то вам никакая встроенная поддержка юникода уже не поможет. Спасибо.
                  +1
                  Радикально!
                    +6
                    Жду не дождусь пхп-пример, где производительность упирается в скорость работы utf-8 строк :)
                    Более того, utf-8 и бинарные строки полностью идентичны в случае, когда не содержат символов кроме латиницы и пунктуации (ну, грубо, печатный ascii). А если содержат, то utf использовать все равно необходимо.

                    Все проблемы с 1251, как я понимаю, лечатся с помощью default_charset = блабла в php.ini

                    Нативная поддержка utf нужна везде, сейчас даже url-ы содержат кириллицу и иероглифы. parse_url() уже небезопасно использовать (баг #52923).
                      0
                      Недавно в аналогичной дискуссии по поводу PHP + UTF-8 кто-то кинул ссылку на интересную статью: www.amiro.ru/blog/tech/how-was-php6-died

                      Пишут, что при попытке внедрить UTF-8 везде и всюду на уровне ядра PHP, разработчики столкнулись с непреодолимыми тормозами, что забили на это дело.
                        0
                        Не UTF-8, а UTF-16. По той статье неясно насколько бы спасла ситуацию UTF-8, но как-то складывается впечатление, что спсла бы:Выбор был остановлен на UTF-16. Основным минусами этой кодировки являлось ... множественные сопутствующие преобразования данных ... Как позднее признаются разработчики, если бы они могли начать все сначала, то выбор бы пал на UTF-8.
                        Как следствие множественных преобразований данных, появилось ощутимое снижение производительности в работе критичных компонент PHP:


                      –2
                      Вы случайно не брат Лердорфа? Array dereferncing может тоже не нужен? Если вы настолько тупой что считаете что свалка из функций для работы со строками с дублирующимся функционалом в пхп это хорошо — ну что ж…
                        0
                        Но это же не «дублирующийся функционал», а совершенно разные вещи — байтовая строка и символьная строка.
                        Два набора функций — это хоть и зло, но едва ли не наименьшее из возможных.

                        Исторически получилось, что в php были только байтовые строки, и нельзя делать вид, что их больше нет, потому что это моментально ломает любой существующий код, работающий с бинарными файлами или сетевыми протоколами, да и вообще с любыми данными не в utf-8.
                        Поэтому нельзя просто так взять и «перейти на юникод» одним волшебным патчем — поди разбери, где в старом коде вызов какого-нибудь substr() должен на самом деле работать с байтами, а где с символами.
                        По этой же причине всякие mbstring.func_overload являются мертворожденными костылями.

                        По-хорошему, htmlspecialchars и многим другим (не всем) строковым функциям должно быть плевать, latin1 у них на входе или utf-8, пока заменяемые и заменяющие байты укладываются в диапазон 0-127, т.е., совпадают с символами.
                        Собственно, в этом и есть одно из ключевых преимуществ utf-8 — код, который ничего знает про различие между байтами и символами, в большинстве случаев просто работает (пока не лезет менять старшие байты).

                        Итого:
                        проблема есть, простого решения она не имеет, выбранное решение, к сожалению, больше создает новых проблем, чем решает старых.
                        Потом все будут жаловаться, а что это хостинги не спешат переходить на новую версию — ну а какому хостеру в здравом уме надо, чтобы при апгрейде у него сломались абсолютно все сайты, использующие кодировку, отличную от utf-8.
                          0
                          В чем сложность для хостера предлагать на выбор две разные версии пхп, одна из которых не совместима со старыми не юникод проектами? Надо же когда-то перейти на юникод, навести порядок в свалке функций, перекочевавших из 4 версии? Имена функций с подчеркиванием и без, и существительные и глаголы, гет в начале и гет в конце имени функции, куча функций которые делают одно и то же (в том числе юникод и не юникод версии), разный порядок аргументов в схожих по функционалу функциях (привет array_map и array_walk). А если и дальше стараться, чтобы написанное под 4 пхп работало — порядка никогда не будет.
                            0
                            Там скорее с php3 свалка, если не раньше (с тройки начинал). Есть ничем необоснованная надежда, что к PHP6 всё же порядок наведут — разбросают функции по классам/объектам/неймспейсам, а нынешний бардак сделают алиасами и деприкэйтед.
                      +2
                      Э… а можно вопрос в кучу?

                      Есть ли какая альтернатива совершенно идиотскому использованию htmlspecialchars для проверки строки в UTF-8 на валидность? Друпальщики используют эту функцию в check_plain, что время от времени приводит к ошибкам, поскольку htmlspecialchars меняет строку, а это не всегда нужно.
                      0
                      Меня давно удивляет, почему такая важная функция, из-за неиспользования которой, в том числе, кросс-скриптится множество сайтов, имеет такое длинное и труднопечатаемое название.
                        0
                        Ад, дикий ад. Разработчиков хочется за это убить, о слове «совместимости» как той же Java они слышать не слышали.
                        Мало того, default_charset не действует на htmlspecialchars, и чтобы действовал, надо всё равно передавать пустую кодировку. То есть всё равно искать абсолютно все (!) вызовы htmlspecialchars.
                        Тот же HTTP_GET_VARS и $_GET, ну какая нахрен разница для парсера как оно называется, зачем надо было в 5.4 жестко это запрещать, чтобы абсолютно все старые скрипты перестали работать и их пришлось срочно переписывать, как будто других дел больше нет?
                        За эту каку и постоянные подножки этим индусам еще и платят.
                        Ну вот после этого PHP можно называться Enterprise решением, если они ложат на совместимость большой болт?
                        Подскажите, кроме Java, есть ещё вменяемые языки для web, где разработчики думают о мозгом о совместимости, и добавляя новые возможности, не делают неработоспособными старые?
                          0
                          Тот же HTTP_GET_VARS и $_GET, ну какая нахрен разница для парсера как оно называется, зачем надо было в 5.4 жестко это запрещать, чтобы абсолютно все старые скрипты перестали работать и их пришлось срочно переписывать, как будто других дел больше нет?

                          Емнип, дело в идеологии, HTTP_GET_VARS — глобальна, а $_GET — суперглобальна. Хотя могу и ошибаться, лень лезть в ман :)

                          Подскажите, кроме Java, есть ещё вменяемые языки для web, где разработчики думают о мозгом о совместимости, и добавляя новые возможности, не делают неработоспособными старые?

                          По слухам (в основном из стана любителей MS) при использовании C# + .NET об обратной совместимости можно не беспокоиться. При использовании C# + Mono никакой инфы на глаза не попадалось по этому поводу.
                            0
                            Да, HTTP_GET_VARS не суперглобальна, но какая нахрена разница в этом случае для старого кода, где везде написано global $HTTP_GET_VARS? Почему это вдруг код, работающий много лет, вдруг становится нерабочим на 5.4, а те же программы на C, написанные в 75, до сих пор компилируются?

                        Only users with full accounts can post comments. Log in, please.