commit e4c96fa7cc491af76ab4323c239f91c6a745136c Author: JMP-MS Date: Fri Aug 28 15:32:36 2026 +0200 Add files via upload diff --git a/html/identification.html b/html/identification.html new file mode 100644 index 0000000..5ffa4fe --- /dev/null +++ b/html/identification.html @@ -0,0 +1,570 @@ + + + + + +ASPIC — Identification + + + + +
+
+ Photo d'un aspic +
+
+
+
A
+
PPLICATION
+ +
de
+
S
+
UIVI
+ +
des
+
P
+
ROSPECTS
+ +
et
+
I
+
NGÉNIERIE
+ +
+
C
+
OMMERCIALE
+
+
+ +
+ +
+ + + + + diff --git a/html/liste_contacts.html b/html/liste_contacts.html new file mode 100644 index 0000000..f2f10fd --- /dev/null +++ b/html/liste_contacts.html @@ -0,0 +1,2216 @@ + + + + +ASPIC — Liste des contacts + + + +
+ + +
+
+
ASPIC
+
LISTE DES CONTACTS
+
+
JMP
+
+ + +
+
+ Nom + + + + +
+
Prénom
+
Dépt
+
+ Ville + + + + +
+
Tél
+
Portable
+
Mail
+
+
+ + +
+
+
+ + +
+ 0/0 contacts + Tri : Nom ascendant + Filtre : aucun + +
+ +
+ + + + + + + + +
+ + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+ + +
+ Contact 1/1 + Tri : Nom ascendant + Filtre : aucun +
+ +
+ +
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + +
+ + +
+
+
Date
+
Qui ?
+
Action
+
Commentaires
+
+
+
+
+
+
+ 0 réalisation + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+ + + + + +
+ + + + diff --git a/php/aspic.php b/php/aspic.php new file mode 100644 index 0000000..8b5e354 --- /dev/null +++ b/php/aspic.php @@ -0,0 +1,93 @@ + false, 'error' => 'Méthode non autorisée']); + exit; +} + +$data = json_decode(file_get_contents('php://input'), true); +$identifiant = isset($data['identifiant']) ? trim((string) $data['identifiant']) : ''; +$motdepasse = isset($data['motdepasse']) ? (string) $data['motdepasse'] : ''; + +if ($identifiant === '' || $motdepasse === '') { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Identifiant et mot de passe sont obligatoires.']); + exit; +} + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Connexion base de données impossible']); + exit; +} + +// USR_Code est un code court (3 caractères) conventionnellement saisi en +// majuscules ; comparaison insensible à la casse sur l'identifiant, comme +// dans le prototype front (voir ASPIC_ecran_identification.html, +// trouverUtilisateurDemo). Le mot de passe, lui, reste comparé strictement +// (sensible à la casse). +$stmt = $pdo->prepare('SELECT USR_Code, USR_Nom, USR_MDP FROM ASP_Users WHERE UPPER(USR_Code) = UPPER(?)'); +$stmt->execute([$identifiant]); +$user = $stmt->fetch(); + +// hash_equals() plutôt que === : comparaison en temps constant, pour ne pas +// laisser une attaque par mesure de timing déduire des informations sur le +// mot de passe stocké. Mot de passe en clair (USR_MDP) assumé consciemment +// pour cette application interne (voir journal section 2) — cette précaution +// est un geste peu coûteux, pas une garantie de sécurité forte sur ce point. +if (!$user || !hash_equals($user['USR_MDP'], $motdepasse)) { + // Message volontairement générique : ne pas révéler si c'est l'identifiant + // ou le mot de passe qui est en cause. Le compteur d'essais / la sortie au + // 3e échec sont gérés côté front, pas ici (voir en-tête de ce fichier). + echo json_encode(['success' => false]); + exit; +} + +// Session ouverte : les endpoints suivants (contacts.php, professions.php, +// statuts.php, realisations.php) exigent désormais $_SESSION['USR_Code'] +// (voir journal, section 15). +$_SESSION['USR_Code'] = $user['USR_Code']; + +echo json_encode([ + 'success' => true, + 'user' => [ + 'USR_Code' => $user['USR_Code'], + 'USR_Nom' => $user['USR_Nom'], + ], +]); diff --git a/php/contacts.php b/php/contacts.php new file mode 100644 index 0000000..1b50508 --- /dev/null +++ b/php/contacts.php @@ -0,0 +1,301 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} + +$method = $_SERVER['REQUEST_METHOD']; + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +// Colonnes éditables depuis le panneau de détail du contact (hors CNT_Code, +// toujours généré/lecture seule, et hors CNT_Incr/CNT_Actif). +const CHAMPS_EDITABLES = [ + 'CNT_Nom', 'CNT_Prenom', 'CNT_Statut', 'CNT_Société', 'CNT_Service', + 'CNT_Fonction', 'CNT_AdrLig1', 'CNT_AdrLig2', 'CNT_AdrLig3', 'CNT_AdrCP', + 'CNT_AdrVille', 'CNT_SIRET', 'CNT_Tel', 'CNT_Portable', 'CNT_Mail', + 'CNT_Notes', 'CNT_CodeProfession', +]; + +// Liste blanche des colonnes de tri autorisées (celles réellement triables +// depuis l'écran liste des contacts). +const COLONNES_TRI_AUTORISEES = ['CNT_Nom', 'CNT_AdrVille']; + +/** + * Construit un motif LIKE à partir d'une valeur de filtre utilisateur, + * en échappant d'abord les caractères spéciaux LIKE déjà présents + * (antislash, %, _) avant de traduire le joker "*" de l'utilisateur en "%". + * Le caractère d'échappement utilisé (un seul antislash) est renvoyé à part, + * pour être passé en paramètre lié à la clause ESCAPE — jamais concaténé + * dans le texte SQL (voir note d'en-tête). + */ +function construireMotifLike(string $valeur): array +{ + $echappe = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $valeur); + $motif = str_replace('*', '%', $echappe); + return [$motif, '\\']; +} + +try { + switch ($method) { + case 'GET': + if (isset($_GET['id'])) { + $stmt = $pdo->prepare('SELECT * FROM ASP_Contacts WHERE CNT_Incr = ? AND CNT_Actif = 1'); + $stmt->execute([$_GET['id']]); + $contact = $stmt->fetch(); + if (!$contact) { + http_response_code(404); + echo json_encode(['error' => 'Contact introuvable']); + exit; + } + echo json_encode($contact); + exit; + } + + $tri = $_GET['tri'] ?? 'CNT_Nom'; + if (!in_array($tri, COLONNES_TRI_AUTORISEES, true)) { + $tri = 'CNT_Nom'; + } + $sens = (isset($_GET['sens']) && strtolower($_GET['sens']) === 'desc') ? 'DESC' : 'ASC'; + + $conditions = ['CNT_Actif = 1']; + $params = []; + + $filtresColonnes = [ + 'f_nom' => 'CNT_Nom', + 'f_prenom' => 'CNT_Prenom', + 'f_ville' => 'CNT_AdrVille', + 'f_tel' => 'CNT_Tel', + 'f_portable' => 'CNT_Portable', + 'f_mail' => 'CNT_Mail', + ]; + foreach ($filtresColonnes as $param => $colonne) { + if (!empty($_GET[$param])) { + [$motif, $echap] = construireMotifLike($_GET[$param]); + $conditions[] = "$colonne LIKE ? ESCAPE ?"; + $params[] = $motif; + $params[] = $echap; + } + } + // Département = 2 premiers caractères de CNT_AdrCP. + if (!empty($_GET['f_dept'])) { + [$motif, $echap] = construireMotifLike($_GET['f_dept']); + $conditions[] = 'LEFT(CNT_AdrCP, 2) LIKE ? ESCAPE ?'; + $params[] = $motif; + $params[] = $echap; + } + + $sql = 'SELECT *, LEFT(CNT_AdrCP, 2) AS CNT_Dept FROM ASP_Contacts WHERE ' + . implode(' AND ', $conditions) + . " ORDER BY $tri $sens, CNT_Prenom ASC"; + + $stmtTotal = $pdo->prepare('SELECT COUNT(*) FROM ASP_Contacts WHERE ' . implode(' AND ', $conditions)); + $stmtTotal->execute($params); + $total = (int)$stmtTotal->fetchColumn(); + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + echo json_encode(['contacts' => $stmt->fetchAll(), 'total' => $total]); + break; + + case 'POST': + $data = json_decode(file_get_contents('php://input'), true) ?? []; + $nom = trim($data['CNT_Nom'] ?? ''); + if ($nom === '') { + http_response_code(400); + echo json_encode(['error' => 'Le nom est obligatoire']); + exit; + } + + // Étape 1 (voir en-tête du fichier) : ANN_Code de l'année courante, + // recherché AVANT l'INSERT pour ne jamais créer un contact sans + // code faute d'année configurée dans ASP_Annees. + $anneeCourante = (int)date('Y'); + $stmtAnnee = $pdo->prepare('SELECT ANN_Code FROM ASP_Annees WHERE ANN_Année = ?'); + $stmtAnnee->execute([$anneeCourante]); + $annCode = $stmtAnnee->fetchColumn(); + if ($annCode === false) { + http_response_code(500); + echo json_encode(['error' => "Aucun code configuré pour l'année $anneeCourante dans ASP_Annees — impossible de générer le Code du contact."]); + exit; + } + + $colonnes = []; + $placeholders = []; + $params = []; + foreach (CHAMPS_EDITABLES as $champ) { + $colonnes[] = $champ; + $placeholders[] = '?'; + $valeur = $data[$champ] ?? null; + $params[] = ($valeur === '' ? null : $valeur); + } + + // Étape 2 : création du contact (CNT_Code encore NULL à ce stade, + // il dépend de CNT_Incr qui n'existe qu'après l'INSERT). + $sql = 'INSERT INTO ASP_Contacts (' . implode(', ', $colonnes) . ') VALUES (' . implode(', ', $placeholders) . ')'; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $nouvelIncr = (int)$pdo->lastInsertId(); + + // Étapes 3-4 : nombre aléatoire 0-999 zéro-padé sur 3 caractères, + // concaténation ANN_Code-CNT_Incr+aléatoire (voir journal section 2 + // pour la logique détaillée, et section 16 pour cette implémentation). + $aleatoire = str_pad((string)random_int(0, 999), 3, '0', STR_PAD_LEFT); + $code = $annCode . '-' . $nouvelIncr . $aleatoire; + + $stmtCode = $pdo->prepare('UPDATE ASP_Contacts SET CNT_Code = ? WHERE CNT_Incr = ?'); + $stmtCode->execute([$code, $nouvelIncr]); + + echo json_encode(['id' => $nouvelIncr, 'code' => $code]); + break; + + case 'PUT': + if (!isset($_GET['id'])) { + http_response_code(400); + echo json_encode(['error' => 'id manquant']); + exit; + } + $data = json_decode(file_get_contents('php://input'), true) ?? []; + $nom = trim($data['CNT_Nom'] ?? ''); + if ($nom === '') { + http_response_code(400); + echo json_encode(['error' => 'Le nom est obligatoire']); + exit; + } + + // Vérification d'EXISTENCE séparée de l'UPDATE lui-même : par + // défaut, rowCount() après un UPDATE reflète le nombre de lignes + // RÉELLEMENT MODIFIÉES par MySQL/MariaDB, pas le nombre de lignes + // trouvées par le WHERE. Une re-soumission du formulaire sans + // changement réel (ou un enregistrement où, par coïncidence, + // aucune des valeurs soumises ne diffère de ce qui est déjà en + // base) donnerait alors rowCount() = 0, et l'ancien code + // l'interprétait à tort comme "contact introuvable" (404) — bug + // corrigé le 24 août 2026, repéré par Jean-Marc ("erreur si je + // remplis Notes ou Informations", en réalité lié à des essais + // répétés sans changement effectif des champs du contact). + $verif = $pdo->prepare('SELECT CNT_Incr FROM ASP_Contacts WHERE CNT_Incr = ? AND CNT_Actif = 1'); + $verif->execute([$_GET['id']]); + if (!$verif->fetch()) { + http_response_code(404); + echo json_encode(['error' => 'Contact introuvable ou déjà désactivé']); + exit; + } + + $assignations = []; + $params = []; + foreach (CHAMPS_EDITABLES as $champ) { + $assignations[] = "$champ = ?"; + $valeur = $data[$champ] ?? null; + $params[] = ($valeur === '' ? null : $valeur); + } + $params[] = $_GET['id']; + + $sql = 'UPDATE ASP_Contacts SET ' . implode(', ', $assignations) . ' WHERE CNT_Incr = ? AND CNT_Actif = 1'; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + echo json_encode(['success' => true]); + break; + + case 'DELETE': + if (!isset($_GET['id'])) { + http_response_code(400); + echo json_encode(['error' => 'id manquant']); + exit; + } + // Suppression logique uniquement (voir journal, section 2) — + // jamais de DELETE SQL réel. Ici rowCount() reste un test valide + // (contrairement au PUT ci-dessus) : CNT_Actif passe toujours + // réellement de 1 à 0, donc rowCount() = 0 signifie bien "déjà + // désactivé ou inexistant". + $stmt = $pdo->prepare('UPDATE ASP_Contacts SET CNT_Actif = 0 WHERE CNT_Incr = ? AND CNT_Actif = 1'); + $stmt->execute([$_GET['id']]); + + if ($stmt->rowCount() === 0) { + http_response_code(404); + echo json_encode(['error' => 'Contact introuvable ou déjà désactivé']); + exit; + } + echo json_encode(['success' => true]); + break; + + default: + http_response_code(405); + echo json_encode(['error' => 'Méthode non autorisée']); + } +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]); +} diff --git a/php/db.php b/php/db.php new file mode 100644 index 0000000..31aa4c2 --- /dev/null +++ b/php/db.php @@ -0,0 +1,16 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]; + + return new PDO($dsn, $user, $pass, $options); +} diff --git a/php/index.php b/php/index.php new file mode 100644 index 0000000..f76b3b6 --- /dev/null +++ b/php/index.php @@ -0,0 +1,17 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} + +$method = $_SERVER['REQUEST_METHOD']; + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +// Whitelist des colonnes de préférence modifiables — n'accepter que celles-ci +// dans le corps d'un PUT, jamais un nom de colonne arbitraire fourni par le +// client (même logique que CHAMPS_EDITABLES dans contacts.php). +const CHAMPS_RATIO = ['USR_RatioPanneauContact', 'USR_RatioPanneauProspect']; + +try { + switch ($method) { + case 'GET': + $stmt = $pdo->prepare( + 'SELECT USR_RatioPanneauContact, USR_RatioPanneauProspect FROM ASP_Users WHERE USR_Code = ?' + ); + $stmt->execute([$_SESSION['USR_Code']]); + $ligne = $stmt->fetch(); + if (!$ligne) { + http_response_code(404); + echo json_encode(['error' => 'Utilisateur introuvable']); + exit; + } + echo json_encode([ + 'USR_RatioPanneauContact' => (int)$ligne['USR_RatioPanneauContact'], + 'USR_RatioPanneauProspect' => (int)$ligne['USR_RatioPanneauProspect'], + ]); + break; + + case 'PUT': + $data = json_decode(file_get_contents('php://input'), true) ?? []; + + $assignations = []; + $params = []; + foreach (CHAMPS_RATIO as $champ) { + if (!array_key_exists($champ, $data)) { + continue; + } + $valeur = $data[$champ]; + if (!is_numeric($valeur) || (int)$valeur < 0 || (int)$valeur > 100) { + http_response_code(400); + echo json_encode(['error' => "$champ doit être un entier compris entre 0 et 100"]); + exit; + } + $assignations[] = "$champ = ?"; + $params[] = (int)$valeur; + } + + if (!$assignations) { + http_response_code(400); + echo json_encode(['error' => 'Aucun champ de préférence reconnu à mettre à jour']); + exit; + } + + $params[] = $_SESSION['USR_Code']; + $sql = 'UPDATE ASP_Users SET ' . implode(', ', $assignations) . ' WHERE USR_Code = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + echo json_encode(['success' => true]); + break; + + default: + http_response_code(405); + echo json_encode(['error' => 'Méthode non autorisée']); + } +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]); +} diff --git a/php/professions.php b/php/professions.php new file mode 100644 index 0000000..2374884 --- /dev/null +++ b/php/professions.php @@ -0,0 +1,79 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} + +$method = $_SERVER['REQUEST_METHOD']; + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +try { + switch ($method) { + case 'GET': + $stmt = $pdo->query('SELECT PRF_Incr, PRF_Libelle FROM ASP_Professions ORDER BY PRF_Libelle ASC'); + echo json_encode(['professions' => $stmt->fetchAll()]); + break; + + case 'POST': + $data = json_decode(file_get_contents('php://input'), true); + $libelle = trim($data['PRF_Libelle'] ?? ''); + if ($libelle === '') { + http_response_code(400); + echo json_encode(['error' => 'Le libellé de la profession est obligatoire']); + exit; + } + + // Pas de contrainte UNIQUE en base sur PRF_Libelle à ce stade (voir + // journal) : on évite ici un doublon évident et strictement identique, + // sans garantie absolue en cas de création concurrente. + $verif = $pdo->prepare('SELECT PRF_Incr FROM ASP_Professions WHERE PRF_Libelle = ?'); + $verif->execute([$libelle]); + $existant = $verif->fetch(); + if ($existant) { + echo json_encode(['id' => (int)$existant['PRF_Incr'], 'existant' => true]); + exit; + } + + $stmt = $pdo->prepare('INSERT INTO ASP_Professions (PRF_Libelle) VALUES (?)'); + $stmt->execute([$libelle]); + echo json_encode(['id' => (int)$pdo->lastInsertId(), 'existant' => false]); + break; + + default: + http_response_code(405); + echo json_encode(['error' => 'Méthode non autorisée']); + } +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]); +} diff --git a/php/prospect_doc.php b/php/prospect_doc.php new file mode 100644 index 0000000..bb498af --- /dev/null +++ b/php/prospect_doc.php @@ -0,0 +1,48 @@ +&b=&c=&d= + +require __DIR__ . '/prospects_core.php'; + +// Destination neutre si les paramètres ont été trafiqués : ta page d'accueil +// plutôt qu'une réponse d'erreur qui confirmerait la tentative au visiteur. +const URL_REDIRECTION_SI_INVALIDE = 'https://mevelia-solutions.bzh'; + +$idProspect = filter_input(INPUT_GET, 'a', FILTER_VALIDATE_INT); +$idDoc = filter_input(INPUT_GET, 'b', FILTER_VALIDATE_INT); +$idCampagne = filter_input(INPUT_GET, 'c', FILTER_VALIDATE_INT); +$signature = $_GET['d'] ?? ''; + +if (!$idProspect || !$idDoc || !$idCampagne || $signature === '') { + header('Location: ' . URL_REDIRECTION_SI_INVALIDE, true, 302); + exit; +} + +$signatureAttendue = signerParametresDoc($idProspect, $idDoc, $idCampagne); +if (!hash_equals($signatureAttendue, $signature)) { + header('Location: ' . URL_REDIRECTION_SI_INVALIDE, true, 302); + exit; +} + +$pdo = getDbConnection(); + +$stmt = $pdo->prepare('SELECT PSP_CodeCampagne FROM ASP_Prospects WHERE PSP_Incr = ?'); +$stmt->execute([$idProspect]); +$prospect = $stmt->fetch(); + +$stmt = $pdo->prepare('SELECT DOC_URL, DOC_Libelle FROM ASP_Docs WHERE DOC_Incr = ?'); +$stmt->execute([$idDoc]); +$doc = $stmt->fetch(); + +if (!$prospect || !$doc) { + header('Location: ' . URL_REDIRECTION_SI_INVALIDE, true, 302); + exit; +} + +journaliserOperation($idProspect, $prospect['PSP_CodeCampagne'] . '-LEC', $doc['DOC_Libelle']); + +header('Location: ' . $doc['DOC_URL'], true, 302); +exit; diff --git a/php/prospect_web-form-001_ajoute.php b/php/prospect_web-form-001_ajoute.php new file mode 100644 index 0000000..0986683 --- /dev/null +++ b/php/prospect_web-form-001_ajoute.php @@ -0,0 +1,8 @@ +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 "-CRE/-VER/-VAL/-DOC/-LEC" + * (codes ASP_Actions) et "-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.']); + } +} diff --git a/php/prospects_verifier.php b/php/prospects_verifier.php new file mode 100644 index 0000000..b23e2b5 --- /dev/null +++ b/php/prospects_verifier.php @@ -0,0 +1,86 @@ +' + . 'ASPIC - Vérification' + . '' + . '

