Wysyłanie plików na serwer (upload) -- obsługa w PHP | #! Shebang

Obsługa wysyłania plików na serwer w PHP

Za pomocą formularzy HTML do serwera można wysyłać nie tylko dane tekstowe lub liczbowe, lecz także pliki dowolnego typu. Kiedy dany plik znajdzie się na serwerze, za pomocą PHP można go sprawdzić i przetworzyć na różne sposoby oraz przenieść do wybranego katalogu. W tym rozdziale kursu dowiesz się, jak utworzyć formularz do wysyłania plików na serwer oraz jak obsługiwać te pliki na serwerze za pomocą skryptów PHP.

W tym rozdziale poznasz odpowiedzi między innymi na następujące pytania:

  • Do czego służy plik php.ini i jakie opcje związane z odbieraniem plików na serwerze można w nim ustawić?
  • Jak utworzyć formularz HTML do wysyłania plików na serwer?
  • Gdzie w skrypcie PHP szukać informacji dotyczących pliku przesłanego na serwer?
  • Jak umieścić przesłany przez użytkownika plik w wybranym katalogu na serwerze?
  • Jak zabezpieczyć przesyłanie plików na serwer i odbieranie ich na serwerze?
Ten rozdział zaczniemy od zapoznania się z bardzo ważnym plikiem o nazwie php.ini, który zawiera wiele istotnych ustawień konfiguracyjnych PHP.

Plik php.ini

Plik php.ini to główny plik konfiguracyjny PHP, który zawiera domyślne globalne ustawienia dla całego środowiska PHP.

Określa on między innymi limity pamięci PHP, maksymalny rozmiar przesyłanych plików, ustawienia dotyczące pokazywania i rejestrowania błędów, maksymalny czas wykonywania skryptów i wiele innych ustawień.

Plik php.ini umożliwia administratorowi serwera dostosowywanie globalnych parametrów działania PHP, czyli dotyczących wszystkich aplikacji, które znajdują się na serwerze.

Zawiera on specjalne dyrektywy o składni nazwa_dyrektywy=wartość. Na przykład dyrektywa memory_limit=512M oznacza, że jeden skrypt może wykorzystać maksymalnie 512 megabajtów pamięci serwera.

Ustawienia środowiska PHP można zmieniać nie tylko w pliku php.ini, ale także w dwóch innych miejscach — w specjalnym pliku .user.ini oraz w skryptach, za pomocą funkcji ini_set().

Jednak tylko plik php.ini daje dostęp do wszystkich ustawień. Pozostałe dwie metody pozwalają na używanie tylko niektórych dyrektyw. Ogólnie działa to w następujący sposób:

  1. Najpierw PHP wczytuje plik php.ini.
  2. Następnie PHP wczytuje plik .user.ini, jeśli jest on obecny, i nadpisuje zawartymi w nim ustawieniami te ustawienia pliku php.ini, które można zmieniać w pliku .user.ini.
  3. Na koniec PHP zaczyna wykonywać skrypt i stosuje ewentualne ustawienia zdefiniowane za pomocą funkcji ini_set(), jeśli ich modyfikacja w ten sposób jest dozwolona.

Teraz pewnie zastanawiasz się, gdzie znajduje się plik php.ini, gdzie jest położony plik .user.ini oraz skąd wiadomo, które dyrektywy można modyfikować w określonych miejscach. Już wyjaśniam.

Gdzie szukać plików php.ini i .user.ini

Umiejscowienie pliku php.ini zależy od sposobu instalacji i wersji PHP oraz typu i wersji serwera. Jeśli na przykład na serwerze Apache zainstalowano PHP 8.4 w trybie FPM, to plik php.ini będzie prawdopodobnie znajdował się w katalogu /etc/php/8.4/fpm.

Najłatwiejszym sposobem na sprawdzenie lokalizacji tego pliku jest użycie funkcji php_ini_loaded_file():


echo php_ini_loaded_file();

Inny sposobem jest wywołanie funkcji phpinfo() i poszukanie w jej wynikach pozycji Loaded Configuration File (załadowany plik konfiguracyjny), która określa ścieżkę do pliku php.ini. Na przykład w moim środowisku testowym ścieżka do tego pliku to C:\xampp\php\php.ini.

Z punktu widzenia zwykłego użytkownika serwera, czyli użytkownika hostingu, większe znaczenie ma plik .user.ini, ponieważ do pliku php.ini dostęp ma tylko administrator serwera.

Gdzie szukać tego pliku? Tam gdzie go umieścisz! Tak naprawdę plików .user.ini może być wiele, nawet po jednym w każdym katalogu.

Najpierw PHP szuka go w katalogu, w którym znajduje się uruchamiany skrypt, następnie w katalogach nadrzędnych i tak aż do katalogu głównego.

PHP wczytuje każdy plik .user.ini napotkany po drodze i jeśli ta sama dyrektywa występuje w kilku z nich, to zastosowanie ma ta, która znajduje się w pliku najbliższym katalogowi zawierającemu skrypt.

No dobrze, to teraz skąd wiadomo, których dyrektyw można używać w plikach .user.ini albo w skryptach PHP za pośrednictwem funkcji ini_set()?

Skąd wiadomo, których dyrektyw można używać

