($p['category'] ?? '') === $category)); } /** * Récupère un projet par son slug */ function getProjectBySlug(string $slug): ?array { foreach (getProjects() as $project) { if (($project['slug'] ?? '') === $slug) { return $project; } } return null; } /** * Récupère les technologies uniques de tous les projets */ function getAllTechnologies(): array { $technologies = []; foreach (getProjects() as $project) { foreach ($project['technologies'] ?? [] as $tech) { if (!in_array($tech, $technologies, true)) { $technologies[] = $tech; } } } sort($technologies); return $technologies; } /** * Helper pour afficher une image projet optimisée */ function projectImage(string $filename, string $alt, int $width, int $height, bool $lazy = true): string { $webp = $filename; $fallback = str_replace('.webp', '.jpg', $filename); $lazyAttr = $lazy ? 'loading="lazy"' : ''; $altEsc = htmlspecialchars($alt, ENT_QUOTES, 'UTF-8'); $webpEsc = htmlspecialchars($webp, ENT_QUOTES, 'UTF-8'); $fallbackEsc = htmlspecialchars($fallback, ENT_QUOTES, 'UTF-8'); return << {$altEsc} HTML; } /** * Compte les projets par technologie */ function getProjectCountByTech(): array { $projects = getProjects(); $count = []; foreach ($projects as $project) { foreach ($project['technologies'] ?? [] as $tech) { $count[$tech] = ($count[$tech] ?? 0) + 1; } } return $count; } /** * Récupère les projets utilisant une technologie */ function getProjectsByTech(string $tech): array { return array_values(array_filter(getProjects(), function ($project) use ($tech) { return in_array($tech, $project['technologies'] ?? [], true); })); } /** * Icônes d'outils */ function getToolIcon(string $icon): string { $icons = [ 'github' => '', 'vscode' => '', 'figma' => '', 'notion' => '', 'docker' => '', ]; return $icons[$icon] ?? '🛠'; } /** * Récupère tous les témoignages */ function getTestimonials(): array { $data = loadJsonData('testimonials.json'); return $data['testimonials'] ?? []; } /** * Récupère les témoignages mis en avant */ function getFeaturedTestimonials(): array { return array_values(array_filter(getTestimonials(), fn($t) => ($t['featured'] ?? false) === true)); } /** * Récupère le témoignage lié à un projet */ function getTestimonialByProject(string $projectSlug): ?array { foreach (getTestimonials() as $testimonial) { if (($testimonial['project_slug'] ?? '') === $projectSlug) { return $testimonial; } } return null; } /** * CSRF token helpers */ function generateCsrfToken(): string { if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } function verifyCsrfToken(?string $token): bool { if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } return !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); } /** * Vérifie le token reCAPTCHA v3 auprès de Google */ function verifyRecaptcha(string $token): float { if (empty($token) || empty(RECAPTCHA_SECRET_KEY)) { error_log('reCAPTCHA: token ou secret manquant'); return 0.3; } $response = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => 'Content-Type: application/x-www-form-urlencoded', 'content' => http_build_query([ 'secret' => RECAPTCHA_SECRET_KEY, 'response' => $token, 'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '' ]) ] ])); if ($response === false) { error_log('reCAPTCHA: impossible de contacter Google'); return 0.3; } $result = json_decode($response, true); if (!($result['success'] ?? false)) { error_log('reCAPTCHA: échec - ' . json_encode($result['error-codes'] ?? [])); return 0.0; } return (float) ($result['score'] ?? 0.0); } /** * Valide et nettoie les données du formulaire de contact * @throws Exception si validation échoue */ function validateContactData(array $input): array { $errors = []; $required = ['nom', 'prenom', 'email', 'categorie', 'objet', 'message']; foreach ($required as $field) { if (empty(trim($input[$field] ?? ''))) { $errors[] = "Le champ {$field} est requis"; } } $emailRaw = trim($input['email'] ?? ''); $email = str_replace(["\r", "\n"], '', $emailRaw); if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = "L'adresse email n'est pas valide"; } $validCategories = ['projet', 'poste', 'autre']; $categorie = $input['categorie'] ?? ''; if ($categorie && !in_array($categorie, $validCategories, true)) { $errors[] = 'Catégorie invalide'; } if (strlen($input['nom'] ?? '') > 100) { $errors[] = 'Le nom est trop long (max 100 caractères)'; } if (strlen($input['prenom'] ?? '') > 100) { $errors[] = 'Le prénom est trop long (max 100 caractères)'; } if (strlen($input['objet'] ?? '') > 200) { $errors[] = "L'objet est trop long (max 200 caractères)"; } if (strlen($input['message'] ?? '') > 5000) { $errors[] = 'Le message est trop long (max 5000 caractères)'; } if (strlen($input['objet'] ?? '') > 0 && strlen($input['objet']) < 5) { $errors[] = "L'objet est trop court (min 5 caractères)"; } if (strlen($input['message'] ?? '') > 0 && strlen($input['message']) < 20) { $errors[] = 'Le message est trop court (min 20 caractères)'; } if (!empty($errors)) { throw new Exception(implode('. ', $errors)); } return [ 'nom' => htmlspecialchars(trim($input['nom'] ?? ''), ENT_QUOTES, 'UTF-8'), 'prenom' => htmlspecialchars(trim($input['prenom'] ?? ''), ENT_QUOTES, 'UTF-8'), 'email' => filter_var($email, FILTER_SANITIZE_EMAIL), 'entreprise' => htmlspecialchars(trim($input['entreprise'] ?? ''), ENT_QUOTES, 'UTF-8'), 'categorie' => $categorie, 'objet' => htmlspecialchars(trim($input['objet'] ?? ''), ENT_QUOTES, 'UTF-8'), 'message' => htmlspecialchars(trim($input['message'] ?? ''), ENT_QUOTES, 'UTF-8'), 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'inconnue', 'date' => date('d/m/Y à H:i:s'), ]; } /** * Envoie l'email de contact */ function sendContactEmail(array $data): bool { $categorieLabels = [ 'projet' => 'Projet freelance', 'poste' => 'Proposition de poste', 'autre' => 'Autre demande' ]; $subject = "[Portfolio] {$categorieLabels[$data['categorie']]} - {$data['objet']}"; $body = <<isSMTP(); $mail->Host = MAIL_HOST; $mail->SMTPAuth = true; $mail->Username = MAIL_USERNAME; $mail->Password = MAIL_PASSWORD; $mail->SMTPSecure = MAIL_ENCRYPTION; $mail->Port = MAIL_PORT; } else { $mail->isMail(); } $mail->CharSet = 'UTF-8'; $mail->setFrom(MAIL_FROM, MAIL_FROM_NAME); $mail->addAddress(CONTACT_EMAIL); $mail->addReplyTo($data['email'], "{$data['prenom']} {$data['nom']}"); $mail->Subject = $subject; $mail->Body = $body; $mail->AltBody = $body; return $mail->send(); } catch (\PHPMailer\PHPMailer\Exception $e) { error_log('Échec envoi email contact: ' . $e->getMessage()); return false; } }