Авг 282013
 

Безопасность в PHP. Явное приведение типов.

Операторы прямого приведения данных.

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

Приведём несколько операндов, которые позволяют сделать это легко и без особых усилий при программировании на языке PHP, а так же поговорим о функциях приведения типов.

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

$var = (int)$some_var, (integer)$some_var – явное приведение переменной $some_var к целочисленному типу
$var = (float)$some_var, (real)$some_var, (double)$some_var – явное приведение переменной $some_var к вещественному типу
$var = (array)$some_var – явное приведение переменной $some_var к массиву
$var = (string)$some_var – явное приведение переменной $some_var к массиву
$var = (bool)$some_var, (boolean)$some_var – явное приведение переменной $some_var логическому значению (true,false)
$var = (object)$some_var – явное приведение переменной $some_var к объекту
Новшества PHP 5:

(unset)$some_var – присвоение типа NULL, уничтожение переменной, освобождение памяти.
Новшества PHP 5.2.1:

(binary) $some_var – приведение переменной к бинарному представлению.
Примеры использования:

Рассмотрим передачу id статьи через массив GET news.php?id=23

Вариант подготовки параметра для безопасной передачи в запрос к базе данных:

?
1
$news_id = (int)$_GET[«id»];
Еще одной интересной функцией является abs, которую советую применять при фильтрации входных данных для заведомо неотрицательных величин, например уникального идентификатора статьи, который является по умолчанию числом уникальным, положительным, с auto increment:

abs($var) – Принимаемые параметры: $var – переменная целого (или вещественного) типа. Возвращает абсолютное по модулю число: для всех отрицательных величин возвращается положительное, для всех положительных возвращается просто значение переменной

Рассмотрим предыдущий пример в новом ракурсе:

Так как integer может быть как положительным, так и отрицательным целым числом, перепишем предыдущий пример с использованием функции abs:

Вариант подготовки параметра для безопасной передачи в запрос к базе данных:

?
1
$news_id = abs((int)$_GET[«id»]);
Функции явного приведения данных.

(int) intval($var,[$base = 10]) — Принимаемые параметры: $var – переменная для приведения типов. Приводит переменную к целочисленному типу. Возвращает переменную, приведенную к целочисленному типу, в случае с массивом 0 если массив пуст, 1 – если есть элементы, в случае со строкой: если удаётся преобразовать к целому числу – то целое число, или 0 в обратном случае.

$base = основание, десятичная система исчисления по умолчанию.

(float) floatval($var) — Принимаемые параметры: $var – переменная для приведения типов. Приводит переменную к вещественному типу. Возвращает переменную, приведенную к вещественному типу, в случае неудачи возвращает 0.
(bool) settype(&$var, string $type ) — Принимаемые параметры: $var – переменная для приведения типов, $type – к какому типу привести переменную:

«boolean»,»bool» – приведение типа переменной к логическому значению (true, false),
«integer»,»int» – приведение типа переменной к целому значению,
«float»,»double» – приведение типа переменной к вещественному значению,
«string» – приведение типа переменной к строковому значению,
«array» – приведение к массиву,
«object» – приведение типа переменной к объекту,
«null» – приведение типа переменной к типу NULL (освобождение занимаемой памяти).
Производит прямое приведение переменной к значению указанного типа. В случае удачи возвращает true, иначе – false.

Общирный пример практического использования вышеописанных функций при программировании на языке PHP:

Приведём пример безопасного запроса к базе данных с получением id новости из строки запроса:

<?php
$id             = isset($_GET["id"]) ? abs(intval($_GET["id"])) : 0;
If(!empty($id)){
    $q          = "SELECT `news_text` FROM `news` WHERE `news_id`=".$id;
    $p          = mysql_query($q);
    if($p && is_resource($p)){
    list($text) = mysql_fetch_row($p);
    echo $text;
}
}
?>
Авг 282013
 

Изучаем PHP. Пишем parser похожих запросов Yandex.

В жизни программиста очень часто возникает задача получения полезной информации из других источников. Это может быть всесторонняя оценка цен конкурентов по определённому товару, сбор изображений и описаний для автоматизации процесса загрузки новых позиций в интернет – магазин, поиск аудио и видео — информации и многое другое.

Здесь на выручку нам так называемый «скрипт – парсер». В задачу которого входит получение и проверка информации, поиск нужных фрагментов и исключение «мусора».

Рассмотрим на примере так называемый «parser – spider» (парсер – паук), в задачу которого будет входить сбор информации со страниц yandex для поиска похожих ключевых слов, которые мы будем использовать в раскрутке нашего проекта. И напишем данный парсер на PHP

Как же нам может помочь parser?
Мы получаем список сходных фраз, которыми пользуются посетители яндекса при поиске нашей тематики, и в дальнейшем сможем ориентироваться на них при написании материалов на нашем сайте, оцениваем спрос к тематике нашего сайта. А так же это даст нам возможность определить поисковые запросы с меньшей конкурентной борьбой сходной тематики, чтобы выйти по ним в ТОП поисковой системы Yandex. Как следствие, благодаря этой информации мы расширим аудиторию нашего проекта.

А откуда мы можем получить информацию по похожим поисковым запросам Yandex?
Дело в том, что при поиске Яндекс подсказывает сам, отображая блок внизу страницы: Вместе с «[фраза для поиска]» ищут — несколько подобных поисковых запросов.

Вверху в левой части каждой страницы указывает, сколько нашлось вариантов, например:
создать сайт — Нашлось 104 млн ответов

Итак, какие же задачи поставим перед скриптом – парсером?

Возможность рекурсивного парсинга похожих поисковых запросов с определением уровня вложенности
Если не найдено ни одного похожего запроса по указанной фразе (а такое тоже бывает), попробуем просмотреть запросы по её сокращению:

Например, если ввести фразу Создать Сайт Харьков, то похожих поисковых запросов не будет в выдаче поисковика, но если ввести: создать сайт – они появятся. Ограничимся урезанием фразы на одно слово вконце.

