5 заметок с тегом

программирование

Файл .htaccess и RewriteRule

В последнее время мне стало интересно изучать низкоуровневые подходы минуя тот функционал, который предлагают нам фреймворки. Таким образом, я открываю для себя новые знания в программировании. Когда-то я проходил все стадии написания «велосипедов», но чтобы быть нормальным проггером, нужно знать как там всё устроено, это нормально. Сегодня рассмотрим перенаправления с помощью файла .htaccess для веб-сервера Apache на небольшом примере.

Чтобы не томиться ожиданием, давайте сразу перейдём к задаче: нужно сделать перенаправление со страницы /pages/about.php на /about, т. е. убрать в адресе папку /pages и расширение .php в названии файла. Ещё нужно позаботиться о том, чтобы в конце URL не было закрывающего слеша. Вот такую файловую структуру содержит наш пример:

┌public_html/
├──pages/
│  ├──about.php
│  ├──contact.php
│  └──subscribe.php
├──index.php
└──.htaccess

В Apache для преобразования URL’ов есть специальный модуль mod_rewrite. Он отвечает за создание ЧПУ — создаёт красивые URL для PHP-скриптов на уровне веб-сервера. Его мы и задействуем для решения нашей задачи. Для начала создадим конфигурационный файл .htaccess в корне сайта со следующим содержимым, а затем я объясню как он отработает:

# Включаем mod_rewrite
RewriteEngine On

# Убираем последний слеш
RewriteRule ^(.*)/$ /$1 [L,R=301]

# Преобразуем /pages/about.php в /about
RewriteRule ^(\w+)$ /pages/$1.php [L,NC]

Командой RewriteEngine On мы включаем модуль mod_rewrite, а с помощью RewriteRule задаём правила преобразования адресов. RewriteRule просто преобразует входной URL по порядку в соответствии с регулярными выражениями. В конце каждого правила можно задать флаги для указания поведения mod_rewrite при обработке URL. Синтаксис прост как три копейки:

RewriteRule новый реальный [флаг1,флаг2,флаг3]

Давайте разберём первое правило из примера: ^(.*)/$ /$1 [L,R=301] — между маркерами ^ и $ мы указали начало строки с любого символа и любой длины (.*), где конец строки должен заканчиваться на слеш /. Всё, что заключено в круглые скобки (.*) образует группу символов, которую можно использовать через макрос $1.

Теперь все URL’ы, которые заканчиваются на слеш, будут переброшены на адрес без последнего слеша, где /$1 — все входные символы без последнего слеша. Флаг [L] останавливает чтение .htaccess, а [R=301] генерирует HTTP-код ответа сервера HTTP/1.1 301 Moved Permanently. После того, как правило отработало и Apache обрезал закрывающий слеш, файл .htaccess выполнится ещё раз и первое правило больше не отработает.

Второе правило: ^(\w+)$ /pages/$1.php [L,NC] — пользователь запросит адрес страницы, который имеет буквенное или цифровое содержимое от одного и больше символов (\w+). Это образует группу символов, которые мы подставим в адрес скрипта /pages/$1.php. По факту выполниться скрипт под другим URL. Флаг [L] останавливает .htaccess, а [NC] отменяет проверку регистра символов.

Все правила выполняются по порядку. Также стоит обратить внимание на значение флагов, которые могут менять поведение RewriteRule и веб-сервера. Ниже я собрал коллекцию флагов применительно для перенаправлений:

Флаг  Описание
———————————————————————————————————————————————————————————————————————————————
[C]   Chain — объединяет несколько правил в цепочку. Если первое правило
      цепочки не срабатывает, то вся цепочка игнорируется.

[F]   Forbidden — возвращает ошибку 403 Forbidden (запрещено).

[G]   Gone — возвращает ошибку 410 Gone (удалён).

[L]   Last — останавливает процесс преобразования, и текущая ссылка
      считается окончательной.

[N]   Next — запускает процесс преобразования с первого по порядку правила.

[NS]  NoSubreq — разрешает срабатывание правила только для настоящих
      запросов, игнорируя подзапросы.

[NC]  NoCase — отключает проверку регистра символов.

[P]   Proxy — даёт команду Apache выполнить подзапрос к указанной странице
      с использованием программного модуля mod_proxy, при этом пользователь
      ничего не узнает об этом подзапросе. Если модуль mod_proxy отсутствует,
      то произойдет ошибка.

[PT]  PassThrough — останавливает процесс преобразования и передает
      полученную новую ссылку дальше по цепочке.
      
[QSA] Qsappend — добавляет исходные параметры запроса (Query String)
      к замене. Если замена не включает в себя новые параметры запроса,
      то исходные параметры запроса добавляются автоматически. Если же
      включает, то без флага QSA исходные параметры запроса будут утеряны.

[R]   Redirect — останавливает процесс преобразования и возвращает
      результат браузеру клиента как редирект на новую страницу.
      По умолчанию передаётся HTTP-код 302 Moved Temporarily (перемещенно
      временно), но его можно изменить путём присвоения нового статуса
      через знак равенства [R=301]. В этом случае будет передан HTTP-код
      301 Moved Permanently (перемещено навсегда).

[S]   Skip — пропускает следующее правило, если текущее правило сработало.
      Можно указать количество последующих игнорируемых правил [S=2].

Полезные ссылки:

    Остальные флаги в документации Apache
    Как на самом деле работает mod_rewrite
    Описание основных флагов mod_rewrite
    Волшебный файл .htaccess
    Коллекция сниппетов .htaccess

2017   apache   htaccess   mod_rewrite   php   RewriteRule   программирование

UploadedUrl — пишем класс для загрузки удалённых файлов на PHP

Задача. Написать класс для загрузки удалённых файлов по URL. Использовать для решения задачи пакет Symfony HttpFoundation. Класс должен иметь настройки и корректно обрабатывать ошибки. Объяснить устройство класса и показать примеры использования.

