Вы еще используете PHP.5.4 ? Тогда поосторожнее с mb_internal_encoding()

Не всегда есть возможность использовать последнюю (или ту, какую хочешь) версию PHP. Один случай (все более редкий) — ваш хостинг-провайдер еще не завез новой версии PHP. Второй — в компании вы работаете на «сертифицированной» *nix системе (например, Oracle Linux Enterprise), и ваш работодатель/админ не планирует в ближайшем будущем менять ее версию. А самовольный upgrade до актуальных версий нарушает статус «сертифицированной» ОС.
В итоге вы сидите на PHP 5.4.

На своей среде с PHP 5.4.7 я столкнулся с багом PHP, который как-то не особо освещался, а просто тихо был поправлен (в проверенных более ранних (5.3.10) и поздних релизах PHP (5.6.23, 7.0.8) — баг не проявляется) (в change-логах разработчиков PHP особо не искал; наверно, где-то там есть упоминание)

Итак, если вы работаете в основной кодировке UTF-8, то вероятно в своем коде использовали вызов mb_internal_encoding(‘UTF-8’), чтобы больше не указыать явно chasrset во всех mb_* функциях ( например, mb_substr($mystring, ‘подстрока’, 0, 10, ‘UTF-8’) )

Так вот, после этого «однобайтовые» функции типа strlen, substr, strpos / stripos и прочие внезапно тоже могут начать работать не с байтами, а с уникод-символами. К чему это приводит ?

Случай первый. Вы на лету (в памяти) создали строку содержимого XML файла (содержащего не только ASCII символы) и хотите выгрузить его в клиента передав в хедерах длину. Пишете команду:

Header('Content-Length: '.strlen($myXmlBody));
//...
exit($myXmlBody);

И думаете, что все хорошо, а на клиенте сохраняется инвалидный, обрезанный на несколько десятков — сотен байт файл. Потому что substr посчитал не байты, а UTF-овские символы (как если бы вы вызвали mb_strlen !)
Я видел ветки обсуждения проблемы на stackoverflow, и все по большей части предалагали отказаться от substr и прочих strlen в пользу mb_* аналогов. Но для данного примера этот подход как раз и не годится ! Мы ведь должны указать реальный размер файла в байтах, а не в уникодовых попугаях !

Второй случай: разбирая длинную unicode-строку, вы последовательно (strpos, stripos) находите нужный фрагмент, вырезаете его из исходной строки с помощью substr(), надеясь таким образом последовательно обработать ее до конца. Но вот ведь незадача, в итоге отрезается далеко не весь фрагмент, а лишь его часть, и следующий поиск снова натыкается на тот же кусок текста. В итоге получается бесконечный цикл.

Так, всенародно любимый набор классов PHPExcel, читающий и выводящий данные из/в табличные файлы (в том числе MS Excel), после задания mb_internal_encoding(‘UTF-8’) стал вешать сервер (при загрузке XLSX шаблона). Ошибка переполнения памяти (либо прекращение по превышению лимита времени выполнения) там происходит в модуле OLEread.php, на функции substr().

Решением (если пока невозможен переход на другую версию PHP) можно считать временная установка internal encoding в режим ASCII, и возврат в основной (UTF-8), когда «опасный» кусок кода пройден:


$curencoding = mb_internal_encoding();
mb_internal_encoding('ASCII');
// ... делаем что нужно, затем возвращаем как было:
mb_internal_encoding($curencoding);

А вот код, позволяющий проверить вашу версию PHP на этот глюк:
(набивать/копипастить переведя ваш редактор в UTF-8 кодировку, на win-1251-ых буквах баг не проявится!)

Скрипт выводит версию вашего PHP, значения длин строки, измеренных в ASCII- и UTF-режимах mb_internal_encoding(), и обнаружив баг, сообщает об этом («Detected strlen bug…»).


<?php
$src = "Test string: Проверка русских буковок на разных режимах";
$utfcode = "UTF-8";
$ascii   = "ASCII";
if (!is_callable('mb_internal_encoding'))
die ("no mbstring support\r\n");
mb_internal_encoding($utfcode);
$len_utf = strlen($src);
$len_utf_mb = mb_strlen($src);
mb_internal_encoding($ascii);
$len_ascii = strlen($src);
$len_ascii_mb = mb_strlen($src);
echo "PHP version: " . PHP_VERSION . "\r\n";
echo "in UTF  : strlen = $len_utf / mb_strlen = $len_utf_mb\r\n";
echo "in ASCII: strlen = $len_ascii / mb_strlen = $len_ascii_mb\r\n";
if ($len_utf != $len_ascii)
echo "Detected strlen() BUG in UTF-8 mb_internal_encoding mode !!!\r\n";
else
echo "strlen bug not detected\r\n";

Я не стал искать все возможные минорные версии PHP 5.4, чтобы протестировать их на наличие проблемы, просто в случае обнаружения похожих симптомов предлагаю воспользоваться скриптом, чтобы быстрее поставить верный диагноз и назначить лечение вашему серверу.

Будьте здоровы вы и ваше железо ! И с наступающим НГ !

Добавить комментарий