Grosse évolution avec, entre autres, l'implémentation des alertes par SMS et email
This commit is contained in:
@@ -43,5 +43,10 @@ define("BDD_BATEAUX_DEV_DATABASE_PREFIXE", "");
|
|||||||
|
|
||||||
// --- Autres ---
|
// --- Autres ---
|
||||||
define("BDD_BATEAU_PASSWORD", "");
|
define("BDD_BATEAU_PASSWORD", "");
|
||||||
define("SMTP_PASSWORD", ""); // mot de passe de l'adresse email d'envoi (fonctions_mail.php)
|
define("SMTP_PASSWORD", "");
|
||||||
define("CLE_CHIFFREMENT_ARCHIVE", "");
|
define("CLE_CHIFFREMENT_ARCHIVE", "");
|
||||||
|
define("OVH_SMS_SERVICE", "");
|
||||||
|
define("OVH_SMS_EXPEDITEUR", "");
|
||||||
|
define("OVH_SMS_APPLICATION_KEY", "");
|
||||||
|
define("OVH_SMS_APPLICATION_SECRET", "");
|
||||||
|
define("OVH_SMS_CONSUMER_KEY", "");
|
||||||
@@ -10,5 +10,6 @@ require_once(__DIR__.'/fonctions_pdf.php'); // Contient les fonctions de cr
|
|||||||
require_once(__DIR__.'/fonctions_traductions.php');
|
require_once(__DIR__.'/fonctions_traductions.php');
|
||||||
require_once(__DIR__.'/fonctions_utilitaires.php');
|
require_once(__DIR__.'/fonctions_utilitaires.php');
|
||||||
require_once(__DIR__.'/fonctions_consultation.php'); // Consultation des données d'un bateau (bases de génération)
|
require_once(__DIR__.'/fonctions_consultation.php'); // Consultation des données d'un bateau (bases de génération)
|
||||||
|
require_once(__DIR__.'/fonctions_alertes_ship2net.php'); // Alertes : préférences, téléphone, alertes envoyées (synthèse §51)
|
||||||
|
require_once(__DIR__.'/fonctions_sms.php');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
//************************************************************************************************************
|
||||||
|
// fonctions_alertes_portail.php - Lot "Alertes", côté PORTAIL (synthèse §51, étape E) : préférences d'alerte de chaque
|
||||||
|
// ShipMember (bateau par bateau), numéro de téléphone (format international, vérification par code), alertes envoyées,
|
||||||
|
// suspensions par le fusible et leur réactivation.
|
||||||
|
// (Le traitement des alertes lui-même est fait par les scripts natifs du VPS : /var/www/fonctions_alertes.php.)
|
||||||
|
//************************************************************************************************************
|
||||||
|
|
||||||
|
const DUREE_VALIDITE_CODE_TELEPHONE = 600; // secondes
|
||||||
|
const ESSAIS_MAX_CODE_TELEPHONE = 5; // saisies erronées tolérées pour un même code
|
||||||
|
const DELAI_ENTRE_CODES_TELEPHONE = 60; // secondes minimum entre deux envois de code
|
||||||
|
|
||||||
|
// Indicatifs des pays les plus probables (numéro national commençant par 0 -> 0 remplacé par l'indicatif). Pour les
|
||||||
|
// autres pays, le numéro doit être saisi au format international (+…).
|
||||||
|
const INDICATIFS_PAYS = array('FR' => '33', 'BE' => '32', 'CH' => '41', 'LU' => '352', 'MC' => '377', 'ES' => '34', 'IT' => '39',
|
||||||
|
'DE' => '49', 'NL' => '31', 'PT' => '351', 'GB' => '44', 'IE' => '353', 'AT' => '43', 'GR' => '30', 'HR' => '385');
|
||||||
|
|
||||||
|
|
||||||
|
//************************************************************************************************************
|
||||||
|
// Fonction : Numéro de téléphone au format international E.164 (+33612345678), à partir d'une saisie libre
|
||||||
|
// ("06 12 34 56 78", "+33 6 12 34 56 78", "0033 6...", "(0)6...").
|
||||||
|
// Reçoit : La saisie, l'ID du pays du ShipMember (TB_Pays) pour les numéros nationaux
|
||||||
|
// Retour : Le numéro E.164, "" si la saisie est vide, ou false si elle n'est pas un numéro valide
|
||||||
|
//************************************************************************************************************
|
||||||
|
function Normaliser_telephone($saisie, $id_pays)
|
||||||
|
{
|
||||||
|
$saisie = trim((string)$saisie);
|
||||||
|
if ($saisie === '') return '';
|
||||||
|
$numero = preg_replace('/[\s.\-\/()]/', '', $saisie);
|
||||||
|
if (strncmp($numero, '00', 2) === 0) $numero = '+'.substr($numero, 2);
|
||||||
|
if ($numero !== '' && $numero[0] !== '+')
|
||||||
|
{
|
||||||
|
if ($numero[0] !== '0') return false;
|
||||||
|
$reponse = Requete_preparee_bdd("SELECT Code_alpha2 FROM TB_Pays WHERE ID=?", array(array('i', (int)$id_pays)), __FILE__, __LINE__);
|
||||||
|
$pays = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
|
||||||
|
Liberation_jeu_de_resultat_bdd($reponse);
|
||||||
|
$indicatif = INDICATIFS_PAYS[strtoupper((string)($pays['Code_alpha2'] ?? ''))] ?? null;
|
||||||
|
if ($indicatif === null) return false;
|
||||||
|
$numero = '+'.$indicatif.substr($numero, 1);
|
||||||
|
}
|
||||||
|
// "+33 (0)6..." : le 0 national après l'indicatif est retiré
|
||||||
|
$numero = preg_replace('/^\+(33|32|41|39|49|31|44|43|30|385|352|377|351|353|34)0/', '+$1', $numero);
|
||||||
|
if (preg_match('/^\+[1-9][0-9]{7,14}$/', $numero) !== 1) return false;
|
||||||
|
// Longueur exacte pour les pays dont le plan de numérotation est fixe (chiffres après l'indicatif) :
|
||||||
|
foreach (array('33' => '9', '34' => '9', '31' => '9', '351' => '9', '41' => '9', '32' => '8,9', '44' => '10') as $indicatif => $longueurs)
|
||||||
|
{
|
||||||
|
if (strncmp($numero, '+'.$indicatif, strlen($indicatif) + 1) === 0)
|
||||||
|
{
|
||||||
|
return in_array((string)(strlen($numero) - strlen($indicatif) - 1), explode(',', $longueurs), true) ? $numero : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $numero;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//************************************************************************************************************
|
||||||
|
// Fonction : Préférences d'alerte d'un ShipMember pour un bateau (sur ses accès actifs ou suspendus).
|
||||||
|
// Retour : ['email' => bool, 'sms' => bool, 'a_un_acces' => bool]
|
||||||
|
//************************************************************************************************************
|
||||||
|
function Preferences_alertes($id_shipmember, $id_bateau)
|
||||||
|
{
|
||||||
|
$reponse = Requete_preparee_bdd("SELECT COUNT(*) AS nb, MAX(Alertes_email) AS email, MAX(Alertes_sms) AS sms FROM TB_Lien_bateau_shipmember
|
||||||
|
WHERE ID_Shipmember=? AND ID_Bateau=? AND Statut IN ('actif','suspendu')",
|
||||||
|
array(array('i', $id_shipmember), array('i', $id_bateau)), __FILE__, __LINE__);
|
||||||
|
$d = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
|
||||||
|
Liberation_jeu_de_resultat_bdd($reponse);
|
||||||
|
return array('email' => ((int)$d['email'] == 1), 'sms' => ((int)$d['sms'] == 1), 'a_un_acces' => ((int)$d['nb'] > 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function Enregistrer_preferences_alertes($id_shipmember, $id_bateau, $email, $sms)
|
||||||
|
{
|
||||||
|
Requete_preparee_bdd("UPDATE TB_Lien_bateau_shipmember SET Alertes_email=?, Alertes_sms=?
|
||||||
|
WHERE ID_Shipmember=? AND ID_Bateau=? AND Statut IN ('actif','suspendu')",
|
||||||
|
array(array('i', $email ? 1 : 0), array('i', $sms ? 1 : 0), array('i', $id_shipmember), array('i', $id_bateau)), __FILE__, __LINE__);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//************************************************************************************************************
|
||||||
|
// Fonction : Téléphone d'un ShipMember et état de sa vérification.
|
||||||
|
// Retour : ['telephone', 'verifie' (bool), 'code_en_cours' (bool), 'id_pays']
|
||||||
|
//************************************************************************************************************
|
||||||
|
function Telephone_shipmember($id_shipmember)
|
||||||
|
{
|
||||||
|
$reponse = Requete_preparee_bdd("SELECT Telephone, Telephone_verifie, ID_PAYS, (Code_telephone_expiration > UTC_TIMESTAMP()) AS code_en_cours
|
||||||
|
FROM TB_Shipmembers WHERE ID=?", array(array('i', $id_shipmember)), __FILE__, __LINE__);
|
||||||
|
$d = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
|
||||||
|
Liberation_jeu_de_resultat_bdd($reponse);
|
||||||
|
return array('telephone' => (string)($d['Telephone'] ?? ''), 'verifie' => ((int)($d['Telephone_verifie'] ?? 0) == 1),
|
||||||
|
'code_en_cours' => ((int)($d['code_en_cours'] ?? 0) == 1), 'id_pays' => (int)($d['ID_PAYS'] ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//************************************************************************************************************
|
||||||
|
// Fonction : Enregistre le numéro d'un ShipMember (même champ que le profil). Un numéro MODIFIÉ n'est plus vérifié.
|
||||||
|
// Retour : '' si enregistré, sinon le message d'erreur
|
||||||
|
//************************************************************************************************************
|
||||||
|
function Enregistrer_telephone($id_shipmember, $saisie, $id_pays)
|
||||||
|
{
|
||||||
|
$numero = Normaliser_telephone($saisie, $id_pays);
|
||||||
|
if ($numero === false) return t("Numéro invalide : saisissez-le au format international (par exemple +33 6 12 34 56 78).");
|
||||||
|
// Ordre des affectations : la vérification est évaluée AVANT le changement du numéro
|
||||||
|
Requete_preparee_bdd("UPDATE TB_Shipmembers SET Telephone_verifie = IF(Telephone <=> ?, Telephone_verifie, 0),
|
||||||
|
Code_telephone_empreinte = IF(Telephone <=> ?, Code_telephone_empreinte, NULL), Telephone = ?
|
||||||
|
WHERE ID=?",
|
||||||
|
array(array('s', $numero), array('s', $numero), array('s', $numero), array('i', $id_shipmember)), __FILE__, __LINE__);
|
||||||
|
$_SESSION['shipmember']['Telephone'] = $numero;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//************************************************************************************************************
|
||||||
|
// Fonction : Envoie un code de vérification à 6 chiffres au numéro du ShipMember (valable 10 min ; seule son
|
||||||
|
// empreinte est enregistrée). Tant que les identifiants OVH ne sont pas renseignés (Sms_configure) : sur un
|
||||||
|
// site de TEST, le code est affiché à l'écran (pour pouvoir tester) ; en production, l'envoi est refusé.
|
||||||
|
// Retour : ['ok' => bool, 'message' => texte à afficher]
|
||||||
|
//************************************************************************************************************
|
||||||
|
function Envoyer_code_telephone($id_shipmember)
|
||||||
|
{
|
||||||
|
$telephone = Telephone_shipmember($id_shipmember);
|
||||||
|
if (preg_match('/^\+[1-9][0-9]{7,14}$/', $telephone['telephone']) !== 1) return array('ok' => false, 'message' => t("Enregistrez d'abord un numéro au format international."));
|
||||||
|
if ($telephone['verifie']) return array('ok' => false, 'message' => t("Ce numéro est déjà vérifié."));
|
||||||
|
|
||||||
|
// Pas plus d'un code par minute :
|
||||||
|
$reponse = Requete_preparee_bdd("SELECT (Code_telephone_expiration IS NOT NULL AND Code_telephone_expiration > UTC_TIMESTAMP() + INTERVAL ? SECOND) AS trop_tot
|
||||||
|
FROM TB_Shipmembers WHERE ID=?",
|
||||||
|
array(array('i', DUREE_VALIDITE_CODE_TELEPHONE - DELAI_ENTRE_CODES_TELEPHONE), array('i', $id_shipmember)), __FILE__, __LINE__);
|
||||||
|
$d = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
|
||||||
|
Liberation_jeu_de_resultat_bdd($reponse);
|
||||||
|
if ((int)$d['trop_tot'] == 1) return array('ok' => false, 'message' => t("Un code vient d'être envoyé : patientez une minute avant d'en demander un autre."));
|
||||||
|
|
||||||
|
$code = sprintf("%06d", random_int(0, 999999));
|
||||||
|
$erreur = null;
|
||||||
|
$envoye = false;
|
||||||
|
if (Sms_configure()) // config/fonctions_sms.php (étape F)
|
||||||
|
{
|
||||||
|
$envoye = Envoyer_sms_ovh($telephone['telephone'], str_replace("[[Code]]", $code, t("ShipHeart : votre code de vérification est [[Code]] (valable 10 min).")), $erreur);
|
||||||
|
// Le détail (réponse d'OVH) va au journal, pas à l'écran :
|
||||||
|
if (!$envoye) error_log("Envoyer_code_telephone(".(int)$id_shipmember.") : ".(string)$erreur);
|
||||||
|
if (!$envoye) return array('ok' => false, 'message' => t("Le SMS n'a pas pu être envoyé. Vérifiez le numéro, ou réessayez plus tard."));
|
||||||
|
}
|
||||||
|
elseif (PRODUCTION) return array('ok' => false, 'message' => t("L'envoi de SMS n'est pas disponible pour l'instant."));
|
||||||
|
|
||||||
|
Requete_preparee_bdd("UPDATE TB_Shipmembers SET Code_telephone_empreinte=?, Code_telephone_expiration=UTC_TIMESTAMP() + INTERVAL ? SECOND, Code_telephone_essais=0 WHERE ID=?",
|
||||||
|
array(array('s', hash('sha256', $code)), array('i', DUREE_VALIDITE_CODE_TELEPHONE), array('i', $id_shipmember)), __FILE__, __LINE__);
|
||||||
|
if ($envoye) return array('ok' => true, 'message' => t("Un code vient de vous être envoyé par SMS."));
|
||||||
|
return array('ok' => true, 'message' => str_replace("[[Code]]", $code, t("Site de test, envoi de SMS non configuré : votre code est [[Code]].")));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//************************************************************************************************************
|
||||||
|
// Fonction : Vérifie le code saisi (valide, non expiré, moins de ESSAIS_MAX_CODE_TELEPHONE erreurs).
|
||||||
|
// Retour : '' si le numéro est désormais vérifié, sinon le message d'erreur
|
||||||
|
//************************************************************************************************************
|
||||||
|
function Verifier_code_telephone($id_shipmember, $saisie)
|
||||||
|
{
|
||||||
|
$code = preg_replace('/\D/', '', (string)$saisie);
|
||||||
|
$reponse = Requete_preparee_bdd("SELECT Code_telephone_empreinte, Code_telephone_essais, (Code_telephone_expiration > UTC_TIMESTAMP()) AS valide
|
||||||
|
FROM TB_Shipmembers WHERE ID=?", array(array('i', $id_shipmember)), __FILE__, __LINE__);
|
||||||
|
$d = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
|
||||||
|
Liberation_jeu_de_resultat_bdd($reponse);
|
||||||
|
if (!$d || $d['Code_telephone_empreinte'] === null || (int)$d['valide'] != 1) return t("Aucun code en cours de validité : demandez-en un nouveau.");
|
||||||
|
if ((int)$d['Code_telephone_essais'] >= ESSAIS_MAX_CODE_TELEPHONE) return t("Trop d'essais : demandez un nouveau code.");
|
||||||
|
if (strlen($code) != 6 || !hash_equals($d['Code_telephone_empreinte'], hash('sha256', $code)))
|
||||||
|
{
|
||||||
|
Requete_preparee_bdd("UPDATE TB_Shipmembers SET Code_telephone_essais = Code_telephone_essais + 1 WHERE ID=?", array(array('i', $id_shipmember)), __FILE__, __LINE__);
|
||||||
|
return t("Code incorrect.");
|
||||||
|
}
|
||||||
|
Requete_preparee_bdd("UPDATE TB_Shipmembers SET Telephone_verifie=1, Code_telephone_empreinte=NULL, Code_telephone_expiration=NULL, Code_telephone_essais=0 WHERE ID=?",
|
||||||
|
array(array('i', $id_shipmember)), __FILE__, __LINE__);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//************************************************************************************************************
|
||||||
|
// Fonction : Dernières alertes d'un bateau, avec le nombre d'envois réussis et en échec.
|
||||||
|
// Retour : Tableau de lignes (Date_evenement, Type, Nom_event, Statut, Envoyes, Echecs)
|
||||||
|
//************************************************************************************************************
|
||||||
|
function Alertes_bateau($id_bateau, $limite = 30)
|
||||||
|
{
|
||||||
|
$alertes = array();
|
||||||
|
$reponse = Requete_preparee_bdd("SELECT A.ID, A.Date_evenement, A.Type, A.Nom_event, A.Statut,
|
||||||
|
(SELECT COUNT(*) FROM TB_Alertes_envois E WHERE E.ID_Alerte=A.ID AND E.Statut='envoye') AS Envoyes,
|
||||||
|
(SELECT COUNT(*) FROM TB_Alertes_envois E WHERE E.ID_Alerte=A.ID AND E.Statut='echec') AS Echecs
|
||||||
|
FROM TB_Alertes A WHERE A.ID_Bateau=? ORDER BY A.Date_evenement DESC, A.ID DESC LIMIT ?",
|
||||||
|
array(array('i', $id_bateau), array('i', $limite)), __FILE__, __LINE__);
|
||||||
|
while ($d = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC)) $alertes[] = $d;
|
||||||
|
Liberation_jeu_de_resultat_bdd($reponse);
|
||||||
|
return $alertes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Suspensions_actives($id_bateau)
|
||||||
|
{
|
||||||
|
$suspensions = array();
|
||||||
|
$reponse = Requete_preparee_bdd("SELECT ID, Nom_event, Date_suspension, Motif FROM TB_Suspensions_alertes
|
||||||
|
WHERE ID_Bateau=? AND Date_reactivation IS NULL ORDER BY Date_suspension DESC",
|
||||||
|
array(array('i', $id_bateau)), __FILE__, __LINE__);
|
||||||
|
while ($d = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC)) $suspensions[] = $d;
|
||||||
|
Liberation_jeu_de_resultat_bdd($reponse);
|
||||||
|
return $suspensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Réactivation (droits vérifiés par l'appelant) - la suspension doit appartenir au bateau et être encore active.
|
||||||
|
// Retour : true si réactivée
|
||||||
|
function Reactiver_suspension($id_suspension, $id_bateau, $id_shipmember)
|
||||||
|
{
|
||||||
|
Requete_preparee_bdd("UPDATE TB_Suspensions_alertes SET Date_reactivation=UTC_TIMESTAMP(), Reactivee_par=?
|
||||||
|
WHERE ID=? AND ID_Bateau=? AND Date_reactivation IS NULL",
|
||||||
|
array(array('i', $id_shipmember), array('i', $id_suspension), array('i', $id_bateau)), __FILE__, __LINE__);
|
||||||
|
return (Nb_lignes_modifiees() == 1);
|
||||||
|
}
|
||||||
@@ -20,9 +20,11 @@ require_once(__DIR__ . "/../vendor/autoload.php");
|
|||||||
// - fichier(s) joint(s) sous forme de tableau "$fichier_joints[N]['fichier']" et "$fichier_joints[N]['nom']"
|
// - fichier(s) joint(s) sous forme de tableau "$fichier_joints[N]['fichier']" et "$fichier_joints[N]['nom']"
|
||||||
// /!\ Le nombre maxi de fichier joints est limité à 10 (cf le code). Si vous en mettez plus, les derniers ne seront pas traités...
|
// /!\ Le nombre maxi de fichier joints est limité à 10 (cf le code). Si vous en mettez plus, les derniers ne seront pas traités...
|
||||||
// Mais on peut changer cette valeur sans soucis...
|
// Mais on peut changer cette valeur sans soucis...
|
||||||
// Retour : Rien
|
// - (facultatif, synthèse §51) une variable qui reçoit le message d'erreur de PHPMailer en cas d'échec
|
||||||
|
// Retour : true si l'e-mail a été accepté par le serveur SMTP, false sinon (les appels existants, qui ignoraient le retour,
|
||||||
|
// ne sont pas affectés)
|
||||||
//************************************************************************************************************
|
//************************************************************************************************************
|
||||||
function Envoi_email($nom_destinataire, $adresse_destinataire, $objet, $texte_brut, $nom_expediteur, $adresse_expediteur, $fichiers_joints)
|
function Envoi_email($nom_destinataire, $adresse_destinataire, $objet, $texte_brut, $nom_expediteur, $adresse_expediteur, $fichiers_joints, &$erreur = null)
|
||||||
{
|
{
|
||||||
global $message_information;
|
global $message_information;
|
||||||
|
|
||||||
@@ -94,10 +96,14 @@ global $message_information;
|
|||||||
if(!$mail->Send()) // Envoi de l'email et test du résultat
|
if(!$mail->Send()) // Envoi de l'email et test du résultat
|
||||||
{
|
{
|
||||||
// $message_information .= "Erreur lors de l'envoi de l'email. PHPMailer Error = [" . $mail->ErrorInfo."]";
|
// $message_information .= "Erreur lors de l'envoi de l'email. PHPMailer Error = [" . $mail->ErrorInfo."]";
|
||||||
|
$erreur = $mail->ErrorInfo;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// $message_information .= "L'email a été envoyé correctement.";
|
// $message_information .= "L'email a été envoyé correctement.";
|
||||||
|
$erreur = null;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================================
|
||||||
|
// fonctions_sms.php - Envoi de SMS par l'API OVHcloud (synthèse §51, étape F).
|
||||||
|
// Commun au portail (code de vérification du numéro) et aux scripts natifs (alertes) : inclus par config/fonctions.php.
|
||||||
|
//
|
||||||
|
// Identifiants : dans config/confidentiel.php (jamais versionné), constantes OVH_SMS_*. Tant qu'elles sont absentes ou
|
||||||
|
// vides, Sms_configure() renvoie false et aucun envoi n'est tenté (l'échec est enregistré comme tel).
|
||||||
|
//
|
||||||
|
// Requête : POST https://eu.api.ovh.com/1.0/sms/{service}/jobs, signée :
|
||||||
|
// X-Ovh-Signature = "$1$" . sha1(AS + "+" + CK + "+" + METHODE + "+" + URL + "+" + CORPS + "+" + HORODATAGE)
|
||||||
|
// L'horodatage doit être proche de l'heure des serveurs OVH (VPS synchronisé par NTP ; sinon : erreur "Invalid
|
||||||
|
// signature" ou "Query out of time"). La clé n'a besoin que des droits : GET /sms/{service} (crédits) et
|
||||||
|
// POST /sms/{service}/jobs (envoi).
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const OVH_API_RACINE = "https://eu.api.ovh.com/1.0";
|
||||||
|
const SMS_LONGUEUR_MAX = 160; // un seul SMS (alphabet GSM 7 bits) : ni découpage, ni surcoût
|
||||||
|
const SMS_VALIDITE_MINUTES = 2880; // téléphone éteint / hors réseau : remise tentée pendant 48 h
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Les identifiants OVH sont-ils renseignés ? (Expéditeur facultatif : sans nom d'expéditeur validé, OVH envoie depuis
|
||||||
|
// un numéro court.)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
function Sms_configure()
|
||||||
|
{
|
||||||
|
foreach (array('OVH_SMS_APPLICATION_KEY', 'OVH_SMS_APPLICATION_SECRET', 'OVH_SMS_CONSUMER_KEY', 'OVH_SMS_SERVICE') as $c)
|
||||||
|
if (!defined($c) || trim((string)constant($c)) === '') return false;
|
||||||
|
return function_exists('curl_init');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Ramène un texte à l'alphabet GSM 7 bits (accents sans équivalent remplacés par la lettre simple, typographie
|
||||||
|
// remplacée par l'équivalent ASCII, le reste par "?") et le limite à SMS_LONGUEUR_MAX (les caractères de l'extension
|
||||||
|
// GSM - € [ ] { } ^ ~ | \ - comptent double). Sans cela, un seul "ê" ferait passer tout le SMS en UCS-2 (70 caractères
|
||||||
|
// par SMS au lieu de 160).
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
function Texte_sms_gsm($texte)
|
||||||
|
{
|
||||||
|
static $base = "@£\$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà";
|
||||||
|
static $extension = "€[]{}^~|\\";
|
||||||
|
static $remplacements = array(
|
||||||
|
'â'=>'a', 'á'=>'a', 'ã'=>'a', 'ê'=>'e', 'ë'=>'e', 'î'=>'i', 'ï'=>'i', 'í'=>'i', 'ô'=>'o', 'ó'=>'o', 'õ'=>'o',
|
||||||
|
'û'=>'u', 'ú'=>'u', 'ç'=>'c', 'ÿ'=>'y', 'œ'=>'oe', 'À'=>'A', 'Â'=>'A', 'Á'=>'A', 'È'=>'E', 'Ê'=>'E', 'Ë'=>'E',
|
||||||
|
'Î'=>'I', 'Ï'=>'I', 'Ô'=>'O', 'Ù'=>'U', 'Û'=>'U', 'Œ'=>'OE', '’'=>"'", '‘'=>"'", '“'=>'"', '”'=>'"', '«'=>'"',
|
||||||
|
'»'=>'"', '–'=>'-', '—'=>'-', '…'=>'...', "\u{00A0}"=>' ', "\u{202F}"=>' ', "\t"=>' ', '°'=>' deg', '•'=>'-');
|
||||||
|
|
||||||
|
$texte = strtr((string)$texte, $remplacements);
|
||||||
|
preg_match_all('/./us', $texte, $caracteres);
|
||||||
|
$resultat = '';
|
||||||
|
$longueur = 0;
|
||||||
|
foreach ($caracteres[0] as $c)
|
||||||
|
{
|
||||||
|
if (strpos($extension, $c) !== false) $poids = 2;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$poids = 1;
|
||||||
|
if (strpos($base, $c) === false) $c = '?';
|
||||||
|
}
|
||||||
|
if ($longueur + $poids > SMS_LONGUEUR_MAX) break;
|
||||||
|
$resultat .= $c;
|
||||||
|
$longueur += $poids;
|
||||||
|
}
|
||||||
|
return $resultat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Appel signé de l'API OVH.
|
||||||
|
// Retour : ['code' => code HTTP (0 si pas de réponse), 'donnees' => réponse JSON décodée (ou null), 'erreur' => texte]
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
function Appel_api_ovh($methode, $chemin, $corps = null)
|
||||||
|
{
|
||||||
|
$url = OVH_API_RACINE.$chemin;
|
||||||
|
$json = ($corps === null) ? '' : json_encode($corps, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
$horodatage = (string)time();
|
||||||
|
$signature = '$1$'.sha1(OVH_SMS_APPLICATION_SECRET.'+'.OVH_SMS_CONSUMER_KEY.'+'.$methode.'+'.$url.'+'.$json.'+'.$horodatage);
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $methode);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||||
|
'Content-Type: application/json; charset=utf-8',
|
||||||
|
'X-Ovh-Application: '.OVH_SMS_APPLICATION_KEY,
|
||||||
|
'X-Ovh-Consumer: '.OVH_SMS_CONSUMER_KEY,
|
||||||
|
'X-Ovh-Timestamp: '.$horodatage,
|
||||||
|
'X-Ovh-Signature: '.$signature));
|
||||||
|
if ($corps !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
|
||||||
|
$reponse = curl_exec($ch);
|
||||||
|
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||||
|
$erreur_curl = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($reponse === false) return array('code' => 0, 'donnees' => null, 'erreur' => "OVH injoignable : ".$erreur_curl);
|
||||||
|
$donnees = json_decode($reponse, true);
|
||||||
|
if ($code !== 200)
|
||||||
|
{
|
||||||
|
$message = is_array($donnees) && isset($donnees['message']) ? $donnees['message'] : substr($reponse, 0, 200);
|
||||||
|
return array('code' => $code, 'donnees' => $donnees, 'erreur' => "OVH HTTP ".$code." : ".$message);
|
||||||
|
}
|
||||||
|
return array('code' => $code, 'donnees' => $donnees, 'erreur' => '');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Envoi d'un SMS à un numéro E.164 (+33612345678).
|
||||||
|
// $erreur : motif de l'échec (sinon null)
|
||||||
|
// $reference : identifiant de l'envoi chez OVH (pour le suivi dans l'espace client), sinon null
|
||||||
|
// Retour : true si OVH a accepté le numéro (la remise effective au téléphone n'est pas connue à ce stade)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
function Envoyer_sms_ovh($numero, $texte, &$erreur = null, &$reference = null)
|
||||||
|
{
|
||||||
|
$erreur = null;
|
||||||
|
$reference = null;
|
||||||
|
if (!Sms_configure()) { $erreur = "envoi de SMS non configuré (identifiants OVH absents)"; return false; }
|
||||||
|
if (preg_match('/^\+[1-9][0-9]{6,14}$/', (string)$numero) !== 1) { $erreur = "numéro invalide"; return false; }
|
||||||
|
|
||||||
|
$corps = array(
|
||||||
|
'message' => Texte_sms_gsm($texte),
|
||||||
|
'receivers' => array($numero),
|
||||||
|
'charset' => 'UTF-8',
|
||||||
|
'coding' => '7bit',
|
||||||
|
'noStopClause' => true, // SMS non commerciaux (alertes, codes) : pas de mention "STOP"
|
||||||
|
'priority' => 'high',
|
||||||
|
'validityPeriod' => SMS_VALIDITE_MINUTES);
|
||||||
|
$expediteur = defined('OVH_SMS_EXPEDITEUR') ? trim((string)OVH_SMS_EXPEDITEUR) : '';
|
||||||
|
if ($expediteur !== '') $corps['sender'] = $expediteur; // nom validé par OVH
|
||||||
|
else $corps['senderForResponse'] = true; // sinon : numéro court d'OVH
|
||||||
|
|
||||||
|
$r = Appel_api_ovh('POST', '/sms/'.rawurlencode(OVH_SMS_SERVICE).'/jobs', $corps);
|
||||||
|
if ($r['erreur'] !== '') { $erreur = $r['erreur']; return false; }
|
||||||
|
|
||||||
|
$valides = $r['donnees']['validReceivers'] ?? array();
|
||||||
|
if (!is_array($valides) || count($valides) == 0)
|
||||||
|
{
|
||||||
|
$erreur = "numéro refusé par OVH (".implode(', ', (array)($r['donnees']['invalidReceivers'] ?? array())).")";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$ids = $r['donnees']['ids'] ?? array();
|
||||||
|
if (is_array($ids) && count($ids) > 0) $reference = (string)$ids[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Crédits SMS restants sur le compte OVH, ou null (non configuré, erreur).
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
function Credits_sms_ovh(&$erreur = null)
|
||||||
|
{
|
||||||
|
$erreur = null;
|
||||||
|
if (!Sms_configure()) { $erreur = "envoi de SMS non configuré (identifiants OVH absents)"; return null; }
|
||||||
|
$r = Appel_api_ovh('GET', '/sms/'.rawurlencode(OVH_SMS_SERVICE));
|
||||||
|
if ($r['erreur'] !== '') { $erreur = $r['erreur']; return null; }
|
||||||
|
return isset($r['donnees']['creditsLeft']) ? (float)$r['donnees']['creditsLeft'] : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================================
|
||||||
|
// gardien.php - Échéance du chien de garde d'un bateau (synthèse §51, étape C).
|
||||||
|
// Lancé par une tâche ponctuelle systemd programmée par Rearmer_gardien() : si aucun signal n'est arrivé depuis le
|
||||||
|
// seuil, le bateau est déclaré injoignable (TB_Bateaux.Injoignable_depuis = date du dernier signal) et le traitement
|
||||||
|
// des alertes est demandé (alerte de perte de connexion, étape D).
|
||||||
|
// Usage : php gardien.php <numéro du bateau>
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
chdir("/var/www/html");
|
||||||
|
require_once __DIR__ . "/config/config.php";
|
||||||
|
require_once __DIR__ . "/fonctions_alertes.php";
|
||||||
|
|
||||||
|
$id_bateau = (int)($argv[1] ?? 0);
|
||||||
|
if ($id_bateau <= 0) { fwrite(STDERR, "Usage : php gardien.php <numéro du bateau>\n"); exit(1); }
|
||||||
|
|
||||||
|
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||||
|
$db = new mysqli("127.0.0.1", BDD_USER, BDD_PASSWORD, BDD_DATABASE, 3307);
|
||||||
|
$seuil = Reglage_entier($db, 'Seuil_injoignable', SEUIL_INJOIGNABLE_DEFAUT);
|
||||||
|
|
||||||
|
// Garde-fou (signal arrivé pendant le lancement de l'échéance, horloge...) : on revérifie l'ancienneté du dernier signal,
|
||||||
|
// et on ne déclare la perte qu'une fois (Injoignable_depuis encore NULL) :
|
||||||
|
$stmt = $db->prepare("UPDATE TB_Bateaux SET Injoignable_depuis = Date_dernier_signal
|
||||||
|
WHERE Cle_primaire=? AND Injoignable_depuis IS NULL AND Date_dernier_signal IS NOT NULL
|
||||||
|
AND Date_dernier_signal <= UTC_TIMESTAMP() - INTERVAL ? SECOND");
|
||||||
|
$stmt->bind_param('ii', $id_bateau, $seuil);
|
||||||
|
$stmt->execute();
|
||||||
|
$declare = ($stmt->affected_rows == 1);
|
||||||
|
$stmt->close();
|
||||||
|
$db->close();
|
||||||
|
|
||||||
|
if ($declare)
|
||||||
|
{
|
||||||
|
echo "Bateau {$id_bateau} : aucun signal depuis plus de {$seuil} s - déclaré injoignable.\n";
|
||||||
|
Demander_traitement_alertes($id_bateau);
|
||||||
|
}
|
||||||
|
else echo "Bateau {$id_bateau} : signal récent ou déjà déclaré injoignable - rien à faire.\n";
|
||||||
+171
-3
@@ -29,7 +29,7 @@ Init_session(basename($_SERVER['SCRIPT_NAME'])); // Fonction ouvrant la session
|
|||||||
if (!isset($_SESSION['Bateaux']['origine'])) $_SESSION['Bateaux']['origine'] = "";
|
if (!isset($_SESSION['Bateaux']['origine'])) $_SESSION['Bateaux']['origine'] = "";
|
||||||
if (!isset($_SESSION['Bateaux']['tri_liste'])) $_SESSION['Bateaux']['tri_liste'] = "Flotte";
|
if (!isset($_SESSION['Bateaux']['tri_liste'])) $_SESSION['Bateaux']['tri_liste'] = "Flotte";
|
||||||
if (!isset($_SESSION['Bateaux']['sens_tri_liste'])) $_SESSION['Bateaux']['sens_tri_liste'] = "ASC";
|
if (!isset($_SESSION['Bateaux']['sens_tri_liste'])) $_SESSION['Bateaux']['sens_tri_liste'] = "ASC";
|
||||||
if (!isset($_SESSION['Bateaux']['type_details_bateau'])) $_SESSION['Bateaux']['type_details_bateau'] = "resume"; // 'resume' ou 'gerer_les_acces'
|
if (!isset($_SESSION['Bateaux']['type_details_bateau'])) $_SESSION['Bateaux']['type_details_bateau'] = "resume"; // 'resume', 'gerer_les_acces' ou 'alertes'
|
||||||
if (!isset($_SESSION['Bateaux']['bateau']['Cle_primaire'])) $_SESSION['Bateaux']['bateau']['Cle_primaire'] = 0;
|
if (!isset($_SESSION['Bateaux']['bateau']['Cle_primaire'])) $_SESSION['Bateaux']['bateau']['Cle_primaire'] = 0;
|
||||||
if (!isset($_SESSION['Bateaux']['bateau']['Nom'])) $_SESSION['Bateaux']['bateau']['Nom'] = "";
|
if (!isset($_SESSION['Bateaux']['bateau']['Nom'])) $_SESSION['Bateaux']['bateau']['Nom'] = "";
|
||||||
if (!isset($_SESSION['Bateaux']['bateau']['ID_Flotte'])) $_SESSION['Bateaux']['bateau']['ID_Flotte'] = 0;
|
if (!isset($_SESSION['Bateaux']['bateau']['ID_Flotte'])) $_SESSION['Bateaux']['bateau']['ID_Flotte'] = 0;
|
||||||
@@ -148,7 +148,7 @@ Init_session(basename($_SERVER['SCRIPT_NAME'])); // Fonction ouvrant la session
|
|||||||
//===================================
|
//===================================
|
||||||
case "type_details_bateau":
|
case "type_details_bateau":
|
||||||
{
|
{
|
||||||
if (isset($_GET['type']) && in_array($_GET['type'], array('resume', 'gerer_les_acces'), true)) $_SESSION['Bateaux']['type_details_bateau'] = $_GET['type'];
|
if (isset($_GET['type']) && in_array($_GET['type'], array('resume', 'gerer_les_acces', 'alertes'), true)) $_SESSION['Bateaux']['type_details_bateau'] = $_GET['type'];
|
||||||
$_SESSION['Bateaux']['ancre'] = "";
|
$_SESSION['Bateaux']['ancre'] = "";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -530,6 +530,53 @@ $requete .= ' TB_Bateaux.Date_derniere_maj AS Date_derniere_maj, ';
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------------------------------------
|
||||||
|
// Alertes (synthèse §51, étape E) : chacun ne règle que SES préférences et SON numéro ; réactivation d'une
|
||||||
|
// suspension : administrateur, ou droit d'inviter sur ce bateau (Creation_profils). Redirection après action.
|
||||||
|
if (isset($_POST['enregistrer_preferences_alertes']) || isset($_POST['enregistrer_telephone']) || isset($_POST['envoyer_code_telephone'])
|
||||||
|
|| isset($_POST['verifier_code_telephone']) || isset($_POST['reactiver_suspension']))
|
||||||
|
{
|
||||||
|
$id_sm = (int)$_SESSION['shipmember']['Cle_primaire'];
|
||||||
|
if (isset($_POST['enregistrer_preferences_alertes']))
|
||||||
|
{
|
||||||
|
$sms = !empty($_POST['alertes_sms']);
|
||||||
|
Enregistrer_preferences_alertes($id_sm, $bateau['Cle_primaire'], !empty($_POST['alertes_email']), $sms);
|
||||||
|
$_SESSION['Bateaux']['flash_information'] = t("Vos préférences d'alerte sont enregistrées.");
|
||||||
|
if ($sms && !Telephone_shipmember($id_sm)['verifie'])
|
||||||
|
$_SESSION['Bateaux']['flash_information'] .= ' '.t("Les SMS ne partiront qu'une fois votre numéro vérifié (ci-dessous).");
|
||||||
|
}
|
||||||
|
else if (isset($_POST['enregistrer_telephone']))
|
||||||
|
{
|
||||||
|
$erreur = Enregistrer_telephone($id_sm, (string)($_POST['telephone'] ?? ''), Telephone_shipmember($id_sm)['id_pays']);
|
||||||
|
if ($erreur == '') $_SESSION['Bateaux']['flash_information'] = t("Numéro enregistré.");
|
||||||
|
else $_SESSION['Bateaux']['flash_erreur'] = $erreur;
|
||||||
|
}
|
||||||
|
else if (isset($_POST['envoyer_code_telephone']))
|
||||||
|
{
|
||||||
|
$resultat = Envoyer_code_telephone($id_sm);
|
||||||
|
$_SESSION['Bateaux'][$resultat['ok'] ? 'flash_information' : 'flash_erreur'] = $resultat['message'];
|
||||||
|
}
|
||||||
|
else if (isset($_POST['verifier_code_telephone']))
|
||||||
|
{
|
||||||
|
$erreur = Verifier_code_telephone($id_sm, (string)($_POST['code_telephone'] ?? ''));
|
||||||
|
if ($erreur == '') $_SESSION['Bateaux']['flash_information'] = t("Votre numéro est vérifié : vous recevrez les alertes par SMS.");
|
||||||
|
else $_SESSION['Bateaux']['flash_erreur'] = $erreur;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ($est_administrateur || (int)($acces_profil['Creation_profils'] ?? 0) == 1)
|
||||||
|
{
|
||||||
|
if (Reactiver_suspension((int)$_POST['reactiver_suspension'], $bateau['Cle_primaire'], $id_sm)) $_SESSION['Bateaux']['flash_information'] = t("Alertes réactivées pour tous les destinataires.");
|
||||||
|
else $_SESSION['Bateaux']['flash_erreur'] = t("Cette suspension n'existe pas ou a déjà été levée.");
|
||||||
|
}
|
||||||
|
else $_SESSION['Bateaux']['flash_erreur'] = t("Votre type d'accès ne permet pas de réactiver les alertes.");
|
||||||
|
}
|
||||||
|
$_SESSION['Bateaux']['type_details_bateau'] = "alertes";
|
||||||
|
Fermeture_bdd();
|
||||||
|
header('Location: bateaux.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($_POST['creer_invitation']))
|
if (isset($_POST['creer_invitation']))
|
||||||
{
|
{
|
||||||
if (!$peut_inviter) $_SESSION['Bateaux']['flash_erreur'] = t("Vous n'avez pas le droit d'inviter sur ce bateau.");
|
if (!$peut_inviter) $_SESSION['Bateaux']['flash_erreur'] = t("Vous n'avez pas le droit d'inviter sur ce bateau.");
|
||||||
@@ -644,6 +691,19 @@ $requete .= ' TB_Bateaux.Date_derniere_maj AS Date_derniere_maj, ';
|
|||||||
Liberation_jeu_de_resultat_bdd($reponse);
|
Liberation_jeu_de_resultat_bdd($reponse);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
//-------------------------------------------------------------------------------------------------------
|
||||||
|
// Alertes (synthèse §51, étape E) :
|
||||||
|
case "alertes":
|
||||||
|
{
|
||||||
|
$vue_alertes = array(
|
||||||
|
'preferences' => Preferences_alertes($_SESSION['shipmember']['Cle_primaire'], $bateau['Cle_primaire']),
|
||||||
|
'telephone' => Telephone_shipmember($_SESSION['shipmember']['Cle_primaire']),
|
||||||
|
'alertes' => Alertes_bateau($bateau['Cle_primaire'], 30),
|
||||||
|
'suspensions' => Suspensions_actives($bateau['Cle_primaire']),
|
||||||
|
'peut_reactiver' => ($est_administrateur || (int)($acces_profil['Creation_profils'] ?? 0) == 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
//-------------------------------------------------------------------------------------------------------
|
//-------------------------------------------------------------------------------------------------------
|
||||||
// Données du bateau (synthèse §50) : lues dans la base de la génération consultée (active par défaut), par
|
// Données du bateau (synthèse §50) : lues dans la base de la génération consultée (active par défaut), par
|
||||||
// une connexion dédiée - la connexion principale du site n'est pas touchée.
|
// une connexion dédiée - la connexion principale du site n'est pas touchée.
|
||||||
@@ -886,13 +946,121 @@ $requete .= ' TB_Bateaux.Date_derniere_maj AS Date_derniere_maj, ';
|
|||||||
echo '<div class="boutons_haut_de_page">';
|
echo '<div class="boutons_haut_de_page">';
|
||||||
echo '<div>';
|
echo '<div>';
|
||||||
echo '<a title="" href="bateaux.php?action=type_details_bateau&type=resume"';
|
echo '<a title="" href="bateaux.php?action=type_details_bateau&type=resume"';
|
||||||
if ($_SESSION['Bateaux']['type_details_bateau'] != "gerer_les_acces") echo ' class="actif" '; echo '>'.t("Données du bateau").'</a>';
|
if ($_SESSION['Bateaux']['type_details_bateau'] == "resume") echo ' class="actif" '; echo '>'.t("Données du bateau").'</a>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '<div>';
|
||||||
|
echo '<a title="" href="bateaux.php?action=type_details_bateau&type=alertes"';
|
||||||
|
if ($_SESSION['Bateaux']['type_details_bateau'] == "alertes") echo ' class="actif" '; echo '>'.t("Alertes").'</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
||||||
echo '<div class="corps_details_bateau">';
|
echo '<div class="corps_details_bateau">';
|
||||||
switch($_SESSION['Bateaux']['type_details_bateau'])
|
switch($_SESSION['Bateaux']['type_details_bateau'])
|
||||||
{
|
{
|
||||||
|
//===========================================================
|
||||||
|
// Alertes (synthèse §51, étape E)
|
||||||
|
case "alertes":
|
||||||
|
{
|
||||||
|
$p = $vue_alertes['preferences'];
|
||||||
|
$tel = $vue_alertes['telephone'];
|
||||||
|
echo '<div class="alertes">';
|
||||||
|
|
||||||
|
// --- Mes alertes pour ce bateau ---
|
||||||
|
echo '<section class="bloc_alertes">';
|
||||||
|
echo '<h2>'.t("Mes alertes pour ce bateau").'</h2>';
|
||||||
|
if (!$p['a_un_acces']) echo '<p>'.t("Vous n'avez pas d'accès à ce bateau : vous ne recevez pas ses alertes.").'</p>';
|
||||||
|
else
|
||||||
|
{
|
||||||
|
echo '<p class="explication">'.t("Vous recevez les alarmes prévues par e-mail ou par SMS dans la configuration du bateau, sur les moyens cochés ci-dessous, ainsi que la perte et le retour de sa connexion.").'</p>';
|
||||||
|
echo '<form action="bateaux.php" method="post" class="preferences_alertes">'.Champ_jeton_csrf();
|
||||||
|
echo '<label><input type="checkbox" name="alertes_email" value="1"'.($p['email'] ? ' checked' : '').'> '.t("Par e-mail").'</label>';
|
||||||
|
echo '<label><input type="checkbox" name="alertes_sms" value="1"'.($p['sms'] ? ' checked' : '').'> '.t("Par SMS").'</label>';
|
||||||
|
echo '<button type="submit" name="enregistrer_preferences_alertes">'.t("Enregistrer").'</button>';
|
||||||
|
echo '</form>';
|
||||||
|
if ($p['sms'] && !$tel['verifie']) echo '<p class="avertissement">'.t("SMS en attente : votre numéro doit être vérifié pour que les SMS partent.").'</p>';
|
||||||
|
}
|
||||||
|
echo '</section>';
|
||||||
|
|
||||||
|
// --- Mon numéro de téléphone (dès que les SMS sont cochés, ou qu'un numéro existe) ---
|
||||||
|
if ($p['a_un_acces'] && ($p['sms'] || $tel['telephone'] != ''))
|
||||||
|
{
|
||||||
|
echo '<section class="bloc_alertes">';
|
||||||
|
echo '<h2>'.t("Mon numéro pour les SMS").'</h2>';
|
||||||
|
echo '<form action="bateaux.php" method="post" class="telephone_alertes">'.Champ_jeton_csrf();
|
||||||
|
echo '<input type="tel" name="telephone" value="'.htmlspecialchars($tel['telephone']).'" placeholder="+33 6 12 34 56 78" maxlength="24">';
|
||||||
|
echo '<button type="submit" name="enregistrer_telephone">'.t("Enregistrer").'</button>';
|
||||||
|
echo $tel['verifie'] ? '<span class="statut statut_actif">'.t("Vérifié").'</span>' : '<span class="statut statut_suspendu">'.t("Non vérifié").'</span>';
|
||||||
|
echo '</form>';
|
||||||
|
if (!$tel['verifie'] && $tel['telephone'] != '')
|
||||||
|
{
|
||||||
|
echo '<form action="bateaux.php" method="post" class="code_telephone">'.Champ_jeton_csrf();
|
||||||
|
echo '<button type="submit" name="envoyer_code_telephone">'.($tel['code_en_cours'] ? t("Renvoyer un code") : t("Recevoir un code par SMS")).'</button>';
|
||||||
|
echo '</form>';
|
||||||
|
if ($tel['code_en_cours'])
|
||||||
|
{
|
||||||
|
echo '<form action="bateaux.php" method="post" class="code_telephone">'.Champ_jeton_csrf();
|
||||||
|
echo '<input type="text" name="code_telephone" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="123456">';
|
||||||
|
echo '<button type="submit" name="verifier_code_telephone">'.t("Valider le code").'</button>';
|
||||||
|
echo '</form>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo '<p class="explication">'.t("Ce numéro est celui de votre profil : le modifier ici ou dans votre profil demande une nouvelle vérification.").'</p>';
|
||||||
|
echo '</section>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Alertes suspendues ---
|
||||||
|
if (count($vue_alertes['suspensions']) > 0)
|
||||||
|
{
|
||||||
|
echo '<section class="bloc_alertes">';
|
||||||
|
echo '<h2>'.t("Alertes suspendues").'</h2>';
|
||||||
|
echo '<p class="explication">'.t("Une alarme qui se déclenche et s'arrête trop souvent suspend ses alertes (e-mail et SMS) pour tous les destinataires : sa configuration est à vérifier. Le son, la LED et les messages à bord ne sont pas concernés.").'</p>';
|
||||||
|
echo '<table>';
|
||||||
|
foreach ($vue_alertes['suspensions'] as $suspension)
|
||||||
|
{
|
||||||
|
echo '<tr><td class="date_alerte"><span data-date-utc="'.htmlspecialchars(str_replace(' ', 'T', $suspension['Date_suspension']).'Z').'"></span></td>';
|
||||||
|
echo '<td class="nom_alerte">'.htmlspecialchars((string)$suspension['Nom_event']).'</td><td>'.htmlspecialchars($suspension['Motif']).'</td><td>';
|
||||||
|
if ($vue_alertes['peut_reactiver'])
|
||||||
|
{
|
||||||
|
echo '<form action="bateaux.php" method="post">'.Champ_jeton_csrf()
|
||||||
|
.'<button type="submit" name="reactiver_suspension" value="'.(int)$suspension['ID'].'">'.t("Réactiver").'</button></form>';
|
||||||
|
}
|
||||||
|
echo '</td></tr>';
|
||||||
|
}
|
||||||
|
echo '</table>';
|
||||||
|
echo '</section>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Alertes envoyées ---
|
||||||
|
echo '<section class="bloc_alertes">';
|
||||||
|
echo '<h2>'.t("Dernières alertes").'</h2>';
|
||||||
|
if (count($vue_alertes['alertes']) == 0) echo '<p class="explication">'.t("Aucune alerte pour ce bateau.").'</p>';
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$libelles_types = array('declenchement' => t("Alarme"), 'fin' => t("Fin d'alarme"), 'perte_connexion' => t("Connexion perdue"),
|
||||||
|
'retour_connexion' => t("Connexion rétablie"), 'suspension' => t("Alertes suspendues"));
|
||||||
|
$libelles_statuts = array('a_envoyer' => t("en cours d'envoi"), 'envoyee' => t("envoyée"), 'pendant_coupure' => t("pendant une coupure (comptée)"),
|
||||||
|
'suspendue' => t("non envoyée (suspendue)"), 'sans_destinataire' => t("sans destinataire"));
|
||||||
|
echo '<table>';
|
||||||
|
echo '<tr><th>'.t("Date").'</th><th>'.t("Type").'</th><th>'.t("Alarme").'</th><th>'.t("Envoi").'</th></tr>';
|
||||||
|
foreach ($vue_alertes['alertes'] as $alerte)
|
||||||
|
{
|
||||||
|
$envoi = $libelles_statuts[$alerte['Statut']] ?? $alerte['Statut'];
|
||||||
|
if ($alerte['Statut'] == 'envoyee') $envoi .= ' ('.(int)$alerte['Envoyes'].((int)$alerte['Echecs'] > 0 ? ', '.(int)$alerte['Echecs'].' '.t("échec(s)") : '').')';
|
||||||
|
echo '<tr class="alerte_'.htmlspecialchars($alerte['Type']).'">';
|
||||||
|
echo '<td class="date_alerte"><span data-date-utc="'.htmlspecialchars(str_replace(' ', 'T', $alerte['Date_evenement']).'Z').'"></span></td>';
|
||||||
|
echo '<td>'.($libelles_types[$alerte['Type']] ?? htmlspecialchars($alerte['Type'])).'</td>';
|
||||||
|
echo '<td class="nom_alerte">'.htmlspecialchars((string)$alerte['Nom_event']).'</td>';
|
||||||
|
echo '<td>'.$envoi.'</td>';
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
echo '</table>';
|
||||||
|
}
|
||||||
|
echo '</section>';
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
//===========================================================
|
//===========================================================
|
||||||
case "gerer_les_acces":
|
case "gerer_les_acces":
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -77,6 +77,19 @@ Fichier bateaux.php
|
|||||||
background-color: var(--bleu_koriolan);
|
background-color: var(--bleu_koriolan);
|
||||||
color: var(--bleu_koriolan_fonce);
|
color: var(--bleu_koriolan_fonce);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#Bateaux #details_bateau .graphique .top_part_graph .vars .var{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .graphique .top_part_graph .vars .var.actif {
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: var(--bleu_koriolan_transparent);
|
||||||
|
color: var(--bleu_koriolan_fonce);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.page_principale {
|
.page_principale {
|
||||||
width: 80% !important;
|
width: 80% !important;
|
||||||
max-width: unset !important;
|
max-width: unset !important;
|
||||||
@@ -109,6 +122,23 @@ Fichier bateaux.php
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Bateaux #details_bateau .onglets a {
|
||||||
|
background-color: var(--bleu_koriolan_fonce);
|
||||||
|
border-radius: 1em 1em 0 0;
|
||||||
|
color: var(--blanc);
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 20px;
|
||||||
|
margin: 1px 1px 0px 1px;
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .onglets a:hover {
|
||||||
|
color: var(--bleu_koriolan);
|
||||||
|
background-color: var(--bleu_koriolan_fonce);
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .onglets a.actif {
|
||||||
|
background-color: var(--bleu_koriolan);
|
||||||
|
color: var(--bleu_koriolan_fonce);
|
||||||
|
}
|
||||||
|
|
||||||
#Bateaux #details_bateau .corps_details_bateau {
|
#Bateaux #details_bateau .corps_details_bateau {
|
||||||
background-color: var(--blanc_casse);
|
background-color: var(--blanc_casse);
|
||||||
}
|
}
|
||||||
@@ -132,6 +162,105 @@ Fichier bateaux.php
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
/* background-color: var(--blanc_casse); */
|
/* background-color: var(--blanc_casse); */
|
||||||
}
|
}
|
||||||
|
.red {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.orange {
|
||||||
|
color: #ffa600;
|
||||||
|
}
|
||||||
|
.green {
|
||||||
|
color: #02a602;
|
||||||
|
}
|
||||||
|
.capteur_state{
|
||||||
|
display: block;
|
||||||
|
padding: 0px;
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
width: 80px;
|
||||||
|
text-align: center !important;
|
||||||
|
font-size: 1.5em;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.capteur_off{
|
||||||
|
background-color: rgba(255, 30, 0, 0.2);
|
||||||
|
color: rgb(255,30,0);
|
||||||
|
}
|
||||||
|
.consommation_instantanee {
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 6px;
|
||||||
|
background-image: url(images/icone_consommation_electrique.png);
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.capteur_on{
|
||||||
|
background-color: rgba(177, 251, 23, 0.3);
|
||||||
|
color: rgb(177, 251, 23);
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .corps_details_bateau .champs .champs_description {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
text-align: justify;
|
||||||
|
font-size:0.8em;
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .boutons_graphique {
|
||||||
|
text-align: center;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr ;
|
||||||
|
grid-column-gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
margin: 20px 0px 20px 0px;
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .boutons_graphique a {
|
||||||
|
background-color: var(--bleu_koriolan_fonce);
|
||||||
|
color: var(--blanc);
|
||||||
|
width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .boutons_graphique a:hover {
|
||||||
|
color: var(--bleu_koriolan);
|
||||||
|
background-color: var(--bleu_koriolan_fonce);
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .boutons_graphique a.actif {
|
||||||
|
background-color: var(--bleu_koriolan);
|
||||||
|
color: var(--bleu_koriolan_fonce);
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .corps_details_bateau .titre_composant {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 3fr 60px;
|
||||||
|
/* height: 70px; */
|
||||||
|
padding: 15px 0px 15px 0px;
|
||||||
|
margin: 2em 0px 0.2em 0px;
|
||||||
|
border-top: 1px solid var(--bleu_koriolan_fonce);
|
||||||
|
border-bottom: 1px solid var(--bleu_koriolan_fonce);
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .corps_details_bateau .titre_composant p{
|
||||||
|
margin: unset;
|
||||||
|
text-align : unset;
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .corps_details_bateau .titre_composant .titre_text{
|
||||||
|
font-size: 1.8em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .corps_details_bateau .titre_composant .titre_desc{
|
||||||
|
/* font-size: 1.8em; */
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .corps_details_bateau .titre_composant .titre_capt{
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .corps_details_bateau .titre_composant .titre_icon {
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
cursor: pointer;
|
||||||
|
background-position: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .corps_details_bateau .titre_composant .more {
|
||||||
|
background-image: url("images/show_more.png");
|
||||||
|
}
|
||||||
|
#Bateaux #details_bateau .corps_details_bateau .titre_composant .less {
|
||||||
|
background-image: url("images/show_less.png");
|
||||||
|
}
|
||||||
#Bateaux #creation_bateau .champs {
|
#Bateaux #creation_bateau .champs {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -143,6 +272,10 @@ Fichier bateaux.php
|
|||||||
#Bateaux #creation_bateau .champs .champs_valeur {
|
#Bateaux #creation_bateau .champs .champs_valeur {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
#Bateaux #creation_bateau .champs .champs_valeur select option.selection_actuelle {
|
||||||
|
color: var(--bleu_koriolan_fonce);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
#Bateaux #creation_bateau .champs .champs_valeur select option[disabled] {
|
#Bateaux #creation_bateau .champs .champs_valeur select option[disabled] {
|
||||||
color: var(--gris_tres_pale);
|
color: var(--gris_tres_pale);
|
||||||
background-color: var(--gris_tres_tres_pale);
|
background-color: var(--gris_tres_tres_pale);
|
||||||
@@ -193,9 +326,18 @@ Fichier bateaux.php
|
|||||||
height: unset !important;
|
height: unset !important;
|
||||||
margin: 20px 0px 20px 0px !important;
|
margin: 20px 0px 20px 0px !important;
|
||||||
}
|
}
|
||||||
|
.graphique .graph {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.graphique .titre{
|
.graphique .titre{
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
.top_part_graph {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
.var {
|
.var {
|
||||||
/* display: flex; */
|
/* display: flex; */
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -207,6 +349,36 @@ Fichier bateaux.php
|
|||||||
.var label{
|
.var label{
|
||||||
display: unset !important;
|
display: unset !important;
|
||||||
}
|
}
|
||||||
|
.date_pick form{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date_pick input {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date_pick label {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 3fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date_pick span {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date_maj_version{
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date_maj_version input,.date_maj_version select{
|
||||||
|
width: 200px !important;
|
||||||
|
margin-left: 30px !important;
|
||||||
|
margin-right: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.gestion_acces{
|
.gestion_acces{
|
||||||
/* height: 100; */
|
/* height: 100; */
|
||||||
@@ -231,6 +403,17 @@ Fichier bateaux.php
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.download{
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 0;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.download:hover {
|
||||||
|
background: red;
|
||||||
|
color: var(--blanc_casse);
|
||||||
|
}
|
||||||
#type_acces {
|
#type_acces {
|
||||||
height: unset;
|
height: unset;
|
||||||
}
|
}
|
||||||
@@ -284,6 +467,10 @@ form.variables input{
|
|||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
#maCarte {
|
||||||
|
height: 600px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------- POP UP EDIT ACCESS----------------------*/
|
/* --------------------- POP UP EDIT ACCESS----------------------*/
|
||||||
#popUpAcces{
|
#popUpAcces{
|
||||||
@@ -358,6 +545,9 @@ form.variables input{
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
#popUpAccesModal form .buttons_acces{
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------- GESTION DES ACCÈS (synthèse §48) ----------------------*/
|
/* --------------------- GESTION DES ACCÈS (synthèse §48) ----------------------*/
|
||||||
|
|
||||||
@@ -498,7 +688,14 @@ form.variables input{
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------ Autres ---------------- */
|
/* ------------------ Autres ---------------- */
|
||||||
|
#dateSlider{
|
||||||
|
margin-bottom: 50px !important;
|
||||||
|
margin-top: 50px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dateSlider .noUi-connect{
|
||||||
|
background-color: var(--bleu_koriolan);
|
||||||
|
}
|
||||||
@media only screen and (max-width: 580px) {
|
@media only screen and (max-width: 580px) {
|
||||||
#Bateaux .boutons_haut_de_page {
|
#Bateaux .boutons_haut_de_page {
|
||||||
grid-template-columns: minmax(50px, auto);
|
grid-template-columns: minmax(50px, auto);
|
||||||
@@ -509,6 +706,10 @@ form.variables input{
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding : 0;
|
padding : 0;
|
||||||
}
|
}
|
||||||
|
.vars {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
}
|
||||||
#popUpAccesModal{
|
#popUpAccesModal{
|
||||||
width: 95% !important;
|
width: 95% !important;
|
||||||
height: 60% !important;
|
height: 60% !important;
|
||||||
@@ -520,6 +721,13 @@ form.variables input{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 850px) {
|
@media only screen and (max-width: 850px) {
|
||||||
|
.top_part_graph {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
.vars {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
.page_principale {
|
.page_principale {
|
||||||
width: 96% !important;
|
width: 96% !important;
|
||||||
}
|
}
|
||||||
@@ -541,6 +749,10 @@ form.variables input{
|
|||||||
width: 80%;
|
width: 80%;
|
||||||
height: 40%;
|
height: 40%;
|
||||||
}
|
}
|
||||||
|
#dateSlider{
|
||||||
|
margin: auto;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Petits écrans : formulaire d'invitation sur une colonne. Placé EN FIN de fichier : le bloc "max-width: 850px"
|
/* Petits écrans : formulaire d'invitation sur une colonne. Placé EN FIN de fichier : le bloc "max-width: 850px"
|
||||||
@@ -659,3 +871,24 @@ form.variables input{
|
|||||||
#Bateaux .entete_bateau .info_mise_a_jour.en_cours::before { content: "\21BB\00A0"; display: inline-block; animation: rotation_mise_a_jour 1.5s linear infinite; }
|
#Bateaux .entete_bateau .info_mise_a_jour.en_cours::before { content: "\21BB\00A0"; display: inline-block; animation: rotation_mise_a_jour 1.5s linear infinite; }
|
||||||
@keyframes rotation_mise_a_jour { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
@keyframes rotation_mise_a_jour { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||||
#Bateaux .entete_bateau { height: auto; min-height: 60px; } /* place pour la ligne d'information ci-dessus */
|
#Bateaux .entete_bateau { height: auto; min-height: 60px; } /* place pour la ligne d'information ci-dessus */
|
||||||
|
|
||||||
|
/* --------------------- ALERTES (synthèse §51) ----------------------*/
|
||||||
|
#Bateaux .alertes .bloc_alertes { margin-bottom: 30px; text-align: left; }
|
||||||
|
#Bateaux .alertes .bloc_alertes h2 { text-align: left; margin-bottom: 10px; }
|
||||||
|
#Bateaux .alertes .explication { font-size: 0.85em; color: var(--gris_pale); text-align: left; }
|
||||||
|
#Bateaux .alertes .avertissement { color: #8a6d00; background-color: var(--jaune_post_it_transparent); padding: 6px 12px; border-radius: 8px; }
|
||||||
|
#Bateaux .alertes form { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 8px 0px; }
|
||||||
|
#Bateaux .alertes form label { display: inline-flex; align-items: center; gap: 6px; margin: 0px; }
|
||||||
|
#Bateaux .alertes form input[type="checkbox"] { width: auto; margin: 0px; }
|
||||||
|
#Bateaux .alertes form input[type="tel"], #Bateaux .alertes form input[type="text"] { width: 14em; margin: 0px; }
|
||||||
|
#Bateaux .alertes form button { padding: 2px 16px; line-height: 1.8em; border-radius: 0.8em; }
|
||||||
|
#Bateaux .alertes .statut { display: inline-block; padding: 2px 10px; border-radius: 1em; font-size: 0.85em; }
|
||||||
|
#Bateaux .alertes .statut_actif { background-color: rgba(2, 166, 2, 0.12); color: #027a02; }
|
||||||
|
#Bateaux .alertes .statut_suspendu { background-color: var(--jaune_post_it_transparent); color: #8a6d00; }
|
||||||
|
#Bateaux .alertes table { width: 100%; }
|
||||||
|
#Bateaux .alertes th { text-align: left; padding: 4px 8px; font-size: 0.85em; }
|
||||||
|
#Bateaux .alertes td { text-align: left; padding: 4px 8px; border-top: 1px solid var(--gris_tres_tres_pale); font-size: 0.9em; vertical-align: middle; }
|
||||||
|
#Bateaux .alertes td.date_alerte { white-space: nowrap; width: 1%; }
|
||||||
|
#Bateaux .alertes td.nom_alerte { font-weight: 600; }
|
||||||
|
#Bateaux .alertes tr.alerte_declenchement td.nom_alerte, #Bateaux .alertes tr.alerte_perte_connexion td { color: #b71c1c; }
|
||||||
|
#Bateaux .alertes td form { margin: 0px; }
|
||||||
|
|||||||
+16
-4
@@ -132,6 +132,10 @@ Init_session(basename($_SERVER['SCRIPT_NAME'])); // Fonction ouvrant la session
|
|||||||
$_SESSION['shipmember']['ID_Pays'] = $_POST['ID_Pays'];
|
$_SESSION['shipmember']['ID_Pays'] = $_POST['ID_Pays'];
|
||||||
$_SESSION['shipmember']['Telephone'] = trim($_POST['Telephone']);
|
$_SESSION['shipmember']['Telephone'] = trim($_POST['Telephone']);
|
||||||
$_SESSION['shipmember']['Email'] = strtolower(trim($_POST['Email']));
|
$_SESSION['shipmember']['Email'] = strtolower(trim($_POST['Email']));
|
||||||
|
// Numéro normalisé au format international (synthèse §51) - un seul numéro par ShipMember, utilisé pour les SMS :
|
||||||
|
$telephone_normalise = Normaliser_telephone($_SESSION['shipmember']['Telephone'], $_SESSION['shipmember']['ID_Pays']);
|
||||||
|
if ($telephone_normalise === false) $message_erreur .= t("Numéro de téléphone invalide : saisissez-le au format international (par exemple +33 6 12 34 56 78).");
|
||||||
|
else $_SESSION['shipmember']['Telephone'] = $telephone_normalise;
|
||||||
|
|
||||||
// Vérifications :
|
// Vérifications :
|
||||||
$message_erreur .= check_longueur_champ('prenom_titre', $_SESSION['shipmember']['Prenom'], MIN_LONGUEUR_PRENOM, MAX_LONGUEUR_PRENOM);
|
$message_erreur .= check_longueur_champ('prenom_titre', $_SESSION['shipmember']['Prenom'], MIN_LONGUEUR_PRENOM, MAX_LONGUEUR_PRENOM);
|
||||||
@@ -171,7 +175,7 @@ Init_session(basename($_SERVER['SCRIPT_NAME'])); // Fonction ouvrant la session
|
|||||||
|
|
||||||
|
|
||||||
// Création du ShipMember :
|
// Création du ShipMember :
|
||||||
$requete = 'INSERT INTO TB_Shipmembers(Nom, Prenom, Entreprise, Rue, Code_postal, Ville, ID_PAYS, Telephone, Email, ID_LANGUAGES, Commentaires, Mdp, Date_creation_compte) VALUES (?,?,?,?,?,?,?,?,?,(SELECT ID FROM TB_Langues WHERE Prefix=?),?,?,?)';
|
$requete = 'INSERT INTO TB_Shipmembers(Nom, Prenom, Entreprise, Rue, Code_postal, Ville, ID_PAYS, Telephone, Email, ID_LANGUAGES, Mdp, Date_creation_compte) VALUES (?,?,?,?,?,?,?,?,?,(SELECT ID FROM TB_Langues WHERE Prefix=?),?,?)';
|
||||||
Requete_preparee_bdd($requete,
|
Requete_preparee_bdd($requete,
|
||||||
array(
|
array(
|
||||||
array('s', $_SESSION['shipmember']['Nom']),
|
array('s', $_SESSION['shipmember']['Nom']),
|
||||||
@@ -184,7 +188,6 @@ Init_session(basename($_SERVER['SCRIPT_NAME'])); // Fonction ouvrant la session
|
|||||||
array('s', $_SESSION['shipmember']['Telephone']),
|
array('s', $_SESSION['shipmember']['Telephone']),
|
||||||
array('s', $_SESSION['shipmember']['Email']),
|
array('s', $_SESSION['shipmember']['Email']),
|
||||||
array('s', $_SESSION['langue']),
|
array('s', $_SESSION['langue']),
|
||||||
array('s', ""),
|
|
||||||
array('s', password_hash($password, PASSWORD_DEFAULT)),
|
array('s', password_hash($password, PASSWORD_DEFAULT)),
|
||||||
array('s', date("Y-m-d H:i:s"))),
|
array('s', date("Y-m-d H:i:s"))),
|
||||||
__FILE__, __LINE__);
|
__FILE__, __LINE__);
|
||||||
@@ -608,6 +611,10 @@ Init_session(basename($_SERVER['SCRIPT_NAME'])); // Fonction ouvrant la session
|
|||||||
$_SESSION['shipmember']['ID_Pays'] = $_POST['ID_Pays'];
|
$_SESSION['shipmember']['ID_Pays'] = $_POST['ID_Pays'];
|
||||||
$_SESSION['shipmember']['Telephone'] = trim($_POST['Telephone']);
|
$_SESSION['shipmember']['Telephone'] = trim($_POST['Telephone']);
|
||||||
$_SESSION['shipmember']['Email'] = strtolower(trim($_POST['Email']));
|
$_SESSION['shipmember']['Email'] = strtolower(trim($_POST['Email']));
|
||||||
|
// Numéro normalisé au format international (synthèse §51) - un seul numéro par ShipMember, utilisé pour les SMS :
|
||||||
|
$telephone_normalise = Normaliser_telephone($_SESSION['shipmember']['Telephone'], $_SESSION['shipmember']['ID_Pays']);
|
||||||
|
if ($telephone_normalise === false) $message_erreur .= t("Numéro de téléphone invalide : saisissez-le au format international (par exemple +33 6 12 34 56 78).");
|
||||||
|
else $_SESSION['shipmember']['Telephone'] = $telephone_normalise;
|
||||||
|
|
||||||
if ($old_email != $_SESSION['shipmember']['Email']) $flag_new_email = true;
|
if ($old_email != $_SESSION['shipmember']['Email']) $flag_new_email = true;
|
||||||
else $flag_new_email = false;
|
else $flag_new_email = false;
|
||||||
@@ -658,7 +665,10 @@ Init_session(basename($_SERVER['SCRIPT_NAME'])); // Fonction ouvrant la session
|
|||||||
// Si les contrôles ne sont pas invalides, on met à jour le ShipMember :
|
// Si les contrôles ne sont pas invalides, on met à jour le ShipMember :
|
||||||
if ($message_erreur == "")
|
if ($message_erreur == "")
|
||||||
{
|
{
|
||||||
$requete = 'UPDATE TB_Shipmembers SET Prenom=?, Nom=?, Entreprise=?, Rue=?, Code_postal=?, Ville=?, ID_Pays=?, Telephone=?, Email=? WHERE ID=?';
|
// Numéro MODIFIÉ : il n'est plus vérifié pour les SMS d'alerte (synthèse §51) - affectation évaluée avant celle du numéro :
|
||||||
|
$requete = 'UPDATE TB_Shipmembers SET Prenom=?, Nom=?, Entreprise=?, Rue=?, Code_postal=?, Ville=?, ID_Pays=?,
|
||||||
|
Telephone_verifie=IF(Telephone <=> ?, Telephone_verifie, 0), Code_telephone_empreinte=IF(Telephone <=> ?, Code_telephone_empreinte, NULL),
|
||||||
|
Telephone=?, Email=? WHERE ID=?';
|
||||||
Requete_preparee_bdd($requete,
|
Requete_preparee_bdd($requete,
|
||||||
array(
|
array(
|
||||||
array('s', $_SESSION['shipmember']['Prenom']),
|
array('s', $_SESSION['shipmember']['Prenom']),
|
||||||
@@ -669,6 +679,8 @@ Init_session(basename($_SERVER['SCRIPT_NAME'])); // Fonction ouvrant la session
|
|||||||
array('s', $_SESSION['shipmember']['Ville']),
|
array('s', $_SESSION['shipmember']['Ville']),
|
||||||
array('i', $_SESSION['shipmember']['ID_Pays']),
|
array('i', $_SESSION['shipmember']['ID_Pays']),
|
||||||
array('s', $_SESSION['shipmember']['Telephone']),
|
array('s', $_SESSION['shipmember']['Telephone']),
|
||||||
|
array('s', $_SESSION['shipmember']['Telephone']),
|
||||||
|
array('s', $_SESSION['shipmember']['Telephone']),
|
||||||
array('s', Protection_mysql($_SESSION['shipmember']['Email'])), // On protège l'Email contre les injections car c'est lui qui va servir aux requetes tant qu'on n'a pas la Clé_primaire.
|
array('s', Protection_mysql($_SESSION['shipmember']['Email'])), // On protège l'Email contre les injections car c'est lui qui va servir aux requetes tant qu'on n'a pas la Clé_primaire.
|
||||||
array('i', $_SESSION['shipmember']['Cle_primaire'])),
|
array('i', $_SESSION['shipmember']['Cle_primaire'])),
|
||||||
__FILE__, __LINE__);
|
__FILE__, __LINE__);
|
||||||
@@ -679,7 +691,7 @@ Init_session(basename($_SERVER['SCRIPT_NAME'])); // Fonction ouvrant la session
|
|||||||
$requete = 'UPDATE TB_Shipmembers SET Mdp=? WHERE ID=?';
|
$requete = 'UPDATE TB_Shipmembers SET Mdp=? WHERE ID=?';
|
||||||
Requete_preparee_bdd($requete,
|
Requete_preparee_bdd($requete,
|
||||||
array(
|
array(
|
||||||
array('s', password_hash(Protection_mysql($_POST['Mot_de_passe_1']), PASSWORD_DEFAULT)),
|
array('s', password_hash($_POST['Mot_de_passe_1'], PASSWORD_DEFAULT)), // Haché tel quel (et non échappé : synthèse §47, §51)
|
||||||
array('i', $_SESSION['shipmember']['Cle_primaire'])),
|
array('i', $_SESSION['shipmember']['Cle_primaire'])),
|
||||||
__FILE__, __LINE__);
|
__FILE__, __LINE__);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================================
|
||||||
|
// sonnette.php - Réception d'un coup de sonnette d'un CM5 (synthèse §51, étape C).
|
||||||
|
// Lancé par systemd pour CHAQUE connexion reçue sur le port de la sonnette (ship2net-sonnette.socket, Accept=yes :
|
||||||
|
// aucun processus permanent) ; la connexion est l'entrée et la sortie standard ; systemd fournit l'adresse de
|
||||||
|
// l'appelant dans $REMOTE_ADDR.
|
||||||
|
//
|
||||||
|
// Protocole : une ligne, "evenement" ou "vie". Réponse : une ligne "ok", ou "inconnu" (module non reconnu).
|
||||||
|
// - "vie" (battement de cœur, au plus toutes les 10 min) : signal enregistré, chien de garde réarmé, rien d'autre ;
|
||||||
|
// - "evenement" (alarme SMS/e-mail, archive prête, retour du réseau) : idem, plus une demande de traitement des
|
||||||
|
// alertes et des archives de ce bateau ;
|
||||||
|
// - tout autre texte est traité comme "evenement" (le signal n'est qu'une indication : il ne porte aucune donnée
|
||||||
|
// ni aucun droit - au pire, une lecture inutile chez un bateau connu).
|
||||||
|
// Un bateau qui était déclaré injoignable déclenche toujours le traitement (alerte de retour de connexion).
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
chdir("/var/www/html");
|
||||||
|
require_once __DIR__ . "/config/config.php";
|
||||||
|
require_once __DIR__ . "/fonctions_alertes.php";
|
||||||
|
|
||||||
|
$adresse = getenv('REMOTE_ADDR') ?: '';
|
||||||
|
// Adresse IPv4 éventuellement présentée sous forme IPv6 (::ffff:100.92.x.y) :
|
||||||
|
if (strncmp($adresse, '::ffff:', 7) === 0) $adresse = substr($adresse, 7);
|
||||||
|
|
||||||
|
// Lecture de la ligne (3 s au plus, 32 caractères au plus) :
|
||||||
|
stream_set_timeout(STDIN, 3);
|
||||||
|
$ligne = strtolower(trim((string)fgets(STDIN, 32)));
|
||||||
|
$motif = ($ligne === 'vie') ? 'vie' : 'evenement';
|
||||||
|
|
||||||
|
if (filter_var($adresse, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false)
|
||||||
|
{
|
||||||
|
error_log("sonnette : adresse d'appelant invalide (".$adresse.")");
|
||||||
|
echo "inconnu\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$db = new mysqli("127.0.0.1", BDD_USER, BDD_PASSWORD, BDD_DATABASE, 3307);
|
||||||
|
$bateau = Bateau_par_adresse_netbird($db, $adresse);
|
||||||
|
if ($bateau === null)
|
||||||
|
{
|
||||||
|
error_log("sonnette : signal de ".$adresse." - module inconnu ou non affecté, ignoré");
|
||||||
|
echo "inconnu\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
$id_bateau = $bateau['id_bateau'];
|
||||||
|
|
||||||
|
// État avant ce signal (le bateau était-il déclaré injoignable ?) :
|
||||||
|
$stmt = $db->prepare("SELECT Injoignable_depuis FROM TB_Bateaux WHERE Cle_primaire=?");
|
||||||
|
$stmt->bind_param('i', $id_bateau);
|
||||||
|
$stmt->execute();
|
||||||
|
$etait_injoignable = ($stmt->get_result()->fetch_row()[0] ?? null) !== null;
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// Signal enregistré :
|
||||||
|
$stmt = $db->prepare("UPDATE TB_Bateaux SET Date_dernier_signal=UTC_TIMESTAMP(), Dernier_motif=? WHERE Cle_primaire=?");
|
||||||
|
$stmt->bind_param('si', $motif, $id_bateau);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
$seuil = Reglage_entier($db, 'Seuil_injoignable', SEUIL_INJOIGNABLE_DEFAUT);
|
||||||
|
$db->close();
|
||||||
|
|
||||||
|
Rearmer_gardien($id_bateau, $seuil);
|
||||||
|
if ($motif === 'evenement' || $etait_injoignable) Demander_traitement_alertes($id_bateau);
|
||||||
|
|
||||||
|
echo "ok\n";
|
||||||
|
}
|
||||||
|
catch (Throwable $e)
|
||||||
|
{
|
||||||
|
error_log("sonnette : ".$e->getMessage());
|
||||||
|
echo "erreur\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Ship2net - surveillance des demandes de traitement des alertes (sonnette, chien de garde)
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
DirectoryNotEmpty=/var/www/demandes/alertes
|
||||||
|
Unit=ship2net-alertes.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=paths.target
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Ship2net - traitement des alertes (alarmes, perte et retour de connexion)
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=koriolan
|
||||||
|
Group=koriolan
|
||||||
|
ExecStart=/usr/bin/php /var/www/traiter_alertes.php
|
||||||
|
Nice=5
|
||||||
|
TimeoutStartSec=5min
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Ship2net - sonnette des CM5 (port TCP 9123, interface NetBird uniquement)
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
# Uniquement l'adresse NetBird du VPS : jamais joignable depuis Internet.
|
||||||
|
ListenStream=100.92.117.186:9123
|
||||||
|
# FreeBind : l'écoute s'installe même si l'interface NetBird (wt0) n'est pas encore montée au démarrage du VPS.
|
||||||
|
FreeBind=yes
|
||||||
|
# Un processus (sonnette.php) par connexion reçue - aucun processus permanent.
|
||||||
|
Accept=yes
|
||||||
|
# Garde-fous contre les rafales :
|
||||||
|
MaxConnections=32
|
||||||
|
MaxConnectionsPerSource=4
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Ship2net - coup de sonnette d'un CM5
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=koriolan
|
||||||
|
Group=koriolan
|
||||||
|
ExecStart=/usr/bin/php /var/www/sonnette.php
|
||||||
|
StandardInput=socket
|
||||||
|
StandardOutput=socket
|
||||||
|
StandardError=journal
|
||||||
|
# Durée maximale du traitement d'un coup de sonnette (pour Type=oneshot, c'est TimeoutStartSec qui s'applique)
|
||||||
|
TimeoutStartSec=15
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================================
|
||||||
|
// test_sms.php - Essai de l'envoi de SMS par OVH (synthèse §51, étape F). En ligne de commande uniquement :
|
||||||
|
// php /var/www/test_sms.php -> vérifie les identifiants (crédits restants), sans rien envoyer
|
||||||
|
// php /var/www/test_sms.php +33612345678 -> envoie en plus un SMS d'essai à ce numéro (1 crédit)
|
||||||
|
// ============================================================================
|
||||||
|
if (PHP_SAPI !== 'cli') exit(1);
|
||||||
|
chdir("/var/www/html");
|
||||||
|
require_once __DIR__ . "/config/config.php";
|
||||||
|
|
||||||
|
if (!Sms_configure()) { echo "Identifiants OVH absents de config/confidentiel.php (ou extension curl manquante).\n"; exit(1); }
|
||||||
|
echo "Compte : ".OVH_SMS_SERVICE." Expéditeur : ".(trim((string)(defined('OVH_SMS_EXPEDITEUR') ? OVH_SMS_EXPEDITEUR : '')) ?: "(numéro court OVH)")."\n";
|
||||||
|
|
||||||
|
$erreur = null;
|
||||||
|
$credits = Credits_sms_ovh($erreur);
|
||||||
|
if ($credits === null) { echo "Échec : ".$erreur."\n"; exit(1); }
|
||||||
|
echo "Identifiants OK - crédits restants : ".$credits."\n";
|
||||||
|
|
||||||
|
if (isset($argv[1]))
|
||||||
|
{
|
||||||
|
$texte = "ShipHeart : SMS d'essai envoyé le ".date('d/m/Y à H:i').". Accents : é è à ù ç ê ô.";
|
||||||
|
echo "Texte envoyé (GSM) : ".Texte_sms_gsm($texte)."\n";
|
||||||
|
$reference = null;
|
||||||
|
if (Envoyer_sms_ovh($argv[1], $texte, $erreur, $reference)) echo "Accepté par OVH - référence ".$reference."\n";
|
||||||
|
else { echo "Échec : ".$erreur."\n"; exit(1); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================================
|
||||||
|
// traiter_alertes.php - Traitement des alertes d'un ou plusieurs bateaux (synthèse §51, étape D).
|
||||||
|
// Exécuté NATIVEMENT par ship2net-alertes.service, déclenché par ship2net-alertes.path dès qu'une demande apparaît
|
||||||
|
// dans /var/www/demandes/alertes (déposée par sonnette.php - motif "evenement" ou retour d'un bateau injoignable - ou
|
||||||
|
// par gardien.php - perte de connexion). Chaque demande (fichier nommé d'après le numéro du bateau) est consommée
|
||||||
|
// avant son traitement.
|
||||||
|
//
|
||||||
|
// Pour un bateau :
|
||||||
|
// 1. perte de connexion (Injoignable_depuis renseigné) : alerte "perte", une seule fois ;
|
||||||
|
// 2. si le bateau est joignable : lecture des déclenchements et fins d'alarmes sur le CM5 depuis le curseur
|
||||||
|
// (Curseur_historique) - alarmes survenues PENDANT une coupure : comptées, pas envoyées ; autres : envoyées sur les
|
||||||
|
// moyens prévus par l'event ET choisis par chaque destinataire, sauf event suspendu par le fusible anti-rafales ;
|
||||||
|
// 3. retour de connexion (signal reçu après la perte) : alerte "retour" (durée de la coupure, nombre d'alarmes
|
||||||
|
// pendant la coupure), puis Injoignable_depuis remis à NULL ;
|
||||||
|
// 4. si le bateau est joignable : traitement de ses archives (cron_archivage.php --bateau=N).
|
||||||
|
// Les alertes de connexion et de suspension partent sur tous les moyens choisis par chaque destinataire.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
chdir("/var/www/html");
|
||||||
|
require_once __DIR__ . "/config/config.php";
|
||||||
|
require_once __DIR__ . "/fonctions_alertes.php";
|
||||||
|
|
||||||
|
const REPERTOIRE_DEMANDES_A_TRAITER_ALERTES = REPERTOIRE_DEMANDES_ALERTES;
|
||||||
|
|
||||||
|
function Html($texte) { return htmlspecialchars((string)$texte, ENT_QUOTES, 'UTF-8'); }
|
||||||
|
|
||||||
|
function Traiter_alertes_bateau($db, $id_bateau)
|
||||||
|
{
|
||||||
|
$stmt = $db->prepare("SELECT B.Nom, B.Date_dernier_signal, B.Injoignable_depuis, B.Curseur_historique,
|
||||||
|
(SELECT C.Adresse_netbird FROM TB_Affectations_bateaux A JOIN TB_Cartes C ON C.Adresse_mac = A.Adresse_mac
|
||||||
|
WHERE A.ID_Bateau = B.Cle_primaire AND A.Date_fin IS NULL)
|
||||||
|
FROM TB_Bateaux B WHERE B.Cle_primaire=? AND B.Validite=1");
|
||||||
|
$stmt->bind_param('i', $id_bateau);
|
||||||
|
$stmt->execute();
|
||||||
|
$b = $stmt->get_result()->fetch_row();
|
||||||
|
$stmt->close();
|
||||||
|
if (!$b) { echo "Bateau {$id_bateau} : inconnu ou dévalidé - ignoré.\n"; return; }
|
||||||
|
[$nom_bateau, $dernier_signal, $injoignable_depuis, $curseur, $adresse] = $b;
|
||||||
|
|
||||||
|
$fuseau = Reglage_texte($db, 'Fuseau_alertes', 'Europe/Paris');
|
||||||
|
$seuil_retard = Reglage_entier($db, 'Seuil_retard_alerte', 900);
|
||||||
|
$destinataires = Destinataires_alertes($db, $id_bateau);
|
||||||
|
$revenu = ($injoignable_depuis !== null && $dernier_signal !== null && $dernier_signal > $injoignable_depuis);
|
||||||
|
$joignable = ($injoignable_depuis === null || $revenu);
|
||||||
|
|
||||||
|
// 0. Reprise des alertes restées "à envoyer" lors d'un passage précédent interrompu :
|
||||||
|
$reprises = Reprendre_alertes_a_envoyer($db, $id_bateau, $destinataires);
|
||||||
|
if ($reprises > 0) echo "Bateau {$id_bateau} : {$reprises} alerte(s) interrompue(s) reprise(s).\n";
|
||||||
|
|
||||||
|
// 1. Perte de connexion (une seule alerte par perte, grâce à la clé unique) :
|
||||||
|
if ($injoignable_depuis !== null)
|
||||||
|
{
|
||||||
|
$texte = "Aucun signal du bateau ".$nom_bateau." depuis le ".Date_alerte($injoignable_depuis, $fuseau)." : connexion perdue.";
|
||||||
|
$id_alerte = Creer_alerte($db, $id_bateau, 0, null, 'perte_connexion', $injoignable_depuis, 'a_envoyer',
|
||||||
|
"[ShipHeart] ".$nom_bateau." : connexion perdue",
|
||||||
|
"<p>".Html($texte)."</p><p>Vous serez prévenu(e) dès le retour de la connexion.</p>", "ShipHeart - ".$texte, true, true);
|
||||||
|
if ($id_alerte > 0)
|
||||||
|
{
|
||||||
|
$vises = Envoyer_alerte($db, $id_alerte, $destinataires);
|
||||||
|
echo "Bateau {$id_bateau} : perte de connexion signalée ({$vises} destinataire(s)).\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Alarmes (bateau joignable seulement) :
|
||||||
|
$nb_pendant_coupure = null; // null = inconnu (CM5 non lu)
|
||||||
|
if ($joignable && filter_var((string)$adresse, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false)
|
||||||
|
{
|
||||||
|
if ($curseur === null)
|
||||||
|
{
|
||||||
|
// Première lecture pour ce bateau : on part de maintenant (pas d'envoi de l'historique antérieur au lot) :
|
||||||
|
$stmt = $db->prepare("UPDATE TB_Bateaux SET Curseur_historique=UTC_TIMESTAMP() WHERE Cle_primaire=?");
|
||||||
|
$stmt->bind_param('i', $id_bateau);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
echo "Bateau {$id_bateau} : curseur d'alarmes initialisé (aucun envoi de l'historique antérieur).\n";
|
||||||
|
// $nb_pendant_coupure reste inconnu (null) : l'historique n'a volontairement pas été lu.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$evenements = Lire_alarmes_cm5($adresse, $curseur);
|
||||||
|
if ($evenements === null) echo "Bateau {$id_bateau} : alarmes non lues (CM5 sans réponse, ou route /alertes absente).\n";
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$nb_pendant_coupure = 0;
|
||||||
|
$nouveau_curseur = $curseur;
|
||||||
|
foreach ($evenements as $ev)
|
||||||
|
{
|
||||||
|
$date = (string)$ev['date'];
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $date) !== 1) continue;
|
||||||
|
$type = ((int)$ev['type'] == 2) ? 'fin' : 'declenchement';
|
||||||
|
$id_event = (int)$ev['id_event'];
|
||||||
|
$nom_event = (string)$ev['nom'];
|
||||||
|
if ($date > $nouveau_curseur) $nouveau_curseur = $date;
|
||||||
|
|
||||||
|
// Pendant une coupure : comptée, pas envoyée.
|
||||||
|
if ($injoignable_depuis !== null && $date >= $injoignable_depuis && $date <= $dernier_signal)
|
||||||
|
{
|
||||||
|
if (Creer_alerte($db, $id_bateau, $id_event, $nom_event, $type, $date, 'pendant_coupure') > 0 && $type == 'declenchement') $nb_pendant_coupure++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event suspendu par le fusible : enregistrée, pas envoyée.
|
||||||
|
if (Event_suspendu($db, $id_bateau, $id_event))
|
||||||
|
{
|
||||||
|
Creer_alerte($db, $id_bateau, $id_event, $nom_event, $type, $date, 'suspendue');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texte (fuseau des alertes ; mention du retard au-delà du seuil) :
|
||||||
|
$heure = Date_alerte($date, $fuseau);
|
||||||
|
if ($type == 'declenchement')
|
||||||
|
{
|
||||||
|
$base = "Alarme « ".$nom_event." » déclenchée le ".$heure." sur ".$nom_bateau.".";
|
||||||
|
$texte_email = (trim((string)($ev['texte_email'] ?? '')) !== '') ? trim($ev['texte_email']) : $base;
|
||||||
|
$texte_sms = (trim((string)($ev['texte_sms'] ?? '')) !== '') ? $nom_bateau." : ".trim($ev['texte_sms']) : $base;
|
||||||
|
$objet = "[ShipHeart] ".$nom_bateau." : ".$nom_event;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Durée : depuis le déclenchement précédent de cet event
|
||||||
|
$stmt = $db->prepare("SELECT MAX(Date_evenement) FROM TB_Alertes WHERE ID_Bateau=? AND ID_Event=? AND Type='declenchement' AND Date_evenement <= ?");
|
||||||
|
$stmt->bind_param('iis', $id_bateau, $id_event, $date);
|
||||||
|
$stmt->execute();
|
||||||
|
$debut = $stmt->get_result()->fetch_row()[0];
|
||||||
|
$stmt->close();
|
||||||
|
$duree = ($debut !== null) ? " (durée ".Duree_alerte(strtotime($date.' UTC') - strtotime($debut.' UTC')).")" : "";
|
||||||
|
$base = "Fin d'alarme « ".$nom_event." » le ".$heure.$duree." sur ".$nom_bateau.".";
|
||||||
|
$texte_email = $base;
|
||||||
|
$texte_sms = $base;
|
||||||
|
$objet = "[ShipHeart] ".$nom_bateau." : fin d'alarme ".$nom_event;
|
||||||
|
}
|
||||||
|
$retard = time() - strtotime($date.' UTC');
|
||||||
|
if ($retard > $seuil_retard) $texte_sms = "(+".Duree_alerte($retard).") ".$texte_sms;
|
||||||
|
$html = "<p>".Html($texte_email)."</p>";
|
||||||
|
if ($texte_email !== $base) $html .= "<p>".Html($base)."</p>";
|
||||||
|
if ($retard > $seuil_retard) $html .= "<p><i>Reçue avec ".Html(Duree_alerte($retard))." de retard.</i></p>";
|
||||||
|
|
||||||
|
$id_alerte = Creer_alerte($db, $id_bateau, $id_event, $nom_event, $type, $date, 'a_envoyer', $objet, $html, $texte_sms,
|
||||||
|
!empty($ev['alarme_email']), !empty($ev['alarme_sms']));
|
||||||
|
if ($id_alerte == 0) continue; // Déjà traitée (lecture rejouée)
|
||||||
|
|
||||||
|
// Fusible : rafale détectée -> suspension, un dernier message à la place de l'alerte elle-même
|
||||||
|
$motif = Fusible_franchi($db, $id_bateau, $id_event, $date);
|
||||||
|
if ($motif !== null)
|
||||||
|
{
|
||||||
|
Statut_alerte($db, $id_alerte, 'suspendue');
|
||||||
|
$stmt = $db->prepare("INSERT IGNORE INTO TB_Suspensions_alertes (ID_Bateau, ID_Event, Nom_event, Date_suspension, Motif) VALUES (?, ?, ?, UTC_TIMESTAMP(), ?)");
|
||||||
|
$stmt->bind_param('iiss', $id_bateau, $id_event, $nom_event, $motif);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
$texte = "Alertes suspendues pour « ".$nom_event." » sur ".$nom_bateau." (".$motif.") : configuration à vérifier. Réactivation sur le portail Ship2net.";
|
||||||
|
$id_suspension = Creer_alerte($db, $id_bateau, $id_event, $nom_event, 'suspension', $date, 'a_envoyer',
|
||||||
|
"[ShipHeart] ".$nom_bateau." : alertes suspendues", "<p>".Html($texte)."</p>", $texte, true, true);
|
||||||
|
if ($id_suspension > 0) Envoyer_alerte($db, $id_suspension, $destinataires);
|
||||||
|
echo "Bateau {$id_bateau} : fusible - alertes de l'event {$id_event} suspendues ({$motif}).\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$vises = Envoyer_alerte($db, $id_alerte, $destinataires);
|
||||||
|
echo "Bateau {$id_bateau} : {$type} de « {$nom_event} » ({$date}) - {$vises} destinataire(s).\n";
|
||||||
|
}
|
||||||
|
$stmt = $db->prepare("UPDATE TB_Bateaux SET Curseur_historique=? WHERE Cle_primaire=?");
|
||||||
|
$stmt->bind_param('si', $nouveau_curseur, $id_bateau);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Retour de connexion (une seule fois : Injoignable_depuis remis à NULL de façon conditionnelle) :
|
||||||
|
if ($revenu)
|
||||||
|
{
|
||||||
|
$stmt = $db->prepare("UPDATE TB_Bateaux SET Injoignable_depuis=NULL WHERE Cle_primaire=? AND Injoignable_depuis=?");
|
||||||
|
$stmt->bind_param('is', $id_bateau, $injoignable_depuis);
|
||||||
|
$stmt->execute();
|
||||||
|
$premier = ($stmt->affected_rows == 1);
|
||||||
|
$stmt->close();
|
||||||
|
if ($premier)
|
||||||
|
{
|
||||||
|
$coupure = Duree_alerte(strtotime($dernier_signal.' UTC') - strtotime($injoignable_depuis.' UTC'));
|
||||||
|
if ($nb_pendant_coupure === null) $bilan = "Les alarmes survenues pendant la coupure n'ont pas pu être lues : consultez le portail Ship2net.";
|
||||||
|
else if ($nb_pendant_coupure == 0) $bilan = "Aucune alarme pendant la coupure.";
|
||||||
|
else $bilan = $nb_pendant_coupure." alarme(s) pendant la coupure : consultez le portail Ship2net pour connaître l'état du bateau.";
|
||||||
|
$texte = "Bateau ".$nom_bateau." de nouveau joignable le ".Date_alerte($dernier_signal, $fuseau)." (coupure de ".$coupure."). ".$bilan;
|
||||||
|
$id_alerte = Creer_alerte($db, $id_bateau, 0, null, 'retour_connexion', $dernier_signal, 'a_envoyer',
|
||||||
|
"[ShipHeart] ".$nom_bateau." : connexion rétablie", "<p>".Html($texte)."</p>", "ShipHeart - ".$texte, true, true);
|
||||||
|
if ($id_alerte > 0)
|
||||||
|
{
|
||||||
|
$vises = Envoyer_alerte($db, $id_alerte, $destinataires);
|
||||||
|
echo "Bateau {$id_bateau} : retour de connexion signalé ({$vises} destinataire(s)).\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Archives (bateau joignable) :
|
||||||
|
return $joignable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir(REPERTOIRE_DEMANDES_A_TRAITER_ALERTES)) { echo "Répertoire ".REPERTOIRE_DEMANDES_A_TRAITER_ALERTES." absent.\n"; exit(1); }
|
||||||
|
require_once __DIR__ . "/config/fonctions_mail.php";
|
||||||
|
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||||
|
|
||||||
|
foreach (scandir(REPERTOIRE_DEMANDES_A_TRAITER_ALERTES) as $nom)
|
||||||
|
{
|
||||||
|
if ($nom === '.' || $nom === '..') continue;
|
||||||
|
@unlink(REPERTOIRE_DEMANDES_A_TRAITER_ALERTES."/".$nom);
|
||||||
|
if (preg_match('/^[1-9][0-9]{0,9}$/', $nom) !== 1) { echo "Demande '{$nom}' invalide - supprimée.\n"; continue; }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$db = new mysqli("127.0.0.1", BDD_USER, BDD_PASSWORD, BDD_DATABASE, 3307);
|
||||||
|
$db->set_charset('utf8mb4');
|
||||||
|
$db->query("SET time_zone = '+00:00'");
|
||||||
|
$joignable = Traiter_alertes_bateau($db, (int)$nom);
|
||||||
|
$db->close();
|
||||||
|
if ($joignable) passthru("/usr/bin/php ".escapeshellarg(__DIR__."/cron_archivage.php")." --bateau=".(int)$nom);
|
||||||
|
}
|
||||||
|
catch (Throwable $e) { echo "Bateau {$nom} : erreur - ".$e->getMessage()."\n"; } // N'empêche pas les demandes suivantes
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user