В Symfony HttpFoundation существует класс UploadedFile, который загружает файлы из $_FILES. Он умеет сохранять файл на диске и безопасно определять MIME-тип. Нам нужно создать класс с такой же функциональностью, но для загрузки удалённых файлов по URL. Свой класс мы назовём UploadedUrl, как бы расширяя Symfony HttpFoundation, и наследуем класс File, чтобы повторить все возможности UploadedFile. Структура нашего проекта будет выглядеть следующим образом:

┌src/
├──Exception/
│  └──UrlException.php
├──UploadedUrl.php
├──tmpfile.php
└──cacert.pem

Файл UploadedUrl.php будет содержать всю логику нашего класса, а остальные будут решать вспомогательные задачи: UrlException.php — класс для обработки исключений, tmpfile.php — класс для работы с временным файлом, cacert.pem — сертификат для безопасного соединения. Для тех, кто хочет сразу посмотреть результат, может скачать код с репозитория denisyukphp/uploaded-url или установить через Composer.

Для загрузки удалённого файла мы задействуем cURL, который скачает весь контент во временный файл, а затем передадим его URI в конструктор класса File. После завершения работы временный файл будет автоматически удалён. Подключить tmpfile.php можно через Composer с моего репозитория denisyukphp/tmpfile. Почему он лучше функции tmpfile() я рассказал на Хабре.

Перед началом написания класса нам необходимо определиться с интерфейсом инициализации. Предлагаю UploadedUrl принимать два аргумента: 1) http:// или https:// ссылку; 2) необязательный массив с настройками. В итоге вот так должен выглядеть запуск класса:

// обычная загрузка удалённого файла
new UploadedUrl('');

// загрузка файла с настройками по умолчанию
new UploadedUrl('', [
    'verify'     => true,
    'cert'       => 'cacert.pem',
    'maxsize'    => null,
    'timeout'    => null,
    'useragent'  => null,
    'buffersize' => 1024,
    'redirects'  => true,
    'maxredirs'  => 10,
    'curl'       => [],
]);

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

Посмотреть 258 строк кода UploadedUrl с комментариями
///////////////////////////////////
// 5 // Как пользоваться классом //
///////////////////////////////////

Для быстрого старта достаточно объявить класс new UploadedUrl('') с валидной ссылкой в первом аргументе. После чего с удалённым файлом можно работать на своём сервере. Файл будет находится во временной папке до конца работы PHP и автоматически удалится, если его не переместить в другое место. Ниже привожу примеры использования UploadedUrl с настройками и моими комментариями:

<?php

$file = new Denisyuk\UploadedUrl('http://i.imgur.com/JRorA8V.gif', [

    // Безопасное соединение. Если установить false, то
    // cURL отключит значения CURLOPT_SSL_VERIFYPEER и
    // CURLOPT_SSL_VERIFYHOST.
    'verify' => true,

    // Можно указать директорию с PEM-сертификатами или отдельный файл.
    // По умолчанию используется cacert.pem, свежию версию которого
    // можно скачать на https://curl.haxx.se/docs/caextract.html
    // или самостоятельно настроить автообновление свежего
    // сертификата по ссылке https://curl.haxx.se/ca/cacert.pem
    'cert' => __DIR__,

    // Зададим максимальный размер загружаемого файла
    // в байтах (по умолчанию без ограничений).
    'maxsize' => 1024 * 1024 * 32,

    // Максимальное время выполнения запроса.
    'timeout' => 60,

    // Информация о пользователе (если null, то будет
    // взято из $_SERVER['HTTP_USER_AGENT']).
    'useragent' => null,

    // Это количество байт, через которое cURL будет
    // сверять размер загружаемого файла. В нашем случае,
    // через каждый килобайт будет выполняться ф-я для
    // проверки размера скачанных данных. Для больших
    // размеров установите значение на 2% меньше
    // от разрешённого (для 1Гб установите 1024 * 1024 * 2).
    'buffersize' => 1024,

    // Разрешить следовать перенаправлениям (по умолчанию 10).
    // cURL будет переходит по ссылкам, где HTTP-ответ
    // имеет заголовок с Location.
    'redirects' => true,

    // Количество перенаправлений. Лучше установить
    // больше двух, т. к. возможны перенаправления
    // с HTTP на HTTPS.
    'maxredirs' => 5,

    // другие CURLOPT_* опции
    'curl' => [
        CURLOPT_AUTOREFERER    => true,
        CURLOPT_CONNECTTIMEOUT => 10,
        /* ... */
    ];
]);

Поскольку UploadedUrl будет наследовать класс File, то он также вернёт набор методов класса SplFileInfo. Это расширяет возможности нашего класса. В общем мы имеем мощный инструмент для работы с файлом. Давайте посмотрим какая функциональность нам доступна:

// получить MIME-тип, который можно
// безопасно использовать для валидации
// определённых файлов
$file->getMimeType();

// получить расширение файла
$file->guessExtension();

// переместить файл в другую папку
$file->move(__DIR__ . '/data', 'foobar.jpg');

// получить размер файла
$file->getSize();

// Остальные доступные методы для SplFileInfo можно
// посмотреть на http://php.net/manual/ru/class.splfileinfo.php
/////////////////////////////////////////
// 6 // Выявляем и обрабатываем ошибки //
/////////////////////////////////////////

UploadedUrl сам не бросает исключение в случае ошибок при загрузке файла. Для отслеживания этого процесса можно использовать три метода: isValid() — проверит были ли ошибки в cURL; getErrorMessage() — вернёт сообщение об ошибке, которое можно показать пользователю; getErrorCode() — вернёт код ошибки cURL (можно использовать для написания сообщений об ошибках на своём языке). Как это всё работает показано ниже:

<?php

use Denisyuk\UploadedUrl;
use Denisyuk\Exception\UrlException;