Обходить парсер будет не только одну фразу, а займётся обработкой целого списка из фраз (передадим массивом)
Сохранение найденных поисковых фраз и их встречаемостью в файл
Нюансы:

Поставим задержку на опрос поисковой системы (не меньше 2 секунд, а то и до 30 секунд в случайном порядке) на каждый поисковый запрос. Мы же не хотим, чтобы Яша нас «отругал» за плохое поведение?

<?php
function getURIContent($url){
        $tuCurl                         = curl_init();
        $tuData                         = '';
        if($tuCurl && is_resource($tuCurl)){
                $opts                   = array(
                CURLOPT_URL             => $url,
                CURLOPT_HTTPGET         => 1,
                CURLOPT_HEADER          => 0,
                CURLOPT_RETURNTRANSFER  => 1,
                CURLOPT_FOLLOWLOCATION  => 1,
                CURLOPT_BINARYTRANSFER  => 1,
                CURLOPT_AUTOREFERER     => 1,
                CURLOPT_CONNECTTIMEOUT  => 90,
                CURLOPT_USERAGENT       => $_SERVER['HTTP_USER_AGENT'],
                CURLOPT_COOKIEJAR       => dirname(__FILE__)."/cookie.txt",
                CURLOPT_COOKIEFILE      => dirname(__FILE__)."/cookie.txt",
                CURLOPT_REFERER         => $url
                                         );
        foreach($opts as $key=>$value){
          curl_setopt($tuCurl,$key,$value);
        }
        $tuData   = curl_exec($tuCurl);
        curl_close($tuCurl);
        }
        return $tuData;
}
function parseRecursive($question,$max_depth,$first = true){
    global $time_wait;
    $time_wait  = $time_wait < 2 ? 2 : $time_wait;
    $rand       = mt_rand($time_wait, $time_wait + 30);
    sleep($rand);
    $question   = urlencode($question);
    $where      = 'http://yandex.ua/yandsearch?text='.$question;
    $content    = getURIContent($where);
    $found      = false;
    if(!empty($content)){
        $how_many= array();
        preg_match_all('~<strong[^>]*?class="b-head-logo__text"[^>]*?>(.*?)</strong>~is',$content,$how_many);
        $numbers= '';
        if(is_array($how_many)
                     && isset($how_many[1][0])
                           && !empty($how_many[1][0])){
            $numbers   = trim($how_many[1][0]);
            $numbers   = preg_replace("~<br[^>]*?>~is",' ', $numbers);
            $numbers   = str_ireplace("&nbsp;",' ', $numbers);
            $numbers   = str_ireplace("\r\n",' ', $numbers);
            $numbers   = str_ireplace("\r",' ', $numbers);
            $numbers   = str_ireplace("\n",' ', $numbers);
            $numbers   = str_ireplace("\t",' ', $numbers);
            $numbers   = str_ireplace("\p",' ', $numbers);
            $numbers   = str_ireplace("\b",' ', $numbers);
            $numbers   = html_entity_decode($numbers,ENT_QUOTES,'UTF-8');
            $numbers   = strip_tags($numbers);
        }
        if(!empty($numbers)){
            $numbers = urldecode($question) . ' - ' . $numbers. "\n";
            $fp = fopen(WHERE_TO_SAVE,'a+');
            if($fp && is_resource($fp)){
                echo 'ADDING '.$numbers. "<br />\n";
                flock($fp,LOCK_EX);
                fwrite($fp,$numbers);
                flock($fp,LOCK_UN);
                fclose($fp);
            }
        }
        $related= array();
        $links  = array();
        preg_match_all('~<table[^>]*?class="b-related__table"[^>]*?>(.*?)</table>~is',$content,$related);
        /* <a[^>]*?href=("|\')([^"\']*?)(\1)[^>]*?>(.*?)</a> */
        if(is_array($related[1]) &&
                    isset($related[1][0])
                        && $max_depth){
            --$max_depth;
            preg_match_all('~<a[^>]*?href=("|\')([^"\']*?)(\1)[^>]*?>(.*?)</a>~is',$related[1][0],$links);
            if(is_array($links)
                           && isset($links[2])
                                && isset($links[4])
                                     && sizeof($links[2])
                                && sizeof($links[4])
                                     && $max_depth){
                $sizeof     = sizeof($links[4]);
                for($i = 0; $i < $sizeof; $i ++){
                    $text   = '';
                    $text   = trim($links[4][$i]);
                    $text   = preg_replace("~<br[^>]*?>~is",' ', $text);
                    $text   = str_ireplace("&nbsp;",' ', $text);
                    $text   = str_ireplace("\r\n",' ', $text);
                    $text   = str_ireplace("\r",' ', $text);
                    $text   = str_ireplace("\n",' ', $text);
                    $text   = str_ireplace("\t",' ', $text);
                    $text   = str_ireplace("\p",' ', $text);
                    $text   = str_ireplace("\b",' ', $text);
                    $text   = html_entity_decode($text,ENT_QUOTES,'UTF-8');
                    $text   = strip_tags($text);
                    if(!empty($links[2][$i]) && !empty($text)){
parseRecursive($text,$max_depth,false);
                    }
                }
            }
        } elseif (is_array($related[1]) && $max_depth){
            $question    = urldecode($question);
            $words= array();
            $words= explode(' ',$question);
            $sizeof= sizeof($words);
            $words= array_map('trim',$words);
            --$sizeof;
            $words= array_slice($words,0,$sizeof);
            $question    = join(' ',$words);
            if(strlen($question) > 4){
                --$max_depth;
                if($max_depth){
                parseRecursive($question,$max_depth,false);
                }
            }
        }
    }
}
ini_set('max_execution_time',999999);
ini_set('max_input_time',999999);
$max_depth      = 2;
$time_wait      = 2;
$questions      = array('создать сайт',
                          'сайт на joomla',
                          'создать сайт Харьков',
                          'сайт на wordpress');
