\r\n * Content-Disposition: form-data; name="file"; filename="photo.jpg"\r\n * Content-Type: image/jpeg\r\n * \r\n * <байты файла>\r\n * --<граница>--\r\n * * Чистая логика без WordPress — проверяется тестами. */ final class Multipart { /** Перевод строки, которого требует формат: CR LF, не просто LF. */ private const CRLF = "\r\n"; /** * Новая случайная граница частей. * * Граница не должна встречаться внутри файла; 32 случайных шестнадцатеричных знака * совпадут с куском фотографии с ничтожной вероятностью. * * @return string */ public static function new_boundary(): string { return 'aimodelka-' . bin2hex( random_bytes( 16 ) ); } /** * Значение заголовка Content-Type для тела с этой границей. * * @param string $boundary Граница частей. * @return string */ public static function content_type( string $boundary ): string { return 'multipart/form-data; boundary=' . $boundary; } /** * Тело запроса с одним файлом. * * @param string $boundary Граница частей. * @param string $field Имя поля формы, у API — «file». * @param string $file_name Имя файла, как его увидит сервер. * @param string $mime_type Тип содержимого файла, например image/jpeg. * @param string $contents Байты файла. * @return string */ public static function file_body( string $boundary, string $field, string $file_name, string $mime_type, string $contents ): string { return '--' . $boundary . self::CRLF . 'Content-Disposition: form-data; name="' . self::safe_token( $field, 'file' ) . '"; filename="' . self::safe_file_name( $file_name ) . '"' . self::CRLF . 'Content-Type: ' . self::safe_mime_type( $mime_type ) . self::CRLF . self::CRLF . $contents . self::CRLF . '--' . $boundary . '--' . self::CRLF; } /** * Имя файла, безопасное для заголовка части. * * Кавычки, переводы строк и не-латиница в имени сломали бы заголовок или его разбор * на сервере. Имя на сервере ни на что не влияет — только помогает человеку узнать фото * (`SourcePhoto.originalFilename`), — поэтому всё лишнее просто заменяется дефисом. * * @param string $file_name Исходное имя файла. * @return string Латиница, цифры, точка, дефис и подчёркивание; «photo.jpg», если не осталось ничего. */ public static function safe_file_name( string $file_name ): string { $base = basename( str_replace( '\\', '/', $file_name ) ); $safe = (string) preg_replace( '/[^A-Za-z0-9._-]+/', '-', $base ); $safe = trim( $safe, '-.' ); return '' === $safe ? 'photo.jpg' : $safe; } /** * Имя поля — только безопасные знаки. * * @param string $value Имя поля. * @param string $fallback Что подставить, если не осталось ничего. * @return string */ private static function safe_token( string $value, string $fallback ): string { $safe = (string) preg_replace( '/[^A-Za-z0-9_-]+/', '', $value ); return '' === $safe ? $fallback : $safe; } /** * Тип содержимого — только вид «type/subtype». * * @param string $mime_type Тип содержимого. * @return string */ private static function safe_mime_type( string $mime_type ): string { return 1 === preg_match( '#^[a-z0-9.+-]+/[a-z0-9.+-]+$#i', $mime_type ) ? $mime_type : 'application/octet-stream'; } }