Aby dowiedzieć się, czy danej dyrektywy można użyć w określonym miejscu, najlepiej jest to sprawdzić w dokumentacji PHP. Na stronie https://www.php.net/manual/en/ini.list.php znajduje się wykaz wszystkich dyrektyw pliku php.ini wraz z informacjami, gdzie każda z nich może być używana.

Zakres dostępności każdej dyrektywy określa specjalna stała ukazana w kolumnie Changeable w tabeli na wymienionej stronie. Istnieją cztery takie stałe:

INI_USER
Dyrektywę można ustawiać w skryptach użytkownika (np. za pomocą funkcji ini_set()), w pliku .user.ini oraz w rejestrze systemu Windows
INI_PERDIR
Dyrektywę można ustawiać w plikach php.ini, .htaccess, httpd.conf oraz .user.ini.
INI_SYSTEM
Dyrektywę można ustawiać w plikach php.ini i httpd.conf
INI_ALL
Dyrektywę można ustawiać wszędzie.

No dobrze, ale po co w ogóle o tym wszystkim teraz opowiadam? Bo przy obsłudze wysyłania plików na serwer przez PHP znaczenie ma kilka dyrektyw konfiguracyjnych środowiska PHP.

Dyrektywy konfiguracji PHP istotne przy obsłudze wysyłania plików na serwer

Jeśli będziesz korzystać z usługi hostingu współdzielonego (polecam np. SeoHost.pl), to będziesz mieć możliwość zmiany wartości tylko niektórych dyrektyw konfiguracyjnych środowiska PHP.

Większość ustawień mających znaczenie przy obsłudze w PHP plików wysyłanych na serwer jest dostępnych do modyfikacji także dla użytkownika. W poniższej tabeli znajdziesz ich zestawienie wraz z informacją, gdzie daną dyrektywę można ustawiać.

Dyrektywa Konfiguracja Opis
upload_max_filesize INI_PERDIR Maksymalny rozmiar jednego pliku
post_max_size INI_PERDIR Maksymalny rozmiar przesyłanych danych, włącznie z przesyłanymi plikami. Wartość tej dyrektywy powinna być większa od wartości dyrektywy upload_max_filesize
max_file_uploads INI_SYSTEM Maksymalna liczba plików, które można przesłać jednocześnie w jednym formularzu
max_input_time INI_PERDIR Maksymalna liczba sekund, jaką PHP czeka na odebranie danych wejściowych. Domyślna wartość -1 oznacza, że używana jest wartość dyrektywy max_execution_time. Aby wyłączyć ograniczenie czasowe, należy nadać wartość 0
memory_limit INI_ALL Limit pamięci w bajtach dla jednego skryptu PHP. Wartość tej dyrektywy powinna być większa od wartości dyrektywy post_max_size
max_execution_time INI_ALL Maksymalna liczba sekund działania skryptu zanim zostanie zakończony przez serwer
file_uploads INI_SYSTEM Określa, czy dozwolone jest przesyłanie plików przez protokół HTTP. Jest to dyrektywa logiczna, a więc przyjmująca wartość true lub false
upload_tmp_dir INI_SYSTEM Katalog tymczasowy, w którym PHP zapisuje przesyłane pliki.
Musi mieć odpowiednie uprawnienia do zapisu przez użytkownika PHP.

Jeśli podczas testów przesyłania plików na serwer coś nie będzie poprawnie działać, być może winę za to będą ponosić powyższe dyrektywy. Nie musisz uczyć się ich na pamięć, ale dobrze jest wiedzieć o ich istnieniu, aby wiedzieć, gdzie szukać możliwej przyczyny niepowodzeń.

Teorię mamy za sobą, więc możemy przejść do praktyki. Zaczniemy od utworzenia formularza zawierającego pole wyboru pliku do przesłania na serwer.

Formularz wysyłania plików na serwer

Aby wysłać na serwer jakikolwiek plik ze strony internetowej, należy odpowiednio przygotować formularz i umieścić w nim specjalny element do wybierania plików z dysku komputera.

Jeśli chodzi o formularz, to przede wszystkim należy pamiętać o dwóch sprawach:

  • Formularze do wysyłania plików muszą być przesyłane metodą HTTP POST (metoda GET nie zadziała).
  • Formularz wysyłający pliki do serwera musi mieć atrybut enctype o wartości multipart/form-data.

Krótko mówiąc, element form poprawnie zdefiniowany do przesyłania plików do serwera powinien wyglądać tak:


<form enctype="multipart/form-data" action="./skrypt.php" method="post">
</form>

Element ten to pierwsza, część historii, ale jest jeszcze druga, czyli kontrolka wyboru pliku z dysku komputera użytkownika. Kontrolkę tę tworzy się w bardzo prosty sposób, za pomocą elementu input typu file, np.:


<input type="file" name="plik">

Atrybut type o wartości file oznacza, że jest to właśnie kontrolka do wyboru pliku z dysku komputera. Wartość atrybutu name umożliwi nam odebranie tego pliku w skrypcie PHP na serwerze.

Opcjonalnie w elemencie intput można jeszcze określić akceptowane typy plików, czyli tzw. typy MIME np.:


<input type="file" accept="image/png, image/jpeg, image/webp, image/avif" name="plik">