define('WHERE_TO_SAVE',dirname(__FILE__).'/prases.txt');
if(!is_file(WHERE_TO_SAVE)){
    $fp  = fopen(WHERE_TO_SAVE,'w+');
    if($fp && is_resource($fp)){
        fclose($fp);
    }
    if(is_file(WHERE_TO_SAVE) && !is_writable(WHERE_TO_SAVE)){
        chmod(WHERE_TO_SAVE,0777);
    }
}
if(is_file(WHERE_TO_SAVE)){
    $fp  = fopen(WHERE_TO_SAVE,'w+');
    if($fp && is_resource($fp)){
        fclose($fp);
    }
}
if(is_file(WHERE_TO_SAVE) && is_writable(WHERE_TO_SAVE)){
    $sizeof     = sizeof($questions);
    for($i = 0; $i < $sizeof; $i ++){
        parseRecursive($questions[$i],$max_depth);
    };
};
?>

Итак, что же здесь происходит:

Для начала с помощью ini_set установим побольше время выполнения для скрипта.

В переменной $max_depth укажем, сколько вложенных уровней «обходить» (на какую глубину «погружаться») для сбора похожих фраз.

Например: $max_depth = 2; — находим похожие поисковые фразы, переходим по каждой из них, и собираем результаты по похожим уже на них поисковым фразам.

Совет: не делайте слишком большим уровень вложенности. Иначе сильно отклонитесь от первоначальной тематической фразы.

Переменной $time_wait указываем, сколько секунд ждать до следующего запроса к поисковику.

Обратите внимание – 2 секунды – это минимальное значение. Иначе сайт выдаст вам капчу или наложит бан по ай-пи адресу.

В массив $questions = array(‘создать сайт’,’сайт на joomla’,’создать сайт Харьков’,’сайт на wordpress’); — списком строк через запятую добавляем запросы для обработки parser ом.

Далее, определяем, существует ли файл prases.txt – в который мы и добавим найденный результат. Если не существует, создаём его и делаем доступным на запись.

После этого обходим массив $questions и передаём каждый запрос функции parseRecursive вместе с уровнем вложения.

(void) function parseRecursive($question,$max_depth,$first = true): принимает одну поисковую фразу (string)$questions[$i] и уровень вложенности поиска $max_depth, а так же неявный флаг (bool) $first, для определения, в какой раз подряд вызывается функция (для чего, рассмотрим ниже).

В функции мы кодируем для возможности передачи в виде запроса нашу ключевую фразу с помощью urlencode и получаем содержимое запроса в функции getURIContent, основанной на curl. После чего регулярным выражением ~]*?class=»b-head-logo__text»[^>]*?>(.*?)~is узнаём, сколько раз встречается данный поисковой запрос.

Очищаем полученный запрос от html и спец символов. Помещаем выражение и количество найденных страниц по этой поисковой фразе в файл prases.txt.

Исследуем содержимое полученной страницы на предмет содержания «похожих поисковых запросов» ~]*?class=»b-related__table»[^>]*?>(.*?)

~is.

Если похожие поисковые фразы найдены, извлекаем ссылки на них и их анкоры: ~]*?href=(«|’)([^»‘]*?)(\1)[^>]*?>(.*?)~is

При этом уменьшая на единицу наш уровень вложенности —$max_depth

Если уровень вложенности позволяет (отличен от нуля), передаём анкоры (текст, заключённый в тег a) рекурсивно в функцию parseRecursive. Обрабатываем следующий уровень.

Если же на странице не было найдено по указанной поисковой фразе ни одного результата, и вызов функции parseRecursive был осуществлён впервые (неявный флаг $first со значением true) то проверяем, содержит ли слова поисковый запрос? (Разбиваем по пробельному символу фразу на слова). Исключаем последнее слово, и передаём полученное словосочетание заново функции parseRecursive.

Примечание автора: для получения содержимого страниц по указанным поисковым запросам используется функция getURIContent, которая принимает адрес страницы и возвращает её содержимое.

Её работа основывается на curl, что даёт ряд преимуществ перед url wrapper ами функции file, fopen, file_get_contents и т.п.:

Более быстрый процесс получения информации
Передача referrer, cookie, user agent
Управление временем ожидания (таймаутом) на получение информации
Возможность корректно перейти по всем пере направлениям (Location:), если таковые встречаются
Бинарная передача данных
В следующих статьях мы поведаем Вам более подробно о возможностях функции curl и приведём другие интересные примеры её использования.

Замечание: каждый программист должен задаваться не только вопросом, что было сделано хорошо, но и «а что можно было бы сделать лучше» (оптимизация кода, повышение качества результата, удобная подача информации и т.п.).

Убрать дубли фраз, обнаруженные при поиске.
Для получения более достоверных вариантов проверять, встречается хотя бы одно слово из целевого поискового словосочетания, заданного в $questions в фразах, обнаруженных при многоуровневом обходе страниц
Каждый результат поиска записать в отдельный файл.
Визуализировать результат при помощи графиков
Задание для тех, кто учит parser ы на PHP:
Для выполнения этой же задачи напишите парсер, который обходит wordstat yandex

Авг 282013
 

Изучаем PHP. Тонкости экстремальной отладки скриптов.

Иногда сайт ведёт себя неадекватно. А разработка сайта всё затягивается. Очень трудно разобраться, в чём проблема, когда вообще выпадает Белый лист, и нигде нет никаких ошибок.

Дело в том, что многие проекты находятся на обычных хостингах, администрация которых действует по правилу «меньше знаешь, лучше спишь». Обычно под выполнение скриптов зарезервированы довольно скудные ресурсы. А вывод ошибок подавляется, или перенаправляется. Как же быть в этой ситуации?

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

Примечание автора: здесь будут рассмотрены подходы, которые касаются самого языка программирования PHP. Этот способ является более универсальным, чем рассмотрение методов, связанных с конфигурацией определённого серверного П.О.

Что же делать, когда у Вас вместо сайта возникает белый лист, для начала давайте включим отображение ошибок: добавьте следующие строки в начало скрипта, требующего отладки.

