Archiwum kategorii: PHP

Kurs programowania w języku PHP

Sesje w PHP – zadania

Zadanie 1: Prosty licznik odwiedzin strony

Opis zadania:

Zlicz, ile razy użytkownik odwiedził stronę podczas jednej sesji.

Kroki:

  1. Rozpocznij sesję za pomocą session_start().
  2. Sprawdź, czy zmienna $_SESSION['counter'] istnieje.
  3. Jeśli tak – zwiększ jej wartość. Jeśli nie – ustaw ją na 1.
  4. Wyświetl aktualną wartość licznika.

Zadanie 2: Prosty system logowania z sesją

Opis zadania:

Zaloguj użytkownika, zapisując jego nazwę w sesji.

Kroki:

  1. Stwórz formularz z polem username.
  2. Po przesłaniu formularza: sprawdź, czy pole nie jest puste.
  3. Jeśli poprawnie – zapisz $_SESSION['user'] = $username.
  4. Przekieruj użytkownika na stronę powitalną.
  5. Na stronie powitalnej sprawdź, czy użytkownik jest zalogowany.

Zadanie 3: Wylogowanie użytkownika (usuwanie sesji)

Opis zadania:

Usuń dane sesji i wyloguj użytkownika.

Kroki:

  1. Użyj session_start() do rozpoczęcia sesji.
  2. Użyj session_unset() i session_destroy() do jej wyczyszczenia.
  3. Przekieruj na stronę logowania.

Zadanie 4: Przechowywanie wielu informacji o użytkowniku w sesji

Opis zadania:

Zapisz imię, nazwisko i rolę użytkownika do sesji i wyświetl je na stronie.

Kroki:

  1. Przyjmij dane z formularza (name, surname, role).
  2. Zapisz je w tablicy sesyjnej jako np. $_SESSION['user'] = [...].
  3. Na innej stronie odczytaj i wyświetl dane.

Zadanie 5: Sesyjny koszyk zakupowy

Opis zadania:

Dodaj produkty do koszyka przechowywanego w sesji.

Kroki:

  1. Stwórz listę produktów z przyciskiem „Dodaj do koszyka”.
  2. Po kliknięciu, dodaj produkt do $_SESSION['cart'].
  3. Na stronie koszyka wyświetl listę produktów.
  4. Dodaj opcję usuwania produktów z koszyka.

Operacje na plikach – zadania

Zadanie 1: Zapis danych użytkownika do pliku tekstowego

Opis zadania:

Użytkownik wprowadza imię i e-mail. Dane mają być zapisane do pliku users.txt, każdy wpis w nowej linii.

Kroki:

  1. Odbierz dane z formularza (POST).
  2. Waliduj, czy pola nie są puste.
  3. Otwórz plik users.txt w trybie dopisywania (a).
  4. Zapisz dane w formacie: Imię - Email.
  5. Zamknij plik.

Zadanie 2: Odczyt i wyświetlenie zawartości pliku

Opis zadania:

Odczytaj plik users.txt i wyświetl jego zawartość jako listę HTML.

Kroki:

  1. Sprawdź, czy plik istnieje.
  2. Wczytaj zawartość pliku za pomocą file() lub fopen().
  3. Przeiteruj linie i wyświetl jako listę.

Zadanie 3: Tworzenie kopii zapasowej pliku

Opis zadania:

Zrób kopię pliku users.txt do pliku backup_users.txt.

Kroki:

  1. Sprawdź, czy users.txt istnieje.
  2. Użyj copy() do utworzenia backupu.
  3. Obsłuż ewentualne błędy.

Zadanie 4: Edycja konkretnej linii w pliku

Opis zadania:

Zmień np. 2. linię w pliku users.txt na nową wartość.

Kroki:

  1. Wczytaj plik do tablicy (file()).
  2. Zmień wartość w konkretnej linii (np. indeks 1).
  3. Zapisz wszystkie linie z powrotem (file_put_contents()).

Zadanie 5: Usuwanie konkretnej linii z pliku

Opis zadania:

Usuń np. linię zawierającą e-mail test@example.com z pliku users.txt.

Kroki:

  1. Wczytaj plik do tablicy (file()).
  2. Usuń linię, która zawiera szukany fragment.
  3. Zapisz nową zawartość pliku.

Projekt: System zarządzania książkami w bibliotece

Cel projektu:

Zbudowanie aplikacji webowej w PHP, która umożliwia zarządzanie książkami w małej bibliotece. Aplikacja ma umożliwiać:

  • dodawanie nowych książek,
  • wyświetlanie listy wszystkich książek,
  • edytowanie danych książek,
  • usuwanie książek,
  • filtrowanie listy książek według autora, roku lub gatunku.

Wymagania funkcjonalne

Tabela books:

KolumnaTyp danychUwagi
idINT AUTO_INCREMENT / SERIALKlucz główny
titleVARCHAR(255)Niepusta
authorVARCHAR(255)Niepusta
genreVARCHAR(100)Np. „kryminał”, „sci-fi”
publish_yearINTRok wydania
created_atDATETIMEAutomatyczna data dodania wpisu

Funkcje aplikacji

  1. Lista książek – tabela z książkami, możliwość filtrowania.
  2. Dodawanie nowej książki – formularz.
  3. Edycja książki – formularz z aktualnymi danymi.
  4. Usuwanie książki – przycisk usuń z potwierdzeniem.
  5. Walidacja danych – nie dopuścić pustych pól, rok w zakresie np. 1500–2025.

Kroki rozwiązania projektu

  • Utwórz bazę danych i tabelę books:
CREATE TABLE books ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, author   VARCHAR(255) NOT NULL, genre VARCHAR(100), publish_year INT CHECK (publish_year BETWEEN 1500 AND 2025), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );
  • Skonfiguruj połączenie z bazą danych (plik db.php) :
 <?php 
    $dsn = 'mysql:host=localhost;dbname=biblioteka;charset=utf8mb4'; 
    $user = 'root'; 
    $pass = ''; 
    try {         $pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); 
    } catch (PDOException $e) { 
        die("Błąd połączenia z bazą: " . $e->getMessage()); 
    } 
 ?>
  • Utwórz plik index.php – wyświetlanie i filtrowanie książek
    • Lista książek z możliwością filtrowania (GET).
    • Przycisk „Dodaj”, „Edytuj”, „Usuń”.
  • Utwórz plik add.php – formularz dodawania książki
    • Formularz HTML
    • Obsługa POST z walidacją danych
    • Wstawienie danych do bazy
  • Utwórz plik edit.php – formularz edycji książki
    • Odczyt danych książki po ID
    • Formularz z danymi książki
    • Aktualizacja danych po przesłaniu
  • Utwórz plik delete.php – usuwanie książki po ID z potwierdzeniem
    • Można to zrealizować też przy pomocy GET z linkiem np. delete.php?id=5
  • Walidacja danych po stronie PHP
    • Sprawdzenie pustych pól
    • Sprawdzenie zakresu roku

Przykładowe rozwiązanie: fragment add.php

 <?php    require 'db.php';
     if ($_SERVER["REQUEST_METHOD"] === "POST") {         $title = trim($_POST['title']);
         $author = trim($_POST['author']);
         $genre = $_POST['genre'];
         $year = (int)$_POST['publish_year'];

         $errors = [];

         if (empty($title) || empty($author)) {
             $errors[] = "Tytuł i autor są wymagane.";
         }
         if ($year < 1500 || $year > 2025) {
             $errors[] = "Rok wydania musi być między 1500 a 2025.";
         }

         if (empty($errors)) {
             $stmt = $pdo->prepare("INSERT INTO books (title, author, genre, publish_year) VALUES (?, ?, ?, ?)");
             $stmt->execute([$title, $author, $genre, $year]);
             header("Location: index.php");
             exit;
         }
     }
?>

 <!-- HTML formularz -->
 <form method="POST">
     <input type="text" name="title" placeholder="Tytuł">
     <input type="text" name="author" placeholder="Autor">
     <input type="text" name="genre" placeholder="Gatunek">
     <input type="number" name="publish_year" placeholder="Rok wydania">
     <button type="submit">Dodaj książkę</button>
 </form>

