Files
ASPIC/php/prospects_core.php
T
2026-08-28 15:32:36 +02:00

278 lines
9.7 KiB
PHP

<?php
// prospects_core.php
// Logique commune à tous les scripts publics de prospection ASPIC.
// Réutilise db.php (déjà en production) pour la connexion PDO.
require __DIR__ . '/db.php';
// --- Configuration ---------------------------------------------------
const BREVO_FROM_EMAIL = 'viktor@ms.bzh';
const BREVO_FROM_NAME = 'Viktor - Mevelia Solutions';
const BREVO_REPLY_TO = 'contact@mevelia-solutions.bzh';
const ASPIC_BASE_URL = 'https://aspic.mvl-tls.fr';
// URL publique du logo, injectée automatiquement dans toutes les variables
// disponibles pour les modèles de mail (utilisable via {(LogoUrl)}).
const LOGO_URL = 'https://aspic.mvl-tls.fr/logo.png';
const ORIGINES_CORS_AUTORISEES = [
'https://mevelia-solutions.bzh',
];
// --- Utilitaires génériques ------------------------------------------
/**
* Envoie les en-têtes CORS nécessaires si l'origine de la requête est autorisée,
* et répond immédiatement aux requêtes de pré-vérification (OPTIONS).
*/
function autoriserCorsSiNecessaire(): void
{
$origine = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origine, ORIGINES_CORS_AUTORISEES, true)) {
header('Access-Control-Allow-Origin: ' . $origine);
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
}
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
http_response_code(204);
exit;
}
}
/**
* Génère un token de vérification imprévisible (64 caractères hexadécimaux).
*/
function genererTokenVerification(): string
{
return bin2hex(random_bytes(32));
}
/**
* Envoie un mail depuis un modèle de ASP_Mails, en substituant les variables
* de la forme {(NomVariable)} dans l'objet et le corps (texte et HTML).
* {(LogoUrl)} est toujours disponible, sans avoir à la passer explicitement.
* Retourne true si l'envoi (via l'API Brevo) a réussi.
*/
function envoyerMail(string $codeMail, string $destinataire, array $variables): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare('SELECT MAI_Objet, MAI_Corps, MAI_CorpsHtml FROM ASP_Mails WHERE MAI_Code = ?');
$stmt->execute([$codeMail]);
$modele = $stmt->fetch();
if (!$modele) {
error_log("ASPIC prospects: modèle de mail introuvable ($codeMail)");
return false;
}
$variables = $variables + ['LogoUrl' => LOGO_URL];
$objet = $modele['MAI_Objet'];
$corpsTexte = $modele['MAI_Corps'];
$corpsHtml = $modele['MAI_CorpsHtml'];
foreach ($variables as $cle => $valeur) {
$motif = '{(' . $cle . ')}';
$objet = str_replace($motif, (string) $valeur, $objet);
$corpsTexte = str_replace($motif, (string) $valeur, $corpsTexte);
if ($corpsHtml !== null) {
$corpsHtml = str_replace($motif, (string) $valeur, $corpsHtml);
}
}
$apiKey = getenv('BREVO_API_KEY');
if (!$apiKey) {
error_log('ASPIC prospects: BREVO_API_KEY non configurée');
return false;
}
$donneesEnvoi = [
'sender' => ['name' => BREVO_FROM_NAME, 'email' => BREVO_FROM_EMAIL],
'to' => [['email' => $destinataire]],
'replyTo' => ['email' => BREVO_REPLY_TO],
'subject' => $objet,
'textContent' => $corpsTexte,
];
if ($corpsHtml !== null && trim($corpsHtml) !== '') {
$donneesEnvoi['htmlContent'] = $corpsHtml;
}
$payload = json_encode($donneesEnvoi);
$ch = curl_init('https://api.brevo.com/v3/smtp/email');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'accept: application/json',
'api-key: ' . $apiKey,
'content-type: application/json',
],
CURLOPT_TIMEOUT => 10,
]);
$reponse = curl_exec($ch);
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$erreurCurl = curl_error($ch);
curl_close($ch);
if ($codeHttp < 200 || $codeHttp >= 300) {
error_log("ASPIC prospects: échec envoi Brevo (HTTP $codeHttp) : $reponse $erreurCurl");
return false;
}
return true;
}
/**
* Journalise une opération sur un prospect dans ASP_Operations.
* OPE_CodeUser est toujours 'Vik' (Viktor) : ces événements sont automatiques,
* jamais déclenchés par un utilisateur ASPIC connecté.
*/
function journaliserOperation(int $idProspect, string $codeAction, ?string $commentaire = null): void
{
$pdo = getDbConnection();
$stmt = $pdo->prepare(
'INSERT INTO ASP_Operations (OPE_IncrProspect, OPE_CodeUser, OPE_Date, OPE_Action, OPE_Comment)
VALUES (?, ?, NOW(), ?, ?)'
);
$stmt->execute([$idProspect, 'Vik', $codeAction, $commentaire]);
}
/**
* Calcule la signature HMAC des paramètres d'un lien de documentation (a/b/c),
* pour empêcher qu'un visiteur ne les modifie sans que ça se voie.
* La clé ne doit JAMAIS être exposée côté client.
*/
function signerParametresDoc(int $idProspect, int $idDoc, int $idCampagne): string
{
$cle = getenv('LIEN_DOC_CLE_SECRETE');
if (!$cle) {
error_log('ASPIC prospects: LIEN_DOC_CLE_SECRETE non configurée');
$cle = '';
}
return substr(hash_hmac('sha256', "$idProspect-$idDoc-$idCampagne", $cle), 0, 16);
}
/**
* Construit un lien de documentation signé, à insérer dans un mail.
*/
function genererLienDoc(int $idProspect, int $idDoc, int $idCampagne): string
{
$signature = signerParametresDoc($idProspect, $idDoc, $idCampagne);
return ASPIC_BASE_URL . '/prospect_doc.php?a=' . $idProspect
. '&b=' . $idDoc . '&c=' . $idCampagne . '&d=' . $signature;
}
/**
* Détermine si un numéro de téléphone saisi va dans PSP_Tel ou PSP_Portable :
* préfixe 06/07 (après suppression des espaces) => portable, sinon => fixe.
*/
function routerTelephone(string $telephoneBrut): array
{
$nettoye = preg_replace('/\s+/', '', $telephoneBrut);
if ($nettoye === '') {
return ['tel' => null, 'portable' => null];
}
$prefixe = substr($nettoye, 0, 2);
if ($prefixe === '06' || $prefixe === '07') {
return ['tel' => null, 'portable' => $nettoye];
}
return ['tel' => $nettoye, 'portable' => null];
}
/**
* Traite la soumission du formulaire public pour une campagne donnée.
* Dérive automatiquement les codes ASP_Actions et le code du mail de vérification
* à partir du code de campagne, suivant la convention "<campagne>-CRE/-VER/-VAL/-DOC/-LEC"
* (codes ASP_Actions) et "<campagne>-DEM" (mail de vérification).
*/
function traiterAjoutProspect(string $codeCampagne): void
{
autoriserCorsSiNecessaire();
header('Content-Type: application/json; charset=utf-8');
$data = json_decode(file_get_contents('php://input'), true) ?? [];
// Honeypot anti-spam : champ invisible pour un humain, rempli seulement par un robot.
if (!empty($data['site_web'] ?? '')) {
http_response_code(200);
echo json_encode(['success' => true]);
exit;
}
$nom = trim((string) ($data['nom'] ?? ''));
$prenom = trim((string) ($data['prenom'] ?? ''));
$telephone = trim((string) ($data['telephone'] ?? ''));
$mail = trim((string) ($data['mail'] ?? ''));
if ($mail === '' && $telephone === '') {
http_response_code(400);
echo json_encode(['error' => 'Merci de renseigner au moins une adresse mail ou un numéro de téléphone.']);
exit;
}
if (strlen($nom) > 40 || strlen($prenom) > 40 || strlen($mail) > 40) {
http_response_code(400);
echo json_encode(['error' => 'Un des champs saisis est trop long.']);
exit;
}
$telNettoye = preg_replace('/\s+/', '', $telephone);
if (strlen($telNettoye) > 14) {
http_response_code(400);
echo json_encode(['error' => 'Le numéro de téléphone saisi est trop long.']);
exit;
}
if ($mail !== '' && !filter_var($mail, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['error' => "L'adresse mail saisie n'est pas valide."]);
exit;
}
$tel = routerTelephone($telephone);
try {
$pdo = getDbConnection();
$token = genererTokenVerification();
$expiration = date('Y-m-d H:i:s', strtotime('+48 hours'));
$stmt = $pdo->prepare(
'INSERT INTO ASP_Prospects
(PSP_Nom, PSP_Prenom, PSP_Tel, PSP_Portable, PSP_Mail, PSP_CodeCampagne,
PSP_MailVerifie, PSP_TokenVerification, PSP_TokenExpiration, PSP_Actif)
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, 1)'
);
$stmt->execute([
$nom !== '' ? $nom : null,
$prenom !== '' ? $prenom : null,
$tel['tel'],
$tel['portable'],
$mail !== '' ? $mail : null,
$codeCampagne,
$token,
$expiration,
]);
$idProspect = (int) $pdo->lastInsertId();
journaliserOperation($idProspect, $codeCampagne . '-CRE');
if ($mail !== '') {
$lien = ASPIC_BASE_URL . '/prospects_verifier.php?token=' . urlencode($token);
$envoiOk = envoyerMail($codeCampagne . '-DEM', $mail, [
'Prenom' => $prenom,
'Nom' => $nom,
'Lien' => $lien,
]);
if ($envoiOk) {
journaliserOperation($idProspect, $codeCampagne . '-VER');
} else {
error_log("ASPIC prospects: échec envoi mail de vérification pour le prospect $idProspect");
}
}
echo json_encode(['success' => true]);
} catch (PDOException $e) {
error_log('ASPIC prospects: erreur PDO - ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Erreur serveur, merci de réessayer plus tard.']);
}
}