Lekcja 15: Przesyłanie Plików na Serwer (Obsługa $_FILES)

Witaj w piętnastej lekcji kursu PHP! Po nauczeniu się, jak obsługiwać dane z formularzy tekstowych, przyszedł czas na kolejny ważny aspekt interakcji z użytkownikiem: przesyłanie plików na serwer. Użytkownicy często potrzebują wgrywać pliki, takie jak awatary, zdjęcia, dokumenty PDF, czy archiwa ZIP. PHP oferuje mechanizmy do obsługi plików przesyłanych za pośrednictwem formularzy HTML, głównie za pomocą superglobalnej tablicy $_FILES. W tej lekcji szczegółowo omówimy, jak bezpiecznie i efektywnie zarządzać przesyłaniem plików.

Formularz HTML do Przesyłania Plików

Aby umożliwić użytkownikowi przesłanie pliku, formularz HTML musi spełniać dwa kluczowe warunki:

  1. Atrybut method tagu <form> musi być ustawiony na POST. Metoda GET nie obsługuje przesyłania plików.
  2. Tag <form> musi zawierać atrybut enctype ustawiony na "multipart/form-data". Ten typ kodowania jest niezbędny do przesyłania plików binarnych wraz z innymi danymi formularza.

Elementem formularza służącym do wyboru pliku jest <input type="file">. Musi on również posiadać atrybut name, który będzie używany jako klucz w tablicy $_FILES w PHP.

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Przesyłanie Pliku</title>
</head>
<body>
    <h2>Prześlij swój awatar</h2>
    <form action="upload_avatar.php" method="POST" enctype="multipart/form-data">
        <p>
            <label for="nazwa_uzytkownika">Nazwa użytkownika:</label>
            <input type="text" id="nazwa_uzytkownika" name="username" required>
        </p>
        <p>
            <label for="plik_awatara">Wybierz plik awatara (JPG, PNG, GIF, maks. 2MB):</label><br>
            <input type="file" id="plik_awatara" name="avatar_file" required>
        </p>
        <p>
            <!-- Opcjonalne ukryte pole do ograniczenia maksymalnego rozmiaru pliku po stronie klienta -->
            <!-- Wartość w bajtach, np. 2MB = 2 * 1024 * 1024 = 2097152 -->
            <input type="hidden" name="MAX_FILE_SIZE" value="2097152" />
        </p>
        <p>
            <input type="submit" value="Prześlij Awatar">
        </p>
    </form>
</body>
</html>

Pole MAX_FILE_SIZE jest wskazówką dla przeglądarki i może być łatwo zmodyfikowane przez użytkownika. Zawsze należy walidować rozmiar pliku po stronie serwera.

Superglobalna Tablica $_FILES

Kiedy formularz z enctype="multipart/form-data" jest wysyłany, PHP automatycznie przetwarza przesłane pliki i udostępnia informacje o nich w superglobalnej tablicy $_FILES. Jest to tablica asocjacyjna, gdzie kluczem jest wartość atrybutu name pola <input type="file"> (w naszym przykładzie "avatar_file").

Każdy element w $_FILES jest również tablicą asocjacyjną zawierającą następujące klucze:

Kody Błędów w $_FILES["nazwa_pola"]["error"]

Wartość $_FILES["nazwa_pola"]["error"] może przyjmować następujące stałe:

Zawsze należy sprawdzać ten kod błędu jako pierwszy krok przy obsłudze przesyłanych plików.

Przetwarzanie Przesłanego Pliku

Proces obsługi przesłanego pliku zazwyczaj obejmuje następujące kroki:

  1. Sprawdzenie, czy formularz został wysłany metodą POST.
  2. Sprawdzenie istnienia danych o pliku w $_FILES i kodu błędu.
  3. Walidacja pliku:
    • Rozmiar pliku (czy nie jest za duży lub za mały).
    • Typ pliku (MIME type i/lub rozszerzenie) – bardzo ważne ze względów bezpieczeństwa.
    • Ewentualne inne kryteria (np. wymiary obrazu).
  4. Wygenerowanie bezpiecznej, unikalnej nazwy dla pliku na serwerze. Nie należy używać oryginalnej nazwy pliku bezpośrednio, ponieważ może zawierać niebezpieczne znaki lub nadpisać istniejące pliki.
  5. Przeniesienie pliku z katalogu tymczasowego do docelowej lokalizacji na serwerze za pomocą funkcji move_uploaded_file().
  6. Obsługa ewentualnych błędów na każdym etapie.