Przykładowy kod pliku index.php:

 <?php
 require 'db.php';

 // Pobierz filtry z URL (GET)
 $author = $_GET['author'] ?? '';
 $genre = $_GET['genre'] ?? '';
 $year = $_GET['year'] ?? '';

 // Buduj zapytanie SQL dynamicznie
 $sql = "SELECT * FROM books WHERE 1=1";
 $params = [];

 if (!empty($author)) {
    $sql .= " AND author LIKE ?";
    $params[] = "%$author%";
 }

 if (!empty($genre)) {
    $sql .= " AND genre LIKE ?";
    $params[] = "%$genre%";
 } 

 if (!empty($year)) {
    $sql .= " AND publish_year = ?";
    $params[] = $year;
 }

 $sql .= " ORDER BY created_at DESC";

 $stmt = $pdo->prepare($sql);
 $stmt->execute($params);
 $books = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>

 <!DOCTYPE html>
 <html lang="pl">
 <head>
    <meta charset="UTF-8">
    <title>Lista książek - Biblioteka</title>
    <style>
        table { border-collapse: collapse; width: 100%; }
        th, td { padding: 8px 12px; border: 1px solid #ccc; }
        form.inline { display: inline; }
    </style>
 </head>
 <body>
    <h1>Lista książek w bibliotece</h1>

    <form method="GET">
        <input type="text" name="author" placeholder="Autor" value="<?= htmlspecialchars($author) ?>">
        <input type="text" name="genre" placeholder="Gatunek" value="<?= htmlspecialchars($genre) ?>">
        <input type="number" name="year" placeholder="Rok" value="<?= htmlspecialchars($year) ?>">
        <button type="submit">Filtruj</button>
        <a href="index.php">Resetuj</a>
    </form>

    <p><a href="add.php">➕ Dodaj nową książkę</a></p>

    <?php if (count($books) === 0): ?>
        <p>Brak książek spełniających kryteria.</p>
    <?php else: ?>
        <table>
            <thead>
                <tr>
                    <th>Tytuł</th>
                    <th>Autor</th>
                    <th>Gatunek</th>
                    <th>Rok</th>
                    <th>Dodano</th>
                    <th>Akcje</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($books as $book): ?>
                <tr>
                    <td><?= htmlspecialchars($book['title']) ?></td>
                    <td><?= htmlspecialchars($book['author']) ?></td>
                    <td><?= htmlspecialchars($book['genre']) ?></td>
                    <td><?= htmlspecialchars($book['publish_year']) ?></td>
                    <td><?= htmlspecialchars($book['created_at']) ?></td>
                    <td>
                        <a href="edit.php?id=<?= $book['id'] ?>">Edytuj</a> |
                        <form action="delete.php" method="POST" class="inline" onsubmit="return confirm('Na pewno usunąć tę książkę?');">
                            <input type="hidden" name="id" value="<?= $book['id'] ?>">
                            <button type="submit">Usuń</button>
                        </form>
                    </td>
                </tr>
                <?php endforeach ?>
            </tbody>
        </table>
    <?php endif; ?>
 </body>
 </html>

Co sprawdza nauczyciel?

  1. Połączenie z bazą danych
  2. Operacje CRUD (4 operacje)
  3. Walidacja danych
  4. Szablon/interfejs graficzny (prostota, funkcjonalność)
  5. Filtrowanie / wyszukiwanie

Sesje w PHP

Wprowadzenie do sesji w PHP

Sesja w PHP to mechanizm pozwalający zachować dane dotyczące danego użytkownika między wieloma żądaniami HTTP (stronami) w trakcie jednej wizyty na witrynie​. W praktyce oznacza to, że informacje przechowywane w sesji “podróżują” wraz z użytkownikiem, dzięki czemu kolejne strony mogą korzystać z danych zapisanych wcześniej. W odróżnieniu od ciasteczek (cookies), które przechowują dane po stronie klienta (w przeglądarce użytkownika), sesje przechowują dane po stronie serwera – jest to więc bezpieczniejsze i bardziej elastyczne rozwiązanie dla danych użytkownika​.

Aby to osiągnąć, PHP przy pierwszym wejściu użytkownika na stronę tworzy unikalny identyfikator sesji (ang. session ID) i zwykle przesyła go do przeglądarki w formie ciasteczka​. To ciasteczko pełni rolę klucza identyfikującego – nie zawiera żadnych danych osobowych ani wrażliwych, a jedynie ową losową wartość ID. Przy każdym kolejnym żądaniu przeglądarka odsyła ten identyfikator, co pozwala skojarzyć żądanie z wcześniej zapamiętanymi na serwerze informacjami użytkownika​. Jeśli aplikacja znajdzie na serwerze dane odpowiadające temu identyfikatorowi, może odtworzyć stan sesji danego użytkownika (np. informacje o jego zalogowaniu, preferencjach itp.)​. W ten sposób sesje umożliwiają stworzenie ciągłego, spójnego doświadczenia dla użytkownika na stronie, pomimo bezstanowego charakteru protokołu HTTP.

Do czego służą sesje? Przykładowe zastosowania sesji to m.in. utrzymanie stanu zalogowania użytkownika (tzw. user session), przechowywanie zawartości koszyka w sklepie internetowym, zachowanie danych wprowadzonych do formularza pomiędzy krokami wielostronicowego formularza, czy wyświetlanie komunikatów (np. jednorazowych komunikatów potwierdzających, tzw. flash messages). Sesje są więc przydatne wszędzie tam, gdzie potrzebne jest tymczasowe przechowanie danych na serwerze na czas trwania wizyty użytkownika na stronie.

Rozpoczęcie sesji: session_start() i zmienna $_SESSION

Aby skorzystać z sesji w PHP, najpierw należy ją zainicjować. Służy do tego funkcja session_start(), którą wywołujemy na samym początku skryptu PHP, przed wysłaniem jakiegokolwiek HTML czy innych danych do przeglądarki. Jeżeli żądanie HTTP nie zawiera jeszcze identyfikatora sesji (np. użytkownik odwiedza stronę po raz pierwszy), wywołanie session_start() utworzy nową sesję i wygeneruje dla niej unikalne ID. Jeśli natomiast użytkownik posiada już aktywną sesję (np. odwiedza kolejną podstronę), session_start() wznowi istniejącą sesję – PHP sprawdzi wtedy, czy w żądaniu przekazano identyfikator (np. w ciasteczku sesyjnym), i jeśli tak, to załaduje odpowiadające mu dane​.

Po pomyślnym wywołaniu session_start() możemy korzystać z superglobalnej tablicy $_SESSION do zapisywania i odczytywania danych sesyjnych. Tablica $_SESSION działa podobnie do innych tablic PHP, z tą różnicą, że jej zawartość jest utrzymywana między kolejnymi odwołaniami HTTP (dopóki trwa sesja). W praktyce każda para klucz-wartość zapisana w $_SESSION zostanie automatycznie zachowana po stronie serwera po zakończeniu skryptu i będzie dostępna przy następnym żądaniu od tego samego użytkownika​.

Przykład – rozpoczęcie sesji i ustawienie zmiennych: Poniższy kod ilustruje prosty scenariusz startu sesji i użycia zmiennych sesyjnych:

 <?php
 // Startujemy sesję (musi być wywołane jako pierwsze polecenie w skrypcie)
 session_start();

 // Ustawiamy zmienne sesyjne
 $_SESSION['username'] = 'JanKowalski';
 $_SESSION['role'] = 'admin';

 // Odczytujemy i wykorzystujemy zmienne sesyjne
 echo "Zalogowano jako: " . $_SESSION['username'];  // wypisze: Zalogowano jako: JanKowalski
 ?>

W powyższym przykładzie wywołujemy session_start(), aby zainicjować (lub wznowić) sesję. Następnie przy pomocy tablicy $_SESSION zapisujemy dwie informacje: nazwę użytkownika oraz jego rolę. Te dane zostają zachowane na serwerze i będą dostępne na wszystkich kolejnych stronach, które również wywołają session_start() na początku. Na końcu zademonstrowano odczyt zmiennej sesyjnej – wypisujemy na stronie nazwę zalogowanego użytkownika. Warto zauważyć, że zmienne sesyjne nie są przekazywane między stronami w URL czy w formularzach – PHP automatycznie utrzymuje je po stronie serwera. Wystarczy jedynie na każdej stronie, na której chcemy z nich skorzystać, ponownie wywołać session_start() przed dostępem do $_SESSION.

Praktyczne przykłady zastosowania sesji

Sesje najczęściej wykorzystujemy do obsługi logowania użytkowników oraz przechowywania tymczasowych danych na potrzeby aplikacji. Poniżej omówimy dwa praktyczne przykłady ilustrujące te scenariusze.

Przykład 1: Logowanie użytkownika z użyciem sesji

Rozważmy uproszczony mechanizm logowania. Gdy użytkownik poprawnie poda dane logowania (np. w formularzu), chcemy zapamiętać informację, że jest zalogowany, aby móc go uwierzytelniać na kolejnych podstronach. Do tego celu idealnie nadaje się sesja.

Załóżmy, że zweryfikowaliśmy poprawność danych logowania użytkownika (np. porównując z bazą danych). Poniższy kod demonstruje co dalej dzieje się po pomyślnym zalogowaniu:

 <?php
 session_start(); // Inicjujemy sesję na początku skryptu (lub kontynuujemy istniejącą)

 // (Pominięto kod sprawdzający poprawność loginu i hasła użytkownika)
 if ($login_ok) {
    // Ustawiamy zmienne sesyjne po udanym logowaniu
    $_SESSION['user_id'] = $userID;       // unikalny identyfikator użytkownika (np. z bazy danych)
    $_SESSION['username'] = $username;    // nazwa użytkownika
    $_SESSION['is_logged_in'] = true;     // pomocnicza flaga stanu zalogowania

    // Dla bezpieczeństwa regenerujemy ID sesji po zalogowaniu
    session_regenerate_id(true);

    echo "Witaj, $username! Zostałeś pomyślnie zalogowany.";
 } else {
    echo "Błędny login lub hasło.";
 }
 ?>

Omówienie: Po wywołaniu session_start() sprawdzamy dane logowania. Jeśli są poprawne, zapisujemy w sesji kluczowe informacje: identyfikator użytkownika, jego nazwę oraz ustawiamy flagę is_logged_in informującą, że użytkownik przeszedł autoryzację. Dzięki temu na kolejnych stronach będziemy mogli łatwo sprawdzić, czy użytkownik jest zalogowany (np. poprzez if ($_SESSION['is_logged_in']) { ... }). Następnie wywołujemy funkcję session_regenerate_id(true), aby zmienić identyfikator bieżącej sesji na nowy​. Jest to ważna czynność zabezpieczająca przed pewnym rodzajem ataku (omówionym później jako session fixation) – po uwierzytelnieniu użytkownika warto zmienić ID jego sesji, aby ewentualny intruz nie mógł przejąć sesji, znając wcześniejsze ID. Funkcja session_regenerate_id() generuje nowy identyfikator, zachowując przy tym zawartość bieżącej sesji (wszystkie zapisane zmienne)​. Argument true powoduje, że stary identyfikator (i odpowiadający mu plik sesji) zostanie unieważniony/usunięty, zapobiegając równoległemu istnieniu dwóch sesji o tym samym stanie.

Na kolejnych stronach możemy sprawdzać, czy użytkownik jest zalogowany, np. tak:

 <?php
 session_start();
 if (!isset($_SESSION['is_logged_in']) || $_SESSION['is_logged_in'] !== true) {
    // Użytkownik nie jest zalogowany – przekierowanie lub wyświetlenie komunikatu
    die("Dostęp wymaga logowania.");
 }
 // Jeśli warunek powyżej nie zadziałał, to znaczy, że użytkownik jest zalogowany
 echo "Witaj ponownie, " . $_SESSION['username'] . "!";
 ?>

Dzięki sesji nie musimy za każdym razem prosić użytkownika o podanie hasła – raz zalogowany, pozostaje rozpoznawany na podstawie identyfikatora sesyjnego przesyłanego przez przeglądarkę.

Przykład 2: Przechowywanie danych tymczasowych (np. koszyk zakupów, komunikaty)

Innym powszechnym zastosowaniem sesji jest przechowywanie danych tymczasowych, które nie wymagają trwałego zapisania w bazie danych, a jedynie zachowania przez pewien czas dla wygody użytkownika. Przykładem może być koszyk zakupowy w sklepie internetowym – dopóki klient kontynuuje zakupy, lista wybranych produktów może być przechowywana właśnie w sesji (zanim zostanie ewentualnie sfinalizowana i zapisana na stałe). Innym przykładem są tzw. flash messages, czyli komunikaty (np. o błędzie lub powodzeniu operacji), które chcemy pokazać użytkownikowi tylko raz, przy kolejnym wyświetleniu strony.

Poniższy fragment kodu ilustruje mechanizm dodawania elementu do koszyka przechowywanego w sesji oraz ustawiania jednorazowego komunikatu:

 <?php
 session_start();

 // Dodanie produktu do koszyka (przechowywanego jako tablica w sesji)
 $productId = 42;
 $_SESSION['cart'][] = $productId;  // dodajemy ID produktu do tablicy 'cart'

 $_SESSION['message'] = "Produkt $productId został dodany do koszyka.";  // ustawiamy komunikat

 // ... (przekierowanie do innej strony lub dalsza logika)
 ?>

W powyższym kodzie wykorzystujemy sesję do przechowania zawartości koszyka: zakładamy, że $_SESSION['cart'] jest tablicą, w której trzymamy identyfikatory wybranych produktów. Wywołanie $_SESSION['cart'][] = $productId; dodaje kolejny produkt do koszyka (jeśli tablica nie istniała, PHP automatycznie ją utworzy). Dodatkowo ustawiamy zmienną $_SESSION['message'] z komunikatem informującym o dodaniu produktu – załóżmy, że chcemy wyświetlić go użytkownikowi na następnej stronie i potem usunąć. Realizacja wyświetlenia mogłaby wyglądać następująco (np. na stronie koszyka):

 <?php
 session_start();
 if (isset($_SESSION['message'])) {
    echo "<div class='msg'>" . $_SESSION['message'] . "</div>";
    unset($_SESSION['message']);  // usuwamy komunikat po wyświetleniu
 }

 // Wyświetlamy zawartość koszyka:
 if (!empty($_SESSION['cart'])) {
    foreach ($_SESSION['cart'] as $prodId) {
        echo "Produkt o ID $prodId<br>";
    }
 }

 ?>

Dzięki sesji komunikat message “przetrwał” przekierowanie na kolejną stronę, po czym został wypisany i skasowany. Podobnie zawartość koszyka (lista ID produktów) jest dostępna tak długo, jak trwa sesja użytkownika lub dopóki jej nie wyczyścimy.

Parametry sesji: czas życia, lokalizacja plików, identyfikator sesji

Sesja posiada pewne istotne parametry konfiguracyjne i środowiskowe, które warto znać:

  • Identyfikator sesji (session ID) – to unikalny ciąg znaków, który identyfikuje sesję użytkownika. Domyślnie PHP generuje losowy 32-znakowy identyfikator w formie szesnastkowej (przykładowy identyfikator może wyglądać tak: 4af5ac6val45rf2d5vre58sd648ce5f7)​. Identyfikator ten jest najczęściej przekazywany do przeglądarki w ciasteczku o nazwie PHPSESSID (domyślna nazwa sesji w PHP)​. Oznacza to, że po stronie klienta sesja jest reprezentowana właśnie przez ciasteczko session ID. Alternatywnie (choć obecnie rzadziej praktykowane ze względów bezpieczeństwa), identyfikator sesji może być przekazywany w URL (tzw. trans SID) lub polu formularza – jednak domyślnie PHP od wersji 7 ma tę funkcję wyłączoną. Sam identyfikator jest wartością losową i nie zawiera żadnych informacji o użytkowniku (jest to tylko token/klucz)​.
  • Lokalizacja i przechowywanie danych sesji – standardowo PHP zapisuje dane sesyjne na serwerze w plikach. Każda nowa sesja tworzy plik, którego nazwa to zazwyczaj sess_<identyfikator> (np. sess_4af5ac6val45rf2d5vre58sd648ce5f7) i który jest zapisywany w określonym katalogu na serwerze. Dzięki temu wszystkie zmienne zapisane w $_SESSION są zachowane między wywołaniami skryptów – po wywołaniu session_start() PHP odczytuje odpowiedni plik i odtwarza zmienne. Domyślna lokalizacja plików sesji to systemowy katalog tymczasowy (np. /tmp na serwerach Linux). Może być ona zmieniona za pomocą dyrektywy konfiguracyjnej session.save_path w php.ini​. Ważne jest, by katalog na pliki sesji był zapisywalny przez mechanizm PHP i odpowiednio zabezpieczony – na serwerach współdzielonych nie powinien to być katalog dostępny dla innych użytkowników systemu, aby nie umożliwić podejrzenia cudzych sesji​.
  • Czas życia sesji – po jakim czasie sesja wygaśnie? Odpowiedź nie jest jednoznaczna, ponieważ składają się na to dwa elementy:
    1. Czas życia ciasteczka sesji po stronie przeglądarki. Domyślnie ciasteczko sesyjne jest nietrwałe – session.cookie_lifetime ustawione jest na 0, co oznacza, że cookie wygaśnie z chwilą zamknięcia przeglądarki​php.net. Można zmienić ten parametr, aby sesja utrzymywała się nawet po zamknięciu i ponownym otwarciu przeglądarki (np. ustawiając session.cookie_lifetime na liczbę sekund odpowiadającą 7 dniom, sesja będzie utrzymywana tydzień – często stosowane przy opcjach typu „Zapamiętaj mnie”).
    2. Czas przechowywania danych sesji na serwerze. Nawet jeśli ciasteczko u klienta będzie ważne, może się okazać, że serwer usunie dane danej sesji po pewnym czasie nieaktywności. Parametr session.gc_maxlifetime określa liczbę sekund, przez jaką dane sesji na serwerze są przechowywane zanim zostaną uznane za nieużywane (ang. garbage) i mogą zostać usunięte​. Innymi słowy, jest to czas, przez jaki nieużywana (nieaktywna) sesja będzie utrzymywana. Domyślnie wynosi on 1440 sekund (24 minuty). Mechanizm usuwania starych sesji realizowany jest poprzez tzw. garbage collector, który uruchamia się z pewnym prawdopodobieństwem przy okazji startu nowej sesji. W praktyce oznacza to, że jeśli użytkownik nie wyśle żadnego żądania przez okres dłuższy niż gc_maxlifetime, jego sesja może wygasnąć (zostać skasowana po stronie serwera). Warto podkreślić, że jest różnica między wygaśnięciem sesji na skutek wyczyszczenia po stronie serwera a utratą sesji na skutek utraty cookie w przeglądarce – aby sesja była aktywna, muszą istnieć oba elementy. Z tego powodu często w aplikacjach wprowadza się dodatkowe własne mechanizmy monitorowania czasu bezczynności użytkownika (np. zapis znacznika czasu ostatniej aktywności w $_SESSION i manualne wylogowanie po określonym okresie braku aktywności), o czym więcej w sekcji o dobrych praktykach.
  • Inne parametry – Każda sesja ma też nazwę (domyślnie wspomniane PHPSESSID), którą można zmienić konfigurując session.name​ – bywa to stosowane np. gdy na jednym serwerze działa wiele aplikacji PHP i chcemy unikatowej nazwy ciasteczka sesji dla każdej z nich. Istnieją również ustawienia dotyczące przekazywania identyfikatora w adresach (session.use_trans_sid, session.use_only_cookies), bezpieczeństwa ID (session.use_strict_mode) i kilka innych – ich omówienie znajdzie się w dalszej części.

Zarządzanie sesjami: usuwanie danych, zamykanie sesji, zmiana ID

Podczas pracy z sesjami często potrzebujemy wykonać operacje takie jak wylogowanie użytkownika, co wiąże się z wyczyszczeniem danych sesji lub całkowitym jej zakończeniem. PHP udostępnia do tego następujące mechanizmy:

  • Usuwanie danych z sesji (pojedynczych zmiennych): Możemy skasować konkretną zmienną sesyjną analogicznie jak każdą inną zmienną PHP – np. unset($_SESSION['username']); usunie z sesji pozycję o kluczu 'username'. Jeśli chcemy wyczyścić wszystkie zmienne sesyjne (np. podczas wylogowania), najprościej użyć funkcji session_unset(), która usuwa całą zawartość tablicy $_SESSION​. Alternatywnie można też ustawić $_SESSION = array(); – efekt będzie podobny (wyczyszczenie tablicy).
  • Zniszczenie sesji: Aby całkowicie zakończyć sesję (np. wylogować użytkownika i unieważnić jego ID sesji), używamy funkcji session_destroy(). Usuwa ona całą sesję po stronie serwera – skasowany zostaje plik sesji oraz identyfikator przestaje być ważny​. Należy pamiętać, że przed wywołaniem session_destroy() dobrze jest wywołać session_start() (jeśli jeszcze nie było w danym skrypcie) oraz ewentualnie session_unset(), aby wyczyścić dane z bieżącej tablicy $_SESSION​. Przykładowy kod wylogowania może wyglądać następująco:
 session_start();     
 session_unset(); // usuń wszystkie zmienne sesji  
 session_destroy(); // zakończ sesję // (opcjonalnie skasuj również cookie sesyjne u klienta, wysyłając ciasteczko z datą w przeszłości)

Po wywołaniu session_destroy() obecna sesja przestaje istnieć – w razie kolejnego żądania od użytkownika (który może mieć jeszcze stare ciasteczko) nie zostanie ono już skojarzone z żadnymi danymi (należy utworzyć nową sesję w razie potrzeby). Innymi słowy, użytkownik został wylogowany.

  • Zmiana identyfikatora sesji: Wspomniana już funkcja session_regenerate_id() służy do wygenerowania nowego ID dla bieżącej sesji. Mechanizm ten nie kończy sesji, a jedynie podmienia jej identyfikator na inny (dzięki czemu np. atakujący, który znał poprzedni ID, traci do niej dostęp). Dane w $_SESSION zostają przeniesione pod nowy identyfikator, a stary może zostać usunięty (jeśli wywołano funkcję z argumentem true). Zaleca się użycie session_regenerate_id() przede wszystkim po zalogowaniu użytkownika oraz ewentualnie okresowo w trakcie bardzo długich sesji, aby utrudnić ataki polegające na przejęciu sesji użytkownika. Uwaga: Nie należy wywoływać tej funkcji zbyt często (np. przy każdym żądaniu), aby nie obciążać serwera i nie komplikować logiki aplikacji – wystarczą kluczowe momenty, takie jak zmiana uprawnień użytkownika (logowanie, nadanie wyższych uprawnień itp.).

Podsumowując, do poprawnego zarządzania sesją należy: odpowiednio czyścić dane (usuwanie pojedynczych kluczy lub wszystkich, w zależności od potrzeb), zamykać sesję gdy nie jest już potrzebna (np. przy wylogowaniu), oraz w razie potrzeby regenerować identyfikator dla bezpieczeństwa.

Bezpieczeństwo sesji

Sesje z założenia zwiększają bezpieczeństwo (względem przechowywania danych po stronie klienta w cookies), ale same w sobie również mogą stać się celem ataków. Główne zagrożenia związane z mechanizmem sesji to przejęcie sesji przez osoby niepowołane oraz tzw. atak utrwalenia sesji. Omówimy je krótko wraz ze sposobami zabezpieczenia aplikacji.

  • Session hijacking (przejęcie sesji): Ten rodzaj ataku polega na tym, że osoba atakująca stara się wykraść lub odgadnąć identyfikator sesji uprawnionego użytkownika, a następnie wykorzystać go, by uzyskać dostęp do jego konta. Innymi słowy, atakujący przejmuje token sesyjny ofiary i tym samym podszywa się pod nią. Jeśli mu się to uda, serwer nie odróżni atakującego od oryginalnego zalogowanego użytkownika, co skutkuje przejęciem jego sesji. Taki atak może być przeprowadzony na różne sposoby – np. poprzez podsłuchanie niezabezpieczonego ruchu (wykradzenie cookie sesyjnego przesyłanego czystym tekstem), poprzez atak XSS (który pozwoli odczytać cookie sesyjne skryptem w przeglądarce), atak Man-in-the-Middle itp. W efekcie atakujący uzyskuje ważny session ID i może uzyskać nieautoryzowany dostęp​. Jak się zabezpieczyć? Przed tego typu atakiem należy wprowadzić szereg zabezpieczeń:
    • Używaj szyfrowanego połączenia (HTTPS): Podstawową ochroną jest użycie protokołu HTTPS, tak aby komunikacja między przeglądarką a serwerem była szyfrowana. Wówczas przechwycenie identyfikatora sesji staje się znacznie trudniejsze. Włączenie flagi session.cookie_secure ustawia ciasteczko sesyjne tak, by było przesyłane tylko po HTTPS​ – dzięki temu przeglądarka nie wyśle tokenu sesji przez niezabezpieczony kanał.
    • Ogranicz dostęp JavaScript do cookie sesji: Ustaw flagę HttpOnly dla ciasteczka sesyjnego (session.cookie_httponly = 1 w konfiguracji) – spowoduje to, że skrypty po stronie klienta (JavaScript) nie będą mogły odczytać wartości cookie​. Chroni to przed kradzieżą ID sesji przy atakach XSS, ponieważ nawet jeśli na stronie jest luka XSS, wstrzyknięty złośliwy skrypt nie wyciągnie wartości sesji z ciasteczka (przeglądarka zablokuje taki dostęp).
    • Dbaj o losowość i długość identyfikatorów: PHP domyślnie generuje dość długie, kryptograficznie losowe identyfikatory sesji, co czyni ich odgadnięcie mało prawdopodobnym (spełniają one wymóg co najmniej 64 bitów entropii zgodnie z zaleceniami OWASP). Ważne, aby nie stosować własnych mechanizmów generowania ID o niższej losowości.
    • Kontroluj ważność sesji w czasie: Nie powinno się pozwalać na nieograniczenie długie sesje. Warto ustawić rozsądny czas żywotności sesji (np. poprzez gc_maxlifetime oraz mechanizm wygaszania po stronie aplikacji) – dzięki temu nawet jeśli ID wycieknie, to po pewnym czasie stanie się bezużyteczne. Również po stronie klienta zbyt długo ważne cookies mogą stanowić ryzyko, dlatego należy rozważyć, czy na pewno potrzebujemy bardzo długiego cookie_lifetime. Często stosuje się strategię, że po okresie bezczynności (np. 15-30 minut) sesja użytkownika jest unieważniana i musi on zalogować się ponownie. Takie podejście znacząco utrudnia wykorzystanie przechwyconych sesji.
    • Weryfikacja tożsamości użytkownika przy żądaniach: Można zaimplementować dodatkowe sprawdzanie, czy sesja nie została przejęta, poprzez weryfikację pewnych cech każdego żądania. Przykładowo, można zapisać w sesji adres IP oraz ciąg identyfikujący przeglądarkę (User-Agent) użytkownika w momencie logowania, a następnie przy każdym kolejnym żądaniu sprawdzać, czy te parametry się zgadzają. Jeśli nastąpi zmiana (np. inne IP w trakcie jednej sesji), może to sugerować przejęcie sesji i skutkować wylogowaniem​. Ograniczeniem jest tu zmienność adresu IP u niektórych dostawców internetu – stąd mechanizm ten należy stosować rozważnie, by nie wylogowywać prawidłowych użytkowników.
    • Unikaj przekazywania session ID w URL: Upewnij się, że opcja przekazywania identyfikatora sesji w adresie URL jest wyłączona (session.use_trans_sid = 0, co jest domyślne). ID widoczne w adresie łatwo przypadkowo ujawnić (np. w logach serwera, historii przeglądarki, poprzez przesłanie linku znajomemu itp.). Dlatego lepiej polegać wyłącznie na cookies (ustawienie session.use_only_cookies = 1).
  • Session fixation (atak utrwalenia sesji): Ten atak różni się od powyższego tym, że zamiast kraść istniejącą sesję, atakujący próbuje narzucić ofierze identyfikator sesji z góry wybrany przez siebie. Scenariusz bywa następujący: napastnik inicjuje sesję na serwerze (uzyskuje pewien session ID), a następnie nakłania ofiarę do skorzystania z tego identyfikatora – np. wysyłając jej specjalny link zawierający PHPSESSID=... lub poprzez wcześniejsze ustawienie cookie tego ID w przeglądarce ofiary (jeśli ma taką możliwość). Gdy ofiara zaloguje się w aplikacji mając już ten narzucony identyfikator, atakujący zna jej session ID i tym samym zyskuje dostęp do uwierzytelnionej sesji​. Jak zapobiegać? Obrona przed session fixation jest stosunkowo prosta: należy zmieniać identyfikator sesji przy przejściu użytkownika do stanu uwierzytelnionego. Innymi słowy, po zalogowaniu (lub utworzeniu nowej sesji gościa) zawsze wywołuj session_regenerate_id() – dzięki temu nawet jeśli ofiara miała narzucony przez atakującego stary identyfikator, to po zalogowaniu otrzyma nowy, którego atakujący już nie zna​. W efekcie atakujący zostaje odcięty od sesji. Dodatkowo warto włączyć opcję session.use_strict_mode = 1 (jeśli nie jest domyślnie włączona). Strict mode sprawia, że PHP nie zaakceptuje identyfikatora sesji, który nie został wygenerowany przez niego uprzednio (np. przez wywołanie session_id() lub session_start())​. Gdy przeglądarka zgłosi nieistniejący ID sesji, serwer utworzy zamiast tego nową sesję o innym ID. Ta opcja została wprowadzona jako zabezpieczenie właśnie przed atakami fixation – uniemożliwia atakującemu “przygotowanie” sesji i zmuszenie ofiary do jej przejęcia.

Podsumowując, główna zasada bezpieczeństwa to traktować identyfikator sesji jak wrażliwe dane uwierzytelniające – podobnie jak hasło. Musi on być tajny i odpowiednio chroniony, a wszelkie operacje zwiększające uprawnienia użytkownika (np. logowanie) powinny skutkować zmianą tego identyfikatora.

Ustawienia konfiguracyjne PHP wpływające na sesje

PHP posiada wiele dyrektyw konfiguracyjnych (ustawianych w php.ini lub funkcjami runtime), które determinują zachowanie mechanizmu sesji. Poniżej wymieniono najważniejsze z nich, wraz z omówieniem:

  • session.cookie_lifetime – czas życia ciasteczka sesyjnego (w sekundach) wysyłanego do przeglądarki. Wartość 0 oznacza, że cookie wygaśnie z chwilą zamknięcia przeglądarki (czyli jest to tzw. session cookie niewykraczające poza sesję przeglądarki). Ustawienie np. 3600 sprawi, że ciasteczko będzie ważne przez 3600 sekund od momentu ustawienia (1 godzina), niezależnie od tego, czy przeglądarka jest zamknięta czy nie.
  • session.gc_maxlifetime – maksymalny czas życia danych sesji po stronie serwera (w sekundach)​. Po upływie tego czasu od ostatniego użycia sesji, dane mogą zostać usunięte przez mechanizm garbage collectora. Należy pamiętać, że jeśli różne skrypty/aplikacje współdzielą magazyn sesji, to najniższa wartość gc_maxlifetime spośród nich będzie decydować o usuwaniu danych (najbardziej restrykcyjny czas wygaśnięcia)​. Domyślnie 1440 sek (24 minuty). Uwaga: na niektórych serwerach (np. dystrybucje Linuksa Debian/Ubuntu) czyszczenie starych sesji jest realizowane przez zewnętrzny mechanizm (cron) bazujący na globalnej konfiguracji, co może sprawić, że zmiana gc_maxlifetime w .htaccess lub skrypcie nie zadziała, jeśli nie zmienimy też globalnie ustawień lub nie zastosujemy własnego mechanizmu przechowywania sesji.
  • session.cookie_secure – flaga określająca, czy ciasteczko sesji ma być przesyłane tylko po bezpiecznym połączeniu (HTTPS)​. Gdy ustawiona na 1 (true), przeglądarka nie wyśle ciasteczka przez połączenie nieszyfrowane HTTP. Zaleca się włączenie tej opcji na stronach działających pod HTTPS (na stronach dostępnych po HTTP nie należy jej włączać, bo wtedy sesje w ogóle by nie działały dla użytkowników na HTTP).
  • session.cookie_httponly – flaga oznaczająca, że ciasteczko sesyjne ma być tylko dla protokołu HTTP​. Ustawienie session.cookie_httponly = 1 spowoduje dodanie flagi HttpOnly do ciasteczka, co zabezpiecza przed dostępem do niego przez JavaScript (mitigacja ataków XSS kradnących cookies).
  • session.cookie_samesite – ustawia atrybut SameSite dla cookie sesyjnego​. Może przyjmować wartości Lax, Strict albo None (lub być pusty/brak, co oznacza brak ustawienia SameSite). Atrybut SameSite pozwala ograniczyć wysyłanie ciasteczka w żądaniach cross-site, co stanowi ochronę przed atakami typu CSRF (Cross-Site Request Forgery). Strict – ciasteczko nie jest wysyłane w ogóle w żądaniach do domeny z innej domeny (nawet przy kliknięciu linku). Lax – ciasteczko nie jest wysyłane przy żądaniach typu POST z innej strony, ale wysyłane jest przy nawigacji (GET). None – wyłącza ograniczenie (ciasteczko zawsze wysyłane, wymaga jednak ustawienia Secure). Dobrym domyślnym wyborem jest Lax lub Strict w zależności od potrzeb aplikacji.
  • session.save_path – ścieżka do katalogu, gdzie przechowywane są pliki sesji (w przypadku domyślnego mechanizmu plikowego)​. Jeśli sesje mają być zapisywane w niestandardowym miejscu (np. osobny katalog dla aplikacji, lub NFS, czy inny współdzielony system plików między serwerami), należy tutaj ustawić odpowiednią ścieżkę. Możliwe jest również ustawienie poziomów zagnieżdżenia katalogów (opcja N; poprzedzająca ścieżkę) w celu lepszej organizacji dużej liczby plików – lecz w prostych zastosowaniach nie ma takiej potrzeby.
  • session.name – nazwa sesji, która będzie używana jako nazwa cookie​. Domyślnie PHPSESSID. Można zmienić, jeśli chcemy ukryć fakt używania PHP lub mieć unikalną nazwę dla aplikacji. Należy używać tylko liter i cyfr (bez spacji, przecinków itp.) żeby cookie działało poprawnie.
  • session.auto_start – czy automatycznie startować sesję dla każdego requestu​. Domyślnie 0 (wyłączone). Gdyby ustawić na 1, sesja wystartuje automatycznie na początku każdego skryptu PHP bez potrzeby wywoływania session_start(). Jednak ta opcja bywa rzadko używana, bo zmniejsza kontrolę nad tym, gdzie sesja jest tworzona; zazwyczaj lepiej samodzielnie wywoływać session_start() wtedy, gdy jest to potrzebne.
  • session.use_cookies / session.use_only_cookies – dotyczą mechanizmu przekazywania ID sesji. session.use_cookies (domyślnie 1) oznacza, że PHP będzie używać ciasteczek do przekazywania identyfikatora sesji. session.use_only_cookies (domyślnie 1) oznacza, że nie będzie doklejać identyfikatora do URL czy formularzy​. Innymi słowy, wymusza korzystanie tylko z cookie. Wyłączenie tej opcji (0) pozwalałoby na alternatywne metody transmisji ID (np. przez URL), co – jak już omawiano – nie jest bezpieczne. Zatem w normalnej sytuacji te opcje powinny pozostać włączone (zwłaszcza use_only_cookies = 1).
  • session.use_trans_sid – powiązana z powyższym, domyślnie 0. Gdyby była włączona, PHP automatycznie dodawałby identyfikator sesji do linków (URL) i formularzy HTML generowanych przez skrypt, gdy cookies są niedostępne. Ze względów bezpieczeństwa (i SEO) ta funkcja jest odradzana – w nowoczesnych aplikacjach zazwyczaj trzymamy się cookies. Dokumentacja ostrzega, że sesje oparte na URL niosą dodatkowe ryzyko (np. udostępnienia linku z aktywnym ID znajomym, zapisanie w zakładkach itp.)​.
  • session.use_strict_mode – jak wspomniano wcześniej, włączenie tej opcji (1) powoduje, że PHP nie zaakceptuje identyfikatora sesji od klienta, jeśli ten identyfikator nie istnieje na serwerze (nie został wcześniej wygenerowany)​. Bez tego mechanizmu, gdy przeglądarka wyśle np. zgadywany lub stary ID, PHP po prostu utworzy nową sesję o takim ID (jeśli nie znalazł istniejącej) – co może zostać wykorzystane w ataku session fixation. W trybie strict, zamiast tego zostanie utworzony nowy ID. Od PHP 7.0+ zaleca się tę opcję włączyć zawsze dla bezpieczeństwa.

Powyższe ustawienia możemy zmieniać w pliku php.ini, a wiele z nich także w trakcie działania skryptu (przy użyciu ini_set() lub specjalnych funkcji jak session_set_cookie_params() dla ustawień cookie). Dobre zrozumienie tych parametrów pozwala dostosować sposób działania sesji do wymagań aplikacji (np. wydłużyć lub skrócić czas trwania sesji, zwiększyć bezpieczeństwo ciasteczek, zmienić magazyn sesji na inny niż plikowy itp.).

Dobre praktyki pracy z sesjami

Na koniec zebraliśmy kluczowe dobre praktyki dotyczące korzystania z sesji w PHP, podsumowując wiele z powyższych zaleceń:

  • Stosuj bezpieczne ustawienia ciasteczek sesji: zawsze włączaj session.cookie_secure na stronach HTTPS, aby token sesji nie był przesyłany przez nieszyfrowany kanał​. Włącz także session.cookie_httponly, by chronić cookie przed dostępem skryptów po stronie klienta​. Rozważ ustawienie session.cookie_samesite=Lax lub Strict zależnie od charakteru aplikacji, by ograniczyć ryzyko CSRF. Te proste ustawienia znacząco podnoszą odporność sesji na ataki.
  • Regeneruj ID sesji przy ważnych wydarzeniach: zawsze wywołuj session_regenerate_id() po zalogowaniu użytkownika lub zmianie jego uprawnień. Dzięki temu unieważnisz ewentualne przechwycone/narzucane wcześniej identyfikatory​. Możesz również regenerować ID co pewną liczbę żądań lub po upływie pewnego czasu podczas dłuższych sesji, aby ograniczyć okno czasowe, w którym skompromitowany ID byłby użyteczny.
  • Ogranicz czas trwania sesji i bezczynności: nie pozwalaj, by sesja trwała w nieskończoność. Implementuj mechanizm automatycznego wylogowania po okresie bezczynności użytkownika (np. 15-30 minut) – zgodnie z zaleceniami, każda sesja powinna mieć ustawiony timeout bezczynności skutkujący unieważnieniem sesji po określonym czasie braku aktywności​. Dodatkowo możesz wprowadzić absolutny czas życia sesji (np. maksymalnie kilka godzin), po upływie którego wymuszone będzie ponowne zalogowanie, nawet jeśli użytkownik był aktywny. Takie zasady minimalizują ryzyko przejęcia sesji – nawet jeśli token wycieknie, atakujący ma mało czasu na jego wykorzystanie.
  • Nie przechowuj w sesji informacji wrażliwych w postaci niezaszyfrowanej: unikaj trzymania w $_SESSION bardzo wrażliwych danych, takich jak hasła w postaci jawnej, numery kart kredytowych, czy inne poufne informacje. Sesja co prawda jest po stronie serwera, ale potencjalny atakujący, który uzyska dostęp do plików sesji (np. poprzez lukę w aplikacji lub dostęp do serwera), mógłby je odczytać. Jeśli już musisz przechować takie dane chwilowo, rozważ ich zaszyfrowanie lub inną formę ochrony​. Generalnie w sesji najlepiej trzymać identyfikatory, flagi stanu, ewentualnie niewielkie mniej wrażliwe informacje – natomiast dane typu hasło czy klucz API lepiej przechowywać bezpiecznie w bazie, a w sesji trzymać jedynie odwołanie do nich (np. ID użytkownika, a nie hasło).
  • Waliduj sesję przy każdym żądaniu (jeśli to potrzebne): za każdym razem, gdy wykonujesz operacje wymagające uwierzytelnienia, upewniaj się, że użytkownik ma aktywną, ważną sesję i odpowiednie uprawnienia. Sprawdzaj istnienie kluczowych zmiennych sesyjnych (np. czy $_SESSION['is_logged_in'] jest ustawione i true). Rozważ dodatkowe walidacje, jak wspomniana kontrola adresu IP czy User-Agent zapisanych w sesji względem bieżących – zwłaszcza w aplikacjach wymagających wysokiego poziomu bezpieczeństwa​. Pozwoli to wykryć potencjalne przejęcia sesji w trakcie jej trwania (choć nie eliminuje wszystkich przypadków).
  • Zawsze poprawnie kończ sesję przy wylogowaniu: to może wydawać się oczywiste, ale warto podkreślić – zapewnij funkcjonalność wylogowania użytkownika i w jej ramach usuń wszystkie dane sesyjne oraz zniszcz sesję (session_destroy()). Dodatkowo możesz skasować ciasteczko sesyjne po stronie przeglądarki (wysyłając nagłówek Set-Cookie z datą wygaśnięcia w przeszłości), aby nie pozostał po nim ślad. Dzięki temu po wylogowaniu nie ma ryzyka, że ktoś “odziedziczy” starą sesję, np. korzystając z tego samego komputera.
  • Unikaj błędów związanych z kolejnością wysyłania danych: pamiętaj zawsze, by rozpocząć sesję przed jakimkolwiek wyjściem. Jeśli zobaczysz błąd typu “Headers already sent”, oznacza to, że wywołałeś session_start() za późno. Dobra praktyka to umieszczanie session_start() zaraz po otwarciu tagu PHP, zanim wykonany zostanie jakikolwiek kod HTML czy wyjście. To drobna kwestia, ale częsta pułapka dla początkujących.

Stosując powyższe zasady, możemy bezpiecznie i efektywnie korzystać z mechanizmu sesji w PHP. Sesje są potężnym narzędziem, które odpowiednio użyte pozwalają tworzyć dynamiczne, spersonalizowane aplikacje webowe – warto jednak zawsze pamiętać o kwestiach bezpieczeństwa i optymalnej konfiguracji. Dzięki temu sesje będą służyć nam jako niezawodny sposób przechowywania danych użytkownika w trakcie jego wizyty na stronie, bez narażania tych danych na niepotrzebne ryzyko.​

Operacje na plikach w PHP

Wprowadzenie do obsługi plików

Obsługa plików w PHP pozwala aplikacjom webowym na trwałe przechowywanie danych oraz dostęp do zasobów plikowych serwera. Dzięki funkcjom do operacji na plikach możemy m.in. zapisywać logi, odczytywać konfiguracje, przetwarzać przesłane pliki czy generować raporty. W przeciwieństwie do operacji po stronie klienta (np. w JavaScript), operacje na plikach w PHP wykonywane są po stronie serwera i dotyczą plików znajdujących się w systemie plików serwera. Należy pamiętać, że skrypt PHP musi mieć odpowiednie uprawnienia do odczytu lub zapisu pliku na serwerze – inaczej operacja się nie powiedzie.

W niniejszym wykładzie omówimy podstawowe operacje na plikach: otwieranie i zamykanie plików, odczyt i zapis danych, tworzenie nowych plików oraz usuwanie plików. Przedstawimy praktyczne przykłady kodu PHP ilustrujące użycie funkcji takich jak fopen, fread, fwrite, fclose, file_get_contents, file_put_contents, unlink i innych. Ponadto poruszymy kwestie obsługi błędów i zabezpieczeń (sprawdzanie istnienia pliku, prawa dostępu, bezpieczne ścieżki), pracę z katalogami (tworzenie, usuwanie, odczyt zawartości katalogów) oraz przetwarzanie plików tekstowych, w tym formatu CSV. Na koniec przedstawimy dobre praktyki, takie jak buforowanie operacji, używanie blokad plików oraz unikanie ataków typu Path Traversal. Wszystkie przykłady będą opatrzone komentarzami ułatwiającymi zrozumienie kodu.

Otwieranie i zamykanie plików

Zanim rozpoczniemy odczyt lub zapis danych, musimy otworzyć plik. W PHP służy do tego funkcja fopen(), która zwraca tzw. wskaźnik (uchwyt) do pliku. Wywołujemy ją z dwoma podstawowymi argumentami: ścieżką/nazwą pliku oraz trybem otwarcia. Tryb określa, w jaki sposób chcemy plik otworzyć – np. tylko do odczytu, do zapisu, do dopisania danych na końcu, itd. Poniżej przedstawiono najczęściej używane tryby otwarcia plików:

  • 'r' – otwiera plik do odczytu (ang. read). Wskaźnik pliku ustawiany jest na początek. Plik musi istnieć, inaczej funkcja zwróci błąd (false).
  • 'w' – otwiera plik do zapisu (ang. write). Tworzy nowy plik, jeśli nie istnieje, lub czyści zawartość istniejącego pliku do zera (nadpisuje go od początku)​. Wskaźnik ustawiany jest na początek pliku.
  • 'a' – otwiera plik do dopisywania danych (ang. append). Wskaźnik ustawiany jest na koniec pliku, więc dane będą dopisywane. Jeśli plik nie istnieje, zostanie utworzony.
  • 'x' – otwiera plik do zapisu wyłącznie gdy plik nie istnieje. Używany do utworzenia nowego pliku; jeśli plik o podanej nazwie już istnieje, funkcja zwróci błąd. Ten tryb pozwala uniknąć przypadkowego nadpisania istniejącego pliku.
  • 'r+', 'w+', 'a+' – tryby łączone umożliwiające jednoczesny odczyt i zapis. Na przykład 'r+' otwiera plik do odczytu i zapisu (wymaga istnienia pliku), 'w+' tworzy nowy plik do odczytu i zapisu (lub czyści istniejący), a 'a+' otwiera plik do dopisywania i odczytu (wskazując na koniec).
  • 'b' – modyfikator trybu binarnego. W systemach Windows tryb tekstowy i binarny różnią się obsługą znaków końca linii. Dodanie 'b' (np. "rb" lub "wb") powoduje odczyt/zapis binarny bez modyfikacji danych (zalecane np. przy odczycie plików binarnych jak obrazy). W systemach Unix/Linux modyfikator ten nie wpływa na działanie, ale dla przenośności warto go używać przy danych binarnych.

Aby otworzyć plik, wywołujemy fopen() z odpowiednimi parametrami. Funkcja ta zwraca uchwyt do pliku (typu resource lub obiekt stream), którego będziemy używać w kolejnych operacjach na pliku. Jeśli otwarcie się nie powiedzie (np. plik nie istnieje w trybie 'r’ lub brak uprawnień), fopen() zwróci wartość false oraz wygeneruje ostrzeżenie (warning). Dlatego często łączy się fopen() z instrukcją warunkową lub operatorem tłumienia błędów. Przykładowe otwarcie pliku do odczytu:

 <?php
 $plik = fopen("dane.txt", "r");
 if ($plik === false) {
    // Obsługa błędu otwarcia pliku
    die("Nie udało się otworzyć pliku do odczytu!");
 }
 // ... (tutaj wykonujemy operacje na pliku) ...
 fclose($plik);
 ?>

W powyższym kodzie próbujemy otworzyć plik dane.txt w trybie odczytu. Jeśli fopen zwróci false, przerywamy działanie skryptu z komunikatem błędu (w rzeczywistej aplikacji zamiast die() można np. wyświetlić komunikat użytkownikowi lub podjąć inną akcję). Gdy plik zostanie pomyślnie otwarty, na zmiennej $plik mamy uchwyt do pliku i możemy wykonywać na nim dalsze operacje.

Zamykanie pliku: Po zakończeniu pracy z plikiem należy go zamknąć funkcją fclose($uchwyt). Zamknięcie pliku zwalnia zasoby systemowe związane z plikiem. Wprawdzie PHP automatycznie zamyka wszystkie otwarte pliki po zakończeniu skryptu, ale zaleca się zamykanie ich explicite, gdy tylko przestają być potrzebne. W powyższym przykładzie zamykamy plik wywołując fclose($plik) na końcu.

Uwaga: Uchwyt pliku (np. zmienna $plik zwrócona przez fopen) jest potrzebny do większości operacji niskopoziomowego odczytu i zapisu. Niektóre uproszczone funkcje (jak file_get_contents czy file_put_contents) nie wymagają jawnego otwierania i zamykania pliku – robią to wewnętrznie – ale w przypadku korzystania z fopen() zawsze pamiętaj o fclose().

Odczyt danych z pliku

Po otwarciu pliku do odczytu możemy pobrać z niego dane na kilka sposobów. Najprostszą funkcją do czytania z pliku (przy użyciu uchwytu) jest fread($uchwyt, $length), która odczytuje z pliku określoną liczbę bajtów. Najczęściej używa się jej, gdy chcemy wczytać całą zawartość pliku na raz – wtedy jako $length podajemy rozmiar pliku w bajtach. Rozmiar pliku można uzyskać np. funkcją filesize($nazwa_pliku). Przykład odczytu całego pliku za pomocą fread():

 <?php
 $plik = fopen("dane.txt", "r");
 if ($plik) {
    $rozmiar = filesize("dane.txt");           // pobranie rozmiaru pliku w bajtach
    $zawartosc = fread($plik, $rozmiar);       // odczyt calej zawartosci
    fclose($plik);
    echo "Zawartość pliku:\n$zawartosc";
 } else {
    echo "Błąd: nie można otworzyć pliku.";
 }
 ?>

W powyższym kodzie otwieramy plik, następnie fread() wczytuje $rozmiar bajtów (czyli cały plik) do zmiennej $zawartosc, po czym plik jest zamykany, a zawartość wyświetlana. Należy jednak uważać przy tej metodzie – jeśli plik jest bardzo duży, próba wczytania go w całości do pamięci może obciążyć serwer lub przekroczyć limity pamięci. W takich sytuacjach lepiej czytać plik stopniowo, fragmentami.

Odczyt liniami lub fragmentami: PHP udostępnia funkcję fgets($uchwyt), która odczytuje jedną linię tekstu z pliku (do napotkania znaku nowej linii \n lub do osiągnięcia zadanej długości). Można jej użyć do wczytywania pliku linia po linii w pętli. Przykład odczytu pliku linia po linii:

 <?php
 $plik = fopen("dane.txt", "r");
 if ($plik) {
    while (($linia = fgets($plik)) !== false) {
        // Usuwamy ewentualny znak nowej linii z końca linii:
        $linia = rtrim($linia, "\r\n");
        echo "Przeczytano: $linia\n";
    }
    fclose($plik);
 }
 ?>

W tej pętli while funkcja fgets() zwraca kolejną linię tekstu przy każdym wywołaniu, a gdy osiągnie koniec pliku, zwróci false i pętla się zakończy. Używamy rtrim aby usunąć znaki końca linii. Alternatywnie można użyć funkcji feof($uchwyt) (end-of-file) w warunku pętli, np.:

 while (!feof($plik)) {
    $linia = fgets($plik);
    if ($linia !== false) {
        // ...
    }
 } 

Jednak preferowanym sposobem iteracji jest ten pierwszy, z bezpośrednim sprawdzaniem wyniku fgets().

Szybkie wczytanie całego pliku: Często wygodniejsze jest skorzystanie z funkcji file_get_contents($filename), która czyta cały plik na raz bez potrzeby użycia fopen() i fclose(). Zwraca ona zawartość pliku jako jeden duży łańcuch tekstowy (string) lub false w razie błędu. Przykład:

 <?php
 $dane = file_get_contents("dane.txt");
 if ($dane === false) {
    echo "Błąd odczytu pliku.";
 } else {
    echo "Zawartość:\n$dane";
 }
 ?>

Funkcja file_get_contents jest bardzo prosta w użyciu – wystarczy podać nazwę pliku. Według dokumentacji jest to preferowany sposób odczytywania zawartości pliku do pojedynczego łańcucha znaków (wykorzystuje wewnętrzne mechanizmy, takie jak mapowanie pliku w pamięci, aby przyspieszyć operację)​w3schools.com. Należy tylko pamiętać, że analogicznie jak w przypadku fread całego pliku, użycie file_get_contents na bardzo dużym pliku może być niewydajne pamięciowo – w takich przypadkach czytanie porcjami jest bezpieczniejsze.

Przykład: Jeśli plik dane.txt zawiera np. listę nazwisk, powyższy kod jednorazowo wczyta całą zawartość i wyświetli ją. Gdybyśmy chcieli przetworzyć plik stopniowo (np. wypisać numerowane linie), lepszy byłby wariant z fgets w pętli.

PHP oferuje także inne funkcje pomocnicze, np. file($filename), która czyta cały plik do tablicy, gdzie każdy element to jeden wiersz tekstu pliku. Można ją wykorzystać, jeśli chcemy łatwo iterować po liniach bez samodzielnego pisania pętli – ale podobnie jak file_get_contents, wczytuje wszystko do pamięci na raz.

Podsumowując, aby odczytać plik w PHP:

  1. Otwieramy go (fopen) w trybie odczytu 'r' (lub 'r+' jeśli także będziemy zapisywać).
  2. Używamy odpowiedniej funkcji do czytania: fread (określoną liczbę bajtów), fgets (po jednej linii), lub prostszych metod jak file_get_contents/file.
  3. Po zakończeniu odczytu zamykamy plik (fclose).

Zapis danych do pliku

Zapis do pliku w PHP również można wykonać na dwa sposoby: przy użyciu uchwytu pliku (fopen + fwrite()/fputs()) lub korzystając z wygodnej funkcji file_put_contents(). Omówimy najpierw podejście z fwrite.

Aby zapisać dane, otwieramy plik w trybie zapisu, np. 'w' (zawsze zaczynamy od początku, nadpisując) lub 'a' (dopisywanie na końcu). Jeżeli plik nie istnieje, tryb 'w' i 'a' spowodują jego utworzenie​. Następnie wywołujemy fwrite($uchwyt, $dane), aby zapisać ciąg znaków do pliku. Funkcja ta zwraca liczbę zapisanych bajtów lub false w przypadku błędu.

Przykład zapisu: Utwórzmy nowy plik i zapiszmy w nim kilka linii tekstu:

 <?php
 $plik = fopen("wyjscie.txt", "w");
 if (!$plik) {
    die("Nie można otworzyć pliku do zapisu!");
 }
 $tekst1 = "Linia pierwsza\n";
 $tekst2 = "Linia druga\n";
 fwrite($plik, $tekst1);
 fwrite($plik, $tekst2);
 fclose($plik);
 echo "Dane zostały zapisane do pliku.";
 ?>

W tym kodzie otwieramy (lub tworzymy) plik wyjscie.txt w bieżącym katalogu i zapisujemy do niego dwie linie tekstu. Zwróć uwagę na dodawanie znaku nowej linii \n na końcu każdej linii – fwrite nie dodaje automatycznie znaków końca linii, zapisuje dokładnie to, co znajduje się w przekazanym łańcuchu. Po zakończeniu zapisu zamykamy plik. Po wykonaniu skryptu plik wyjscie.txt będzie zawierał:

Linia pierwsza
Linia druga

Jeśli w powyższym przykładzie plik wyjscie.txt już istniał, został nadpisany od początku (wcześniejsza zawartość utracona). Gdybyśmy chcieli dopisywać do istniejącego pliku bez kasowania poprzednich danych, należy otworzyć go w trybie 'a' (append):

 $plik = fopen("log.txt", "a"); fwrite($plik, "Nowy wpis logu\n");fclose($plik);

Powyższy fragment dopisze na końcu pliku log.txt nową linię. Tryb 'a' gwarantuje, że wskaźnik jest zawsze na końcu – nawet jeśli inny proces dopisze coś w międzyczasie, nasz fwrite i tak dołoży dane na końcu aktualnego stanu pliku.

PHP posiada również funkcję file_put_contents($filename, $data, [$flags]), która upraszcza proces zapisu. Wewnątrz jest ona odpowiednikiem kolejnych wywołań fopen(), fwrite() i fclose()​ – wystarczy jedna linijka, aby zapisać dane do wskazanego pliku. Jeśli plik o podanej nazwie nie istnieje, zostanie utworzony; jeśli istnieje – domyślnie jego zawartość zostanie nadpisana od początku​php.net. Możemy jednak przekazać opcjonalną flagę FILE_APPEND, aby dopisywać dane na końcu pliku zamiast go nadpisywać.

Przykłady użycia file_put_contents():

 // Zapis do nowego pliku (lub nadpisanie istniejącego):
 file_put_contents("raport.txt", "Treść raportu\n");

 // Dopisanie do pliku (nie kasuje poprzedniej zawartości):
 file_put_contents("raport.txt", "Kolejna linia raportu\n", FILE_APPEND);

Pierwsza linia utworzy plik raport.txt z podaną zawartością (lub zastąpi całą zawartość jeśli plik istniał). Druga linia demonstruje użycie flagi FILE_APPEND – sprawia ona, że tekst zostanie dodany na końcu pliku raport.txt zamiast nadpisywać go od początku. Funkcja file_put_contents zwraca liczbę zapisanych bajtów lub false w przypadku błędu, więc również można (choć rzadziej się to robi) sprawdzić jej wynik dla pewności.

Uwaga: file_put_contents ma też flagę LOCK_EX, która powoduje założenie wyłącznej blokady na plik podczas zapisu​. Omówimy to szerzej w sekcji o blokadach plików, ale w skrócie: jeśli wykonujemy jednoczesny zapis z wielu procesów/PHP (np. przy dużym ruchu), warto użyć LOCK_EX, by uniknąć nadpisywania się danych (dzieje się to kosztem krótkiego zablokowania pliku na czas zapisu).

Tworzenie nowych plików

Tworzenie pliku w PHP następuje zazwyczaj po prostu poprzez jego otwarcie w trybie zapisu. Jak już wspomniano, tryby takie jak 'w' czy 'a' spowodują utworzenie pliku, jeśli nie istnieje​. Nie istnieje osobna funkcja „utwórz pusty plik” – zamiast tego używamy fopen. Na przykład:

 // Utworzenie pustego pliku (ew. wyczyszczenie istniejącego):
 fopen("nowy_plik.txt", "w");
 fclose($plik);

Powyższy kod (otwórz i zaraz zamknij w trybie 'w') utworzy plik nowy_plik.txt o zerowej długości. Oczywiście zazwyczaj od razu po otwarciu dokonujemy zapisu danych. Można również użyć funkcji touch("plik.txt"), która działa podobnie jak polecenie touch w systemie – tworzy pusty plik lub aktualizuje czas modyfikacji pliku, jeśli już istnieje.

Warto pamiętać, że jeżeli chcemy utworzyć nowy plik, ale zabezpieczyć się przed wyścigiem z innym procesem (dwa procesy naraz próbujące utworzyć plik o tej samej nazwie) lub przed przypadkowym nadpisaniem istniejącego pliku, lepiej użyć trybu 'x'. Tryb 'x' spowoduje, że jeśli plik już istnieje, fopen zwróci błąd zamiast go otwierać. Możemy to wykorzystać do bezpiecznego tworzenia plików, które nie powinny już istnieć (np. unikalnych plików tymczasowych).

Usuwanie plików

Usuwanie (kasowanie) pliku z dysku realizuje funkcja unlink($filename). Nazwa pochodzi od funkcji systemowej unlink w Uniksie i oznacza usunięcie powiązania nazwy pliku z jego zawartością (w praktyce – skasowanie pliku). Funkcja unlink przyjmuje ścieżkę do pliku i zwraca true w przypadku powodzenia lub false gdy operacja się nie powiedzie​ (np. gdy plik nie istnieje lub brak uprawnień). W razie błędu pojawi się także ostrzeżenie PHP. Przykład:

 <?php
 $plik = "dane.txt";
 if (file_exists($plik)) {
    if (unlink($plik)) {
        echo "Plik $plik został usunięty.";
    } else {
        echo "Błąd: nie udało się usunąć pliku $plik.";
    }
 } else {
    echo "Plik $plik nie istnieje.";
 }
 ?>

Tutaj najpierw sprawdzamy, czy plik istnieje (funkcją file_exists). Jeśli tak, wywołujemy unlink. W zależności od wyniku informujemy, czy operacja się powiodła. W praktyce, często wywołuje się po prostu unlink("sciezka/do/pliku") bez sprawdzania istnienia – jeśli plik nie istnieje, to i tak nie ma co usuwać (ewentualny warning można wyłapać lub zignorować). Natomiast sprawdzenie jest przydatne, gdy chcemy zareagować na brak pliku inaczej niż na błąd przy próbie usunięcia.

Uwaga: Usunięcie otwartego pliku – na systemach Unixowych – nie spowoduje błędu, jeśli mamy uprawnienia, ale dopóki jakiś proces trzyma uchwyt do tego pliku, plik fizycznie pozostanie na dysku (bez nazwy) i zostanie ostatecznie zwolniony dopiero po zamknięciu ostatniego uchwytu. W systemie Windows natomiast nie można usunąć pliku otwartego przez jakikolwiek proces. Dlatego bezpieczniej jest zawsze zamknąć plik (fclose), a dopiero potem usuwać go unlinkiem. Od PHP 7.3 zniesiono część ograniczeń na Windows (można usunąć plik otwarty przez ten sam proces PHP), ale wciąż nie można utworzyć nowego pliku o tej samej nazwie dopóki stary uchwyt nie zostanie zamknięty​.

Podsumowując sekcję podstawową: potrafimy już otworzyć plik, odczytać jego zawartość różnymi metodami, zapisać dane do pliku, utworzyć nowy plik oraz usunąć plik. Następnie zajmiemy się bardziej zaawansowanymi kwestiami: jak radzić sobie z błędami i uprawnieniami, jak operować na katalogach oraz jak bezpiecznie przetwarzać dane plikowe.

Obsługa błędów i zabezpieczenia przy pracy z plikami

Operacje na plikach mogą napotkać różne problemy – plik może nie istnieć, możemy nie mieć uprawnień do jego odczytu/zapisu, dysk może być pełny, ścieżka nieprawidłowa, itp. Dlatego ważne jest, aby obsługiwać błędy i upewnić się, że operacje są wykonywane w bezpieczny sposób.

Sprawdzanie istnienia i uprawnień pliku

Zanim otworzymy plik do odczytu lub spróbujemy go usunąć, warto sprawdzić, czy faktycznie istnieje. Służy do tego funkcja file_exists($sciezka), która zwraca true/false. Podobnie można użyć is_file($sciezka) (sprawdza, czy istnieje i czy jest zwykłym plikiem) lub is_dir($sciezka) dla katalogów. Przykład użycia:

 if (!file_exists("konfig.txt")) {
     echo "Plik konfiguracyjny nie istnieje!";
 } else {
    $fp = fopen("konfig.txt", "r");
    // ...
 } 

Jeśli zamierzamy zapisywać dane, przydatne mogą być funkcje is_writable($sciezka) i is_readable($sciezka), które sprawdzają uprawnienia do zapisu lub odczytu. Przykładowo:

 if (file_exists("dane.txt") && !is_writable("dane.txt")) {
    echo "Brak uprawnień do zapisu pliku dane.txt";
 } 

Warto zauważyć, że is_writable zwróci false także wtedy, gdy plik nie istnieje (bo nie można pisać do czegoś, czego nie ma). Jeżeli więc chcemy utworzyć plik, lepiej sprawdzić uprawnienia do katalogu, w którym plik ma powstać.

Łapanie błędów funkcji plikowych

Większość funkcji operujących na plikach nie rzuca wyjątków, ale zwraca wartości oznaczające błąd (np. false). Dodatkowo PHP generuje ostrzeżenia (warnings), które nie przerywają skryptu, ale są wyświetlane (lub logowane). Sposoby obsługi:

  • Sprawdzanie zwracanych wartości: jak pokazywaliśmy w przykładach, zawsze warto weryfikować wynik fopen, fread, fwrite, file_get_contents, unlink itd. przed założeniem, że operacja się udała.
  • Blokowanie ostrzeżeń: można użyć operatora @ przed wywołaniem funkcji, np. $fp = @fopen("plik.txt", "r");. To spowoduje, że ewentualny komunikat ostrzegawczy nie pojawi się. Nie jest to jednak zalecane na etapie debugowania, bo ukrywa informację o błędzie. Lepiej przechwycić błąd inaczej lub po prostu sprawdzić wynik.
  • Użycie funkcji error_get_last() po nieudanym wywołaniu może dać szczegóły ostatniego błędu (np. No such file or directory lub Permission denied). Można to wykorzystać do logowania problemów.
  • Wyjątki: standardowe funkcje plikowe nie wykorzystują wyjątków, ale istnieją obiektowe odpowiedniki (np. klasa SplFileObject), które mogą rzucać wyjątki. W tym wykładzie skupimy się na funkcjach proceduralnych, więc obsługa polega głównie na if/else.

Bezpieczne ścieżki i dane wejściowe

Bardzo ważnym aspektem bezpieczeństwa jest walidacja i kontrola ścieżek do plików używanych w funkcjach. Nigdy nie należy bezkrytycznie używać danych pochodzących od użytkownika jako fragmentu ścieżki pliku na serwerze. Klasycznym atakiem jest tzw. Path Traversal (Traversal Directory), gdzie atakujący manipulując ścieżką (najczęściej dodając ../) próbuje uzyskać dostęp do plików poza dozwolonym katalogiem aplikacji.

Path Traversal polega na takim skonstruowaniu parametru (np. nazwy pliku przekazanej w URL lub formularzu), by uzyskać dostęp do plików, do których normalnie aplikacja nie powinna zaglądać (np. plików konfiguracyjnych systemu). Dzieje się to poprzez „wychodzenie” do katalogów nadrzędnych za pomocą ciągów "../" w ścieżce​sekurak.pl. Jeśli aplikacja nie filtruje takich sekwencji, ktoś może np. zażądać pliku ../../etc/passwd zamiast dozwolonego dane/uzytkownik.txt. W rezultacie serwer może ujawnić wrażliwe pliki.

Jak się przed tym bronić? Nigdy nie ufaj ścieżce lub nazwie pliku podanej przez użytkownika. Kilka dobrych praktyk:

  • Walidacja nazwy pliku: Jeżeli użytkownik ma podać nazwę pliku do otwarcia, dopuszczaj tylko konkretny format nazwy, np. same litery/cyfry bez ukośników. Można użyć wyrażeń regularnych lub funkcji ctype. Warto też ograniczyć dopuszczalne rozszerzenia (np. tylko .txt).
  • Nie pozwalaj na znaki ../: Najprostszym zabezpieczeniem jest odrzucenie wejścia, które zawiera ciąg ".." lub inne podejrzane sekwencje (jak zaczynające się od „/” itp.). Jednak samo usuwanie ciągów "../" może być niewystarczające – atakujący mogą użyć zaskakujących zapisów (np. zakodowanych URL-owo), które ominą naiwny filtr​sekurak.pl. Lepiej całkowicie zablokować dane wejściowe zawierające kropki i ukośniki, jeśli oczekujemy zwykłej nazwy pliku.
  • Użycie bezpiecznej ścieżki bazowej: Można zdefiniować pewien katalog bazowy, w którym trzymamy pliki do odczytu/zapisu (np. uploads/ lub dane/). Następnie łączyć go z podaną nazwą pliku i kanonicznie przetwarzać. Pomocna jest funkcja realpath(), która zwraca kanoniczną (oczyszczoną) pełną ścieżkę do pliku. Przykładowo, realpath("dane/../config.php") zwróci pełną ścieżkę bez fragmentów ../. Po uzyskaniu realpath możemy sprawdzić, czy dany plik faktycznie znajduje się w naszym dozwolonym katalogu bazowym. Prosty schemat: phpKopiujEdytuj$baseDir = __DIR__ . "/dane/"; // dozwolony katalog bazowy $podanaNazwa = $_GET["plik"]; // np. parametr z URL $sciezka = realpath($baseDir . $podanaNazwa); if ($sciezka === false || strpos($sciezka, $baseDir) !== 0) { die("Nieprawidłowa ścieżka pliku!"); } // teraz $sciezka jest pełną, bezpieczną ścieżką w katalogu dane/ $fp = fopen($sciezka, "r"); W powyższym kodzie realpath zwróci pełną ścieżkę do pliku jeśli on istnieje (inaczej zwróci false). Następnie strpos sprawdza, czy ta pełna ścieżka zaczyna się od naszego katalogu bazowego (czyli czy plik leży w nim, a nie gdzie indziej). Jeżeli warunek nie jest spełniony, przerywamy działanie – oznacza to, że ktoś próbował wyjść poza dozwoloną lokalizację lub plik nie istnieje.
  • Wykorzystanie basename(): Czasem wystarczy ograniczyć wejście tylko do nazwy pliku bez ścieżki. Funkcja basename($sciezka) zwróci ostatni komponent ścieżki, ignorując resztę. Np. basename("../../../etc/passwd") da "passwd"acunetix.com. Możemy więc zrobić $plik = basename($_GET["plik"]) i użyć tej nazwy w naszym katalogu: fopen("dane/$plik", "r"). W ten sposób ciągi ../ zostaną niejako obcięte (bo basename wyrzuci wszystko oprócz nazwy pliku). Jednak uwaga – jeśli ktoś poda samą nazwę np. passwd i nasza aplikacja użyje go w katalogu dane/, to nie wyjdzie poza katalog, ale pliku może tam nie być. Z tego powodu metoda basename() bywa łączona z realpath() dla pełnej pewności​.

Podsumowując: najlepszą obroną przed atakami Path Traversal jest poprawna walidacja i ograniczenie danych wejściowych. Jeśli to możliwe, nie pozwalaj użytkownikowi wprowadzać dowolnej ścieżki – lepiej dać wybór z listy lub użyć identyfikatorów plików zamiast nazw. Jeżeli jednak musisz użyć nazwy od użytkownika, czyść i sprawdzaj ją dokładnie (usuniecie niedozwolonych znaków, użycie realpath, porównanie do dozwolonej ścieżki). Jak wskazuje OWASP i inni eksperci, unikanie czarnej listy (blokowania tylko pewnych znaków) jest ważne – lepsze jest podejście białej listy (zezwalanie tylko na konkretne znaki)​.

Inne aspekty bezpieczeństwa

  • Uprawnienia plików: Upewnij się, że pliki, którymi operuje PHP, mają odpowiednie prawa w systemie plików. Pliki, które nie powinny być modyfikowane przez proces webserwera, niech mają tylko do odczytu (0444 lub 0644 bez prawa zapisu przez użytkownika serwera). Katalogi z uploadami plików powinny mieć restrykcyjne uprawnienia i często mechanizmy dodatkowe (np. brak możliwości wykonania skryptów z tego katalogu, by ktoś nie wgrał złośliwego PHP).
  • try…catch przy SPL: Jeśli używasz obiektowych klas plikowych (jak SplFileObject), pamiętaj o blokach try/catch, bo np. metoda SplFileObject::__construct rzuci wyjątek przy nieudanym otwarciu pliku.
  • Konfiguracja PHP: W środowiskach produkcyjnych warto ustawić open_basedir w php.ini – ogranicza on katalogi, do których PHP może mieć dostęp. Nie zwalnia to z obowiązku walidacji, ale stanowi dodatkową warstwę ochrony. Ponadto wyłączanie nieużywanych funkcji (np. jeśli nie potrzebujemy możliwości usuwania plików, można poprzez disable_functions zablokować unlink – to drastyczny środek, rzadko stosowany, ale wspominany dla kompletności).
  • Backupy: Operując na ważnych plikach (np. nadpisując pliki konfiguracyjne), rozważ tworzenie kopii zapasowych przed zapisem.

Praca z katalogami (folderami)

Operacje na katalogach są podobnie ważne jak na pojedynczych plikach. PHP udostępnia funkcje do tworzenia i usuwania katalogów, a także przeglądania ich zawartości.

Tworzenie katalogu

Aby utworzyć nowy katalog (folder) na dysku, używamy funkcji mkdir($sciezka, $uprawnienia, $rekurencyjnie). Podajemy ścieżkę (może być względna lub absolutna). Drugi parametr to prawa dostępu dla nowego katalogu w notacji ósemkowej (analogicznie jak w Unixie, np. 0775). Jeśli go pominiemy, domyślnie będzie to 0777 zmodyfikowane przez bieżącą maskę umask (zazwyczaj więc efektywnie 0755). Trzeci parametr (opcjonalny) to bool oznaczający, czy tworzyć ewentualnie rekurencyjnie całą strukturę katalogów. Jeśli ustawimy go na true, a w podanej ścieżce jest kilka poziomów nieistniejących katalogów, PHP spróbuje utworzyć je wszystkie po kolei. Przykład:

 // Utworzenie pojedynczego katalogu "dane"
 if (!file_exists("dane")) {
    mkdir("dane", 0755);
 }

 // Utworzenie zagnieżdżonych katalogów "upload/2025/zdjecia"
 mkdir("upload/2025/zdjecia", 0777, true);

W powyższym kodzie najpierw tworzymy katalog dane (o ile jeszcze nie istnieje). Ustawiamy uprawnienia na 0755 (w systemie Unix oznacza: właściciel może czytać/zapisywać, grupa i inni tylko czytać i wykonywać – typowe dla folderów webowych). Następnie jednym poleceniem mkdir z opcją rekurencyjną tworzymy całą strukturę katalogów: najpierw upload, w nim 2025, w nim zdjecia. Gdy recursive jest true, PHP nie zgłosi błędu jeśli któryś katalog po drodze już istnieje – po prostu pominie tworzenie tych istniejących i utworzy brakujące.

Funkcja mkdir zwraca true/false w zależności od sukcesu operacji, więc warto to sprawdzić lub użyć w warunku, aby wychwycić ewentualne błędy (np. brak uprawnień do utworzenia katalogu w danej lokalizacji).

Usuwanie katalogu

Do usunięcia pustego katalogu służy funkcja rmdir($sciezka). Uwaga – katalog musi być pusty (nie zawierać żadnych plików ani podkatalogów), inaczej rmdir się nie powiedzie. Przykład:

 if (file_exists("stary")) {
    $ok = rmdir("stary");
    if ($ok) {
        echo "Katalog został usunięty.";
    } else {
        echo "Nie udało się usunąć katalogu. Sprawdź, czy jest pusty.";
    }
 } 

Jeśli potrzebujemy usunąć katalog wraz z całą jego zawartością (rekurencyjnie), PHP nie ma pojedynczej wbudowanej funkcji do tego (poza wykorzystaniem np. polecenia systemowego exec('rm -rf …'), czego raczej unikamy). Trzeba w takiej sytuacji najpierw usunąć wszystkie pliki w katalogu (np. w pętli), a następnie sam katalog. Można napisać własną funkcję rekurencyjną do usuwania katalogów. W naszym wykładzie skupiamy się na podstawach, więc zakładamy usuwanie katalogów pustych za pomocą rmdir.

Odczyt zawartości katalogu

Często zachodzi potrzeba uzyskania listy plików w danym katalogu. Możemy to osiągnąć na kilka sposobów:

  • opendir(), readdir(), closedir() – zestaw niskopoziomowych funkcji podobny koncepcją do fopen. opendir($sciezka) otwiera katalog i zwraca uchwyt (zasób) katalogu. Następnie wielokrotne wywołanie readdir($uchwyt_katalogu) zwraca kolejne nazwy wpisów (pliki lub podkatalogi) z tego katalogu, lub false gdy dotrze do końca. Gdy skończymy, zamykamy katalog closedir($uchwyt).
  • scandir($sciezka) – wygodna funkcja, która zwraca tablicę nazw zawartości katalogu (działa nieco jak połączenie opendir+readdir które od razu czyta całość). Można ją użyć, gdy chcemy po prostu szybko dostać listę plików.
  • glob($wzorzec) – ta funkcja nie tyle czyta katalog bezpośrednio, co zwraca listę plików pasujących do podanego wzorca (np. *.txt dla wszystkich plików .txt). Internie też przegląda katalog, ale pozwala od razu filtrować wyniki po wzorcu.

Pokażemy przykład z użyciem opendir/readdir, aby zobrazować podstawowy mechanizm:

 <?php
$dir = "dane";
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
echo "Zawartość katalogu $dir:\n";
while (($plik = readdir($dh)) !== false) {
if ($plik === "." || $plik === "..") continue; // pomijamy bieżący i nadrzędny katalog
echo " - $plik\n";
}
closedir($dh);
} else {
echo "Nie można otworzyć katalogu $dir.";
}
} else {
echo "Katalog $dir nie istnieje.";
}
?>