Pamiętaj, że atrybut accept jest tylko udogodnieniem dla użytkownika, ponieważ w żaden sposób nie powstrzymuje go przed wybraniem plików innego typu niż wymienione na liście. Różnica jest tylko taka, że kiedy użytkownik kliknie przycisk Przeglądaj..., to w oknie wysyłania pliku domyślnie będą wyświetlane tylko te typy plików, które zostały określone w tym atrybucie (spójrz na poniższy zrzut ekranu).

php file input zrzut ekranu typów plików
Domyślny wybór tylko obsługiwanych plików w przeglądarce nie powstrzyma użytkownika przed wybraniem opcji Wszystkie pliki i wybraniem dowolnego innego typu pliku

Wiesz już, jak utworzyć formularz internetowy do przesyłania plików, więc teraz pokażę ci, jak odbierać te pliki na serwerze. Poniżej przedstawiam kompletny formularz HTML, którego będę używał w ramach przykładu:


<form enctype="multipart/form-data" action="/skrypt.php" method="post">
  <label for="plik">Wybierz plik:</label>
  <input type="file" accept="image/png, image/jpeg, image/webp, image/avif" name="plik" id="plik">
  <input type="submit" value="Wyślij">
</form>

Odbieranie pliku na serwerze w PHP

Wszystkie informacje potrzebne do odebrania na serwerze pliku lub plików przesłanych przez formularz HTML znajdują się w tablicy superglobalnej $_FILES. Zazwyczaj jest to dwuwymiarowa tablica asocjacyjna, w której każdy wymiar zawiera dane jednego z przesłanych plików.

Zobaczmy, jakie konkretnie dane można znaleźć w tablicy $_FILES dla pliku przesłanego przez formularz internetowy:

  • atrybut_name (typ array) — nazwa tablicy zawierającej dane dotyczące pliku przesłanego za pomocą pola o tej nazwie. Jest to wartość atrybutu name danego pola, a więc w naszym przykładzie byłaby to nazwa plik.
  • name (typ string) — oryginalna nazwa pliku z komputera użytkownika.
  • full_path (typ string) — pełna ścieżka do przesłanego pliku na komputerze użytkownika. Ten element został dodany w PHP 8.1 i nie wszystkie przeglądarki w pełni go obsługują, np. niektóre przesyłają tylko nazwę pliku, a nie pełną ścieżkę.
  • type (typ string) — typ MIME przesłanego pliku.
  • tmp_name (typ string) — ścieżka do tymczasowego pliku, który reprezentuje przesyłany plik na serwerze.
  • error (typ int) — kod błędu przesyłania pliku. Jeśli plik został przesłany bezbłędnie, to kod ten ma wartość 0.
  • size (typ int) — rozmiar w bajtach pliku przesłanego na serwer.

Gdybyśmy więc za pomocą naszego przykładowego formularza przesłali na serwer plik o nazwie testy.jpg, to zawartość tablicy $_FILES moglibyśmy wyświetlić za pomocą instrukcji print_r($_FILES);. Wyglądałaby ona tak:


Array
(
  [plik] => Array
    (
       [name] => testy.jpg
       [full_path] => testy.jpg
       [type] => image/jpeg
       [tmp_name] => C:\xampp\tmp\php49C6.tmp
       [error] => 0
       [size] => 45340
    )
)

Jak widać, plik został przesłany i możemy się nim zająć na serwerze. Aby nie wpaść w jakąś pułapkę albo nie przetwarzać nieodpowiedniego typu pliku w niewłaściwy sposób, najpierw sprawdzimy, czy plik ten jest w interesującym nas formacie.

Weryfikacja typu pliku w PHP — metoda prostacka

Przypomnę, że w atrybucie accept elementu intput zaznaczyliśmy, że interesują nas tylko takie typy plików: image/png, image/jpeg, image/webp, image/avif.

Choć nie mogliśmy zmusić użytkownika do wybrania tylko pliku interesującego nas typu, to możemy sobie to teraz odbić na serwerze i odrzucić jego plik, jeśli nie będzie nam odpowiadał.

W tym celu najpierw utworzymy tablicę dozwolonych typów MIME, a następnie za pomocą funkcji in_array() sprawdzimy, czy rzeczywisty typ przesłanego pliku jest jednym z nich:


$dozwolone_typy = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'];

if (!in_array($_FILES['plik']['type'], $dozwolone_typy)) {
  exit('<p>Niewłaściwy typ pliku.</p>');
}

Ta metoda ma jednak poważną wagę pod względem bezpieczeństwa: element tablicy $_FILES['plik']['type'] zawiera informację przesłaną do serwera przez przeglądarkę, a więc od użytkownika, co oznacza, że ktoś nieprzyjaźnie do nas nastawiony może ją zmanipulować, aby narobić nam kłopotów.

Weryfikacja typu pliku w PHP — funkcja mime_content_type()

Jeśli chcesz zwiększyć poziom bezpieczeństwa swojego skryptu, koniecznie zawsze sprawdzaj typ MIME pliku przesłanego przez formularz za pomocą odpowiedniej funkcji PHP.

Dlaczego to takie ważne? Aby się przekonać, weź dowolny plik w formacie, którego Twój skrypt „nie obsługuje” i zmień jego rozszerzenie np. na jpg, a następnie prześlij ten plik przez swój formularz. Okaże się, że taki „oszukaniec” przejdzie bez żadnego problemu. A to tylko najbardziej prymitywna sztuczka, jaką udało mi się wymyślić. Inni są o wiele bardziej pomysłowi w kwestii tego, jak dokuczyć programistom formularzy.