Skrypt PHP (upload_avatar.php) do obsługi przesyłania pliku:

<?php
$katalogDocelowy = "uploads/"; // Katalog, gdzie będą przechowywane awatary
$komunikaty = [];

// Upewnij się, że katalog docelowy istnieje i ma odpowiednie uprawnienia
if (!is_dir($katalogDocelowy)) {
    if (!mkdir($katalogDocelowy, 0755, true)) {
        die("Nie udało się utworzyć katalogu docelowego: $katalogDocelowy");
    }
}

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $username = isset($_POST["username"]) ? htmlspecialchars(trim($_POST["username"])) : "nieznany_uzytkownik";

    // Sprawdzenie, czy plik został przesłany i czy nie ma błędów
    if (isset($_FILES["avatar_file"]) && $_FILES["avatar_file"]["error"] === UPLOAD_ERR_OK) {
        $plikInfo = $_FILES["avatar_file"];

        $oryginalnaNazwa = $plikInfo["name"];
        $rozmiarPliku = $plikInfo["size"];
        $tymczasowaSciezka = $plikInfo["tmp_name"];
        // $typMIMEprzegladarki = $plikInfo["type"]; // Nie ufaj tej wartości!

        $komunikaty[] = "Otrzymano plik: " . htmlspecialchars($oryginalnaNazwa);
        $komunikaty[] = "Rozmiar: $rozmiarPliku bajtów";
        $komunikaty[] = "Ścieżka tymczasowa: " . htmlspecialchars($tymczasowaSciezka);

        // --- KROK WALIDACJI --- 

        // 1. Walidacja rozmiaru pliku (np. maks. 2MB)
        $maxRozmiar = 2 * 1024 * 1024; // 2MB w bajtach
        if ($rozmiarPliku > $maxRozmiar) {
            $komunikaty[] = "BŁĄD: Plik jest za duży. Maksymalny dozwolony rozmiar to 2MB.";
        } elseif ($rozmiarPliku == 0) {
            $komunikaty[] = "BŁĄD: Plik jest pusty.";
        } else {
            // 2. Walidacja typu pliku (bezpieczniejsza niż poleganie na $_FILES[...]["type"])
            // Sprawdzenie rozszerzenia
            $dozwoloneRozszerzenia = ["jpg", "jpeg", "png", "gif"];
            $rozszerzeniePliku = strtolower(pathinfo($oryginalnaNazwa, PATHINFO_EXTENSION));
            
            if (!in_array($rozszerzeniePliku, $dozwoloneRozszerzenia)) {
                $komunikaty[] = "BŁĄD: Niedozwolone rozszerzenie pliku. Akceptowane są tylko: " . implode(", ", $dozwoloneRozszerzenia);
            } else {
                // Dodatkowa weryfikacja typu MIME na podstawie zawartości pliku (bardziej niezawodne)
                $finfo = finfo_open(FILEINFO_MIME_TYPE);
                $typMIMEserwera = finfo_file($finfo, $tymczasowaSciezka);
                finfo_close($finfo);

                $dozwoloneTypyMIME = ["image/jpeg", "image/png", "image/gif"];
                if (!in_array($typMIMEserwera, $dozwoloneTypyMIME)) {
                    $komunikaty[] = "BŁĄD: Wykryty typ MIME pliku ($typMIMEserwera) jest niedozwolony.";
                } else {
                    $komunikaty[] = "Wykryty typ MIME (serwer): $typMIMEserwera - OK";

                    // --- KROK GENEROWANIA BEZPIECZNEJ NAZWY I PRZENOSZENIA --- 
                    // Stwórz unikalną i bezpieczną nazwę pliku
                    // np. username_timestamp.extension lub hash.extension
                    $bezpiecznaNazwaPliku = preg_replace("/[^a-zA-Z0-9_-]/", "_", $username);
                    $nowaNazwaPliku = $bezpiecznaNazwaPliku . "_" . time() . "." . $rozszerzeniePliku;
                    $sciezkaDocelowa = $katalogDocelowy . $nowaNazwaPliku;

                    // Przenieś plik z lokalizacji tymczasowej do docelowej
                    if (move_uploaded_file($tymczasowaSciezka, $sciezkaDocelowa)) {
                        $komunikaty[] = "SUKCES: Plik został pomyślnie przesłany i zapisany jako: " . htmlspecialchars($nowaNazwaPliku);
                        $komunikaty[] = "Ścieżka na serwerze: " . htmlspecialchars($sciezkaDocelowa);
                        // Tutaj można zapisać $sciezkaDocelowa w bazie danych dla użytkownika
                    } else {
                        $komunikaty[] = "BŁĄD KRYTYCZNY: Nie udało się przenieść przesłanego pliku. Sprawdź uprawnienia katalogu docelowego.";
                    }
                }
            }
        }
    } elseif (isset($_FILES["avatar_file"])) {
        // Obsługa błędów przesyłania
        $kodBledu = $_FILES["avatar_file"]["error"];
        $komunikatBleduUpload = "Nieznany błąd przesyłania.";
        switch ($kodBledu) {
            case UPLOAD_ERR_INI_SIZE:
                $komunikatBleduUpload = "Plik przekracza maksymalny dozwolony rozmiar (upload_max_filesize w php.ini).";
                break;
            case UPLOAD_ERR_FORM_SIZE:
                $komunikatBleduUpload = "Plik przekracza maksymalny rozmiar określony w formularzu (MAX_FILE_SIZE).";
                break;
            case UPLOAD_ERR_PARTIAL:
                $komunikatBleduUpload = "Plik został przesłany tylko częściowo.";
                break;
            case UPLOAD_ERR_NO_FILE:
                $komunikatBleduUpload = "Żaden plik nie został wybrany do przesłania.";
                break;
            case UPLOAD_ERR_NO_TMP_DIR:
                $komunikatBleduUpload = "Brak katalogu tymczasowego na serwerze.";
                break;
            case UPLOAD_ERR_CANT_WRITE:
                $komunikatBleduUpload = "Nie można zapisać pliku na dysku serwera.";
                break;
            case UPLOAD_ERR_EXTENSION:
                $komunikatBleduUpload = "Przesyłanie pliku zostało zatrzymane przez rozszerzenie PHP.";
                break;
        }
        $komunikaty[] = "BŁĄD PRZESYŁANIA: $komunikatBleduUpload (kod: $kodBledu)";
    } else {
        $komunikaty[] = "Nie otrzymano żadnego pliku lub wystąpił nieoczekiwany błąd.";
    }
} else {
    $komunikaty[] = "Formularz nie został wysłany metodą POST.";
}