W powyższym kodzie sprawdzamy najpierw is_dir, czy dane jest istniejącym katalogiem. Następnie opendir otwiera katalog i zwraca uchwyt do przeglądania. W pętli while pobieramy kolejne wpisy. Każdy wpis to nazwa pliku lub podkatalogu (np. "plik1.txt", "obrazek.png", "podkatalog"). Zawsze w każdym katalogu są specjalne wpisy "." (oznaczający ten sam katalog) oraz ".." (rodzic, katalog nadrzędny) – zazwyczaj chcemy je pominąć, co robimy instrukcją continue gdy zostaną napotkane. Resztę wyświetlamy. Na końcu zamykamy katalog closedir.

Jeśli zamiast wypisywać chcielibyśmy np. zebrać te nazwy do tablicy, można w pętli dodawać do tablicy, lub prościej – użyć wspomnianej funkcji scandir:

 $pliki = scandir($dir);
 foreach ($pliki as $plik) {
    if ($plik !== "." && $plik !== "..") {
        echo "Znaleziono: $plik\n";
    }
 } 

scandir zwraca już tablicę, którą iterujemy. W praktyce to podejście jest krótsze, ale mniej elastyczne w przypadku bardzo dużych katalogów (gdzie nie chcemy wszystkiego naraz w pamięci, choć to rzadko bywa problemem).

