Przejdź do głównej treści
INF.03-10-26.06-SGCzerwiec 2026 · 40 kroków · ok. 92 min · 41 kryteriów z klucza

Rozwiązanie krok po kroku: Portal wyników laboratorium (Nowoczesny kod (PDO, bezpieczeństwo))

To drugi sposób rozwiązania tego samego arkusza. Wynik jest ten sam - baza laboratorium z czterema zapytaniami, strona logowania, strona wyników dostępna tylko po zalogowaniu i plik wylogowania - i tak samo spełnia każde kryterium klucza. Inna jest droga:

  • PHP na PDO z trybem wyjątków: dane pobierane na górze pliku, a tabela wypisywana pętlą foreach w szablonie HTML,
  • bezpieczeństwo od pierwszej linii: exit po każdym przekierowaniu (inaczej strona wyników wysyła dane pacjentów także niezalogowanym), htmlspecialchars na wszystkim, co trafia z bazy i sesji na stronę, filter_input przy odczycie formularza, porównanie hasła hash_equals, nowy identyfikator sesji po zalogowaniu, try/catch zamiast białej strony przy awarii bazy,
  • formularz na siatce CSS zamiast znaczników <br>, tabela z <thead> i <tbody>, zmienne CSS z kolorami.

Na końcu krótkie porównanie z wersją podstawową (zakładka obok). Baza pochodzi z prawdziwego archiwum pliki20, zapytania zostały wykonane na tej bazie w MariaDB z XAMPP-a, a podgląd każdej strony PHP to wynik prawdziwego uruchomienia kodu z danego kroku w PHP 8.2: z zalogowaniem, złym hasłem, wejściem bez sesji i wylogowaniem.

