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:
@@ -194,19 +194,35 @@ switch ($Route->_action) {
|
||||
case "getdata":
|
||||
$chp = $_POST["chp"];
|
||||
$typ = $Route->_param1;
|
||||
$sql = "";
|
||||
$upls = array();
|
||||
|
||||
switch ($typ) {
|
||||
case "tiers":
|
||||
$sql = "SELECT $chp AS data FROM clients WHERE rowid=" . $fk_tiers . ";";
|
||||
$dbn = "groupe";
|
||||
// SÉCURITÉ : Liste blanche des colonnes autorisées
|
||||
$allowedColumns = ['code', 'libelle', 'adresse1', 'adresse2', 'adresse3', 'cp', 'ville',
|
||||
'contact_nom', 'contact_prenom', 'contact_fonction', 'telephone', 'mobile', 'email'];
|
||||
|
||||
if (!in_array($chp, $allowedColumns)) {
|
||||
echo json_encode(array('error' => 'Colonne non autorisée'));
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
// SÉCURITÉ : Utilisation de requête préparée pour l'ID
|
||||
$sql = "SELECT `$chp` AS data FROM clients WHERE rowid = :id";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([':id' => intval($fk_tiers)]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($result) {
|
||||
$upls = $result;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur getdata : " . $e->getMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$upls = array();
|
||||
if ($sql != "") {
|
||||
$upls = getinfos($sql, $dbn);
|
||||
$upls = $upls[0];
|
||||
}
|
||||
|
||||
echo json_encode($upls);
|
||||
break;
|
||||
|
||||
@@ -216,63 +232,8 @@ switch ($Route->_action) {
|
||||
}
|
||||
break;
|
||||
|
||||
case "autocomplete":
|
||||
if (isset($_POST["term"])) {
|
||||
$term = $_POST["term"];
|
||||
$tabl = $_POST["table"];
|
||||
$fiel = $_POST["field"];
|
||||
$fiel2 = isset($_POST["field2"]) ? $_POST["field2"] : "";
|
||||
$fiel3 = isset($_POST["field3"]) ? $_POST["field3"] : "";
|
||||
$fiel4 = isset($_POST["field4"]) ? $_POST["field4"] : "";
|
||||
$fiel5 = isset($_POST["field5"]) ? $_POST["field5"] : "";
|
||||
$fiel6 = isset($_POST["field6"]) ? $_POST["field6"] : "";
|
||||
$fiel7 = isset($_POST["field7"]) ? $_POST["field7"] : "";
|
||||
$fiel8 = isset($_POST["field8"]) ? $_POST["field8"] : "";
|
||||
$grou = isset($_POST["group"]) ? $_POST["group"] : "";
|
||||
|
||||
if (strtolower(substr($tabl, 0, 7)) == "select ") {
|
||||
//! C'est directement une requête
|
||||
$sql = $tabl;
|
||||
$minisql = strtolower($sql);
|
||||
$poswhere = strpos($minisql, " where ");
|
||||
if ($poswhere === FALSE) {
|
||||
//! il n'y a pas de clause WHERE dans la requête
|
||||
//! on regarde s'il y a une clause ORDER BY pour pouvoir insérer la clause WHERE juste avant
|
||||
$posorder = strpos($minisql, " order by ");
|
||||
if ($posorder === FALSE) {
|
||||
//! il n'y a pas non plus de clause ORDER BY dans la requête, on ajoute le WHERE à la fin
|
||||
$posgroup = strpos($minisql, " group by ");
|
||||
if ($posgroup === FALSE) {
|
||||
$sql = str_replace(';', ' WHERE ' . $fiel . ' LIKE "%' . $term . '%";', $sql);
|
||||
} else {
|
||||
//! il y a une clause GROUP BY
|
||||
$sql = str_replace(' GROUP BY ', ' WHERE ' . $fiel . ' LIKE "%' . $term . '%" GROUP BY ', $sql);
|
||||
}
|
||||
} else {
|
||||
//! il y a une clause ORDER BY
|
||||
$sql = str_replace(' ORDER BY ', ' WHERE ' . $fiel . ' LIKE "%' . $term . '%" ORDER BY ', $sql);
|
||||
}
|
||||
} else {
|
||||
//! il y a déjà une condition WHERE dans la requête définie
|
||||
$sql = str_replace(' WHERE ', ' WHERE ' . $fiel . ' LIKE "%' . $term . '%" AND ', $sql);
|
||||
}
|
||||
} else {
|
||||
if ($grou == "") {
|
||||
$sql = 'SELECT * FROM ' . $tabl . ' WHERE ' . $fiel . ' LIKE "%' . $term . '%" ORDER BY ' . $fiel . ';';
|
||||
} else {
|
||||
$sql = 'SELECT * FROM ' . $tabl . ' WHERE ' . $fiel . ' LIKE "%' . $term . '%" GROUP BY ' . $fiel . ' ORDER BY ' . $fiel . ';';
|
||||
}
|
||||
}
|
||||
eLog("autocomplete : " . $sql);
|
||||
$res = qSQL($sql);
|
||||
$rows = array();
|
||||
while ($r = mysqli_fetch_assoc($res)) {
|
||||
$rows[] = $r;
|
||||
}
|
||||
echo json_encode($rows);
|
||||
exit();
|
||||
}
|
||||
break;
|
||||
// case "autocomplete" supprimé car non utilisé dans l'application
|
||||
// L'autocomplétion est gérée côté client JavaScript
|
||||
|
||||
case "get_context":
|
||||
//! Renvoie le contexte de l'utilisateur
|
||||
@@ -288,11 +249,23 @@ switch ($Route->_action) {
|
||||
//! Réception et lecture de la demande en json
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if (isset($data->cid)) {
|
||||
$cid = nettoie_input($data->cid);
|
||||
$sql = 'SELECT c.* FROM clients c WHERE c.rowid=' . $cid . ';';
|
||||
echo getinfos($sql, "gen", "json");
|
||||
// SÉCURITÉ : Validation de l'ID et requête préparée
|
||||
$cid = intval($data->cid);
|
||||
if ($cid <= 0) {
|
||||
echo json_encode(array('error' => 'ID client invalide'));
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$result = $db->getById('clients', $cid);
|
||||
echo json_encode($result ?: array());
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur load_client : " . $e->getMessage());
|
||||
echo json_encode(array('error' => 'Erreur lors du chargement du client'));
|
||||
}
|
||||
} else {
|
||||
echo "Erreur : pas de client";
|
||||
echo json_encode(array('error' => 'Pas de client spécifié'));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -301,14 +274,35 @@ switch ($Route->_action) {
|
||||
//! Réception et lecture de la demande en json
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if (isset($data->search)) {
|
||||
$search = nettoie_input($data->search);
|
||||
$sql = 'SELECT c.rowid, c.libelle, c.type_client, c.adresse1, c.cp, c.ville FROM clients c ';
|
||||
$sql .= 'WHERE c.libelle LIKE "%' . $search . '%" OR c.adresse1 LIKE "%' . $search . '%" OR c.cp LIKE "%' . $search . '%" OR c.ville LIKE "%' . $search . '%" OR c.contact_nom LIKE "%' . $search . '%" OR c.contact_prenom LIKE "%' . $search . '%" OR c.contact_fonction LIKE "%' . $search . '%" OR c.email LIKE "%' . $search . '%" ';
|
||||
$sql .= 'ORDER BY c.libelle;';
|
||||
echo getinfos($sql, "gen", "json");
|
||||
// SÉCURITÉ : Utilisation de requêtes préparées pour la recherche
|
||||
$search = trim($data->search);
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT c.rowid, c.libelle, c.type_client, c.adresse1, c.cp, c.ville FROM clients c
|
||||
WHERE c.libelle LIKE :search
|
||||
OR c.adresse1 LIKE :search
|
||||
OR c.cp LIKE :search
|
||||
OR c.ville LIKE :search
|
||||
OR c.contact_nom LIKE :search
|
||||
OR c.contact_prenom LIKE :search
|
||||
OR c.contact_fonction LIKE :search
|
||||
OR c.email LIKE :search
|
||||
ORDER BY c.libelle';
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$searchParam = '%' . $search . '%';
|
||||
$stmt->bindParam(':search', $searchParam, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode($results);
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur search_clients : " . $e->getMessage());
|
||||
echo json_encode(array('error' => 'Erreur lors de la recherche'));
|
||||
}
|
||||
} else {
|
||||
$ret = array('ret' => "ko");
|
||||
echo json_encode($ret);
|
||||
echo json_encode(array('ret' => "ko"));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -516,21 +510,36 @@ switch ($Route->_action) {
|
||||
//! Réception de l'id du marché à supprimer
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if (isset($data->cid)) {
|
||||
$cid = nettoie_input($data->cid);
|
||||
$sql = 'DELETE FROM marches m WHERE m.rowid=' . $cid . ';';
|
||||
qSQL($sql, "gen");
|
||||
eLog($sql);
|
||||
//! on supprime aussi la ligne dans la table marches_listes
|
||||
$sql = 'DELETE FROM marches_listes ml WHERE ml.fk_marche=' . $cid . ';';
|
||||
qSQL($sql, "gen");
|
||||
eLog($sql);
|
||||
//! on supprime aussi les lignes produits de ce marché dans la table produits
|
||||
$sql = 'DELETE FROM produits p WHERE p.fk_marche=' . $cid . ';';
|
||||
qSQL($sql, "gen");
|
||||
eLog($sql);
|
||||
|
||||
$ret = array('ret' => "ok", 'msg' => 'Marché supprimé');
|
||||
echo json_encode($ret);
|
||||
// SÉCURITÉ : Validation de l'ID comme entier
|
||||
$cid = intval($data->cid);
|
||||
if ($cid <= 0) {
|
||||
echo json_encode(array('ret' => "ko", 'msg' => 'ID invalide'));
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Utilisation de requêtes préparées pour la suppression
|
||||
$sql1 = 'DELETE FROM marches WHERE rowid = :id';
|
||||
$stmt1 = $db->prepare($sql1);
|
||||
$stmt1->execute(['id' => $cid]);
|
||||
|
||||
$sql2 = 'DELETE FROM marches_listes WHERE fk_marche = :id';
|
||||
$stmt2 = $db->prepare($sql2);
|
||||
$stmt2->execute(['id' => $cid]);
|
||||
|
||||
$sql3 = 'DELETE FROM produits WHERE fk_marche = :id';
|
||||
$stmt3 = $db->prepare($sql3);
|
||||
$stmt3->execute(['id' => $cid]);
|
||||
|
||||
eLog("Marché supprimé : ID=$cid");
|
||||
$ret = array('ret' => "ok", 'msg' => 'Marché supprimé');
|
||||
echo json_encode($ret);
|
||||
} catch (Exception $e) {
|
||||
eLog("Erreur suppression marché : " . $e->getMessage());
|
||||
echo json_encode(array('ret' => "ko", 'msg' => 'Erreur lors de la suppression'));
|
||||
}
|
||||
} else {
|
||||
$ret = array('ret' => "ko", 'msg' => 'Marché non supprimé');
|
||||
echo json_encode($ret);
|
||||
@@ -703,17 +712,32 @@ switch ($Route->_action) {
|
||||
//! Réception et lecture de la demande en json
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if (isset($data->cid)) {
|
||||
$cid = nettoie_input($data->cid);
|
||||
// TODO : Supprimer les devis créés par cet utilisateur
|
||||
|
||||
$sql = 'DELETE FROM users WHERE rowid=' . $cid . ';';
|
||||
eLog($sql);
|
||||
qSQL($sql, "gen");
|
||||
$ret = array('ret' => "ok");
|
||||
echo json_encode($ret);
|
||||
// SÉCURITÉ : Validation de l'ID comme entier
|
||||
$cid = intval($data->cid);
|
||||
if ($cid <= 0) {
|
||||
echo json_encode(array('ret' => "ko", 'msg' => 'ID invalide'));
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
// TODO : Supprimer les devis créés par cet utilisateur
|
||||
|
||||
// Utilisation de la fonction sécurisée deleteById
|
||||
$result = $db->deleteById('users', $cid);
|
||||
|
||||
if ($result) {
|
||||
eLog("Utilisateur supprimé : ID=$cid");
|
||||
echo json_encode(array('ret' => "ok"));
|
||||
} else {
|
||||
echo json_encode(array('ret' => "ko", 'msg' => 'Utilisateur non trouvé'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
eLog("Erreur suppression utilisateur : " . $e->getMessage());
|
||||
echo json_encode(array('ret' => "ko", 'msg' => 'Erreur lors de la suppression'));
|
||||
}
|
||||
} else {
|
||||
$ret = array('ret' => "ko");
|
||||
echo json_encode($ret);
|
||||
echo json_encode(array('ret' => "ko", 'msg' => 'ID manquant'));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -757,10 +781,20 @@ switch ($Route->_action) {
|
||||
$filename = "devis_" . $cid . "_" . date('Y_m_d_hi') . ".csv";
|
||||
$fields = array("Code", "Etablissement", "Adresse 1", "Adresse 2");
|
||||
|
||||
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2 FROM clients c WHERE c.rowid=' . $devis["fk_client"] . ';';
|
||||
eLog($sql);
|
||||
$cli = getinfos($sql, "gen");
|
||||
$client = $cli[0];
|
||||
// SÉCURITÉ : Utilisation de requête préparée pour l'ID client
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2 FROM clients c WHERE c.rowid = :id';
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([':id' => intval($devis["fk_client"])]);
|
||||
$client = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$client) {
|
||||
$client = ['code' => '', 'libelle' => '', 'adresse1' => '', 'adresse2' => ''];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur export_sap_devis : " . $e->getMessage());
|
||||
$client = ['code' => '', 'libelle' => '', 'adresse1' => '', 'adresse2' => ''];
|
||||
}
|
||||
|
||||
$excelData = implode("\t", array_values($fields)) . "\n";
|
||||
|
||||
@@ -840,13 +874,29 @@ switch ($Route->_action) {
|
||||
//! Réception et lecture de la demande en json
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if (isset($data->cid)) {
|
||||
$cid = nettoie_input($data->cid);
|
||||
$sql = 'DELETE FROM infos i WHERE i.rowid=' . $cid . ';';
|
||||
eLog($sql);
|
||||
qSQL($sql, "gen");
|
||||
|
||||
$ret = array('ret' => "ok");
|
||||
echo json_encode($ret);
|
||||
// SÉCURITÉ : Validation de l'ID comme entier
|
||||
$cid = intval($data->cid);
|
||||
if ($cid <= 0) {
|
||||
echo json_encode(array('ret' => "ko", 'msg' => 'ID invalide'));
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'DELETE FROM infos WHERE rowid = :id';
|
||||
$stmt = $db->prepare($sql);
|
||||
$result = $stmt->execute(['id' => $cid]);
|
||||
|
||||
if ($result) {
|
||||
eLog("Info supprimée : ID=$cid");
|
||||
echo json_encode(array('ret' => "ok"));
|
||||
} else {
|
||||
echo json_encode(array('ret' => "ko", 'msg' => 'Info non trouvée'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
eLog("Erreur suppression info : " . $e->getMessage());
|
||||
echo json_encode(array('ret' => "ko", 'msg' => 'Erreur lors de la suppression'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user