' . htmlspecialchars($texte) . '

' + . ''; +} + +$token = $_GET['token'] ?? ''; +if ($token === '') { + http_response_code(400); + pageMessage('Lien de vérification invalide.'); + exit; +} + +$pdo = getDbConnection(); +$stmt = $pdo->prepare( + 'SELECT PSP_Incr, PSP_Prenom, PSP_Nom, PSP_Mail, PSP_CodeCampagne, PSP_TokenExpiration + FROM ASP_Prospects + WHERE PSP_TokenVerification = ? AND PSP_MailVerifie = 0 AND PSP_Actif = 1' +); +$stmt->execute([$token]); +$prospect = $stmt->fetch(); + +if (!$prospect) { + pageMessage('Ce lien de vérification est invalide ou a déjà été utilisé.'); + exit; +} + +if (strtotime($prospect['PSP_TokenExpiration']) < time()) { + pageMessage('Ce lien de vérification a expiré.'); + exit; +} + +$idProspect = (int) $prospect['PSP_Incr']; +$codeCampagne = $prospect['PSP_CodeCampagne']; + +$pdo->prepare( + 'UPDATE ASP_Prospects + SET PSP_MailVerifie = 1, PSP_TokenVerification = NULL, PSP_TokenExpiration = NULL + WHERE PSP_Incr = ?' +)->execute([$idProspect]); + +journaliserOperation($idProspect, $codeCampagne . '-VAL'); + +// Envoi automatique de la doc par défaut de la campagne, si elle en a une de définie. +$stmt = $pdo->prepare('SELECT CMP_Incr, CMP_DocDefaut FROM ASP_Campagnes WHERE CMP_CodeCampagne = ?'); +$stmt->execute([$codeCampagne]); +$campagne = $stmt->fetch(); + +if ($campagne && $campagne['CMP_DocDefaut']) { + $idDoc = (int) $campagne['CMP_DocDefaut']; + $idCampagneIncr = (int) $campagne['CMP_Incr']; + + $stmt = $pdo->prepare('SELECT DOC_Libelle FROM ASP_Docs WHERE DOC_Incr = ?'); + $stmt->execute([$idDoc]); + $doc = $stmt->fetch(); + + if ($doc) { + $lien = genererLienDoc($idProspect, $idDoc, $idCampagneIncr); + $envoiOk = envoyerMail($codeCampagne . '-DOC', $prospect['PSP_Mail'], [ + 'Prenom' => $prospect['PSP_Prenom'], + 'Nom' => $prospect['PSP_Nom'], + 'Lien' => $lien, + 'Titre' => $doc['DOC_Libelle'], + ]); + if ($envoiOk) { + journaliserOperation($idProspect, $codeCampagne . '-DOC'); + } else { + error_log("ASPIC prospects: échec envoi mail de doc pour le prospect $idProspect"); + } + } +} + +pageMessage('Merci, votre adresse mail est vérifiée ! Vous allez recevoir notre documentation très prochainement.'); diff --git a/php/realisations.php b/php/realisations.php new file mode 100644 index 0000000..2e09f60 --- /dev/null +++ b/php/realisations.php @@ -0,0 +1,100 @@ + : historique des réalisations d'un contact, avec le + * libellé de l'action (jointure sur ASP_Actions), trié du plus ancien + * au plus récent. + * POST : journalise une nouvelle réalisation (REA_IncrContact, REA_Action + * obligatoires ; REA_Comment optionnel). REA_Date est déterminée côté + * serveur (NOW()). + * + * ⚠️ Session utilisateur intégrée le 24 août 2026 (voir journal, sections 14 + * et 15) : accès réservé aux utilisateurs identifiés, et REA_CodeUser est + * désormais lu directement dans `$_SESSION['USR_Code']` (ouverte par + * `aspic.php`) — le champ REA_CodeUser du corps de la requête POST n'est + * plus utilisé et ne peut plus être falsifié par le client. Le "JMP" en dur + * précédent a été supprimé (n'avait de sens que faute de session). + */ + +session_start(); +header('Content-Type: application/json; charset=utf-8'); +require __DIR__ . '/db.php'; + +if (empty($_SESSION['USR_Code'])) { + http_response_code(401); + echo json_encode(['error' => 'Non identifié — veuillez vous connecter.']); + exit; +} + +$method = $_SERVER['REQUEST_METHOD']; + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +try { + switch ($method) { + case 'GET': + if (!isset($_GET['id'])) { + http_response_code(400); + echo json_encode(['error' => 'id (REA_IncrContact) manquant']); + exit; + } + $stmt = $pdo->prepare( + 'SELECT r.REA_Incr, r.REA_Date, r.REA_CodeUser, r.REA_Action, r.REA_Comment, + a.ACT_Libelle + FROM ASP_Realisations r + LEFT JOIN ASP_Actions a ON a.ACT_Code = r.REA_Action + WHERE r.REA_IncrContact = ? + ORDER BY r.REA_Date ASC' + ); + $stmt->execute([$_GET['id']]); + echo json_encode(['realisations' => $stmt->fetchAll()]); + break; + + case 'POST': + $data = json_decode(file_get_contents('php://input'), true); + $idContact = $data['REA_IncrContact'] ?? null; + $action = $data['REA_Action'] ?? null; + if (!$idContact || !$action) { + http_response_code(400); + echo json_encode(['error' => 'REA_IncrContact et REA_Action sont obligatoires']); + exit; + } + + // REA_CodeUser vient de la session (voir en-tête de ce fichier), + // plus du corps de la requête : impossible pour le client de se + // faire passer pour un autre utilisateur. + $codeUser = $_SESSION['USR_Code']; + $commentaire = $data['REA_Comment'] ?? ''; + + $stmt = $pdo->prepare( + 'INSERT INTO ASP_Realisations (REA_IncrContact, REA_CodeUser, REA_Date, REA_Action, REA_Comment) + VALUES (?, ?, NOW(), ?, ?)' + ); + $stmt->execute([$idContact, $codeUser, $action, $commentaire]); + echo json_encode(['id' => (int)$pdo->lastInsertId()]); + break; + + default: + http_response_code(405); + echo json_encode(['error' => 'Méthode non autorisée']); + } +} catch (PDOException $e) { + // Toute exception PDO (ex. REA_Action trop long pour la colonne, valeur + // rejetée par une contrainte) renvoie du JSON exploitable côté front + // plutôt qu'une erreur PHP brute — voir contacts.php pour le même + // correctif du 24 août 2026. + http_response_code(500); + echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]); +} diff --git a/php/statuts.php b/php/statuts.php new file mode 100644 index 0000000..14398e2 --- /dev/null +++ b/php/statuts.php @@ -0,0 +1,48 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + http_response_code(405); + echo json_encode(['error' => 'Méthode non autorisée']); + exit; +} + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +try { + // Tri par STA_Incr (ordre de création/incrément), pas par libellé — + // demande explicite de Jean-Marc du 24 août 2026. + $stmt = $pdo->query('SELECT STA_Code, STA_Libellé FROM ASP_Statuts ORDER BY STA_Incr ASC'); + echo json_encode(['statuts' => $stmt->fetchAll()]); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]); +} diff --git a/png/logo.png b/png/logo.png new file mode 100644 index 0000000..532b6f0 Binary files /dev/null and b/png/logo.png differ diff --git a/polices/MetroDF.ttf b/polices/MetroDF.ttf new file mode 100644 index 0000000..9d31af4 Binary files /dev/null and b/polices/MetroDF.ttf differ diff --git a/polices/MetroDF.woff2 b/polices/MetroDF.woff2 new file mode 100644 index 0000000..005307f Binary files /dev/null and b/polices/MetroDF.woff2 differ