Pobieranie informacji o plikach: Mając listę nazw możemy też od razu sprawdzać pewne informacje o każdym z nich, np. czy dany wpis jest katalogiem czy plikiem (is_file/is_dir), jaka jest jego wielkość (filesize), data modyfikacji (filemtime), itd. PHP oferuje sporo funkcji file* oraz stat do uzyskiwania szczegółów – ale to już materiał na osobny temat.

Na marginesie, dla bardziej obiektowego podejścia, PHP ma klasy takie jak DirectoryIterator czy FilesystemIterator (z przestrzeni SPL), które pozwalają przeglądać katalog w stylu iteracji obiektowej. Dla początkujących jednak proste opendir/readdir lub scandir są w zupełności wystarczające.

Przetwarzanie plików tekstowych i CSV

Pliki tekstowe są najprostszą formą przechowywania danych – mogą to być np. logi, pliki .txt z paragrafami tekstu, wartości oddzielone przecinkami (CSV) czy inne dane w formacie tekstowym. W tej sekcji zajmiemy się technikami wczytywania i interpretowania takich danych.

Praca z prostymi plikami tekstowymi

Załóżmy, że mamy prosty plik tekstowy lista.txt, w którym każda linia zawiera jedno słowo (np. lista haseł, imion, itp.). Chcemy wczytać ten plik i np. wyświetlić numerowane linie na stronie. Możemy wykorzystać poznane już metody odczytu liniowego:

 <?php
 $fp = fopen("lista.txt", "r");
 if (!$fp) {
    die("Nie można otworzyć pliku lista.txt");
 }
 $i = 1;
 while (($linia = fgets($fp)) !== false) {
    $linia = rtrim($linia, "\r\n");
    echo $i++ . ". " . $linia . "<br>";
 }
 fclose($fp);
 ?>

