nmk

Лекція 8 (2 години). Завантаження та обробка файлів

План лекції

  1. Налаштування HTML-форми для передачі файлів (enctype).
  2. Суперглобальний масив $_FILES та його структура.
  3. Процес збереження файлу на сервер: функція move_uploaded_file.
  4. Безпека завантажень (валідація типу, розміру, генерація унікальних імен).

Перелік умовних скорочень

Вступ

Додавання аватарки до профілю, прикріплення резюме (PDF) або завантаження фотографій товару — це стандартний функціонал майже будь-якого сучасного веб-застосунку. Робота з файлами кардинально відрізняється від роботи з простим текстом із форми. Файли мають вагу (іноді гігабайти), вони передаються частинами і, що найважливіше, вони є головним вектором хакерських атак (через завантаження шкідливих PHP-скриптів під виглядом картинок). У цій лекції ми навчимось правильно конфігурувати форму і безпечно обробляти вхідні файли в PHP.


1. Налаштування HTML-форми для передачі файлів

Звичайна форма <form method="POST"> вміє відправляти лише текст. Спробувавши відправити через неї файл, ви отримаєте в PHP просто його назву (рядок “my-photo.jpg”), а не самі байти зображення.

Щоб “навчити” браузер розрізати файл на частини та відправляти його на сервер у бінарному вигляді, форму обов’язково потрібно наділити атрибутом enctype="multipart/form-data".

<!-- Атрибут enctype критично необхідний! Без нього $_FILES буде порожнім -->
<form action="upload.php" method="POST" enctype="multipart/form-data">
  <label>Оберіть своє фото (Аватар):</label>
  <!-- type="file" створює кнопку "Огляд..." в браузері -->
  <!-- Атрибут accept (необов'язковий) підказує браузеру показувати лише картинки -->
  <input type="file" name="avatar" accept="image/png, image/jpeg" />

  <button type="submit">Завантажити</button>
</form>

2. Суперглобальний масив $_FILES

Коли файл прилітає на сервер, PHP зберігає його у свою приховану тимчасову системну папку (Temp dir). А для розробника він люб’язно створює суперглобальний двовимірний масив $_FILES.

Якщо наш input мав name="avatar", то масив $_FILES['avatar'] буде містити 5 стандартних ключів-характеристик цього завантаження:

<?php
// Для дебагінгу подивимось, що всередині:
// var_dump($_FILES['avatar']);

/* Приблизний вивід:
array(5) {
  ["name"]     => string(10) "my-cat.jpg"  // Оригінальна назва файлу на ПК користувача
  ["type"]     => string(10) "image/jpeg"  // MIME-тип (визначається браузером)
  ["tmp_name"] => string(24) "C:\xampp\tmp\php6A3E.tmp" // Де він ЗАРАЗ тимчасово лежить на сервері
  ["error"]    => int(0)                   // Код помилки (0 - означає успіх)
  ["size"]     => int(102400)              // Розмір у Байтах (102400 = 100 Кб)
}
*/
?>

Коди помилок error

3. Збереження файлу: move_uploaded_file

Файл, який лежить у системній тимчасовій папці (tmp_name), буде безповоротно видалений самим PHP автоматично одразу після того, як скрипт upload.php завершить роботу (тобто через долі секунди).

Наше завдання — встигнути перемістити його з tmp_name до нашої постійної папки проєкту (наприклад, /uploads/avatars/). Для цього використовується спеціалізована захищена функція move_uploaded_file(). Заборонено використовувати звичайну функцію копіювання copy(), оскільки вона не перевіряє, чи дійсно цей файл був завантажений через HTTP, чи це системний файл сервера.

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $file = $_FILES['avatar'];

    // 1. Перевіряємо, чи взагалі надійшов файл і чи не було помилок під час транспортування
    if ($file['error'] === UPLOAD_ERR_OK) {

        // 2. Звідки беремо? (Тимчасовий системний шлях)
        $tmpPath = $file['tmp_name'];

        // 3. Куди кладемо? (Наприклад, в папку uploads поруч зі скриптом)
        // ВАЖЛИВО: папка "uploads" має фізично існувати на диску до запуску!
        $destination = 'uploads/' . $file['name'];

        // 4. Переміщуємо
        if (move_uploaded_file($tmpPath, $destination)) {
            echo "Файл успішно збережений як: " . $destination;
        } else {
            echo "Помилка запису на диск (можливо, немає прав у папки uploads).";
        }
    } else {
        echo "Файл не було завантажено (Помилка " . $file['error'] . ")";
    }
}
?>