Aby bezpieczniej sprawdzać typ MIME pliku przesłanego na serwer, można posłużyć się funkcją mime_content_type(). Poniżej przedstawiam ulepszoną wersję powyższego kodu:


if (!in_array(mime_content_type($_FILES['plik']['tmp_name']), $dozwolone_typy)) {
  exit('<p>Niewłaściwy typ pliku.</p>');
}

Funkcja PHP mime_content_type() jest bezpieczniejsza, ponieważ nie opiera się na danych otrzymanych od przeglądarki, tylko sama sprawdza typ MIME pliku.

Jeśli chcesz tylko sprawdzić typ MIME pliku przesłanego na serwer, to funkcja mime_content_type() jest bardzo dobrym wyborem. Jeśli potrzebujesz czegoś bardziej rozbudowanego, to możesz użyć obiektu klasy finfo, ale techniki obiektowe poznasz nieco później.

Weryfikacja typu pliku w PHP — trójca finfo_open(), finfo_close(), finfo_file()

Innym sposobem na sprawdzenie typu MIME pliku jest użycie funkcji finfo_file(). Jest ona trochę nowsza od funkcji mime_content_type() i nieco bardziej od niej skomplikowana.

Na dodatek w starszych wersjach PHP (przed PHP 8) powinna występować w towarzystwie dwóch innych funkcji: finfo_open() i finfo_close(). Wiąże się to z tym, że dawniej funkcja finfo_open() zwracała zasób, który następnie trzeba było zamknąć za pomocą funkcji finfo_close().

Od PHP 8 funkcja finfo_open() zwraca obiekt klasy finfo, który podlega automatycznemu usuwaniu, więc nie trzeba już pamiętać o wywoływaniu funkcji finfo_close().

Krótko mówiąc, we wcześniejszych wersjach PHP wzorzec obejmujący funkcje finfo_open(), finfo_close() i finfo_file() był często spotykany. W nowocześniejszych wersjach PHP odchodzi się od tej proceduralnej techniki na rzecz technik obiektowych, o których będzie mowa w dalszej części kursu.

Dlatego poniżej zamieszczam opis tej starej techniki z zastrzeżeniem, że ma on już bardziej wartość historyczną — jeśli kiedyś napotkasz coś takiego w starszym kodzie, będziesz wiedzieć, o co w tym chodzi.

Ogólny schemat procesu sprawdzania typu pliku przy użyciu tych funkcji jest następujący:

  1. Wywołanie funkcji finfo_open(), aby uzyskać zasób fileinfo.
  2. Wywołanie funkcji finfo_file() na otrzymanym zasobie (jeśli udało się go otrzymać), aby uzyskać interesujące nas informacje.
  3. Wywołanie funkcji finfo_close(), aby zamknąć już niepotrzebny zasób.

Poniżej znajduje się kod analogiczny do poprzedniego, tylko z użyciem omawianych tu funkcji PHP:


$finfo = finfo_open(FILEINFO_MIME_TYPE);

if (!$finfo ) {
  exit('Błąd.');
}

if (!in_array(finfo_file($finfo, $_FILES['plik']['tmp_name']), $dozwolone_typy)) {
  exit('<p>Niewłaściwy typ pliku.</p>');
}
finfo_close($finfo);

Jak widać, ten kod jest bardziej skomplikowany:

  1. W wierszu 1 tworzymy zasób fileinfo (pod warunkiem, że używamy PHP w wersji starszej od PHP 8).
  2. W wierszu 3 sprawdzamy, czy zasób został utworzony.
  3. W wierszu 7 sprawdzamy, czy plik ma akceptowany przez nas typ MIME.

Kiedy już się upewnimy, że użytkownik nie nadużywa naszej uprzejmości i grzecznie przesłał plik właściwego typu, dobrze jest też się upewnić, że nie wysyła nam czegoś o gigantycznym rozmiarze tylko po to, żeby pognębić nasz serwer.

Weryfikacja rozmiaru pliku w PHP

Kontrolę rozmiaru pliku można zaimplementować na dwa sposoby. Pierwszy jest czymś w rodzaju testu przesiewowego w HTML, który użytkownik może łatwo „oszukać”, ale warto go stosować, bo nie każdy przecież ma złe zamiary i wtedy wyeliminujemy błędy jeszcze zanim cokolwiek dotrze do serwera.

Metoda, którą mam na myśli, polega na dodaniu do formularza HTML ukrytego pola input o nazwie MAX_FILE_SIZE i wartości reprezentującej maksymalny dopuszczany przez nas rozmiar pliku:


<input type="hidden" name="MAX_FILE_SIZE" value="10485760">

To pole oznacza, że maksymalny rozmiar przesyłanego pliku to 10485760 bajtów, czyli 10 megabajtów. Oczywiście wartość MAX_FILE_SIZE można zapisać też małymi literami, ale za dokumentacją PHP utarł się taki zapis, a zawsze lepiej jest trzymać się powszechnie przyjętych konwencji.

I jeszcze jedna ważna uwaga: ukryty element określający maksymalny dopuszczalny rozmiar pliku musi być umieszczony przed elementem wyboru pliku z komputera. Czyli teraz nasz formularz HTML wygląda tak:


<form enctype="multipart/form-data" action="/skrypt.php" method="post">
  <label for="plik">Wybierz plik:</label>
  <input type="hidden" name="MAX_FILE_SIZE" value="10485760">
  <input type="file" accept="image/png, image/jpeg, image/webp, image/avif" name="plik" id="plik">
  
  <input type="submit" value="Wyślij">
</form>

Jeśli użytkownik wybierze zbyt duży plik i spróbuje go przesłać za pomocą takiego formularza, to w większości przeglądarek plik ten nie zostanie wysłany do serwera. Dodatkowo w elemencie error tablicy $_FILES pojawi się wartość 2, oznaczająca niewłaściwy rozmiar przesłanych danych (UPLOAD_ERR_FORM_SIZE).

To jednak jest tylko pierwsza linia naszych zasieków obronnych, które mają nas chronić przed niecnymi zakusami złośliwych użytkowników. Teraz przejdziemy do drugiej, ważniejszej.

Jako że użytkownik strony internetowej może swobodnie mieszać w jej kodzie HTML, nie możemy w pełni ufać temu, co znajduje się w elemencie size tablicy $_FILES. Jest ku temu kilka powodów, między innymi:

  • Element tablicy $_FILES['plik']['size'] będzie zawierał rozmiar przesłanego pliku tylko wtedy, gdy PHP faktycznie odbierze bez błędów ten plik w całości.
  • Jeśli rozmiar przesyłanego pliku przekroczy wartość dyrektywy PHP upload_max_filesize, to nie zostanie on zapisany w katalogu tymczasowym, w efekcie czego element $_FILES['plik']['size'] będzie miał wartość 0, a element $_FILES['plik']['error'] będzie zawierał kod 1, oznaczający niewłaściwy rozmiar pliku (UPLOAD_ERR_INI_SIZE).
  • Jeśli z kolei przesyłany plik będzie mieścił się w limicie określonym w pliku php.ini, ale będzie większy od wartości MAX_FILE_SIZE (którą użytkownik może łatwo zmanipulować) to może być różnie — wszystko zależy od zachowania przeglądarki, więc lepiej na tym nie polegać.

Przechodząc do rzeczy, na serwerze koniecznie osobno sprawdź, jaki jest rozmiar pliku tymczasowego otrzymanego z formularza. Do tego celu możesz użyć funkcji PHP filesize():

$maksymalny_rozmiar = 10485760;

if (filesize($_FILES['plik']['tmp_name']) > $maksymalny_rozmiar) {
  exit('<p>Za duży plik!</p>');
}

W trzecim wierszu tego skryptu za pomocą funkcji filesize() sprawdzamy rozmiar tymczasowego pliku utworzonego na serwerze. Następnie otrzymaną wartość porównujemy z wartością zmiennej $maksymalny_rozmiar i podejmujemy odpowiednią decyzję w zależności do wyniku tego porównania.

Dla uporządkowania wiadomości, poniżej przedstawiam obecny stan naszego skryptu:


if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if (!empty($_FILES['plik']) && $_FILES['plik']['error'] === UPLOAD_ERR_OK) {
    $dozwolone_typy = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'];
    $maksymalny_rozmiar = 10485760;
    
    if (!in_array($_FILES['plik']['type'], $dozwolone_typy)) {
      exit('<p>Niewłaściwy typ pliku.</p>');
      // tu można zrobić tylko zmienną sygnalizacyjną typu $typ_ok = 1, a nawet można to zrobić operatorem trójkowym - podobnie jak z rozmiarem pliku
    }
    if (filesize($_FILES['plik']['tmp_name']) > $maksymalny_rozmiar) {
      exit('<p>Za duży plik!</p>');
    }
  }
  else {
    exit('<p>Coś się nie udało.</p>');
  }
}

Po upewnieniu się, że użytkownik przesłał plik odpowiedniego typu i rozmiaru, możemy w końcu przenieść ten plik w miejsce docelowe na serwerze i nadać mu odpowiednią nazwę.

Przenoszenie pliku do katalogu docelowego w PHP

Wiemy już, że przesłany przez użytkownika plik spełnia wszystkie nasze wymagania, więc chcemy go przenieść do wybranego katalogu na serwerze, niech będzie uploads, w katalogu głównym.

Czynność ta nie jest skomplikowana, ale trzeba wykonać parę kroków, aby utrzymać porządek i mieć pewność, że wszystko zostanie załatwione tak, jak trzeba. Oto lista czynności, które wykonamy:

  1. Zdefiniowanie zmiennych, które ułatwią dalszą pracę. Zdefiniujemy między innymi zmienną do przechowywania nazwy pliku tymczasowego i oryginalnej nazwy pliku, ale nie tylko.
  2. Zdefiniowanie zmiennej do przechowywania ścieżki do katalogu docelowego, do którego chcemy przenieść plik.
  3. Utworzenie katalogu docelowego, jeśli nie istnieje.
  4. Sprawdzenie, czy w katalogu docelowym nie ma już pliku o danej nazwie i w razie potrzeby dodanie przyrostka do nazwy naszego pliku, aby uniknąć nadpisania starego.
  5. Przeniesienie pliku do katalogu docelowego.

Zaczniemy od zdefiniowania zmiennych wymienionych w punktach 1 i 2:


$tmp_name = $_FILES['plik']['tmp_name'];
$original_name = pathinfo($_FILES['plik']['name'], PATHINFO_BASENAME);
$target_dir = __DIR__ . '/uploads/';

Pierwsza z powyższych definicji zmiennych ($tmp_name) nie wymaga tłumaczenia — po prostu zapisujemy w zmiennej wartość elementu tablicy dwuwymiarowej. Reprezentuje ona tymczasową nazwę pliku przesłanego na serwer.

W drugiej definicji ($original_name) użyłem funkcji pathinfo(), której w tym kursie jeszcze nie omawiałem. Funkcja ta pobiera ścieżkę do pliku i zwraca różne jej części, w zależności do tego, co przekażemy jako drugi argument wywołania. W tym przykładzie drugim argumentem jest stała PATHINFO_BASENAME, która powoduje zwrócenie przez funkcję nazwy pliku wraz z rozszerzeniem.

Trzecia zmienna ($target_dir) przechowuje ścieżkę do katalogu, do którego przeniesiemy plik przesłany na serwer przez formularz. __DIR__ to stała magiczna PHP, zawierająca ścieżkę do katalogu, w którym znajduje się zawierający ją plik. Użyłem jej, bo przesyłane pliki chcę umieszczać w podkatalogu uploads tego folderu.

Kolejną naszą czynnością będzie sprawdzenie, czy wybrany przez nas katalog istnieje i utworzenie go w razie potrzeby. Do tego celu posłużą nam dwie funkcje PHP: is_dir() i mkdir():


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

W tym przykładzie za pomocą funkcji PHP is_dir() sprawdzamy, czy istnieje interesujący nas katalog. Jeśli nie, to go tworzymy za pomocą funkcji mkdir(), której jako argumenty przekazałem ścieżkę do tworzonego katalogu ($target_dir), maskę uprawnień do katalogu (0755) oraz wartość true, oznaczającą, że funkcja ma w razie potrzeby utworzyć także katalogi nadrzędne.

Zadbaliśmy już o utworzenie katalogu, na wypadek, gdyby nie istniał, więc teraz jeszcze sprawdzimy, czy katalog ten nie zawiera pliku o takiej samej nazwie, jak ten, który chcemy w nim umieścić. Jeśli tak, to do nazwy nowego pliku dodamy przedrostek.

Gdybyśmy pominęli tę część skryptu, to w razie kolizji nazw plików nowy plik po prostu zastąpiłby stary, przez co moglibyśmy stracić ważne dane. Spójrz na poniższy fragment skryptu:


$target_path = $target_dir . $original_name;
$counter = 1;
while (file_exists($target_path)) {
  $target_path = $target_dir . "{$counter}_" . $original_name;
  $counter++;
}

Najpierw zdefiniowaliśmy zmienną o nazwie $target_path, która zawiera pełną ścieżkę wraz z nazwą pliku. Następnie zdefiniowaliśmy zmienną $counter o początkowej wartości 1. Wartość ta zostanie dodana do nazwy przenoszonego pliku, jeśli okaże się że plik o takiej nazwie już istnieje.

Pętla PHP while sprawdza, czy istnieje interesujący nas plik i jeśli tak, dodaje na początku jego nazwy numer ze znakiem podkreślenia (w pierwszej iteracji dodaje 1_) i zwiększa wartość licznika (zmiennej $counter) o jeden. Potem jeszcze raz sprawdza, czy istnieje plik o nazwie z dodanym pierwszym numerem. Jeśli tak, to dodaje do oryginalnej nazwy drugi numer (1_) itd., aż znajdzie nazwę, która nie będzie się powtarzać.

Jeśli na przykład w naszym folderze uploads znajdują się pliki test.jpg, 1_test.jpg i 2_test.jpg i prześlemy plik o nazwie test.jpg, to plikowi temu zostanie nadana nazwa 3_test.jpg.

Teraz już wszystko mamy gotowe do umieszczenia pliku w odpowiednim katalogu na serwerze. Do tego celu użyjemy funkcji move_uploaded_file():


if (move_uploaded_file($tmp_name, $target_path)) {
  echo 'Plik został zapisany jako:' . htmlspecialchars(pathinfo($target_path, PATHINFO_BASENAME));
} else {
  echo 'Błąd zapisu pliku.';
}

Jeśli wykonanie funkcji move_uploaded_file() się powiedzie, wyświetlamy informację, że plik został zapisany i podajemy, pod jaką nazwą został zapisany.

Jeśli podczas przenoszenia pliku wystąpi jakiś błąd, wyświetlamy komunikat Błąd zapisu pliku.

W ten sposób ukończyliśmy nasz skrypt do przesyłania plików na serwer. Poniżej przedstawiam go w całej okazałości:


if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if (!empty($_FILES['plik']) && $_FILES['plik']['error'] === UPLOAD_ERR_OK) {
    $dozwolone_typy = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'];
    $maksymalny_rozmiar = 10485760;
    $tmp_name = $_FILES['plik']['tmp_name'];
    $original_name = pathinfo($_FILES['plik']['name'], PATHINFO_BASENAME);
    $target_dir = __DIR__ . '/uploads/';
    
    if (!in_array(mime_content_type($tmp_name), $dozwolone_typy)) {
      exit('<p>Niewłaściwy typ pliku.</p>');
    }

    if (filesize($tmp_name) > $maksymalny_rozmiar) {
      exit('<p>Za duży plik!</p>');
    }

    if (!is_dir($target_dir)) {
      mkdir($target_dir, 0750, true);
    }

    $target_path = $target_dir . $original_name;
    $counter = 1;
    while (file_exists($target_path)) {
      $target_path = $target_dir . "{$counter}_" . $original_name;
      $counter++;
    }

    if (move_uploaded_file($tmp_name, $target_path)) {
      echo 'Plik został zapisany jako:' . htmlspecialchars(pathinfo($target_path, PATHINFO_BASENAME));
    } else {
      echo 'Błąd zapisu pliku.';
    }
  }
  else {
    exit('<p>Coś się nie udało.</p>');
  }
}

Skoro masz już opanowane odbieranie na serwerze pojedynczych plików, to opanowanie przyjmowania więcej niż jednego pliku nie sprawi ci już żadnego problemu.

Przesyłanie większej liczby plików

Przesłanie więcej niż jednego pliku na raz za pośrednictwem formularza można umożliwić na dwa sposoby:

  • Przez dodanie większej liczby elementów input typu file.
  • Przez dodanie do jednego elementu input typu file atrybutu multiple i zakończenie jego nazwy nawiasem kwadratowym. W tym przypadku w formularzu będzie widoczny jeden element wyboru pliku, ale będzie on pozwalał na wybranie więcej niż jednego pliku w oknie.

Poniżej zwięźle opisuję obie metody już tylko od strony tego, jak przesyłane w taki sposób pliki są reprezentowane na serwerze. Nie rozwodzę się już na temat bezpieczeństwa i metod dalszej pracy z nimi, bo niczym nie różnią się od tych, które opisałem powyżej.

Kilka elementów input

Jeśli w formularzu umieścimy kilka elementów input typu file umożliwiających wybór jednego pliku, to po jego przesłaniu na serwerze zostanie utworzona tablica tablic reprezentujących każdy z tych przesłanych plików. Powiedzmy na przykład, że w naszym formularzu HTML mamy takie elementy HTML intput:


<input type="file" name="plik">
<input type="file" name="plik2">
<input type="file" name="plik3">

Gdy zatwierdzimy ten formularz, to na serwerze zostanie utworzona tablica o takiej strukturze:


array(3) {
  ["plik"]=>
  array(6) {
    ["name"]=>
    string(11) "testowy-plik.jpg"
    ["full_path"]=>
    string(11) "testowy-plik.jpg"
    ["type"]=>
    string(10) "image/jpeg"
    ["tmp_name"]=>
    string(24) "C:\xampp\tmp\php1418.tmp"
    ["error"]=>
    int(0)
    ["size"]=>
    int(117879)
  }
  ["plik2"]=>
  array(6) {
    ["name"]=>
    string(11) "testowy-plik2.png"
    ["full_path"]=>
    string(11) "testowy-plik2.png"
    ["type"]=>
    string(9) "image/png"
    ["tmp_name"]=>
    string(24) "C:\xampp\tmp\php1419.tmp"
    ["error"]=>
    int(0)
    ["size"]=>
    int(215944)
  }
  ["plik3"]=>
  array(6) {
    ["name"]=>
    string(12) "testowy-plik3.webp"
    ["full_path"]=>
    string(12) "testowy-plik3.webp"
    ["type"]=>
    string(10) "image/webp"
    ["tmp_name"]=>
    string(24) "C:\xampp\tmp\php141A.tmp"
    ["error"]=>
    int(0)
    ["size"]=>
    int(75930)
  }
}

Teraz wystarczy zająć się każdym z plików przedstawionych w tej tablicy dwuwymiarowej w odpowiedni sposób, opisany wcześniej.

Jeden element input

Drugim sposobem na umożliwienie przesłania przez formularz kilku plików na raz jest nadanie elementowi input typu file atrybutu multiple i dodanie do nazwy tego elementu nawiasu kwadratowego. Na przykład:


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

Atrybut multiple to logiczny atrybut HTML, a więc nie trzeba ma nadawać wartości, tylko wystarczy go zdefiniować.

Teraz w formularzu będzie tylko jeden element wyboru pliku, ale w oknie Wysyłanie pliku (lub o podobnej nazwie), które pojawi się po jego kliknięciu, użytkownik będzie mógł wybrać więcej niż jeden plik — na przykład klikając wybrane pliki przy wciśniętym klawiszu Ctrl.

Powiedzmy, że użytkownik wybrał te same trzy pliki, co poprzednio. W tym przypadku na serwerze zostanie utworzona następująca tablica:


array(1) {
  ["pliki"]=>
  array(6) {
    ["name"]=>
    array(3) {
      [0]=>
      string(11) "testowy-plik.jpg"
      [1]=>
      string(11) "testowy-plik2.png"
      [2]=>
      string(12) "testowy-plik3.webp"
    }
    ["full_path"]=>
    array(3) {
      [0]=>
      string(11) "testowy-plik.jpg"
      [1]=>
      string(11) "testowy-plik2.png"
      [2]=>
      string(12) "testowy-plik3.webp"
    }
    ["type"]=>
    array(3) {
      [0]=>
      string(10) "image/jpeg"
      [1]=>
      string(9) "image/png"
      [2]=>
      string(10) "image/webp"
    }
    ["tmp_name"]=>
    array(3) {
      [0]=>
      string(24) "C:\xampp\tmp\php593E.tmp"
      [1]=>
      string(24) "C:\xampp\tmp\php593F.tmp"
      [2]=>
      string(24) "C:\xampp\tmp\php5940.tmp"
    }
    ["error"]=>
    array(3) {
      [0]=>
      int(0)
      [1]=>
      int(0)
      [2]=>
      int(0)
    }
    ["size"]=>
    array(3) {
      [0]=>
      int(117879)
      [1]=>
      int(215944)
      [2]=>
      int(75930)
    }
  }
}