// Wyświetlanie komunikatów
echo "<h1>Status Przesyłania Pliku</h1>";
if (!empty($komunikaty)) {
    echo "<ul>";
    foreach ($komunikaty as $komunikat) {
        echo "<li>$komunikat</li>";
    }
    echo "</ul>";
}

echo "<p><a href=\"formularz_upload.html\">Powrót do formularza przesyłania</a></p>";

// Wyświetlenie zawartości $_FILES do celów debugowania
// echo "<h3>Zawartość ".htmlspecialchars("$_FILES").":</h3><pre>";
// print_r($_FILES);
// echo "</pre>";
?>

Kluczowe Aspekty Bezpieczeństwa przy Przesyłaniu Plików:

  1. Nigdy nie ufaj nazwie pliku od klienta ($_FILES[...]["name"]): Może zawierać znaki specjalne, próby obejścia ścieżek (np. ../../tajne.txt) lub niebezpieczne rozszerzenia. Zawsze generuj nową, bezpieczną nazwę pliku na serwerze.
  2. Nigdy nie ufaj typowi MIME od klienta ($_FILES[...]["type"]): Może być łatwo sfałszowany. Weryfikuj typ pliku na serwerze za pomocą funkcji takich jak finfo_file() (z rozszerzenia Fileinfo) lub sprawdzając magiczne liczby pliku. Ograniczaj dozwolone typy plików do tych, które są absolutnie niezbędne.
  3. Waliduj rozmiar pliku na serwerze: Nie polegaj tylko na MAX_FILE_SIZE z formularza. Sprawdzaj $_FILES[...]["size"] oraz konfigurację PHP (upload_max_filesize, post_max_size).
  4. Używaj move_uploaded_file(): Ta funkcja jest specjalnie zaprojektowana do przenoszenia plików przesłanych przez HTTP POST. Sprawdza, czy plik faktycznie został przesłany tą metodą, co zapobiega niektórym typom ataków. Nie używaj copy() ani rename() dla plików z $_FILES[...]["tmp_name"].
  5. Przechowuj przesyłane pliki poza głównym katalogiem dostępnym przez WWW (Document Root): Jeśli to możliwe, przechowuj pliki w katalogu, który nie jest bezpośrednio dostępny przez URL. Dostęp do nich powinien być kontrolowany przez skrypt PHP, który sprawdza uprawnienia itp. Jeśli muszą być w katalogu publicznym (np. awatary), upewnij się, że serwer nie będzie próbował ich wykonywać (np. pliki .php, .phtml). Skonfiguruj serwer tak, aby traktował te pliki jako dane statyczne.
  6. Ustawiaj odpowiednie uprawnienia dla katalogu docelowego: Katalog, do którego przenosisz pliki, musi być zapisywalny dla procesu PHP, ale niekoniecznie dla wszystkich użytkowników serwera.
  7. Skanuj pliki antywirusem: Jeśli to możliwe, skanuj przesyłane pliki za pomocą oprogramowania antywirusowego na serwerze.
  8. Ograniczaj typy plików: Zezwalaj tylko na te typy plików, które są niezbędne dla Twojej aplikacji. Używaj białej listy (whitelist) dozwolonych rozszerzeń i typów MIME, a nie czarnej listy (blacklist) niedozwolonych.