<?php
ini_set("error_reporting",E_ALL & ~E_NOTICE);
ini_set("display_errors",1);
ini_set("error_log","");
function errPrint(){
    print_r(error_get_last());
}
register_shutdown_function("errPrint");
 ...
?>

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

Разберём приведённый выше PHP код:

Описание функции ini_set:

(string) ini_set ($server_var_name,$server_var_value) – устанавливает значение $server_var_value для переменной среды выполнения PHP $server_var_name, в случае удачного присвоения возвращает значение переменной, иначе возвращает false.

Примечание автора: со всеми переменными окружения, значение которых можно переопределить, Вы можете ознакомиться здесь

Для начала мы включаем отображение ошибок ini_set(«display_errors»,1).

После этого мы устанавливаем значение для вывода ошибок «error_reporting», E_ALL & ~E_NOTICE, равное отображению всех ошибок, но без подсказок E_NOTICE.

Примечание автора: E_NOTICE – подсказки, как «правильно программировать» на усмотрение PHP интерпретатора. Зачастую, подсказки могут быть очень полезны при отладке сайта в целом, но при отладке в критических ситуациях они, в основном, несут малополезную информацию да ещё и в огромном количестве.

Затем мы переопределяем вывод ошибок в стандартный поток браузера посетителя ini_set(«error_log»,»»);.

Примечание автора: вторым параметром здесь можно передать абсолютный путь к файлу, доступному на запись, и весь поток ошибок будет перенаправлен в него.

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

Но, благо, с версии 5.2 (о чудо!), заявленная ещё в php 4, функция — перехватчик register_shutdown_function заработала!.

Описание функции register_shutdown_function:

(void) register_shutdown_function ( callback $function ) — принимает имя функции $function, которая выполняется по завершению исполнения кода, даже во время возникновения критической ошибки с прерыванием работы скрипта.

Примечание автора: в данном случае функция $function должна быть определена (или подключена) до передачи в register_shutdown_function.

Определим функцию errPrint, которая распечатает последнюю ошибку print_r(error_get_last()) после завершения скрипта. И передадим название этой функции обработчику register_shutdown_function:

<?php
register_shutdown_function("errPrint");
?>

Описание функции error_get_last:

(array) error_get_last ( void ) – возвращает в виде массива информацию о последней случившейся ошибке. Массив содержит ключи «type» – тип ошибки, «message» – подробное сообщение об ошибке, «file» – путь к файлу, в котором произошла ошибка, «line» – номер строки, в которой произошла ошибка и номер символа.

А в нашем случае, последняя «случившаяся» ошибка – и есть та самая, которая приводит к остановке выполнения скрипта.

Благодаря такому подходу можно будет «отловить» буквально любую ошибку и получить детальную информацию о ней.

Не забывайте отключать информацию о выводе ошибок в браузер пользователя!
Но, после отладки php скрипта и устранения критической ошибки, не забывайте отключить полностью информацию о выводе ошибок в браузер пользователя. Связано это как с соображениями безопасности (многие ошибки и предупреждения могут доступной для злоумышленника конфиденциальную информацию, например: об абсолютном пути на сервере к Вашим скриптам, их местоположении и т.п.), а так же могут препятствовать нормальной инициализации сессий и работе функций, передающих заголовки header, так и с эстетической точки зрения не стоит выводить много малопонятной информации в браузер посетителя Вашего сайта. Для этого можно подавить полностью вывод ошибок. Или же переопределить вывод ошибок в файл с помощью функции ini_set(«error_log»,»путь к файлу ошибок, доступному на запись»);. При этом желательно, чтобы этот файл находился вне папок Вашего сайта, или доступ к файлу извне был полностью ограничен.

Примечание автора:

Приведём код полного подавления ошибок:

<?php
ini_set("error_reporting",0);
ini_set("display_errors",0);
ini_set("error_log","путь к файлу хранения ошибок");
?>

Так же для некоторых функций, которые всё равно генерируют ошибки и предупреждения, можно использовать оператор подавления ошибок. Для этого достаточно перед именем функции указать: @, например:

<?php
@mkdir("/site/cahce",0777);
?>

Хотя, настоятельно рекомендую Вам вместо этого «ленивого» способа с помощью условных операторов обработать все ситуации, которые приводят к появлению данной ошибки. Это добавит стабильности и увеличит «запас прочности» Ваших скриптов во время возникновения внештатных ситуаций, и оставит меньше «почвы для размышления» злоумышленникам.
Я всё равно вижу белую страницу!

Здесь может быть несколько причин:

Данный подход универсален для php 5.2 и выше. Для того, чтобы узнать версию Вашего PHP интерпретатора, добавьте в Ваш скрипт вызов функции phpinfo();
В первых строках будет описана версия PHP.

Если версия php ниже 5.2:

Для более ранних версий php Дмитрием Котеровым было разработано гибкое решение по перехвату критических ошибок, с ним Вы можете ознакомиться здесь

Если у Вас версия PHP выше 5.2, но белая страница всё таки продолжает появляться с настроенным выводом ошибок, значит Ваш скрипт автоматически завершается сервером (Вы превышаете ресурсы процессорного времени, происходит не обрабатываемая критическая ситуация в одной из библиотек PHP и т.п.). В данном случае советую Вам обратиться к хостеру за предоставлением логов ошибок и нагрузки.

Но если Вы не нашли ничего полезного и в логах, предоставленных хостером — самая последняя инстанция: пошаговая отладка с помощью функций принудительной остановки exit или die в «проблемных местах» и распечаткой нужной информации обнаружить «неустойчивый код», и переписать его с учётом меньшей нагрузки.

Приведём пример выполнения пошаговой отладки с простой установкой флагов состояний:

<?php
function add_user_avatar($user_id,$avatar_name,$watermark){
...
};
echo "вызов функции add_user_avatar";
add_user_avatar($user_id,$avatar_name,$watermark);
echo "окончание работы функции add_user_avatar";
die();
?>