Jak liczą się punkty. Klucz ma 41 kryteriów (R.1.1 - R.5.10), każde za 1 punkt. Próg zdania to 75%, czyli 31 punktów. Przy każdym kroku jest napisane, które kryteria spełnia.

  1. Etap 1: Baza danych

    Kroki 1-7 · po tym etapie masz 6 z 41 kryteriów (pkt)

    Krok 1: Baza danych laboratorium

    ok. 2 min
    Polecenie z arkusza

    Utwórz bazę danych o nazwie laboratorium, z zestawem polskich znaków (np. utf8_polish_ci).

    Archiwum pliki20.7z rozpakuj hasłem LaBoRaToR2um (w środku cyfra 2) do folderu z numerem zdającego: laboratorium.sql i trzy pliki PDF z wynikami. W phpMyAdmin Nowa → laboratorium, porównywanie utf8_polish_ci, albo:

    CREATE DATABASE laboratorium CHARACTER SET utf8 COLLATE utf8_polish_ci;
    
  2. Krok 2: Import tabel z pliku laboratorium.sql

    ok. 2 min
    Polecenie z arkusza

    Do utworzonej bazy zaimportuj tabele z pliku laboratorium.sql.

    Import → laboratorium.sql. Cztery tabele: Diagnosta, Pacjent, Zlecenie (data i godzina, pacjent) i Wynik (plik PDF, data wykonania, diagnosta, zlecenie). Wynik mają tylko trzy z dwunastu zleceń - to zdecyduje o rodzaju złączenia w zapytaniu 4.

  3. Krok 3: Zrzut ekranu import.png

    Kryteria:

    R.1.1 Wykonany import tabel do bazy danych laboratorium - plik import.png

    ok. 2 min
    Polecenie z arkusza

    Wykonaj zrzut ekranu po imporcie. Zapisz zrzut w formacie PNG pod nazwą import. Nie kadruj zrzutu. Powinien on obejmować cały ekran monitora, z widocznym paskiem zadań. Na zrzucie powinny być widoczne elementy wskazujące na poprawnie wykonany import tabel.

    Cały ekran z paskiem zadań, zapis jako import.png.

    Typowe błędy

    Wykadrowany zrzut albo zrzut bez paska zadań nie jest oceniany.

  4. Krok 4: Zapytanie 1: diagności malejąco po nazwisku

    Kryteria: ,

    R.1.2 W pliku kwerendy.txt zapisane co najmniej jedno zapytanie SQL, wynikające z treści zadania

    R.1.3 Wykonane zapytanie 1 wybierające jedynie imię i nazwisko diagnosty laboratoryjnego z tabeli Diagnosta posortowane malejąco według nazwiska. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT imie,nazwisko FROM Diagnosta ORDER BY nazwisko DESC; oraz na zrzucie widocznych jest dokładnie 10 rekordów

    ok. 1 min
    Polecenie z arkusza

    Zapytanie 1: wybierające jedynie imię i nazwisko diagnosty laboratoryjnego z tabeli Diagnosta posortowane malejąco według nazwiska.

    Klauzula w linii: co, skąd, w jakiej kolejności (DESC - malejąco, od Z do A). Wynik: 10 wierszy, od Anny Zalewskiej. Zapytania zapisuj w kwerendy.txt, zrzut jako kw1.png.

    kwerendy.txt Tekst nowy plik
    -- Zapytanie 1
    SELECT imie, nazwisko
    FROM Diagnosta
    ORDER BY nazwisko DESC;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 4
  5. Krok 5: Zapytanie 2: liczba zleceń w kolejnych dniach

    Kryteria:

    R.1.4 Wykonane zapytanie 2 zliczające liczbę zleceń pogrupowanych po datach (bez uwzględnienia godziny). W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT DATE(data),COUNT(*)FROM Zlecenie GROUP BY DATE(data); oraz na zrzucie widocznych jest dokładnie 6 rekordów

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 2: zliczające liczbę zleceń pogrupowanych po datach (bez uwzględnienia godziny). Wybierane są jedynie daty i liczby zleceń.

    • DATE(data) obcina godzinę, a GROUP BY DATE(data) łączy w jedną grupę wszystkie zlecenia z tego samego dnia - grupować trzeba po tym samym wyrażeniu, które się wybiera,
    • COUNT(*) liczy zlecenia w grupie,
    • aliasy dzien i liczba_zlecen dają czytelne nagłówki kolumn, a ORDER BY dzien - dni po kolei.

    Wynik: 6 wierszy - po jednym na dzień. Zrzut: kw2.png.

    Typowe błędy

    GROUP BY data bez DATE() tworzy osobną grupę dla każdej godziny - każde zlecenie ma inną, więc wychodzi 12 grup po 1.

    kwerendy.txt Tekst zmienione: 6 linii
    -- Zapytanie 1
    SELECT imie, nazwisko
    FROM Diagnosta
    ORDER BY nazwisko DESC;
    
    -- Zapytanie 2
    SELECT DATE(data) AS dzien, COUNT(*) AS liczba_zlecen
    FROM Zlecenie
    GROUP BY DATE(data)
    ORDER BY dzien;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 5
  6. Krok 6: Zapytanie 3: daty zleceń pacjenta Nowickiego

    Kryteria:

    R.1.5 Wykonane zapytanie 3 wybierające tylko daty zleceń dla pacjenta o nazwisku Nowicki. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT data FROM Zlecenie JOIN Pacjent ON Pacjent.id = idPacjenta WHERE nazwisko = "Nowicki"; (możliwe również porównanie kluczy po WHERE) oraz na zrzucie widoczne są dokładnie 2 rekordy

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 3: wybierające jedynie daty zleceń dla pacjenta o nazwisku Nowicki. Należy posłużyć się relacją.

    Aliasy z (zlecenie) i p (pacjent), warunek złączenia przy JOIN ... ON, warunek z zadania w WHERE. z.data, bo kolumnę data ma też tabela Wynik - prefiks od razu mówi, o którą chodzi. Wynik: 2 wiersze (8 czerwca 2024, 10:00 i 11:00). Zrzut: kw3.png.

    kwerendy.txt Tekst zmienione: 6 linii
    -- Zapytanie 1
    SELECT imie, nazwisko
    FROM Diagnosta
    ORDER BY nazwisko DESC;
    
    -- Zapytanie 2
    SELECT DATE(data) AS dzien, COUNT(*) AS liczba_zlecen
    FROM Zlecenie
    GROUP BY DATE(data)
    ORDER BY dzien;
    
    -- Zapytanie 3
    SELECT z.data
    FROM Zlecenie AS z
    JOIN Pacjent AS p ON p.id = z.idPacjenta
    WHERE p.nazwisko = 'Nowicki';
    Podgląd strony po tym kroku
    Podgląd strony po kroku 6
  7. Krok 7: Zapytanie 4: wszystkie zlecenia z wynikiem albo bez

    Kryteria:

    R.1.6 Wykonane zapytanie 4 wybierające jedynie imię, nazwisko oraz pesel pacjenta z tabeli Pacjent, datę z tabeli Zlecenie, datę i plik z tabeli Wynik. W wyniku wyświetlane są wszystkie zlecenia (także te, które nie mają jeszcze wyniku) . W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT imie, nazwisko, PESEL, Zlecenie.data, Wynik.data, plik FROM Pacjent JOIN Zlecenie ON idPacjenta = Pacjent.id LEFT JOIN Wynik ON Zlecenie.id = idZlecenia; (możliwe użycie dwa razy LEFT JOIN) oraz na zrzucie widocznych jest dokładnie 12 rekordów

    ok. 3 min
    Polecenie z arkusza

    Zapytanie 4: wybierające jedynie imię, nazwisko oraz pesel pacjenta z tabeli Pacjent, datę z tabeli Zlecenie, datę i plik z tabeli Wynik. Należy posłużyć się relacją i odpowiednim rodzajem złączenia. Wybierane są wszystkie zlecenia niezależenie od tego czy jest im przypisany wynik.

    Zapytanie zaczyna się od tabeli, której wszystkie wiersze mają się pojawić - FROM Zlecenie:

    • JOIN Pacjent AS p - każde zlecenie ma pacjenta, wystarczy złączenie wewnętrzne,
    • LEFT JOIN Wynik AS w - złączenie zewnętrzne lewe: zlecenie bez wyniku zostaje, a kolumny z Wynik mają wtedy NULL,
    • dwie kolumny o nazwie data dostają aliasy data_zlecenia i data_wyniku. To przyda się w PHP: w tablicy asocjacyjnej dwa klucze data nadpisałyby się nawzajem.

    Wynik: 12 wierszy, z czego 9 bez wyniku. Sprawdzone: ze zwykłym JOIN zostają 3 wiersze. Zrzut: kw4.png.

    Typowe błędy

    JOIN Wynik (wewnętrzne) gubi zlecenia bez wyniku - 3 wiersze zamiast 12.

    kwerendy.txt Tekst zmienione: 8 linii
    -- Zapytanie 1
    SELECT imie, nazwisko
    FROM Diagnosta
    ORDER BY nazwisko DESC;
    
    -- Zapytanie 2
    SELECT DATE(data) AS dzien, COUNT(*) AS liczba_zlecen
    FROM Zlecenie
    GROUP BY DATE(data)
    ORDER BY dzien;
    
    -- Zapytanie 3
    SELECT z.data
    FROM Zlecenie AS z
    JOIN Pacjent AS p ON p.id = z.idPacjenta
    WHERE p.nazwisko = 'Nowicki';
    
    -- Zapytanie 4
    SELECT p.imie, p.nazwisko, p.pesel,
           z.data AS data_zlecenia,
           w.data AS data_wyniku, w.plik
    FROM Zlecenie AS z
    JOIN Pacjent AS p ON p.id = z.idPacjenta
    LEFT JOIN Wynik AS w ON w.idZlecenia = z.id;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 7
  8. Etap 2: Strony HTML

    Kroki 8-18 · po tym etapie masz 17 z 41 kryteriów (pkt)

    Krok 8: Plik login.php w HTML5

    ok. 2 min
    Polecenie z arkusza

    Witryna internetowa składa się ze stron login.php, wyniki.php oraz pliku wyloguj.php. Cechy wspólne dla stron login.php i wyniki.php: Zapisana w języku HTML5.

    Najpierw login.php; wyniki.php powstanie z jego kopii (nagłówek i stopka są wspólne). Pliki leżą w htdocs XAMPP-a i otwierasz je przez http://localhost/... - otwarte z dysku nie wykonają PHP.

    login.php PHP nowy plik
    <!DOCTYPE html>
    <html>
    <head>
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  9. Krok 9: Język strony

    Kryteria:

    R.2.1 W plikach login.php lub wyniki.php znajduje się deklaracja HTML5 <!DOCTYPE HTML>, deklaracja języka zawartości: polski np. <html lang="pl">

    ok. 1 min
    Polecenie z arkusza

    Zadeklarowany polski język zawartości witryny.

    lang="pl" w <html> - razem z deklaracją HTML5 to R.2.1.

    login.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  10. Krok 10: Kodowanie polskich znaków

    ok. 1 min
    Polecenie z arkusza

    Jawnie zastosowany właściwy standard kodowania polskich znaków.

    <meta charset="UTF-8"> jako pierwszy element <head>. Druga linia, viewport, to standard każdego współczesnego szablonu: telefon nie udaje szerokiego ekranu i nie pomniejsza strony.

    login.php PHP zmienione: 2 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  11. Krok 11: Tytuł i arkusz stylów

    Kryteria: ,

    R.2.2 W sekcji head znajduje się jawnie podany standard kodowania polskich znaków i stronie login.php nadano tytuł "Zaloguj się" lub stronie wyniki.php tytuł "Przeglądaj wyniki"

    R.3.2 Strona zawiera działające połączenie z zewnętrznym arkuszem stylów o nazwie styl.css formatowanie CSS pochodzi jedynie z tego arkusza

    ok. 1 min
    Polecenie z arkusza

    Cechy strony login.php: Tytuł strony widoczny na karcie przeglądarki: „Zaloguj się”. Arkusz stylów w pliku o nazwie styl.css prawidłowo połączony z kodem strony.

    <title> z treścią z arkusza (z kodowaniem to R.2.2) i <link> do styl.css - całe formatowanie z tego pliku, bez atrybutów style (R.3.2).

    login.php PHP zmienione: 2 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  12. Krok 12: Bloki: nagłówek z h1, blok główny, stopka

    Kryteria:

    R.2.3 W sekcji body zdefiniowany jest podział na bloki za pomocą semantycznych znaczników HTML 5: header, main, footer (dopuszcza się stosowanie bloków pomocniczych)

    ok. 2 min
    Polecenie z arkusza

    Podział strony na bloki: nagłówkowy, główny oraz stopka. Podział zrealizowany za pomocą semantycznych znaczników języka HTML5, tak aby po uruchomieniu w przeglądarce układ bloków na stronie był zgody z ilustracją 4. Zawartość bloku nagłówkowego: nagłówek pierwszego stopnia o treści „Portal laboratorium”. Zwartość bloku stopki: Paragraf o treści „Wykonał:” dalej wstawiony numer zdającego.

    <header>, <main>, <footer> (R.2.3) - bloki stoją jeden pod drugim w zwykłym przepływie dokumentu, jak na ilustracji 4. W nagłówku <h1>, w stopce akapit: na egzaminie po „Wykonał:” wpisujesz swój numer zdającego (PESEL), tu stoi imię i nazwisko autora rozwiązania.

    Blok główny logowania wygląda inaczej niż blok główny strony wyników, więc dostaje identyfikator login-box.

    login.php PHP zmienione: 8 linii
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  13. Krok 13: Nagłówek i formularz wysyłany metodą POST do siebie

    Kryteria:

    R.3.3 Formularz wysyła dane metodą POST do tego samego pliku (poprawne też bez atrybutu action)

    ok. 2 min
    Polecenie z arkusza

    Zawartość bloku głównego: Nagłówek drugiego stopnia o treści „Zaloguj się do systemu”. Formularz wysyłający dane metodą bezpieczną do pliku login.php.

    method="post" - login i hasło idą w treści żądania, nie w adresie (przy get hasło zostałoby w historii przeglądarki i w logach serwera). action="login.php" - dane wracają do tego samego pliku, gdzie sprawdzi je skrypt 1 (R.3.3).

    Typowe błędy

    method="get" dopisuje hasło do adresu strony - arkusz pod „metodą bezpieczną” ma na myśli POST i klucz uznaje tylko taki zapis.

    login.php PHP zmienione: 3 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
            <h2>Zaloguj się do systemu</h2>
            <form action="login.php" method="post">
            </form>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  14. Krok 14: Etykieta i pole loginu

    Kryteria:

    R.2.5 Strona z logowaniem zawiera formularz składający się z dwóch pól edycyjnych i przycisku. Co najmniej jedno pole ma powiązaną etykietę (label z for i id lub zagnieżdżenie kontrolki w label)

    ok. 2 min
    Polecenie z arkusza

    Etykieta o treści „Login:”. Pole edycyjne. Etykiety są powiązane z odpowiednimi polami edycyjnymi.

    <label for="login"> i id="login" - powiązanie etykiety z polem (R.2.5): klik w napis ustawia kursor w polu, czytnik ekranu czyta nazwę pola. name="login" to klucz w $_POST.

    Dwa atrybuty spoza arkusza: required - przeglądarka nie wyśle pustego formularza (wygoda, nie zabezpieczenie: serwer i tak sprawdza dane sam), autocomplete="username" - menedżer haseł wie, co tu wpisać.

    login.php PHP zmienione: 2 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
            <h2>Zaloguj się do systemu</h2>
            <form action="login.php" method="post">
                <label for="login">Login:</label>
                <input type="text" id="login" name="login" autocomplete="username" required>
            </form>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  15. Krok 15: Etykieta i pole hasła

    Kryteria:

    R.3.5 Pole z hasłem jest typu password

    ok. 1 min
    Polecenie z arkusza

    Etykieta o treści „Hasło:”. Pole edycyjne służące, do wpisywania hasła, ukrywające wprowadzone znaki.

    type="password" zastępuje znaki kropkami (R.3.5) - tylko na ekranie. Przez sieć hasło idzie zwykłym tekstem, dlatego POST, a na prawdziwym serwerze także HTTPS.

    login.php PHP zmienione: 2 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
            <h2>Zaloguj się do systemu</h2>
            <form action="login.php" method="post">
                <label for="login">Login:</label>
                <input type="text" id="login" name="login" autocomplete="username" required>
                <label for="password">Hasło:</label>
                <input type="password" id="password" name="password" autocomplete="current-password" required>
            </form>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  16. Krok 16: Przycisk „Zaloguj”

    Kryteria:

    R.3.4 Przycisk realizuje funkcję wysłania danych z formularza (input lub button z typem submit lub button bez typu)

    ok. 1 min
    Polecenie z arkusza

    Przycisk o treści „Zaloguj” wysyłający dane z formularza do skryptu 1. Układ pól zgodny z ilustracją 2.

    <button type="submit"> wysyła formularz pod adres z action (R.3.4). Etykiety, pola i przycisk stoją na razie w jednym rzędzie - zamiast łamać linie znacznikami <br>, układ z ilustracji 2 ustawi siatka CSS w etapie stylów.

    login.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
            <h2>Zaloguj się do systemu</h2>
            <form action="login.php" method="post">
                <label for="login">Login:</label>
                <input type="text" id="login" name="login" autocomplete="username" required>
                <label for="password">Hasło:</label>
                <input type="password" id="password" name="password" autocomplete="current-password" required>
                <button type="submit">Zaloguj</button>
            </form>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  17. Krok 17: Strona wyniki.php: tytuł, nagłówek i tabela

    Kryteria: ,

    R.2.4 W bloku nagłówkowym znajduje się <h1>: "Portal laboratorium", w stopce <p> z numerem zdającego, w bloku głównym strony logowania <h2>: "Zaloguj się do systemu", w bloku głównym strony z wynikami <h2>: "Wyniki pacjentów"

    R.2.7 Pierwszy wiersz tabeli zawiera komórki nagłówkowe o treści: "Imię pacjenta", "Nazwisko pacjenta", "PESEL", "Data zlecenia", "Data wykonania", "Wynik", znaczniki <tr>, <th>

    ok. 3 min
    Polecenie z arkusza

    Cechy strony wyniki.php: Tytuł strony widoczny na karcie przeglądarki: „Przeglądaj wyniki”. Zawartość bloku głównego: Nagłówek drugiego stopnia o treści „Wyniki pacjentów”. Blok zawierający tabelę z 6 kolumnami. Pierwszy wiersz zawiera komórki nagłówkowe kolejno: „Imię pacjenta”, „Nazwisko pacjenta”, „PESEL”, „Data zlecenia”, „Data wykonania”, „Wynik”. Zawartość tabeli generowana jest skryptem 3.

    Kopia login.php jako wyniki.php: nowy tytuł, w bloku głównym <h2> i blok <div id="results"> z tabelą. Tabela ma dwie części:

    • <thead> - wiersz komórek nagłówkowych <th> (R.2.7),
    • <tbody> - na razie pusty; wiersze z danymi wypisze skrypt 3.

    Podział mówi przeglądarce i czytnikowi ekranu, który wiersz opisuje kolumny; przy wydruku długiej tabeli <thead> powtarza się na każdej stronie, a w stylach łatwo wskazać tylko wiersze z danymi. Komplet nagłówków z R.2.4: <h1>, oba <h2> i <p> w stopce.

    Typowe błędy

    Na ilustracji 3 ostatnia kolumna ma napis „Wyniki”, a w treści arkusza i w kluczu „Wynik” - wpisz jak w treści.

    wyniki.php PHP nowy plik
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Przeglądaj wyniki</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main>
            <h2>Wyniki pacjentów</h2>
            <div id="results">
                <table>
                    <thead>
                        <tr>
                            <th>Imię pacjenta</th>
                            <th>Nazwisko pacjenta</th>
                            <th>PESEL</th>
                            <th>Data zlecenia</th>
                            <th>Data wykonania</th>
                            <th>Wynik</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </div>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (wyniki.php)
  18. Krok 18: Odnośnik „Wyloguj”

    Kryteria:

    R.3.6 Odnośnik o treści "Wyloguj" otwiera stronę wylogowania

    ok. 1 min
    Polecenie z arkusza

    Paragraf z odnośnikiem do pliku wyloguj.php o treści „Wyloguj”.

    Akapit z odnośnikiem w nagłówku - kliknięcie otwiera stronę wylogowania (R.3.6). Nad nim stanie powitanie ze skryptu 2.

    wyniki.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Przeglądaj wyniki</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
            <p><a href="wyloguj.php">Wyloguj</a></p>
        </header>
        <main>
            <h2>Wyniki pacjentów</h2>
            <div id="results">
                <table>
                    <thead>
                        <tr>
                            <th>Imię pacjenta</th>
                            <th>Nazwisko pacjenta</th>
                            <th>PESEL</th>
                            <th>Data zlecenia</th>
                            <th>Data wykonania</th>
                            <th>Wynik</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </div>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (wyniki.php)
  19. Etap 3: Styl CSS

    Kroki 19-25 · po tym etapie masz 28 z 41 kryteriów (pkt)

    Krok 19: Zmienne z kolorami i ciało strony

    Kryteria:

    R.4.1 selektor body: krój czcionki Verdana - jako pierwszy, sans-serif - jako ostatni, kolor tła #D4F1F4, kolor czcionki #05445E

    ok. 2 min
    Polecenie z arkusza

    Dla selektora ciała strony: krój czcionki Verdana, w przypadku jego braku sans-serif, kolor tła #D4F1F4, kolor czcionki #05445E.

    Plik styl.css zaczyna się od zmiennych w :root: cztery kolory z arkusza, każdy wpisany raz. Turkus #75E6DA ma trzy bloki - w regule stoi var(--box), więc zmiana odcienia to jedna linia. Klucz każe sprawdzać kryteria R.4 w kodzie CSS: wartości są w kodzie, tylko w jednym miejscu.

    Reguła body (selektor znacznika, jak wymaga uwaga): Verdana, a gdy jej brak - sans-serif, tło i kolor tekstu (R.4.1).

    styl.css CSS nowy plik
    :root {
        --page: #D4F1F4;
        --text: #05445E;
        --box: #75E6DA;
        --line: #189AB4;
    }
    
    body {
        font-family: Verdana, sans-serif;
        background-color: var(--page);
        color: var(--text);
    }
    Podgląd strony po tym kroku (login.php)
  20. Krok 20: Wymiary i marginesy bloków

    Kryteria:

    R.3.1 Układ bloków po uruchomieniu strony w przeglądarce jest zgodny z ilustracją 4 w arkuszu egzaminacyjnym (żaden blok nie zachodzi na inne bloki - sprawdzić w inspektorze)

    ok. 3 min
    Polecenie z arkusza

    Dla bloku głównego na stronie login.php: szerokość 50 em, marginesy zewnętrzne automatycznie wyliczane przez przeglądarkę, marginesy wewnętrzne 20 px. Dla bloku stopki: wyrównanie tekstu do prawej strony. Dla bloku nagłówkowego: marginesy zewnętrzne 40 px. Dla bloku z tabelą: marginesy zewnętrzne 40 px, marginesy wewnętrzne 20 px.

    Reguły w kolejności, w jakiej bloki stoją na stronie: nagłówek, okno logowania, blok z tabelą, stopka.

    • margin: auto przy ustalonej szerokości (50em - względem rozmiaru czcionki, więc okno rośnie razem z powiększeniem tekstu) dzieli wolne miejsce po równo na lewy i prawy margines - blok staje na środku,
    • bloki stoją jeden pod drugim bez float, flex czy siatki - to zwykły przepływ dokumentu i dokładnie układ z ilustracji 4 (R.3.1).
    styl.css CSS zmienione: 19 linii
    :root {
        --page: #D4F1F4;
        --text: #05445E;
        --box: #75E6DA;
        --line: #189AB4;
    }
    
    body {
        font-family: Verdana, sans-serif;
        background-color: var(--page);
        color: var(--text);
    }
    
    header {
        margin: 40px;
    }
    
    #login-box {
        width: 50em;
        margin: auto;
        padding: 20px;
    }
    
    #results {
        margin: 40px;
        padding: 20px;
    }
    
    footer {
        text-align: right;
    }
    Podgląd strony po tym kroku (login.php)
  21. Krok 21: Wspólny wygląd trzech bloków: tło, zaokrąglenie, cień

    Kryteria: , ,

    R.4.2 kolor tła: dla bloku głównego na stronie logowania, dla bloku nagłówkowego, dla bloku z tabelą - #75E6DA

    R.4.7 zaokrąglenie obramowania: blok główny, nagłówkowy, blok z tabelą - 5px

    R.4.8 cień elementu: blok główny, nagłówkowy, blok z tabelą - box-shadow: 10px 10px 5px LightBlue

    ok. 2 min
    Polecenie z arkusza

    Dla bloku głównego na stronie login.php, dla bloku nagłówkowego oraz dla bloku z tabelą: kolor tła #75E6DA, zaokrąglenie obramowania 5 px, cień elementu o przesunięciu poziomym i pionowym 10 px, rozmyciu 5 px i kolorze LightBlue.

    Jedna reguła, trzy selektory - każdy w osobnej linii, łatwo dopisać czwarty. box-shadow: 10px 10px 5px LightBlue: przesunięcie w poziomie, w pionie, rozmycie, kolor; dodatnie przesunięcia kładą cień w prawo i w dół (R.4.2, R.4.7, R.4.8).

    styl.css CSS zmienione: 8 linii
    :root {
        --page: #D4F1F4;
        --text: #05445E;
        --box: #75E6DA;
        --line: #189AB4;
    }
    
    body {
        font-family: Verdana, sans-serif;
        background-color: var(--page);
        color: var(--text);
    }
    
    header {
        margin: 40px;
    }
    
    #login-box {
        width: 50em;
        margin: auto;
        padding: 20px;
    }
    
    #results {
        margin: 40px;
        padding: 20px;
    }
    
    footer {
        text-align: right;
    }
    
    header,
    #login-box,
    #results {
        background-color: var(--box);
        border-radius: 5px;
        box-shadow: 10px 10px 5px LightBlue;
    }
    Podgląd strony po tym kroku (login.php)
  22. Krok 22: Nagłówki i formularz na środku, pola na siatce

    Kryteria:

    R.4.4 wyrównanie tekstu: blok stopki - do prawej strony, selektory h1, h2, form - do środka

    ok. 3 min
    Polecenie z arkusza

    Dla selektora nagłówka pierwszego stopnia, selektora nagłówka drugiego stopnia oraz selektora formularza: wyrównanie tekstu do środka. Dla selektora formularza: marginesy zewnętrzne 10 px.

    text-align: center dla h1, h2 i form (selektory znaczników, jak wymaga uwaga - R.4.4). Druga reguła dla formularza - margines 10 px z arkusza i siatka zamiast znaczników <br>:

    • display: grid + grid-template-columns: max-content max-content - dwie kolumny, każda szeroka na swoją najszerszą zawartość: w lewej etykiety, w prawej pola. Elementy formularza wpadają do komórek po kolei, więc pary etykieta - pole same ustawiają się w wierszach,
    • justify-content: center - siatka węższa niż blok staje na środku,
    • gap: 4px - odstęp między komórkami zamiast marginesów pól.

    Przycisk zajmie cały wiersz pod polami w kolejnym kroku.

    styl.css CSS zmienione: 14 linii
    :root {
        --page: #D4F1F4;
        --text: #05445E;
        --box: #75E6DA;
        --line: #189AB4;
    }
    
    body {
        font-family: Verdana, sans-serif;
        background-color: var(--page);
        color: var(--text);
    }
    
    header {
        margin: 40px;
    }
    
    #login-box {
        width: 50em;
        margin: auto;
        padding: 20px;
    }
    
    #results {
        margin: 40px;
        padding: 20px;
    }
    
    footer {
        text-align: right;
    }
    
    header,
    #login-box,
    #results {
        background-color: var(--box);
        border-radius: 5px;
        box-shadow: 10px 10px 5px LightBlue;
    }
    
    h1,
    h2,
    form {
        text-align: center;
    }
    
    form {
        display: grid;
        grid-template-columns: max-content max-content;
        justify-content: center;
        gap: 4px;
        margin: 10px;
    }
    Podgląd strony po tym kroku (login.php)
  23. Krok 23: Przycisk „Zaloguj”: szerokość i własny wiersz

    Kryteria:

    R.4.3 szerokość bloku głównego na stronie logowania - 50em, dla przycisku "Zaloguj" - 14em

    ok. 2 min
    Polecenie z arkusza

    Dla przycisku o treści “Zaloguj”: szerokość 14 em.

    width: 14em z arkusza (z oknem 50 em to R.4.3). grid-column: 1 / -1 rozciąga obszar przycisku od pierwszej do ostatniej linii siatki (-1 to zawsze ostatnia), czyli na oba kolumny, a justify-self: center stawia przycisk na środku tego obszaru - jak na ilustracji 2.

    styl.css CSS zmienione: 6 linii
    :root {
        --page: #D4F1F4;
        --text: #05445E;
        --box: #75E6DA;
        --line: #189AB4;
    }
    
    body {
        font-family: Verdana, sans-serif;
        background-color: var(--page);
        color: var(--text);
    }
    
    header {
        margin: 40px;
    }
    
    #login-box {
        width: 50em;
        margin: auto;
        padding: 20px;
    }
    
    #results {
        margin: 40px;
        padding: 20px;
    }
    
    footer {
        text-align: right;
    }
    
    header,
    #login-box,
    #results {
        background-color: var(--box);
        border-radius: 5px;
        box-shadow: 10px 10px 5px LightBlue;
    }
    
    h1,
    h2,
    form {
        text-align: center;
    }
    
    form {
        display: grid;
        grid-template-columns: max-content max-content;
        justify-content: center;
        gap: 4px;
        margin: 10px;
    }
    
    button {
        grid-column: 1 / -1;
        justify-self: center;
        width: 14em;
    }
    Podgląd strony po tym kroku (login.php)
  24. Krok 24: Tabela: wyśrodkowanie, połączone linie, komórki

    Kryteria: ,

    R.4.5 marginesy wewnętrzne (padding) : blok główny na stronie logowania, blok z tabelą - 20px, komórki nagłówkowe tabeli i komórki tabeli - 10px, marginesy zewnętrzne (margin) : blok główny na stronie logowania, tabela - auto, blok nagłówkowy, blok z tabelą - 40px, formularz - 10px

    R.4.6 obramowanie dowolną, pojedynczą linią ciągłą o szerokości - 1px i kolorze #189AB4 dla tabeli i wiersza tabeli; obramowanie połączone dla tabeli (border-collapse: collapse;)

    ok. 2 min
    Polecenie z arkusza

    Dla tabeli: marginesy zewnętrzne automatycznie wyliczone przez przeglądarkę, obramowanie połączone. Dla tabeli i wiersza tabeli: obramowanie linią ciągłą 1 px o kolorze #189AB4. Dla komórek nagłówkowych tabeli i komórek tabeli: marginesy wewnętrzne 10 px.

    • margin: auto centruje tabelę w bloku, border-collapse: collapse łączy sąsiednie linie w jedną,
    • obramowanie dla table i tr (nie dla komórek): widać obwódkę i poziome linie między wierszami, bez pionowych - jak na ilustracji 3 (R.4.6),
    • padding: 10px w komórkach domyka kryterium marginesów: wewnętrzne okna, bloku z tabelą i komórek oraz zewnętrzne okna, tabeli, nagłówka, bloku z tabelą i formularza (R.4.5).
    Typowe błędy

    Ramka wiersza tr rysuje się tylko przy border-collapse: collapse.

    styl.css CSS zmienione: 15 linii
    :root {
        --page: #D4F1F4;
        --text: #05445E;
        --box: #75E6DA;
        --line: #189AB4;
    }
    
    body {
        font-family: Verdana, sans-serif;
        background-color: var(--page);
        color: var(--text);
    }
    
    header {
        margin: 40px;
    }
    
    #login-box {
        width: 50em;
        margin: auto;
        padding: 20px;
    }
    
    #results {
        margin: 40px;
        padding: 20px;
    }
    
    footer {
        text-align: right;
    }
    
    header,
    #login-box,
    #results {
        background-color: var(--box);
        border-radius: 5px;
        box-shadow: 10px 10px 5px LightBlue;
    }
    
    h1,
    h2,
    form {
        text-align: center;
    }
    
    form {
        display: grid;
        grid-template-columns: max-content max-content;
        justify-content: center;
        gap: 4px;
        margin: 10px;
    }
    
    button {
        grid-column: 1 / -1;
        justify-self: center;
        width: 14em;
    }
    
    table {
        margin: auto;
        border-collapse: collapse;
    }
    
    table,
    tr {
        border: 1px solid var(--line);
    }
    
    th,
    td {
        padding: 10px;
    }
    Podgląd strony po tym kroku (wyniki.php)
  25. Krok 25: Odnośniki bez podkreślenia, wiersz pod kursorem

    Kryteria: ,

    R.4.9 brak podkreślenia dla odnośnika

    R.4.10 gdy kursor znajdzie się na wierszu tabeli kolor tła zmienia się na biały o przezroczystości 0.8

    ok. 2 min
    Polecenie z arkusza

    Dla odnośnika: brak podkreślenia. Gdy kursor znajdzie się na wierszu tabeli kolor tła zmienia się na biały o przeźroczystości 0.8.

    • text-decoration: none dla wszystkich odnośników (R.4.9),
    • rgb(255 255 255 / 0.8) - nowa składnia koloru: składowe oddzielone spacjami, krycie po ukośniku. To ten sam kolor co rgba(255, 255, 255, 0.8); 80% bieli, przez które prześwituje turkus bloku (R.4.10),
    • selektor tbody tr:hover podświetla tylko wiersze z danymi - wiersz nagłówków w <thead> zostaje bez zmian.
    Typowe błędy

    opacity: 0.8 robi półprzezroczystym cały wiersz razem z tekstem - arkusz mówi o przezroczystości koloru tła.

    styl.css CSS zmienione: 8 linii
    :root {
        --page: #D4F1F4;
        --text: #05445E;
        --box: #75E6DA;
        --line: #189AB4;
    }
    
    body {
        font-family: Verdana, sans-serif;
        background-color: var(--page);
        color: var(--text);
    }
    
    header {
        margin: 40px;
    }
    
    #login-box {
        width: 50em;
        margin: auto;
        padding: 20px;
    }
    
    #results {
        margin: 40px;
        padding: 20px;
    }
    
    footer {
        text-align: right;
    }
    
    header,
    #login-box,
    #results {
        background-color: var(--box);
        border-radius: 5px;
        box-shadow: 10px 10px 5px LightBlue;
    }
    
    h1,
    h2,
    form {
        text-align: center;
    }
    
    form {
        display: grid;
        grid-template-columns: max-content max-content;
        justify-content: center;
        gap: 4px;
        margin: 10px;
    }
    
    button {
        grid-column: 1 / -1;
        justify-self: center;
        width: 14em;
    }
    
    table {
        margin: auto;
        border-collapse: collapse;
    }
    
    table,
    tr {
        border: 1px solid var(--line);
    }
    
    th,
    td {
        padding: 10px;
    }
    
    a {
        text-decoration: none;
    }
    
    tbody tr:hover {
        background-color: rgb(255 255 255 / 0.8);
    }
    Podgląd strony po tym kroku (wyniki.php)
  26. Etap 4: Skrypty PHP

    Kroki 26-38 · po tym etapie masz 41 z 41 kryteriów (pkt)

    Krok 26: Skrypt 1: start sesji na początku pliku

    Kryteria:

    R.5.1 Co najmniej jeden ze skryptów realizuje obsługę sesji w języku PHP. Przynajmniej raz użyte polecenia session_start() na początku pliku PHP

    ok. 2 min
    Polecenie z arkusza

    Napisane w języku PHP. Skrypty wykorzystują mechanizm sesji. Efekt działania skryptu 1.

    session_start() w pierwszej linii login.php, przed <!DOCTYPE html>: serwer zaczyna pamiętać dane tego użytkownika między stronami (rozpoznaje go po ciasteczku z identyfikatorem sesji). Ciasteczko idzie w nagłówkach odpowiedzi, a nagłówki muszą wyjść przed jakimkolwiek HTML-em (R.5.1). Od tego kroku podgląd to wynik prawdziwego uruchomienia w PHP 8.2.

    Typowe błędy

    Spacja albo pusta linia przed <?php to już wypisana treść - session_start() kończy się ostrzeżeniem „headers already sent”.

    login.php PHP zmienione: 3 linie
    <?php
    session_start();
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
            <h2>Zaloguj się do systemu</h2>
            <form action="login.php" method="post">
                <label for="login">Login:</label>
                <input type="text" id="login" name="login" autocomplete="username" required>
                <label for="password">Hasło:</label>
                <input type="password" id="password" name="password" autocomplete="current-password" required>
                <button type="submit">Zaloguj</button>
            </form>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (login.php)
  27. Krok 27: Skrypt 1: dane z formularza

    Kryteria:

    R.5.10 Zastosowana przynajmniej jedna zmienna oraz nazewnictwo jest w języku polskim lub angielskim i odzwierciedla cel zastosowania

    ok. 3 min
    Polecenie z arkusza

    Jeżeli dane zostały wysłane z formularza.

    • $_SERVER['REQUEST_METHOD'] === 'POST' - formularz był wysłany (pierwsze wejście na stronę to żądanie GET),
    • filter_input(INPUT_POST, 'login') czyta pole z formularza, a gdy pola nie ma, zwraca null zamiast ostrzeżenia „Undefined array key”, jak przy $_POST['login'],
    • (string) zamienia ewentualne null na pusty napis, a trim obcina spacje z brzegów loginu (często wklejane razem z nazwą). hasło zostaje bez zmian - spacja może być jego częścią,
    • $login i $error dostają wartości początkowe przed if, bo szablon HTML będzie ich używał także przy pierwszym wejściu.

    Nazwy zmiennych po angielsku, mówią, co przechowują (R.5.10).

    login.php PHP zmienione: 8 linii
    <?php
    session_start();
    
    $login = '';
    $error = '';
    
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $login = trim((string) filter_input(INPUT_POST, 'login'));
        $password = (string) filter_input(INPUT_POST, 'password');
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
            <h2>Zaloguj się do systemu</h2>
            <form action="login.php" method="post">
                <label for="login">Login:</label>
                <input type="text" id="login" name="login" autocomplete="username" required>
                <label for="password">Hasło:</label>
                <input type="password" id="password" name="password" autocomplete="current-password" required>
                <button type="submit">Zaloguj</button>
            </form>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 26.

  28. Krok 28: Skrypt 1: sprawdzenie loginu i hasła, zmienna sesji

    Kryteria:

    R.5.3 Skrypt 1 (login.php) pobiera dane z formularza. Jeżeli w formularzu podany login admin oraz hasło TajneHaslo321* to ustawiana jest zmienna sesji zawierająca aktualną nazwę użytkownika. Wykorzystana tablica $_SESSION

    ok. 3 min
    Polecenie z arkusza

    Jeżeli dane zostały wysłane z formularza i login to admin, a hasło to TajneHaslo321* ustawiana jest zmienna sesji zawierająca aktualną nazwę użytkownika.

    • $login === 'admin' - porównanie ścisłe: ten sam typ i ta sama wartość,
    • hash_equals('TajneHaslo321*', $password) porównuje napisy zawsze w tym samym czasie, niezależnie od tego, na którym znaku się różnią. Zwykłe == kończy przy pierwszej różnicy, więc z czasu odpowiedzi da się (przy wielu próbach) zgadywać hasło znak po znaku. Hasło z arkusza zostaje w kodzie dosłownie - egzaminator sprawdza R.5.3 w kodzie źródłowym,
    • session_regenerate_id(true) - po zalogowaniu sesja dostaje nowy identyfikator, a stary jest usuwany. Jeśli ktoś podsunął użytkownikowi swój identyfikator przed logowaniem, po zalogowaniu nic mu on nie da,
    • $_SESSION['user'] = $login - nazwa użytkownika w sesji (R.5.3).
    Typowe błędy

    Hasło jest TajneHaslo321* - z gwiazdką na końcu i wielkimi literami T i H. Bez gwiazdki egzaminator nie zaloguje się na stronę.

    login.php PHP zmienione: 5 linii
    <?php
    session_start();
    
    $login = '';
    $error = '';
    
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $login = trim((string) filter_input(INPUT_POST, 'login'));
        $password = (string) filter_input(INPUT_POST, 'password');
    
        if ($login === 'admin' && hash_equals('TajneHaslo321*', $password)) {
            session_regenerate_id(true);
            $_SESSION['user'] = $login;
        }
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
            <h2>Zaloguj się do systemu</h2>
            <form action="login.php" method="post">
                <label for="login">Login:</label>
                <input type="text" id="login" name="login" autocomplete="username" required>
                <label for="password">Hasło:</label>
                <input type="password" id="password" name="password" autocomplete="current-password" required>
                <button type="submit">Zaloguj</button>
            </form>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 26.

  29. Krok 29: Skrypt 1: przejście na stronę wyników

    Kryteria:

    R.5.4 Skrypt 1 (login.php) ładuje stronę wyniki.php (np. header("location: wyniki.php"))

    ok. 2 min
    Polecenie z arkusza

    Oraz zostaje załadowana strona wyniki.php.

    header('Location: wyniki.php') wysyła przeglądarce polecenie przejścia na stronę wyników (R.5.4), a exit kończy skrypt. Bez exit PHP wykonuje plik dalej i wysyła cały formularz, choć nikt go nie zobaczy - przy stronie wyników to samo zaniechanie wysłałoby dane pacjentów (kolejne kroki). Zasada: po każdym header('Location: ...') stoi exit.

    login.php PHP zmienione: 2 linie
    <?php
    session_start();
    
    $login = '';
    $error = '';
    
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $login = trim((string) filter_input(INPUT_POST, 'login'));
        $password = (string) filter_input(INPUT_POST, 'password');
    
        if ($login === 'admin' && hash_equals('TajneHaslo321*', $password)) {
            session_regenerate_id(true);
            $_SESSION['user'] = $login;
            header('Location: wyniki.php');
            exit;
        }
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
            <h2>Zaloguj się do systemu</h2>
            <form action="login.php" method="post">
                <label for="login">Login:</label>
                <input type="text" id="login" name="login" autocomplete="username" required>
                <label for="password">Hasło:</label>
                <input type="password" id="password" name="password" autocomplete="current-password" required>
                <button type="submit">Zaloguj</button>
            </form>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 26.

  30. Krok 30: Skrypt 1: komunikat przy złym haśle i zapamiętany login

    ok. 4 min
    Polecenie z arkusza

    Jeżeli dane zostały wysłane z formularza i login to admin, a hasło to TajneHaslo321* ustawiana jest zmienna sesji zawierająca aktualną nazwę użytkownika oraz zostaje załadowana strona wyniki.php.

    Arkusz nie mówi, co ma się stać przy złym haśle - wersja podstawowa po prostu pokazuje pusty formularz. Tu:

    • po nieudanej próbie $error dostaje komunikat, a szablon wypisuje go pod przyciskiem (role="alert" - czytnik ekranu przeczyta go od razu). Komunikat nie zdradza, czy błędny był login, czy hasło,
    • pole loginu dostaje value="<?= htmlspecialchars($login) ?>" - wpisany login zostaje, trzeba poprawić tylko hasło,
    • htmlspecialchars jest tu konieczne: login pochodzi od użytkownika. Sprawdzone: login "><script>alert(1)</script> wraca jako zwykły tekst w polu, a bez funkcji zamknąłby atrybut value i dopisał do strony skrypt,
    • <?= ... ?> to skrót od <?php echo ... ?>, a if (...): ... endif; - składnia alternatywna, czytelniejsza w środku HTML-u niż nawiasy klamrowe. Komunikat zajmuje cały wiersz siatki formularza (reguła form p w stylach).

    Podgląd: stan po wpisaniu loginu ze spacjami i złego hasła - login przycięty, komunikat pod przyciskiem.

    login.php PHP zmienione: 6 linii
    <?php
    session_start();
    
    $login = '';
    $error = '';
    
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $login = trim((string) filter_input(INPUT_POST, 'login'));
        $password = (string) filter_input(INPUT_POST, 'password');
    
        if ($login === 'admin' && hash_equals('TajneHaslo321*', $password)) {
            session_regenerate_id(true);
            $_SESSION['user'] = $login;
            header('Location: wyniki.php');
            exit;
        }
    
        $error = 'Nieprawidłowy login lub hasło.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Zaloguj się</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
        </header>
        <main id="login-box">
            <h2>Zaloguj się do systemu</h2>
            <form action="login.php" method="post">
                <label for="login">Login:</label>
                <input type="text" id="login" name="login" value="<?= htmlspecialchars($login) ?>" autocomplete="username" required>
                <label for="password">Hasło:</label>
                <input type="password" id="password" name="password" autocomplete="current-password" required>
                <button type="submit">Zaloguj</button>
                <?php if ($error !== ''): ?>
                    <p role="alert"><?= $error ?></p>
                <?php endif; ?>
            </form>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    styl.css CSS zmienione: 4 linie
    :root {
        --page: #D4F1F4;
        --text: #05445E;
        --box: #75E6DA;
        --line: #189AB4;
    }
    
    body {
        font-family: Verdana, sans-serif;
        background-color: var(--page);
        color: var(--text);
    }
    
    header {
        margin: 40px;
    }
    
    #login-box {
        width: 50em;
        margin: auto;
        padding: 20px;
    }
    
    #results {
        margin: 40px;
        padding: 20px;
    }
    
    footer {
        text-align: right;
    }
    
    header,
    #login-box,
    #results {
        background-color: var(--box);
        border-radius: 5px;
        box-shadow: 10px 10px 5px LightBlue;
    }
    
    h1,
    h2,
    form {
        text-align: center;
    }
    
    form {
        display: grid;
        grid-template-columns: max-content max-content;
        justify-content: center;
        gap: 4px;
        margin: 10px;
    }
    
    form p {
        grid-column: 1 / -1;
    }
    
    button {
        grid-column: 1 / -1;
        justify-self: center;
        width: 14em;
    }
    
    table {
        margin: auto;
        border-collapse: collapse;
    }
    
    table,
    tr {
        border: 1px solid var(--line);
    }
    
    th,
    td {
        padding: 10px;
    }
    
    a {
        text-decoration: none;
    }
    
    tbody tr:hover {
        background-color: rgb(255 255 255 / 0.8);
    }
    Podgląd strony po tym kroku (login.php)
  31. Krok 31: Skrypt 2: strona tylko dla zalogowanych

    ok. 3 min
    Polecenie z arkusza

    Jeżeli zmienna sesji zawierająca aktualną nazwę użytkownika nie jest ustawiona następuje przekierowanie do strony login.php.

    Na początku wyniki.php: session_start(), a potem !isset($_SESSION['user']) - „w sesji nie ma użytkownika”. Wtedy header('Location: login.php') i exit.

    Sprawdzone uruchomieniem: wejście na wyniki.php bez sesji daje odpowiedź 302 z przekierowaniem na login.php i pustą treścią. Wersja podstawowa (bez exit) w tej samej sytuacji wysyła całą tabelę z danymi pacjentów - przeglądarka i tak przejdzie na logowanie, ale program, który nie słucha przekierowań (np. curl), dostaje dane bez logowania. Podgląd od teraz to strona po prawdziwym zalogowaniu formularzem.

    wyniki.php PHP zmienione: 8 linii
    <?php
    session_start();
    
    if (!isset($_SESSION['user'])) {
        header('Location: login.php');
        exit;
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Przeglądaj wyniki</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
            <p><a href="wyloguj.php">Wyloguj</a></p>
        </header>
        <main>
            <h2>Wyniki pacjentów</h2>
            <div id="results">
                <table>
                    <thead>
                        <tr>
                            <th>Imię pacjenta</th>
                            <th>Nazwisko pacjenta</th>
                            <th>PESEL</th>
                            <th>Data zlecenia</th>
                            <th>Data wykonania</th>
                            <th>Wynik</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </div>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (wyniki.php)
  32. Krok 32: Skrypt 2: powitanie z nazwą z sesji

    Kryteria: ,

    R.2.8 Na stronie z wynikami nazwa użytkownika jest zapisana w znaczniku <strong> (w skrypcie lub HTML)

    R.5.5 Skrypt 2 (wyniki.php): jeżeli nie jest ustawiona zmienna sesji zawierająca nazwę użytkownika, następuje przekierowanie do strony logowania. Napis w bloku nagłówkowym to: "Witaj <użytkownik>” gdzie <użytkownik> to nazwa użytkownika pobrana ze zmiennej sesji

    ok. 3 min
    Polecenie z arkusza

    W bloku nagłówkowym, w akapicie wyświetlany jest napis „Witaj, <użytkownik>” gdzie <użytkownik> to nazwa użytkownika pobrana ze zmiennej sesji. Nazwa użytkownika jest zapisana w znaczniku semantycznym oznaczającym tekst ważny, formatowany domyślnie jako pogrubiony.

    Akapit „Witaj, ” z nazwą z sesji w <strong> - tekst ważny, pogrubiony (R.2.8, R.5.5). HTML stoi w szablonie, a PHP wstawia tylko wartość.

    Funkcja e() to krótka nazwa dla htmlspecialchars($text ?? '', ENT_QUOTES): zamienia <, >, & i oba cudzysłowy na encje, a null na pusty napis. Będzie potrzebna przy każdej wartości z bazy, więc warto ją mieć pod ręką - e(...) łatwo dopisać i łatwo zauważyć, gdy go brakuje. ?string w nagłówku funkcji mówi, że przyjmuje napis albo null.

    wyniki.php PHP zmienione: 6 linii
    <?php
    session_start();
    
    if (!isset($_SESSION['user'])) {
        header('Location: login.php');
        exit;
    }
    
    function e(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Przeglądaj wyniki</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
            <p>Witaj, <strong><?= e($_SESSION['user']) ?></strong></p>
            <p><a href="wyloguj.php">Wyloguj</a></p>
        </header>
        <main>
            <h2>Wyniki pacjentów</h2>
            <div id="results">
                <table>
                    <thead>
                        <tr>
                            <th>Imię pacjenta</th>
                            <th>Nazwisko pacjenta</th>
                            <th>PESEL</th>
                            <th>Data zlecenia</th>
                            <th>Data wykonania</th>
                            <th>Wynik</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </div>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (wyniki.php)
  33. Krok 33: Skrypt 3: połączenie PDO, zamknięcie i obsługa awarii

    Kryteria:

    R.5.2 Skrypt 3 (wyniki.php) realizuje połączenie z serwerem bazy danych i wybór bazy laboratorium oraz ostatnią operacją na bazie jest jej zamknięcie

    ok. 4 min
    Polecenie z arkusza

    Łączy się z serwerem bazodanowym na localhost, użytkownik root bez hasła, baza danych o nazwie laboratorium. Ostatnią operacją skryptu jest zamknięcie połączenia z bazą danych.

    Skrypt 3 stoi na górze pliku, pod sprawdzeniem sesji - najpierw dane, potem HTML. Połączenie przez PDO (klucz R.5 dopuszcza MySQLi i PDO):

    • 'mysql:host=localhost;dbname=laboratorium;charset=utf8mb4' - serwer, baza i kodowanie w jednym napisie (DSN); utf8mb4 to pełny UTF-8,
    • 'root', '' - użytkownik i puste hasło z arkusza,
    • PDO::ERRMODE_EXCEPTION - każdy błąd bazy rzuca wyjątek, zamiast po cichu zwracać false; PDO::FETCH_ASSOC - wiersze jako tablice z nazwami kolumn.

    PDO nie ma funkcji „zamknij” - połączenie kończy się, gdy nie wskazuje na nie już żadna zmienna. $pdo = null zamyka je jawnie i to jest ostatnia operacja na bazie (R.5.2); zapytanie trafi między te dwie linie.

    try / catch (PDOException $exception): gdy baza nie odpowiada, wyjątek trafia do catch. Szczegóły (z nazwą serwera i użytkownika) idą tylko do dziennika serwera przez error_log, a użytkownik dostanie ogólny komunikat z $dbError. Wartości początkowe $orders = [] i $dbError = '' stoją przed try, żeby szablon miał co wypisać w każdym przypadku.

    wyniki.php PHP zmienione: 14 linii
    <?php
    session_start();
    
    if (!isset($_SESSION['user'])) {
        header('Location: login.php');
        exit;
    }
    
    function e(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $orders = [];
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=laboratorium;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wyników. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Przeglądaj wyniki</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
            <p>Witaj, <strong><?= e($_SESSION['user']) ?></strong></p>
            <p><a href="wyloguj.php">Wyloguj</a></p>
        </header>
        <main>
            <h2>Wyniki pacjentów</h2>
            <div id="results">
                <table>
                    <thead>
                        <tr>
                            <th>Imię pacjenta</th>
                            <th>Nazwisko pacjenta</th>
                            <th>PESEL</th>
                            <th>Data zlecenia</th>
                            <th>Data wykonania</th>
                            <th>Wynik</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </div>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 32.

  34. Krok 34: Skrypt 3: zapytanie 4

    Kryteria:

    R.5.6 Skrypt 3 (wyniki.php) zawiera instrukcję wysyłającą do bazy danych zapytanie 4

    ok. 2 min
    Polecenie z arkusza

    Wysyła do bazy danych zapytanie 4.

    Zapytanie 4 z aliasami, rozpisane na kilka linii w apostrofach. $pdo->query(...) wysyła je do bazy, a ->fetchAll() od razu odbiera wszystkie wiersze do tablicy $orders - 12 tablic z kluczami imie, nazwisko, pesel, data_zlecenia, data_wyniku, plik (R.5.6). Po tej linii połączenie nie jest już potrzebne, więc zaraz za nią stoi $pdo = null.

    query wystarcza, bo w zapytaniu nie ma żadnej wartości od użytkownika. Gdyby była (np. nazwisko z wyszukiwarki), byłoby prepare i execute z parametrem - nigdy sklejanie napisu.

    wyniki.php PHP zmienione: 6 linii
    <?php
    session_start();
    
    if (!isset($_SESSION['user'])) {
        header('Location: login.php');
        exit;
    }
    
    function e(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $orders = [];
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=laboratorium;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        $orders = $pdo->query(
            'SELECT p.imie, p.nazwisko, p.pesel, z.data AS data_zlecenia, w.data AS data_wyniku, w.plik
             FROM Zlecenie AS z
             JOIN Pacjent AS p ON p.id = z.idPacjenta
             LEFT JOIN Wynik AS w ON w.idZlecenia = z.id'
        )->fetchAll();
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wyników. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Przeglądaj wyniki</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
            <p>Witaj, <strong><?= e($_SESSION['user']) ?></strong></p>
            <p><a href="wyloguj.php">Wyloguj</a></p>
        </header>
        <main>
            <h2>Wyniki pacjentów</h2>
            <div id="results">
                <table>
                    <thead>
                        <tr>
                            <th>Imię pacjenta</th>
                            <th>Nazwisko pacjenta</th>
                            <th>PESEL</th>
                            <th>Data zlecenia</th>
                            <th>Data wykonania</th>
                            <th>Wynik</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </div>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 32.

  35. Krok 35: Skrypt 3: wiersze tabeli z danymi

    Kryteria: ,

    R.2.6 Strona z wynikami zawiera tabelę składającą się z 6 kolumn. Zastosowano znaczniki <table>, <tr>, <td> (w skrypcie lub HTML)

    R.5.7 Skrypt 3 (wyniki.php) dla każdego zwróconego zapytaniem rekordu tworzy nowy wiersz tabeli i wypełnia go danymi pobranymi z bazy

    ok. 3 min
    Polecenie z arkusza

    Wyniki zapytania wyświetlane są jako kolejne wiersze tabeli (zgodnie z ilustracją 3).

    W <tbody> pętla foreach ($orders as $order): ... endforeach; w składni alternatywnej: dla każdego zlecenia wiersz <tr> z komórkami <td> (R.2.6, R.5.7). Wartości po nazwach kolumn ($order['pesel']), nie po numerach - kolejność kolumn w SELECT może się zmienić, a kod nadal trafi w dobrą.

    Każda wartość przez e(): dane z bazy też mogą zawierać znaki < czy & (wpisane kiedyś przez inny program), a e(null) dla zleceń bez wyniku daje pustą komórkę bez ostrzeżenia. Ostatnia komórka jest na razie pusta. Podgląd: 12 wierszy w kolejności z ilustracji 3 (tak oddała je MariaDB; bez ORDER BY kolejność nie jest gwarantowana).

    wyniki.php PHP zmienione: 10 linii
    <?php
    session_start();
    
    if (!isset($_SESSION['user'])) {
        header('Location: login.php');
        exit;
    }
    
    function e(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $orders = [];
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=laboratorium;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        $orders = $pdo->query(
            'SELECT p.imie, p.nazwisko, p.pesel, z.data AS data_zlecenia, w.data AS data_wyniku, w.plik
             FROM Zlecenie AS z
             JOIN Pacjent AS p ON p.id = z.idPacjenta
             LEFT JOIN Wynik AS w ON w.idZlecenia = z.id'
        )->fetchAll();
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wyników. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Przeglądaj wyniki</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
            <p>Witaj, <strong><?= e($_SESSION['user']) ?></strong></p>
            <p><a href="wyloguj.php">Wyloguj</a></p>
        </header>
        <main>
            <h2>Wyniki pacjentów</h2>
            <div id="results">
                <table>
                    <thead>
                        <tr>
                            <th>Imię pacjenta</th>
                            <th>Nazwisko pacjenta</th>
                            <th>PESEL</th>
                            <th>Data zlecenia</th>
                            <th>Data wykonania</th>
                            <th>Wynik</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($orders as $order): ?>
                            <tr>
                                <td><?= e($order['imie']) ?></td>
                                <td><?= e($order['nazwisko']) ?></td>
                                <td><?= e($order['pesel']) ?></td>
                                <td><?= e($order['data_zlecenia']) ?></td>
                                <td><?= e($order['data_wyniku']) ?></td>
                                <td></td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (wyniki.php)
  36. Krok 36: Skrypt 3: odnośnik do pliku z wynikiem

    Kryteria: ,

    R.3.7 Tabela zawiera przynajmniej jeden odnośnik, który otwiera plik PDF w nowej karcie przeglądarki

    R.5.8 W ostatniej kolumnie tabeli jest tworzony odnośnik prowadzący do pliku o nazwie zwróconej z bazy tylko wtedy, gdy nazwa jest niepusta

    ok. 3 min
    Polecenie z arkusza

    W ostatniej kolumnie tabeli, gdy z bazy została zwrócona niepusta nazwa pliku wstawiany jest odnośnik o treści „wynik” prowadzący do pliku o nazwie zwróconej z bazy. Odnośnik otwiera się w nowej karcie przeglądarki.

    • !empty($order['plik']) jest fałszywe dla NULL (zlecenie bez wyniku) i dla pustego napisu - jeden warunek na oba przypadki; komórka zostaje wtedy pusta, a wiersz ma 6 kolumn,
    • href z nazwą pliku przez e() - nazwa trafia do atrybutu, więc cudzysłów w nazwie nie może go zamknąć,
    • target="_blank" - nowa karta (R.3.7, R.5.8); rel="noopener" odcina otwartą kartę od strony, z której ją otwarto (nowe przeglądarki robią to same, ale atrybut mówi to wprost).

    Podgląd: trzy odnośniki „wynik”. Pliki PDF z archiwum muszą leżeć obok stron.

    wyniki.php PHP zmienione: 5 linii
    <?php
    session_start();
    
    if (!isset($_SESSION['user'])) {
        header('Location: login.php');
        exit;
    }
    
    function e(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $orders = [];
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=laboratorium;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        $orders = $pdo->query(
            'SELECT p.imie, p.nazwisko, p.pesel, z.data AS data_zlecenia, w.data AS data_wyniku, w.plik
             FROM Zlecenie AS z
             JOIN Pacjent AS p ON p.id = z.idPacjenta
             LEFT JOIN Wynik AS w ON w.idZlecenia = z.id'
        )->fetchAll();
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wyników. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Przeglądaj wyniki</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
            <p>Witaj, <strong><?= e($_SESSION['user']) ?></strong></p>
            <p><a href="wyloguj.php">Wyloguj</a></p>
        </header>
        <main>
            <h2>Wyniki pacjentów</h2>
            <div id="results">
                <table>
                    <thead>
                        <tr>
                            <th>Imię pacjenta</th>
                            <th>Nazwisko pacjenta</th>
                            <th>PESEL</th>
                            <th>Data zlecenia</th>
                            <th>Data wykonania</th>
                            <th>Wynik</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($orders as $order): ?>
                            <tr>
                                <td><?= e($order['imie']) ?></td>
                                <td><?= e($order['nazwisko']) ?></td>
                                <td><?= e($order['pesel']) ?></td>
                                <td><?= e($order['data_zlecenia']) ?></td>
                                <td><?= e($order['data_wyniku']) ?></td>
                                <td>
                                    <?php if (!empty($order['plik'])): ?>
                                        <a href="<?= e($order['plik']) ?>" target="_blank" rel="noopener">wynik</a>
                                    <?php endif; ?>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (wyniki.php)
  37. Krok 37: Skrypt 3: komunikat, gdy baza nie odpowiada

    ok. 3 min
    Polecenie z arkusza

    Wyniki zapytania wyświetlane są jako kolejne wiersze tabeli (zgodnie z ilustracją 3).

    Pod pętlą jeden wiersz na całą szerokość tabeli (colspan="6") z komunikatem z $dbError - tylko wtedy, gdy catch coś do niego wpisał.

    Sprawdzone uruchomieniem z błędną nazwą bazy: strona się wyświetla, w tabeli stoi „Nie udało się pobrać wyników. Spróbuj ponownie za chwilę.”, a w treści strony nie ma ani nazwy bazy, ani kodu błędu SQL. Bez try/catch PHP przerwałby stronę w połowie tabeli komunikatem „Fatal error: Uncaught PDOException” z pełną ścieżką pliku i nazwą serwera - informacją dla włamywacza, nie dla pracownika laboratorium. W normalnym działaniu (podgląd) wiersza komunikatu nie ma.

    wyniki.php PHP zmienione: 5 linii
    <?php
    session_start();
    
    if (!isset($_SESSION['user'])) {
        header('Location: login.php');
        exit;
    }
    
    function e(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $orders = [];
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=laboratorium;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        $orders = $pdo->query(
            'SELECT p.imie, p.nazwisko, p.pesel, z.data AS data_zlecenia, w.data AS data_wyniku, w.plik
             FROM Zlecenie AS z
             JOIN Pacjent AS p ON p.id = z.idPacjenta
             LEFT JOIN Wynik AS w ON w.idZlecenia = z.id'
        )->fetchAll();
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wyników. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Przeglądaj wyniki</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Portal laboratorium</h1>
            <p>Witaj, <strong><?= e($_SESSION['user']) ?></strong></p>
            <p><a href="wyloguj.php">Wyloguj</a></p>
        </header>
        <main>
            <h2>Wyniki pacjentów</h2>
            <div id="results">
                <table>
                    <thead>
                        <tr>
                            <th>Imię pacjenta</th>
                            <th>Nazwisko pacjenta</th>
                            <th>PESEL</th>
                            <th>Data zlecenia</th>
                            <th>Data wykonania</th>
                            <th>Wynik</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($orders as $order): ?>
                            <tr>
                                <td><?= e($order['imie']) ?></td>
                                <td><?= e($order['nazwisko']) ?></td>
                                <td><?= e($order['pesel']) ?></td>
                                <td><?= e($order['data_zlecenia']) ?></td>
                                <td><?= e($order['data_wyniku']) ?></td>
                                <td>
                                    <?php if (!empty($order['plik'])): ?>
                                        <a href="<?= e($order['plik']) ?>" target="_blank" rel="noopener">wynik</a>
                                    <?php endif; ?>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                        <?php if ($dbError !== ''): ?>
                            <tr>
                                <td colspan="6"><?= e($dbError) ?></td>
                            </tr>
                        <?php endif; ?>
                    </tbody>
                </table>
            </div>
        </main>
        <footer>
            <p>Wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (wyniki.php)
  38. Krok 38: Skrypt 4: wylogowanie

    Kryteria:

    R.5.9 Skrypt 4 (wyloguj.php) po kliknięciu na odnośnik "Wyloguj" usuwa zmienną sesji i przekierowuje do strony login.php

    ok. 3 min
    Polecenie z arkusza

    Skrypt 4 (wyloguj.php): Jeżeli jest ustawiona zmienna sesji zawierająca nazwę użytkownika to jest ona usuwana (dane sesji są czyszczone). Następuje przekierowanie do strony login.php.

    wyloguj.php to sam skrypt, bez HTML-u - i bez zamykającego ?> na końcu pliku: w pliku z samym PHP brak zamknięcia jest zalecany, bo przypadkowa pusta linia za ?> byłaby wypisaną treścią i blokowała header.

    • $_SESSION = [] czyści wszystkie dane sesji naraz - także nazwę użytkownika (warunek „jeżeli jest ustawiona” nie jest potrzebny: wyczyszczenie pustej sesji niczego nie psuje),
    • session_destroy() usuwa sesję z serwera,
    • przekierowanie na logowanie i exit (R.5.9).

    Sprawdzone uruchomieniem w jednej „przeglądarce”: logowanie, wyloguj.php (302 na login.php), potem wyniki.php - znowu 302 na login.php, czyli sesji już nie ma. Podgląd pokazuje stronę, na którą trafia użytkownik.

    wyloguj.php PHP nowy plik
    <?php
    session_start();
    
    $_SESSION = [];
    session_destroy();
    
    header('Location: login.php');
    exit;
    Podgląd strony po tym kroku (login.php)
  39. Etap 5: Porównanie z wersją podstawową

    Krok 39 · po tym etapie masz 41 z 41 kryteriów (pkt)

    Krok 39: Co jest inaczej i którą wersję wybrać na egzaminie

    ok. 2 min

    Obie wersje dają komplet 41 punktów. Różnice:

    wersja podstawowawersja nowoczesna
    bazamysqli_connect, mysqli_fetch_row, $row[0] ... $row[5]PDO z wyjątkami, fetchAll, $order['pesel']
    zamknięciemysqli_close($connection)$pdo = null
    przekierowaniasam headerheader + exit (bez tego tabela idzie do niezalogowanych)
    dane na stroniewproste() = htmlspecialchars(..., ENT_QUOTES)
    formularz$_POST + isset, pusty formularz po błędziefilter_input, trim, komunikat, zapamiętany login
    hasło==hash_equals + session_regenerate_id
    awaria bazybiała strona z błędemkomunikat w tabeli, szczegóły w error_log
    układ formularza<br>siatka CSS

    Na egzaminie wersja podstawowa jest szybsza do napisania i trzyma się funkcji z tabel arkusza. Z nowoczesnej warto przenieść choćby exit po header - jedno słowo, a zamyka realną dziurę. Czego tu świadomie nie ma: hasła jako skrótu password_hash (klucz R.5.3 sprawdza w kodzie, że hasło to TajneHaslo321* - pokazuje to wersja podstawowa w etapie „Dobre praktyki”), osobnego pliku config.php z danymi logowania do bazy (R.5.2 każe szukać połączenia w wyniki.php) i tokenu CSRF w formularzu - w prawdziwej aplikacji formularze zmieniające dane go potrzebują, a klucz go nie wymaga.

    Podgląd strony bez zmian - taki jak po kroku 38.

  40. Etap 6: Oddanie pracy

    Krok 40 · po tym etapie masz 41 z 41 kryteriów (pkt)

    Krok 40: Plik przeglądarka.txt i nagranie płyty

    ok. 5 min
    Polecenie z arkusza

    Po zakończeniu pracy utwórz plik tekstowy o nazwie przeglądarka.txt. Zapisz w nim nazwę przeglądarki internetowej, w której weryfikowana była poprawność działania witryny. Umieść go w folderze z numerem zdającego. Nagraj płytę z rezultatami pracy.

    W folderze z numerem zdającego: login.php, wyniki.php, wyloguj.php, styl.css, kwerendy.txt, import.png, kw1.png … kw4.png, trzy pliki PDF z wynikami i przeglądarka.txt. Płytę sprawdź po nagraniu.

    przeglądarka.txt Tekst nowy plik
    Google Chrome

    Podgląd strony bez zmian - taki jak po kroku 38.

Podgląd

To jedno z możliwych rozwiązań. Egzaminator ocenia spełnienie kryteriów z klucza odpowiedzi, a nie zgodność z tym kodem - inna, poprawna droga też daje punkty.

Autor rozwiązania: . Licencja CC BY-NC 4.0 - kopiowanie i przeróbki do celów niekomercyjnych, z podaniem autora i źródła (link do tej strony) (zasady wykorzystania). Treść arkusza i klucza oceniania pochodzi z CKE.