Konfiguracja PHP związana z Przesyłaniem Plików (php.ini)

Istnieje kilka dyrektyw w php.ini, które wpływają na proces przesyłania plików:

Pamiętaj, że po zmianie tych wartości w php.ini, konieczne jest zrestartowanie serwera WWW (np. Apache, Nginx) lub procesu PHP-FPM.

Przesyłanie Wielu Plików Jednocześnie

HTML5 pozwala na przesyłanie wielu plików za pomocą jednego pola <input type="file">, dodając do niego atrybut multiple:

<input type="file" name="moje_pliki[]" multiple>

Zwróć uwagę na dodanie [] do atrybutu name (moje_pliki[]). Dzięki temu PHP potraktuje dane z tego pola jako tablicę w $_FILES. Struktura $_FILES["moje_pliki"] będzie wtedy inna:

Aby przetworzyć te pliki, należy iterować po jednej z tych tablic (np. po $_FILES["moje_pliki"]["name"]) i dla każdego indeksu odczytywać odpowiednie informacje z pozostałych tablic.

<?php
// Przykład obsługi wielu plików (fragment skryptu)
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_FILES["moje_pliki"])) {
    $liczbaPlikow = count($_FILES["moje_pliki"]["name"]);
    echo "Otrzymano $liczbaPlikow plików.<br>";

    for ($i = 0; $i < $liczbaPlikow; $i++) {
        echo "<h4>Przetwarzanie pliku #" . ($i + 1) . "</h4>";
        if ($_FILES["moje_pliki"]["error"][$i] === UPLOAD_ERR_OK) {
            $oryginalnaNazwa = $_FILES["moje_pliki"]["name"][$i];
            $tymczasowaSciezka = $_FILES["moje_pliki"]["tmp_name"][$i];
            $rozmiar = $_FILES["moje_pliki"]["size"][$i];

            echo "Nazwa: " . htmlspecialchars($oryginalnaNazwa) . ", Rozmiar: $rozmiar bajtów<br>";
            // Tutaj logika walidacji i przenoszenia dla każdego pliku, podobnie jak dla pojedynczego pliku
            // $sciezkaDocelowa = "uploads/" . time() . "_" . basename($oryginalnaNazwa);
            // if (move_uploaded_file($tymczasowaSciezka, $sciezkaDocelowa)) {
            //     echo "Plik " . htmlspecialchars($oryginalnaNazwa) . " przesłany pomyślnie.<br>";
            // } else {
            //     echo "Błąd przy przenoszeniu pliku " . htmlspecialchars($oryginalnaNazwa) . ".<br>";
            // }
        } else {
            echo "Błąd przesyłania pliku #" . ($i + 1) . ": kod " . $_FILES["moje_pliki"]["error"][$i] . "<br>";
        }
    }
}
?>

Podsumowanie Lekcji

