feat(avatar): implement avatar upload processing and validation, update user interface for avatar display
This commit is contained in:
@@ -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
|
||||
|
||||
+4
-1
@@ -253,7 +253,10 @@ require_once __DIR__ . '/env_badge.php';
|
||||
<a href="/profile.php" class="flex items-center space-x-2.5 group hover:opacity-90 transition px-2 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/60" title="Mijn Profiel & Instellingen">
|
||||
<div class="w-7 h-7 rounded-lg bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 flex-shrink-0 shadow-xs overflow-hidden">
|
||||
<?php if (!empty($currentUser['avatar_url'])): ?>
|
||||
<img src="<?= htmlspecialchars($currentUser['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-[6px] object-cover bg-white dark:bg-slate-950">
|
||||
<img src="<?= htmlspecialchars($currentUser['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-[6px] object-cover bg-white dark:bg-slate-950" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');">
|
||||
<div class="hidden w-full h-full rounded-[6px] bg-white dark:bg-slate-950 flex items-center justify-center font-bold text-[11px] text-indigo-600 dark:text-indigo-400 uppercase">
|
||||
<?= strtoupper(substr($currentUser['name'], 0, 1)) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="w-full h-full rounded-[6px] bg-white dark:bg-slate-950 flex items-center justify-center font-bold text-[11px] text-indigo-600 dark:text-indigo-400 uppercase">
|
||||
<?= strtoupper(substr($currentUser['name'], 0, 1)) ?>
|
||||
|
||||
Reference in New Issue
Block a user