Если же Вы увидели белый лист раньше отображения «окончание работы функции add_user_avatar», значит проблему стоит искать именно в ходе выполнения данной функции add_user_avatar. Если же нет, мы «передвигаем» функцию die или exit ниже, обозначая вызов и завершение далее следующих функций подобным способом.

А в следующих статьях мы рассмотрим с Вами, как написать собственный перехватчик ошибок и расширить ресурсы, выделяемые хостингом для работы наших скриптов.

Авг 282013
 

Практикум PHP. Умная загрузка изображений.

Во время создания сайтов на PHP очень часто приходится работать с изображениями. Информация о работе с изображениями на PHP довольно разрозненная. Существует много громоздких и разноликих примеров.

Но на просторах интернета было найдено очень интересное решение.

Colin VEROT написал замечательный класс, который всего в несколько строк обработает загрузку изображения или выполнит действие над уже существующей фотографией на сервере. Класс основан на gd library и имеет множество настроек. Отлично справляется с php 4 + версии и изображениями png, gif, jpeg форматов.

Вот немногое из того, что класс умеет:

Обработка изображений из массива $_FILES
Работа над изображениями на сервере
Изменение размеров изображений (с сохранением пропорций или с «растягиванием» изображений). При этом доступны такие фильтры, как:
Не изменять/изменять изображения меньше указанной длинны/высоты
Не изменять/изменять изображения больше указанной длинны/высоты
Дополнение изображений указанным цветом до определённых размеров
Преобразования всех входящих изображений к одному типу
Переименование изображений или их перезапись, если указанный путь сохранения уже содержит такой файл
«Обрезка» области изображений по указанным параметрам
Наложение водяного знака watermark а и текста
Даёт управлять именем нового изображения, его расширением, правами, устанавливаемыми на файл
Обладает множеством встроенных эффектов — управляет яркостью и контрастностью, имеет несколько предопределённых эффектов (отражение, замена цветов – инвертирование, оттенки серого, размытие, поворот, рамки, скос и т.п.).
Кроме загрузки изображений, управляется и с загрузкой файлов (может контролировать загружаемые файлы по mime типам).
Скрипт ведёт статистику изменений, которую можно просмотреть в режиме отладки. И множество переменных для контроля.
А теперь давайте рассмотрим примеры обработки изображений из массива $_FILES с помощью данного класса, и почувствуйте, насколько всё просто:

Примечание автора: более подробные примеры и полное описание на английском Вы можете обнаружить прямо вначале файла класса class.upload.php

Для начала скачаем класс и поместим его в web директорию нашего сайта, подключаем его в нужном месте:

require_once(dirname(__FILE__)."/class.upload.php");

Создадим сам объект класса:

$handle = new Upload($file);

Где $file – путь к файлу, может быть так же объектом из массива $_FILES.

После чего укажем, куда сохранять файл:

$savepath = dirname(__FILE__)."/images";

Обработаем и сохраним файл:

$handle->Process($savepath);
$handle->Clean();

Вот и всё! Файл загружен. Осталось только проверить Его наличие по указанному пути $savepath с помощью функции file_exists().

bool file_exists ( string $filename ) — проверяет существование файла, указанного в строковой переменной $filename, если файл существует: возвращает true, иначе – false.

Переменные, которые можно просмотреть до обработки изображения и вызова [имя объекта]->Process([путь сохранения файла]): и до вызова [имя объекта]->clean():

$handle-> file_src_name – имя файла до обработки;
$handle->file_src_name_body – имя файла без расширения до обработки;
$handle->file_src_name_ext – расширение файла до обработки;
$handle->file_src_pathname – полный путь к файлу;
$handle->file_src_size – размер файла;
$handle->file_src_error – ошибка во время обработки/загрузки файла;
$handle->file_is_image – является ли файл изображением?
$handle->image_src_x – ширина изображения?
$handle->image_src_y – высота изображения?
$handle->image_src_pixels – сколько пикселей всего?
$handle->image_src_type – тип изображения.
Переменные, которые можно просмотреть после обработки изображения и вызова [имя объекта]->Process([путь сохранения файла]): и до вызова [имя объекта]->clean():

$handle-> file_dst_path – путь к файлу после обработки;
$handle->dst_name_body – имя файла без расширения после обработки;
$handle->file_dst_name_ext – расширение изображения после обработки;
$handle->file_dst_name – имя файла после обработки;
$handle->file_dst_pathname – полный путь к файлу после обработки;
$handle->image_dst_x – ширина обработанного изображения;
$handle->image_convert – тип преобразованного изображения.
А теперь давайте рассмотрим настройки, которые мы задаём после инициализации объекта класса Upload и до вызова [имя объекта]->Process([путь сохранения файла]):

Загрузить только «изображения»:

$handle->allowed = array("image/*");

Добавим ещё разрешение на загрузку для zip архивов:

$handle->allowed[]   = "application/x-zip";

А что делать, если Файл с таким именем уже существует? Не проблема:

$handle->file_auto_rename = true;

И файл будет переименован.
А как задать имя файла?

$handle->file_new_name_body  =  $image_name;

Преобразовать все загружаемые изображения к типу jpeg?

$handle->image_convert       = "jpg";

А как перезаписать файл, если он уже существует?

$handle->file_overwrite      = true;

Добавить к изображению Watermark слева внизу? Нет проблем!

$handle->image_watermark                 = PATH_SITE."/".$watermark_bigger;
$handle->image_watermark_position            = "LR";

Изменить размеры изображения до 500 на 500 с соблюдением пропорций?

$handle->image_resize        = true;
$handle->image_x             = 500;
$handle->image_y             = 500;
$handle->image_ratio         = true;

«Дополнить» изображения до указанного размера фоновым цветом при изменении размера?

$handle->image_resize            = true;
$handle->image_ratio_fill        = true;
$handle->image_ratio             = true;
$handle->image_background_color  = "#FF00FF";