W tej lekcji nauczyliśmy się, jak obsługiwać przesyłanie plików na serwer w PHP. Dowiedzieliśmy się, jak skonfigurować formularz HTML z enctype="multipart/form-data" i polem <input type="file">. Zgłębiliśmy strukturę superglobalnej tablicy $_FILES oraz znaczenie kodów błędów. Przeanalizowaliśmy kluczowe kroki przetwarzania przesłanego pliku, w tym walidację rozmiaru i typu, generowanie bezpiecznej nazwy oraz przenoszenie pliku za pomocą move_uploaded_file(). Podkreśliliśmy fundamentalne zasady bezpieczeństwa, które należy stosować przy obsłudze plików od użytkowników. Omówiliśmy również odpowiednie dyrektywy konfiguracyjne PHP oraz sposób obsługi przesyłania wielu plików jednocześnie.

Umiejętność bezpiecznego zarządzania przesyłanymi plikami jest niezbędna w wielu aplikacjach webowych. W następnej lekcji skupimy się bardziej szczegółowo na zaawansowanych technikach filtrowania i walidacji danych wejściowych w PHP.


Zadanie praktyczne

Stwórz prostą galerię zdjęć.

  1. Utwórz formularz HTML (galeria_form.html) pozwalający użytkownikowi przesłać plik obrazu (JPG, PNG) oraz podać jego tytuł (pole tekstowe). Ustaw limit rozmiaru pliku na 1MB (MAX_FILE_SIZE).
  2. Napisz skrypt PHP (upload_zdjecia.php), który:
    • Odbiera przesłany plik i tytuł.
    • Sprawdza błędy przesyłania.
    • Waliduje plik: czy rozmiar nie przekracza 1MB, czy rozszerzenie to JPG lub PNG, oraz czy typ MIME (sprawdzony na serwerze za pomocą finfo_file) to image/jpeg lub image/png.
    • Jeśli walidacja przejdzie pomyślnie, generuje unikalną nazwę dla pliku (np. na podstawie timestampa i oryginalnego rozszerzenia) i przenosi go do podkatalogu galeria_zdjec/ (upewnij się, że katalog istnieje i jest zapisywalny).
    • Zapisuje informację o zdjęciu (tytuł i nowa nazwa pliku) do pliku tekstowego opisy_zdjec.txt (każde zdjęcie w nowej linii, np. TytulObrazka;nazwa_pliku.jpg).
    • Wyświetla komunikat o sukcesie lub błędzie.
  3. Napisz drugi skrypt PHP (pokaz_galerie.php), który odczytuje plik opisy_zdjec.txt, a następnie dla każdego wpisu wyświetla tytuł oraz obrazek (<img src="galeria_zdjec/nazwa_pliku.jpg" alt="TytulObrazka">).

Pokaż przykładowe rozwiązanie

galeria_form.html

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Dodaj Zdjęcie do Galerii</title>
</head>
<body>
    <h2>Dodaj Nowe Zdjęcie</h2>
    <form action="upload_zdjecia.php" method="POST" enctype="multipart/form-data">
        <p>
            <label for="tytul_zdjecia">Tytuł zdjęcia:</label><br>
            <input type="text" id="tytul_zdjecia" name="photo_title" required>
        </p>
        <p>
            <label for="plik_zdjecia">Wybierz plik (JPG, PNG, maks. 1MB):</label><br>
            <input type="file" id="plik_zdjecia" name="photo_file" required>
        </p>
        <input type="hidden" name="MAX_FILE_SIZE" value="1048576" /> <!-- 1MB -->
        <p>
            <input type="submit" value="Dodaj Zdjęcie">
        </p>
    </form>
    <hr>
    <p><a href="pokaz_galerie.php">Zobacz Galerię</a></p>
</body>
</html>
            

upload_zdjecia.php

<?php
$katalogGalerii = "galeria_zdjec/";
$plikOpisow = "opisy_zdjec.txt";
$komunikatyUpload = [];