Tutaj odczytujemy plik linia po linii i dodajemy numerację. Używamy rtrim by pozbyć się znaków nowej linii przed doklejeniem "<br>" (znacznika HTML nowej linii). W wyniku np. plik zawierający:

jabłko
gruszka
śliwka

zostanie wypisany jako:

1. jabłko
2. gruszka
3. śliwka

Inny scenariusz: plik tekstowy może zawierać pary klucz-wartość, np. konfigurację w formie:

 host=localhost user=root
 pass=secret

Aby to przetworzyć, możemy czytać linia po linii, użyć explode("=", $linia) do podziału na klucz i wartość i zbudować np. tablicę asocjacyjną:

 $config = [];
 $fp = fopen("config.ini", "r");
 while (($linia = fgets($fp)) !== false) {
    $linia = trim($linia);
    if ($linia === "" || $linia[0] === "#") continue; // pomiń puste i komentarze
    list($klucz, $wartosc) = explode("=", $linia, 2);
    $config[$klucz] = $wartosc;
 }
 fclose($fp);
 // Teraz $config['host'] będzie np. "localhost"

To tylko przykład jak można obrabiać dane tekstowe własnym kodem. PHP posiada również funkcje parse_ini_file do takich plików .ini, ale chciałem zilustrować manualne przetwarzanie linii tekstu.

