Add files via upload

This commit is contained in:
2026-08-28 20:47:44 +02:00
committed by GitHub
parent 1abbb489d6
commit 61a0a08f64
17 changed files with 3962 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
<?php
/**
* ASPIC — contacts.php
* API CRUD sur ASP_Contacts (GET liste + fiche / POST création / PUT
* modification / DELETE suppression logique).
*
* ⚠️ Reconstruction du 24 août 2026, à comparer avec le fichier actuellement
* en production avant remplacement. Cette session n'avait pas accès au
* contenu exact de la version en ligne (alignée le 21 août 2026, avec les
* correctifs du 23 août sur le joker « * » et sur PUT/DELETE — voir journal
* de développement) ; ce fichier reprend fidèlement tout ce qui est
* documenté dans le journal, mais mérite une comparaison avant déploiement
* si la version en ligne contient des ajustements non journalisés.
*
* Reprend :
* - GET liste : tri restreint à une liste blanche de colonnes, jusqu'à 7
* filtres optionnels avec joker "*", WHERE CNT_Actif = 1 systématique.
* - GET fiche (?id=) : un contact actif.
* - POST : création. Génère CNT_Code (25 août 2026, voir journal section 16) :
* 1. Recherche ANN_Code dans ASP_Annees pour l'année courante — AVANT
* l'INSERT, pour ne jamais créer un contact sans code si l'année n'est
* pas encore configurée (échec net, 500, plutôt qu'un contact orphelin
* de code à corriger après coup).
* 2. INSERT du contact, lastInsertId() = CNT_Incr.
* 3. Nombre aléatoire 0-999, zéro-padé sur 3 caractères.
* 4. UPDATE CNT_Code = ANN_Code . "-" . CNT_Incr (sans zéro de tête) .
* nombre aléatoire (3 caractères).
* La journalisation ASP_Realisations (CNT_CRE) reste à la charge du
* front-end, sur le même principe que CNT_MAJ pour la modification (POST
* séparé vers realisations.php, best-effort, ne bloque pas la création).
* - PUT (?id=) : modification, sur l'ensemble des champs éditables du
* panneau de détail (étendu le 24 août 2026 — l'ancien prototype ne
* couvrait que Nom/Prénom/Mail).
* - DELETE (?id=) : suppression logique (CNT_Actif = 0), jamais de DELETE
* SQL réel.
* - PUT vérifie l'EXISTENCE du contact par un SELECT séparé de l'UPDATE lui
* même (corrigé le 24 août 2026 — voir note dans le bloc PUT ci-dessous).
* DELETE continue de s'appuyer sur rowCount(), valide dans son cas.
* - Toute exception PDO (contenu qui ne passe pas une contrainte de colonne,
* erreur de connexion perdue en cours de route, etc.) est interceptée et
* renvoyée en JSON avec un code 500, plutôt que de laisser remonter une
* erreur PHP brute (non-JSON) que le front-end ne peut pas interpréter
* proprement — correctif du 24 août 2026, voir journal.
*
* Note sur le joker "*" : contrairement à la première version (bug corrigé
* le 23 août 2026), l'échappement du LIKE n'est JAMAIS construit par
* concaténation dans le texte SQL. Le caractère d'échappement est passé en
* paramètre lié (ESCAPE ?), comme le motif lui-même — ce qui évite par
* construction le piège du antislash interprété différemment dans un
* littéral SQL selon le moteur (MySQL/MariaDB vs SQLite, voir journal).
*
* ⚠️ Session utilisateur intégrée le 24 août 2026 (voir journal, sections 14
* et 15) : tout appel exige désormais une session PHP valide, ouverte par
* `aspic.php` à l'identification (`$_SESSION['USR_Code']`). Sans elle,
* l'endpoint répond 401 — y compris en lecture (GET). C'est `aspic.php` qui
* doit appeler `session_start()` et renseigner `$_SESSION['USR_Code']` au
* moment où l'identifiant/mot de passe sont validés ; sans cette mise à jour
* côté `aspic.php`, plus aucun appel à ce fichier ne pourra aboutir.
*/
session_start();
header('Content-Type: application/json; charset=utf-8');
require __DIR__ . '/db.php';
// Accès réservé aux utilisateurs identifiés (session ouverte par aspic.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;
}
// 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()]);
}