if (!is_dir($katalogGalerii)) {
    mkdir($katalogGalerii, 0755, true);
}

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $tytulZdjecia = isset($_POST["photo_title"]) ? htmlspecialchars(trim($_POST["photo_title"])) : "Bez tytułu";

    if (isset($_FILES["photo_file"]) && $_FILES["photo_file"]["error"] === UPLOAD_ERR_OK) {
        $plik = $_FILES["photo_file"];
        $maxRozmiarPliku = 1 * 1024 * 1024; // 1MB

        if ($plik["size"] > $maxRozmiarPliku) {
            $komunikatyUpload[] = "BŁĄD: Plik jest za duży (maks. 1MB).";
        } elseif ($plik["size"] == 0) {
            $komunikatyUpload[] = "BŁĄD: Plik jest pusty.";
        } else {
            $oryginalnaNazwa = $plik["name"];
            $rozszerzenie = strtolower(pathinfo($oryginalnaNazwa, PATHINFO_EXTENSION));
            $dozwoloneRozszerzenia = ["jpg", "jpeg", "png"];

            if (!in_array($rozszerzenie, $dozwoloneRozszerzenia)) {
                $komunikatyUpload[] = "BŁĄD: Niedozwolone rozszerzenie pliku. Tylko JPG, JPEG, PNG.";
            } else {
                $finfo = finfo_open(FILEINFO_MIME_TYPE);
                $typMIME = finfo_file($finfo, $plik["tmp_name"]);
                finfo_close($finfo);
                $dozwoloneTypyMIME = ["image/jpeg", "image/png"];

                if (!in_array($typMIME, $dozwoloneTypyMIME)) {
                    $komunikatyUpload[] = "BŁĄD: Niedozwolony typ MIME pliku ($typMIME).";
                } else {
                    $nowaNazwaPliku = uniqid("img_", true) . "." . $rozszerzenie;
                    $sciezkaDocelowa = $katalogGalerii . $nowaNazwaPliku;

                    if (move_uploaded_file($plik["tmp_name"], $sciezkaDocelowa)) {
                        $wpisDoPliku = htmlspecialchars($tytulZdjecia) . ";" . htmlspecialchars($nowaNazwaPliku) . PHP_EOL;
                        if (file_put_contents($plikOpisow, $wpisDoPliku, FILE_APPEND | LOCK_EX) !== false) {
                            $komunikatyUpload[] = "SUKCES: Zdjęcie ".htmlspecialchars($tytulZdjecia)." zostało dodane do galerii.";
                        } else {
                            $komunikatyUpload[] = "BŁĄD: Zdjęcie przesłane, ale nie udało się zapisać opisu.";
                            // Potencjalnie usunąć przesłany plik, jeśli zapis opisu jest krytyczny
                            // if(file_exists($sciezkaDocelowa)) unlink($sciezkaDocelowa);
                        }
                    } else {
                        $komunikatyUpload[] = "BŁĄD KRYTYCZNY: Nie udało się przenieść pliku zdjęcia.";
                    }
                }
            }
        }
    } elseif (isset($_FILES["photo_file"])) {
        $komunikatyUpload[] = "BŁĄD PRZESYŁANIA: Kod błędu: " . $_FILES["photo_file"]["error"];
    }
}

echo "<h1>Dodawanie Zdjęcia - Wynik</h1>";
if (!empty($komunikatyUpload)) {
    echo "<ul>";
    foreach ($komunikatyUpload as $k) echo "<li>$k</li>";
    echo "</ul>";
}
echo "<p><a href=\"galeria_form.html\">Dodaj kolejne zdjęcie</a> | <a href=\"pokaz_galerie.php\">Zobacz Galerię</a></p>";
?>
            

pokaz_galerie.php

<?php
$katalogGalerii = "galeria_zdjec/";
$plikOpisow = "opisy_zdjec.txt";

echo "<h1>Galeria Zdjęć</h1>";

