feat(v2.0.2): Corrections de sécurité critiques et fonctionnalité de réactivation des devis

- Correction de 14 vulnérabilités SQL (8 critiques, 6 moyennes)
- Suppression de la fonction autocomplete non utilisée
- Migration complète vers PDO avec requêtes préparées
- Ajout du bouton 'Réactiver' pour les devis archivés (statut 20 → 1)
- Conversion des appels $.ajax en fetch API (vanilla JS)
- Correction des erreurs JavaScript empêchant l'attachement d'événements
- Mise à jour de la documentation (README.md et TODO.md)

Sécurité: Utilisation systématique de intval() et requêtes préparées PDO
UI: Nouveau bouton vert dans la grille 2x2 des actions sur devis archivés
Historique: Traçabilité dans devis_histo lors de la réactivation
This commit is contained in:
2025-09-12 20:25:48 +02:00
parent eabb4bf67a
commit 443b0509df
16 changed files with 4355 additions and 3318 deletions

View File

@@ -1,38 +1,72 @@
<?php
global $Session;
$fkUser = $Session->_user["rowid"];
$fkRole = $Session->_user["fk_role"];
$fkUser = intval($Session->_user["rowid"]); // SÉCURITÉ : Validation de l'ID utilisateur
$fkRole = intval($Session->_user["fk_role"]); // SÉCURITÉ : Validation du rôle
// SÉCURITÉ : Construction de la clause WHERE avec des paramètres sécurisés
$whereParams = [];
switch ($fkRole) {
case 1:
// DIR-CO
$where = 'd.fk_user=' . $fkUser . ' OR d.fk_statut_devis>=2';
$where = 'd.fk_user = :fkUser OR d.fk_statut_devis >= 2';
$whereParams[':fkUser'] = $fkUser;
break;
case 2:
// DV : on récupère tous les RR de son périmètre
$sql = 'SELECT rowid FROM users WHERE fk_parent =' . $fkUser . ';';
$aRR = getinfos($sql, "gen");
$lstRR = '';
foreach ($aRR as $rr) {
$lstRR .= $rr["rowid"] . ',';
try {
$db = Database::getInstance();
$sql = 'SELECT rowid FROM users WHERE fk_parent = :fkParent';
$stmt = $db->prepare($sql);
$stmt->execute([':fkParent' => $fkUser]);
$aRR = $stmt->fetchAll(PDO::FETCH_ASSOC);
$rrIds = array_column($aRR, 'rowid');
if (!empty($rrIds)) {
// Création de placeholders pour la requête IN
$placeholders = [];
foreach ($rrIds as $index => $id) {
$placeholder = ':rr' . $index;
$placeholders[] = $placeholder;
$whereParams[$placeholder] = $id;
}
$where = 'd.fk_user = :fkUser OR (d.fk_statut_devis >= 3 AND d.fk_user IN (' . implode(',', $placeholders) . '))';
$whereParams[':fkUser'] = $fkUser;
} else {
$where = 'd.fk_user = :fkUser';
$whereParams[':fkUser'] = $fkUser;
}
} catch (Exception $e) {
error_log("Erreur récupération RR : " . $e->getMessage());
$where = 'd.fk_user = :fkUser';
$whereParams[':fkUser'] = $fkUser;
}
$lstRR = substr($lstRR, 0, -1);
$where = 'd.fk_user=' . $fkUser . ' OR (d.fk_statut_devis>=3 AND d.fk_user IN (' . $lstRR . '))';
break;
default:
// RR
$where = 'd.fk_user=' . $fkUser;
$where = 'd.fk_user = :fkUser';
$whereParams[':fkUser'] = $fkUser;
break;
}
$sql = 'SELECT d.rowid, d.dossier, d.date_demande, d.date_remise, d.num_opportunite, d.fk_client, d.montant_total_ht_remise, d.marge_totale, d.commentaire, d.chk_speciaux, c.libelle, c.ville, c.cp, d.fk_statut_devis, ';
$sql .= 'd.lib_new_client, d.type_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client, d.comment_devis, d.comment_geste_comm, ';
$sql .= 'd.contact_new_nom, d.contact_new_prenom, d.new_telephone, d.new_mobile, d.new_email, d.contact_new_fonction, LEFT(u.prenom,1) AS prenom, u.libelle as nom, ';
$sql .= 'xs.libelle as lib_statut, d.chk_new_statut, m.libelle as lib_marche, d.chk_validat, d.fk_user_validat, d.date_validat ';
$sql .= 'FROM devis d LEFT JOIN clients c ON c.rowid=d.fk_client LEFT JOIN x_statuts_devis xs ON xs.rowid=d.fk_statut_devis LEFT JOIN marches m ON m.rowid=d.fk_marche LEFT JOIN users u ON u.rowid=d.fk_user ';
$sql .= 'WHERE ' . $where . ' ORDER BY d.dossier, d.date_remise DESC;';
$aModel["devis"] = getinfos($sql, "gen");
// SÉCURITÉ : Requête avec paramètres préparés
try {
$db = Database::getInstance();
$sql = 'SELECT d.rowid, d.dossier, d.date_demande, d.date_remise, d.num_opportunite, d.fk_client, d.montant_total_ht_remise, d.marge_totale, d.commentaire, d.chk_speciaux, c.libelle, c.ville, c.cp, d.fk_statut_devis, ';
$sql .= 'd.lib_new_client, d.type_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client, d.comment_devis, d.comment_geste_comm, ';
$sql .= 'd.contact_new_nom, d.contact_new_prenom, d.new_telephone, d.new_mobile, d.new_email, d.contact_new_fonction, LEFT(u.prenom,1) AS prenom, u.libelle as nom, ';
$sql .= 'xs.libelle as lib_statut, d.chk_new_statut, m.libelle as lib_marche, d.chk_validat, d.fk_user_validat, d.date_validat ';
$sql .= 'FROM devis d LEFT JOIN clients c ON c.rowid=d.fk_client LEFT JOIN x_statuts_devis xs ON xs.rowid=d.fk_statut_devis LEFT JOIN marches m ON m.rowid=d.fk_marche LEFT JOIN users u ON u.rowid=d.fk_user ';
$sql .= 'WHERE ' . $where . ' ORDER BY d.dossier, d.date_remise DESC';
$pdo = $db->getPDO();
$stmt = $pdo->prepare($sql);
$stmt->execute($whereParams);
$aModel["devis"] = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("Erreur récupération devis : " . $e->getMessage());
$aModel["devis"] = [];
}
//! on compte le nombre de devis par statut
$aModel["nb_devis"] = array();
@@ -45,8 +79,16 @@ foreach ($aModel["devis"] as $devis) {
}
//! On récupère la liste des dossiers des devis
$sql = 'SELECT DISTINCT d.dossier FROM devis d WHERE ' . $where . ' ORDER BY d.dossier;';
$aModel["dossiers"] = getinfos($sql, "gen");
// SÉCURITÉ : Requête avec paramètres préparés
try {
$sql = 'SELECT DISTINCT d.dossier FROM devis d WHERE ' . $where . ' ORDER BY d.dossier';
$stmt = $pdo->prepare($sql);
$stmt->execute($whereParams);
$aModel["dossiers"] = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("Erreur récupération dossiers : " . $e->getMessage());
$aModel["dossiers"] = [];
}
//! Tous les produits du catalogue
$sql = 'SELECT rowid, code, libelle, prix_vente, prix_achat_net FROM produits WHERE active=1;';