Format CSV (Comma-Separated Values)

Pliki CSV to popularny format przechowywania tabelarycznych danych w formacie tekstowym. W CSV poszczególne pola (kolumny) są oddzielone przecinkami (lub innym separatorem, np. średnikiem), a rekordy (wiersze) są oddzielone nowymi liniami. Przykładowy plik CSV dane.csv może wyglądać tak:

imie,nazwisko,wiek
Jan,Kowalski,30
Anna,Nowak,25
Piotr,Zieliński,40

Pierwsza linia często jest nagłówkiem z nazwami kolumn, a kolejne to dane.

Ręczne parsowanie CSV (np. użycie explode(",",$linia)) może działać dla prostych przypadków, ale zawodzi, gdy pola zawierają przecinki w wartościach lub są ujęte w cudzysłowy. Dlatego PHP udostępnia specjalne funkcje:

  • fgetcsv($uchwyt, $długość=0, $delimiter=',', $enclosure='"', $escape='\\') – czyta jedną linię CSV z otwartego pliku i zwraca tablicę wartości. Parametry określają separator (domyślnie przecinek), znacznik zawarcia pola (domyślnie cudzysłów) i znak escape.
  • fputcsv($uchwyt, $tablica, $delimiter=',', $enclosure='"', $escape='\\') – zapisuje tablicę wartości jako jedną linię CSV do pliku (dokłada separator i ewentualne cudzysłowy automatycznie).