if (file_exists($plikOpisow)) {
    $wpisy = file($plikOpisow, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    if ($wpisy !== false && count($wpisy) > 0) {
        echo "<div style=\"display: flex; flex-wrap: wrap;\">";
        foreach ($wpisy as $wpis) {
            list($tytul, $nazwaPliku) = explode(";", $wpis, 2);
            $tytul = htmlspecialchars_decode($tytul); // Odwrócenie htmlspecialchars dla wyświetlenia
            $nazwaPliku = htmlspecialchars_decode($nazwaPliku);
            $sciezkaZdjecia = $katalogGalerii . $nazwaPliku;

            if (file_exists($sciezkaZdjecia)) {
                echo "<div style=\"margin: 10px; border: 1px solid #ccc; padding: 5px; text-align: center;\">";
                echo "<h3>" . htmlspecialchars($tytul) . "</h3>";
                echo "<img src=\"" . htmlspecialchars($sciezkaZdjecia) . "\" alt=\"" . htmlspecialchars($tytul) . "\" style=\"max-width: 200px; max-height: 200px;\">";
                echo "</div>";
            } else {
                 echo "<p style=\"color:red;\">Nie znaleziono pliku zdjęcia: ".htmlspecialchars($nazwaPliku)." dla tytułu: ".htmlspecialchars($tytul)."</p>";
            }
        }
        echo "</div>";
    } else {
        echo "<p>Galeria jest pusta lub nie udało się odczytać opisów.</p>";
    }
} else {
    echo "<p>Galeria jest pusta (brak pliku opisów).</p>";
}

echo "<hr><p><a href=\"galeria_form.html\">Dodaj zdjęcie do galerii</a></p>";
?>
            

Zadanie do samodzielnego wykonania

Rozbuduj powyższą galerię o następujące funkcjonalności:

  1. Dodaj możliwość usuwania zdjęć z galerii. W skrypcie pokaz_galerie.php, obok każdego zdjęcia, dodaj link lub przycisk "Usuń". Kliknięcie powinno prowadzić do skryptu usun_zdjecie.php, który przyjmuje jako parametr (np. GET) nazwę pliku do usunięcia.
  2. Skrypt usun_zdjecie.php powinien usunąć plik zdjęcia z katalogu galeria_zdjec/ oraz odpowiedni wpis z pliku opisy_zdjec.txt. Pamiętaj o bezpieczeństwie – waliduj nazwę pliku, upewnij się, że nie zawiera prób obejścia katalogu (np. ../).
  3. Zabezpiecz operację usuwania, np. poprzez wyświetlenie potwierdzenia przed faktycznym usunięciem.


FAQ - Przesyłanie Plików na Serwer

Dlaczego $_FILES["moj_plik"]["type"] nie jest wiarygodne?

Typ MIME w $_FILES jest wysyłany przez przeglądarkę klienta i może być łatwo sfałszowany. Zawsze należy weryfikować typ pliku na serwerze, np. za pomocą rozszerzenia Fileinfo (finfo_file) lub sprawdzając rozszerzenie pliku (choć to drugie też może być mylące).

Jaka jest różnica między upload_max_filesize a post_max_size?

upload_max_filesize to limit rozmiaru dla pojedynczego przesyłanego pliku. post_max_size to limit dla całkowitego rozmiaru danych wysyłanych w żądaniu POST (w tym wszystkich plików i innych pól formularza). post_max_size musi być większe lub równe upload_max_filesize.

Czy mogę zmienić ustawienia php.ini dotyczące uploadu w skrypcie za pomocą ini_set()?

Niektórych dyrektyw, jak upload_max_filesize czy post_max_size, nie można zmienić za pomocą ini_set(), ponieważ muszą być one znane przed rozpoczęciem przetwarzania żądania POST. Należą one do kategorii PHP_INI_PERDIR, co oznacza, że mogą być ustawione tylko w php.ini, httpd.conf, lub plikach .htaccess (jeśli serwer na to pozwala).

Jak najlepiej generować unikalne i bezpieczne nazwy dla przesyłanych plików?

Unikaj używania oryginalnej nazwy pliku. Dobrym podejściem jest generowanie nazwy na podstawie np. skrótu (hash) zawartości pliku, unikalnego identyfikatora (uniqid()), timestampa, lub kombinacji tych elementów z bezpiecznym rozszerzeniem pliku pobranym z oryginalnej nazwy lub na podstawie wykrytego typu MIME.

Co zrobić, jeśli move_uploaded_file() zwraca false?

Jeśli move_uploaded_file() zawiedzie, może to oznaczać problemy z uprawnieniami do zapisu w katalogu docelowym, nieprawidłową ścieżkę docelową, lub że plik tymczasowy już nie istnieje (np. skrypt działał zbyt długo). Sprawdź logi błędów PHP i serwera oraz uprawnienia katalogu.

Jak wyświetlić pasek postępu przesyłania pliku?

Standardowy PHP po stronie serwera nie dostarcza łatwego sposobu na śledzenie postępu w czasie rzeczywistym dla klienta. Do implementacji pasków postępu często używa się JavaScript (np. z wykorzystaniem XMLHttpRequest Level 2 lub Fetch API do asynchronicznego przesyłania i monitorowania zdarzeń postępu) lub specjalnych rozszerzeń PHP jak APC (dla starszych wersji) lub OPCache z odpowiednią konfiguracją.

Czy mogę przesłać plik bez formularza HTML, np. przez API?

Tak, pliki można przesyłać programistycznie, np. używając cURL w PHP do wysłania żądania POST z danymi multipart/form-data do innego skryptu lub API. Klient (np. aplikacja mobilna, inny serwer) musi odpowiednio sformatować żądanie.