try {
    // (1) ссылка на загружаемый файл
    $url = 'http://windows.php.net/downloads/releases/php-7.1.1-src.zip';

    // (2) загрузим свежую версию PHP, которая весит ~26 Мбайт
    $file = new UploadedUrl($url, [

        // (3) укажем максимальный размер файла 5 Мбайт
        'maxsize' => 1024 * 1024 * 5,
    ]);

    // (4) проверяем на ошибки
    if (!$file->isValid()) {

        // (5) бросаем исключение
        throw new UrlException(
            $file->getErrorMessage(),
            $file->getErrorCode()
        );

        // Здесь мы можем сформировать свои названия
        // ошибок исходя из getErrorCode(), т. к. это
        // код ошибки cURL. Например:
        //
        // $code = $file->getErrorCode();
        // $errors = [
        //     /*  1 */ CURLE_UNSUPPORTED_PROTOCOL => '',
        //     /*  6 */ CURLE_COULDNT_RESOLVE_HOST => '',
        //     /* 22 */ CURLE_HTTP_RETURNED_ERROR  => '',
        //     /* 42 */ CURLE_ABORTED_BY_CALLBACK  => '',
        // ];
        // $message = isset($errors[$code]) ? $errors[$code] : '';
        //
        // Также мы можем использовать свойства класса
        // $file->url и $file->maxsize для написания
        // более красивых названий своим ошибкам.
        //
        // throw new UrlException($message, $code);
    }

    // (6) работаем дальше, если нет ошибок
    echo $file->getMimeType();

// (7) ловим исключение
} catch(UrlException $e) {

    // (8) выводим сообщение об ошибке
    // выведет: 'Файл "php-7.1.1-src.zip" не должен превышать 5 Мбайт.'
    echo $e->getMessage();
}

Я дальше планирую поддерживать и развивать этот класс на denisyukphp/uploaded-url как дополнение к Symfony HttpFoundation. Если у вас есть какие-либо замечания или предложния, то можете отправить их мне на почту a@denisyuk.by или Вконтакте. Буду очень благодарен за любую обратную связь, которая поможет улучшить UploadedUrl. В ближайшее время намереваюсь нормально оформить проект на Гитхабе и добавить несколько косметических улучшений.

2017   php   Symfony   URL   загрузка   кейсы   программирование   файл

Путеводитель по нововведениям PHP 7.0 и 7.1

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

Это сравнение скорости между версиями 5.6 и 7.0 на различных платформах. Zend Framework даёт прирост скорости на 133%, а WordPress — 129%. Такое улучшение PHP имеет позитивные последствия для бизнеса.

В прошлом году Badoo, WordPress.com, Tumblr и многие другие гиганты перешли на PHP7. Так, например, компании Badoo за счёт снижения CPU в два раза и уменьшения потребления памяти в 8 раз удалось сэкономить 1 млн долларов. Сайт WordPress.com также полностью переведён на PHP7, где прирост производительности действительно заметен:

Быстродействие в новых версиях стало самым крутым и обсуждаемым нововведением. Теперь не нужно заморачиваться по оптимизации каких-то участков кода, а сразу писать понятный и поддерживаемый код. Во вторых, в PHP 7.0 и 7.1 введено большое количество улучшений на уровне синтаксиса и функциональности. Далее на примерах я покажу, что мне понравилось в новых версиях и как эти нововведения поменяют язык PHP в будущем на уровне кода.

Замена тернарного оператора

На замену тернарного оператора пришёл «оператор объединения с null» или «null-коалесцентный оператор». Через два знака вопроса (??) можно указать цепочку значений в одну строку и будет выбрано первое значение, которое не равно null. Раньше этот подход применялся вместе с функцией isset(). Сейчас можно указать даже не существующие переменные и не мучатся c большой вложенностью как с тернарным оператором.

<?php

// PHP 5.6
$test = isset($foo) ? $foo : null;

// PHP 7+
$test = $foo ?? null;

/* ... */

// PHP 5.6
$test = isset($foo) ? $foo : (isset($bar) ? $bar : null);

// PHP 7+
$test = $foo ?? $bar ?? null;

/* ... */

// PHP 7+
someFunction($unknownVariable ?? 'default');

Оператор объединения с null
Тернарный оператор и оператор null-coalescing

Групповое объявление классов, констант и функций

Начиная с версии 7.0 появилась возможность группировать объявления импорта классов, функций и констант, находящиеся в одном пространстве имён, в одной строке с помощью оператора use. Это нововведение на уровне синтаксического сахара, которое может наделить объявление имён каких-то компонентов определённой логикой.

<?php

// PHP 5.6
use App\Bundle\Foo;
use App\Bundle\Bar;
use App\Bundle\Baz as B;

// PHP 7+
use App\Bundle\{Foo, Bar, Baz as B};

// или

use App\Bundle\{
    Foo,
    Bar,
    Baz as B
};

/* ... */

// PHP 5.6
use const app\namespace\ru_RU;
use const app\namespace\en_US;
use function app\namespace\firstFunc;
use function app\namespace\secondFunc;

// PHP 7+
use const app\namespace\{ru_RU, en_US};
use function app\namespace\{firstFunc, secondFunc};

Групповые декларации use

Декларация типов и возвращаемых значений

Это самое мощное нововведение для ООП. Теперь при объявлении метода для каждой переменной можно указать свой тип, а также тип данных, который вернёт этот метод. В PHP 7.0 доступны следующие типы: array, callable, bool, float, int, string, имя класса или интерфейса. В версии 7.1 добавили ещё void и iterable. Тип void можно использовать только в возвращаемых значениях функций, которые ничего не возвращают. Псевдо-тип iterable используется в качестве значения массива или объекта, реализующего интерфейс Traversable.

<?php

class Foo
{
    public static function bar(int $a, float $b, string $c) : int
    {
        return $a + $b + $c;
    }
}

// вернёт int(6)
var_dump( Foo::bar('1', 2, 3.0) );

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

