363 lines
18 KiB
PHP
363 lines
18 KiB
PHP
<?php
|
|
// ============================================================================
|
|
// fonctions_archivage.php - Fonctions communes à cron_archivage.php (traitement
|
|
// initial des archives reçues des CM5) et cron_quarantaine.php (réexamen des
|
|
// archives mises en quarantaine) - cf synthèse §32-39.
|
|
//
|
|
// Nécessite d'avoir déjà inclus config/config.php et config/fonctions_bdd.php,
|
|
// et que CLE_CHIFFREMENT_ARCHIVE, BDD_BATEAUX_HOST/PORT/USER/PASSWORD soient
|
|
// définis (via config/confidentiel.php).
|
|
// ============================================================================
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Répertoires partagés entre les deux scripts (cf synthèse §38) :
|
|
// - REPERTOIRE_VERSIONS_SQL : fichiers de migration SQL (lecture seule).
|
|
// - REPERTOIRE_QUARANTAINE : archives en attente d'une décision administrative
|
|
// (MMSI divergent, ou aucune affectation active) - écrit par
|
|
// cron_archivage.php, lu ET écrit par cron_quarantaine.php.
|
|
// - REPERTOIRE_ERREURS : archives rejetées suite à une erreur technique non
|
|
// transitoire - écrit par les deux scripts, à usage de suivi manuel.
|
|
// ----------------------------------------------------------------------------
|
|
const REPERTOIRE_VERSIONS_SQL = "/home/koriolan/versions/shipheart/ressources/sql";
|
|
const REPERTOIRE_QUARANTAINE = "/home/koriolan/bateaux/archives/quarantaines";
|
|
const REPERTOIRE_ERREURS = "/home/koriolan/bateaux/archives/erreurs";
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Résout l'affectation active d'un boîtier (par adresse MAC) - cf synthèse
|
|
// §38. Renvoie ['existe' => bool, 'id_bateau' => int|null]. 'existe' => false
|
|
// couvre deux cas distincts à cet endroit, traités de la même façon : boîtier
|
|
// jamais affecté (première connexion), ou affecté par le passé puis retiré
|
|
// (toutes ses lignes ont une Date_fin renseignée).
|
|
// ----------------------------------------------------------------------------
|
|
function Resoudre_affectation_active($db, $adresse_mac)
|
|
{
|
|
$requete = "SELECT ID_Bateau FROM TB_Affectations_bateaux WHERE Adresse_mac=? AND Date_fin IS NULL";
|
|
$stmt = $db->prepare($requete);
|
|
$stmt->bind_param('s', $adresse_mac);
|
|
$stmt->execute();
|
|
$stmt->bind_result($id_bateau);
|
|
$trouve = $stmt->fetch();
|
|
$stmt->close();
|
|
|
|
if (!$trouve) return ['existe' => false, 'id_bateau' => null];
|
|
return ['existe' => true, 'id_bateau' => $id_bateau];
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Déchiffre + décompresse le contenu brut d'une archive (même chaîne openssl,
|
|
// exactement, que côté CM5 - cf synthèse §33.2), puis extrait les champs de
|
|
// son en-tête (# [Champ=Valeur]). Renvoie ['ok'=>true, 'entete'=>[...],
|
|
// 'sql'=>string] ou ['ok'=>false, 'raison'=>string].
|
|
// Nécessite CLE_CHIFFREMENT_ARCHIVE (config/confidentiel.php) - même valeur,
|
|
// exactement, que côté CM5 (cf synthèse §35.6).
|
|
// ----------------------------------------------------------------------------
|
|
function Decrypter_archive($contenu_chiffre)
|
|
{
|
|
$fichier_enc = tempnam(sys_get_temp_dir(), 'arch_enc_');
|
|
$fichier_gz = $fichier_enc . '.gz';
|
|
file_put_contents($fichier_enc, $contenu_chiffre);
|
|
|
|
$commande = "openssl enc -aes-256-cbc -pbkdf2 -iter 10000 -md sha256 -d -in " . escapeshellarg($fichier_enc)
|
|
. " -out " . escapeshellarg($fichier_gz) . " -pass pass:" . escapeshellarg(CLE_CHIFFREMENT_ARCHIVE) . " 2>&1";
|
|
exec($commande, $sortie, $code_retour);
|
|
if ($code_retour !== 0)
|
|
{
|
|
unlink($fichier_enc);
|
|
return ['ok' => false, 'raison' => "échec du déchiffrement"];
|
|
}
|
|
|
|
$sql = @shell_exec("gzip -dc " . escapeshellarg($fichier_gz) . " 2>&1");
|
|
unlink($fichier_enc);
|
|
@unlink($fichier_gz);
|
|
|
|
if ($sql === null || $sql === '')
|
|
{
|
|
return ['ok' => false, 'raison' => "échec de la décompression"];
|
|
}
|
|
|
|
// Lecture des lignes d'en-tête (uniquement les toutes premières lignes '# [Champ=Valeur]') :
|
|
$entete = [];
|
|
foreach (explode("\n", $sql) as $ligne)
|
|
{
|
|
if (!preg_match('/^#\s*\[([A-Za-z_]+)=(.*)\]\s*$/', $ligne, $m)) break; // fin de l'en-tête dès la 1ère ligne qui ne correspond plus
|
|
$entete[$m[1]] = $m[2];
|
|
}
|
|
|
|
return ['ok' => true, 'entete' => $entete, 'sql' => $sql];
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Résout la génération active d'un bateau (cf synthèse §35.4). Renvoie
|
|
// ['existe'=>bool, 'cle'=>int|null, 'numero'=>int|null, 'nom_base'=>string|null]
|
|
// - 'existe'=>false signifie "aucune génération active" (tout premier bateau,
|
|
// cf synthèse §35.5 point 4), PAS une erreur.
|
|
// ----------------------------------------------------------------------------
|
|
function Resoudre_generation_active($db, $id_bateau)
|
|
{
|
|
$requete = "SELECT Cle_primaire, Numero_generation, Nom_base FROM TB_Generations_bateaux WHERE ID_Bateau=? AND Date_fin IS NULL";
|
|
$stmt = $db->prepare($requete);
|
|
$stmt->bind_param('i', $id_bateau);
|
|
$stmt->execute();
|
|
$stmt->bind_result($cle, $numero, $nom_base);
|
|
$trouve = $stmt->fetch();
|
|
$stmt->close();
|
|
|
|
if (!$trouve) return ['existe' => false, 'cle' => null, 'numero' => null, 'nom_base' => null];
|
|
return ['existe' => true, 'cle' => $cle, 'numero' => $numero, 'nom_base' => $nom_base];
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Détermine si cette archive doit démarrer une NOUVELLE génération (§35.5) :
|
|
// - 'factory' ou 'restauration' dans l'en-tête -> toujours une nouvelle
|
|
// génération, quelle que soit la génération active actuelle.
|
|
// - 'normale' mais aucune génération active -> tout premier bateau,
|
|
// création de la génération 1 (même traitement que factory/restauration
|
|
// à partir de ce point, cf synthèse §35.5).
|
|
// - 'normale' avec une génération active -> pas de nouvelle génération,
|
|
// traitement incrémental habituel dans la génération existante.
|
|
// ----------------------------------------------------------------------------
|
|
function Nouvelle_generation_necessaire($origine_bdd, $generation_active)
|
|
{
|
|
if ($origine_bdd === 'factory' || $origine_bdd === 'restauration') return true;
|
|
return !$generation_active['existe'];
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Résout le nom du fichier de migration SQL correspondant à une version
|
|
// logicielle donnée (Version_soft_no_package, transmis dans l'en-tête de
|
|
// chaque archive - cf synthèse §32.2). Renvoie le nom de fichier (string),
|
|
// ou false si la version est inconnue ou la ligne TB_Versions_sql absente.
|
|
// ----------------------------------------------------------------------------
|
|
// ----------------------------------------------------------------------------
|
|
// Compare le MMSI reçu à celui déjà enregistré pour ce bateau (cf synthèse
|
|
// §32.3). Si TB_Bateaux.MMSI est NULL, la première valeur reçue est affectée
|
|
// automatiquement (aucun risque, rien à contredire). Renvoie :
|
|
// ['statut' => 'ok']
|
|
// ['statut' => 'quarantaine', 'attendu' => ..., 'recu' => ...]
|
|
// ----------------------------------------------------------------------------
|
|
function Verifier_mmsi($db_principale, $id_bateau, $mmsi_recu)
|
|
{
|
|
$requete = "SELECT MMSI FROM TB_Bateaux WHERE Cle_primaire=?";
|
|
$stmt = $db_principale->prepare($requete);
|
|
$stmt->bind_param('i', $id_bateau);
|
|
$stmt->execute();
|
|
$stmt->bind_result($mmsi_attendu);
|
|
$stmt->fetch();
|
|
$stmt->close();
|
|
|
|
if ($mmsi_attendu === null)
|
|
{
|
|
$requete = "UPDATE TB_Bateaux SET MMSI=? WHERE Cle_primaire=?";
|
|
$stmt = $db_principale->prepare($requete);
|
|
$stmt->bind_param('si', $mmsi_recu, $id_bateau);
|
|
$stmt->execute();
|
|
$stmt->close();
|
|
// Auto-affectation : l'attendu devient le reçu (rien à contredire) - toujours renseigné en sortie,
|
|
// jamais NULL, pour ne jamais laisser planer une ambiguïté entre "MMSI inconnu" et "MMSI connu mais
|
|
// pas indiqué ici" (cf synthèse §39) :
|
|
return ['statut' => 'ok', 'recu' => $mmsi_recu, 'attendu' => $mmsi_recu];
|
|
}
|
|
|
|
if ($mmsi_attendu === $mmsi_recu) return ['statut' => 'ok', 'recu' => $mmsi_recu, 'attendu' => $mmsi_attendu];
|
|
|
|
return ['statut' => 'quarantaine', 'attendu' => $mmsi_attendu, 'recu' => $mmsi_recu];
|
|
}
|
|
|
|
function Resoudre_fichier_sql($db, $version_package)
|
|
{
|
|
$requete = "SELECT vs.Fichier
|
|
FROM TB_Versions v
|
|
JOIN TB_Versions_sql vs ON vs.Cle_primaire = v.ID_Version_sql
|
|
WHERE v.Nom = ?";
|
|
$stmt = $db->prepare($requete);
|
|
$stmt->bind_param('s', $version_package);
|
|
$stmt->execute();
|
|
$stmt->bind_result($fichier);
|
|
$trouve = $stmt->fetch();
|
|
$stmt->close();
|
|
|
|
if (!$trouve) return false;
|
|
return $fichier;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Remplace le nom de base littéral "shipheart" (utilisé dans Factory_structure
|
|
// pour le CM5 lui-même) par le vrai nom de la base par bateau/génération
|
|
// côté serveur (cf synthèse §35.4/36) - ancré précisément sur le contexte
|
|
// (DROP/CREATE DATABASE, USE), jamais un remplacement aveugle du mot : une
|
|
// valeur de configuration ("logo_shipheart.png") contient aussi ce mot sans
|
|
// aucun rapport avec un nom de base. Renvoie [contenu_adapte, nb_remplacements].
|
|
// ----------------------------------------------------------------------------
|
|
function Adapter_nom_base($contenu_sql, $nom_base_cible)
|
|
{
|
|
$nom_echappe = '`' . $nom_base_cible . '`';
|
|
$contenu_sql = preg_replace('/\bDATABASE IF EXISTS `?shipheart`?\b/', 'DATABASE IF EXISTS ' . $nom_echappe, $contenu_sql, -1, $n1);
|
|
$contenu_sql = preg_replace('/\bDATABASE IF NOT EXISTS `?shipheart`?\b/', 'DATABASE IF NOT EXISTS ' . $nom_echappe, $contenu_sql, -1, $n2);
|
|
$contenu_sql = preg_replace('/\bUSE `?shipheart`?\s*;/', 'USE ' . $nom_echappe . ';', $contenu_sql, -1, $n3);
|
|
return [$contenu_sql, $n1 + $n2 + $n3];
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Crée une nouvelle génération pour un bateau (cf synthèse §35.4/35.5/36) :
|
|
// calcule le nom de la nouvelle base, adapte et exécute le SQL fourni (déjà
|
|
// résolu par Extraire_zone_usine), clôture l'ancienne génération active si
|
|
// elle existe (jamais supprimée), puis enregistre la nouvelle dans
|
|
// TB_Generations_bateaux. Renvoie ['ok'=>bool, 'nom_base'=>string, ...].
|
|
// Exécution via le client mysql en ligne de commande (comme côté CM5,
|
|
// updater.php) - un bloc SQL avec DROP/CREATE DATABASE ne se prête pas bien
|
|
// à mysqli::multi_query.
|
|
// ----------------------------------------------------------------------------
|
|
function Creer_generation($db_principale, $id_bateau, $motif, $contenu_sql)
|
|
{
|
|
$generation_actuelle = Resoudre_generation_active($db_principale, $id_bateau);
|
|
$prochain_numero = $generation_actuelle['existe'] ? $generation_actuelle['numero'] + 1 : 1;
|
|
$nom_base = "ship2net_bateau_{$id_bateau}_{$prochain_numero}";
|
|
|
|
[$sql_adapte, $nb_remplacements] = Adapter_nom_base($contenu_sql, $nom_base);
|
|
if ($nb_remplacements < 1)
|
|
{
|
|
return ['ok' => false, 'raison' => "aucune substitution de nom de base effectuée - fichier SQL probablement mal formé"];
|
|
}
|
|
|
|
$fichier_temp = tempnam(sys_get_temp_dir(), 'usine_');
|
|
file_put_contents($fichier_temp, $sql_adapte);
|
|
$commande = "mysql --host=" . escapeshellarg(BDD_BATEAUX_HOST) . " --port=" . escapeshellarg(BDD_BATEAUX_PORT)
|
|
. " --user=" . escapeshellarg(BDD_BATEAUX_USER) . " --password=" . escapeshellarg(BDD_BATEAUX_PASSWORD)
|
|
. " < " . escapeshellarg($fichier_temp) . " 2>&1";
|
|
exec($commande, $sortie, $code_retour);
|
|
unlink($fichier_temp);
|
|
|
|
if ($code_retour !== 0)
|
|
{
|
|
return ['ok' => false, 'raison' => "échec de l'exécution SQL : " . implode(" | ", $sortie)];
|
|
}
|
|
|
|
if ($generation_actuelle['existe'])
|
|
{
|
|
$requete = "UPDATE TB_Generations_bateaux SET Date_fin=NOW() WHERE Cle_primaire=?";
|
|
$stmt = $db_principale->prepare($requete);
|
|
$stmt->bind_param('i', $generation_actuelle['cle']);
|
|
$stmt->execute();
|
|
$stmt->close();
|
|
}
|
|
|
|
$requete = "INSERT INTO TB_Generations_bateaux (ID_Bateau, Numero_generation, Nom_base, Motif) VALUES (?, ?, ?, ?)";
|
|
$stmt = $db_principale->prepare($requete);
|
|
$stmt->bind_param('iiss', $id_bateau, $prochain_numero, $nom_base, $motif);
|
|
if (!$stmt->execute())
|
|
{
|
|
$erreur = $db_principale->error;
|
|
$stmt->close();
|
|
return ['ok' => false, 'raison' => "base créée mais son enregistrement dans TB_Generations_bateaux a échoué : " . $erreur];
|
|
}
|
|
$stmt->close();
|
|
|
|
return ['ok' => true, 'nom_base' => $nom_base, 'numero' => $prochain_numero];
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Extrait la zone "usine" d'un fichier de migration (Factory_structure +
|
|
// Factory_data ensemble, jusqu'à la fin du fichier) - pour une génération
|
|
// neuve (cf synthèse §32.2, §35.5). Renvoie false si le marqueur est
|
|
// introuvable (fichier mal formé - ne devrait jamais arriver en pratique).
|
|
// ----------------------------------------------------------------------------
|
|
function Extraire_zone_usine($contenu_fichier)
|
|
{
|
|
if (!preg_match('/^#\s*Factory_structure\b/m', $contenu_fichier, $m, PREG_OFFSET_CAPTURE)) return false;
|
|
return substr($contenu_fichier, $m[0][1]);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Extrait les segments incrémentaux entre deux versions (exclusive pour le
|
|
// départ, inclusive pour la cible) - pour une mise à jour incrémentale d'une
|
|
// génération déjà existante (cf synthèse §32.2). $depart et $cible sont des
|
|
// noms de fichier ("shipheart_sql_AAMMJJ_HHMMSS.sql"), résolus au préalable
|
|
// via TB_Versions/TB_Versions_sql (bloc 4a). Si $depart === $cible, renvoie
|
|
// une chaîne vide (rien à migrer - cas normal si le bateau n'a pas été mis à
|
|
// jour depuis la dernière archive). Renvoie false si l'un des deux marqueurs
|
|
// est introuvable.
|
|
// ----------------------------------------------------------------------------
|
|
function Extraire_segments_incrementaux($contenu_fichier, $depart, $cible)
|
|
{
|
|
if ($depart === $cible) return '';
|
|
|
|
$motif_depart = '/^#\s*Fin de ' . preg_quote($depart, '/') . '\b.*$/m';
|
|
if (!preg_match($motif_depart, $contenu_fichier, $m_depart, PREG_OFFSET_CAPTURE)) return false;
|
|
$debut = $m_depart[0][1] + strlen($m_depart[0][0]);
|
|
|
|
$motif_cible = '/^#\s*Fin de ' . preg_quote($cible, '/') . '\b.*$/m';
|
|
if (!preg_match($motif_cible, $contenu_fichier, $m_cible, PREG_OFFSET_CAPTURE)) return false;
|
|
$fin = $m_cible[0][1] + strlen($m_cible[0][0]);
|
|
|
|
if ($fin <= $debut) return false; // Ordre incohérent (cible avant départ) - ne devrait jamais arriver, "pas d'update en arrière" (§32.2)
|
|
|
|
return substr($contenu_fichier, $debut, $fin - $debut);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Détermine la liste des tables fragmentées (incrémentales) d'une base bateau,
|
|
// par la MÊME méthode que côté CM5 (SHOW TABLES LIKE 'TB_Mesures%' + TB_Historique,
|
|
// cf sauvegarde_bdd()/bdd_admin.c) - jamais une liste codée en dur, pour rester
|
|
// robuste si une nouvelle table TB_Mesures_xxx apparaît un jour.
|
|
// ----------------------------------------------------------------------------
|
|
function Resoudre_tables_fragmentees($connexion_bateau)
|
|
{
|
|
$tables = ['TB_Historique'];
|
|
$resultat = $connexion_bateau->query("SHOW TABLES LIKE 'TB_Mesures%'");
|
|
while ($ligne = $resultat->fetch_row()) $tables[] = $ligne[0];
|
|
return $tables;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Charge le contenu réel d'une archive (déjà déchiffré) dans la base d'une
|
|
// génération : les tables permanentes sont vidées puis rechargées, les tables
|
|
// incrémentales (fragmentées) sont uniquement complétées (jamais vidées) -
|
|
// cf synthèse §32/37. Le contenu de l'archive (ses INSERT) complète donc
|
|
// naturellement les tables fragmentées et remplace les permanentes, sans
|
|
// avoir besoin de séparer les deux : seul le DELETE préalable fait la
|
|
// différence entre les deux comportements. Exécution via mysql CLI (comme
|
|
// Creer_generation), SET FOREIGN_KEY_CHECKS=0 par précaution (l'ordre des
|
|
// INSERT du dump ne respecte pas forcément les dépendances).
|
|
// ----------------------------------------------------------------------------
|
|
function Charger_contenu_archive($nom_base, $sql_archive)
|
|
{
|
|
$connexion_bateau = new mysqli(BDD_BATEAUX_HOST, BDD_BATEAUX_USER, BDD_BATEAUX_PASSWORD, $nom_base, (int)BDD_BATEAUX_PORT);
|
|
if ($connexion_bateau->connect_error)
|
|
{
|
|
return ['ok' => false, 'raison' => "connexion à {$nom_base} impossible : " . $connexion_bateau->connect_error];
|
|
}
|
|
|
|
$tables_fragmentees = Resoudre_tables_fragmentees($connexion_bateau);
|
|
|
|
$resultat_tables = $connexion_bateau->query("SHOW TABLES");
|
|
$toutes_tables = [];
|
|
while ($ligne = $resultat_tables->fetch_row()) $toutes_tables[] = $ligne[0];
|
|
$tables_permanentes = array_diff($toutes_tables, $tables_fragmentees);
|
|
|
|
$connexion_bateau->close();
|
|
|
|
$script = "SET FOREIGN_KEY_CHECKS=0;\n";
|
|
foreach ($tables_permanentes as $table)
|
|
{
|
|
$script .= "DELETE FROM `{$table}`;\n";
|
|
}
|
|
$script .= $sql_archive . "\n";
|
|
$script .= "SET FOREIGN_KEY_CHECKS=1;\n";
|
|
|
|
$fichier_temp = tempnam(sys_get_temp_dir(), 'charge_');
|
|
file_put_contents($fichier_temp, $script);
|
|
$commande = "mysql --host=" . escapeshellarg(BDD_BATEAUX_HOST) . " --port=" . escapeshellarg(BDD_BATEAUX_PORT)
|
|
. " --user=" . escapeshellarg(BDD_BATEAUX_USER) . " --password=" . escapeshellarg(BDD_BATEAUX_PASSWORD)
|
|
. " " . escapeshellarg($nom_base) . " < " . escapeshellarg($fichier_temp) . " 2>&1";
|
|
exec($commande, $sortie, $code_retour);
|
|
unlink($fichier_temp);
|
|
|
|
if ($code_retour !== 0)
|
|
{
|
|
return ['ok' => false, 'raison' => "échec du chargement : " . implode(" | ", $sortie)];
|
|
}
|
|
|
|
return ['ok' => true];
|
|
}
|