Files
Cleo/config/Database.php
Pierre 443b0509df 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
2025-09-12 20:25:48 +02:00

308 lines
9.9 KiB
PHP

<?php
class Database {
private static $instance = null;
private $pdo;
private $host;
private $dbname;
private $username;
private $password;
private $charset = 'utf8mb4';
private $options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
];
private function __construct() {
$this->loadEnvironment();
$this->connect();
}
private function loadEnvironment() {
$envFile = dirname(__DIR__) . '/.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!isset($_ENV[$name])) {
putenv(sprintf('%s=%s', $name, $value));
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
}
}
$this->host = $_ENV['DB_HOST'] ?? 'localhost';
$this->dbname = $_ENV['DB_DATABASE'] ?? 'cleo';
$this->username = $_ENV['DB_USERNAME'] ?? 'root';
$this->password = $_ENV['DB_PASSWORD'] ?? '';
}
private function connect() {
try {
$dsn = "mysql:host={$this->host};dbname={$this->dbname};charset={$this->charset}";
$this->pdo = new PDO($dsn, $this->username, $this->password, $this->options);
if ($_ENV['LOG_SQL'] ?? false) {
$this->logConnection(true);
}
} catch (PDOException $e) {
if ($_ENV['APP_DEBUG'] ?? false) {
throw new Exception("Erreur de connexion à la base de données: " . $e->getMessage());
} else {
throw new Exception("Erreur de connexion à la base de données");
}
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getPDO() {
return $this->pdo;
}
public function query($sql, $params = []) {
$start = microtime(true);
try {
if (empty($params)) {
$stmt = $this->pdo->query($sql);
} else {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
}
$this->logQuery($sql, $params, microtime(true) - $start);
return $stmt;
} catch (PDOException $e) {
$this->logError($sql, $params, $e->getMessage());
throw $e;
}
}
public function fetchAll($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt->fetchAll();
}
public function fetchOne($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt->fetch();
}
public function fetchColumn($sql, $params = [], $column = 0) {
$stmt = $this->query($sql, $params);
return $stmt->fetchColumn($column);
}
public function insert($table, $data) {
$columns = array_keys($data);
$values = array_map(function($col) { return ':' . $col; }, $columns);
$sql = sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
$table,
implode(', ', $columns),
implode(', ', $values)
);
$this->query($sql, $data);
return $this->pdo->lastInsertId();
}
public function update($table, $data, $where, $whereParams = []) {
$set = [];
foreach ($data as $column => $value) {
$set[] = "$column = :set_$column";
}
$sql = sprintf(
"UPDATE %s SET %s WHERE %s",
$table,
implode(', ', $set),
$where
);
$params = [];
foreach ($data as $column => $value) {
$params["set_$column"] = $value;
}
$params = array_merge($params, $whereParams);
$stmt = $this->query($sql, $params);
return $stmt->rowCount();
}
public function delete($table, $where, $params = []) {
$sql = "DELETE FROM $table WHERE $where";
$stmt = $this->query($sql, $params);
return $stmt->rowCount();
}
public function beginTransaction() {
return $this->pdo->beginTransaction();
}
public function commit() {
return $this->pdo->commit();
}
public function rollback() {
return $this->pdo->rollBack();
}
public function lastInsertId() {
return $this->pdo->lastInsertId();
}
private function logQuery($sql, $params, $executionTime) {
// Debug désactivé pour les requêtes SQL
return;
}
private function logError($sql, $params, $error) {
// On garde seulement le log d'erreur dans error_log, pas de debug
error_log("SQL Error: $error | Query: $sql");
}
private function logConnection($success) {
// Debug désactivé pour les connexions
return;
}
/**
* Récupère un enregistrement par ID de manière sécurisée
*/
public function getById($table, $id, $columns = '*') {
// Liste blanche des tables autorisées
$allowedTables = [
'clients', 'devis', 'devis_lignes', 'produits', 'marches',
'users', 'contacts', 'marches_listes', 'marches_produits',
'devis_histo', 'medias', 'y_pages', 'z_logs', 'z_sessions'
];
if (!in_array($table, $allowedTables)) {
throw new Exception("Table non autorisée : $table");
}
$sql = "SELECT $columns FROM $table WHERE rowid = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
}
/**
* Supprime un enregistrement par ID de manière sécurisée
*/
public function deleteById($table, $id) {
// Liste blanche des tables autorisées pour suppression
$allowedTables = [
'clients', 'devis', 'devis_lignes', 'contacts',
'devis_histo', 'medias', 'z_logs', 'z_sessions',
'users', 'infos', 'marches', 'marches_listes', 'produits'
];
if (!in_array($table, $allowedTables)) {
throw new Exception("Suppression non autorisée sur la table : $table");
}
$sql = "DELETE FROM $table WHERE rowid = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
return $stmt->execute();
}
/**
* Recherche sécurisée dans un champ
*/
public function searchByField($table, $field, $value, $orderBy = null) {
// Liste blanche des tables et colonnes autorisées
$allowedSearches = [
'clients' => ['libelle', 'raison_sociale', 'ville', 'cp', 'email', 'contact_nom', 'contact_prenom'],
'produits' => ['reference', 'designation', 'famille'],
'devis' => ['num_devis', 'num_facture', 'opportunite'],
'users' => ['username', 'firstname', 'lastname', 'email'],
'marches' => ['libelle', 'description']
];
if (!isset($allowedSearches[$table]) || !in_array($field, $allowedSearches[$table])) {
throw new Exception("Recherche non autorisée : $table.$field");
}
$sql = "SELECT * FROM $table WHERE $field LIKE :value";
if ($orderBy && in_array($orderBy, $allowedSearches[$table])) {
$sql .= " ORDER BY $orderBy";
}
$stmt = $this->pdo->prepare($sql);
$searchValue = '%' . $value . '%';
$stmt->bindParam(':value', $searchValue, PDO::PARAM_STR);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Fonction autocompleteSearch supprimée car non utilisée
// L'autocomplétion est gérée côté client dans l'application
}
function getinfos($sql, $dbn = "gen", $format = "normal") {
try {
$db = Database::getInstance();
$result = $db->fetchAll($sql);
if (strtolower($format) == "json") {
return json_encode($result);
}
return $result;
} catch (Exception $e) {
if ($_ENV['APP_DEBUG'] ?? false) {
error_log("Erreur getinfos: " . $e->getMessage());
}
return ($format == "json") ? json_encode([]) : [];
}
}
function qSQL($sql, $dbn = "gen", $lastid = false) {
try {
$db = Database::getInstance();
$queryType = strtoupper(substr(trim($sql), 0, 6));
if ($queryType === 'INSERT' || $queryType === 'UPDATE' || $queryType === 'DELETE') {
$stmt = $db->query($sql);
if ($lastid && $queryType === 'INSERT') {
return $db->lastInsertId();
}
return $stmt;
} else {
return $db->query($sql);
}
} catch (Exception $e) {
if ($_ENV['APP_DEBUG'] ?? false) {
error_log("Erreur qSQL: " . $e->getMessage());
}
return false;
}
}