Строгую типизацию можно включить, если в начале скрипта прописать declare(strict_types=1). Все значения должны полностью соответствовать указанным типам. В противном случае будет сгенерирована ошибка TypeError. Строгая типизация не затрагивает весь остальной код и её нужно прописывать для каждого скрипта отдельно.

<?php

// включили строгую типизацию
declare(strict_types=1);

class Foo
{
    public static function bar(int $a, int $b) : int
    {
        return $a + $b;
    }
}

// вернёт ошибку TypeError, т. к.
// первый аргумент является string,
// а должен int
var_dump( Foo::bar('1', 2) );

// вернёт int(3)
var_dump( Foo::bar(1, 2) );

В версии 7.1 появилась возможность обнулить возвращаемые типы. Это расширяет список возвращаеммых типов до null. Теперь функция может вернуть какой-то явно указанный тип или null. Достигается такое поведение путём добавления префикса в виде знака вопроса к указанному типу:

<?php

declare(strict_types=1);

class Foo
{
    public static function bar(?string $name) : ?string
    {
        return $name;
    }
}

// string(13) "Hello, world!"
var_dump( Foo::bar('Hello, world!') );

// NULL
var_dump( Foo::bar(null) );

// string(0) ""
var_dump( Foo::bar('' ?? null) );

// NULL
var_dump( Foo::bar($unknownVariable ?? null) );

// вернёт ошибку TypeError
var_dump( Foo::bar(1) );

// вернёт ошибку ArgumentCountError
var_dump( Foo::bar() );

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

Декларация скалярных типов
Декларация возвращаемых значений
Обнуляемые типы
Ничего не возвращающие функции
Псевдо-тип iterable

Обработка ошибок и исключений

В PHP 7.0 появился новый класс для внутренних ошибок Error и интерфейс исключений Throwable. Теперь Error и старый класс Excetion реализуют Throwable (пользовательские классы не могут реализовывать данный интерфейс). Exception можно использовать для отлова исключений, которые будут обработаны и выполнение программы продолжится. Класс Error служит для необратимых исключений и выбрасывается в случае ошибки PHP или на уровне ошибок разработчиков.

Большинство ошибок уровня E_ERROR или E_RECOVERABLE_ERROR будут выбрасывать Error, но некоторые будут выбрасывать объекты подклассов: ArithmeticError, AssertionError, ParseError, DivisionByZeroError, TypeError и ArgumentCountError (с версии 7.1). Эти подклассы ошибок не могут быть брошены самостоятельно, а только лишь словлены. Иерархию всех исключений можно представить в виде дерева:

┌Throwable
├──Error
│  ├──ArithmeticError
│  ├──AssertionError
│  ├──DivisionByZeroError
│  ├──ParseError
│  ├──TypeError
│  └──ArgumentCountError
└──Exception
   ├──ErrorException
   ├──LogicException
   │  ├──BadFunctionCallException
   │  │  └──BadMethodCallException
   │  ├──DomainException
   │  ├──InvalidArgumentException
   │  ├──LengthException
   │  └──OutOfRangeException
   └──RuntimeException
      ├──OutOfBoundsException
      ├──OverflowException
      ├──RangeException
      ├──UnderflowException
      └──UnexpectedValueException

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

<?php

try {
    /*
     * код программы
     */
} catch (
    ParseError         |
    TypeError          |
    ArgumentCountError $e
) {
    /*
     * ловим три вида ошибок PHP в одном блоке
     */
} catch (AppException | UserException $e) {
    /*
     * ловим пользовательские исключения
     */
} catch (Exception $e) {
    /*
     * ловим остальные исключения
     */
} catch (Throwable $t) {
    /*
     * ловим и логируем все ошибки и исключения
     */
}

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

Предопределённые исключения
Изменения в обработке ошибок и исключений
Обработка нескольких исключений в одном блоке catch

Остальные нововведения одним списком

Нововведения в PHP7+ это лишь начало большого перехода на новый уровень языка. С каждым релизом приложения будут становиться быстрее и безопаснее. На версии 7.0 и 7.1 нужно отреагировать уже сегодня и будь в курсе новых тенденций PHP, дабы не выпадать из сферы. Ниже привожу одним списком остальные нововведения:

7.0 Оператор spaceship (космический корабль)
7.0 Задание констант массивов с помощью define()
7.0 Появились анонимные классы
7.0 Синтаксис кодирования Unicode
7.0 Добавлен метод в замыкания Closure::call()
7.0 unserialize() с фильтрацией
7.0 Новый класс IntlChar
7.0 Улучшена старая функция assert()
7.0 Выражение return в генераторах
7.0 Делегация генератора с помощью конструкции yield from
7.0 Новая функция intdiv() для целочисленного деления
7.0 session_start() принимает массив опций
7.0 Новая функция preg_replace_callback_array()
7.0 Новые функции random_bytes() и random_int()
7.0 list() может раскрывать объекты реализующие ArrayAccess
7.0 Обращение к методам и свойствам класса при клонировании (clone $foo)->bar()
7.1 Короткий синтаксис для list()
7.1 Публичные и приватные константы классов
7.1 Поддержка ключей в list()
7.1 Поддержка отрицательных смещений для строк
7.1 Расширены функции openssl_encrypt() и openssl_decrypt()
7.1 Преобразование callable в Closure с помощью Closure::fromCallable()
7.1 Новая функция pcntl_async_signals()
7.1 Поддержка Server Push в CURL 7.46

Дополнительное чтиво

Миграция из PHP 5.6.x к PHP 7.0.x
Миграция из PHP 7.0.x к PHP 7.1.x
Сравнение скорости для различных фреймворков в PHP7
Инфографика: Turbocharging the Web with PHP7
Введение в PHP 7: что добавлено, что убрано
PHP 7.1: обзор новых возможностей
Return types и Scalar type в PHP7
Throwable исключения и ошибки в PHP7

