Доброго времени суток!

MIME (Multipurpose Internet Mail Extensions) — универсальные расширения для Интернет-почты. Используется для определения типа данных, формата файла, вида контента файла.

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

Введение

Если вы работали с ОС Windows (да если вы вообще работали с ПК или файлами), то замечали, что у каждого типа файла есть свое расширение. Например, у фотографий: jpeg, jpg, jpe, raw и т. д; у документов PDF (Portable Document Format) — pdf, а у установщиков (пакетов) приложений для Android — apk.

С закодированными файлами все понятно — в самом файле зашито расширение (тип MIME) файла, а вот с простыми текстовыми данными или распространёнными архивами дела обстоят иначе.

Рассмотрим типичные проблемы файла и содержимого.

Изменение расширения файла

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

Для продвинутых пользователей ПК известно, что изменив расширение файла (например, с png в jpg или 3gp в avi) внутренняя составляющая файла не изменится, т. е. не произойдёт чудо-конвертирования файла в новое расширение перекодировав все данные в новый формат (а было бы неплохо). Произойдет просто изменение имени файла.

Но, имя файла, особенно в ОС Windows, жёстко привязано к системе, но не спроста. Например, расширения в именах снижают количество обработок для ОС, а соответственно и увеличивают производительность. Представьте, что для определения типа файла и отображения его содержимого приходилось лезть в каждый файл, проверять его MIME, и только после этого проделывать остальные операции, такие как отображение миниатюры изображения, отображение типа файла по его содержимому (как это работает в Linux и происходит это не очень быстро, но 100% правильно, хотя могут быть и другие причины низких скоростей получения информации о файлах).

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

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

Тип MIME не соответствует расширению файла

Вторая проблема типа MIME — существует множество типов файлов и расширений, которые всего лишь обёртки для системы, но не ПО работающими с ними.

По-русски: есть файл с расширением svg (векторная графика). Векторная графика представляется точками, функциями, значениями и т. п. и хранится в открытом виде, в виде обычного XML. Но к счастью, стандартная функция PHP mime_content_type определяет именно этот файл как надо и выдает значение «image/svg+xml», т. е. это SVG сохраненный в XML.

Капнем глубже и найдем нашу проблему. Возьмем файл пакета под Андроид с расширением apk. Что это за файл? Обычный архив. И вот тут, при считывании MIME в получаем значение «application/zip» вместо «application/vnd.android.package-archive». Самое интересное, что подобных форматов файлов достаточно много и это не единственное возвращаемое значение.

Выше была описана вторая проблема: расширение файла одно, содержимое — другое.

Скачивание файлов и их обработка

Еще две проблемы, точнее как следствия, которые вытекают из расширений и типов MIME.

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

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

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

Поэтому, я решил найти пакет, который решил бы мою проблему: по файлам будет определять их MIME, кроме этого, мне дополнительно нужна группа файла, которая также указывается в MIME, но во многих случаях она общая, т. е. «application», что значит файл открывается только со специализированным приложением (ПО), но мне нужно другое — определение групп image, document, video, archive и т. д. (две из перечисленных существует).

Обзор популярных пакетов PHP для получения MIME

В PHP существует стандартная функция для определения MIME-типа содержимого файла.

FileInfo

Это пакет, включаемый по умолчанию в PHP в версия 5.3, 7 и выше. До этого использовались другой способ определения типов данных: расширение Mimetype.

Как описано на сайте php, определение типа с помощью Mimetype определялось из файла соответствий (своего рода БД) подобному файлу поставляемому с Apache (файл magic). Если мы заглянем в папку Апач, то увидим этот файл и его содержимое. Можно предположить, что считывание файла происходит на основе считанных первых байт содержимого файла и сопоставления с существующей таблицей. В соответствии с этой таблицей и определяется тип данных. Такой способ мы увидим дальше, а этот пакет был удален из PHP в пользу FileInfo.

Сначала рассмотрим упрощенную функцию mime_content_type.

string mime_content_type ( string $filename )

Возвращает MIME-тип содержимого файла, используя для определения информацию из файла magic.mime (как оказалось, он встроен уже в сам пакет).

В этом пакете есть и другие способы для определения типа файла — функция finfo_open с разными опциями. Но результат вывода будет соответственно такой же как и у функции mime_content_type.

Единственное отличие: мы можем указать свой магический файл с типами.

