mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-08-28 22:23:14 +00:00
Add files via upload
This commit is contained in:
+301
@@ -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()]);
|
||||
}
|
||||
Reference in New Issue
Block a user