2017   php   обзор   программирование

Полное руководство по загрузке изображений на PHP

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

§1. Общие принципы
§2. Правила безопасности
§3. Конфигурация php.ini
§4. Загрузка картинки из формы
§5. Загрузка изображения по ссылке
§6. Настройка выбора нескольких файлов

§1. Общие принципы

Всю последовательность загрузки изображения на сервер можно отобразить следующим образом: настройка php.iniполучение файлапроверка безопасностивалидация данныхсохранение на диск. Процесс загрузки картинки с компьютера пользователя или по URL ничем не отличаются, за исключением способа получения изображения и его сохранения. Общая схема загрузки картинки на сервер выглядит следующим образом:

Для валидации картинки по URL мы будем использовать функцию getimagesizefromstring(), т. к. cURL скачает её в переменную для дальнейших манипуляций.

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

§2. Правила безопасности

Безопасность загрузки изображений сводится к недопущению попадания на сервер чужеродного кода и его выполнения. На практике загрузка картинок наиболее уязвимое место в PHP-приложениях: попадание shell-скриптов, запись вредоносного кода в бинарные файлы, подмена EXIF-данных. Для того, чтобы избежать большинства методов взлома нужно придерживаться следующих правил:

а не доверять данным из $_FILES;
б не проверять MIME-тип картинки из функции getimagesize();
в загружаемому файлу генерировать новое имя и расширение;
г запретить выполнение PHP-скриптов в папке с картинками;
д не вставлять пользовательские данные через require и include;
е для $_FILES использовать is_uploaded_file() и move_uploaded_file().
Если есть чем дополнить «Правила безопасности», тогда оставляйте свои замечания или ссылки на статьи по безопасности в комментариях к этому руководству, а я опубликую их в этом параграфе.

Безопасная загрузка изображений на сервер
PHP check if file is an image
How to Securely Allow Users to Upload Files
Безопасная загрузка файлов на сервер
Надёжная защита веб-сайта от большинства видов атак
Ваш сайт тоже позволяет заливать всё подряд?
PHP image upload security check list
Unrestricted File Upload
Опасный getimagesize() или Zip Bomb для PHP
Почему надо пользоваться стандартными функциями при загрузке файлов
Руководство по написанию защищённых PHP-приложений в 2018-м

§3. Конфигурация php.ini

PHP позволяет внести определённые конфигурационные значения в процесс загрузки любых файлов. Для этого необходимо в файле php.ini найти блоки «Resource Limits», «Data Handling» и «File Uploads», а затем отредактировать, по необходимости, следующие значения:

[Resource Limits]
; Максимальное время выполнения скрипта в секундах
max_execution_time = 60

; Максимальное потребление памяти одним скриптом
memory_limit = 64M

[Data Handling]
; Максимально допустимый размер данных отправляемых методом POST
post_max_size = 5M

[File Uploads]
; Разрешение на загрузку файлов
file_uploads = On

; Папка для хранения файлов во время загрузки
upload_tmp_dir = home/user/temp

; Максимальный размер загружаемого файла
upload_max_filesize = 5M

; Максимально разрешённое количество одновременно загружаемых файлов
max_file_uploads = 10

Исходя из указанных значений, пользователь не сможет за один раз загрузить больше десяти файлов, причём каждый файл не должен превышать 5 Мбайт. Параметры из блока «Resource Limits» больше нужны для загрузки удалённого файла, т. к. с помощью cURL мы будем скачивать содержимое в переменную и проверять её по нужным нам критериям, а для этого необходимо дополнительное время и память.

Конфигурационный файл php.ini всегда необходимо настраивать согласно бизнес-логики разрабатываемого веб-приложения. Например, мы планируем загружать не более десяти файлов до 5 Мбайт, а это значит нам понадобиться ~50 Мбайт памяти. Кроме того, нам нужно знать максимальное время загрузки одного файла с локальной машины и по ссылке, дабы установить достаточное время выполнения скрипта в max_execution_time и не пугать пользователей ошибками.

§4. Загрузка картинок из формы

Сейчас мы не будем рассматривать загрузку нескольких файлов на сервер, а разберём лишь саму механику загрузки на примере одного файла. Итак, для загрузки картинки с компьютера пользователя необходимо с помощью HTML-формы отправить файл PHP-скрипту методом POST и указать способ кодирования данных enctype="multipart/form-data" (в данном случае данные не кодируются и это значение применяется только для отправки бинарных файлов). С формой ниже мы будем работать дальше:

<form action="file-handler.php" method="post" enctype="multipart/form-data">
  <input type="file" name="upload">
  <button>Загрузить</button>
</form>

Для поля выбора файла мы используем имя name="upload" в нашей HTML-форме, хотя оно может быть любым. После отправки файла PHP-скрипту file-handler.php его можно перехватить с помощью суперглобальной переменной $_FILES['upload'] с таким же именем, которая в массиве содержит информацию о файле:

Array
(
    [name]     => picture.jpg                // оригинальное имя файла
    [type]     => image/jpeg                 // MIME-тип файла
    [tmp_name] => home\user\temp\phpD07E.tmp // бинарный файл
    [error]    => 0                          // код ошибки
    [size]     => 17170                      // размер файла в байтах
)

Не всем данным из $_FILES можно доверять: MIME-тип и размер файла можно подделать, т. к. они формируются из HTTP-ответа, а расширению в имени файла не стоит доверять в силу того, что за ним может скрываться совершенно другой файл. Тем не менее, дальше нам нужно проверить корректно ли загрузился наш файл и загрузился ли он вообще. Для этого необходимо проверить ошибки в $_FILES['upload']['error'] и удостовериться, что файл загружен методом POST с помощью функции is_uploaded_file(). Если что-то идёт не по плану, значит выводим ошибку на экран.

<?php