4. Безпека завантажень (Критично важливо)

Код вище є вкрай небезпечним. Якщо хакер перейменує вірус на virus.php і завантажить його як аватарку, а потім відкриє адресу yoursite.com/uploads/virus.php — сервер люб’язно виконає його код. Ваша система буде повністю зламана.

Щоб цього уникнути, потрібен жорсткий 3-етапний контроль.

Крок 1: Валідація за Розміром

Перевіряти $file['size']. Захищає від переповнення дисків (DDoS-атаки на сховище).

$maxSize = 2 * 1024 * 1024; // 2 Мегабайти
if ($file['size'] > $maxSize) {
    die("Файл завеликий! Максимум 2 МБ.");
}

Крок 2: Валідація Розширення та Типу файлу

Браузерному атрибуту accept="image/png" вірити не можна (його можна видалити в F12). $_FILES['type'] також формується браузером клієнта. Найнадійніший алгоритм — забрати оригінальне розширення з кінця імені файла (pathinfo()) та перевірити його вручну через так званий “White list” (Білий список дозволених).

// Білий список безпечних розширень:
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];

// Дістаємо "jpg" з тексту "my_photo-2023.min.jpg"
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

if (!in_array($ext, $allowedExtensions)) {
    die("Дозволено завантажувати ТІЛЬКИ картинки (jpg, png, gif)!");
}

Крок 3: Генерація Унікального Імені файлу (Anti-Collision)

Ніколи не зберігайте файл під його оригінальним $file['name']. По-перше, зловмисник може вбудувати туди шкідливі символи. По-друге, якщо два користувачі завантажать аватарку з ідентичним ім’ям photo.jpg (що масово зустрічається на телефонах) — другий файл автоматично перезапише й знищить перший без будь-яких попереджень!

Генеруємо унікальний, гарантовано рандомний хеш імені:

// Створюємо нове ім'я на основі унікального ідентифікатора часу
$newName = uniqid('avatar_', true) . '.' . $ext;
// Вийде щось накшталт: avatar_65f01...b2.jpg

$destination = 'uploads/' . $newName;
move_uploaded_file($tmpPath, $destination);

Висновки

  1. Завантаження файлів з комп’ютера клієнта вимагає модифікації форми атрибутом enctype="multipart/form-data" і підтримується лише через метод HTTP POST.
  2. PHP отримує файли і структурує інформацію про них (тимчасову адресу, оригінальне ім’я, розмір, код помилки) у глобальному масиві $_FILES.
  3. Фізично зберегти файл означає “Врятувати його від видалення з тимчасової теки сервера”. Для цього застосовують спеціальну функцію move_uploaded_file().
  4. Збереження файлу без тотальної валідації розміру, його оригінального розширення та без перейменування (генерації унікального імені uniqid()) є головною причиною злому більшості веб-додатків у світі.

Джерела

  1. Робота з файлами: https://www.php.net/manual/ru/features.file-upload.php
  2. Функція переміщення завантаженого файлу: https://www.php.net/manual/ru/function.move-uploaded-file.php

Запитання для самоперевірки

  1. Знайдіть помилку і поясніть, чому метод $_POST['avatar_file'] завжди буде повертати просто рядок тексту, а масив $_FILES буде порожнім, якщо форма має вигляд: <form method="POST"><input type="file" name="avatar_file"></form>. Якого фундаментального атрибута тут не вистачає?
  2. Яким числовим кодом помилки error в масиві $_FILES позначається успішне, стовідсоткове скачування файлу на тимчасову територію сервера (Temp dir)?
  3. Де фізично перебуває файл до виклику move_uploaded_file()? Згадайте, якою буде його доля по завершенні роботи upload.php, якщо програміст його так і НЕ перемістить?
  4. Назвіть щонайменше 2 причини, через які категорично заборонено зберігати картинки користувачів (аватарки) на сервері під їхнім оригінальним ім’ям ['name'].
  5. Концепція “White list” (Білого списку) для розширень файлів при завантаженні вважається в рази надійнішою, ніж “Black list” (Чорний список, напр. заборона лише .php і .exe). Чому?