Из документации можно увидеть, что в PHP 7.2 появился новый параметр FILEINFO_EXTENSION. Как понятно из названия, он выводит список расширений файла (если их несколько) или знаки вопроса, когда тип неопределен.

К сожалению, используемая функция не дает нам нужного результата на версии PHP 7.0, но возможно что-то изменилось в версии 7.2.

Интересно: в системе Ubuntu (возможно и других Linux) есть пакеты для определения MIME типов: file и mimetype. Они выдают разный результат, и конечно же, все три используемых пакета выдают разный результат.

Результаты выводов функций для определения MIME-типов:

  • PHP 7.0 FileInfo (mime_content_type, finfo, file_open) при определения содержимого файла с Android-пакетом (файл «name.apk»): application/zip.
    Результат меня вообще не устраивает. Такой же неподобаемый результат выдает при определении любых скриптов.

  • file --mime-type name.apk (пакет для Linux): application/java-archive.
    Это также неподобаемый результат, т. к. на самом деле файлы с таким типом должны иметь расширение jar.

  • mimetype name.apk (пакет для Linux): application/vnd.android.package-archive.
    Результат вывода этой команды меня полностью устроил. Но часть результатов на опробованных файлах все равно требует улучшения, хоть и не указываются стандартные выводы, типа «text/plain» или «application/zip».

Вывод: все пакеты хороши, но тут качество зависит только от заполненности базы (соответствий типов).

Пакет для PHP Mimey

Возможно неправильно сюда относить этот пакет, но он относительно хорош.

Mimey работает со значениями MIME и расширениями файлов. Он позволяляет «конвертировать» название MIME-типа в расширение и наоборот. Если у данного типа несколько расширений (как у JPEG), то можно вывести все, а можно только один.

Данный пакет работает только с названиями типов и расширениями, но не работает с файлами, т. е. вопрос о корректном определении все равно остается, но часть потребностей мы можем закрыть этим пакетом, например сопоставить тип MIME и расширение, или наоборот. Но перед этим нужно корректно определить тип файла.

Работает он с версией PHP 5.4 и выше. Установка пакета через composer:

composer require ralouphie/mimey

Пакет позволяет добавлять произвольные типы и их расширения, т. е. пакет можно расширять.

Пример использования:

$mimes = new \Mimey\MimeTypes;
// Convert extension to MIME type:
$mimes->getMimeType('json'); // application/json
// Convert MIME type to extension:
$mimes->getExtension('application/json'); // json

Страница на гитхаб: https://github.com/ralouphie/mimey

Вывод: работает только с текстом и отлично справляется (хорошая база). Не работает с файлами.

Пакет PHP Mime Detector

Пакет довольно молодой и косяки на лицо, даже еще сыроват. Суть у пакета такая же, как у стандартной функции PHP - определение типа файла по ему содержимому.

Все упирается в БД. Тут она небольшая, да и пока много кода на использование пакета.

use SoftCreatR\MimeDetector\MimeDetector;
use SoftCreatR\MimeDetector\MimeDetectorException;

// create an instance of the MimeDetector
$mimeDetector = MimeDetector::getInstance();

// set our file to read
try {
    $mimeDetector->setFile('foo.bar');
} catch (MimeDetectorException $e) {
    die('An error occured while trying to load the given file.');
}

// try to determine it's mime type and the correct file extension
$fileData = $mimeDetector->getFileType();

Ссылка на гитхаб: https://github.com/SoftCreatR/php-mime-detector.

Мне мешает тут отладка, я не хочу получать раздутые методы.

Вывод: сами разработчики нацелены на использование этого пакета, где нет поддержки FileInfo на сервере (но при этом поддержка PHP 7.1), а также на безопасное определение типа файла (чтоб 100% было точно). Пакет можно использовать, но с такими файлами как мне нужно - нет.

P.S. Если вы используете стандартный пакет FileInfo, то большая вероятность, что вам не понадобиться данный пакет (маленькая база).

Пакет PhpMimeType v2

Тут очень странно (описание со страницы пакета): простой PHP-класс для угадывания MIME-типа файла на основе расширения файла с возможностью использования в проекте Symfony.

Ну и сразу пример:

// from string, can be used on non-existing files
echo \Defr\PhpMimeType\MimeType::get('index.php'); // outputs text/html

