🚑️ Ajout du fonctionnement du formulaire de contact en production, utilisation de PHPMailer.

This commit is contained in:
2026-01-24 03:48:37 +01:00
parent 9180f116ec
commit db285e2006
8 changed files with 306 additions and 24 deletions

View File

@@ -1,29 +1,26 @@
<?php
/**
* Endpoint de traitement du formulaire de contact
* Reçoit les données en JSON, valide, vérifie reCAPTCHA, envoie l'email
*/
require_once __DIR__ . '/../includes/config.php';
require_once __DIR__ . '/../includes/functions.php';
// Démarrer la session pour le CSRF
ini_set('display_errors', 1);
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Headers
header('Content-Type: application/json; charset=utf-8');
//header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
// Uniquement POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
exit;
}
// Récupérer les données JSON
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
@@ -33,29 +30,23 @@ if (!$input) {
}
try {
// 1. Valider le token CSRF
if (!verifyCsrfToken($input['csrf_token'] ?? '')) {
throw new Exception('Token de sécurité invalide. Veuillez rafraîchir la page.');
}
// 2. Vérifier reCAPTCHA
$recaptchaScore = verifyRecaptcha($input['recaptcha_token'] ?? '');
if ($recaptchaScore < RECAPTCHA_THRESHOLD) {
error_log("reCAPTCHA score trop bas: {$recaptchaScore}");
throw new Exception('Vérification anti-spam échouée. Veuillez réessayer.');
}
// 3. Valider et nettoyer les données
$data = validateContactData($input);
// 4. Envoyer l'email
$sent = sendContactEmail($data);
if (!$sent) {
throw new Exception('Erreur lors de l\'envoi du message. Veuillez réessayer plus tard.');
}
// 5. Succès
echo json_encode([
'success' => true,
'message' => 'Votre message a bien été envoyé ! Je vous répondrai dans les meilleurs délais.'

70
api/contact.php.old Normal file
View File

@@ -0,0 +1,70 @@
<?php
/**
* Endpoint de traitement du formulaire de contact
* Reçoit les données en JSON, valide, vérifie reCAPTCHA, envoie l'email
*/
require_once __DIR__ . '/../includes/config.php';
require_once __DIR__ . '/../includes/functions.php';
// Démarrer la session pour le CSRF
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Headers
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
// Uniquement POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
exit;
}
// Récupérer les données JSON
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Données invalides']);
exit;
}
try {
// 1. Valider le token CSRF
if (!verifyCsrfToken($input['csrf_token'] ?? '')) {
throw new Exception('Token de sécurité invalide. Veuillez rafraîchir la page.');
}
// 2. Vérifier reCAPTCHA
$recaptchaScore = verifyRecaptcha($input['recaptcha_token'] ?? '');
if ($recaptchaScore < RECAPTCHA_THRESHOLD) {
error_log("reCAPTCHA score trop bas: {$recaptchaScore}");
throw new Exception('Vérification anti-spam échouée. Veuillez réessayer.');
}
// 3. Valider et nettoyer les données
$data = validateContactData($input);
// 4. Envoyer l'email
$sent = sendContactEmail($data);
if (!$sent) {
throw new Exception('Erreur lors de l\'envoi du message. Veuillez réessayer plus tard.');
}
// 5. Succès
echo json_encode([
'success' => true,
'message' => 'Votre message a bien été envoyé ! Je vous répondrai dans les meilleurs délais.'
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}

View File

@@ -99,7 +99,7 @@
.input {
@apply w-full px-4 py-3
bg-surface border border-border rounded-lg
text-text-primary placeholder-text-muted
text-text-muted placeholder-text-muted
transition-all duration-150
focus:outline-none focus:border-primary focus:shadow-input-focus;
}

View File

@@ -4,6 +4,7 @@
"type": "project",
"require": {
"php": ">=8.0",
"vlucas/phpdotenv": "^5.6"
"vlucas/phpdotenv": "^5.6",
"phpmailer/phpmailer": "^7.0"
}
}

84
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "adbddd7a48b14ed78896b2d6c5ef28e9",
"content-hash": "ef9466a44690e608fe2d148c314ef38c",
"packages": [
{
"name": "graham-campbell/result-type",
@@ -68,6 +68,88 @@
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "phpmailer/phpmailer",
"version": "v7.0.2",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
"reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^10.0.0@dev",
"squizlabs/php_codesniffer": "^3.13.5",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-only"
],
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.0.2"
},
"funding": [
{
"url": "https://github.com/Synchro",
"type": "github"
}
],
"time": "2026-01-09T18:02:33+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",

View File

@@ -55,5 +55,17 @@ define('RECAPTCHA_THRESHOLD', 0.5); // Score minimum (0.0 à 1.0)
// Contact
define('CONTACT_EMAIL', $_ENV['CONTACT_EMAIL'] ?? '');
// SMTP
define('SMTP_HOST', $_ENV['SMTP_HOST'] ?? '127.0.0.1');
define('SMTP_PORT', (int) $_ENV['SMTP_PORT'] ?? '1025');
define('SMTP_USERNAME', $_ENV['SMTP_USERNAME'] ?? '');
define('SMTP_PASSWORD', $_ENV['SMTP_PASSWORD'] ?? '');
define('SMTP_ENCRYPTION', $_ENV['SMTP_ENCRYPTION'] ?? 'none'); // none|tls|ssl
define('MAIL_FROM_ADDRESS', $_ENV['MAIL_FROM_ADDRESS'] ?? SMTP_USERNAME);
define('MAIL_FROM_NAME', $_ENV['MAIL_FROM_NAME'] ?? 'Portfolio - Contact');
// Sécurité
define('APP_SECRET', $_ENV['APP_SECRET'] ?? '');

View File

@@ -3,6 +3,11 @@
* Fonctions helpers du portfolio
*/
require_once __DIR__ . '/../vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception as MailerException;
/**
* Inclut un template avec des données
* @param string $name Nom du template (sans .php)
@@ -410,6 +415,127 @@ function validateContactData(array $input): array
* @return bool True si envoyé avec succès
*/
function sendContactEmail(array $data): bool
{
// Vérifier config minimale
if (!defined('CONTACT_EMAIL') || empty(CONTACT_EMAIL)) {
error_log('CONTACT_EMAIL non configuré');
return false;
}
if (!defined('SMTP_HOST') || !defined('SMTP_PORT')) {
error_log('SMTP_HOST/SMTP_PORT non configurés');
return false;
}
// Autoload PHPMailer (Composer)
$autoload = __DIR__ . '/../vendor/autoload.php';
if (!file_exists($autoload)) {
error_log('PHPMailer autoload introuvable. As-tu installé Composer ?');
return false;
}
require_once $autoload;
$categorieLabels = [
'projet' => 'Projet freelance',
'poste' => 'Proposition de poste',
'autre' => 'Autre demande'
];
$categorie = $categorieLabels[$data['categorie']] ?? 'Autre';
$entreprise = $data['entreprise'] ?: 'Non renseignée';
$subject = "[Portfolio] {$categorie} - {$data['objet']}";
$body = "═══════════════════════════════════════════\n";
$body .= "NOUVEAU MESSAGE - PORTFOLIO\n";
$body .= "═══════════════════════════════════════════\n\n";
$body .= "DE: {$data['prenom']} {$data['nom']}\n";
$body .= "EMAIL: {$data['email']}\n";
$body .= "ENTREPRISE: {$entreprise}\n";
$body .= "CATÉGORIE: {$categorie}\n\n";
$body .= "───────────────────────────────────────────\n";
$body .= "OBJET: {$data['objet']}\n";
$body .= "───────────────────────────────────────────\n\n";
$body .= "MESSAGE:\n\n";
$body .= "{$data['message']}\n\n";
$body .= "═══════════════════════════════════════════\n";
$body .= "Envoyé le {$data['date']}\n";
$body .= "IP: {$data['ip']}\n";
$body .= "═══════════════════════════════════════════";
$mail = new PHPMailer(true);
try {
// Mode SMTP
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->Port = SMTP_PORT;
// Auth SMTP si configurée
$hasAuth = !empty(SMTP_USERNAME) && !empty(SMTP_PASSWORD);
$mail->SMTPAuth = $hasAuth;
if ($hasAuth) {
$mail->Username = SMTP_USERNAME;
$mail->Password = SMTP_PASSWORD;
}
// Encryption
$enc = strtolower((string) SMTP_ENCRYPTION);
if ($enc === 'tls') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
} elseif ($enc === 'ssl') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
} else {
// none
$mail->SMTPSecure = false;
$mail->SMTPAutoTLS = false;
}
$mail->CharSet = 'UTF-8';
$mail->Encoding = 'base64';
// Expéditeur (doit être un alias autorisé si Proton)
$fromAddress = defined('MAIL_FROM_ADDRESS') ? MAIL_FROM_ADDRESS : SMTP_USERNAME;
$fromName = defined('MAIL_FROM_NAME') ? MAIL_FROM_NAME : 'Portfolio - Contact';
if (empty($fromAddress)) {
error_log('MAIL_FROM_ADDRESS/SMTP_USERNAME vide : impossible de définir From');
return false;
}
$mail->setFrom($fromAddress, $fromName);
// Reply-To = email du visiteur
if (!empty($data['email'])) {
$mail->addReplyTo($data['email'], trim(($data['prenom'] ?? '') . ' ' . ($data['nom'] ?? '')));
}
// Destinataire
$mail->addAddress(CONTACT_EMAIL);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->isHTML(false);
$mail->SMTPDebug = 2;
$mail->Debugoutput = function ($str, $level) {
error_log("PHPMailer debug($level): $str");
};
// Timeouts/log (utile en debug)
$mail->Timeout = 10;
$mail->send();
return true;
} catch (\Throwable $e) {
error_log('Mail send unexpected error: ' . $e->getMessage());
return false;
}
}
/*function sendContactEmail(array $data): bool
{
$categorieLabels = [
'projet' => 'Projet freelance',
@@ -460,4 +586,4 @@ function sendContactEmail(array $data): bool
}
return $result;
}
}*/