Запретить изменять размеры изображений меньше 500 пикселей в ширину и длину?

$handle->image_ratio_no_zoom_in  = true;

«Растянуть» изображение без соблюдения пропорций?

$handle->image_ratio             = false;

Сменить права на файл после его обработки?

$handle->dir_chmod = 0777;

Попробуем сменить размер загружаемого файла (в байтах)?

$handle->file_max_size = "1024";

Запретим загрузку только определённых файлов по mime type?

$handle->forbidden = array("application/*");

Изменяем яркость изображения?

$handle->image_brightness = 40;

Отражаем изображение по горизонатли?

$handle->image_flip = "h";

Повернём изображение на 90 градусов?

$handle->image_rotate = 90;

Урежем изображение по заданному образцу на 50 пикселей сверху, 40 срава, 30 снизу и 20 слева?

$handle->image_crop = array(50,40,30,20);

Добавим к изображению белую рамку?

$handle->image_border = "3px";
$handle->image_border_color = "#FFFFFF";

Добавим эффект растворения к чёрному с отражением и 60 процентной прозрачностью отражения?

$handle->image_reflection_height = "25%";
$handle->image_default_color = "#000000";
$handle->image_reflection_opacity = 60;

А в следующей статье мы расширим класс с помощью curl (если поддерживается) или url wrapper ов функции fopen, file_get_contents для загрузки и сохранения изображений, расположенных на других сайтах.

Авг 282013
 

Давайте поговорим о безопасности при обработке данных, связанной с выводом отладочной информации в браузер посетителя.

Очень часто, при обработке клиентских данных весь вывод отладочной информации, как и ошибок, программисты отправляют в браузер посетителя, совершенно не задумываясь при этом о дальнейшем профилировании проекта и его усовершенствовании, а также о том, что своими же руками они предоставляют конфиденциальные технические данные работы проекта любому желающему, да и попросту раздражают этим рядового пользователя. Это при том, что PHP уже давно обладает богатым набором функций и средств для логирования, журналирования и сбора ошибок и непредвиденных ситуаций.

Кроме этого часть ошибок так и остается «за кадром», которые можно выявить и устранить уже в процессе работы, тем самым повышая стабильность сайта и избегая потери новых посетителей, которые, столкнувшись с определенными трудностями и ошибками, попросту уйдут на другой ресурс

Итак, перейдем к сути вопроса.

Допустим, у нас есть страница, отображающая новости раздела, в контексте которой согласно id страницы $_GET[‘ limitfrom’] категории $_GET[‘id’] мы выбираем количество новостей $_GET[‘limit’] из связанной таблицы материалов `news`.

Обычно (утрировано и упращенно) это выглядит, например, так:

<?php
$id         = $_GET['id'];
$limitfrom  = $_GET['limitfrom'];
$limit      = $_GET['limit'];
$sql        = 'SELECT * FROM news WHERE category_id ='
                  . (int)$id.'  LIMIT '. (int)$limitfrom.','.(int)$limit;
$resource   = mysql_query($sql);
If($resource && is_resource(resource)){
    //обработка данных
} else {
   echo mysql_error();
   die();
}
?>

Пример «взят» не из воздуха, вот так, приблизительно, но в более сложной форме выглядит разбитие на постраничную навигацию в разделах известного новостного движка DLE. А также вот так (echo mysql_error(); die()) выводится, зачастую, в браузер посетителя возникновение mysql ошибки в Joomla 1.5 и более новых версий (чуть сложнее, но смысл остается тот же).

Здесь была осознанно допущена одна оплошность. Дело в том, что данный вариант будет работать отлично! И в повседневной работе программист не увидит выполнение echo mysql_error().

Но почему не следует выводить в браузер отладочную информацию и ошибки исполнения PHP?

Итак, первое, из за того, что мы обходимся без проверки, что же приходит со стороны клиента, например, отрицательные значения $id и $limit, которые допускаются для типа данных integer, или несуществующие значения приведут к выводу отладочной информации mysql_error(). Итак, ясным по белому, мы дадим информацию злоумышленнику о существующих таблицах и пищу для размышления, как же проще осуществить mysql injection в другом месте, где используются эти же таблицы при составлении запросов. Или же в некоторых случаях покажем посетителям техническую информацию, которую видеть они не должны. С одной стороны здесь нам помогут более строгие проверки входных данных. Перепишем этот пример с использованием выше сказанного:

 

<?php
$id         = abs((int)$_GET['id']);
$limit      = abs((int)$_GET['limit']);
$limit      = $limit ? $limit : 10;
$limitfrom  = abs((int)$_GET['limitfrom']);
$sql        = 'SELECT * FROM news '
.($id && !empty($id) ? 'WHERE category_id='. $id
: '')
.( $limitfrom && !empty($limitfrom) ?
'  LIMIT '. $limitfrom.', '. $limit.''
: '  LIMIT '. $limit.'');
$resource   = mysql_query($sql);
If($resource && is_resource(resource)){
    //обработка данных
} else {
   echo mysql_error();
   die();
}
?>

Мы предотвратили, как минимум, возникновение нескольких исключительных ситуаций. Мы знаем, что нам нужны только лишь целые положительные значения $id,$limit – поэтому используем явное приведени типов для этих переменных, оператор приведения к целому (int) и функцию получения целого числа abs(). Также мы знаем, что количество выбранных новостей не должно быть равно нулю, поэтому определяем минимальный «порог» в 10 новостей $limit = $limit ? $limit : 10;, формируя запрос, мы проверяем данные, и если они есть и отличны от 0, то добавляем их в mysql запрос по частям.

Все бы хорошо, но и здесь могут быть подводные камни. И все равно существует некоторая вероятность того, что при манипуляции с входными данными злоумышленник увидит вывод той самой информации echo mysql_error();. Зачастую это может получиться даже чисто случайно.

