php
PHP 5.6
Немного времени осталось до выхода PHP 5.6. Похоже, авторы взяли в привычку добавлять в каждой версии в язык несколько нововведений. Сейчас эта версия в стадии альфы, это означает, что до беты может добавиться
echo 2**3, "\n", // восемь
-2**2, "\n", // минус четыре
2 ** 3 ** 2; // 512«Крышка» (^), которая во многих языках занята под степень, в ПХП занята под «исключающее или», поэтому сделали две «звёздочки», как в Пайтоне. Из приведённых примеров мы видим, что «вес» унарного минуса ниже, чем у степени — он применяется уже к результату, об этом надо помнить, лично я бы ожидал, что в степень будет возведено отрицательное число. И, кстати, сама операция является правоассоциативной (последний пример).
$a = gmp_init(2);
$b = gmp_init(3);
$c = $a**2 + $b;
echo $c, "\n"; // выведет 7
var_dump($c); // покажет объект класса GMP, у которого есть свойство num=7
echo array_sum([1,2,gmp_init(-3)]); // 3, печальКроме этого, переход к объектам (от ресурсов) позволит сериализовать эти числа, что является хорошим бонусом. Увы, не всё ещё гладко — последний вызов (где суммируется массив) возвращает число три, а не ноль.
const alf = 2;
const bolk = alf*alf . alf; // работает новый синтаксис
echo bolk, "\n"; // 42
define("TEST", alf*alf . alf); // работает старый синтаксис
echo TEST, "\n"; // 42// сколько бы аргументов после первого не было, они попадут в $params массивом
function query($query, ...$params) {
$stmt = $this->pdo->prepare($query);
$stmt->execute($params);
return $stmt;
}Синтаксис с тремя точками используется в Си, Джаве и новой версии Джаваскрипта. Что интересно, с новым синтакисом работает и указание типа аргумента, и передача по ссылке. Ограничения этого синтаксиса вполне понятные — такой аргумент должен быть всегда последним (и/или единственным), задавать значения
// всё это неправильно:
function fn(...$args, $arg) // такой агрумент не первый
function fn(...$args1, ...$args2) // не единственный
function fn($arg, ...$args = []) // задано значение по-умолчаниюВыбор именно такого синтаксиса основывался на следующих вещах:
// выглядит как будто мы требуем, чтобы $args был массивом, что бесполезно — $args уже
// массив, по определению; на самом деле этот синтаксис должен иметь значение «каждый переданный
// в $args элемент — массив»
function fn(array $args...)
// будет путать ребят, которым приходится программировать и на Си тоже — очень похоже
// на указатель
function fn(Char *$args)
// в Си# используется ключевое слово «params», для ПХП не подходит, так как будет путаница
// с указанием типа
function fn(params $args)$a = $b = 2;
$c = ['a', 'b'];
var_dump(call_user_func_array('compact', $c)); // прежний синтаксис
var_dump(compact(...$c)); // новый синтакисис
var_dump(compact(...$c, 'c')); // можно ещё и так, будут переданы как аргументы 'a', 'b' и 'c'
var_dump(...new ArrayIterator([1, 2, 3])); // даже так работает! утиная типизация во всей красе
var_dump(...[1 => 1, 3 => 1]); // будет переданы два параметра, без пропусков в номерации
var_dump([...$c, $a]); // а вот так, увы, нельзя
list($k, ...$d) = $c; // так тоже нельзя
echo ...$c; // даже так нельзя!К сожалению, ПХП традиционно не радует согласованностью синтаксиса и последняя группа выражения не работает. Кстати, синтаксис в тремя точками улучшает картину производительности вызова функций с переменным числом аргументов.
Но даже с этими недостатками, нововведение хорошее. Посмотрите как чудесно выглядит каррирование в новом синтаксисе:
function bind(callable $function, ...$boundArgs) {
return function(...$args) use($function, $boundArgs) {
return $function(...$boundArgs, ...$args);
}
}
$arrayToLower = bind('array_map', 'strtolower');
$arrayToLower(['Foo', 'BAR', 'baZ']); // вернёт ['foo', 'bar', 'baz']Локальные переменные
В прошлом я несколько лет программировал на Перле, очень любил этот язык и неплохо знал, до сих пор могу без особых проблем читать программы на нём. Прошло немало лет, наиболее часто я теперь использую ПХП и Джаваскрипт, на Перле не пишу вовсе (хотя в «Яндексе» был краткий эпизод — написал небольшой пример для документации
Вообще я сейчас считаю, что будущее скорее за языками с лаконичным синтаксисом (Пайтон), чем за тем, у кого он сверхнасыщен (Перл, Руби).
Тем не менее, некоторых вещей из Перла мне до сих пор нехвает в других языках. Я научился без этого обходиться, но много лет меня не отпускает чувство, что у меня
Например, локальные переменные. В ПХП часто приходится смотреть (особенно, если достаётся
В Перле такой проблемы нет — там локальную переменную можно создать почти в любом месте, произвольном месте кода, просто поставив фигурные скобки — так создаётся новый лексический блок, у которого могут быть свои локальные переменные:
my @a = (1, 2, 3);
my @b = ('a', 'b', 'c');
for my $i (@a) { # первая переменная $i
for my $i (@b) { # вторая переменная $i, перекроет первую только внутри цикла
print $i . "\n";
}
{
my $i = 'end'; # третья переменная $i, перекроет первую только в этом блоке
print $i . "\n";
}
print $i . "\n"; # выводится первая переменная $i
}Конечно, я не призываю именно так писать код — во вложенных циклах с одной и той же переменной легко запутаться, просто иллюстрирую принцип.
В Джаваскрипте ситуация меняется, появилась конструкция «let» (впрочем, раньше её можно было эмулировать), а ПХП ничего похожего так и нет. Я переписывался с разработчиками много лет назад на эту тему, но воз и ныне там.
Сейчас есть возможность опубликовать предложение, описав его в виде RFC — эта новая процедура появилась недавно, если предложение понравится, то синтаксис имеет все шансы попасть в язык. Надо будет
Проблема в стемере для PHP
В модуле stem для ПХП обнаружилась неожиданная проблема. Этот модуль — стеммер, позволяющий обрезать окончания слов для того, чтобы искать потом по тексту без учёта падежей и склонений.
Для русского языка в нём целых две функции — для кодировки
Я обратил внимание, что из командной строки код с этим модулем работает нормально, а в рабочем проекте кодировка портится — как будто стеммер
У меня возникла мысль, что дело в выставляемой локали, мы провели эксперимент — так и есть, дело в этом. Такой код работать правильно не будет:
setlocale(LC_TIME, 'ru_RU.cp1251');
echo stem_russian_unicode(iconv('windows-1251', 'utf-8', 'Приветы'));Кодировка получится битой. Перед вызовом функций стеммера нужно переставлять кодировку (setlocale)
// файл stem.c
php_strtolower(z->p, arglen); // ← вот искомый вызов
stem(z);
z->p[z->l]= '\0';
// … ищем эту функцию в исходниках PHP…
// string.c из PHP
PHPAPI char* php_strtolower(char * s, size_t len) {
unsigned char *c, *e;
c = (unsigned char *)s;
e = c+len;
while (c < e) {
*c = tolower(*c); // ← а это проблемное место
c++;
}
return s;
}Согласно манам, функция tolower ориентируется в своей работе на локаль, в этом и проблема.
Igbinary и PHP serialize
Если вкратце, модуль igbinary в ПХП — замена встроенным функциям сериализации. Встроеные функции хороши только в одном — сериализованное значение получается читаемым (хотя, на мой взгляд, JSON всё же получше будет).
Igbinary пренебрегает читаемостью (это вообще бинарный формат) в пользу скорости и компактности. Вчера ночью я перевёл пару мест в одном из проектов нашей компании, а утром мой друг, И. Б., кинул мне ссылку на статью, где сравнивается скорость работы встроенных функций и igbinary, причём в тестах на сохранение строк последний сливает первому.
Удивительный результат, так как, например, с числами всё иначе — там разница в несколько раз в пользу igbinary (в случае типа с плавающей запятой — более почти в 20), что же за проблема со строками?
У меня возникло подозрение, что я знаю в чём дело. Обычно, когда я изучаю новый модуль, я смотрю так же и на его настройки. Вчера я тоже их просматривал (в этом модуле их
Опция хорошая — помогает экономить память (например, при сериализации в memcached), работает она очень просто — при сериализации, если в данных попадается строка, проверяем не встречалась ли такая же строка ранее, если встречалась, повторно строку не сохраняем, сохраняем лишь ссылку на предыдущее вхождение.

Как видите, разница разительная. Так что если вы ставите igbinary для скорости, то эту опцию лучше бы выключить.
Числа из PHP в MongoDB
Не знал, что официальный драйвер Монги для ПХП передаёт числа в Монгу как строки.
Долго не понимал почему удаление строк из коллекции срабатывает почти сразу, а не через заданное время. Наконец, догадался посмотреть на получившийся индекс, оказалалось, что все числа там — строки (в том числе и период через который значения удаляются).
А чтобы получились числа их надо завернуть в специальный объект:
$mongo->ensureIndex([
'date' => new MongoInt32(1)
], [
'expireAfterSeconds' => new MongoInt32(7200)
]);Вот как выглядит правильный индекс:

В неправильном числа будут окружены апострофами.
PHPFuck
JSFuck у нас уже был (это намёк на язык Брейнфак, а не ругательство), почему бы не изобрести PHPFuck?
Часа полтора потратил на скрипт, который выводит слово «Hello!»:
<?
$__=$_[$_[]++].=$_;$_[!_]+=++$_[!_];@$$_=($_[$$_][$_]&$_[$$_][$_[!_]]).$_[$$_][$_];
$_[]=&$$_;@$$_.=$_[!!_][$__]^$__[$_[!_]]|$__[!_];$___=$__[!_]|_&~$__[!!_];
@$$_.=++$___.($__[$_]^_^$__[$_[!_]]).($_[!!_][!$_]^$_[_==_][$_[!_]>>!!_]^_);
$___&=$__[$_[!_]];$___.=${$_}[!_]^${$_}[$_[!_]]|$__[!_].($$$_=$__[!_]^_^$__[!!_]).
$$$_.($_[_]=$__[$_[!_]]^_^${$_}[$_[!_]>>!!_]);$$_($___.
(~${$_}[~-$_[!_]]&$___[$_[!_]]).($_[_]&~$___[!!_]));Это настоящий код на PHP, он работает и выполняет заявленное. За вдохновение спасибо неизвестному автору бэкдора, как только я увидел код, понял, что так написать можно что угодно.
Что вкратце происходит у меня. Я создаю массив, привожу его к строке, получается слово «Array» (это такая особенность PHP, если кто не знает), потом при помощи бинарных операций над буквами (с буквами можно использовать «~», «&», «|», «^», а так же операцию инкремента, получая другие буквы), собираю из этого слова слово «printf» (точнее «prINTf», функции в PHP регистронезависимые), которое в дальнейшем будет использовано в качестве имени функции. Потом из двух слов — «Array», «prINTf» и знака подчёркивания теми же бинарными преобразованиями получаю «Hello!» с переводом строки.
Например, буква «N» получается операцией «A» | «_» | ~«r» с последующим инкрементом результата, а «l» (латинская «эл») — «A» ^ «r» ^ «_». Подчёркивание можно использовать без кавычек — все незакавыченные идентификаторы PHP рассматривает как строки.
В конце просто вызываю функцию, передавая ей полученную строку.
Немаловажное про генераторы в PHP
Наткнулся на важный пост о генераторах в ПХП, я их попробовал использовать, ещё когда они были в альфе. Хорошая штука, мне после Пайтона их в языке не хватало.
Суть же рассматриваемой проблемы в следующем. Смотрите, вот функция, которая возвращает построчно файл и пример её вызова:
function getLines($file) {
$f = fopen($file, 'r');
while ($line = fgets($f)) {
yield $line;
}
fclose($f);
}
foreach (getLines("file.txt") as $line) {
echo $line;
}Пока всё чудесно, ничего странного и необычного, а теперь представьте, что нам нужно прочитать только четыре первые строки:
foreach (getLines("file.txt") as $n => $line) {
if ($n > 5) break;
echo $line;
}Если в файле их было больше, то в функции getLines вызов fclose не выполнится никогда — до него программа просто не дойдёт, файл останется открытым, память занятой и так далее. Плохо. Что делать?
Оказывается, документация обходит этот вопрос стороной, а вот автор введения в ПХП этой конструкции позаботился о том, чтобы эту тему раскрыть — если прочитать соответствующий RFC, становится ясно как быть.
Оказывается, при высвобождении ссылки на генератор, внутри его обязаны выполнится все блоки finally, т. е. вышеприведённый код нужно переписать следующим образом:
function getLines($file) {
$f = fopen($file, 'r');
try {
while ($line = fgets($f)) {
yield $line;
}
} finally {
fclose($f);
}
}Пятничная загадка для PHPшников
В ЖЖ увидел прекрасное — загадку про ПХП.
Те, кто знает этот язык, скажите, почему этот код не выдаёт ошибок или предупреждений (все ошибки, конечно, включены):
<?php
//bolknote.ru
?>Оптимизирующий транслятор Brainfuck → PHP
Грех было не запустить свою же программу из предыдущего поста на собственном стареньком (2005 года) оптимизирующем трансляторе Брейнфака в ПХП. Оказалось, оттранслировал его неправильно.
Нашёл баг (неверно транслировалась последовательность «[<]» в некоторых случаях), исправил, почистил, убрал наследие ПХП 4, ввёл небольшую новую оптимизацию и выхожил на ГитХаб.
История этого интерпретатора тянется ещё
В 2005 году я переделал его на ПХП и попробовал предложить на ПЕАР (к тому времени у меня там уже было несколько модулей), но там он не прошёл предворительный осмотр, сказали, что такой модуль не нужен.
Так он у меня и валялся в разделе «Храню», пока я не запустил сегодня на нём свою программу. Должен, кстати, сказать, что интерпретатор держит марку, например, вот вычисление числа «Пи» на моём трансляторе и интерпретаторе на Джаве:

Хотя, может быть, Джава просто медленно стартует.
Заодно, кстати, на своей же программе нашёл баг в расцветке Брейфака, которую я делал для библиотеки highlight.js.
Чудны́ дела твои, PHP!
Может быть
Я делал это функцией copy, первом аргументом передавал УРЛ, вторым — путь на файловой системе. В логах, после изменения конфигурации, ПХП ругался, что «http:» — это неправильное начало для разрешённого пути. Удивительно, но стоило заменить copy// на её, фактически, эквивалент, как всё заработало:
if ($fin = fopen($url, 'rb') and $fout = fopen($file, 'wb')) {
stream_copy_to_stream($fin, $fout);
fclose($fout);
fclose($fin);
return true;
} else {
return false;
}Такое поведение больше похоже на баг, ПХП у меня не последний (5.3.21 сейчас), может поправили уже.
О стоимости доступа к объектам в PHP
Удивительное — рядом: недавно искали с нашими админами место в коде где больше всего тормозит вывод одной страницы одного из сайтов, сделанных в недрах нашей компании в незапамятные времена.
Код был довольно запутанный, попросили меня помочь. Место я вычислил. Если его закоментировать, страница начинала выдаваться не за десятки секунд, а за единицы.
Никогда такого не видел. Как вы думаете, что тормозило? Запросы к базе?
Вовсе нет.
После выборки из базы, данные выкладывались в массив сложных объектов, в данном месте это было массив из почти что 500 объектов, внутри объектов лежали другие объекты, а них — ещё. Заполнение этой структуры занимало немало времени, но тормозило другое — цикл перебора массива объектов с выводом свойств объектов третьей вложенности.
В цикле не было вызвано ни одной функции (кроме htmlspecialchars), а вывод всего этого хозяйства занимал порядка десятков секунд.
Неразборчиво в ОБД «Мемориал»
Я думал, что в ОБД «Мемориал» записаны данные только тех карточек, данные которых удалось разобрать достоверно. Случайно выяснил, что очень много данных с пропусками.

Решил написать сканер неполных данных, чтобы не перебирать их руками. Никого из родственников, правда, не нашёл, но мало ли, пригодится ещё.
Финализированные конструкторы под разными именами
Вчера вечером подумалось, что если в PHP сделать следующее, будет ошибка или нет:
class A {
final public function a() {
echo 1;
}
}
class B extends A {
public function b() {
echo 2;
}
}
$o = new B();Интересно тут то, что не сразу видно в чём вообще загвоздка. Ну или я бы увидел не сразу, думаю, если бы сам этот пример не придумал. Кажется, все уже отвыкли в PHP от «старых» конструкторов, где конструктором считается метод, имя которого совпадает с именем класса (если нет метода «__construct»).
От осознания этого факта дополнительно отвлекают ничего незначащие имена классов и методов.
Проблема же в том, что в первом классе конструктор финализирован, его дальше переопределять нельзя. Но у второго класса имя конструктора другое. Вот и вопрос — что будет? От PHP можно чего угодно ожидать.
Тем не менее, тут язык выдаст логичную ошибку, которая, правда, способна вызвать лёгкое недоумение своей формулировкой:
Fatal error: Cannot override final A::a() with B::b() in test.php on line 12Наведение порядка в PHP (версия 5.5.0)
Нужно ещё похвалить авторов PHP за то, что они действительно стараются навести порядок в языке. С каждой версией он становится немного логичнее.
Например, теперь уже можно сделать вот так:
[
POW => function($x) { return $x*$x; },
SIN => function($x) { return sin($x); },
][$selector]Правда, всё ещё нельзя вызвать функцию сразу. В 5.5.0 будет работать вот такая вещь:
if (empty(somefunc())) { /* … */}Но то же самое нельзя сделать с isset, так как сложно сказать что это будет значить — в языке нет отдельного неопределённого значения переменной, она бывает только недекларированной; функция, конечно, может её вернуть, но на самом деле вернётся NULL, а isset от значения NULL — «ложь».
Появилась возможность использовать list внутри цикла следующим образом:
foreach ($somehash as list($key, $value)) { /* … */ }И сделано ещё несколько мелочей, но работы впереди ещё много.
Генераторы в PHP 5.5
Самым большим нововведением (и очень важным для языка) в PHP 5.5.0 будут генераторы. Я установил себе
Посмотрите на код, который я написал, пока мы ждали всех участников встречи, на которую я был приглашён:
<?
function enumerate($arr)
{
$i = 0;
foreach ($arr as $value) {
yield [$i++, $value];
}
}
function ifilter(callable $predicate = null, $arr)
{
if ($predicate === null) {
$predicate = 'boolval';
}
foreach ($arr as $value) {
if ($predicate($value)) {
yield $value;
}
}
}
function islice($arr, $start, $stop)
{
if (is_array($arr)) {
reset($arr);
for ($i = $start; $i > 0 && each($arr); $i--);
for ($i = $stop - $start + 1; $i > 0 && list(,$value) = each($arr); $i--) {
yield $value;
}
} else {
for ($i = $start; $i > 0 && $arr->valid(); $i--) {
$arr->next();
}
for ($i = $stop - $start; $i > 0 && $arr->valid(); $i--) {
yield $arr->current();
$arr->next();
}
}
}
$array = [10, 20, 30, 33, 40, 50, 60];
$gen = islice(
enumerate(
ifilter(
function($x) { return ($x % 10) === 0; },
$array
)),
1, 4
);
foreach ($gen as $value) {
print_r($value);
}Три генератора, пропускающие через себя данные, обрабатывают массив, всё работает. Названия взяты из Пайтона. Первый нумерует входящий поток данных (возвращает пары номер и значение), второй фильтрует значения через функцию, третий отрезает заданную часть от входных данных.
Концепция генераторов проста, как герань. PHP поддерживает два способа их создания (как и Пайтон), но мне милее оператор yield. Работает он примерно как return, но при повторном входе в функцию её выполнение происходит не сначала, а с того места, откуда управление было возвращено при помощи yield. Можно ещё передавать значения внутрь генератора, но я про это сейчас не буду. Вцелом, получаются настоящие ленивые вычисления — всё это вызывается ровно тогда, когда это реально нужно.
Генераторы, обычно, работают с итерируемыми объектами — возращают их и/или принимают. Итерируемый объект, проще говоря, это то, по чему можно пробежаться циклом. Генераторы, кстати, тоже итерируемые.
И вот тут опять проявляются недостатоки PHP: отсутствие общей логики, непродуманность.
Массив тут до сих пор примитивный тип и он требует иной способ обработки. Посмотрите на генератор islice, тут проблема видна во всей красе. Начинается она уже с хинтинга, я не могу потребовать от $arr быть типа array, потому что на вход может прийти другой генератор, и указать класс генератора тоже не могу — массив ему не принадлежит. Думаю, следует ожидать появления нового типа «iterable», как это уже случилось с callable.
Вторая проблема ещё более серьёзная. У массива нет методов (он же примитивный тип), а генераторы,
Мне кажется, PHP следует развивать класс ArrayObject, который уже присуствует в языке и делать прозрачное преобразование примитивных типов в объекты (как это происходит в JavaScript), это решило бы некоторые проблемы.
WEBP в PHP

Когда я делал биндинг GD для языка «Гугл Гоу», заметил, что в ПХП появилась поддержка формата WEBP. О самом формате
Чтобы поддержка формата (пока довольно куцая) появилась, нужно установить библиотеку libvpx и пересобрать ПХП с ключом
./configure --with-vpx-dir=/usr/local/Cellar/libvpx/1.1.0 \
… остальные опции
make
sudo make installПравда в phpinfo информация как у меня на картинке не появится (это я сам немного пропатчил исходники), не пугайтесь — так должно быть, проверить наличие поддержки WEBP можно следующим образом:
var_dump(function_exists('imagewebp'));Собственно, в языке появятся две новые функции: imagecreatefromwebp и imagewebp. У первой только одни аргумент — имя файла из которого надо создать ресурс GD (по действию ничем не отличается от любой другой подобной функции, например imagecreatefromjpeg), вторая принимает два параметра — ресурс и необязательные имя файла и качество, описание функции полностью совпадает с imagejpeg, с той только разницей, что сохраняет она формат WEBP.
Параметр качества изменяется от 0 до 100 и соотвествует параметру VP8 QP кодека в диапазоне от 1 до 63.
Код для защиты GIF, PNG и JPEG (к предыдущей статье)
В коментариях к предыдущей заметке о моём предложении по защите графических форматов от внедрённого кода один из читателей попросил привести код, который показывал бы как моё предложение работает.
Для форматов GIF, JPEG и PNG я за полчаса написал следующее (язык — PHP 5.4 и выше):
<?
// //bolknote.ru август 2012
// Защищаем изображения, возращает false в случае неудачи
function ProtectPicture($inname, $outname)
{
// типы изображения, которые мы можем защитить и функции защиты
$enabled = [
IMAGETYPE_JPEG => 'Jpeg',
IMAGETYPE_PNG => 'Png',
IMAGETYPE_GIF => 'Gif',
];
if (list(,, $type) = @getimagesize($inname)) {
if (isset($enabled[$type])) {
if ($fi = @fopen($inname, 'rb')) {
flock($fi, LOCK_SH);
if ($fo = @fopen($outname, 'wb')) {
flock($fo, LOCK_EX);
// если все файлы удалось открыть, то вызываем функцию защиты
$ret = call_user_func('ProtectPicture'.$enabled[$type], $fi, $fo, filesize($inname));
fclose($fo);
fclose($fi);
if (!$ret) {
unlink($outname);
}
return $ret;
} else {
fclose($fi);
}
}
}
}
return false;
}
/* Private functions */
function ProtectPictureJpeg($fi, $fo, $controllen)
{
// заголовок JPEG
if (!@fwrite($fo, fread($fi, 2))) return false;
$protect = '<?php __halt_compiler();';
$length = pack('n', strlen($protect) + 2);
// дописываю секцию COM (comment)
if (!@fwrite($fo, "\xFF\xFE$length$protect")) return false;
// остаток файла
if (@stream_copy_to_stream($fi, $fo) != $controllen - 2) return false;
return true;
}
function ProtectPicturePng($fi, $fo, $controllen)
{
// заголовок PNG
if (!@fwrite($fo, fread($fi, 8))) return false;
// пропускаем кусок IHDR (он должен быть первым))
$len = unpack('N', $lenbin = fread($fi, 4))[1] + 8;
$ihdr = $lenbin . fread($fi, $len);
// Смотрим, не написал ли в этот кусок хакер свой код
if (($pos = strpos($ihdr, '<?')) !== false) {
// в стандарте перечислены поля, которые есть в этом куске — там должно быть 13 байт (+4 — длина)
if ($len != 17) return false;
// эта комбинация может встретиться только в первых восьми байтах заголовка — дальше поля не
// могут принимать такие большие значения
if ($pos > 11) return false;
// если разрешены короткие теги, нужно присмотреться к этому коду
// в столь ограниченном пространстве вред можно натворить только через запуск
// чего-то внешнего
if (ini_get('short_open_tag') && strpos($ihdr, '`', $pos + 2) !== false) {
return false;
}
}
if (!@fwrite($fo, $ihdr)) return false;
// кусок iTXt (text) с нашим кодом
$protect = "<?php __halt_compiler();";
$length = pack('N', strlen($protect));
$sum = pack('N', sprintf('%u', crc32($protect)));
if (!@fwrite($fo, "{$length}iTXt{$protect}{$sum}")) return false;
// остаток файла
if (@stream_copy_to_stream($fi, $fo) != $controllen - 8 - 4 - $len) return false;
return true;
}
function ProtectPictureGif($fi, $fo, $controllen)
{
$protect = "<?php __halt_compiler();";
// заголовок GIF
if (!@fwrite($fo, fread($fi, 6))) return false;
// поле Logical Screen Descriptor
$lsd = fread($fi, 7);
$flag = ord($lsd[4]);
// указана ли Global Color Table?
if ($flag & 128) {
$pixel = 1 + ($flag & 0b111);
$colors = pow(2, $pixel);
$gctlen = $colors * 3;
// читаем Global Color Table
$gct = fread($fi, $gctlen);
// не вставил ли уже туда злоумышленник „<?“?
// меняем у такой комбинации последний бит — это малозаметно,
// безопасность важнее!
if (strpos($gct, '<?') !== false) {
$gct = str_replace('<', '=', $gct);
}
if (!@fwrite($fo, $lsd . $gct)) return false;
} else {
$gctlen = 0;
if (!@fwrite($fo, $lsd)) return false;
}
// пишем комментарий с куском защиты
fwrite($fo, "!\xfe" . chr(strlen($protect)) . $protect . "\0");
if (@stream_copy_to_stream($fi, $fo) != $controllen - 6 - 7 - $gctlen) return false;
return true;
}В коде есть два места где я ищу не встретилась ли комбинация <? раньше, чем я вставляю защиту — это в заголовочном куске (IHDR) в ПНГ и в глобальной таблице цветов у ГИФа.
В случае ПНГ я анализирую заголовок, чтобы понять как там оказалась эта комбинация, она валидна только в первых восьми байтах (на зло остаётся всего 6 байт), кроме того, в этом случае я контролирую размер заголовка, в стандарте там указано 13 байт, хотя, кажется, размер не ограничивается. В общем, в шести байтах особо не разгуляешься, всё что я придумал туда засунуть — `rm *`, т. е. очистка текущего каталога. На этот случай, я контролирую появление символа `. На мой взгляд, вероятность встретить в нормальном ПНГ в заголовке <? после которого идёт ` ничтожно мала.
С ГИФом проще — меняю младший бит у цвета, это вряд ли будет заметно, а безопасность важнее.
Защита от внедрения PHP-кода в картинки (JPEG, GIF, PNG)
На «Хабре» всплыла тема атаки, старой как мир — заливаем на сайт картинку, у которой внутри (в поле комментариев) есть
Во всех форматах принятых в вебе, есть поле, позволяющее вставить такие данные. В JPEG есть специальные секции APPn и COM, в PNG — iTXt, tEXt и можно создавать произвольные секции, в GIF — блоки расширений, в SVG можно использовать обычный
В качестве абсолютного метода защиты на «Хабре» предлагается пропустить картинку через
Можно ещё попробовать вырезать все секции файла, куда можно запихнуть текстовую информацию, но это не спасёт от файлов, где ПХП вставлен прямо в графическую часть, так, чтобы детектор валидности картинки (в ПХП для этого обычно используют getimagesize) не спотыкался на файле.
Мне кажется, я придумал более удачный метод защиты.
В загруженный файл можно добавить первой секцией (сразу после заголовка) свой собственный кусок
# смотрим что внутри файла
bolk@Bolk ~ $ hexdump -Cn128 sample.jpg
00000000 ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 |......JFIF......|
00000010 00 01 00 00 ff fe 00 1a 3c 3f 70 68 70 20 5f 5f |........<?php __|
00000020 68 61 6c 74 5f 63 6f 6d 70 69 6c 65 72 28 29 3b |halt_compiler();|
00000030 ff db 00 43 00 02 02 02 02 02 01 02 02 02 02 03 |...C............|
00000040 02 02 03 03 06 04 03 03 03 03 07 05 05 04 06 08 |................|
00000050 07 09 08 08 07 08 08 09 0a 0d 0b 09 0a 0c 0a 08 |................|
00000060 08 0b 0f 0b 0c 0d 0e 0e 0f 0e 09 0b 10 11 10 0e |................|
00000070 11 0d 0e 0e 0e ff db 00 43 01 02 03 03 03 03 03 |........C.......|
# запускаем
bolk@Bolk ~ $ php sample.jpg
????JFIF??Т. е. на экран выводится бинарный заголовок JPEG, первая секция, а потом интерпретатор дальше просто не идёт. Будет дальше ход хакера или нет, он не проинтерпретируется.
Патч модуля bcompiler для PHP 5.4
Мы тут по работе пытаемся закешировать
Я посидел над сообщениями при компиляции, посмотрел что другие ребята делали в модуле xcache для того, чтобы он заработал в 5.4, посмотрел внутренние структуры ПХП и родил патч, который позволяет успешно скомпилировать модуль. Модуль мои простенькие тесты проходит, но это правда не значит, что всё действительно работает.
Добавлено позднее: на более сложных тестах модуль Неудобно только то, что модуль не позволяет работать с объектами и анонимными функциями из ПХП 5.3 (которые тоже объекты), а так же компилирует всё в файл. Но второй недостаток легко обходится:Патч и немного кода.
diff -wrdu bcompiler-1.0.2/bcompiler_zend.c bcompiler-1.0.2.new/bcompiler_zend.c
--- bcompiler-1.0.2/bcompiler_zend.c 2011-06-09 15:58:42.000000000 +0400
+++ bcompiler-1.0.2.new/bcompiler_zend.c 2012-07-24 21:59:30.000000000 +0400
@@ -471,12 +471,6 @@
apc_serialize_zend_class_entry(zv->obj.ce, NULL, 0, NULL, 0 TSRMLS_CC);
#endif
break;
-#ifdef ZEND_ENGINE_2_4
- case IS_CLASS:
- case IS_SCALAR:
- case IS_NUMERIC:
- break;
-#endif
default:
/* The above list enumerates all types. If we get here,
* something very very bad has happened. */
@@ -551,12 +545,6 @@
apc_create_hashtable(&zv->obj.properties, apc_create_zval, NULL, sizeof(zval *) TSRMLS_CC);
#endif
break;
-#ifdef ZEND_ENGINE_2_4
- case IS_CLASS:
- case IS_SCALAR:
- case IS_NUMERIC:
- break;
-#endif
default:
/* The above list enumerates all types. If we get here,
* something very very bad has happened. */
@@ -636,8 +624,10 @@
}
SERIALIZE_SCALAR(arg_info->allow_null , zend_bool);
SERIALIZE_SCALAR(arg_info->pass_by_reference, zend_bool);
+#ifndef ZEND_ENGINE_2_4
SERIALIZE_SCALAR(arg_info->return_reference , zend_bool);
SERIALIZE_SCALAR(arg_info->required_num_args, int);
+#endif
#ifdef ZEND_ENGINE_2_1
if (BCOMPILERG(current_write) > 20) {
# ifdef ZEND_ENGINE_2_4
@@ -667,8 +657,10 @@
else ZS2SR(arg_info->class_name) = NULL;
DESERIALIZE_SCALAR(&arg_info->allow_null, zend_bool);
DESERIALIZE_SCALAR(&arg_info->pass_by_reference, zend_bool);
+#ifndef ZEND_ENGINE_2_4
DESERIALIZE_SCALAR(&arg_info->return_reference, zend_bool);
DESERIALIZE_SCALAR(&arg_info->required_num_args, int);
+#endif
#ifdef ZEND_ENGINE_2_1
if (BCOMPILERG(current_version) > 20) {
# ifdef ZEND_ENGINE_2_4
@@ -883,7 +875,9 @@
#else
SERIALIZE_SCALAR(*(zce->refcount) , int);
#endif
+#ifndef ZEND_ENGINE_2_4
SERIALIZE_SCALAR(zce->constants_updated, zend_bool);
+#endif
#ifdef ZEND_ENGINE_2
SERIALIZE_SCALAR(zce->ce_flags, zend_uint);
#endif
@@ -930,15 +924,15 @@
* and all the functions. */
BCOMPILER_DEBUG(("-----------------------------\nBUILTIN:\n"));
count = 0;
- if (zce->type == ZEND_INTERNAL_CLASS && zce->builtin_functions) {
- for (zfe = (zend_function_entry*)zce->builtin_functions; zfe->fname != NULL; zfe++) {
+ if (zce->type == ZEND_INTERNAL_CLASS && zce->info.internal.builtin_functions) {
+ for (zfe = (zend_function_entry*)zce->info.internal.builtin_functions; zfe->fname != NULL; zfe++) {
count++;
}
}
SERIALIZE_SCALAR(count, int);
for (i = 0; i < count; i++) {
- apc_serialize_zend_function_entry((zend_function_entry *)&zce->builtin_functions[i] TSRMLS_CC);
+ apc_serialize_zend_function_entry((zend_function_entry *)&zce->info.internal.builtin_functions[i] TSRMLS_CC);
}
#ifndef ZEND_ENGINE_2
/** old code: SERIALIZE_SCALAR(zce->handle_function_call, void*); */
@@ -974,12 +968,12 @@
/* do reflection info (file/start/end) !! TODO: check for PHP6 !! */
#ifdef ZEND_ENGINE_2
if (BCOMPILERG(current_write) >= 25) {
- char* fname = bcompiler_handle_filename(zce->filename TSRMLS_CC);
+ char* fname = bcompiler_handle_filename(zce->info.user.filename TSRMLS_CC);
apc_serialize_string(fname TSRMLS_CC);
- SERIALIZE_SCALAR(zce->line_start, int);
- SERIALIZE_SCALAR(zce->line_end, int);
- apc_serialize_zstring(zce->doc_comment, zce->doc_comment_len TSRMLS_CC);
- SERIALIZE_SCALAR(zce->doc_comment_len, int);
+ SERIALIZE_SCALAR(zce->info.user.line_start, int);
+ SERIALIZE_SCALAR(zce->info.user.line_end, int);
+ apc_serialize_zstring(zce->info.user.doc_comment, zce->info.user.doc_comment_len TSRMLS_CC);
+ SERIALIZE_SCALAR(zce->info.user.doc_comment_len, int);
}
#endif
@@ -1089,7 +1083,9 @@
*zce->refcount = 1; /* val: to avoid memort leaks */
}
#endif
+#ifndef ZEND_ENGINE_2_4
DESERIALIZE_SCALAR(&zce->constants_updated, zend_bool);
+#endif
#ifdef ZEND_ENGINE_2
DESERIALIZE_SCALAR(&zce->ce_flags, zend_uint);
#endif
@@ -1184,11 +1180,11 @@
zend_class_entry.builtin_functions member */
DESERIALIZE_SCALAR(&count, int);
- zce->builtin_functions = NULL;
+ zce->info.internal.builtin_functions = NULL;
if (count > 0) {
- zce->builtin_functions = (zend_function_entry*) ecalloc(count+1, sizeof(zend_function_entry));
+ zce->info.internal.builtin_functions = (zend_function_entry*) ecalloc(count+1, sizeof(zend_function_entry));
for (i = 0; i < count; i++) {
- apc_deserialize_zend_function_entry((zend_function_entry*)&zce->builtin_functions[i] TSRMLS_CC);
+ apc_deserialize_zend_function_entry((zend_function_entry*)&zce->info.internal.builtin_functions[i] TSRMLS_CC);
}
}
#ifndef ZEND_ENGINE_2
@@ -1237,11 +1233,11 @@
if (BCOMPILERG(current_version) >= 25) {
char* fname;
apc_create_string_u(&fname, -1 TSRMLS_CC);
- zce->filename = fname ? fname : estrdup(BCOMPILERG(current_filename));
- DESERIALIZE_SCALAR(&zce->line_start, int);
- DESERIALIZE_SCALAR(&zce->line_end, int);
- apc_create_string_u(ZS2SP(zce->doc_comment), -1 TSRMLS_CC);
- DESERIALIZE_SCALAR(&zce->doc_comment_len, int);
+ zce->info.user.filename = fname ? fname : estrdup(BCOMPILERG(current_filename));
+ DESERIALIZE_SCALAR(&zce->info.user.line_start, int);
+ DESERIALIZE_SCALAR(&zce->info.user.line_end, int);
+ apc_create_string_u(ZS2SP(zce->info.user.doc_comment), -1 TSRMLS_CC);
+ DESERIALIZE_SCALAR(&zce->info.user.doc_comment_len, int);
}
#endif
@@ -1564,7 +1560,7 @@
#ifdef ZEND_ENGINE_2_4
SERIALIZE_SCALAR(zoa->last_literal, zend_uint);
- SERIALIZE_SCALAR(zoa->size_literal, zend_uint);
+ //SERIALIZE_SCALAR(zoa->size_literal, zend_uint);
for (i = 0; i < (int) zoa->last_literal; i++) {
bc_serialize_zend_literal(&zoa->literals[i] TSRMLS_CC);
}
@@ -1572,7 +1568,7 @@
SERIALIZE_SCALAR(zoa->refcount[0], zend_uint);
SERIALIZE_SCALAR(zoa->last, zend_uint);
- SERIALIZE_SCALAR(zoa->size, zend_uint);
+ //SERIALIZE_SCALAR(zoa->size, zend_uint);
/* If a file 'A' is included twice in a single request, the following
* situation can occur: A is deserialized and its functions added to
@@ -1595,7 +1591,7 @@
SERIALIZE_SCALAR(zoa->T, zend_uint);
SERIALIZE_SCALAR(zoa->last_brk_cont, zend_uint);
- SERIALIZE_SCALAR(zoa->current_brk_cont, zend_uint);
+ //SERIALIZE_SCALAR(zoa->current_brk_cont, zend_uint);
#ifndef ZEND_ENGINE_2
SERIALIZE_SCALAR(zoa->uses_globals, zend_bool);
#endif
@@ -1608,8 +1604,8 @@
}
apc_serialize_hashtable(zoa->static_variables, apc_serialize_zval_ptr TSRMLS_CC);
assert(zoa->start_op == NULL);
- SERIALIZE_SCALAR(zoa->return_reference, zend_bool);
- SERIALIZE_SCALAR(zoa->done_pass_two, zend_bool);
+ //SERIALIZE_SCALAR(zoa->return_reference, zend_bool);
+ //SERIALIZE_SCALAR(zoa->done_pass_two, zend_bool);
fname = bcompiler_handle_filename(zoa->filename TSRMLS_CC);
apc_serialize_string(fname TSRMLS_CC);
if (fname) efree(fname);
@@ -1628,9 +1624,9 @@
#endif
SERIALIZE_SCALAR(zoa->fn_flags, zend_uint);
SERIALIZE_SCALAR(zoa->required_num_args, zend_uint);
- SERIALIZE_SCALAR(zoa->pass_rest_by_reference, zend_bool);
+ //SERIALIZE_SCALAR(zoa->pass_rest_by_reference, zend_bool);
- SERIALIZE_SCALAR(zoa->backpatch_count, int);
+ //SERIALIZE_SCALAR(zoa->backpatch_count, int);
#ifdef ZEND_ENGINE_2_3
SERIALIZE_SCALAR(zoa->this_var, zend_uint);
#else
@@ -1642,7 +1638,7 @@
#ifdef ZEND_ENGINE_2_1
if (BCOMPILERG(current_write) >= 0x0007) {
SERIALIZE_SCALAR(zoa->last_var, int);
- SERIALIZE_SCALAR(zoa->size_var, int);
+ //SERIALIZE_SCALAR(zoa->size_var, int);
for (i = 0; i < zoa->last_var; i++) {
SERIALIZE_SCALAR(zoa->vars[i].name_len, int);
apc_serialize_zstring(ZS2S(zoa->vars[i].name), ZLEN(zoa->vars[i].name_len) TSRMLS_CC);
@@ -1706,7 +1702,7 @@
#ifdef ZEND_ENGINE_2_4
DESERIALIZE_SCALAR(&zoa->last_literal, zend_uint);
- DESERIALIZE_SCALAR(&zoa->size_literal, zend_uint);
+ //DESERIALIZE_SCALAR(&zoa->size_literal, zend_uint);
zoa->literals = NULL;
if (zoa->last_literal) {
zoa->literals = (zend_literal*) ecalloc(zoa->last_literal, sizeof(zend_literal));
@@ -1722,7 +1718,7 @@
*zoa->refcount = 1; /* val: to avoid memort leaks */
}
DESERIALIZE_SCALAR(&zoa->last, zend_uint);
- DESERIALIZE_SCALAR(&zoa->size, zend_uint);
+ //DESERIALIZE_SCALAR(&zoa->size, zend_uint);
zoa->opcodes = NULL;
@@ -1736,7 +1732,7 @@
DESERIALIZE_SCALAR(&zoa->T, zend_uint);
DESERIALIZE_SCALAR(&zoa->last_brk_cont, zend_uint);
- DESERIALIZE_SCALAR(&zoa->current_brk_cont, zend_uint);
+ //DESERIALIZE_SCALAR(&zoa->current_brk_cont, zend_uint);
#ifndef ZEND_ENGINE_2
DESERIALIZE_SCALAR(&zoa->uses_globals, zend_bool);
#endif
@@ -1749,10 +1745,10 @@
}
apc_create_hashtable(&zoa->static_variables, apc_create_zval, NULL, sizeof(zval *) TSRMLS_CC);
- zoa->start_op = NULL;
+ //zoa->start_op = NULL;
- DESERIALIZE_SCALAR(&zoa->return_reference, zend_bool);
- DESERIALIZE_SCALAR(&zoa->done_pass_two, zend_bool);
+ //DESERIALIZE_SCALAR(&zoa->return_reference, zend_bool);
+ //DESERIALIZE_SCALAR(&zoa->done_pass_two, zend_bool);
apc_create_string_u(&fname, -1 TSRMLS_CC);
if (fname) {
zoa->filename = zend_set_compiled_filename(fname TSRMLS_CC);
@@ -1798,8 +1794,8 @@
zoa->fn_flags &= ~ZEND_ACC_IMPLEMENTED_ABSTRACT; /* this is set later by zend_do_inheritance() */
#endif
DESERIALIZE_SCALAR(&zoa->required_num_args, zend_uint);
- DESERIALIZE_SCALAR(&zoa->pass_rest_by_reference, zend_bool);
- DESERIALIZE_SCALAR(&zoa->backpatch_count, int);
+ //DESERIALIZE_SCALAR(&zoa->pass_rest_by_reference, zend_bool);
+ //DESERIALIZE_SCALAR(&zoa->backpatch_count, int);
#ifdef ZEND_ENGINE_2_3
DESERIALIZE_SCALAR(&zoa->this_var, zend_uint);
#else
@@ -1811,15 +1807,15 @@
#ifdef ZEND_ENGINE_2_1
if (BCOMPILERG(current_version) >= 0x0007) {
DESERIALIZE_SCALAR(&zoa->last_var, int);
- DESERIALIZE_SCALAR(&zoa->size_var, int);
- if (zoa->size_var > 0) {
+ //DESERIALIZE_SCALAR(&zoa->size_var, int);
+ /*if (zoa->size_var > 0) {
zoa->vars = ecalloc(zoa->size_var, sizeof(zoa->vars[0]));
for (i = 0; i < zoa->last_var; i++) {
DESERIALIZE_SCALAR(&(zoa->vars[i].name_len), int);
apc_create_string_u(ZS2SP(zoa->vars[i].name), -1 TSRMLS_CC);
DESERIALIZE_SCALAR(&(zoa->vars[i].hash_value), ulong);
}
- }
+ }*/
}
#endif
diff -wrdu bcompiler-1.0.2/php_bcompiler.c bcompiler-1.0.2.new/php_bcompiler.c
--- bcompiler-1.0.2/php_bcompiler.c 2011-06-09 15:58:42.000000000 +0400
+++ bcompiler-1.0.2.new/php_bcompiler.c 2012-07-24 21:27:31.000000000 +0400
@@ -571,7 +571,7 @@
#ifndef ZEND_ENGINE_2
if (i > n_old_class)
#else
- if (zc->filename && strcmp(zc->filename, real_path) == 0)
+ if (zc->info.user.filename && strcmp(zc->info.user.filename, real_path) == 0)
#endif
{
SERIALIZE_SCALAR(BCOMPILER_CLASS_ENTRY, char)
@@ -632,7 +632,7 @@
#ifndef ZEND_ENGINE_2
if (i > n_old_class)
#else
- if (zc->filename && strcmp(zc->filename, real_path) == 0)
+ if (zc->info.user.filename && strcmp(zc->info.user.filename, real_path) == 0)
#endif
{
#if PHP_MAJOR_VERSION >= 6
diff -wrdu bcompiler-1.0.2/php_bcompiler.h bcompiler-1.0.2.new/php_bcompiler.h
--- bcompiler-1.0.2/php_bcompiler.h 2011-06-09 15:58:42.000000000 +0400
+++ bcompiler-1.0.2.new/php_bcompiler.h 2012-07-24 22:13:31.000000000 +0400
@@ -22,7 +22,7 @@
#ifndef PHP_BCOMPILER_H
#define PHP_BCOMPILER_H
-#define PHP_BCOMPILER_VERSION "1.0.2"
+#define PHP_BCOMPILER_VERSION "1.0.2 (bolk) "
#ifdef HAVE_CONFIG_H
#include "config.h"$fp = fopen('php://memory', 'rw');
bcompiler_write_header($fp);
bcompiler_write_function($fp, 'тут имя вашей функции, которую надо скомпилировать');
bcompiler_write_footer($fp);
fseek($fp, 0);
$compiled = stream_get_contents($fp); // байт-код теперь в переменной $compiled
fclose($fp);
ПХП: красавица и чудовище
Одна из превосходнейших вещей в ПХП — обработчики протоколов. Они позволяют работать с разнообразными источниками данных как с файлами. Встроенные протоколы умеют понимать ftp, http, data URL, ssh2, rar, zlib и ещё несколько менее известных штук. Т. е. традиционный путь до файла в ПХП на самом деле является УРЛом, по виду которого интерпретатор решает какой обработчик за него ответственен. Если протокол (в терминах УРЛ) не указан, считается, что идёт обращение к локальному файлу. Язык так же позволяет создавать свои обработчики.
ПХП единственный из известных мне языков, где я могу с лёгкостью сделать такие вещи:
copy('ftp://user:pass@example.org/pub/somefile.txt', '/tmp/somefile.txt');
$var = file_get_contents('https://example.org/index.html');
# и при этом
copy('/tmp/somefile', '/tmp/anotherfile.txt');
$var = file_get_contents('/etc/passwd');Немного более сложный синтаксис позволяет сделать и более сложные вещи:
$post = http_build_query([
'login' => $login,
'pass' => $password,
]);
$options = [
'http' => [
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded\r\n'.
'Content-length: ' . strlen($post),
'content' => $post,
'proxy'=>"tcp://192.168.1.1:3128"
]
];
$result = file_get_contents('http://example.org/action.php', false, stream_context_create($options));От полного изящества далеко,
Что сделано из рук вон плохо — это получение заголовков ответа для протоколов HTTP и HTTPS: ответ просто помещается в локальную переменную http_response_header, это печальное и достойное сожаления решение.
Более неявное поведение трудно и выдумать. Оно настолько неявное, что многие о нём даже не подозревают. Ещё отвратительно, что эта переменная содержит заголовки не в виде ключ/значение, а просто набор строк:
file_get_contents('//bolknote.ru');
var_dump($http_response_header);вернёт:
array(14) {
[0]=>
string(15) "HTTP/1.1 200 OK"
[1]=>
string(13) "Server: nginx"
[2]=>
string(35) "Date: Wed, 11 Jul 2012 19:50:10 GMT"
… и так далееКак такой непродуманный функционал может являться продолжением такой прекрасной штуки мне непонятно.