Użycie tych funkcji jest bardzo wygodne, bo zajmują się wszystkimi zawiłościami formatu (np. jeśli pole zawiera przecinek, fgetcsv zauważy, że jest ono ujęte w cudzysłowy i nie rozbije go na dwa pola).

Przykład odczytu pliku CSV:

Załóżmy, że mamy plik pracownicy.csv w formacie: imie,nazwisko,email (bez nagłówka dla uproszczenia tutaj). Możemy go wczytać następująco:

 <?php
 $fp = fopen("pracownicy.csv", "r");
 if (!$fp) {
    die("Nie udało się otworzyć pliku CSV.");
 }
 echo "<ul>\n";
 while (($wiersz = fgetcsv($fp)) !== false) {
    // $wiersz jest tablicą elementów [imie, nazwisko, email]
    if (count($wiersz) < 3) continue; // na wypadek pustej linii
    list($imie, $nazwisko, $email) = $wiersz;
    echo "<li>$imie $nazwisko — email: $email</li>\n";
 }
 echo "</ul>\n";
 fclose($fp);

 ?>

Ten kod otwiera plik CSV, następnie w pętli while pobiera kolejne wiersze jako tablicę. Rozpakowujemy tablicę na trzy zmienne $imie, $nazwisko, $email i wykorzystujemy je (tu akurat generując listę HTML). Po zakończeniu zamykamy plik. fgetcsv automatycznie przechodzi do następnej linii przy kolejnym wywołaniu.

Przykład zapisu do pliku CSV:

Załóżmy, że mamy tablicę danych, którą chcemy zapisać jako plik CSV. Możemy użyć fputcsv. Poniżej zapisujemy kilka wierszy do pliku wynik.csv:

 <?php
 $dane = [
    ["Produkt", "Cena", "Ilość"],
    ["Jabłka", "3.50", "10"],
    ["Gruszki", "4.20", "5"],
 ];
 $fp = fopen("wynik.csv", "w");
 foreach ($dane as $wiersz) {
    fputcsv($fp, $wiersz); // domyślny delimiter to ',', więc CSV oddzielone przecinkami
 }
 fclose($fp);
 echo "Zapisano plik wynik.csv";
 ?>

Po uruchomieniu tego kodu plik wynik.csv będzie zawierał:

Produkt,Cena,Ilość
Jabłka,3.50,10
Gruszki,4.20,5

Funkcja fputcsv sama dodaje znak nowej linii na końcu każdego zapisanego wiersza, więc nie musimy go dopisywać. Gdybyśmy chcieli użyć innego separatora (np. średnika ;, który jest popularny w polskich wersjach Excela jako separator CSV), możemy podać go jako trzeci argument fputcsv($fp, $wiersz, ";") oraz analogicznie fgetcsv($fp, 0, ";") przy odczycie.

Uwaga dot. kodowania znaków: Jeśli plik CSV zawiera polskie znaki, upewnij się, że używane jest poprawne kodowanie (najlepiej UTF-8). PHP nie zmienia kodowania, zapisuje bajty jak leci. Dlatego jeśli generujesz CSV z polskimi znakami do otwarcia np. w Excelu, czasem wymagane jest dodanie na początku specjalnego BOM lub użycie kodowania Windows-1250 – to jednak poza zakresem tego wykładu. W naszych przykładach zakładamy UTF-8 wszędzie.

Dobre praktyki przy operacjach na plikach

Na koniec omówmy kilka dobrych praktyk i wskazówek, które warto stosować podczas programowania operacji plikowych w PHP. Dzięki nim nasze skrypty będą bardziej efektywne, bezpieczne i mniej podatne na błędy.

Buforowanie operacji wejścia/wyjścia