// Перезапишем переменные для удобства
$filePath  = $_FILES['upload']['tmp_name'];
$errorCode = $_FILES['upload']['error'];

// Проверим на ошибки
if ($errorCode !== UPLOAD_ERR_OK || !is_uploaded_file($filePath)) {

    // Массив с названиями ошибок
    $errorMessages = [
        UPLOAD_ERR_INI_SIZE   => 'Размер файла превысил значение upload_max_filesize в конфигурации PHP.',
        UPLOAD_ERR_FORM_SIZE  => 'Размер загружаемого файла превысил значение MAX_FILE_SIZE в HTML-форме.',
        UPLOAD_ERR_PARTIAL    => 'Загружаемый файл был получен только частично.',
        UPLOAD_ERR_NO_FILE    => 'Файл не был загружен.',
        UPLOAD_ERR_NO_TMP_DIR => 'Отсутствует временная папка.',
        UPLOAD_ERR_CANT_WRITE => 'Не удалось записать файл на диск.',
        UPLOAD_ERR_EXTENSION  => 'PHP-расширение остановило загрузку файла.',
    ];

    // Зададим неизвестную ошибку
    $unknownMessage = 'При загрузке файла произошла неизвестная ошибка.';

    // Если в массиве нет кода ошибки, скажем, что ошибка неизвестна
    $outputMessage = isset($errorMessages[$errorCode]) ? $errorMessages[$errorCode] : $unknownMessage;

    // Выведем название ошибки
    die($outputMessage);
}

Для того, чтобы злоумышленник не загрузил вредоносный код встроенный в изображение, нельзя доверять функции getimagesize(), которая также возвращает MIME-тип. Функция ожидает, что первый аргумент является ссылкой на корректный файл изображения. Определить настоящий MIME-тип картинки можно через расширение FileInfo. Код ниже проверит наличие ключевого слова image в типе нашего загружаемого файла и если его не окажется, выдаст ошибку:

// Создадим ресурс FileInfo
$fi = finfo_open(FILEINFO_MIME_TYPE);

// Получим MIME-тип
$mime = (string) finfo_file($fi, $filePath);

// Проверим ключевое слово image (image/jpeg, image/png и т. д.)
if (strpos($mime, 'image') === false) die('Можно загружать только изображения.');

На данном этапе мы уже можем загружать абсолютно любые картинки на наш сервер, прошедшие проверку на MIME-тип, но для загрузки изображений по определённым характеристикам нам необходимо валидировать их с помощью функции getimagesize(), которой скормим сам бинарный файл $_FILES['upload']['tmp_name']. В результате мы получим массив максимум из 7 элементов:

Array
(
    [0]        => 1280                      // ширина
    [1]        => 768                       // высота
    [2]        => 2                         // тип
    [3]        => width="1280" height="768" // аттрибуты для HTML
    [bits]     => 8                         // глубина цвета
    [channels] => 3                         // цветовая модель
    [mime]     => image/jpeg                // MIME-тип
)

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

// Результат функции запишем в переменную
$image = getimagesize($filePath);

// Зададим ограничения для картинок
$limitBytes  = 1024 * 1024 * 5;
$limitWidth  = 1280;
$limitHeight = 768;

// Проверим нужные параметры
if (filesize($filePath) > $limitBytes) die('Размер изображения не должен превышать 5 Мбайт.');
if ($image[1] > $limitHeight)          die('Высота изображения не должна превышать 768 точек.');
if ($image[0] > $limitWidth)           die('Ширина изображения не должна превышать 1280 точек.');

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

// Сгенерируем новое имя файла на основе MD5-хеша
$name = md5_file($filePath);

// Сгенерируем расширение файла на основе типа картинки
$extension = image_type_to_extension($image[2]);

// Сократим .jpeg до .jpg
$format = str_replace('jpeg', 'jpg', $extension);

// Переместим картинку с новым именем и расширением в папку /pics
if (!move_uploaded_file($filePath, __DIR__ . '/pics/' . $name . $format)) {
    die('При записи изображения на диск произошла ошибка.');
}

На этом загрузка изображения завершена. Для более удобной загрузки файлов можете использовать класс UploadedFile из пакета Symfony HttpFoundation, который является обёрткой для $_FILES и также сохраняет файл через move_uploaded_file().

§5. Загрузка изображения по ссылке

Для загрузки изображения по ссылке нам понадобиться библиотека cURL, которая работает с удалёнными ресурсами. С помощью неё мы скачаем контент в переменную. С одной стороны может показаться, что для этих целей подойдёт file_get_contents(), но на самом деле мы не сможем контролировать объём скачиваемых данных и нормально обрабатывать все возникшие ошибки. Для того, чтобы cURL корректно скачал данные нам нужно: разрешить следовать перенаправлениям, включить проверку сертификата, указать максимальное время работы cURL (формируется за счёт объёма скачиваемых данных и средней скорости работы с ресурсом). Как правильно скачать файл в переменную показано ниже с необходимыми параметрами:

<?php

// Каким-то образом получим ссылку
$url = 'https://site.ru/picture.jpg';

// Проверим HTTP в адресе ссылки
if (!preg_match("/^https?:/i", $url) && filter_var($url, FILTER_VALIDATE_URL)) {
    die('Укажите корректную ссылку на удалённый файл.');
}

// Запустим cURL с нашей ссылкой
$ch = curl_init($url);

