Szybki quiz – przypomnienie poprzedniej lekcji
Lekcja 20 jest w kursie pomijana, więc pytania dotyczą Lekcji 19:
INSERT,UPDATE,DELETEz PHP.
-
Co wypisze ten kod, jeśli gracz o
id = 2istnieje w bazie?$premia = 0; $uzytkownikId = 2; $zapytanie = $polaczenie->prepare('UPDATE uzytkownicy SET xp = xp + ? WHERE id = ?'); $zapytanie->bind_param('ii', $premia, $uzytkownikId); $zapytanie->execute(); var_dump($zapytanie->affected_rows); -
Kolega usuwa zadanie tak:
$zapytanie = $polaczenie->prepare('DELETE FROM zadania WHERE id = ?'); $zapytanie->bind_param('i', $id);$idprzychodzi z ukrytego pola formularza. Zapytanie jest przygotowane, więc SQL injection nie grozi. Co mimo to jest tu błędem i jak to naprawić? -
Dlaczego
bind_param('ii', 5, $uzytkownikId)kończy się błędem, abind_param('ii', $premia, $uzytkownikId)już nie? -
Który krok znany z
SELECTodpada przyUPDATEiDELETE? Czym zastępujemy sprawdzenie wyniku? -
Dlaczego XP przyznajemy tylko wtedy, gdy
affected_rowspo zapytaniuUPDATE zadania SET data_wykonania = CURDATE() WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULLwynosi 1, zamiast najpierw wczytać zadanie i sprawdzić w PHP
if (!$zadanie->czyWykonane())? -
dodaj-zadanie.phppo udanym zapisie wywołujeheader('Location: index.php'); exit;. Co zobaczyłby gracz, gdyby zamiast tego skrypt wypisał „Dodano zadanie” i gracz nacisnął F5?
Odpowiedzi
int(0). Wiersz pasuje doWHERE, alexp + 0to ta sama wartość, więc nic się nie zmieniło.affected_rowsprzyUPDATEliczy wiersze zmienione, nie pasujące.- Brakuje warunku na właściciela:
WHERE id = ? AND uzytkownik_id = ?z$_SESSION['uzytkownik_id']. Ukryte pole formularza może podmienić każdy (F12), więc bez tego warunku gracz usunie cudze zadanie. To nie SQL injection, tylko brak sprawdzenia uprawnień — prepared statement przed tym nie chroni. bind_param()przyjmuje argumenty przez referencję, czyli tylko zmienne. Stała5nie ma adresu w pamięci, który można przekazać — stądError: ... could not be passed by reference. Wartość trzeba najpierw zapisać w zmiennej.- Odpada
get_result()—INSERT,UPDATEiDELETEnie zwracają wierszy. Efekt sprawdzamy przez$zapytanie->affected_rows(a poINSERTdodatkowoinsert_id). - Bo między odczytem a zapisem może wejść drugie żądanie (dwie karty, podwójne kliknięcie). Oba przeszłyby przez
ifi oba dodałyby XP. Warunekdata_wykonania IS NULLwewnątrzUPDATEsprawia, że tylko jedno żądanie rzeczywiście zmieni wiersz i dostanieaffected_rowsrówne 1. - Przeglądarka zapytałaby o ponowne wysłanie danych formularza i po potwierdzeniu wykonałaby ten sam
INSERTjeszcze raz — powstałyby duplikaty zadania. Wzorzec POST → przekierowanie → GET sprawia, że F5 odświeża tylkoindex.php.
Superglobals (superzmienne globalne) to specjalne wbudowane tablice w PHP, które są automatycznie dostępne w każdym zakresie (scope) – w funkcjach, metodach, klasach i skryptach.
Nie musisz ich deklarować ani importować za pomocą słowa kluczowego global.
Poznaliśmy już tablice $_GET, $_POST, $_SESSION i $_COOKIE.
UWAGA: ZAWSZE waliduj dane – nigdy nie ufaj danym od użytkownika
Lista wszystkich Superglobals w PHP
PHP udostępnia 9 superglobalnych tablic:
| Superglobal | Zastosowanie | Kiedy używać |
|---|---|---|
$_GET | Dane z URL (query string) | Parametry URL, linki, wyszukiwanie |
$_POST | Dane z formularzy POST | Formularze, logowanie, rejestracja |
$_REQUEST | Dane z GET, POST i COOKIE | Rzadko – lepiej używać $_GET lub $_POST |
$_FILES | Przesłane pliki | Upload plików, zdjęć, dokumentów |
$_SESSION | Dane sesji użytkownika | Przechowywanie stanu zalogowania |
$_COOKIE | Cookies przeglądarki | Zapamiętywanie preferencji użytkownika |
$_SERVER | Informacje o serwerze i żądaniu | Adres IP, user agent, ścieżki |
$_ENV | Zmienne środowiskowe | Konfiguracja, dane wrażliwe |
$GLOBALS | Wszystkie zmienne globalne | Dostęp do zmiennych globalnych |
1. $_GET – Dane z URL (Query String)
Dane są przekazywane w adresie URL po znaku ? w formacie klucz=wartość, oddzielone znakiem &.
profil.php?id=5&username=ProGamer&sort=level
Odczytywanie danych z $_GET
<?php
// profil.php
// Sprawdzamy czy parametr istnieje
if (isset($_GET['id'])) {
$player_id = (int)$_GET['id']; // Rzutowanie na int dla bezpieczeństwa
echo "ID gracza: {$player_id}<br>";
} else {
echo "Brak parametru ID w URL<br>";
}
// Operator null coalescing (PHP 7+)
$username = $_GET['username'] ?? 'Nieznany';
$sort = $_GET['sort'] ?? 'id';
echo "Username: {$username}<br>";
echo "Sortowanie: {$sort}<br>";
// Wyświetl wszystkie parametry GET
echo "<pre>";
print_r($_GET);
echo "</pre>";
?>
Wyjście dla URL profil.php?id=5&username=ProGamer&sort=level:
ID gracza: 5
Username: ProGamer
Sortowanie: level
Array
(
[id] => 5
[username] => ProGamer
[sort] => level
)
Przykład 1: Wyszukiwarka z parametrami GET
<?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die('Błąd połączenia');
}
$db->set_charset('utf8mb4');
// Pobieramy parametry z URL
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['q'])) {
$search_query = trim($_GET['q']);
$min_level = (int)($_GET['min_level'] ?? 0);
$rank = $_GET['rank'] ?? '';
if (!empty($search_query)) {
// Budujemy zapytanie dynamicznie
$sql = "SELECT * FROM players WHERE (username LIKE ? OR email LIKE ?)";
$params = ["%{$search_query}%", "%{$search_query}%"];
$types = "ss";
if ($min_level > 0) {
$sql .= " AND level >= ?";
$types .= "i";
$params[] = $min_level;
}
if (!empty($rank)) {
$sql .= " AND rank_name = ?";
$types .= "s";
$params[] = $rank;
}
$sql .= " ORDER BY level DESC LIMIT 20";
$stmt = $db->prepare($sql);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$wynik = $stmt->get_result();
$safe_query = htmlspecialchars($search_query);
echo "<h2>Wyniki wyszukiwania dla: \"{$safe_query}\"</h2>";
if ($wynik->num_rows > 0) {
echo "<p>Znaleziono: {$wynik->num_rows} graczy</p>";
while ($gracz = $wynik->fetch_assoc()) {
$username = htmlspecialchars($gracz['username']);
echo "<div style='background:#f4f4f4; padding:10px; margin:5px 0;'>";
echo "<strong>{$username}</strong> - Level: {$gracz['level']}, Rank: {$gracz['rank_name']}<br>";
echo "<a href='profil.php?id={$gracz['id']}'>Zobacz profil</a>";
echo "</div>";
}
} else {
echo "<p>Nie znaleziono graczy</p>";
}
$stmt->close();
}
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Wyszukiwarka graczy</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.search-form {
background: #f4f4f4;
padding: 20px;
border-radius: 5px;
margin-bottom: 30px;
}
input, select {
padding: 10px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #764ba2;
}
</style>
</head>
<body>
<h1>Wyszukiwarka graczy</h1>
<form method="GET" class="search-form">
<input type="text"
name="q"
placeholder="Wpisz nick lub email"
value="<?= htmlspecialchars($_GET['q'] ?? '') ?>"
required>
<input type="number"
name="min_level"
placeholder="Min. poziom"
min="1"
value="<?= htmlspecialchars($_GET['min_level'] ?? '') ?>">
<select name="rank">
<option value="">Wszystkie rangi</option>
<option value="Bronze" <?= ($_GET['rank'] ?? '') == 'Bronze' ? 'selected' : '' ?>>Bronze</option>
<option value="Silver" <?= ($_GET['rank'] ?? '') == 'Silver' ? 'selected' : '' ?>>Silver</option>
<option value="Gold" <?= ($_GET['rank'] ?? '') == 'Gold' ? 'selected' : '' ?>>Gold</option>
<option value="Diamond" <?= ($_GET['rank'] ?? '') == 'Diamond' ? 'selected' : '' ?>>Diamond</option>
</select>
<button type="submit">Szukaj</button>
</form>
<p><small>Przykłady wyszukiwań:</small></p>
<ul>
<li><a href="?q=Pro">?q=Pro</a></li>
<li><a href="?q=Gamer&min_level=20">?q=Gamer&min_level=20</a></li>
<li><a href="?q=Test&rank=Gold">?q=Test&rank=Gold</a></li>
</ul>
</body>
</html>
Kiedy używać $_GET?
- Wyszukiwanie i filtrowanie
- Paginacja (
?page=2) - Sortowanie (
?sort=date&order=desc) - Linki do konkretnych zasobów (
?id=123) - Parametry, które użytkownik może udostępnić (share)
Nie stosujemy do:
- Haseł i danych wrażliwych (widoczne w URL!)
- Modyfikacji danych w bazie (INSERT, UPDATE, DELETE)
- Dużych ilości danych (limit ~2000 znaków)
2. $_POST – Dane z formularzy POST
Dane są wysyłane w ciele żądania HTTP (nie w URL), więc są niewidoczne dla użytkownika.
Odczytywanie danych z $_POST
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Sprawdzamy czy pole istnieje
if (isset($_POST['username'])) {
$username = trim($_POST['username']);
echo "Username: {$username}<br>";
}
// Operator null coalescing
$email = trim($_POST['email'] ?? '');
$age = (int)($_POST['age'] ?? 0);
echo "Email: {$email}<br>";
echo "Wiek: {$age}<br>";
// Wyświetl wszystkie dane POST
echo "<pre>";
print_r($_POST);
echo "</pre>";
}
?>
Formularz rejestracji z walidacją
<?php
session_start();
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die('Błąd połączenia');
}
$db->set_charset('utf8mb4');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Pobieramy wszystkie dane z formularza
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$password_confirm = $_POST['password_confirm'] ?? '';
$age = (int)($_POST['age'] ?? 0);
$terms = isset($_POST['terms']) ? 1 : 0; // Checkbox
// Walidacja
if (empty($username)) {
$errors[] = "Username jest wymagany";
} elseif (strlen($username) < 3) {
$errors[] = "Username musi mieć min. 3 znaki";
}
if (empty($email)) {
$errors[] = "Email jest wymagany";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Nieprawidłowy format email";
}
if (empty($password)) {
$errors[] = "Hasło jest wymagane";
} elseif (strlen($password) < 8) {
$errors[] = "Hasło musi mieć min. 8 znaków";
}
if ($password !== $password_confirm) {
$errors[] = "Hasła nie są identyczne";
}
if ($age < 13) {
$errors[] = "Musisz mieć co najmniej 13 lat";
}
if (!$terms) {
$errors[] = "Musisz zaakceptować regulamin";
}
// Jeśli brak błędów - zapisujemy
if (empty($errors)) {
// Sprawdzamy czy username już istnieje
$stmt = $db->prepare("SELECT id FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
$errors[] = "Username '{$username}' jest już zajęty";
}
$stmt->close();
if (empty($errors)) {
$password_hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO users (username, email, password, age, created_at) VALUES (?, ?, ?, ?, NOW())");
$stmt->bind_param("sssi", $username, $email, $password_hash, $age);
if ($stmt->execute()) {
$new_user_id = $stmt->insert_id;
$stmt->close();
$db->close();
$_SESSION['registration_success'] = "Konto utworzone pomyślnie!";
header("Location: logowanie.php");
exit;
} else {
$errors[] = "Błąd podczas rejestracji";
}
$stmt->close();
}
}
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Rejestracja</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
max-width: 500px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
input:focus {
outline: none;
border-color: #667eea;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
}
.checkbox-group input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.checkbox-group label {
margin: 0;
cursor: pointer;
}
button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
}
button:hover {
background: #764ba2;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #f5c6cb;
}
.error ul {
margin-left: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Rejestracja</h1>
<?php if (!empty($errors)): ?>
<div class="error">
<strong>Błędy walidacji:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label>Username:</label>
<input type="text"
name="username"
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
required>
</div>
<div class="form-group">
<label>Email:</label>
<input type="email"
name="email"
value="<?= htmlspecialchars($_POST['email'] ?? '') ?>"
required>
</div>
<div class="form-group">
<label>Hasło:</label>
<input type="password"
name="password"
required>
</div>
<div class="form-group">
<label>Powtórz hasło:</label>
<input type="password"
name="password_confirm"
required>
</div>
<div class="form-group">
<label>Wiek:</label>
<input type="number"
name="age"
min="13"
max="120"
value="<?= htmlspecialchars($_POST['age'] ?? '') ?>"
required>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox"
name="terms"
id="terms"
<?= isset($_POST['terms']) ? 'checked' : '' ?>
required>
<label for="terms">Akceptuję regulamin</label>
</div>
</div>
<button type="submit">Zarejestruj się</button>
</form>
</div>
</body>
</html>
Kiedy używać $_POST?
- Logowanie i rejestracja
- Formularze kontaktowe
- Modyfikacja danych (INSERT, UPDATE, DELETE)
- Przesyłanie dużych ilości danych
- Dane wrażliwe (hasła, dane osobowe)
Złe zastosowania:
- Wyszukiwanie (lepiej GET – możliwość udostępnienia linku)
- Linki do konkretnych stron (GET jest lepsze)
$_REQUEST – Połączenie GET, POST i COOKIE
$_REQUEST zawiera dane z $_GET, $_POST i $_COOKIE.
<?php
// Przykład: odbiera dane zarówno z GET jak i POST
$search = $_REQUEST['search'] ?? '';
echo "Wartość search: {$search}";
?>
UWAGA: $_REQUEST jest niezalecany ze względów bezpieczeństwa. Lepiej użyć konkretnej tablicy ($_GET lub $_POST).
4. $_FILES – Przesyłanie plików
Struktura $_FILES
Gdy użytkownik wysyła plik, PHP automatycznie wypełnia tablicę $_FILES:
$_FILES['nazwa_pola'] = [
'name' => 'oryginalnanazwa.jpg', // Nazwa pliku
'type' => 'image/jpeg', // Typ MIME
'tmp_name' => '/tmp/phpXXXXXX', // Tymczasowa lokalizacja
'error' => 0, // Kod błędu (0 = OK)
'size' => 245678 // Rozmiar w bajtach
];
Przykład 3: Upload zdjęcia profilowego
<?php
session_start();
$errors = [];
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
$file = $_FILES['avatar'];
// Sprawdzamy czy nie ma błędu uploadu
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = "Błąd podczas uploadu pliku (kod: {$file['error']})";
} else {
// Walidacja rozmiaru (max 2MB)
$max_size = 2 * 1024 * 1024; // 2MB w bajtach
if ($file['size'] > $max_size) {
$errors[] = "Plik jest za duży (max 2MB)";
}
// Walidacja typu pliku
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($file['type'], $allowed_types)) {
$errors[] = "Nieprawidłowy typ pliku (dozwolone: JPG, PNG, GIF)";
}
// Dodatkowa walidacja przez rozszerzenie
$file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
if (!in_array($file_extension, $allowed_extensions)) {
$errors[] = "Nieprawidłowe rozszerzenie pliku";
}
// Jeśli brak błędów - zapisujemy plik
if (empty($errors)) {
// Generujemy unikalną nazwę pliku
$new_filename = uniqid('avatar_', true) . '.' . $file_extension;
$upload_dir = 'uploads/avatars/';
// Tworzymy katalog jeśli nie istnieje
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$destination = $upload_dir . $new_filename;
// Przenosimy plik z lokalizacji tymczasowej
if (move_uploaded_file($file['tmp_name'], $destination)) {
$success = true;
$uploaded_file_path = $destination;
// Możesz zapisać ścieżkę do bazy danych
// UPDATE users SET avatar = ? WHERE id = ?
} else {
$errors[] = "Nie udało się zapisać pliku";
}
}
}
}
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Upload avatara</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.upload-form {
background: #f4f4f4;
padding: 30px;
border-radius: 10px;
}
input[type="file"] {
margin: 20px 0;
padding: 10px;
border: 2px dashed #ddd;
border-radius: 5px;
width: 100%;
cursor: pointer;
}
button {
padding: 12px 30px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #764ba2;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.preview {
margin-top: 20px;
text-align: center;
}
.preview img {
max-width: 300px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<h1>Upload zdjęcia profilowego</h1>
<?php if (!empty($errors)): ?>
<div class="error">
<strong>Błędy:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($success): ?>
<div class="success">
Plik został przesłany pomyślnie!
<div class="preview">
<img src="<?= htmlspecialchars($uploaded_file_path) ?>" alt="Avatar">
</div>
</div>
<?php endif; ?>
<form method="POST" enctype="multipart/form-data" class="upload-form">
<p>Wybierz zdjęcie (max 2MB, JPG/PNG/GIF):</p>
<input type="file" name="avatar" accept="image/*" required>
<button type="submit">Prześlij plik</button>
</form>
<p><small>Informacje o pliku:</small></p>
<?php if (isset($_FILES['avatar'])): ?>
<pre><?php print_r($_FILES['avatar']); ?></pre>
<?php endif; ?>
</body>
</html>
WAŻNE: Formularz uploadu MUSI mieć enctype="multipart/form-data"!
Kody błędów $_FILES
| Kod | Stała | Znaczenie |
|---|---|---|
| 0 | UPLOAD_ERR_OK | Sukces |
| 1 | UPLOAD_ERR_INI_SIZE | Plik większy niż upload_max_filesize w php.ini |
| 2 | UPLOAD_ERR_FORM_SIZE | Plik większy niż MAX_FILE_SIZE w formularzu |
| 3 | UPLOAD_ERR_PARTIAL | Plik został przesłany tylko częściowo |
| 4 | UPLOAD_ERR_NO_FILE | Nie przesłano pliku |
| 6 | UPLOAD_ERR_NO_TMP_DIR | Brak folderu tymczasowego |
| 7 | UPLOAD_ERR_CANT_WRITE | Nie można zapisać pliku na dysku |
| 8 | UPLOAD_ERR_EXTENSION | Rozszerzenie PHP zablokowało upload |
5. $_SESSION – Przechowywanie danych użytkownika
Sesje pozwalają przechowywać dane między różnymi stronami dla konkretnego użytkownika. Informacje dodatkowe w temacie PHP – obsługa cookies i sesje
<?php
// ZAWSZE na początku pliku!
session_start();
// Zapisywanie danych w sesji
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'ProGamer';
$_SESSION['logged_in'] = true;
// Odczytywanie danych z sesji
$user_id = $_SESSION['user_id'] ?? 0;
$username = $_SESSION['username'] ?? 'Gość';
// Sprawdzanie czy klucz istnieje
if (isset($_SESSION['logged_in'])) {
echo "Jesteś zalogowany jako: {$username}";
} else {
echo "Nie jesteś zalogowany";
}
// Usuwanie konkretnego klucza
unset($_SESSION['username']);
// Niszczenie całej sesji
session_destroy();
?>
System logowania z sesją
Plik: logowanie.php
<?php
session_start();
// Jeśli użytkownik już zalogowany - przekieruj
if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
header("Location: panel.php");
exit;
}
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($username)) {
$errors[] = "Username jest wymagany";
}
if (empty($password)) {
$errors[] = "Hasło jest wymagane";
}
if (empty($errors)) {
$stmt = $db->prepare("SELECT id, username, email, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$wynik = $stmt->get_result();
$user = $wynik->fetch_assoc();
$stmt->close();
if ($user && password_verify($password, $user['password'])) {
// Zapisz dane w sesji
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['email'] = $user['email'];
$_SESSION['logged_in'] = true;
$_SESSION['login_time'] = time();
$db->close();
header("Location: panel.php");
exit;
} else {
$errors[] = "Nieprawidłowy username lub hasło";
}
}
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Logowanie</title>
</head>
<body>
<h1>Logowanie</h1>
<?php if (!empty($errors)): ?>
<div style="background:#f8d7da; padding:15px; color:#721c24;">
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="POST">
<label>Username:</label><br>
<input type="text" name="username" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>" required><br><br>
<label>Hasło:</label><br>
<input type="password" name="password" required><br><br>
<button type="submit">Zaloguj</button>
</form>
</body>
</html>
Plik: panel.php
<?php
session_start();
// Sprawdź czy użytkownik zalogowany
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
header("Location: logowanie.php");
exit;
}
$username = htmlspecialchars($_SESSION['username']);
$user_id = $_SESSION['user_id'];
$login_time = $_SESSION['login_time'];
$time_elapsed = time() - $login_time;
$minutes = floor($time_elapsed / 60);
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Panel użytkownika</title>
</head>
<body>
<h1>Witaj, <?= $username ?>!</h1>
<p>Twoje ID: <?= $user_id ?></p>
<p>Zalogowany od: <?= $minutes ?> minut</p>
<p><a href="wyloguj.php">Wyloguj się</a></p>
<h2>Dane sesji:</h2>
<pre><?php print_r($_SESSION); ?></pre>
</body>
</html>
Plik: wyloguj.php
<?php
session_start();
// Usuń wszystkie dane sesji
$_SESSION = [];
// Usuń cookie sesyjne
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time() - 3600, '/');
}
// Zniszcz sesję
session_destroy();
// Przekieruj do logowania
header("Location: logowanie.php");
exit;
?>
6. $_COOKIE – Przechowywanie danych w przeglądarce
Cookies to małe pliki tekstowe przechowywane w przeglądarce użytkownika. Więcej w temacie PHP – obsługa cookies i sesje
Ustawianie cookies
<?php
// setcookie(nazwa, wartość, wygasa, ścieżka, domena, secure, httponly)
// Prosty cookie na 30 dni
setcookie('username', 'ProGamer', time() + (30 * 24 * 60 * 60));
// Bezpieczny cookie
setcookie('session_token', 'abc123xyz', [
'expires' => time() + 3600, // 1 godzina
'path' => '/', // Dostępny w całej domenie
'domain' => '', // Aktualna domena
'secure' => true, // Tylko HTTPS
'httponly' => true, // Niedostępny dla JavaScript
'samesite' => 'Strict' // Ochrona CSRF
]);
?>
UWAGA: setcookie() MUSI być wywołane PRZED jakimkolwiek output (echo, HTML)!
Odczytywanie cookies
<?php
// Sprawdzamy czy cookie istnieje
if (isset($_COOKIE['username'])) {
$username = htmlspecialchars($_COOKIE['username']);
echo "Witaj ponownie, {$username}!";
} else {
echo "Witaj po raz pierwszy!";
}
// Operator null coalescing
$theme = $_COOKIE['theme'] ?? 'light';
echo "Aktywny motyw: {$theme}";
?>
Usuwanie cookies
<?php
// Ustaw datę wygaśnięcia w przeszłości
setcookie('username', '', time() - 3600);
// Lub
setcookie('username', '', 1);
?>
„Zapamiętaj mnie” przy logowaniu
<?php
session_start();
// Sprawdź czy istnieje cookie "remember_me"
if (!isset($_SESSION['logged_in']) && isset($_COOKIE['remember_token'])) {
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$token = $_COOKIE['remember_token'];
// Sprawdź token w bazie (w prawdziwej aplikacji przechowuj hash tokenu!)
$stmt = $db->prepare("SELECT id, username, email FROM users WHERE remember_token = ?");
$stmt->bind_param("s", $token);
$stmt->execute();
$wynik = $stmt->get_result();
$user = $wynik->fetch_assoc();
$stmt->close();
$db->close();
if ($user) {
// Automatyczne logowanie
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['email'] = $user['email'];
$_SESSION['logged_in'] = true;
}
}
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$remember = isset($_POST['remember']) ? true : false;
// Walidacja i sprawdzanie użytkownika...
$stmt = $db->prepare("SELECT id, username, email, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$wynik = $stmt->get_result();
$user = $wynik->fetch_assoc();
$stmt->close();
if ($user && password_verify($password, $user['password'])) {
// Zapisz w sesji
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['logged_in'] = true;
// Jeśli zaznaczono "Zapamiętaj mnie"
if ($remember) {
$token = bin2hex(random_bytes(32)); // Generuj bezpieczny token
// Zapisz token w bazie
$stmt_token = $db->prepare("UPDATE users SET remember_token = ? WHERE id = ?");
$stmt_token->bind_param("si", $token, $user['id']);
$stmt_token->execute();
$stmt_token->close();
// Ustaw cookie na 30 dni
setcookie('remember_token', $token, [
'expires' => time() + (30 * 24 * 60 * 60),
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
$db->close();
header("Location: panel.php");
exit;
} else {
$errors[] = "Nieprawidłowy username lub hasło";
}
$db->close();
}
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Logowanie</title>
</head>
<body>
<h1>Logowanie</h1>
<form method="POST">
<label>Username:</label><br>
<input type="text" name="username" required><br><br>
<label>Hasło:</label><br>
<input type="password" name="password" required><br><br>
<label>
<input type="checkbox" name="remember">
Zapamiętaj mnie (30 dni)
</label><br><br>
<button type="submit">Zaloguj</button>
</form>
</body>
</html>
7. $_SERVER – Informacje o serwerze i żądaniu
$_SERVER zawiera informacje o środowisku serwera, nagłówkach HTTP i żądaniu.
<?php
// Metoda HTTP
echo $_SERVER['REQUEST_METHOD']; // GET, POST, PUT, DELETE
// URL i ścieżki
echo $_SERVER['REQUEST_URI']; // /profil.php?id=5
echo $_SERVER['PHP_SELF']; // /profil.php
echo $_SERVER['SCRIPT_NAME']; // /profil.php
echo $_SERVER['QUERY_STRING']; // id=5&sort=level
// Informacje o kliencie
echo $_SERVER['REMOTE_ADDR']; // Adres IP użytkownika (np. 192.168.1.1)
echo $_SERVER['HTTP_USER_AGENT']; // Przeglądarka (np. Mozilla/5.0...)
echo $_SERVER['HTTP_REFERER']; // Poprzednia strona (skąd przyszedł)
// Informacje o serwerze
echo $_SERVER['SERVER_NAME']; // localhost
echo $_SERVER['SERVER_ADDR']; // 127.0.0.1
echo $_SERVER['SERVER_PORT']; // 80 lub 443
echo $_SERVER['DOCUMENT_ROOT']; // C:/xampp/htdocs
// HTTPS
echo $_SERVER['HTTPS'] ?? 'off'; // on jeśli HTTPS
// Nagłówki HTTP
echo $_SERVER['HTTP_ACCEPT']; // Akceptowane typy treści
echo $_SERVER['HTTP_ACCEPT_LANGUAGE']; // Preferowany język
?>
Wyświetlanie informacji o żądaniu
<?php
function getClientIP() {
$ip = $_SERVER['REMOTE_ADDR'];
// Sprawdź czy użyto proxy
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
return $ip;
}
function getBrowser() {
$user_agent = $_SERVER['HTTP_USER_AGENT'];
if (strpos($user_agent, 'Firefox') !== false) {
return 'Mozilla Firefox';
} elseif (strpos($user_agent, 'Chrome') !== false) {
return 'Google Chrome';
} elseif (strpos($user_agent, 'Safari') !== false) {
return 'Safari';
} elseif (strpos($user_agent, 'Edge') !== false) {
return 'Microsoft Edge';
} else {
return 'Nieznana przeglądarka';
}
}
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Informacje o żądaniu</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
border: 1px solid #ddd;
text-align: left;
}
th {
background: #667eea;
color: white;
}
tr:hover {
background: #f5f5f5;
}
</style>
</head>
<body>
<h1>Informacje o Twoim żądaniu</h1>
<table>
<tr>
<th>Parametr</th>
<th>Wartość</th>
</tr>
<tr>
<td><strong>Twój adres IP:</strong></td>
<td><?= htmlspecialchars(getClientIP()) ?></td>
</tr>
<tr>
<td><strong>Przeglądarka:</strong></td>
<td><?= htmlspecialchars(getBrowser()) ?></td>
</tr>
<tr>
<td><strong>User Agent:</strong></td>
<td><?= htmlspecialchars($_SERVER['HTTP_USER_AGENT']) ?></td>
</tr>
<tr>
<td><strong>Metoda HTTP:</strong></td>
<td><?= htmlspecialchars($_SERVER['REQUEST_METHOD']) ?></td>
</tr>
<tr>
<td><strong>Request URI:</strong></td>
<td><?= htmlspecialchars($_SERVER['REQUEST_URI']) ?></td>
</tr>
<tr>
<td><strong>Query String:</strong></td>
<td><?= htmlspecialchars($_SERVER['QUERY_STRING'] ?? 'brak') ?></td>
</tr>
<tr>
<td><strong>Protokół:</strong></td>
<td><?= htmlspecialchars($_SERVER['SERVER_PROTOCOL']) ?></td>
</tr>
<tr>
<td><strong>HTTPS:</strong></td>
<td><?= isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'TAK' : 'NIE' ?></td>
</tr>
<tr>
<td><strong>Serwer:</strong></td>
<td><?= htmlspecialchars($_SERVER['SERVER_NAME']) ?></td>
</tr>
<tr>
<td><strong>Port:</strong></td>
<td><?= htmlspecialchars($_SERVER['SERVER_PORT']) ?></td>
</tr>
<tr>
<td><strong>Document Root:</strong></td>
<td><?= htmlspecialchars($_SERVER['DOCUMENT_ROOT']) ?></td>
</tr>
<tr>
<td><strong>Skrypt PHP:</strong></td>
<td><?= htmlspecialchars($_SERVER['SCRIPT_NAME']) ?></td>
</tr>
<tr>
<td><strong>Referer (poprzednia strona):</strong></td>
<td><?= htmlspecialchars($_SERVER['HTTP_REFERER'] ?? 'brak') ?></td>
</tr>
<tr>
<td><strong>Akceptowany język:</strong></td>
<td><?= htmlspecialchars($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'brak') ?></td>
</tr>
</table>
<h2>Wszystkie dane $_SERVER:</h2>
<pre><?php print_r($_SERVER); ?></pre>
</body>
</html>
$_ENV – Zmienne środowiskowe
$_ENV zawiera zmienne środowiskowe systemu operacyjnego. W PHP zwykle używane z plikami .env (przez biblioteki jak vlucas/phpdotenv).
<?php
// Odczytywanie zmiennej środowiskowej
$db_host = $_ENV['DB_HOST'] ?? 'localhost';
$db_user = $_ENV['DB_USER'] ?? 'root';
$db_pass = $_ENV['DB_PASSWORD'] ?? '';
// Lub za pomocą getenv()
$db_name = getenv('DB_NAME');
echo "Host: {$db_host}<br>";
echo "User: {$db_user}<br>";
?>
Przykład pliku .env:
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=secret123
DB_NAME=gaming_hub
APP_ENV=development
9. $GLOBALS – Dostęp do zmiennych globalnych
$GLOBALS to tablica zawierająca wszystkie zmienne globalne.
<?php
$username = "ProGamer";
$level = 50;
function displayInfo() {
// Dostęp do zmiennych globalnych przez $GLOBALS
echo "Username: {$GLOBALS['username']}<br>";
echo "Level: {$GLOBALS['level']}<br>";
}
displayInfo();
// Wyświetl wszystkie zmienne globalne
echo "<pre>";
print_r($GLOBALS);
echo "</pre>";
?>
UWAGA: Używanie $GLOBALS jest niezalecane – lepiej przekazywać zmienne jako parametry funkcji.
TaskQuest 2.1 — avatar gracza
KARTA PRACY
Po Lekcji 19 TaskQuest zapisuje w bazie zadania, XP i serię dni. Gracze różnią się jednak tylko loginem i liczbami. Dziś każdy gracz dostanie avatar: obrazek, który sam wyśle ze swojego komputera. Gracz bez avatara zobaczy kółko z pierwszą literą imienia.
Z tablicami $_GET, $_POST, $_SESSION i $_COOKIE już pracujesz. Wszystkie przenoszą tekst. Plik to coś innego: może mieć kilka megabajtów, może być czymkolwiek, a jego nazwę i typ podaje przeglądarka użytkownika. Dlatego PHP obsługuje pliki osobną tablicą superglobalną $_FILES. Tak jak pozostałe superglobalne, jest dostępna wszędzie, także wewnątrz funkcji, bez przekazywania jej w parametrze.
Lekcja 20 jest w tym kursie pomijana. Pracujesz na projekcie w stanie po Lekcji 19.
Cel lekcji
Nauczyć się:
- budować formularz wysyłający plik (
enctype="multipart/form-data",<input type="file">), - czytać tablicę
$_FILES: nazwę, rozmiar, kod błędu i miejsce, w którym PHP tymczasowo trzyma plik, - rozpoznawać kody błędów przesyłania (
UPLOAD_ERR_...), - sprawdzać plik przed zapisem: kod błędu, rozmiar, rozszerzenie z listy dozwolonych i zawartość (
getimagesize()), - zapisywać plik na serwerze pod własną nazwą (
move_uploaded_file()), a w bazie przechowywać tylko tę nazwę.
Efekt: nowa strona profil.php z formularzem avatara. Po wysłaniu poprawnego obrazka gracz wraca na listę zadań i widzi swój avatar obok powitania. Plik zły, za duży albo niebędący obrazkiem jest odrzucany z czytelnym komunikatem.
Uwaga — co jest, a czego jeszcze nie ma:
- Stary plik avatara zostaje na dysku po zmianie na nowy. To świadome uproszczenie; sprzątanie jest zadaniem końcowym A.
- Nie ma przycisku „Usuń avatar” (zadanie końcowe B) ani avatarów w rankingu (zadanie końcowe C).
- Katalog z avatarami dostanie dodatkowe zabezpieczenie po stronie serwera w lekcji o bezpieczeństwie (22). Dziś chronimy go walidacją w PHP.
Rozgrzewka — 4 pytania z Lekcji 19
- Dlaczego „Zrobione!” jest przyciskiem w formularzu POST, a nie linkiem
oznacz-wykonane.php?id=5? UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ? AND uzytkownik_id = ?zwróciłaffected_rowsrówne 0. Podaj dwie różne przyczyny.- Dlaczego XP gracza zwiększamy zapytaniem
xp = xp + ?, a nie zapisujemy liczby policzonej w PHP? - Po udanym dodaniu zadania
dodaj-zadanie.phpwywołujeheader('Location: index.php'). Co by się stało, gdyby tego przekierowania nie było, a gracz odświeżył stronę?
1. Wracamy do naszego TaskQuest
Co zostaje, co dochodzi, co zmieniamy
| Element | Po Lekcji 19 | Po Lekcji 21 |
|---|---|---|
tabela uzytkownicy (sql/taskquest.sql) | bez avatara | dochodzi kolumna avatar (nazwa pliku albo NULL) |
klasa Uzytkownik | id, login, imię, XP, streak, data aktywności | dochodzi właściwość avatar i metoda pobierzAvatar() |
wczytajUzytkownika() | czyta 6 kolumn | zmiana: czyta też avatar |
includes/funkcje_bazy.php | 10 funkcji | dochodzi zapiszAvatarUzytkownika() |
includes/funkcje_avatara.php | nie ma | nowy plik: rozszerzenieAvatara(), walidujAvatar() |
config/config.php | stałe XP, widoku, bazy | dochodzą 3 stałe avatara |
profil.php | nie ma | nowy plik: formularz avatara |
katalog uploads/avatary/ | nie ma | nowy katalog na pliki avatarów |
index.php | powitanie bez obrazka | zmiana: avatar albo pierwsza litera imienia, link do profilu |
| logowanie, zadania, „Zrobione!”, usuwanie, ranking | działają | zostają bez zmian |
Schemat strony z formularzem znasz z dodaj-zadanie.php (Lekcja 13): logika na górze, tablica $bledy, walidacja w funkcji, zapis w try, komunikat jednorazowy, przekierowanie. profil.php zbudujemy dokładnie tak samo. Nowe jest tylko to, skąd przychodzą dane i co trzeba z nimi zrobić.
Docelowa struktura projektu
taskquest/
├── index.php (zmiana: avatar przy powitaniu, link do profilu)
├── profil.php NOWY
├── dodaj-zadanie.php bez zmian
├── oznacz-wykonane.php bez zmian
├── usun-zadanie.php bez zmian
├── zaloguj.php bez zmian
├── wyloguj.php bez zmian
├── diagnostyka.php (zmiana: 21 ścieżek, sekcja „Avatary”)
├── classes/
│ ├── Zadanie.php bez zmian
│ └── Uzytkownik.php (zmiana: avatar)
├── config/
│ └── config.php (zmiana: stałe avatara)
├── includes/
│ ├── header.php (zmiana: style .avatar)
│ ├── footer.php (zmiana: „TaskQuest 2.1”)
│ ├── funkcje_avatara.php NOWY
│ ├── funkcje_bazy.php (zmiana: wczytajUzytkownika(), zapiszAvatarUzytkownika())
│ └── ... pozostałe bez zmian
├── sql/
│ └── taskquest.sql (zmiana: kolumna avatar)
└── uploads/ NOWY
└── avatary/ NOWY (pusty — pliki wysyłają gracze)
Przygotowanie (zrób to teraz)
- Zrób kopię zapasową katalogu
taskquest/jakotaskquest_lekcja19/(obok, nie w środku). - Uruchom w XAMPP Apache i MySQL.
- Przygotuj w jednym miejscu (np. na pulpicie, w katalogu
pliki_testowe) pięć plików. Będą potrzebne w brudnopisie i w testach:
| Plik | Jak go zdobyć |
|---|---|
kot.png (albo .jpg) | dowolny mały obrazek, poniżej 1 MB, np. zrzut fragmentu ekranu |
duzy.jpg | zdjęcie z telefonu albo tapeta, powyżej 1 MB (sprawdź we właściwościach pliku) |
notatka.png | utwórz plik tekstowy, wpisz w nim kilka słów, zapisz i zmień rozszerzenie z .txt na .png (w Eksploratorze Windows włącz wcześniej „Rozszerzenia nazw plików”) |
pusty.png | utwórz pusty plik tekstowy (nic nie wpisuj) i zmień rozszerzenie na .png |
dokument.pdf | dowolny plik PDF |
2. Problem w aplikacji
Kama chce, żeby koledzy w rankingu i na liście od razu ją rozpoznawali. Chce wstawić swój obrazek, zapisany na dysku jako C:\Users\Kama\Pictures\kot.png.
Formularze, które mamy, przesyłają tylko tekst: login, tytuł, termin, numer zadania. Gdyby wysłać sam napis kot.png, serwer dostałby nazwę pliku, ale nie jego zawartość. Obrazek zostałby na komputerze Kamy.
Aplikacja musi więc:
- odebrać plik z komputera gracza,
- sprawdzić go. Plik mógł się nie dojść. Może być za duży albo może nie być obrazkiem. Może też być skryptem PHP z nazwą udającą obrazek,
- zapisać go na serwerze w katalogu aplikacji,
- zapamiętać w bazie, który plik należy do którego gracza,
- pokazać go na stronie.
Punkt 2 jest najważniejszy. Do tej pory gracz wysyłał tekst, który co najwyżej trafiał do bazy. Teraz wysyła plik, który zostanie zapisany na naszym serwerze. Jeśli przepuścimy plik atak.php, a ktoś otworzy go w przeglądarce, serwer go wykona.
3. Nowy mechanizm: $_FILES
3.1 Formularz z plikiem
Pole wyboru pliku to <input type="file">. Samo pole nie wystarczy. Formularz musi też mieć dwa atrybuty:
<form method="post" action="brudnopis.php" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">Wyślij</button>
</form>
method="post"— pliku nie da się wysłać w adresie strony (GET),enctype="multipart/form-data"— mówi przeglądarce, żeby wysłała dane w częściach: osobno każde pole tekstowe i osobno surową zawartość pliku. Bez tego atrybutu przeglądarka wyśle tylko nazwę wybranego pliku, jako zwykły tekst (sprawdzisz to w Eksperymencie 1).
Atrybut accept=".jpg,.jpeg,.png,.webp" sprawia, że okno wyboru pliku pokaże tylko pliki o tych rozszerzeniach. Tak jak required czy maxlength z Lekcji 13, to wygoda, nie ochrona. Użytkownik zmieni filtr w oknie na „Wszystkie pliki” albo wyśle żądanie bez formularza.
3.2 Co PHP robi z przesłanym plikiem
Kiedy żądanie z plikiem dociera na serwer, PHP zanim uruchomi Twój skrypt:
- zapisuje zawartość pliku w katalogu tymczasowym (w XAMPP np.
C:\xampp\tmp\) pod losową nazwą, np.php3A1F.tmp, - wpisuje informacje o pliku do tablicy
$_FILES, pod nazwą pola z formularza (name="avatar"→$_FILES['avatar']).
$_FILES['avatar'] to tablica asocjacyjna z pięcioma kluczami:
$_FILES['avatar'] = [
'name' => 'kot.png', // nazwa pliku na komputerze użytkownika
'type' => 'image/png', // typ pliku podany przez przeglądarkę
'tmp_name' => 'C:\xampp\tmp\php3A1F.tmp', // gdzie PHP zapisał plik tymczasowo
'error' => 0, // kod błędu przesyłania (0 = bez błędu)
'size' => 48213, // rozmiar w bajtach
];
(Od PHP 8.1 jest jeszcze szósty klucz, full_path. Nie będzie nam potrzebny.)
Najważniejsza zasada: plik tymczasowy istnieje tylko do końca skryptu. Po ostatniej linii PHP go usuwa. Jeśli skrypt nie przeniesie pliku w stałe miejsce, plik przepada.
3.3 Kody błędów
$_FILES['avatar']['error'] mówi, czy przesyłanie się udało. Zamiast liczb używamy stałych PHP:
| Stała | Wartość | Co się stało |
|---|---|---|
UPLOAD_ERR_OK | 0 | plik doszedł cały |
UPLOAD_ERR_INI_SIZE | 1 | plik jest większy niż limit serwera (upload_max_filesize w php.ini) |
UPLOAD_ERR_FORM_SIZE | 2 | plik jest większy niż limit zapisany w formularzu (MAX_FILE_SIZE, nie używamy go) |
UPLOAD_ERR_PARTIAL | 3 | przesyłanie się urwało, doszła tylko część pliku |
UPLOAD_ERR_NO_FILE | 4 | użytkownik nie wybrał pliku |
UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_EXTENSION | 6, 7, 8 | problem po stronie serwera |
Kiedy error jest inny niż UPLOAD_ERR_OK, pozostałe klucze nie mają znaczenia: tmp_name jest pusty, a size wynosi 0.
3.4 Zapis pliku: move_uploaded_file()
move_uploaded_file($_FILES['avatar']['tmp_name'], 'uploads/moj_plik.png');
Funkcja przenosi plik z katalogu tymczasowego w podane miejsce. Zwraca true albo false, na przykład gdy katalog docelowy nie istnieje.
Dlaczego nie rename() czy copy(), które znasz z pracy z plikami? move_uploaded_file() najpierw sprawdza, czy podana ścieżka to naprawdę plik przesłany w tym żądaniu. Dzięki temu nie da się jej podsunąć np. config/config.php i skopiować go do katalogu dostępnego z przeglądarki.
3.5 Któremu kluczowi wierzyć?
| Klucz | Kto go ustala | Wierzymy? |
|---|---|---|
name | przeglądarka użytkownika | nie — może to być dowolny tekst: kot.png.php, index.php, nazwa z polskimi znakami, nazwa taka sama jak u innego gracza |
type | przeglądarka użytkownika | nie — przeglądarka zgaduje typ z rozszerzenia; atakujący wpisze tu, co chce |
size | PHP (policzył bajty) | tak |
error | PHP | tak |
tmp_name | PHP | tak |
Wniosek dla TaskQuest:
- z nazwy pliku bierzemy tylko rozszerzenie, żeby sprawdzić je z listą dozwolonych. Plik zapisujemy pod własną nazwą,
- klucza
typenie używamy wcale. Czy plik to obrazek, sprawdzamy sami, zaglądając do jego zawartości funkcjągetimagesize().
getimagesize($sciezka) otwiera plik i próbuje odczytać nagłówek obrazka. Dla obrazka zwraca tablicę (szerokość, wysokość, typ). Dla pliku, który obrazkiem nie jest, zwraca false:
var_dump(getimagesize('kot.png')); // array(...) — [0] => szerokość, [1] => wysokość
var_dump(getimagesize('notatka.png')); // bool(false) — to tekst, choć ma końcówkę .png
Pusty plik (0 bajtów) to przypadek szczególny. getimagesize() zwraca wtedy false, ale dodatkowo wypisuje Notice: ... Error reading from .... Dlatego pusty plik odrzucimy wcześniej, zanim zajrzymy do zawartości.
3.6 Limity serwera
W php.ini są dwa ustawienia, które decydują, co w ogóle dotrze do skryptu:
upload_max_filesize— największy pojedynczy plik. Większy →error=UPLOAD_ERR_INI_SIZE,post_max_size— największe całe żądanie POST (plik + pozostałe pola). Większe → PHP odrzuca całe żądanie:$_POSTi$_FILESsą puste, choć formularz został wysłany metodą POST.
Drugi przypadek jest podchwytliwy. $_FILES['avatar'] wtedy nie istnieje, więc skrypt, który od razu czyta $_FILES['avatar']['error'], dostanie ostrzeżenie o braku klucza. Odczyt przez ?? null rozwiązuje problem, tak samo jak ?? '' przy polach tekstowych.
W TaskQuest ustawimy własny limit 1 MB, niższy niż limity serwera. Dzięki temu zwykle to nasza walidacja zgłosi „za duży plik” z czytelnym komunikatem, zanim zadziała limit serwera.
Typowe błędy
- brak
enctype="multipart/form-data"—$_FILESjest puste, a w$_POST['avatar']jest sama nazwa pliku, - zapis pod nazwą z
$_FILES['avatar']['name']— dwaj gracze z plikiemavatar.pngnadpisują sobie obrazki (Eksperyment 3), a nazwaatak.phpzamienia avatar w skrypt, - sprawdzanie typu przez
$_FILES['avatar']['type']— ten klucz podaje przeglądarka, - brak
move_uploaded_file()— skrypt „zapisał” avatar w bazie, ale sam plik zniknął razem z plikiem tymczasowym, - odczyt
$_FILES['avatar']['error']bez sprawdzenia, czy$_FILES['avatar']istnieje.
4. SAMODZIELNIE — pierwszy przesłany plik
Utwórz w katalogu taskquest/ plik brudnopis.php. Napisz w nim:
- formularz wysyłający jeden plik z pola o nazwie
avatardo tego samego pliku (brudnopis.php), - nad formularzem kod PHP, który tylko po wysłaniu formularza wypisze całą zawartość
$_FILESw czytelnej postaci.
Wyślij kot.png. Potem wyślij formularz bez wybierania pliku. Porównaj oba wyniki z tabelą kodów błędów.
HINT
Jak w dodaj-zadanie.php sprawdzasz, że formularz został wysłany? Do czytelnego wypisania tablicy użyj funkcji, którą znasz z pracy z tablicami (wynik wstaw między znaczniki <pre>).
POMOC
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
echo '<pre>';
// wypisz $_FILES
echo '</pre>';
}
?>
<form method="post" action="brudnopis.php" enctype="...">
<!-- pole pliku o nazwie avatar i przycisk -->
</form>
ROZWIĄZANIE
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
echo '<pre>';
print_r($_FILES);
echo '</pre>';
}
?>
<form method="post" action="brudnopis.php" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">Wyślij</button>
</form>
Oczekiwane po wysłaniu kot.png: tablica z kluczem avatar, w nim name = kot.png, type = image/png, tmp_name = ścieżka w katalogu tymczasowym XAMPP, error = 0, size = rozmiar pliku w bajtach (porównaj z właściwościami pliku w Windows).
Oczekiwane bez pliku: name pusty, tmp_name pusty, error = 4 (UPLOAD_ERR_NO_FILE), size = 0. Klucz avatar istnieje, bo pole było w formularzu.
Druga część — zapis. Utwórz obok brudnopisu katalog test_upload/. Pod linią z print_r() dopisz przeniesienie pliku do test_upload/ pod nazwą proba.png i wypisz wynik funkcji (var_dump()). Wyślij kot.png i zajrzyj do katalogu.
var_dump(move_uploaded_file($_FILES['avatar']['tmp_name'], __DIR__ . '/test_upload/proba.png'));
Oczekiwane: bool(true) i plik proba.png w test_upload/. Otwórz w przeglądarce http://localhost/taskquest/test_upload/proba.png. Widzisz obrazek, więc wszystko, co trafi do tego katalogu, jest dostępne z przeglądarki. Zapamiętaj to, wrócimy do tego w Eksperymencie 2.
Wyślij teraz notatka.png. Znów bool(true): move_uploaded_file() przenosi każdy plik. Sprawdzanie, co to za plik, to nasze zadanie.
Wyślij formularz bez pliku. Wynik: bool(false), bo nie ma czego przenosić.
Trzecia część — limity Twojego serwera. Dopisz na końcu części PHP (poza warunkiem POST):
echo 'upload_max_filesize: ' . ini_get('upload_max_filesize') . '<br>';
echo 'post_max_size: ' . ini_get('post_max_size') . '<br>';
ini_get() odczytuje ustawienie z php.ini. Zapisz oba wyniki. Porównaj je z limitem 1 MB, który ustawimy w TaskQuest, i odpowiedz sobie: który limit zadziała pierwszy przy pliku 3 MB?
Katalog test_upload/ usuń po skończeniu. Brudnopis zostaw, przyda się w kroku 4.
5. Włączamy avatar do TaskQuest
Od tego miejsca pracujesz na projekcie.
brudnopis.phpzostaje tylko do testowania funkcji.
Plan: najpierw miejsce na dane (baza, klasa, stałe, katalog — kroki 1–3), potem funkcje (kroki 4–5), potem strona profil.php (krok 6) i diagnostyka (krok 7). Wyświetlenie avatara na stronie głównej to zadanie z sekcji 6. Po każdym kroku aplikacja działa.
Krok 1 — kolumna avatar w bazie
Otwórz sql/taskquest.sql. W CREATE TABLE uzytkownicy, pod linią data_ostatniej_aktywnosci DATE NULL, dopisz nową kolumnę. Nie zapomnij postawić przecinka na końcu linii data_ostatniej_aktywnosci, bo teraz nie jest już ostatnia:
data_ostatniej_aktywnosci DATE NULL, -- NULL = brak aktywności
avatar VARCHAR(100) NULL -- (L21) nazwa pliku w uploads/avatary; NULL = brak avatara
Decyzje, które za tym stoją:
- W bazie trzymamy samą nazwę pliku (np.
avatar_1_1790000000.png), a nie ścieżkęuploads/avatary/...ani obrazek. Katalog zapiszemy raz, w stałej wconfig.php. Jeśli kiedyś go przeniesiemy, zmienimy jedną linię kodu, a nie wszystkie wiersze w bazie. Samego obrazka nie trzymamy w bazie, bo pliki przechowuje system plików, a baza przechowuje informację, który plik do kogo należy. NULL= gracz nie ma avatara. To ta sama zasada co przydata_wykonaniaidata_ostatniej_aktywnosci.VARCHAR(100)wystarczy z zapasem, bo nazwę pliku tworzymy sami i ma około 30 znaków.
Sprawdź w tym samym pliku polecenie INSERT INTO uzytkownicy. Jeśli wymienia kolumny w nawiasie (INSERT INTO uzytkownicy (login, imie, ...) VALUES ...), nie zmieniasz nic: kolumna, której nie wymieniono, dostanie NULL. Jeśli INSERT nie ma listy kolumn, dopisz na końcu każdego wiersza wartości , NULL.
Uruchom cały skrypt w phpMyAdmin (baza taskquest → SQL). Zajrzyj do tabeli uzytkownicy: Kama i Ola mają w kolumnie avatar wartość NULL.
Odśwież TaskQuest. Działa jak po Lekcji 19. Nowa kolumna niczego nie psuje, bo wczytajUzytkownika() wymienia kolumny w SELECT (bez *), więc jej nie widzi.
Krok 2 — SAMODZIELNIE: Uzytkownik zna swój avatar
2a. Klasa. Otwórz classes/Uzytkownik.php. Dodaj właściwość avatar według zasad z Lekcji 15:
- pod właściwością
dataOstatniejAktywnoscinowa właściwość prywatnaavatar. Jaki typ, skoro kolumna może byćNULL? (Podpowiedź: tak samo jakdataOstatniejAktywnosciod Lekcji 18.) - konstruktor: nowy ostatni parametr
$avatartego samego typu i linia, która przepisuje go do właściwości, - pod metodą
pobierzStreak()getterpobierzAvatar().
2b. Funkcja. W includes/funkcje_bazy.php, w funkcji wczytajUzytkownika():
- dopisz kolumnę
avatarna końcu listy wSELECT, - dopisz argument nazwany
avatar:na końcu wywołanianew Uzytkownik(...).
Sprawdzenie: w brudnopisie (pod całym dotychczasowym kodem albo w nowym pliku brudnopis2.php):
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
$polaczenie = polaczZBaza();
var_dump(wczytajUzytkownika($polaczenie, 1)->pobierzAvatar());
Oczekiwane: NULL. W phpMyAdmin wpisz Kamie w kolumnę avatar tekst test.png i odśwież brudnopis: string(8) "test.png". Przywróć NULL (w phpMyAdmin zaznacz pole „Null” przy edycji wiersza).
HINT
Typ, który dopuszcza tekst albo null, zapisujesz ze znakiem zapytania przed nazwą typu. Getter zwraca dokładnie ten typ. Jeśli po zmianie klasy strona pokazuje błąd ArgumentCountError albo Unknown named parameter, porównaj nazwę parametru w konstruktorze z nazwą argumentu w wczytajUzytkownika().
ROZWIĄZANIE
W klasie Uzytkownik:
private ?string $dataOstatniejAktywnosci;
private ?string $avatar;
Konstruktor (pozostałe linie ciała bez zmian):
public function __construct(
int $id,
string $login,
string $imieUzytkownika,
int $xp,
int $streak,
?string $dataOstatniejAktywnosci,
?string $avatar
) {
// ... dotychczasowe przypisania ...
$this->avatar = $avatar;
}
Jeśli Twój konstruktor ma parametry w jednej linii, dopisz ?string $avatar na jej końcu po przecinku. Zapis w wielu liniach to tylko kwestia czytelności.
Getter pod pobierzStreak():
public function pobierzAvatar(): ?string
{
return $this->avatar;
}
W wczytajUzytkownika():
$zapytanie = $polaczenie->prepare(
'SELECT id, login, imie, xp, streak, data_ostatniej_aktywnosci, avatar FROM uzytkownicy WHERE id = ?'
);
oraz na końcu tworzenia obiektu:
dataOstatniejAktywnosci: $wiersz['data_ostatniej_aktywnosci'],
avatar: $wiersz['avatar'],
);
Nie ma settera ustawAvatar(). Avatar zmienia się w bazie, a obiekt żyje tylko przez jedno żądanie (Lekcja 15). Po zmianie avatara strona i tak zostanie wczytana od nowa.
Krok 3 — stałe i katalog
3a. Katalog. W katalogu taskquest/ utwórz katalog uploads, a w nim avatary. Aplikacja nie tworzy go sama. Tak jak sql/, classes/ i config/, jest częścią struktury projektu, a jego obecność sprawdzi diagnostyka.php (krok 7).
3b. Stałe. Na końcu config/config.php dopisz:
// (L21) avatary graczy
define('KATALOG_AVATAROW', 'uploads/avatary'); // względem katalogu projektu
define('MAKS_ROZMIAR_AVATARA', 1024 * 1024); // 1 MB w bajtach
define('DOZWOLONE_ROZSZERZENIA_AVATARA', ['jpg', 'jpeg', 'png', 'webp']);
KATALOG_AVATAROWjest ścieżką względną, bez ukośnika na końcu. Użyjemy jej w dwóch miejscach. Przy zapisie pliku doklejamy przed nią katalog projektu na dysku (__DIR__ . '/' . KATALOG_AVATAROW). W<img src="...">używamy jej tak, jak jest: przeglądarka liczy ją względem adresu strony, a wszystkie strony TaskQuest leżą w katalogu głównym projektu.1024 * 1024: 1 KB to 1024 bajty, 1 MB to 1024 KB. Zapis mnożenia jest czytelniejszy niż1048576.- Stała może przechowywać tablicę. Lista dozwolonych rozszerzeń jest w jednym miejscu. Dopisanie
gifto zmiana jednej linii.
Rozszerzenia piszemy małymi literami. Za chwilę zobaczysz dlaczego.
Krok 4 — SAMODZIELNIE: funkcje avatara
Utwórz plik includes/funkcje_avatara.php (bez zamykającego ?>). Napiszemy w nim dwie funkcje.
4a. rozszerzenieAvatara() — gotowa. Wklej:
<?php
// Funkcje TaskQuest do obsługi avatarów (Lekcja 21).
// Wymagają wcześniejszego dołączenia config/config.php (stałe ..._AVATARA).
function rozszerzenieAvatara(string $nazwaPliku): string
{
return strtolower(pathinfo($nazwaPliku, PATHINFO_EXTENSION));
}
pathinfo() znasz z listaPlikowWKatalogu() (Lekcja 10). Z drugim argumentem PATHINFO_EXTENSION zwraca samo rozszerzenie: dla kot.png → png, dla kot.png.php → php (liczy się ostatnia kropka), dla kot → pusty tekst. strtolower() zamienia litery na małe: plik KOT.PNG z aparatu też ma przejść.
4b. walidujAvatar() — napisz sama/sam. Nagłówek:
function walidujAvatar(array $plik): array
Funkcja dostaje jedną tablicę z $_FILES (np. $_FILES['avatar']) i zwraca tablicę komunikatów. [] oznacza, że plik jest poprawny. Tak samo działa walidujNoweZadanie(): nie rzuca wyjątków, niczego nie wyświetla, a komunikaty nie zawierają danych od użytkownika (nie powtarzamy nazwy pliku).
Reguły i komunikaty, w tej kolejności:
| Sytuacja | Komunikat | Co dalej |
|---|---|---|
error = brak pliku | Wybierz plik z obrazkiem. | zwróć od razu |
error = przekroczony limit serwera albo formularza | Plik może mieć najwyżej 1 MB. | zwróć od razu |
error = każdy inny błąd | Nie udało się przesłać pliku. Spróbuj ponownie. | zwróć od razu |
size = 0 | Wybrany plik jest pusty. | zwróć od razu |
size większy niż MAKS_ROZMIAR_AVATARA | Plik może mieć najwyżej 1 MB. | sprawdzaj dalej |
rozszerzenie spoza DOZWOLONE_ROZSZERZENIA_AVATARA | Dozwolone formaty: JPG, PNG, WEBP. | — |
| rozszerzenie dozwolone, ale zawartość to nie obrazek | Wybrany plik nie jest obrazkiem. | — |
Dlaczego czasem „zwróć od razu”? Kiedy przesyłanie się nie udało, nie ma pliku, więc nie ma czego sprawdzać dalej (tmp_name jest pusty). Pusty plik też kończy sprawdzanie, bo getimagesize() wypisałaby dla niego Notice (sekcja 3.5). Rozmiar i rozszerzenie to dwa niezależne problemy, więc pokazujemy oba naraz, jak w formularzu zadania. Zawartość sprawdzamy tylko przy dozwolonym rozszerzeniu: plik dokument.pdf ma dostać jeden komunikat o formacie, a nie dwa.
HINT
- Porównuj
$plik['error']ze stałymiUPLOAD_ERR_...z sekcji 3.3, nie z liczbami. - Do sprawdzenia, czy wartość jest w tablicy, służy
in_array($wartosc, $tablica, true). Trzeci argumenttruewłącza porównanie ścisłe (===), tak jak zawsze w tym kursie. - Ostatnie dwie reguły to jedna konstrukcja
if/elseif.
POMOC
function walidujAvatar(array $plik): array
{
$bledy = [];
if ($plik['error'] === UPLOAD_ERR_NO_FILE) {
$bledy[] = 'Wybierz plik z obrazkiem.';
return $bledy;
}
// ... dwa kolejne warunki z kodem błędu ...
// ... pusty plik ...
if ($plik['size'] > MAKS_ROZMIAR_AVATARA) {
// ...
}
if (!in_array(rozszerzenieAvatara($plik['name']), DOZWOLONE_ROZSZERZENIA_AVATARA, true)) {
// ...
} elseif (getimagesize($plik['tmp_name']) === false) {
// ...
}
return $bledy;
}
ROZWIĄZANIE
function walidujAvatar(array $plik): array
{
$bledy = [];
if ($plik['error'] === UPLOAD_ERR_NO_FILE) {
$bledy[] = 'Wybierz plik z obrazkiem.';
return $bledy;
}
if ($plik['error'] === UPLOAD_ERR_INI_SIZE || $plik['error'] === UPLOAD_ERR_FORM_SIZE) {
$bledy[] = 'Plik może mieć najwyżej 1 MB.';
return $bledy;
}
if ($plik['error'] !== UPLOAD_ERR_OK) {
$bledy[] = 'Nie udało się przesłać pliku. Spróbuj ponownie.';
return $bledy;
}
if ($plik['size'] === 0) {
$bledy[] = 'Wybrany plik jest pusty.';
return $bledy;
}
if ($plik['size'] > MAKS_ROZMIAR_AVATARA) {
$bledy[] = 'Plik może mieć najwyżej 1 MB.';
}
if (!in_array(rozszerzenieAvatara($plik['name']), DOZWOLONE_ROZSZERZENIA_AVATARA, true)) {
$bledy[] = 'Dozwolone formaty: JPG, PNG, WEBP.';
} elseif (getimagesize($plik['tmp_name']) === false) {
$bledy[] = 'Wybrany plik nie jest obrazkiem.';
}
return $bledy;
}
Przy błędzie limitu serwera (UPLOAD_ERR_INI_SIZE) piszemy „1 MB”, choć limit serwera jest wyższy. Użytkownika interesuje jedna zasada TaskQuest, a nie ustawienia php.ini.
4c. Test funkcji w brudnopisie. Funkcja przyjmuje zwykłą tablicę, więc możesz ją przetestować bez formularza, podając tablice ułożone jak $_FILES. Skopiuj kot.png i notatka.png do katalogu taskquest/, a w nowym brudnopis2.php (albo pod kodem z kroku 2) napisz:
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_avatara.php';
$przypadki = [
['name' => 'kot.png', 'tmp_name' => __DIR__ . '/kot.png', 'error' => UPLOAD_ERR_OK, 'size' => 5000],
// ... dopisz kolejne przypadki z tabeli poniżej ...
];
foreach ($przypadki as $plik) {
echo $plik['name'] . ': ';
print_r(walidujAvatar($plik));
echo '<br>';
}
Klucza type nie podajemy, bo funkcja go nie czyta. Rozmiar size wpisujemy sami, żeby sprawdzić granice limitu.
Przewidź wynik, zanim uruchomisz:
name | tmp_name | error | size | Oczekiwane |
|---|---|---|---|---|
kot.png | kot.png | UPLOAD_ERR_OK | 5000 | [] |
KOT.PNG | kot.png | UPLOAD_ERR_OK | 5000 | [] |
kot | kot.png | UPLOAD_ERR_OK | 5000 | format |
kot.gif | kot.png | UPLOAD_ERR_OK | 5000 | format |
kot.png.php | kot.png | UPLOAD_ERR_OK | 5000 | format |
notatka.png | notatka.png | UPLOAD_ERR_OK | 20 | „nie jest obrazkiem” |
atak.php.png | notatka.png | UPLOAD_ERR_OK | 20 | „nie jest obrazkiem” |
kot.png | kot.png | UPLOAD_ERR_OK | 1048576 | [] (dokładnie 1 MB — jeszcze wolno) |
kot.png | kot.png | UPLOAD_ERR_OK | 1048577 | rozmiar |
duzy.gif | kot.png | UPLOAD_ERR_OK | 2000000 | rozmiar i format (dwa komunikaty) |
pusty.png | kot.png | UPLOAD_ERR_OK | 0 | „pusty” |
| (pusty tekst) | (pusty tekst) | UPLOAD_ERR_NO_FILE | 0 | „Wybierz plik” |
x.png | (pusty tekst) | UPLOAD_ERR_INI_SIZE | 0 | rozmiar |
x.png | (pusty tekst) | UPLOAD_ERR_PARTIAL | 0 | „Nie udało się przesłać” |
Zwróć uwagę na atak.php.png: rozszerzenie png przechodzi, ale zawartość (tekst) zatrzymuje getimagesize().
Krok 5 — SAMODZIELNIE: zapiszAvatarUzytkownika()
W includes/funkcje_bazy.php, na końcu pliku, napisz funkcję:
function zapiszAvatarUzytkownika(mysqli $polaczenie, int $uzytkownikId, string $nazwaPlikuAvatara): void
Ma wpisać nazwę pliku w kolumnę avatar wiersza gracza o podanym id. Zasady jak w Lekcji 19: prepared statement, niczego nie wyświetla, wyjątków nie łapie. Popraw komentarz na górze pliku: „(Lekcje 18–21)”.
HINT
To UPDATE jednego wiersza tabeli uzytkownicy, podobne do zapiszPostepUzytkownika(), tylko prostsze. Jaki warunek w WHERE sprawia, że zmieni się dokładnie jeden gracz?
ROZWIĄZANIE
function zapiszAvatarUzytkownika(mysqli $polaczenie, int $uzytkownikId, string $nazwaPlikuAvatara): void
{
$zapytanie = $polaczenie->prepare('UPDATE uzytkownicy SET avatar = ? WHERE id = ?');
$zapytanie->bind_param('si', $nazwaPlikuAvatara, $uzytkownikId);
$zapytanie->execute();
}
Funkcja nie sprawdza affected_rows. id pochodzi z sesji (zaufane), a nazwa pliku jest za każdym razem inna, więc zmiana zawsze obejmie jeden wiersz. Dlaczego „za każdym razem inna”, zobaczysz w kroku 6c.
Sprawdzenie w brudnopisie: zapiszAvatarUzytkownika($polaczenie, 2, 'test.png'); → w phpMyAdmin Ola ma test.png. Uruchom ponownie sql/taskquest.sql.
Krok 6 — strona profil.php
Budujemy ją według schematu strony z formularzem (Lekcja 13), który znasz z dodaj-zadanie.php. Cały plik znajdziesz w sekcji 9. Zajrzyj tam dopiero po skończeniu.
6a. Szkielet strony
Utwórz profil.php. Na górze:
session_start();,require_onceplików:config/config.php,includes/funkcje_avatara.php,includes/funkcje_sesji.php,includes/funkcje_bazy.php,wymagajLogowania();,- wartości początkowe:
$bledy = [];i$bladZapisu = null;, - pusty na razie warunek
if ($_SERVER['REQUEST_METHOD'] === 'POST') { }, $nazwaStrony = 'Profil — TaskQuest';i dołączenieheader.php.
Pod częścią PHP część HTML:
?>
<h2>Twój profil</h2>
<p><a href="index.php">← Wróć do listy zadań</a></p>
<?php if ($bledy !== []) { ?>
<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ę zapisać avatara: <?= $bladZapisu; ?></p>
<?php } ?>
<form method="post" action="profil.php" enctype="multipart/form-data">
<label for="avatar">Avatar (JPG, PNG lub WEBP, najwyżej 1 MB)</label>
<input type="file" id="avatar" name="avatar" accept=".jpg,.jpeg,.png,.webp" required>
<button type="submit">Zapisz avatar</button>
</form>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
Czym ten formularz różni się od formularza zadania?
- ma
enctype="multipart/form-data", - pole pliku nie ma
value, więc po błędzie formularz nie zachowuje wybranego pliku. To nie przeoczenie: przeglądarka nie pozwala stronie wskazać pliku z dysku użytkownika. Inaczej każda strona mogłaby „wybrać” za Ciebie dowolny plik i go wysłać. Po błędzie gracz wybiera plik jeszcze raz.
Link do profilu. W index.php, bezpośrednio pod akapitem „Zalogowano jako …” (nad formularzem „Wyloguj”), dopisz:
<p><a href="profil.php">Twój profil — zmień avatar</a></p>
Sprawdzenie: z listy zadań wejdź w profil. Widzisz formularz. Wysłanie niczego jeszcze nie robi. Wyloguj się i wpisz w pasku adresu profil.php: przekierowanie na logowanie.
6b. SAMODZIELNIE: odbiór i walidacja pliku
Wewnątrz warunku POST:
- odczytaj z
$_FILEStablicę polaavatardo zmiennej$plik. Pamiętaj o sytuacji z sekcji 3.6: przy przekroczeniupost_max_sizeten klucz nie istnieje, - jeśli
$pliknie istnieje, dopisz do$bledykomunikatPlik jest za duży albo nie został wysłany., - w przeciwnym razie
$bledyto wynikwalidujAvatar().
Sprawdzenie: wyślij dokument.pdf. Oczekiwane: ramka „Popraw formularz:” z komunikatem o formacie.
HINT
Odczyt z tablicy, gdy klucza może nie być, robisz tak samo jak przy $_POST, tylko wartością zastępczą nie jest pusty tekst. Czego użyjesz, skoro w $plik ma być tablica albo „nic”?
ROZWIĄZANIE
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$plik = $_FILES['avatar'] ?? null;
if ($plik === null) {
$bledy[] = 'Plik jest za duży albo nie został wysłany.';
} else {
$bledy = walidujAvatar($plik);
}
}
6c. SAMODZIELNIE: zapis pliku i bazy
Wciąż wewnątrz warunku POST, pod kodem z 6b, gdy $bledy jest puste:
- utwórz nazwę pliku w zmiennej
$nazwaPlikuAvatarawedług wzoruavatar_<id gracza>_<aktualny czas>.<rozszerzenie>, np.avatar_1_1790000000.png. Id gracza jest w sesji, aktualny czas w sekundach zwraca funkcja znana z ciasteczek (Lekcja 14), rozszerzenie zwraca Twoja funkcja z kroku 4, - w zmiennej
$sciezkaAvatarazbuduj pełną ścieżkę na dysku: katalog tego pliku +/+KATALOG_AVATAROW+/+ nazwa, - przenieś plik tymczasowy pod tę ścieżkę. Jeśli się nie uda, ustaw
$bladZapisunaNie udało się zapisać pliku na serwerze., - jeśli się uda: w
trypołącz się z bazą, zapisz nazwę pliku funkcją z kroku 5, ustaw komunikatAvatar został zmieniony.i przekieruj naindex.php. Wcatchwpisz komunikat wyjątku do$bladZapisu, jak wdodaj-zadanie.php.
Dlaczego taka nazwa pliku?
- id gracza — dwaj gracze nigdy nie dostaną tej samej nazwy,
- czas — każdy nowy avatar tego samego gracza ma inną nazwę. Przeglądarka pamięta obrazki (pamięć podręczna). Gdyby nowy avatar Kamy nazywał się jak stary, mogłaby dalej widzieć stary,
- rozszerzenie sprawdzone przez walidację — plik na pewno kończy się na
.jpg,.jpeg,.pngalbo.webp, - nic z nazwy od użytkownika poza rozszerzeniem — żadnych spacji, polskich znaków,
.phpw środku ani cudzych nazw.
HINT
Kolejność ma znaczenie. Najpierw plik, potem baza. Odwrotnie baza wskazywałaby plik, który może się nie zapisać. Warunek „gdy $bledy jest puste” już znasz z zaloguj.php. Wynik move_uploaded_file() sprawdzasz w if, tak jak wynik każdej funkcji zwracającej bool.
POMOC
if ($bledy === []) {
$nazwaPlikuAvatara = 'avatar_' . ... . '_' . ... . '.' . ...;
$sciezkaAvatara = __DIR__ . '/' . KATALOG_AVATAROW . '/' . $nazwaPlikuAvatara;
if (!move_uploaded_file(..., ...)) {
$bladZapisu = 'Nie udało się zapisać pliku na serwerze.';
} else {
try {
// połączenie, zapis w bazie, komunikat, przekierowanie
} catch (Exception $wyjatek) {
$bladZapisu = $wyjatek->getMessage();
}
}
}
ROZWIĄZANIE
if ($bledy === []) {
$nazwaPlikuAvatara = 'avatar_' . $_SESSION['uzytkownik_id'] . '_' . time() . '.' . rozszerzenieAvatara($plik['name']);
$sciezkaAvatara = __DIR__ . '/' . KATALOG_AVATAROW . '/' . $nazwaPlikuAvatara;
if (!move_uploaded_file($plik['tmp_name'], $sciezkaAvatara)) {
$bladZapisu = 'Nie udało się zapisać pliku na serwerze.';
} else {
try {
$polaczenie = polaczZBaza();
zapiszAvatarUzytkownika($polaczenie, $_SESSION['uzytkownik_id'], $nazwaPlikuAvatara);
ustawKomunikat('Avatar został zmieniony.');
header('Location: index.php');
exit;
} catch (Exception $wyjatek) {
$bladZapisu = $wyjatek->getMessage();
}
}
}
Nieudane move_uploaded_file() nie jest wyjątkiem, tylko wynikiem false. Sprawdzamy go więc warunkiem i nie rzucamy wyjątku po to, żeby go zaraz złapać w tym samym miejscu.
Przy awarii bazy plik zostaje w uploads/avatary/, ale baza go nie wskazuje. Powstaje „sierota” na dysku. Przy jednym graczu i rzadkich awariach to akceptowalne. Zobaczysz to w Teście 9.
6d. Sprawdzenie
Zaloguj się jako Kama, wejdź w profil, wyślij kot.png. Oczekiwane: powrót na listę zadań, komunikat „Avatar został zmieniony.”, w uploads/avatary/ plik avatar_1_<liczba>.png, w phpMyAdmin Kama ma w avatar tę samą nazwę. Samego avatara na stronie jeszcze nie widać. To następne zadanie.
Krok 7 — diagnostyka.php i stopka
7a. W diagnostyka.php dopisz do $wymaganePliki dwie ścieżki: 'profil.php' i 'includes/funkcje_avatara.php' (razem 21).
7b. Pod sekcją <h2>Baza danych</h2> dopisz sekcję <h2>Avatary</h2>. W stylu pozostałych sekcji wypisz jeden z trzech wyników:
[BRAK] Katalog uploads/avatary nie istnieje — utwórz go w katalogu projektu., gdyis_dir()zwracafalse,[BRAK] Do katalogu uploads/avatary nie można zapisywać plików., gdy katalog istnieje, aleis_writable()zwracafalse,[OK] Katalog uploads/avatary istnieje i można do niego zapisywać.w pozostałych przypadkach.
diagnostyka.php celowo nie dołącza config.php (Lekcja 10), więc ścieżkę wpisz wprost, tak jak w $wymaganePliki, doklejając ją do __DIR__ . '/'.
<h2>Avatary</h2>
<?php if (!is_dir(__DIR__ . '/uploads/avatary')) { ?>
<p>[BRAK] Katalog uploads/avatary nie istnieje — utwórz go w katalogu projektu.</p>
<?php } elseif (!is_writable(__DIR__ . '/uploads/avatary')) { ?>
<p>[BRAK] Do katalogu uploads/avatary nie można zapisywać plików.</p>
<?php } else { ?>
<p>[OK] Katalog uploads/avatary istnieje i można do niego zapisywać.</p>
<?php } ?>
Na Windows w XAMPP katalog praktycznie zawsze będzie zapisywalny. Na serwerze z Linuksem brak uprawnień do zapisu to najczęstsza przyczyna, dla której upload „działa u mnie, a nie na serwerze”.
7c. W includes/footer.php zmień wersję na TaskQuest 2.1 (Lekcja 20 jest pominięta, więc numer 2.0 też).
6. Kolejne zadanie — avatar na stronie głównej
Avatar jest zapisany, ale nikt go nie widzi. Dodaj go do index.php.
Wymagania:
- Obrazek stoi bezpośrednio nad zdaniem powitalnym „Witaj, …!” (tym, w którym jest
$uzytkownik->pobierzImie()). - Gdy gracz ma avatar:
<img>z klasąavatar, adresemKATALOG_AVATAROW+/+ nazwa pliku z obiektu i tekstem alternatywnymAvatar gracza. - Gdy gracz nie ma avatara:
<span>z klasamiavatariavatar-litera, a w środku pierwsza litera imienia. Imię może zaczynać się polską literą („Łucja”), więc pamiętaj, które funkcje tekstowe znają polskie znaki (Lekcja 8). - Użyj konstrukcji
<?php if (...) { ?> ... <?php } else { ?> ... <?php } ?>, tak jak w pozostałej części prezentacji.
Style dopisz w includes/header.php, w <style>, pod regułą form.akcja:
.avatar { display: inline-block; width: 64px; height: 64px; border-radius: 50%; object-fit: cover; vertical-align: middle; }
.avatar-litera { background: #5b6ee1; color: #fff; font-size: 32px; font-weight: bold; line-height: 64px; text-align: center; }
border-radius: 50% robi z kwadratu koło. object-fit: cover przycina zdjęcie do kwadratu, zamiast je rozciągać.
HINT
Gracz „nie ma avatara”, gdy getter zwraca null. Pierwszą literę wytniesz funkcją, której używa skrocOpis().
POMOC
<?php if ($uzytkownik->pobierzAvatar() !== null) { ?>
<img class="avatar" src="..." alt="Avatar gracza">
<?php } else { ?>
<span class="avatar avatar-litera"><?= ...; ?></span>
<?php } ?>
ROZWIĄZANIE
<?php if ($uzytkownik->pobierzAvatar() !== null) { ?>
<img class="avatar" src="<?= KATALOG_AVATAROW . '/' . $uzytkownik->pobierzAvatar(); ?>" alt="Avatar gracza">
<?php } else { ?>
<span class="avatar avatar-litera"><?= mb_substr($uzytkownik->pobierzImie(), 0, 1); ?></span>
<?php } ?>
substr($imie, 0, 1) dla „Łucja” wyciąga pierwszy bajt, a „Ł” w UTF-8 zajmuje dwa bajty. Na stronie pojawiłby się znak „�” (sprawdź w Teście 1). mb_substr() liczy znaki.
index.php nie potrzebuje nowych require_once: KATALOG_AVATAROW jest w config.php, który strona już dołącza.
Sprawdzenie: Kama (z avatarem z kroku 6d) widzi swój obrazek w kółku. Ola widzi fioletowe kółko z literą „O”.
7. Testy
Przed testami uruchom ponownie sql/taskquest.sql i opróżnij uploads/avatary/ (sam katalog zostaje).
Test 1 — regresja i litera
Zaloguj się jako Kama, potem jako Ola. Oczekiwane: kółka z literami „K” i „O”, reszta strony jak po Lekcji 19 (zadania, „Zrobione!”, „Usuń”, ranking).
W phpMyAdmin zmień imię Oli na Łucja i odśwież. Oczekiwane: „Ł”. Tymczasowo zamień w index.php mb_substr na substr i odśwież: zamiast litery pojawia się �. Przywróć mb_substr i imię Ola.
Test 2 — pierwszy avatar
Jako Kama wyślij kot.png. Oczekiwane: komunikat „Avatar został zmieniony.”, obrazek w kółku, w uploads/avatary/ jeden plik avatar_1_<liczba>.png, w bazie ta sama nazwa. Kliknij prawym przyciskiem na avatar → „Otwórz grafikę w nowej karcie”: adres kończy się na uploads/avatary/avatar_1_....png.
Test 3 — zmiana avatara
Jako Kama wyślij inny poprawny obrazek (np. kot.png zapisany jako .jpg albo inny zrzut ekranu). Oczekiwane: nowy obrazek od razu, bez odświeżania z czyszczeniem pamięci. W uploads/avatary/ są teraz dwa pliki Kamy, a baza wskazuje nowszy (ograniczenie z początku lekcji; zadanie końcowe A).
Test 4 — walidacja przez formularz
Dla każdego pliku przewidź komunikat, potem sprawdź. Po każdym błędzie avatar i baza bez zmian.
| Plik | Oczekiwany komunikat |
|---|---|
duzy.jpg | Plik może mieć najwyżej 1 MB. |
dokument.pdf | Dozwolone formaty: JPG, PNG, WEBP. |
notatka.png | Wybrany plik nie jest obrazkiem. |
pusty.png | Wybrany plik jest pusty. |
kot.png przemianowany na KOT.PNG | sukces |
Przy dokument.pdf zmień w oknie wyboru pliku filtr na „Wszystkie pliki” (accept tylko podpowiada).
Po błędzie pole pliku jest puste. To oczekiwane zachowanie (krok 6a).
Test 5 — formularz bez pliku
W narzędziach deweloperskich (F12 → Elements/Inspektor) usuń z pola pliku atrybut required i kliknij „Zapisz avatar” bez wybierania pliku. Oczekiwane: Wybierz plik z obrazkiem. To dowód, że PHP sprawdza zawsze, niezależnie od przeglądarki.
Test 6 — każdy gracz ma swój avatar
Zaloguj się jako Ola i wyślij obrazek. Oczekiwane: plik avatar_2_<liczba>..., Ola widzi swój avatar. Zaloguj się jako Kama: nadal widzi swój.
Test 7 — wejście bez formularza
Wpisz w pasku adresu profil.php (GET), będąc zalogowaną/zalogowanym. Oczekiwane: sam formularz, bez komunikatów. Wyloguj się i wejdź na profil.php: przekierowanie na zaloguj.php.
Test 8 — diagnostyka
Otwórz diagnostyka.php. Oczekiwane: 21 plików [OK], sekcja „Avatary” z [OK]. Zmień nazwę katalogu uploads/avatary na avatary_x i odśwież: [BRAK] Katalog ... nie istnieje. Przywróć nazwę.
Test 9 — baza nie działa
Zatrzymaj MySQL w XAMPP. Wyślij poprawny obrazek. Oczekiwane: ramka „Nie udało się zapisać avatara: Nie udało się połączyć z bazą danych.” Zajrzyj do uploads/avatary/: plik jest, choć baza go nie wskazuje. Uruchom MySQL. Ten plik możesz usunąć ręcznie.
8. Obowiązkowe eksperymenty
Po każdym eksperymencie przywróć kod.
Eksperyment 1: formularz bez enctype
W brudnopis.php usuń z formularza enctype="multipart/form-data". Pod print_r($_FILES); dopisz print_r($_POST);. Wyślij kot.png.
Oczekiwane: $_FILES jest puste, a w $_POST jest [avatar] => kot.png. Przeglądarka wysłała zwykły formularz tekstowy, a za wartość pola pliku wzięła jego nazwę. Zawartość pliku nie opuściła komputera.
Pytanie: co zrobiłoby profil.php, gdyby ktoś zapomniał enctype? (Sprawdź: usuń atrybut w profil.php i wyślij obrazek.)
Eksperyment 2: bez sprawdzania zawartości
W walidujAvatar() zamień warunek z getimagesize() na komentarz (całą gałąź elseif razem z jej zawartością). Jako Kama wyślij notatka.png.
Oczekiwane: „Avatar został zmieniony.”, a w kółku ikonka zepsutego obrazka. Otwórz ten „avatar” w nowej karcie: przeglądarka próbuje pokazać tekst jako obrazek.
Teraz wyobraź sobie, że w notatka.png nie ma zwykłego tekstu, tylko kod PHP. Ponieważ plik kończy się na .png, Apache pokaże go jako obrazek i nie wykona go. Ochronę daje więc przede wszystkim rozszerzenie z listy dozwolonych, a getimagesize() pilnuje, żeby avatar był obrazkiem, a nie czymkolwiek. Pytanie: która linia kodu musiałaby zawieść, żeby na serwer trafił plik z końcówką .php? (Wskazówki: krok 4 i 6c.)
Przywróć warunek. Uruchom ponownie sql/taskquest.sql i usuń plik z uploads/avatary/.
Eksperyment 3: nazwa od użytkownika
W profil.php tymczasowo zamień budowanie nazwy na:
$nazwaPlikuAvatara = $plik['name'];
Zaloguj się jako Kama i wyślij obrazek o nazwie avatar.png. Zaloguj się jako Ola i wyślij inny obrazek, też nazwany avatar.png. Zaloguj się z powrotem jako Kama.
Oczekiwane: Kama ma avatar Oli. W katalogu jest jeden plik, drugi go nadpisał, a w bazie oboje wskazują tę samą nazwę. Ktoś mógłby w ten sposób podmieniać avatary innym graczom, znając tylko nazwę ich pliku. Pomyśl też, co by się stało przy nazwie pliku atak.php, gdyby lista rozszerzeń miała błąd.
Przywróć kod. Uruchom ponownie sql/taskquest.sql i opróżnij uploads/avatary/.
Eksperyment 4: brak katalogu
Zmień nazwę uploads/avatary na avatary_x. Wyślij poprawny obrazek.
Oczekiwane: nad stroną ostrzeżenie PHP Warning: move_uploaded_file(...): Failed to open stream: No such file or directory (oraz drugie, Unable to move ...), a pod nim nasza ramka „Nie udało się zapisać avatara: Nie udało się zapisać pliku na serwerze.”. Funkcja zwróciła false i nasz kod to obsłużył. Ostrzeżenia wypisał sam PHP. Zwróć uwagę, że zawierają pełne ścieżki na dysku serwera. Użytkownik nie powinien ich widzieć. Jak wyłączyć ich wyświetlanie, poznasz w Lekcji 23.
Przywróć nazwę katalogu.
Eksperyment 5 (dla chętnych albo pokaz nauczyciela): limity serwera
- W panelu XAMPP przy Apache kliknij „Config” → „PHP (php.ini)”. Znajdź
upload_max_filesizeipost_max_size, zapisz obecne wartości i zmień je naupload_max_filesize=500Kipost_max_size=2M. Zapisz plik i zrestartuj Apache (Stop → Start). - Wyślij obrazek o rozmiarze między 500 KB a 1 MB. Oczekiwane:
Plik może mieć najwyżej 1 MB., choć plik ma mniej niż 1 MB. Zadziałał limit serwera (UPLOAD_ERR_INI_SIZE), a nasz komunikat wprowadza w błąd. Wniosek: własny limit aplikacji musi być niższy niż limity serwera. - Wyślij
duzy.jpg(powyżej 2 MB). Oczekiwane:Plik jest za duży albo nie został wysłany., bo przekroczonypost_max_sizesprawił, że$_FILESjest puste. Nad stroną może się też pojawić ostrzeżenie PHPPOST Content-Length of ... bytes exceeds the limit of ... bytes. PHP zgłasza je, zanim uruchomi nasz skrypt. Tymczasowo zamień wprofil.php$_FILES['avatar'] ?? nullna$_FILES['avatar']i wyślij plik jeszcze raz:Warning: Undefined array key "avatar"i dalsze błędy. - Przywróć kod i oryginalne wartości w
php.ini, zrestartuj Apache.
9. ROZWIĄZANIE
Poniżej wszystkie pliki, które zmieniły się w Lekcji 21. Pliki niewymienione są identyczne jak po Lekcji 19 (wersja w 19_TaskQuest.md, sekcja 9, a tam, gdzie L19 odsyła dalej — w 18_TaskQuest.md).
ROZWIĄZANIE
Nowy katalog
uploads/avatary/ pusty katalog; tworzony ręcznie, nie przez aplikację
sql/taskquest.sql — zmiana w CREATE TABLE uzytkownicy
CREATE TABLE uzytkownicy (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
login VARCHAR(20) NOT NULL UNIQUE,
imie VARCHAR(30) NOT NULL,
haslo_hash VARCHAR(255) NULL,
xp INT UNSIGNED NOT NULL DEFAULT 0,
streak INT UNSIGNED NOT NULL DEFAULT 0,
data_ostatniej_aktywnosci DATE NULL, -- NULL = brak aktywności
avatar VARCHAR(100) NULL -- (L21) nazwa pliku w uploads/avatary; NULL = brak avatara
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Reszta skryptu bez zmian (jeśli INSERT INTO uzytkownicy nie wymienia kolumn — patrz krok 1).
config/config.php — dopisane na końcu
// (L21) avatary graczy
define('KATALOG_AVATAROW', 'uploads/avatary'); // względem katalogu projektu
define('MAKS_ROZMIAR_AVATARA', 1024 * 1024); // 1 MB w bajtach
define('DOZWOLONE_ROZSZERZENIA_AVATARA', ['jpg', 'jpeg', 'png', 'webp']);
includes/funkcje_avatara.php (nowy plik)
<?php
// Funkcje TaskQuest do obsługi avatarów (Lekcja 21).
// Wymagają wcześniejszego dołączenia config/config.php (stałe ..._AVATARA).
function rozszerzenieAvatara(string $nazwaPliku): string
{
return strtolower(pathinfo($nazwaPliku, PATHINFO_EXTENSION));
}
function walidujAvatar(array $plik): array
{
$bledy = [];
if ($plik['error'] === UPLOAD_ERR_NO_FILE) {
$bledy[] = 'Wybierz plik z obrazkiem.';
return $bledy;
}
if ($plik['error'] === UPLOAD_ERR_INI_SIZE || $plik['error'] === UPLOAD_ERR_FORM_SIZE) {
$bledy[] = 'Plik może mieć najwyżej 1 MB.';
return $bledy;
}
if ($plik['error'] !== UPLOAD_ERR_OK) {
$bledy[] = 'Nie udało się przesłać pliku. Spróbuj ponownie.';
return $bledy;
}
if ($plik['size'] === 0) {
$bledy[] = 'Wybrany plik jest pusty.';
return $bledy;
}
if ($plik['size'] > MAKS_ROZMIAR_AVATARA) {
$bledy[] = 'Plik może mieć najwyżej 1 MB.';
}
if (!in_array(rozszerzenieAvatara($plik['name']), DOZWOLONE_ROZSZERZENIA_AVATARA, true)) {
$bledy[] = 'Dozwolone formaty: JPG, PNG, WEBP.';
} elseif (getimagesize($plik['tmp_name']) === false) {
$bledy[] = 'Wybrany plik nie jest obrazkiem.';
}
return $bledy;
}
includes/funkcje_bazy.php — zmiany
- Komentarz na górze: „(Lekcje 18–21)”.
wczytajUzytkownika()—SELECT id, login, imie, xp, streak, data_ostatniej_aktywnosci, avatar FROM uzytkownicy WHERE id = ?oraz ostatni argumentavatar: $wiersz['avatar'],wnew Uzytkownik(...).- Na końcu pliku:
function zapiszAvatarUzytkownika(mysqli $polaczenie, int $uzytkownikId, string $nazwaPlikuAvatara): void
{
$zapytanie = $polaczenie->prepare('UPDATE uzytkownicy SET avatar = ? WHERE id = ?');
$zapytanie->bind_param('si', $nazwaPlikuAvatara, $uzytkownikId);
$zapytanie->execute();
}
classes/Uzytkownik.php — zmiany
- nowa ostatnia właściwość
private ?string $avatar;(poddataOstatniejAktywnosci), - konstruktor: nowy ostatni parametr
?string $avatar, ostatnia linia ciała$this->avatar = $avatar;, - nowa metoda pod
pobierzStreak():
public function pobierzAvatar(): ?string
{
return $this->avatar;
}
Pozostałe metody bez zmian.
profil.php (nowy plik)
<?php
session_start();
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_avatara.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
wymagajLogowania();
$bledy = [];
$bladZapisu = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$plik = $_FILES['avatar'] ?? null;
if ($plik === null) {
$bledy[] = 'Plik jest za duży albo nie został wysłany.';
} else {
$bledy = walidujAvatar($plik);
}
if ($bledy === []) {
$nazwaPlikuAvatara = 'avatar_' . $_SESSION['uzytkownik_id'] . '_' . time() . '.' . rozszerzenieAvatara($plik['name']);
$sciezkaAvatara = __DIR__ . '/' . KATALOG_AVATAROW . '/' . $nazwaPlikuAvatara;
if (!move_uploaded_file($plik['tmp_name'], $sciezkaAvatara)) {
$bladZapisu = 'Nie udało się zapisać pliku na serwerze.';
} else {
try {
$polaczenie = polaczZBaza();
zapiszAvatarUzytkownika($polaczenie, $_SESSION['uzytkownik_id'], $nazwaPlikuAvatara);
ustawKomunikat('Avatar został zmieniony.');
header('Location: index.php');
exit;
} catch (Exception $wyjatek) {
$bladZapisu = $wyjatek->getMessage();
}
}
}
}
$nazwaStrony = 'Profil — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
<h2>Twój profil</h2>
<p><a href="index.php">← Wróć do listy zadań</a></p>
<?php if ($bledy !== []) { ?>
<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ę zapisać avatara: <?= $bladZapisu; ?></p>
<?php } ?>
<form method="post" action="profil.php" enctype="multipart/form-data">
<label for="avatar">Avatar (JPG, PNG lub WEBP, najwyżej 1 MB)</label>
<input type="file" id="avatar" name="avatar" accept=".jpg,.jpeg,.png,.webp" required>
<button type="submit">Zapisz avatar</button>
</form>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
index.php — zmiany
- Bezpośrednio nad zdaniem powitalnym „Witaj, …!”:
<?php if ($uzytkownik->pobierzAvatar() !== null) { ?>
<img class="avatar" src="<?= KATALOG_AVATAROW . '/' . $uzytkownik->pobierzAvatar(); ?>" alt="Avatar gracza">
<?php } else { ?>
<span class="avatar avatar-litera"><?= mb_substr($uzytkownik->pobierzImie(), 0, 1); ?></span>
<?php } ?>
- Pod akapitem „Zalogowano jako …”, nad formularzem „Wyloguj”:
<p><a href="profil.php">Twój profil — zmień avatar</a></p>
Reszta pliku (dane z bazy, obsługa awarii, liczniki, widok listy, przyciski przy zadaniach, pasek XP, ranking) bez zmian. require_once bez zmian.
includes/header.php
W <style>, pod form.akcja { display: inline; }:
.avatar { display: inline-block; width: 64px; height: 64px; border-radius: 50%; object-fit: cover; vertical-align: middle; }
.avatar-litera { background: #5b6ee1; color: #fff; font-size: 32px; font-weight: bold; line-height: 64px; text-align: center; }
diagnostyka.php
W $wymaganePliki dopisane 'profil.php' i 'includes/funkcje_avatara.php' (razem 21 ścieżek). Pod sekcją „Baza danych” nowa sekcja:
<h2>Avatary</h2>
<?php if (!is_dir(__DIR__ . '/uploads/avatary')) { ?>
<p>[BRAK] Katalog uploads/avatary nie istnieje — utwórz go w katalogu projektu.</p>
<?php } elseif (!is_writable(__DIR__ . '/uploads/avatary')) { ?>
<p>[BRAK] Do katalogu uploads/avatary nie można zapisywać plików.</p>
<?php } else { ?>
<p>[OK] Katalog uploads/avatary istnieje i można do niego zapisywać.</p>
<?php } ?>
Reszta bez zmian (nadal bez sesji, bez config.php, bez połączenia z bazą).
includes/footer.php
<p>TaskQuest 2.1</p>
</div>
</body>
</html>
10. Co zmieniliśmy w TaskQuest?
Zostaje
wszystko z Lekcji 19: logowanie (login bez hasła), sesja z uzytkownik_id, zadania w bazie,
dodawanie, „Zrobione!” z XP i streakiem, usuwanie, ranking, filtr widoku z cookie, komunikaty jednorazowe
schemat strony z formularzem (L13), walidacja zwracająca tablicę błędów, POST → przekierowanie → GET
funkcje bazy z L18–L19, klasy Zadanie i Uzytkownik (poza nowym polem), wszystkie funkcje z L1–L15
Dochodzi
baza: uzytkownicy.avatar VARCHAR(100) NULL
config.php: KATALOG_AVATAROW, MAKS_ROZMIAR_AVATARA, DOZWOLONE_ROZSZERZENIA_AVATARA
includes/funkcje_avatara.php:
rozszerzenieAvatara(string $nazwaPliku): string
walidujAvatar(array $plik): array
includes/funkcje_bazy.php:
zapiszAvatarUzytkownika(mysqli, int $uzytkownikId, string $nazwaPlikuAvatara): void
Uzytkownik: ?string $avatar, pobierzAvatar(): ?string
profil.php — formularz avatara (multipart/form-data)
uploads/avatary/ — katalog na pliki
index.php: avatar albo litera przy powitaniu, link do profilu
header.php: .avatar, .avatar-litera
diagnostyka.php: 21 plików, sekcja „Avatary”
Nowe narzędzia: enctype="multipart/form-data", <input type="file"> z accept, tablica $_FILES (name, type, tmp_name, error, size), stałe UPLOAD_ERR_..., move_uploaded_file(), getimagesize(), pathinfo(..., PATHINFO_EXTENSION), strtolower(), in_array(..., true), stała z tablicą, is_dir(), is_writable(), ini_get() (brudnopis), limity upload_max_filesize i post_max_size.
Zmieniamy
wczytajUzytkownika(): czyta kolumnęavatar,- konstruktor
Uzytkownik: nowy ostatni parametr, - stopka:
2.1.
Usuwamy
Nic.
Co nowego potrafi TaskQuest po tej lekcji?
Po Lekcji 21 TaskQuest przyjmuje od gracza plik: każdy może wgrać własny avatar, który pojawia się obok powitania. Aplikacja odrzuca pliki za duże, puste, w złym formacie i takie, które tylko udają obrazek, a zapisuje je pod własną nazwą, więc gracze nie nadpisują sobie plików. Gracz bez avatara widzi kółko z pierwszą literą imienia.
11. Zadania końcowe — rozbuduj TaskQuest samodzielnie
Nie są częścią wersji referencyjnej.
A. Sprzątanie po starym avatarze. Po udanej zmianie w bazie usuń poprzedni plik gracza funkcją unlink($sciezka). Skąd weźmiesz nazwę starego pliku? (Musisz ją odczytać przed zapisem nowej — którą funkcją?) Co, jeśli gracz nie miał avatara (null)? Dlaczego stary plik usuwamy dopiero po udanym zapisie w bazie, a nie przed? Po zmianie Test 3 ma zostawiać w katalogu jeden plik Kamy.
B. Przycisk „Usuń avatar”. Na profil.php drugi formularz (POST, bez pliku) z przyciskiem „Usuń avatar”. Nowa strona akcji usun-avatar.php w stylu usun-zadanie.php: ustawia w bazie avatar = NULL (nowa funkcja usunAvatarUzytkownika(); jak przekazać NULL — czy potrzebujesz bind_param() dla tego NULL?), usuwa plik z dysku, ustawia komunikat i przekierowuje. Gracz znów widzi kółko z literą.
C. Avatary w rankingu. W „Najlepsi gracze” obok loginu mały avatar (np. klasa .avatar-maly 24×24 px) albo kółko z pierwszą literą loginu. Co musisz zmienić w zapytaniu pobierzRanking()? Czy powtórzysz w pętli całą konstrukcję if/else z sekcji 6, czy zamkniesz ją w funkcji zwracającej fragment HTML? Uzasadnij wybór.
Sprzątanie
- Usuń
brudnopis.php,brudnopis2.php, katalogtest_upload/oraz kopiekot.pnginotatka.pngz katalogu projektu. - Sprawdź, że przywrócone są:
enctypewprofil.php(Eksperyment 1), gałąź zgetimagesize()(Eksperyment 2), nazwa plikuavatar_<id>_<czas>(Eksperyment 3), nazwa kataloguuploads/avatary(Eksperyment 4, Test 8),?? nullprzy$_FILESi wartości wphp.ini(Eksperyment 5),mb_substrwindex.php(Test 1). - Uruchom ponownie
sql/taskquest.sqli opróżnijuploads/avatary/(katalog zostaje). - Zaloguj się jako Kama, wgraj avatar, sprawdź, że się wyświetla.
diagnostyka.php— wszystko[OK].
Do samodzielnego sprawdzenia
[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Potrafię zbudować formularz wysyłający plik i wiem, po co jest enctype="multipart/form-data".
[ ] Znam pięć kluczy tablicy $_FILES['pole'] i wiem, którym z nich można ufać.
[ ] Wiem, co się dzieje z plikiem tymczasowym, gdy skrypt go nie przeniesie.
[ ] Potrafię rozpoznać brak pliku, przekroczony limit i przerwane przesyłanie po kodzie UPLOAD_ERR_...
[ ] Wiem, dlaczego $_FILES['avatar'] czytam przez ?? null.
[ ] Potrafię sprawdzić rozszerzenie pliku z listą dozwolonych i wiem, po co strtolower().
[ ] Wiem, co sprawdza getimagesize() i czego nie sprawdza samo rozszerzenie.
[ ] Potrafię wyjaśnić, dlaczego plik zapisuję pod własną nazwą, a nie pod $_FILES['avatar']['name'].
[ ] Wiem, dlaczego w bazie zapisuję nazwę pliku, a nie cały plik ani pełną ścieżkę.
[ ] Wiem, dlaczego pole pliku nie zachowuje wybranego pliku po błędzie.
[ ] Usunęłam/usunąłem brudnopisy i pliki testowe.
Pytania kontrolne
1. Formularz ma method="post", pole <input type="file" name="zdjecie">, ale nie ma enctype. Gracz wybiera wakacje.jpg. Co jest w $_FILES, a co w $_POST?
2. Przewidź wynik walidujAvatar() (lista komunikatów) dla:
a) name = Selfie.JPEG, prawdziwe zdjęcie, error = 0, size = 800 000,
b) name = mem.gif, prawdziwy obrazek, error = 0, size = 3 000 000,
c) name = skrypt.php.jpg, zawartość to kod PHP, error = 0, size = 300,
d) error = UPLOAD_ERR_NO_FILE.
3. Kolega sprawdza format tak: if ($plik['type'] !== 'image/png' && $plik['type'] !== 'image/jpeg') { ... }. Dlaczego to nie chroni aplikacji? Co sprawdza TaskQuest zamiast tego?
4. Koleżanka pominęła move_uploaded_file(): po walidacji od razu zapisuje w bazie nazwę pliku i przekierowuje. Co zobaczy gracz na stronie głównej i dlaczego?
5. Dlaczego nowy avatar Kamy dostaje nazwę avatar_1_<czas>.png, a nie po prostu avatar_1.png? Podaj dwa powody.
6. $_FILES['avatar'] nie istnieje, choć gracz wysłał formularz metodą POST. Jaka jest najbardziej prawdopodobna przyczyna? Co pokaże TaskQuest?
7. Serwer ma upload_max_filesize = 512K, a TaskQuest dopuszcza 1 MB. Gracz wysyła obrazek 700 KB. Który warunek w walidujAvatar() zadziała i dlaczego komunikat wprowadzi gracza w błąd? Jak to naprawić bez zmiany kodu PHP?
8. Znajdź trzy błędy:
$plik = $_FILES['avatar'];
if ($plik['error'] == 0 && $plik['size'] < 1000000) {
move_uploaded_file($plik['tmp_name'], 'uploads/avatary/' . $plik['name']);
}9. Dlaczego w kolumnie avatar zapisujemy avatar_1_1790000000.png, a nie uploads/avatary/avatar_1_1790000000.png? Co trzeba by zrobić z bazą w każdym z tych wariantów, gdyby katalog zmienił nazwę na media/avatary?
10. Po awarii bazy w Teście 9 w uploads/avatary/ został plik, którego baza nie wskazuje. Czy zamiana kolejności (najpierw baza, potem plik) rozwiązałaby problem? Co wtedy mogłoby pójść źle?
PODSUMOWANIE LEKCJI
W skrócie
Czym są superglobale
- To 9 wbudowanych tablic PHP dostępnych w każdym zakresie: w skrypcie, w funkcji, w metodzie klasy — bez słowa
globali bez przekazywania w parametrze. - Wszystkie są zwykłymi tablicami: czytamy je jak każdą inną (
$_POST['login']), więc brakujący klucz to ostrzeżenie. Dlatego zawszeisset()albo?? wartość domyślna.
Komu ufamy, a komu nie
| Tablica | Kto wpisuje dane | Ufamy? |
|---|---|---|
$_GET, $_POST, $_COOKIE, $_REQUEST |
użytkownik (przeglądarka) | nie |
$_FILES – name, type |
przeglądarka użytkownika | nie |
$_FILES – size, error, tmp_name |
PHP | tak |
$_SESSION |
nasz kod na serwerze | tak |
$_SERVER – HTTP_* (m.in. HTTP_USER_AGENT, HTTP_REFERER) |
klient | nie |
$_SERVER – reszta (m.in. REQUEST_METHOD, REMOTE_ADDR, DOCUMENT_ROOT) |
serwer | tak |
To jest sedno całej lekcji: superglobala nie dzielimy na „bezpieczną” i „niebezpieczną” jako całość, tylko pytamy o każdy klucz z osobna, kto go ustawił.
Rzeczy, które trzeba pamiętać, bo psują kod
session_start()musi być przed jakimkolwiek wyjściem (HTML,echo), tak samosetcookie()iheader().- Ciasteczko ustawione przez
setcookie()pojawi się w$_COOKIEdopiero przy następnym żądaniu — w tym samym skrypcie go jeszcze nie odczytasz. $_REQUESTi$GLOBALSdziałają, ale w tym kursie ich nie używamy:$_REQUESTmiesza trzy źródła danych (nie wiadomo, skąd przyszła wartość), a zamiast$GLOBALSprzekazujemy zmienne parametrami funkcji.- GET nadaje się do wyszukiwania, filtrowania, sortowania, stronicowania i linków do zasobu. Nie nadaje się do haseł ani do zmiany danych w bazie — adres da się zapisać w zakładce, wysłać komuś i odświeżyć.
$_FILES i przesyłanie pliku
- Formularz z plikiem wymaga dwóch rzeczy naraz:
method="post"ienctype="multipart/form-data". Bezenctypew$_FILESnie ma nic, a w$_POSTjest sama nazwa pliku. - Zanim ruszy nasz skrypt, PHP zapisuje plik w katalogu tymczasowym pod losową nazwą. Ten plik znika po zakończeniu skryptu — jeśli go nie przeniesiemy
move_uploaded_file(), przepada. move_uploaded_file()zamiastcopy()/rename(), bo sprawdza, czy ścieżka wskazuje plik faktycznie przesłany w tym żądaniu.- Kolejność walidacji: najpierw
error, potem rozmiar, potem rozszerzenie z listy dozwolonych, na końcu zawartość (getimagesize()). Gdyerrorjest różny odUPLOAD_ERR_OK, reszta kluczy nic nie znaczy. - Rozszerzenie i
typemówią tylko, jak plik się nazywa. O tym, czym plik jest, mówi dopiero zajrzenie do zawartości. - Plik zapisujemy pod własną nazwą (
avatar_<id>_<czas>.<rozszerzenie>), a w bazie trzymamy samą nazwę pliku — nie zawartość i nie pełną ścieżkę. - Przekroczony
post_max_sizeto pułapka: żądanie jest metodą POST, ale$_POSTi$_FILESsą puste. Dlatego$_FILES['avatar'] ?? null.
Co trzeba umieć na INF.03?
- odczytać parametr przesłany metodą GET i podstawić wartość domyślną, gdy go nie ma — w arkuszu: „pobiera wartość przesłaną metodą GET, a w przypadku braku – wartością 6”;
- zbudować odnośnik, który przekazuje parametr GET do tej samej strony —
<a href="index.php?id=3">Europa</a>generowane w pętli z rekordów z bazy; - rozpoznać polecenie „formularz wysyłający dane metodą bezpieczną” — chodzi o
method="post", i umieć wskazać wactionten sam plik albo inny skrypt; - sprawdzić, czy formularz w ogóle został wysłany, zanim skrypt zacznie zapisywać do bazy — w arkuszu: „działa tylko w przypadku, gdy z formularza zostały przesłane dane”. Wystarczy
isset($_POST['pole'])lub$_SERVER['REQUEST_METHOD'] === 'POST'(isset()jest w tabeli funkcji dołączonej do arkusza); - wstawić do bazy wartości pobrane z formularza i na końcu zamknąć połączenie z bazą — to osobny punkt wymagań w arkuszu;
- użyć sesji i ciasteczek do zapamiętania stanu użytkownika między stronami (
session_start(),$_SESSION,setcookie(),$_COOKIE).
Minimum, które powinno wejść w pamięć
Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:
- Superglobale działają wszędzie, także w funkcji — bez
global. $_GETi$_POSTto dane od użytkownika. Czytaj je zawsze przez?? ''lubisset()i rzutuj liczby:(int)($_GET['id'] ?? 0).- GET = pobieranie i filtrowanie, POST = zmiana danych, logowanie, pliki.
$_SESSIONżyje na serwerze i jej ufamy.$_COOKIEżyje w przeglądarce i jej nie ufamy.session_start(),setcookie(),header()— zawsze przed jakimkolwiek wypisaniem treści.- Formularz z plikiem:
method="post"+enctype="multipart/form-data". $_FILES['pole']ma 5 kluczy:name,type,tmp_name,error,size. Ufamy trzem ostatnim.- Bez
move_uploaded_file()plik znika po zakończeniu skryptu. - Rozszerzenie i
typemożna podrobić — zawartość obrazka sprawdzagetimagesize(). - Plik zapisujemy pod własną, unikalną nazwą; w bazie trzymamy tylko tę nazwę.
Ściągawka – najważniejsze polecenia i składnia
Odczyt danych od użytkownika
$id = (int)($_GET['id'] ?? 0); // liczba z adresu, 0 gdy brak
$szukaj = trim($_GET['q'] ?? ''); // tekst z adresu
$login = trim($_POST['login'] ?? ''); // tekst z formularza
$motyw = $_COOKIE['motyw'] ?? 'jasny'; // ciasteczko z wartością domyślną
if ($_SERVER['REQUEST_METHOD'] === 'POST') { /* formularz został wysłany */ }
if (isset($_POST['login'])) { /* wersja egzaminacyjna: sprawdzenie pola */ }
echo htmlspecialchars($szukaj); // zanim pokażesz dane użytkownika na stronie
Sesja
session_start(); // przed jakimkolwiek wyjściem
$_SESSION['uzytkownik_id'] = 1;
$id = $_SESSION['uzytkownik_id'] ?? null;
unset($_SESSION['komunikat']); // usunięcie jednego klucza
session_destroy(); // koniec całej sesji (wylogowanie)
Ciasteczka
setcookie('motyw', 'ciemny', time() + 30 * 24 * 60 * 60); // na 30 dni
setcookie('motyw', '', time() - 3600); // usunięcie
// odczyt dopiero przy NASTĘPNYM żądaniu: $_COOKIE['motyw']
Formularz z plikiem
<form method="post" action="profil.php" enctype="multipart/form-data">
<input type="file" name="avatar" accept=".jpg,.jpeg,.png,.webp" required>
<button type="submit">Zapisz avatar</button>
</form>
Odbiór i walidacja pliku
$plik = $_FILES['avatar'] ?? null; // null, gdy przekroczono post_max_size
$plik['error'] // 0 = OK; 1 = za duży (php.ini); 3 = urwane; 4 = nie wybrano pliku
$plik['size'] // rozmiar w bajtach (PHP)
$plik['tmp_name'] // plik tymczasowy — znika po skrypcie
$plik['name'] // nazwa od użytkownika — tylko do wyciągnięcia rozszerzenia
$plik['type'] // typ od przeglądarki — nie używamy
$rozszerzenie = strtolower(pathinfo($plik['name'], PATHINFO_EXTENSION));
in_array($rozszerzenie, ['jpg', 'jpeg', 'png', 'webp'], true);
getimagesize($plik['tmp_name']) === false; // to nie jest obrazek
$nazwa = 'avatar_' . $_SESSION['uzytkownik_id'] . '_' . time() . '.' . $rozszerzenie;
move_uploaded_file($plik['tmp_name'], __DIR__ . '/uploads/avatary/' . $nazwa);
Stałe kodów błędów
UPLOAD_ERR_OK (0), UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3), UPLOAD_ERR_NO_FILE (4), UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7), UPLOAD_ERR_EXTENSION (8).
Przydatne klucze $_SERVER
$_SERVER['REQUEST_METHOD'] // 'GET' albo 'POST'
$_SERVER['REQUEST_URI'] // /profil.php?id=5
$_SERVER['QUERY_STRING'] // id=5&sort=level
$_SERVER['REMOTE_ADDR'] // adres IP użytkownika
$_SERVER['HTTP_USER_AGENT'] // przeglądarka (dane od klienta!)
$_SERVER['DOCUMENT_ROOT'] // katalog główny serwera
Quiz – sprawdź się
-
Strona otwarta adresem
profil.php?id=7. Co wypisze ten kod i dlaczego?function pokazId() { echo $_GET['id'] ?? 'brak'; } pokazId(); -
Formularz wygląda tak:
<form method="post" action="wyslij.php"> <input type="file" name="foto"> <button type="submit">Wyślij</button> </form>Użytkownik wybiera
pies.jpgi wysyła. Co znajdzie się w$_FILES, a co w$_POST? -
Które klucze tablicy
$_FILES['avatar']ustawia PHP, a które przychodzą od przeglądarki użytkownika? Wypisz je w dwóch grupach. -
Skrypt dostaje
$_FILES['zdjecie']['error'] === 4. Co dokładnie się stało i jaki komunikat powinien zobaczyć użytkownik? A co znaczy sytuacja, w której$_FILES['zdjecie']w ogóle nie istnieje, choć formularz został wysłany metodą POST? -
Znajdź trzy błędy:
$plik = $_FILES['avatar']; if ($plik['type'] === 'image/png') { move_uploaded_file($plik['tmp_name'], 'uploads/avatary/' . $plik['name']); zapiszAvatarUzytkownika($polaczenie, $id, $plik['name']); } -
Dlaczego ten kod przy pierwszym wejściu na stronę wypisuje „jasny”, mimo że ciasteczko właśnie zostało ustawione?
setcookie('motyw', 'ciemny', time() + 3600); echo $_COOKIE['motyw'] ?? 'jasny'; -
Uzupełnij kod tak, aby pobrał identyfikator kontynentu przesłany metodą GET, a przy jego braku użył wartości 6 (zadanie w stylu arkusza egzaminacyjnego):
$idKontynentu = ______________________; -
Który warunek poprawnie sprawdza, że formularz został wysłany metodą POST? Uzasadnij odrzucenie pozostałych.
a) if ($_POST['login'] != '') { ... } b) if ($_SERVER['REQUEST_METHOD'] === 'POST') { ... } c) if (isset($_POST)) { ... } -
W formularzu logowania zamieniono
method="post"namethod="get". Kod PHP poprawiono, żeby czytał$_GET. Aplikacja działa. Podaj dwa konkretne powody, dla których to zła zmiana. -
Skrypt waliduje avatar, zapisuje nazwę pliku w bazie i przekierowuje na
index.php— ale ktoś usunął z niego wywołaniemove_uploaded_file(). Co zobaczy gracz na stronie głównej i gdzie jest jego obrazek? -
Napisz fragment, który odrzuci plik większy niż 1 MB, korzystając ze stałej
MAKS_ROZMIAR_AVATARA. Na podstawie którego klucza$_FILESto sprawdzisz i dlaczego nie na podstawie tego, co napisała przeglądarka? -
Dlaczego w kolumnie
avatarzapisujemyavatar_1_1790000000.png, a nie zawartość pliku aniuploads/avatary/avatar_1_1790000000.png?
Odpowiedzi – quiz „Sprawdź się”
Odpowiedzi
- Wypisze
7.$_GETjest superglobalna, więc jest widoczna wewnątrz funkcji bezglobali bez przekazywania jej parametrem. $_FILESbędzie puste, a$_POST['foto']będzie zawierać sam napispies.jpg. Brakujeenctype="multipart/form-data", więc przeglądarka wysłała tylko nazwę pliku jako zwykły tekst.- PHP ustawia
tmp_name,error,size. Od przeglądarki (czyli od użytkownika) pochodząnameitype— tym dwóm nie ufamy. 4toUPLOAD_ERR_NO_FILE: użytkownik wysłał formularz, nie wybierając pliku — komunikat w stylu „Wybierz plik z obrazkiem.”. Brak całego klucza$_FILES['zdjecie']oznacza najczęściej przekroczonypost_max_size: PHP odrzuciło całe żądanie, więc$_POSTteż jest puste. Dlatego czytamy$_FILES['avatar'] ?? null.- (a) sprawdzanie formatu przez
type— ten klucz podaje przeglądarka i da się go podmienić; (b) zapis pod nazwą zname— dwóch graczy nadpisze sobie pliki, a plikatak.phpstanie się skryptem na serwerze; (c) brak sprawdzeniaerror, rozmiaru i rzeczywistej zawartości (getimagesize()) — dodatkowo nie sprawdzono, czymove_uploaded_file()zwróciłotrue, zanim zapisano nazwę w bazie. setcookie()wysyła nagłówekSet-Cookiedo przeglądarki. Tablica$_COOKIEpowstaje z ciasteczek przysłanych razem z bieżącym żądaniem, więc nowe ciasteczko pojawi się w niej dopiero przy kolejnym wczytaniu strony.$idKontynentu = $_GET['id'] ?? 6;(na egzaminie równie dobra jest wersjaisset($_GET['id']) ? $_GET['id'] : 6; w TaskQuest dopisalibyśmy rzutowanie:(int)($_GET['id'] ?? 6)).- Poprawne jest b. Wariant a) odwołuje się do
$_POST['login'], którego przy wejściu GET nie ma — ostrzeżenie o braku klucza. Wariant c) zawsze dajetrue, bo tablica$_POSTistnieje zawsze (przy GET jest po prostu pusta). Sprawdzenie konkretnego pola przezisset($_POST['login'])też jest poprawne. - Po pierwsze, login i hasło znajdą się w adresie URL: w historii przeglądarki, w zakładkach, w logach serwera, na ekranie przy rzutniku. Po drugie, GET służy do pobierania danych — adres da się skopiować, wysłać i odświeżyć, a w ten sposób nie powinno się uruchamiać logowania ani zmieniać danych. Dochodzi jeszcze limit długości adresu.
- Gracz zobaczy uszkodzony obrazek (baza wskazuje plik, którego nie ma w
uploads/avatary/). Obrazek był w katalogu tymczasowym i PHP usunęło go po zakończeniu skryptu. -
Kluczif ($plik['size'] > MAKS_ROZMIAR_AVATARA) { $bledy[] = 'Plik może mieć najwyżej 1 MB.'; }sizewylicza PHP na podstawie rzeczywiście odebranych bajtów. Gdybyśmy wierzyli deklaracji przeglądarki, wystarczyłoby spreparować żądanie z fałszywym rozmiarem. - Zawartości nie trzymamy w bazie, bo baza obsługuje dane tekstowe i liczbowe, a pliki są od tego, żeby leżeć na dysku i być serwowane przez serwer WWW. Pełnej ścieżki nie zapisujemy, bo powielałaby się w każdym wierszu, a po zmianie nazwy katalogu (np. na
media/avatary) trzeba byłoby poprawić wszystkie rekordy. Katalog jest jeden i siedzi w stałejKATALOG_AVATAROW.