Buforowanie odnosi się do optymalizacji polegającej na zmniejszeniu liczby bezpośrednich operacji dyskowych przez grupowanie danych. Dostęp do dysku bywa stosunkowo wolny, więc czytając lub pisząc plik, lepiej robić to w większych blokach niż bajt po bajcie.

  • Czytanie dużych plików: Zamiast czytać cały plik naraz do pamięci (co może zużywać dużo RAM), można czytać go porcjami, np. po 4KB czy 8KB danych, przetwarzać tę porcję i wczytywać następną. W PHP można to osiągnąć wywołując fread w pętli z ograniczoną długością lub używając fgets jeśli dane są liniowe. Np.: phpKopiujEdytuj$fp = fopen("bigfile.dat", "r"); while (!feof($fp)) { $chunk = fread($fp, 8192); // czytaj 8KB na raz // ... przetwórz $chunk ... } fclose($fp); Taka pętla pozwoli obchodzić się nawet z bardzo dużymi plikami bez ryzyka przeciążenia pamięci – w danym momencie przetwarzamy tylko fragment.
  • Pisanie w pętli: Jeśli masz zamiar w pętli dopisywać do pliku wiele małych fragmentów, rozważ alternatywę:
    • Albo otwórz plik raz przed pętlą, zapisuj kolejne fragmenty fwrite i zamknij po pętli (to na szczęście naturalne podejście i tak było w naszych przykładach; unikaj otwierania i zamykania pliku przy każdym przebiegu pętli, bo to ogromny narzut).
    • Albo zbuforuj dane w zmiennej i zapisz je jednorazowo. Np. buduj duży string przez konkatenację lub (lepiej przy wielu operacjach) użyj tablicy i implode – a na koniec zapisz całość funkcją file_put_contents. To czasem łatwiejsze niż martwienie się o synchronizację przy równoczesnych zapisach.
    • Buforowanie ma granice – nie chcemy też na siłę trzymać w pamięci ogromu danych. Trzeba dobrać strategię do rozmiaru danych.
  • Wykorzystanie wbudowanych mechanizmów: Jak już wspomniano, funkcja file_get_contents używa wewnętrznie wydajnych technik (jak memory mapping) by przyspieszyć odczyt dużych plików​w3schools.com. Z kolei file_put_contents jest napisana w C i z pewnymi optymalizacjami, więc często będzie szybsza niż równoważny kod PHP wykonujący pętlę fwrite. Korzystaj z tych wyższego poziomu funkcji gdy to możliwe – nie tylko upraszczają kod, ale i mogą działać szybciej.
  • Output buffering (bufor wyjścia): Choć to temat związany bardziej z wysyłaniem danych do przeglądarki, czasem używa się mechanizmu bufora wyjścia, by zgromadzić duży tekst generowany przez skrypt (np. HTML lub CSV) i zapisać go do pliku jedną funkcją zamiast dokonywać wielu zapisów. Można np. zrobić: phpKopiujEdytujob_start(); // generuj dużo danych, używając echo, print itd. $calosc = ob_get_clean(); file_put_contents("duzyraport.txt", $calosc); Tutaj ob_start włącza buforowanie wyjścia (nic nie jest wysyłane do przeglądarki, tylko trafia do bufora), następnie generujemy zawartość, ob_get_clean pobiera cały bufor jako string i czyści go, po czym file_put_contents zapisuje wszystko do pliku. To wygodny sposób, by przekierować generowanie np. raportu HTML do pliku na dysku.

Podsumowując, chodzi o to, by minimalizować liczbę operacji I/O. Lepiej zrobić jedno duże fwrite niż sto małych, jeśli to możliwe. Buforowanie i batch processing danych zawsze powinny iść w parze z rozwagą – czasem upraszczamy kod kosztem kilku dodatkowych I/O i to też jest w porządku, dopóki nie wpływa na wydajność w zauważalny sposób.

Blokady plików (współbieżny dostęp)

W środowisku webowym, gdzie wiele skryptów PHP może uruchamiać się jednocześnie (np. wielu użytkowników jednocześnie dokonuje operacji), istnieje ryzyko, że dwa procesy spróbują jednocześnie pisać do tego samego pliku lub jeden czyta, gdy inny akurat go modyfikuje. To może prowadzić do niespójnych danych lub uszkodzenia pliku. Aby temu zapobiec, stosuje się mechanizm blokad plików (ang. file locking).

PHP udostępnia funkcję flock($uchwyt, $mode) do zakładania i zdejmowania blokady na plik. Użycie:

  • $mode może być LOCK_SH (udziałowa blokada do odczytu – wiele procesów może mieć taką jednocześnie, o ile nikt nie ma wyłącznej) albo LOCK_EX (wyłączna blokada do zapisu – tylko jeden proces może mieć ją na dany plik). Dodatkowo można dodać LOCK_NB żeby nie czekać na zwolnienie blokady (non-blocking).
  • Wywołujemy flock($fp, LOCK_EX) przed krytyczną sekcją (np. przed zapisem do pliku), a gdy skończymy, wywołujemy flock($fp, LOCK_UN) żeby zwolnić blokadę.
  • Blokadę zawsze zakładamy na otwarty uchwyt pliku. To znaczy plik musi być otwarty (np. przez fopen) zanim wykonamy flock.

Przykład scenariusza: Mamy plik licznik.txt, który przechowuje jakąś liczbę (licznik odwiedzin strony). Każde odświeżenie strony skryptem ma zwiększyć ten licznik o 1. Bez blokad, jeśli dwa zapytania w tym samym czasie spróbują to zrobić, może dojść do tzw. race condition – oba odczytają starą wartość X, oba zapiszą X+1, a powinniśmy dostać X+2 net sumarycznie.

Rozwiązanie z blokadą:

 <?php
 $fp = fopen("licznik.txt", "c+");  // tryb c+ umożliwia odczyt i zapis bez kasowania zawartości
 flock($fp, LOCK_EX);              // zakładamy wyłączną blokadę
 // Sekcja krytyczna - tylko jeden proces naraz tu wejdzie
 $wartosc = (int)fread($fp, 100);  // odczytaj bieżącą wartość (zakładamy, że jest mała)
 fseek($fp, 0);                    // cofnij wskaźnik na początek, by nadpisać od początku
 $wartosc++;
 fwrite($fp, $wartosc);
 // Koniec sekcji krytycznej
 flock($fp, LOCK_UN);              // zwolnij blokadę
 fclose($fp);
 ?>

W powyższym kodzie użyliśmy trybu "c+" do otwarcia pliku. Tryb ten (od „create”) jest podobny do "r+" z tą różnicą, że utworzy plik jeśli nie istnieje (ale nie kasuje zawartości jak „w”). Przydaje się do liczników, logów, itp. gdzie chcemy edytować plik „w miejscu”. Zakładamy blokadę exkluzywną LOCK_EX – od tego momentu, jeśli inny proces spróbuje wykonać flock na ten sam plik, będzie czekał aż zwolnimy (chyba że użył LOCK_NB, wtedy dostanie natychmiastowy błąd). Z blokadą odczytujemy wartość, zwiększamy ją, zapisujemy z powrotem. Ważne jest użycie fseek – po odczytaniu wskaźnik był na końcu pliku, musieliśmy cofnąć na początek przed zapisem, by nadpisać starą wartość. Następnie zdejmujemy blokadę.

Taki mechanizm gwarantuje, że tylko jeden proces naraz modyfikuje licznik, więc nic się nie „zgubi”. Gdyby dwóch użytkowników jednocześnie wywołało ten skrypt, drugi poczeka w miejscu flock($fp, LOCK_EX) aż pierwszy wykona flock($fp, LOCK_UN).

Dla operacji czysto odczytowych można stosować blokadę współdzieloną LOCK_SH, która pozwala wielu procesom czytać jednocześnie, ale zablokuje zapis w tym czasie (proces, który będzie chciał założyć LOCK_EX, poczeka aż wszyscy czytający zwolnią LOCK_SH). W praktyce w PHP rzadko jawnie zakłada się blokady dla odczytu – raczej dba się o blokady przy zapisie.

Warto wspomnieć, że file_put_contents ma wygodny skrót: flagę LOCK_EX, którą można podać w trzecim argumencie (lub OR razem z FILE_APPEND). Gdy użyjemy file_put_contents("plik.txt", $dane, LOCK_EX), funkcja ta wewnętrznie wykona flock na czas zapisu​php.net. To ułatwia bezpieczny zapis, jeśli nie chcemy sami wywoływać flock.

Blokady plików w PHP są advisory, co oznacza, że działają tylko jeśli wszystkie zainteresowane procesy przestrzegają ich. To my musimy używać flock – jeśli jakiś inny proces zignoruje to i będzie pisał bez blokady, to nasza blokada go nie powstrzyma. W większości zastosowań webowych skrypty PHP to jedyne coś co dotyka te pliki, więc jest w porządku.

Unikanie ataków typu Path Traversal

Ten temat omówiliśmy już częściowo w sekcji o bezpiecznych ścieżkach, ale powtórzmy najważniejsze jako dobrą praktykę bezpieczeństwa: waliduj i filtruj wszelkie dane, które wpływają na wybór pliku lub katalogu. Atak Path Traversal, jak opisano, ma na celu wyjście poza zamierzony katalog i uzyskanie dostępu do niedozwolonych zasobów​sekurak.pl. Aby się przed nim uchronić:

  • Ograniczaj wybór plików – np. jeżeli użytkownik ma wybierać avatar spośród dostępnych, zamiast pozwalać mu wpisać nazwę pliku, daj listę opcji.
  • Jeżeli musisz przyjąć ścieżkę/nazwę pliku od użytkownika, stosuj funkcje takie jak basename i realpath do normalizacji ścieżki i upewnienia się, że pozostaje w dozwolonym obszarze​.
  • Stosuj listy dozwolonych wartości tam, gdzie to możliwe. Np. zamiast pozwolić użytkownikowi podać rozszerzenie pliku, narzuć z góry, że akceptowane są tylko .jpg i .png (i sprawdź to po otrzymaniu pliku).
  • Uświadom sobie, że prosty filtr usuwający ciągi "../" może być łatwo obejdowany​. Atakujący mogą np. potroić kropki, dodawać ukośniki w różnej postaci kodowania (np. %2e to kropka, %2f to / w kodowaniu URL). Dlatego nie próbuj na ślepo czyścić, raczej weryfikuj poprawność.

Dobrym nawykiem jest przemyślenie, jakie najgorsze działanie może wykonać atakujący manipulując ścieżką w naszym skrypcie, i upewnienie się, że scenariusz jest zablokowany. W kontekście plików, najgorsze to odczyt lub nadpisanie plików konfiguracyjnych, haseł, lub wykonanie przesłanego skryptu. Oprócz walidacji samej ścieżki, pamiętajmy też, by nie nadawać katalogom z plikami użytkownika prawa wykonywania skryptów (czyli jeśli ktoś wgra shell.php jako plik, żeby nie można go było potem odwiedzić w przeglądarce i uruchomić). To już jednak zagadnienie konfiguracji serwera (np. trzymanie uploadów poza katalogiem publicznym).

Inne dobre praktyki

  • Zamykanie plików tak szybko, jak to możliwe: Nie trzymaj otwartego uchwytu dłużej niż potrzebne. Zwłaszcza gdy używasz blokad – natychmiast po zapisie zwalniaj blokadę i zamykaj plik, aby inni mogli z niego korzystać.
  • Transakcje plikowe (atomiczność zapisu): Jeśli zapisujesz ważne dane, które nie powinny zostać uszkodzone przy przerwaniu, rozważ strategię zapisu do pliku tymczasowego i podmiany. Np. zapisz do plik.txt.tmp, a po sukcesie zamień nazwy (funkcją rename) – dzięki temu zawsze albo stary albo nowy plik będzie kompletny. To zabezpiecza przed sytuacją, że np. skrypt przerwał się w trakcie zapisu i plik docelowy jest częściowo nadpisany.
  • Logowanie błędów: Jeżeli operacja plikowa się nie uda, dobrze jest mieć w logach informację dlaczego. Np. error_get_last() może posłużyć do zapisu komunikatu błędu (ścieżka, powód) do logu, co ułatwi debug.
  • Testy i uprawnienia środowiska: Warto przetestować nasz kod plikowy w warunkach zbliżonych do docelowych – np. czy na serwerze produkcyjnym skrypt ma prawo zapisu tam gdzie myślimy. Często w środowiskach developerskich mamy pełne uprawnienia i wszystko działa, a na produkcji np. brak prawa do jakiegoś folderu powoduje błędy. Dlatego po wdrożeniu funkcjonalności plikowych, sprawdź logi serwera czy nie pojawiają się ostrzeżenia o prawach dostępu.

Podsumowanie

Operacje na plikach w PHP obejmują szeroki zakres czynności – od prostego odczytu/zapisu po zarządzanie całymi katalogami i dbanie o bezpieczeństwo. W tej prezentacji przedstawiliśmy podstawowe funkcje i ich zastosowania: od otwierania plików (fopen) i czytania (fread, fgets, file_get_contents), przez zapis (fwrite, file_put_contents) po tworzenie (mkdir, tryby zapisu) i usuwanie plików (unlink, rmdir). Omówiliśmy też sposoby radzenia sobie z błędami (sprawdzanie wyników funkcji, istnienia plików, uprawnień) oraz zabezpieczania ścieżek przed atakami. Przyjrzeliśmy się, jak przetwarzać pliki tekstowe i CSV przy użyciu wygodnych narzędzi wbudowanych w język PHP, co znacznie ułatwia zadania importu/eksportu danych. Na koniec podkreśliliśmy znaczenie dobrych praktyk: odpowiedniego buforowania operacji dla wydajności, stosowania blokad plików przy dostępie współbieżnym i zachowania zasad bezpieczeństwa, by nasze aplikacje były odporne na typowe ataki i błędy.

Uzbrojeni w tę wiedzę, początkujący i średnio zaawansowani programiści PHP powinni czuć się pewniej w pracy z systemem plików. Zachęcam do dalszych eksperymentów – np. spróbowania odczytu i zapisu innych formatów (JSON, XML) czy użycia obiektowych interfejsów SPL do plików – pamiętając jednocześnie o fundamentach, które tutaj omówiliśmy. Powodzenia w implementacji bezpiecznych i efektywnych operacji na plikach w Waszych projektach PHP!