Tieni aggiornati i tuoi dati: li usiamo per le prenotazioni e per contattarti.
Per sicurezza ti chiediamo la password attuale prima di impostarne una nuova.
getConnection(); $iduserlogin = (int) $iduserlogin; if (!function_exists('e')) { function e($value): string { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } } if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } if (empty($_SESSION['csrf_profile'])) { $_SESSION['csrf_profile'] = bin2hex(random_bytes(32)); } $csrf = $_SESSION['csrf_profile']; $profileFeedback = null; // ['type' => 'success'|'error', 'messages' => [...]] $passwordFeedback = null; $avatarFeedback = null; $postedProfile = null; // valori reinviati in caso di errore /* Avatar */ $avatarDir = 'upload/users/'; $avatarDefault = 'meditation.png'; $avatarMaxBytes = 5 * 1024 * 1024; // 5 MB $avatarSide = 400; // lato in px dell'immagine salvata /** Solo i file creati da questa pagina (nome casuale) possono essere cancellati dal disco */ function isOwnAvatarFile($name): bool { return is_string($name) && preg_match('/^[a-f0-9]{32}\.(jpg|png|webp)$/', $name) === 1; } /* ------------------------------------------------------------------------- * Dati di supporto * ---------------------------------------------------------------------- */ function loadUser(PDO $pdo, int $id) { $stmt = $pdo->prepare( "SELECT id, email, password, first_name, last_name, phone, address, country_id, birthday, avatar, created_at FROM auth_users WHERE id = :id LIMIT 1" ); $stmt->execute([':id' => $id]); return $stmt->fetch(PDO::FETCH_ASSOC); } $user = loadUser($pdo, $iduserlogin); if (!$user) { http_response_code(403); exit('Utente non trovato.'); } // Elenco paesi (se la tabella non è leggibile il campo viene nascosto) $countries = []; try { $q = $pdo->query("SELECT id, name FROM auth_countries ORDER BY name"); $countries = $q ? $q->fetchAll(PDO::FETCH_KEY_PAIR) : []; } catch (Throwable $ex) { $countries = []; } /* ------------------------------------------------------------------------- * Salvataggio (POST) * ---------------------------------------------------------------------- */ $action = $_POST['action'] ?? ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action !== '') { if (!hash_equals($csrf, (string) ($_POST['csrf'] ?? ''))) { $fb = ['type' => 'error', 'messages' => ['La sessione è scaduta. Ricarica la pagina e riprova.']]; if ($action === 'password') { $passwordFeedback = $fb; } elseif ($action === 'avatar' || $action === 'avatar_remove') { $avatarFeedback = $fb; } else { $profileFeedback = $fb; } } elseif ($action === 'profile') { /* ---------- Dati personali ---------- */ $firstName = trim($_POST['first_name'] ?? ''); $lastName = trim($_POST['last_name'] ?? ''); $phone = trim($_POST['phone'] ?? ''); $address = trim($_POST['address'] ?? ''); $birthdayInput = trim($_POST['birthday'] ?? ''); $countryInput = trim((string) ($_POST['country_id'] ?? '')); $errors = []; if ($firstName === '') { $errors[] = 'Il nome è obbligatorio.'; } if ($lastName === '') { $errors[] = 'Il cognome è obbligatorio.'; } foreach (['nome' => $firstName, 'cognome' => $lastName, 'indirizzo' => $address] as $label => $val) { if (mb_strlen($val) > 191) { $errors[] = "Il campo $label è troppo lungo (max 191 caratteri)."; } } // Telefono facoltativo if ($phone !== '' && !preg_match('/^[0-9+\s().\/-]{6,30}$/', $phone)) { $errors[] = 'Il numero di telefono non è valido.'; } // Data di nascita facoltativa, GG/MM/AAAA $birthdayDb = null; if ($birthdayInput !== '') { $bObj = DateTime::createFromFormat('!d/m/Y', $birthdayInput); $valid = $bObj && $bObj->format('d/m/Y') === $birthdayInput; if (!$valid) { $errors[] = 'La data di nascita non è valida. Usa il formato GG/MM/AAAA.'; } elseif ($bObj > new DateTime('today') || (int) $bObj->format('Y') < 1900) { $errors[] = 'La data di nascita non è plausibile.'; } else { $birthdayDb = $bObj->format('Y-m-d'); } } // Paese: se l'elenco non è disponibile si mantiene il valore attuale if (empty($countries)) { $countryId = $user['country_id']; } elseif ($countryInput === '') { $countryId = null; } elseif (isset($countries[(int) $countryInput])) { $countryId = (int) $countryInput; } else { $errors[] = 'Il paese selezionato non è valido.'; $countryId = $user['country_id']; } if (empty($errors)) { $sql = "UPDATE auth_users SET first_name = :first_name, last_name = :last_name, phone = :phone, address = :address, birthday = :birthday, country_id = :country_id, updated_at = NOW() WHERE id = :id"; $stmt = $pdo->prepare($sql); $ok = $stmt->execute([ ':first_name' => $firstName, ':last_name' => $lastName, ':phone' => $phone !== '' ? $phone : null, ':address' => $address !== '' ? $address : null, ':birthday' => $birthdayDb, ':country_id' => $countryId, ':id' => $iduserlogin, ]); if ($ok) { $profileFeedback = ['type' => 'success', 'messages' => ['Modifiche salvate.']]; $user = loadUser($pdo, $iduserlogin); $firstname = $user['first_name']; // aggiorna il saluto nella topbar/pagina } else { $profileFeedback = ['type' => 'error', 'messages' => ['Salvataggio non riuscito. Riprova.']]; } } else { $profileFeedback = ['type' => 'error', 'messages' => $errors]; $postedProfile = array_merge($user, [ 'first_name' => $firstName, 'last_name' => $lastName, 'phone' => $phone, 'address' => $address, 'country_id' => $countryInput, 'birthday_display' => $birthdayInput, ]); } } elseif ($action === 'avatar') { /* ---------- Caricamento foto profilo ---------- */ $f = $_FILES['avatar'] ?? null; if (!$f || $f['error'] === UPLOAD_ERR_NO_FILE) { $avatarFeedback = ['type' => 'error', 'messages' => ['Seleziona un\'immagine da caricare.']]; } elseif ($f['error'] === UPLOAD_ERR_INI_SIZE || $f['error'] === UPLOAD_ERR_FORM_SIZE || $f['size'] > $avatarMaxBytes) { $avatarFeedback = ['type' => 'error', 'messages' => ['L\'immagine supera la dimensione massima di 5 MB.']]; } elseif ($f['error'] !== UPLOAD_ERR_OK) { $avatarFeedback = ['type' => 'error', 'messages' => ['Caricamento non riuscito. Riprova.']]; } else { $mimeMap = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp']; $mime = (new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']); $info = @getimagesize($f['tmp_name']); if (!isset($mimeMap[$mime]) || !$info) { $avatarFeedback = ['type' => 'error', 'messages' => ['Formato non consentito. Carica un\'immagine JPG, PNG o WebP.']]; } else { if (!is_dir($avatarDir)) { @mkdir($avatarDir, 0755, true); } $base = bin2hex(random_bytes(16)); $fileName = null; // Con GD: ritaglio quadrato centrale, ridimensionamento e ricodifica in JPG // (rimuove anche dati EXIF e qualsiasi contenuto non grafico) if (function_exists('imagecreatefromstring') && function_exists('imagejpeg')) { $src = @imagecreatefromstring((string) file_get_contents($f['tmp_name'])); if ($src) { // Foto da smartphone: corregge l'orientamento EXIF if ($mime === 'image/jpeg' && function_exists('exif_read_data')) { $exif = @exif_read_data($f['tmp_name']); $orientation = (int) ($exif['Orientation'] ?? 1); $angles = [3 => 180, 6 => -90, 8 => 90]; if (isset($angles[$orientation])) { $rotated = imagerotate($src, $angles[$orientation], 0); if ($rotated) { imagedestroy($src); $src = $rotated; } } } $w = imagesx($src); $h = imagesy($src); $side = min($w, $h); $out = min($avatarSide, $side); $dst = imagecreatetruecolor($out, $out); imagefill($dst, 0, 0, imagecolorallocate($dst, 255, 255, 255)); imagecopyresampled($dst, $src, 0, 0, (int) (($w - $side) / 2), (int) (($h - $side) / 2), $out, $out, $side, $side); if (imagejpeg($dst, $avatarDir . $base . '.jpg', 85)) { $fileName = $base . '.jpg'; } imagedestroy($src); imagedestroy($dst); } } // Senza GD (o formato non gestito da GD): salva il file originale if ($fileName === null && move_uploaded_file($f['tmp_name'], $avatarDir . $base . '.' . $mimeMap[$mime])) { $fileName = $base . '.' . $mimeMap[$mime]; } if ($fileName === null) { $avatarFeedback = ['type' => 'error', 'messages' => ['Salvataggio dell\'immagine non riuscito. Riprova.']]; } else { $stmt = $pdo->prepare("UPDATE auth_users SET avatar = :avatar, updated_at = NOW() WHERE id = :id"); if ($stmt->execute([':avatar' => $fileName, ':id' => $iduserlogin])) { $old = $user['avatar'] ?? ''; if (isOwnAvatarFile($old) && is_file($avatarDir . $old)) { @unlink($avatarDir . $old); } $user = loadUser($pdo, $iduserlogin); $avatarFeedback = ['type' => 'success', 'messages' => ['Foto profilo aggiornata.']]; } else { @unlink($avatarDir . $fileName); $avatarFeedback = ['type' => 'error', 'messages' => ['Salvataggio dell\'immagine non riuscito. Riprova.']]; } } } } } elseif ($action === 'avatar_remove') { /* ---------- Rimozione foto profilo ---------- */ $stmt = $pdo->prepare("UPDATE auth_users SET avatar = NULL, updated_at = NOW() WHERE id = :id"); if ($stmt->execute([':id' => $iduserlogin])) { $old = $user['avatar'] ?? ''; if (isOwnAvatarFile($old) && is_file($avatarDir . $old)) { @unlink($avatarDir . $old); } $user = loadUser($pdo, $iduserlogin); $avatarFeedback = ['type' => 'success', 'messages' => ['Foto profilo rimossa.']]; } else { $avatarFeedback = ['type' => 'error', 'messages' => ['Rimozione non riuscita. Riprova.']]; } } elseif ($action === 'password') { /* ---------- Cambio password ---------- */ $current = (string) ($_POST['current_password'] ?? ''); $new = (string) ($_POST['new_password'] ?? ''); $confirm = (string) ($_POST['confirm_password'] ?? ''); // Limite tentativi sulla password attuale: 5 errori, poi pausa di 15 minuti $fails = $_SESSION['pwd_fail_count'] ?? 0; $lockUntil = $_SESSION['pwd_lock_until'] ?? 0; if ($lockUntil > time()) { $minutes = (int) ceil(($lockUntil - time()) / 60); $passwordFeedback = ['type' => 'error', 'messages' => ["Troppi tentativi errati. Riprova tra $minutes minuti."]]; } elseif ($current === '' || $new === '' || $confirm === '') { $passwordFeedback = ['type' => 'error', 'messages' => ['Compila tutti e tre i campi.']]; } elseif (!password_verify($current, $user['password'])) { $fails++; $_SESSION['pwd_fail_count'] = $fails; if ($fails >= 5) { $_SESSION['pwd_lock_until'] = time() + 15 * 60; $_SESSION['pwd_fail_count'] = 0; } $passwordFeedback = ['type' => 'error', 'messages' => ['La password attuale non è corretta.']]; } elseif (mb_strlen($new) < 8) { $passwordFeedback = ['type' => 'error', 'messages' => ['La nuova password deve avere almeno 8 caratteri.']]; } elseif (!preg_match('/[A-Za-z]/', $new) || !preg_match('/[0-9]/', $new)) { $passwordFeedback = ['type' => 'error', 'messages' => ['La nuova password deve contenere almeno una lettera e un numero.']]; } elseif ($new !== $confirm) { $passwordFeedback = ['type' => 'error', 'messages' => ['Le due nuove password non coincidono.']]; } elseif (password_verify($new, $user['password'])) { $passwordFeedback = ['type' => 'error', 'messages' => ['La nuova password deve essere diversa da quella attuale.']]; } else { $stmt = $pdo->prepare( "UPDATE auth_users SET password = :pwd, remember_token = NULL, updated_at = NOW() WHERE id = :id" ); $ok = $stmt->execute([ ':pwd' => password_hash($new, PASSWORD_DEFAULT), ':id' => $iduserlogin, ]); if ($ok) { unset($_SESSION['pwd_fail_count'], $_SESSION['pwd_lock_until']); session_regenerate_id(true); $user = loadUser($pdo, $iduserlogin); $passwordFeedback = ['type' => 'success', 'messages' => ['Password aggiornata.']]; } else { $passwordFeedback = ['type' => 'error', 'messages' => ['Aggiornamento della password non riuscito. Riprova.']]; } } } } /* ------------------------------------------------------------------------- * Valori da mostrare nel modulo * ---------------------------------------------------------------------- */ $form = $postedProfile ?? $user; if (!isset($form['birthday_display'])) { $form['birthday_display'] = !empty($user['birthday']) ? date('d/m/Y', strtotime($user['birthday'])) : ''; } $fullName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '')); $avatarValue = (string) ($user['avatar'] ?? ''); $avatarUrl = null; if ($avatarValue !== '' && preg_match('#^https?://#i', $avatarValue)) { $avatarUrl = $avatarValue; } elseif ($avatarValue !== '' && is_file($avatarDir . basename($avatarValue))) { $avatarUrl = $avatarDir . rawurlencode(basename($avatarValue)); } $hasCustomAvatar = $avatarUrl !== null; if (!$hasCustomAvatar) { $avatarUrl = $avatarDir . $avatarDefault; } $memberSince = !empty($user['created_at']) ? date('d/m/Y', strtotime($user['created_at'])) : null; function renderFeedback(?array $fb): void { if (!$fb) { return; } $cls = $fb['type'] === 'success' ? 'success' : 'danger'; echo '
Tieni aggiornati i tuoi dati: li usiamo per le prenotazioni e per contattarti.
Per sicurezza ti chiediamo la password attuale prima di impostarne una nuova.