// Укажем настройки для cURL
curl_setopt_array($ch, [

    // Укажем максимальное время работы cURL
    CURLOPT_TIMEOUT => 60,

    // Разрешим следовать перенаправлениям
    CURLOPT_FOLLOWLOCATION => 1,

    // Разрешим результат писать в переменную
    CURLOPT_RETURNTRANSFER => 1,

    // Включим индикатор загрузки данных
    CURLOPT_NOPROGRESS => 0,

    // Укажем размер буфера 1 Кбайт
    CURLOPT_BUFFERSIZE => 1024,

    // Напишем функцию для подсчёта скачанных данных
    // Подробнее: http://stackoverflow.com/a/17642638
    CURLOPT_PROGRESSFUNCTION => function ($ch, $dwnldSize, $dwnld, $upldSize, $upld) {

        // Когда будет скачано больше 5 Мбайт, cURL прервёт работу
        if ($dwnld > 1024 * 1024 * 5) {
            return -1;
        }
    },

    // Включим проверку сертификата (по умолчанию)
    CURLOPT_SSL_VERIFYPEER => 1,

    // Проверим имя сертификата и его совпадение с указанным хостом (по умолчанию)
    CURLOPT_SSL_VERIFYHOST => 2,

    // Укажем сертификат проверки
    // Скачать: https://curl.haxx.se/docs/caextract.html
    CURLOPT_CAINFO => __DIR__ . '/cacert.pem',
]);

$raw   = curl_exec($ch);    // Скачаем данные в переменную
$info  = curl_getinfo($ch); // Получим информацию об операции
$error = curl_errno($ch);   // Запишем код последней ошибки

// Завершим сеанс cURL
curl_close($ch);

Если всё прошло успешно и cURL уложился в 60 секунд, тогда содержимое по ссылке будет скачано в переменную $raw. Кроме того, функция curl_getinfo() вернёт информацию о проделанном запросе, откуда мы можем получить дополнительную информацию для анализа работы с удалёнными ресурсами:

Array
(
    [content_type]            => image/jpeg // MIME-тип из Content-Type
    [http_code]               => 200        // последний HTTP-код
    [redirect_count]          => 0          // количество перенаправлений
    [total_time]              => 0.656      // общее время работы cURL
    [connect_time]            => 0.188      // время на соединение с хостом
    [size_download]           => 4504       // реальный размер полученных данных
    [download_content_length] => 4504       // размер данных из Content-Length
    /* ... */
)

Дальше нам нужно проверить нет ли ошибок в curl_errno() и удостовериться, что ресурс отдаёт HTTP-код равный 200, иначе мы скажем, что по такому-то URL ничего не найдено. После всех проверок переменную $raw передаём в getimagesizefromstring() и работаем уже по отработанной схеме как в случае с загрузкой картинок из формы.

Обратите внимание, что мы валидируем размер файла в момент получения данных, т. к. мы не можем на 100% доверять curl_getinfo(), поскольку значения content_type, http_code, download_content_length формируются на основе полученных HTTP-заголовков. Скачивать файл полностью, а потом проверять количество байт потребует много времени и памяти. Поэтому мы контролировали размер получаемых данных с помощью опции CURLOPT_PROGRESSFUNCTION: как только cURL получит больше данных, чем наш лимит, он прекратит работу и выдаст ошибку CURLE_ABORTED_BY_CALLBACK.
// Проверим ошибки cURL и доступность файла
if ($error === CURLE_OPERATION_TIMEDOUT)  die('Превышен лимит ожидания.');
if ($error === CURLE_ABORTED_BY_CALLBACK) die('Размер не должен превышать 5 Мбайт.');
if ($info['http_code'] !== 200)           die('Файл не доступен.');

// Создадим ресурс FileInfo
$fi = finfo_open(FILEINFO_MIME_TYPE);

// Получим MIME-тип используя содержимое $raw
$mime = (string) finfo_buffer($fi, $raw);

// Закроем ресурс FileInfo
finfo_close($fi);

// Проверим ключевое слово image (image/jpeg, image/png и т. д.)
if (strpos($mime, 'image') === false) die('Можно загружать только изображения.');

// Возьмём данные изображения из его содержимого
$image = getimagesizefromstring($raw);

// Зададим ограничения для картинок
$limitWidth  = 1280;
$limitHeight = 768;

// Проверим нужные параметры
if ($image[1] > $limitHeight) die('Высота изображения не должна превышать 768 точек.');
if ($image[0] > $limitWidth)  die('Ширина изображения не должна превышать 1280 точек.');

// Сгенерируем новое имя из MD5-хеша изображения
$name = md5($raw);

// Сгенерируем расширение файла на основе типа картинки
$extension = image_type_to_extension($image[2]);

// Сократим .jpeg до .jpg
$format = str_replace('jpeg', 'jpg', $extension);

// Сохраним картинку с новым именем и расширением в папку /pics
if (!file_put_contents(__DIR__ . '/pics/' . $name . $format, $raw)) {
    die('При сохранении изображения на диск произошла ошибка.');
}

Для сохранения изображения на диск можно воспользоваться file_put_contents(), которая запишет контент в файл. Новое имя файла мы создадим через функцию md5(), а расширение сделаем из image_type_to_extension(). Теперь мы можем загружать любые картинки по ссылке.

§6. Настройка выбора нескольких файлов

В этом параграфе разберём способы загрузки нескольких изображений за один раз с локальной машины пользователя и по удалённым ссылкам. Для отправки ссылок мы задействуем $_POST и передадим ей все данные с помощью тега textarea. Для загрузки файлов из формы мы продолжим дальше работать с $_FILES. Наша новая HTML-форма будет немного отличаться от старой.

<form action="handler.php" method="post" enctype="multipart/form-data">
  <textarea name="upload"></textarea>
  <input type="file" name="upload[]" multiple>
  <button>Загрузить</button>
</form>

В конец имени поля выбора файла name="upload[]" добавились фигурные скобки и аттрибут multiple, который разрешает браузеру выбрать несколько файлов. Все файлы снова загрузятся во временную папку, если не будет никаких ошибок в php.ini. Перехватить их можно в $_FILES, но на этот раз суперглобальная переменная будет иметь неудобную структуру для обработки данных в массиве. Решается эта задача небольшими манипуляциями с массивом:

<?php