Как видно, угадал, но не так. Для этого пакета должно было быть явно другое расширение, но т. к. тип MIME не зарегистрирован, то можно выводить что угодно (text/plain, application/x-php).

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

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

Пакет mm (davidpersson/mm)

Большой пакет рассчитанный на обработку медиафайлов. Он хоть и довольно старый (точнее давно не обновлялся), но некоторые функции для меня там присутствуют (определение группы файла).

Пример определения групп (он мне просто нужен):

Type::guessName('example.png'); // returns 'image'
Type::guessName('example.webm'); // returns 'video'
Type::guessName('application/pdf'); // returns 'document'
Type::guessName('/path/to/example.png'); // returns 'image'

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

Гитхаб: https://github.com/davidpersson/mm.

Вывод: что-то может определять, но проще воспользоваться стандартными методами.

Пакет PhpMimeType

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

$type = \MimeType\MimeType::getType('my-file.pdf'); // returns "application/pdf"

Однако есть небольшой минус: он определяет MIME-тпи файлов только по расширению.

Как было описано выше у PHP Mime Detector — небезопасно.

Пакет Mimey полностью может перекрыть функции данного пакета.

Гитхаб: https://github.com/katzien/PhpMimeType.

Вывод: определяет только по расширению. Mimey лучше.

Пакет blob-mimes

Пакет по своей базе достаточно обширный и основан на нескольких источниках о MIME.

Использует встроенные функции языка для определения типов и расширений файлов, а также сторонние расширения. На основе имеющейся БД уже делает выводы о типе файла.

Некоторые функции пакета: сравнение расширений и типов файла, возвращение массива других возможных расширений по текущему расширению и т. п.

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

print_r(blobfolio\mimes\mimes::finfo('../wp/img/blobfolio.svg'));
/*
array(
    [dirname] => /var/www/blob-common/wp/img
    [basename] => blobfolio.svg
    [extension] => svg
    [filename] => blobfolio
    [path] => /var/www/blob-common/wp/img/blobfolio.svg
    [mime] => image/svg+xml
    [suggested_filename] => array()
)
*/

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

Данный пакет меня очень заинтересовал и возможно отвечает моим требованиям.

Проверив его результат меня немного порадовал, но без проблем не обошлось:

  • во время установки пакета, я заметил, что он тянет за собой лишние пакеты, которые (с моей точки зрения) вообще не нужны;

  • проведя свои тесты, часть из них было выполнено (вариант «адекватный пользователь», часть нет (подмена названий файлов).

Успешные результаты тестирования соответствовали результату пакета на Linux mimetype. А вот подмена имен файлов и содержимого не прошла тестирование. В большинстве случаев были ответы MIME-типов в соответствии с расширением файла. В одном случае выдал частично правильный результат (возможно по одному из стандартов он и действительный): php скрипт без расширения — результат text/x-php.

Но не стоит отчаиваться, т. к. и команда mimetype дала ложный результат переименовав rar-архив в apk-файл.

  • Результат команды mimetype: application/vnd.android.package-archive.

  • Результат обработки Blob-mimes: application/zip.

  • Необходимый результат: application/vnd.rar.

Гитхаб: https://github.com/Blobfolio/blob-mimes#get_mimes

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

P.S. Разработчик ответив на результаты моих тестирований, подтвердил, что определения содержимого файла используется на основе сторонних библиотек. Остальная обработка определяется исходя из расширения файла.

Вывод

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

Можно надеется, что стандартная функция php для определения типа файла всё-таки станет максимально точно определять MIME-тип (на крайняк, можно указать свои типы файлов).

Для тех, кто пользуется версиями ниже 7.2 и нужно просто конвертация типа файла или минимальная проверка, то можно воспользоваться стандартными средствами PHP и пакетом Mimey.

Для тех, кто пользуется версией 7.2 пакет Mimey уже не нужен будет, т. к. расширение FileInfo уже включает в себя эту функцию.

Для тех, кому нужен реальный MIME-тип файла (а это скорее всего в том случае, если вы хотите обработать данный файл: переконвертировать, изменить, создать модификацию), тому нужно проверять тип файла на содержимое. Если содержимое файла отличается от стандартных общеизвестных (image, audio, video и другими типами неизвестными для FileInfo), то стоит воспользоваться сторонними пакетами, но если ваш сервер на Linux есть возможность использования системных утилит, то ими можно воспользоваться (например, mimetype).

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

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

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