341 lines
18 KiB
PHP
341 lines
18 KiB
PHP
<?php
|
|
// ============================================================================
|
|
// fonctions_alertes.php - Fonctions communes du lot "Alertes" (synthèse §51), scripts NATIFS du VPS :
|
|
// sonnette.php (réception des coups de sonnette des CM5), gardien.php (chien de garde de connexion), et le
|
|
// traitement des alertes (étape D).
|
|
// ============================================================================
|
|
|
|
const REPERTOIRE_DEMANDES_ALERTES = "/var/www/demandes/alertes"; // Surveillé par ship2net-alertes.path (étape D)
|
|
const SEUIL_INJOIGNABLE_DEFAUT = 720; // secondes, si TB_Configuration.Seuil_injoignable absent
|
|
const PREFIXE_UNITE_GARDIEN = "ship2net-gardien-";
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Bateau correspondant à l'adresse NetBird d'un module (TB_Cartes.Adresse_netbird -> affectation active).
|
|
// L'adresse source d'une connexion NetBird ne peut pas être usurpée (chaque pair a sa clé WireGuard) : c'est
|
|
// l'identification de l'émetteur, sans mot de passe ni jeton.
|
|
// Retour : ['id_bateau' => int, 'adresse_mac' => string] ou null (module inconnu, ou non affecté à un bateau)
|
|
// ----------------------------------------------------------------------------
|
|
function Bateau_par_adresse_netbird($db, $adresse)
|
|
{
|
|
$stmt = $db->prepare("SELECT A.ID_Bateau, C.Adresse_mac FROM TB_Cartes C
|
|
JOIN TB_Affectations_bateaux A ON A.Adresse_mac = C.Adresse_mac AND A.Date_fin IS NULL
|
|
JOIN TB_Bateaux B ON B.Cle_primaire = A.ID_Bateau AND B.Validite = 1
|
|
WHERE C.Adresse_netbird = ?");
|
|
$stmt->bind_param('s', $adresse);
|
|
$stmt->execute();
|
|
$ligne = $stmt->get_result()->fetch_row();
|
|
$stmt->close();
|
|
return $ligne ? array('id_bateau' => (int)$ligne[0], 'adresse_mac' => $ligne[1]) : null;
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Valeur entière d'un réglage de TB_Configuration (base principale), ou $defaut.
|
|
// ----------------------------------------------------------------------------
|
|
function Reglage_entier($db, $nom, $defaut)
|
|
{
|
|
$stmt = $db->prepare("SELECT Valeur FROM TB_Configuration WHERE Nom=? AND Validite=1");
|
|
$stmt->bind_param('s', $nom);
|
|
$stmt->execute();
|
|
$ligne = $stmt->get_result()->fetch_row();
|
|
$stmt->close();
|
|
$valeur = $ligne ? (int)$ligne[0] : 0;
|
|
return ($valeur > 0) ? $valeur : $defaut;
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// (Ré)arme le chien de garde d'un bateau : une tâche ponctuelle systemd (de l'utilisateur koriolan), qui lancera
|
|
// gardien.php dans $secondes - sauf si elle est réarmée entre-temps par un nouveau signal. Aucune interrogation : une
|
|
// échéance repoussée à chaque signe de vie.
|
|
// Retour : true si l'échéance est programmée
|
|
// ----------------------------------------------------------------------------
|
|
function Rearmer_gardien($id_bateau, $secondes)
|
|
{
|
|
$unite = PREFIXE_UNITE_GARDIEN.(int)$id_bateau;
|
|
$environnement = Environnement_systemd_utilisateur();
|
|
// L'échéance précédente est annulée (sans erreur si elle n'existait pas ou était déjà échue) :
|
|
exec($environnement."systemctl --user stop ".escapeshellarg($unite.".timer")." 2>/dev/null");
|
|
exec($environnement."systemctl --user reset-failed ".escapeshellarg($unite.".service")." 2>/dev/null");
|
|
exec($environnement."systemd-run --user --quiet --collect --unit=".escapeshellarg($unite)
|
|
." --on-active=".(int)$secondes."s /usr/bin/php ".escapeshellarg(__DIR__."/gardien.php")." ".(int)$id_bateau." 2>&1",
|
|
$sortie, $code);
|
|
if ($code !== 0) error_log("Rearmer_gardien(".$id_bateau.") : ".implode(" | ", $sortie));
|
|
return ($code === 0);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Variables nécessaires à "systemctl --user" / "systemd-run --user" depuis un service système exécuté sous koriolan
|
|
// (le gestionnaire systemd de l'utilisateur tourne en permanence grâce à "loginctl enable-linger koriolan").
|
|
// ----------------------------------------------------------------------------
|
|
function Environnement_systemd_utilisateur()
|
|
{
|
|
$uid = posix_getuid();
|
|
return "XDG_RUNTIME_DIR=/run/user/".$uid." DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/".$uid."/bus ";
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Dépose une demande de traitement des alertes (et des archives) pour un bateau - traitée par ship2net-alertes.path.
|
|
// ----------------------------------------------------------------------------
|
|
function Demander_traitement_alertes($id_bateau)
|
|
{
|
|
if (!is_dir(REPERTOIRE_DEMANDES_ALERTES)) @mkdir(REPERTOIRE_DEMANDES_ALERTES, 02775, true);
|
|
return (@file_put_contents(REPERTOIRE_DEMANDES_ALERTES."/".(int)$id_bateau, gmdate('Y-m-d H:i:s')." UTC\n") !== false);
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// ÉTAPE D - Traitement des alertes (synthèse §51)
|
|
// ============================================================================
|
|
|
|
const PORT_API_CM5_ALERTES = 8080;
|
|
const EXPEDITEUR_ALERTES = "do_not_answer@koriolan.tech";
|
|
const NOM_EXPEDITEUR_ALERTES = "ShipHeart";
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Réglage texte de TB_Configuration (base principale), ou $defaut.
|
|
// ----------------------------------------------------------------------------
|
|
function Reglage_texte($db, $nom, $defaut)
|
|
{
|
|
$stmt = $db->prepare("SELECT Valeur FROM TB_Configuration WHERE Nom=? AND Validite=1");
|
|
$stmt->bind_param('s', $nom);
|
|
$stmt->execute();
|
|
$ligne = $stmt->get_result()->fetch_row();
|
|
$stmt->close();
|
|
return ($ligne && trim($ligne[0]) !== '') ? trim($ligne[0]) : $defaut;
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Date UTC ('Y-m-d H:i:s') affichée dans le fuseau des alertes (TB_Configuration.Fuseau_alertes, Europe/Paris par
|
|
// défaut) - les destinataires d'un e-mail ou d'un SMS n'ont pas de navigateur pour la convertir.
|
|
// ----------------------------------------------------------------------------
|
|
function Date_alerte($utc, $fuseau)
|
|
{
|
|
try { $d = new DateTime($utc, new DateTimeZone('UTC')); $d->setTimezone(new DateTimeZone($fuseau)); }
|
|
catch (Exception $e) { return $utc." UTC"; }
|
|
return $d->format('d/m/Y H:i');
|
|
}
|
|
|
|
function Duree_alerte($secondes)
|
|
{
|
|
$secondes = max(0, (int)$secondes);
|
|
if ($secondes < 60) return $secondes." s";
|
|
if ($secondes < 3600) return intdiv($secondes, 60)." min";
|
|
if ($secondes < 86400) return intdiv($secondes, 3600)." h ".sprintf("%02d", intdiv($secondes % 3600, 60))." min";
|
|
return intdiv($secondes, 86400)." j ".intdiv($secondes % 86400, 3600)." h";
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Destinataires d'un bateau : ShipMembers ayant un accès ACTIF, compte valide, et au moins un moyen choisi. Un SM ayant
|
|
// plusieurs accès au même bateau n'apparaît qu'une fois (moyens cumulés).
|
|
// Retour : tableau de ['id', 'nom', 'email', 'telephone', 'email_choisi', 'sms_choisi', 'sms_possible']
|
|
// ----------------------------------------------------------------------------
|
|
function Destinataires_alertes($db, $id_bateau)
|
|
{
|
|
$stmt = $db->prepare("SELECT M.ID, TRIM(CONCAT(IFNULL(M.Prenom,''), ' ', IFNULL(M.Nom,''))), M.Email, M.Telephone, M.Telephone_verifie,
|
|
MAX(L.Alertes_email), MAX(L.Alertes_sms)
|
|
FROM TB_Lien_bateau_shipmember L
|
|
JOIN TB_Shipmembers M ON M.ID = L.ID_Shipmember
|
|
WHERE L.ID_Bateau=? AND L.Statut='actif' AND L.Fin_validite >= NOW() AND M.Validite=1
|
|
AND (L.Alertes_email=1 OR L.Alertes_sms=1)
|
|
GROUP BY M.ID");
|
|
$stmt->bind_param('i', $id_bateau);
|
|
$stmt->execute();
|
|
$resultat = $stmt->get_result();
|
|
$destinataires = array();
|
|
while ($l = $resultat->fetch_row())
|
|
{
|
|
$destinataires[] = array('id' => (int)$l[0], 'nom' => $l[1], 'email' => $l[2], 'telephone' => $l[3],
|
|
'email_choisi' => ((int)$l[5] == 1), 'sms_choisi' => ((int)$l[6] == 1),
|
|
// SMS seulement vers un numéro vérifié, au format international :
|
|
'sms_possible' => ((int)$l[4] == 1 && preg_match('/^\+[1-9][0-9]{6,14}$/', (string)$l[3]) === 1));
|
|
}
|
|
$stmt->close();
|
|
return $destinataires;
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Enregistre une alerte, avec tout ce qu'il faut pour l'envoyer (objet, corps HTML de l'e-mail, texte du SMS, moyens
|
|
// prévus). Statut 'a_envoyer' pour une alerte à envoyer : elle ne passera à 'envoyee' qu'APRÈS les envois
|
|
// (Envoyer_alerte) - un arrêt entre les deux ne la perd pas, elle sera reprise (Reprendre_alertes_a_envoyer).
|
|
// La clé unique (bateau, event, type, date) empêche de traiter deux fois le même événement.
|
|
// Retour : l'ID de l'alerte créée, ou 0 si elle existait déjà (rien à faire)
|
|
// ----------------------------------------------------------------------------
|
|
function Creer_alerte($db, $id_bateau, $id_event, $nom_event, $type, $date_evenement, $statut,
|
|
$objet = null, $html = null, $texte_sms = null, $moyen_email = false, $moyen_sms = false)
|
|
{
|
|
$email = $moyen_email ? 1 : 0;
|
|
$sms = $moyen_sms ? 1 : 0;
|
|
$stmt = $db->prepare("INSERT IGNORE INTO TB_Alertes (ID_Bateau, ID_Event, Nom_event, Type, Date_evenement, Statut, Objet, Texte, Texte_sms, Moyen_email, Moyen_sms)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
$stmt->bind_param('iisssssssii', $id_bateau, $id_event, $nom_event, $type, $date_evenement, $statut, $objet, $html, $texte_sms, $email, $sms);
|
|
$stmt->execute();
|
|
$id = ($stmt->affected_rows == 1) ? (int)$db->insert_id : 0;
|
|
$stmt->close();
|
|
return $id;
|
|
}
|
|
|
|
function Statut_alerte($db, $id_alerte, $statut)
|
|
{
|
|
$stmt = $db->prepare("UPDATE TB_Alertes SET Statut=? WHERE ID=?");
|
|
$stmt->bind_param('si', $statut, $id_alerte);
|
|
$stmt->execute();
|
|
$stmt->close();
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Envoi d'un SMS d'alerte (étape F) : par l'API OVH (config/fonctions_sms.php). Le texte y est ramené à l'alphabet GSM
|
|
// et à 160 caractères. $reference : identifiant de l'envoi chez OVH.
|
|
// ----------------------------------------------------------------------------
|
|
function Envoyer_sms($numero, $texte, &$erreur, &$reference = null)
|
|
{
|
|
return Envoyer_sms_ovh($numero, $texte, $erreur, $reference);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Envoie une alerte 'a_envoyer' à ses destinataires, d'après son contenu ENREGISTRÉ, sur les moyens prévus pour elle ET
|
|
// choisis par chacun ; enregistre chaque envoi ; puis passe l'alerte à 'envoyee' (ou 'sans_destinataire').
|
|
// Reprise sans doublon : un destinataire déjà servi sur un moyen (envoi enregistré, réussi ou non) ne l'est pas à
|
|
// nouveau. (Un arrêt entre l'envoi d'un e-mail et son enregistrement pourrait, très rarement, le faire repartir : on
|
|
// préfère un message en double à un message perdu.)
|
|
// Retour : nombre de destinataires visés
|
|
// ----------------------------------------------------------------------------
|
|
function Envoyer_alerte($db, $id_alerte, $destinataires)
|
|
{
|
|
$stmt = $db->prepare("SELECT Objet, Texte, Texte_sms, Moyen_email, Moyen_sms FROM TB_Alertes WHERE ID=? AND Statut='a_envoyer'");
|
|
$stmt->bind_param('i', $id_alerte);
|
|
$stmt->execute();
|
|
$alerte = $stmt->get_result()->fetch_assoc();
|
|
$stmt->close();
|
|
if (!$alerte) return 0;
|
|
|
|
$texte_sms = (string)$alerte['Texte_sms']; // ramené à 160 caractères GSM par Envoyer_sms_ovh
|
|
|
|
$deja = function($id_sm, $canal) use ($db, $id_alerte) {
|
|
$stmt = $db->prepare("SELECT 1 FROM TB_Alertes_envois WHERE ID_Alerte=? AND ID_Shipmember=? AND Canal=?");
|
|
$stmt->bind_param('iis', $id_alerte, $id_sm, $canal);
|
|
$stmt->execute();
|
|
$trouve = ($stmt->get_result()->num_rows > 0);
|
|
$stmt->close();
|
|
return $trouve;
|
|
};
|
|
$enregistrer = function($id_sm, $canal, $destinataire, $ok, $erreur, $reference = null) use ($db, $id_alerte) {
|
|
$statut = $ok ? 'envoye' : 'echec';
|
|
$stmt = $db->prepare("INSERT IGNORE INTO TB_Alertes_envois (ID_Alerte, ID_Shipmember, Canal, Destinataire, Statut, Reference_fournisseur, Detail_erreur) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
$stmt->bind_param('iisssss', $id_alerte, $id_sm, $canal, $destinataire, $statut, $reference, $erreur);
|
|
$stmt->execute();
|
|
$stmt->close();
|
|
};
|
|
|
|
$vises = 0;
|
|
foreach ($destinataires as $d)
|
|
{
|
|
$vise = false;
|
|
if ($alerte['Moyen_email'] && $d['email_choisi'] && filter_var($d['email'], FILTER_VALIDATE_EMAIL))
|
|
{
|
|
$vise = true;
|
|
if (!$deja($d['id'], 'email'))
|
|
{
|
|
$erreur = null;
|
|
$ok = Envoi_email($d['nom'], $d['email'], (string)$alerte['Objet'], (string)$alerte['Texte'], NOM_EXPEDITEUR_ALERTES, EXPEDITEUR_ALERTES, array(), $erreur);
|
|
$enregistrer($d['id'], 'email', $d['email'], $ok, $ok ? null : (string)$erreur);
|
|
}
|
|
}
|
|
if ($alerte['Moyen_sms'] && $d['sms_choisi'] && $d['sms_possible'])
|
|
{
|
|
$vise = true;
|
|
if (!$deja($d['id'], 'sms'))
|
|
{
|
|
$erreur = null;
|
|
$reference = null;
|
|
$ok = Envoyer_sms($d['telephone'], $texte_sms, $erreur, $reference);
|
|
$enregistrer($d['id'], 'sms', $d['telephone'], $ok, $ok ? null : (string)$erreur, $reference);
|
|
}
|
|
}
|
|
if ($vise) $vises++;
|
|
}
|
|
Statut_alerte($db, $id_alerte, ($vises > 0) ? 'envoyee' : 'sans_destinataire');
|
|
return $vises;
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Reprise des alertes d'un bateau restées 'a_envoyer' (arrêt du script entre leur enregistrement et leur envoi).
|
|
// Retour : nombre d'alertes reprises
|
|
// ----------------------------------------------------------------------------
|
|
function Reprendre_alertes_a_envoyer($db, $id_bateau, $destinataires)
|
|
{
|
|
$stmt = $db->prepare("SELECT ID FROM TB_Alertes WHERE ID_Bateau=? AND Statut='a_envoyer' ORDER BY ID");
|
|
$stmt->bind_param('i', $id_bateau);
|
|
$stmt->execute();
|
|
$ids = array_map(fn($l) => (int)$l[0], $stmt->get_result()->fetch_all());
|
|
$stmt->close();
|
|
foreach ($ids as $id) Envoyer_alerte($db, $id, $destinataires);
|
|
return count($ids);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Lecture des déclenchements et fins d'alarmes sur le CM5 (API, route GET /alertes - étape B), à partir d'une date.
|
|
// Retour : tableau trié par date (['date', 'type' (1 déclenchement, 2 fin), 'id_event', 'nom', 'alarme_email',
|
|
// 'alarme_sms', 'texte_email', 'texte_sms']), ou null si le CM5 n'a pas répondu (ou ne connaît pas la route).
|
|
// ----------------------------------------------------------------------------
|
|
function Lire_alarmes_cm5($adresse_netbird, $depuis_utc)
|
|
{
|
|
$ch = curl_init("http://".$adresse_netbird.":".PORT_API_CM5_ALERTES."/alertes?depuis=".rawurlencode($depuis_utc));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
|
$reponse = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
curl_close($ch);
|
|
if ($reponse === false || $code !== 200) return null;
|
|
$donnees = json_decode($reponse, true);
|
|
if (!is_array($donnees) || ($donnees['status'] ?? '') !== 'ok' || !is_array($donnees['evenements'] ?? null)) return null;
|
|
$evenements = $donnees['evenements'];
|
|
usort($evenements, fn($a, $b) => strcmp((string)$a['date'], (string)$b['date']));
|
|
return $evenements;
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Fusible anti-rafales : nombre de déclenchements ET de fins d'un event dans les deux fenêtres (réglables) se terminant à
|
|
// $date_utc. Retour : le motif si un seuil est franchi, null sinon.
|
|
// ----------------------------------------------------------------------------
|
|
function Fusible_franchi($db, $id_bateau, $id_event, $date_utc)
|
|
{
|
|
foreach (array(array('Rafale_courte_nombre', 5, 'Rafale_courte_duree', 60), array('Rafale_longue_nombre', 15, 'Rafale_longue_duree', 3600)) as $s)
|
|
{
|
|
$nombre = Reglage_entier($db, $s[0], $s[1]);
|
|
$duree = Reglage_entier($db, $s[2], $s[3]);
|
|
$stmt = $db->prepare("SELECT SUM(Type='declenchement'), SUM(Type='fin') FROM TB_Alertes
|
|
WHERE ID_Bateau=? AND ID_Event=? AND Type IN ('declenchement','fin')
|
|
AND Date_evenement > ? - INTERVAL ? SECOND AND Date_evenement <= ?");
|
|
$stmt->bind_param('iisis', $id_bateau, $id_event, $date_utc, $duree, $date_utc);
|
|
$stmt->execute();
|
|
$l = $stmt->get_result()->fetch_row();
|
|
$stmt->close();
|
|
if ((int)$l[0] >= $nombre && (int)$l[1] >= $nombre) return (int)$l[0]." déclenchements et ".(int)$l[1]." fins en ".Duree_alerte($duree);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function Event_suspendu($db, $id_bateau, $id_event)
|
|
{
|
|
$stmt = $db->prepare("SELECT 1 FROM TB_Suspensions_alertes WHERE ID_Bateau=? AND ID_Event=? AND Date_reactivation IS NULL");
|
|
$stmt->bind_param('ii', $id_bateau, $id_event);
|
|
$stmt->execute();
|
|
$suspendu = ($stmt->get_result()->num_rows > 0);
|
|
$stmt->close();
|
|
return $suspendu;
|
|
}
|