// Изменим структуру $_FILES
foreach($_FILES['upload'] as $key => $value) {
    foreach($value as $k => $v) {
        $_FILES['upload'][$k][$key] = $v;
    }

    // Удалим старые ключи
    unset($_FILES['upload'][$key]);
}

// Загружаем все картинки по порядку
foreach ($_FILES['upload'] as $k => $v) {

    // Загружаем по одному файлу
    $_FILES['upload'][$k]['tmp_name'];
    $_FILES['upload'][$k]['error'];
}

Для загрузки нескольких картинок по URL передадим наши ссылки через textarea с именем name="upload", где их можно указать через пробел или с новой строки. Функция preg_split разберёт все данные из $_POST['upload'] и сформирует массив, по которому нужно пройтись циклом и каждый валидный URL отправить в обработчик.

$data = preg_split("/\s+/", $_POST['upload'], -1, PREG_SPLIT_NO_EMPTY);

foreach ($data as $url) {
    // Валидируем и загружаем картинку по URL
}

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

Важные замечания:

(1) в файле php.ini значения post_max_size и upload_max_filesize должны совпадать;
(2) до валидации изображения стоит говорить, что мы работаем с файлом;
(3) используйте класс UploadedFile для $_FILES в боевых условиях;
(4) ошибку UPLOAD_ERR_FORM_SIZE можно словить, если в форме указать MAX_FILE_SIZE;
(5) библиотека FastImage работает быстрее getimagesize();
(6) для имитации браузера в cURL нужно установить CURLOPT_USERAGENT;
(7) ограничить редиректы можно через CURLOPT_MAXREDIRS.

Дополнительное чтиво:

Как загрузить картинку по ссылке
Загрузка и хранение фотографий в Web приложениях
Отдаем файлы эффективно с помощью PHP
Хранение файлов
Теория хранения файлов на сервере
Можно ли полагаться на HTTP-заголовки при загрузке файла?
Загрузка файлов (картинок) на сервер через AJAX и jQuery
Как прочитать большой файл средствами PHP (не грохнув при этом сервак)
Как сделать Drag-and-Drop загрузчик файлов на чистом JavaScript
Хранение большого количества файлов

2017   php   безопасность   картинки   программирование   скрипты

Наполняем БД тестовыми данными на PHP за 5 минут

Для разработки сайтов программисту почти всегда нужны тестовые данные. Иногда бывает так, что данные нужны в большом количестве и разного качества. Я поделюсь небольшим рецептом заполнения БД любой информацией на PHP за 5 минут. В качестве основного генератора данных мы будем использовать библиотеку Faker, которая поддерживает ru_RU локализацию. Для доступа к БД я возьму RedBeanPHP — это простая ORM-библиотека, позволяющая быстро вставлять данные налету. Такая комбинация инструментов хороша когда данные не обязательно должны быть уникальными и программисту, возможно, ещё до конца неизвестна структура БД.

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

Мы специально опустим это правило в целях демонстрации процесса и сэкономим немного времени, чтобы пройтись по ключевым возможностям Faker и RedBeanPHP. Предположим программисту нужно быстро запостить фейковые данные о пользователях. Ничего сложного, но давайте для начала установим наши библиотеки. Сделаем это через Composer:

> composer require fzaninotto/faker gabordemooij/redbean

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

<?php

// Подключим автозагрузку классов
require 'vendor/autoload.php';

// Объявим неймспейсы
use Faker\Factory as Faker;
use RedBeanPHP\R  as RedBean;

// Запишем экземпляр класса Faker\Factory в переменную
$faker = Faker::create('ru_RU');

// Подключаемся к БД как в PDO
RedBean::setup('mysql:host=localhost;dbname=faker', 'root', '');

/* ... */

Когда всё подключено, можно приступить к генерации данных. Поскольку мы создали экземпляр класса Faker\Factory через статический метод create('ru_RU') с русской локализацией, то теперь все сгенерированные данные для наших фейковых пользователей будут на русском языке. Библиотека предлагает достаточно много генераторов данных, но давайте посмотрим как их получить для нашего примера.

<?php

/* ... */

// Создадим ассоциативный массив, где
// укажем рандомные данные о пользователе
// с помощью Faker
$data = [
    'login'     => $faker->userName,
    'password'  => password_hash($faker->md5, PASSWORD_DEFAULT),
    'firstName' => $faker->firstNameMale,
    'lastName'  => $faker->lastName,
    'email'     => $faker->email,
];

Это произвольные данные для примера, но вы можете сотавить более сложные согласно своей задаче используя все возможности Faker. Теперь эти данные нужно пропустить через свою модель в приложении и сохранить. В качестве такой у нас выступает RedBeanPHP без различных валидаций и прочей логики. Допустим у нас в БД ещё нет таблицы users и её нужно заполнить. RedBeanPHP позволяет добавлять данные налету, т. е. нет необходимости заранее создавать структуру таблиц. Она сама создаст таблицу со всеми нужными полями. Это основное преимещество RedBeanPHP, которое ускоряет работу с БД. На примере это выглядит так:

<?php

/* ... */

// Создаём молель с именем users
$user = RedBean::dispense('users');

// В стиле ActiveRecord присваиваем наши значения
foreach ($data as $key => $value) {
    $user->{$key} = $value;
}

// Сохраняем модель в БД
RedBean::store($user);

Если нужно добавить сразу несколько пользователей в БД, тогда оберните всё логику в цикл for и укажите количество итераций. На этом весь рецепт быстрого добавления данных в БД заканчивается. По существу мы не написали ни одного SQL-запроса, даже не открыли PhpMyAdmin, но на выходе получили заполненую таблицу с нужными нам полями. Вот такую таблицу сгенерировал RedBeanPHP:

Для чтения или модификации данных можно использовать RedBeanPHP, т. к. он заточен под CRUD-операции, умеет работать с множественными таблицами, интегрируется в свои модели как полноценная ORM. Этого достаточно для быстрого доступа к БД и вывода данных.

2016   php   программирование   скрипты