Ta tablica różni się od poprzedniej i od wcześniej oglądanych. Jest to tablica trójwymiarowa, w której pierwszy wymiar reprezentuje nazwę elementu formularza (pliki), a drugi jest tablicą tablic zawierających informacje o każdym z plików, zgrupowane pod jednakowymi kluczami.

Znając strukturę tablicy, możemy bez problemu wykonywać wszystkie niezbędne czynności na przesłanych plikach. A jeśli nie pamiętasz, jak pracuje się z tablicami wielowymiarowymi, to przeczytaj rozdział Tablice wielowymiarowe PHP.

Podsumowanie

blank
  1. Plik php.ini to główny plik konfiguracyjny środowiska PHP.
  2. Dodatkowo parametry środowiska PHP można definiować w plikach .user.ini i za pomocą funkcji PHP ini_set().
  3. Najpierw PHP wczytuje plik php.ini, a następnie szuka plików .user.ini i wykonuje wywołania funkcji ini_set().
  4. Informację o tym, w których miejscach można ustawiać wybrane dyrektywy konfiguracji PHP, można znaleźć w dokumentacji PHP.
  5. Element HTML form formularza służącego do wysyłania plików na serwer powinien mieć atrybut enctype o wartości multipart/form-data.
  6. Formularz służący do przesyłania plików na serwer musi być wysyłany metodą HTTP POST.
  7. Element umożliwiający użytkownikowi wybranie pliku do wysłania to input typu file.
  8. W atrybucie accept elementu intput można umieścić listę akceptowanych typów MIME, ale jest to tylko udogodnienie dla użytkownika, a nie forma zabezpieczenia.
  9. Po przesłaniu pliku przez użytkownika za pomocą formularza na serwerze jest tworzona tablica superglobalna o nazwie $_FILES, która zawiera dane tego pliku.
  10. Typ MIME pliku odebranego na serwerze należy sprawdzić za pomocą odpowiedniej funkcji, np. mime_content_type().
  11. Rozmiar pliku odebranego na serwerze należy sprawdzić za pomocą funkcji filesize().
  12. Kiedy typ i rozmiar pliku są w porządku, można przenieść go do katalogu docelowego za pomocą funkcji move_uploaded_file().
  13. Przed przeniesieniem pliku do katalogu docelowego dobrze jest sprawdzić, czy ten katalog istnieje oraz czy nie ma w nim już pliku o danej nazwie.
  14. Aby umożliwić użytkownikowi przesłanie więcej niż jednego pliku na raz, można dodać do formularza kilka elementów input typu file albo jednemu takiemu elementowi można nadać atrybut multiple i wartość atrybutu zakończyć nawiasem kwadratowym.


blank

Pytania

  1. Czym jest plik php.ini?
  2. Czym jest plik .user.ini?
  3. Do czego służy funkcja ini_set()?
  4. Jak sprawdzić, gdzie znajduje się plik php.ini?
  5. Jak sprawdzić, gdzie można używać wybranej dyrektywy konfiguracji środowiska PHP?
  6. Jakie atrybuty powinien mieć element HTML form formularza służącego do wysyłania plików na serwer?
  7. Jaką metodą HTTP należy przesyłać formularze umożliwiające wysyłanie plików na serwer?
  8. Jaki element formularza umożliwia użytkownikom wysyłanie plików na serwer?
  9. Do czego służy atrybut accept elementu intput?
  10. Gdzie można znaleźć dane pliku przesłanego na serwer przez formularz?
  11. Jak należy sprawdzać typ MIME pliku odebranego na serwerze?
  12. Jak należy sprawdzać rozmiar pliku odebranego na serwerze?
  13. W jaki sposób należy przenieść plik przesłany na serwer do wybranego katalogu?
  14. Jak umożliwić przesłanie przez formularz więcej niż jednego pliku na raz?
  15. Jak obsługuje się tak przesłane pliki?


blank

Ćwiczenia

  1. Utwórz formularz umożliwiający użytkownikowi przesłanie na serwer trzech plików na raz i każdy z tych plików zapisz w innym katalogu o dowolnej nazwie.
Udostępnij:
Share

Podobał Ci się ten artykuł?

Oceń go!

Średnia 0 / 5. Liczba głosów: 0

Jeszcze nikt nie głosował. Wyprzedź innych i zagłosuj.

Skoro spodobał Ci się ten artykuł...

Poleć go znajomym!

Ojej :( Powiedz nam, co powinniśmy poprawić!

blank
Podoba Ci się ta strona?

Pomóż nam się rozwijać, wykupując płatne konto. Dzięki temu będziemy mogli tworzyć dla Ciebie jeszcze więcej ciekawych treści, a Ty pozbędziesz się reklam.

Dodaj komentarz