View File

@@ -47,7 +47,7 @@ include_template('navbar', compact('currentPage'));
type="text"
id="nom"
name="nom"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
required
maxlength="100"
autocomplete="family-name"
@@ -65,7 +65,7 @@ include_template('navbar', compact('currentPage'));
type="text"
id="prenom"
name="prenom"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
required
maxlength="100"
autocomplete="given-name"
@@ -86,7 +86,7 @@ include_template('navbar', compact('currentPage'));
type="email"
id="email"
name="email"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
required
maxlength="255"
autocomplete="email"
@@ -104,7 +104,7 @@ include_template('navbar', compact('currentPage'));
type="text"
id="entreprise"
name="entreprise"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
maxlength="200"
autocomplete="organization"
placeholder="Nom de votre entreprise"
@@ -120,13 +120,13 @@ include_template('navbar', compact('currentPage'));
<select
id="categorie"
name="categorie"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
required
>
<option value="" disabled selected>Sélectionnez une catégorie...</option>
<option value="projet">Je souhaite parler de mon projet</option>
<option value="poste">Je souhaite vous proposer un poste</option>
<option value="autre">Autre</option>
<option value="autre">J'ai une autre idée en tête</option>
</select>
<p class="text-error text-sm mt-1 hidden" data-error="categorie"></p>
</div>
@@ -140,7 +140,7 @@ include_template('navbar', compact('currentPage'));
type="text"
id="objet"
name="objet"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
required
maxlength="200"
placeholder="Résumez votre demande en quelques mots"
@@ -156,7 +156,7 @@ include_template('navbar', compact('currentPage'));
<textarea
id="message"
name="message"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors resize-y min-h-[150px]"
class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors resize-y min-h-[150px]"
required
maxlength="5000"
rows="6"