Другой распространенный пример. Многие программисты добавляют вывод технической информации еще на этапе подключения к базе данных. Что, в случае отсутствия соединения с базой данных (сверх нагрузки/временной недоступности сервера баз данных) также выводит отладочную информацию посетителям. При этом там, зачастую, могут в простом тексте встречаться даже логин/название базы/хост подключения к базе данных. А сам программист может никогда и не увидеть подобной информации, и даже не узнать о временном полягании проекта (например из за превышения числа подсоединений к базе данных в случае с увеличением аудитории, как итог: при частом повторении проблемы потеря новых посетителей, раскрытие важных технических данных).

А ведь сбор такой информации бывает полезным для профилирования приложения и дальнейшего его усовершенствования (обеспечения стабильности работы). Для выявления «подводных» камней в стабильной работе сайта.

Итак, давайте обратимся к документации по php. К разделу о создании собственных исключений и перенаправлении отладочной информации и ошибок, логировании и журналировании.

Какие функции и данные для логирования в PHP могут быть полезны?
trigger_error – для генерации исключения.
ini_set , error_reporting – для подавления вывода ошибок
set_error_handler – для установки собственной функции «перехвата» ошибок
register_shutdown_function – для отлова критических ошибок (которая работает, как нужно, начиная с php 5.2)
error_get_last – функция получения последней ошибки, произошедшей в скрипте, очень полезна в случае прерывания скрипта из за возникновения критической ошибки
$_SERVER – суперглобальный массив – источник информации – где и когда и какая ошибка произошла
getenv – функция для получения значения переменных среды окружения сервера, в том случае, если какие-то данные в суперглобальном массиве $_SERVER отсутствуют
Давайте напишем свой перехватичк исключительных ситуаций для дальнейшего логирования данных об ошибках. Суть заключается в следующем, мы переопределим вывод отладочной информации с помощью собственной функции в текстовый файл в определенной папке /errors, чтобы его не смогли прочесть извне, присвоим ему расширение .php (можно и при помощи запрета прямого доступа извне к этой папке, но данный метод более универсален для любых серверов и не требует дополнительной настройки окружения сервера) и добавим первой строчкой , название же зададим ему, совпадающее с текущей датой, чтобы было проще ориентироваться. В итоге один файл ошибок – одна дата.Также мы добавим «перехват» критических ошибок. И при помощи функции trigger_error для логирования непредвиденных ситуаций и генерации собственных ошибок типа E_USER:

Примечание автора: желательно подключить подобный код как можно раньше, еще на этапе инициализации созданного php приложения:

<?php
error_reporting(0);
ini_set('error_reporting','0');
ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
ini_set('ignore_repeated_errors', '1');
define ('ROOT_PATH', dirname(dirname(__FILE__))."/");
//запретить/разрешить вывод ошибок
define('_ERR_HANDLING',true);
//где будем хранить файлы ошибок
define('_ERR_DIR',ROOT_PATH.'errs/');
function error_reporting_log($error_num, $error_var=null,
$error_file=null, $error_line=null) {
$error_desc= '';
$error_desc  = 'Error';
switch ($error_num){
case E_WARNING:
$error_desc = 'E_WARNING';
break;
case E_USER_WARNING:
$error_desc = 'E_USER_WARNING';
break;
case E_NOTICE:
$error_desc = 'E_NOTICE';
break;
case E_USER_NOTICE:
$error_desc = 'E_USER_NOTICE';
break;
case E_USER_ERROR:
$error_desc = 'E_USER_ERROR';
break;
case E_ERROR:
$error_desc = 'E_USER_ERROR';
break;
default:
$error_desc  = 'E_ALL';
break;
}
$date_file = date('y-m-d H:I:S');
$logfile= LOG_FILE;
$url = $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
$date_time = date('d.m.y - H:i:s');
$ip= isset($_SERVER['REMOTE_ADDR']) &&
!empty($_SERVER['REMOTE_ADDR']) ?
$_SERVER['REMOTE_ADDR']
: getenv('REMOTE_ADDR ') ;
$from= isset($_SERVER['HTTP_REFERRER'])&&
!empty($_SERVER['HTTP_REFERRER'])?
$_SERVER['HTTP_REFERRER']
:getenv('HTTP_REFERRER');
$errortext = $error_desc.': '.$error_var."\t".'
Line: '.$error_line."\t".'
File: '.$error_file."\t".'
Link: '.$url."\t".'
Date: '.$date_time."\t".'IP: '.$ip."\t".'
FROM:'.$from."\n";
unset($from,$error_desc,
$error_var,$error_line,
$error_file,$url,$date_time,$error_write);
$secuire= '<?php die("Forbidden."); ?>';
if(is_file($logfile)&&is_writeable($logfile)){
$fp = fopen($logfile,'r');
if($fp && is_resource($fp)){
$strings= fgets($fp);
if(isset($strings)&&!empty($strings)
&&strpos($strings,$secuire)===false){
unlink($logfile);
}
fclose($fp);
};
unset($fp);
}
if(!is_file($logfile)){
$dir= dirname($logfile);
if(is_dir($dir)&&is_writable($dir)){
$fp     = fopen($logfile,'w+');
if(is_resource($fp)){
flock($fp,LOCK_EX);
fwrite($fp,$secuire."\n");
flock($fp,LOCK_UN);
fclose($fp);
$fp= null;
}
unset($dir,$fp);
}
}
unset($secuire);
if(is_file($logfile)&&!is_writable($logfile)){
chmod($logfile,0775);
}
if(is_file($logfile)&&is_writeable($logfile)){
$fp     = fopen($logfile,'a+');
if(is_resource($fp)){
flock($fp,LOCK_EX);
fwrite($fp,$errortext);
flock($fp,LOCK_UN);
fclose($fp);
$fp= null;
unset($fp);
}
}
unset($logfile);
                return true;
}
function err_handler(){
if(_ERR_HANDLING){
$error_reporting= '';
$error_reporting= ini_get('error_reporting');
$error_reporting= $error_reporting?$error_reporting:E_ALL;
error_reporting(E_ERROR);
$date_file = date('dmY').'.php';
$dir= _ERR_DIR;
$path= $dir.$date_file;
$logfile= '';
if(!is_dir($dir) || !is_writable($dir)){
if(is_dir($dir)&&!is_writable($dir)){
chmod($dir,0775);
} else if(!is_dir($dir)){
$isdir= false;
$isdir= mkdir($dir,0775);
}
if(!$isdir&&!is_writable($dir)){
$dir= ROOT_PATH;
$path= $date_file;
}
}
if(is_dir($dir) && is_writable($dir)){
if(!is_file($path)){
$fp= fopen($path,'w+');
if($fp && is_resource($fp)){
$secuire= '<?php die("Forbidden."); ?>';
flock($fp,LOCK_EX);
                fwrite($fp,$secuire."\n");
                flock($fp,LOCK_UN);
                fclose($fp);
                $fp= null;
unset($secuire);
}
}
if(is_file($path) && !is_writable($path)){
chmod($path,0775);
}
if(is_file($path) && is_writable($path)){
ini_set('display_errors',0);
set_error_handler('error_reporting_log', (E_ALL & ~E_NOTICE));
$logfile= $path;
define('LOG_FILE',$logfile);
}
unset($date_file,$dir,$path,$logfile);
}
error_reporting($error_reporting);
unset($error_reporting);
}
}
function critical_error(){
$error = error_get_last();
if(is_array($error) && sizeof($error) && isset($error['type'])
&& $error['type'] == E_ERROR && !empty($error['message'])
&& !empty($error['file']) && !empty($error['line'])){
error_reporting_log($error['type'],
$error['message'],$error['file'],$error['line']);
}
}
if(function_exists('register_shutdown_function')
&& function_exists('error_get_last')
&& _ERR_HANDLING){
register_shutdown_function('critical_error');
}
err_handler();

