feat(avatar): implement avatar upload processing and validation, update user interface for avatar display

This commit is contained in:
2026-09-11 16:00:48 +02:00
parent e012d6d21c
commit 35ce20cd59
12 changed files with 210 additions and 57 deletions
+139
View File
@@ -384,6 +384,141 @@ function ensure_theme_column(PDO $db): void {
}
}
/**
* Valideert en normaliseert een avatar URL.
* Controleert of geüploade bestanden nog fysiek aanwezig zijn op de server (bijv. na container herstart).
* Indien een bestand ontbreekt, wordt de database automatisch hersteld (self-healing) en null geretourneerd.
*/
function fociz_get_valid_avatar_url(?string $avatarUrl, ?int $userId = null): ?string {
if (empty($avatarUrl)) {
return null;
}
// 1. Data-URI (base64) of meegeleverde repository preset SVG: altijd persistent en geldig
if (str_starts_with($avatarUrl, 'data:image/') || str_starts_with($avatarUrl, '/assets/avatars/')) {
return $avatarUrl;
}
// 2. Geüpload bestand (/assets/uploads/...): controleer fysieke aanwezigheid op server
if (str_starts_with($avatarUrl, '/assets/uploads/')) {
$localPath = dirname(__DIR__) . '/' . ltrim($avatarUrl, '/');
if (file_exists($localPath)) {
return $avatarUrl;
}
// Fysiek bestand ontbreekt (bijv. door container herstart of git deploy)
if ($userId) {
try {
$db = get_db_connection();
$stmt = $db->prepare("UPDATE users SET avatar_url = NULL WHERE id = ? AND avatar_url = ?");
$stmt->execute([$userId, $avatarUrl]);
if (isset($_SESSION['user_avatar_url']) && $_SESSION['user_avatar_url'] === $avatarUrl) {
unset($_SESSION['user_avatar_url']);
}
} catch (Throwable $e) {
error_log("Self-healing avatar cleanup notice: " . $e->getMessage());
}
}
return null;
}
return $avatarUrl;
}
/**
* Verwerkt een geüploade avatar-afbeelding.
* - Knipt vierkant uit en comprimeert naar max. 256x256 via GD
* - Converteert naar Base64 Data URI voor 100% gegarandeerde persistentie in MariaDB
* - Slaat tevens optioneel een backup op in assets/uploads/avatars/
*/
function fociz_process_avatar_upload(array $file, int $userId): array {
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowedTypes)) {
return ['success' => false, 'error' => 'Alleen JPG, PNG, WebP of GIF afbeeldingen zijn toegestaan.'];
}
$ext = match($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif',
default => 'jpg'
};
$rawContent = file_get_contents($file['tmp_name']);
if ($rawContent === false || strlen($rawContent) === 0) {
return ['success' => false, 'error' => 'Kon geüploade afbeelding niet inlezen.'];
}
$finalMime = $mime;
$finalData = $rawContent;
// Verklein en comprimeer naar max 256x256 vierkant via GD
if (extension_loaded('gd') && function_exists('imagecreatefromstring')) {
try {
$srcImg = @imagecreatefromstring($rawContent);
if ($srcImg !== false) {
$w = imagesx($srcImg);
$h = imagesy($srcImg);
$maxDim = 256;
$minDim = min($w, $h);
$cropX = (int)(($w - $minDim) / 2);
$cropY = (int)(($h - $minDim) / 2);
$targetSize = min($maxDim, $minDim);
$dstImg = imagecreatetruecolor($targetSize, $targetSize);
imagealphablending($dstImg, false);
imagesavealpha($dstImg, true);
imagecopyresampled($dstImg, $srcImg, 0, 0, $cropX, $cropY, $targetSize, $targetSize, $minDim, $minDim);
ob_start();
if ($mime === 'image/png') {
imagepng($dstImg, null, 8);
$finalMime = 'image/png';
} elseif (function_exists('imagewebp') && ($mime === 'image/webp' || $mime === 'image/jpeg')) {
imagewebp($dstImg, null, 85);
$finalMime = 'image/webp';
} else {
imagejpeg($dstImg, null, 85);
$finalMime = 'image/jpeg';
}
$compressed = ob_get_clean();
if (!empty($compressed)) {
$finalData = $compressed;
}
imagedestroy($dstImg);
imagedestroy($srcImg);
}
} catch (Throwable $e) {
error_log("fociz avatar resize notice: " . $e->getMessage());
}
}
// Bouw data URI voor permanente opslag in MariaDB
$dataUri = 'data:' . $finalMime . ';base64,' . base64_encode($finalData);
// Sla bestand ook lokaal op als backup / snelle cache
$uploadDir = dirname(__DIR__) . '/assets/uploads/avatars';
if (!is_dir($uploadDir)) {
@mkdir($uploadDir, 0755, true);
}
$filename = "avatar_{$userId}_" . time() . "_" . bin2hex(random_bytes(4)) . ".{$ext}";
@file_put_contents($uploadDir . '/' . $filename, $finalData);
return [
'success' => true,
'data_uri' => $dataUri,
'web_path' => "/assets/uploads/avatars/{$filename}"
];
}
/**
* Genereert een cryptografisch veilig dagsessie-token dat strikt geldig is tot 23:59:59 van vandaag.
* Voorkomt dat gebruikers tussentijds worden uitgelogd bij idle timeouts of browserherstart.
@@ -534,6 +669,8 @@ function get_logged_in_user(): ?array {
$_SESSION['auth_fingerprint'] = $clientFingerprint;
$_SESSION['session_expires_at'] = strtotime('today 23:59:59');
$_SESSION['last_activity'] = $now;
$user['avatar_url'] = fociz_get_valid_avatar_url($user['avatar_url'] ?? null, (int)$user['id']);
$_SESSION['user_avatar_url'] = $user['avatar_url'];
return $user;
}
// Gebruiker bestaat niet meer
@@ -568,6 +705,8 @@ function get_logged_in_user(): ?array {
$_SESSION['last_activity'] = $now;
$_SESSION['login_time'] = $now;
$_SESSION['last_rotated'] = $now;
$user['avatar_url'] = fociz_get_valid_avatar_url($user['avatar_url'] ?? null, (int)$user['id']);
$_SESSION['user_avatar_url'] = $user['avatar_url'];
return $user;
}
// Token is ongeldig of van gisteren