- 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
308 lines
9.9 KiB
PHP
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;
|
|
}
|
|
} |