Что здесь происходит? При помощи функции ini_set и error_reporting мы подавляем вывод ошибок в браузер посетителя, константами определяем местоположение нашей директории для хранения логов ошибок define(‘_ERR_DIR’,ROOT_PATH.’errs/’);. Определенная в самом начале константа (bool) _ERR_HANDLING – при значении true разрешит перенаправление всех наших ошибок в файлы, при значении false дальнейшие инструкции выполняться не будут. Для перехвата самих ошибок используется функция error_reporting_log, которая принимает тип ошибки, сообщение об ошибке, имя файла, где произошла ошибка, и строку, в которой она произошла. В ней мы собираем кроме этой информации, и другие полезные данные. А именно:

$_SERVER[‘HTTP_REFERRER’] — откуда пришел посетитель, с какой страницы?
$_SERVER[‘REMOTE_ADDR’] — ip посетителя,
$_SERVER[‘HTTP_HOST’]. $_SERVER[ ‘REQUEST_URI’] – полный адрес страницы вместе со строкой запроса, на которой возникла ошибка
date(‘y-m-d H:I:S’) — текущее время возникновения ошибки.
Зачастую, подобной информации хватит с головой и для логирования и исправления ошибок, которые были не замечены на этапе разработки, так и для отлова злоумышленников. Но есть один тип ошибок, который не будет перехвачен. Это критические ошибки PHP E_ERROR, которые приводят к прерыванию php скрипта . Начиная с версии PHP 5.2 для перехвата критических ошибок можно определить собственную функцию при мопощи register_shutdown_function. Мы создаем собственную функцию для этих целей critical_error, в которой при помощи функции error_get_last получаем информацию о текущей ошибке. Так как данная функция будет всегда выполняться после завершения php скрипта, мы логируем информацию только об ошибках с типом E_ERROR.

Можно было бы использовать функцию error_log с указанием пути к источнику логирования, но было отдано предпочтение простым файловым функциям, при помощи которых можно установить функцией flock приоритетное эксклюзивное запирание во избежание попыток одновременной записи в логфайл несколькими скриптами (и возникновения «битых» файлов).

Давайте на примере все того же скрипта с mysql рассмотрим принцип логирования mysql ошибок (впрочем, по такому же принципу можно построить логирование любых внештатных ситуаций):

<?php
$id         = abs((int)$_GET['id']);
$limit      = abs((int)$_GET['limit']);
$limit      = $limit ? $limit : 10;
$limitfrom  = abs((int)$_GET['limitfrom ']);
$sql        = 'SELECT * FROM news '
.($id && !empty($id) ?
'WHERE category_id='. $id
: '')
.( $limitfrom && !empty($limitfrom)
? '  LIMIT '. $limitfrom.', '. $limit.''
: '  LIMIT '. $limit.'');
$resource   = mysql_query($sql);
If($resource && is_resource(resource)){
    //обработка данных
} else {
  $string                 = print_f(
'Ошибка mysql при получении новостей из категории с ID: %d,'
  . 'на странице:  %d , '
  . 'информация о запросе:  %s,'
  . ' информация об ошибке: %s',
  $id,$limitfrom, $sql, mysql_error());
   trigger_error($string,E_USER_ERROR);
   die();
}
?>

Вот и все, в файле ошибок при возникновении ошибки mysql будет добавлена новая строка с подробной информацией. Таким способом вы сможете логировать практически любую полезную для вас информацию, скрыв ее от Ваших посетителей. А сбор и анализ подобной информации поможет в дальнейшем в повышении стабильности и совершенствовании Вашего проекта.

Примечание автора: также для генерации и перехвата ошибок Вы можете использовать конструкции try{}catch(Exeption $e){ thrown(throw new Exception("$name contains the word name");}

Примечание автора: правилом хорошего тона при перехвате и обработке ошибок (особенно критических, которые ведут к невозможности предоставить посетителю необходимую информацию, за которой он, собственно, и пришел к Вам на сайт) является создание для каждого вида подобных ошибок статической страницы с объяснением на человекопонятном языке без технических данных сути проблемы (также на такой странице можно оставить контакты админитрации) и перенаправление посетителя на нужную страницу в ходе возникновения ошибки.