Files
ship2net-www/config/fonctions_acces.php
T

325 lines
18 KiB
PHP

<?php
//************************************************************************************************************
// fonctions_acces.php - Accès des ShipMembers aux bateaux et invitations (synthèse §47)
//
// Regroupe ce qui était recopié dans shipmembers.php (5 blocs d'activation de code, 7 requêtes "accès actifs")
// et administration.php (1 + 1). Modèle de TB_Lien_bateau_shipmember :
// - Statut : 'en_attente' (invitation non acceptée), 'actif', 'suspendu', 'revoque' ;
// - une invitation désigne une adresse e-mail (Email_invite) : seul un compte ShipMember ACTIVÉ ayant cette
// adresse peut l'accepter (l'activation prouve le contrôle de la boîte - un compte non activé ne le prouve pas) ;
// - Expiration_invitation : date limite pour accepter ; Fin_validite : fin de l'accès accordé.
//
// Toutes les fonctions utilisent la connexion courante ($_SESSION['db'], ouverte par Ouverture_bdd()).
//************************************************************************************************************
// Alphabet des codes d'invitation : majuscules et chiffres SANS les caractères ambigus à la lecture (0/O, 1/I/L) -
// 31 symboles. 8 caractères présentés en deux groupes (ex : K7QX-M3PW), soit 31^8 = 8,5.10^11 combinaisons.
const CODE_INVITATION_ALPHABET = "23456789ABCDEFGHJKMNPQRSTUVWXYZ";
const CODE_INVITATION_LONGUEUR = 8;
// Durée de validité par défaut d'une invitation (jours), si TB_Configuration.Duree_validite_invitation n'est pas
// chargée en session :
const DUREE_VALIDITE_INVITATION_DEFAUT = 30;
//************************************************************************************************************
// Fonction : Nombre de lignes réellement modifiées par la DERNIÈRE requête exécutée sur la connexion courante.
// NB : mysqli_affected_rows() ne convient pas après Requete_preparee_bdd(), qui appelle
// mysqli_stmt_get_result() même pour un UPDATE - ce qui remet le compteur côté PHP à -1 (constaté en
// test). ROW_COUNT() interroge directement le serveur. À appeler IMMÉDIATEMENT après la requête.
// Reçoit : Rien
// Retour : Le nombre de lignes modifiées
//************************************************************************************************************
function Nb_lignes_modifiees()
{
$reponse = Requete_bdd("SELECT ROW_COUNT() AS nb", __FILE__, __LINE__);
$donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
Liberation_jeu_de_resultat_bdd($reponse);
return (int)$donnees['nb'];
}
//************************************************************************************************************
// Fonction : Liste des accès ACTIFS d'un ShipMember (statut 'actif' et Fin_validite non dépassée).
// Reçoit : La clé primaire du ShipMember
// Retour : Un tableau de lignes ['ID_Bateau', 'ID_Type_acces_bateau_shipmember'] (vide si aucun accès)
//************************************************************************************************************
function Acces_actifs_shipmember($id_shipmember)
{
$acces = [];
$requete = "SELECT ID_Bateau, ID_Type_acces_bateau_shipmember FROM TB_Lien_bateau_shipmember
WHERE ID_Shipmember=? AND Statut='actif' AND Fin_validite >= NOW()";
$reponse = Requete_preparee_bdd($requete, array(array('i', $id_shipmember)), __FILE__, __LINE__);
while ($donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC)) $acces[] = $donnees;
Liberation_jeu_de_resultat_bdd($reponse);
return $acces;
}
//************************************************************************************************************
// Fonction : Indique si le compte d'un ShipMember est activé, c'est-à-dire si son titulaire a prouvé qu'il
// contrôle l'adresse e-mail (connexion avec le mot de passe provisoire reçu, puis choix d'un mot de
// passe : la ligne TB_Activation est alors supprimée).
// Reçoit : La clé primaire du ShipMember
// Retour : true si activé
//************************************************************************************************************
function Compte_est_active($id_shipmember)
{
$reponse = Requete_preparee_bdd("SELECT COUNT(*) AS nb FROM TB_Activation WHERE ID_SHIPMEMBERS=?",
array(array('i', $id_shipmember)), __FILE__, __LINE__);
$donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
Liberation_jeu_de_resultat_bdd($reponse);
return ($donnees['nb'] == 0);
}
//************************************************************************************************************
// Fonction : Normalise un code d'invitation saisi ou reçu (majuscules, espaces et tirets retirés, puis tiret
// remis au milieu) et vérifie son format.
// Reçoit : La valeur brute (GET, POST, saisie manuelle...)
// Retour : Le code normalisé (ex : "K7QX-M3PW"), ou false si le format est invalide
//************************************************************************************************************
function Normaliser_code_invitation($valeur)
{
if (!is_string($valeur)) return false; // écarte ?code_acces[]=... (tableau)
$brut = strtoupper(preg_replace('/[\s-]/', '', $valeur));
$moitie = intdiv(CODE_INVITATION_LONGUEUR, 2);
if (preg_match('/^['.CODE_INVITATION_ALPHABET.']{'.CODE_INVITATION_LONGUEUR.'}\z/', $brut) !== 1) return false;
return substr($brut, 0, $moitie).'-'.substr($brut, $moitie);
}
//************************************************************************************************************
// Fonction : Génère un code d'invitation unique (générateur aléatoire cryptographique random_int).
// Reçoit : Rien
// Retour : Le code (ex : "K7QX-M3PW")
//************************************************************************************************************
function Generer_code_invitation()
{
$max = strlen(CODE_INVITATION_ALPHABET) - 1;
do
{
$brut = "";
for ($i = 0; $i < CODE_INVITATION_LONGUEUR; $i++) $brut .= CODE_INVITATION_ALPHABET[random_int(0, $max)];
$code = Normaliser_code_invitation($brut);
$reponse = Requete_preparee_bdd("SELECT COUNT(*) AS nb FROM TB_Lien_bateau_shipmember WHERE Code=?",
array(array('s', $code)), __FILE__, __LINE__);
$donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
Liberation_jeu_de_resultat_bdd($reponse);
}
while ($donnees['nb'] != 0);
return $code;
}
//************************************************************************************************************
// Fonction : Crée une invitation nominative (statut 'en_attente').
// Reçoit : Le bateau, l'adresse e-mail du destinataire (obligatoire), son prénom et son nom (facultatifs, pour
// l'affichage), le type d'accès, la fin de l'accès accordé (format 'Y-m-d', vide = sans limite) et le
// ShipMember qui invite.
// Retour : ['ok'=>true, 'id'=>int, 'code'=>string] ou ['ok'=>false, 'raison'=>string]
// NB : les droits de celui qui invite sont vérifiés par la page appelante.
//************************************************************************************************************
function Creer_invitation($id_bateau, $email, $prenom, $nom, $id_type_acces, $fin_acces, $id_cree_par)
{
$email = strtolower(trim((string)$email));
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) return ['ok' => false, 'raison' => t("Adresse e-mail invalide.")];
$prenom = trim((string)$prenom);
$nom = trim((string)$nom);
$prenom = ($prenom === '') ? null : mb_substr($prenom, 0, 50);
$nom = ($nom === '') ? null : mb_substr($nom, 0, 50);
// Type d'accès : doit exister et être valide (l'ancien type "Administrateur" est dévalidé) :
$reponse = Requete_preparee_bdd("SELECT COUNT(*) AS nb FROM TB_Types_acces_bateau_shipmember WHERE Cle_primaire=? AND Validite=1",
array(array('i', $id_type_acces)), __FILE__, __LINE__);
$donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
Liberation_jeu_de_resultat_bdd($reponse);
if ($donnees['nb'] == 0) return ['ok' => false, 'raison' => t("Type d'accès invalide.")];
// Fin de l'accès accordé : date du jour en fin de journée, ou sans limite :
if ($fin_acces === null || trim($fin_acces) === '') $fin_validite = '9999-12-31 23:59:59';
else
{
$date = date_create_from_format('Y-m-d', trim($fin_acces));
if ($date === false) return ['ok' => false, 'raison' => t("Date de fin d'accès invalide.")];
$fin_validite = $date->format('Y-m-d').' 23:59:59';
}
$duree = isset($_SESSION['Config__Duree_validite_invitation']) ? (int)$_SESSION['Config__Duree_validite_invitation'] : DUREE_VALIDITE_INVITATION_DEFAUT;
if ($duree <= 0) $duree = DUREE_VALIDITE_INVITATION_DEFAUT;
$code = Generer_code_invitation();
$requete = "INSERT INTO TB_Lien_bateau_shipmember
(Statut, ID_Shipmember, ID_Bateau, Email_invite, Prenom_invite, Nom_invite, Code, Expiration_invitation,
Fin_validite, Cree_par, ID_Type_acces_bateau_shipmember)
VALUES ('en_attente', 0, ?, ?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL ? DAY), ?, ?, ?)";
Requete_preparee_bdd($requete, array(
array('i', $id_bateau), array('s', $email), array('s', $prenom), array('s', $nom),
array('s', $code), array('i', $duree), array('s', $fin_validite),
array('i', $id_cree_par), array('i', $id_type_acces)),
__FILE__, __LINE__);
return ['ok' => true, 'id' => Recuperation_id_cree_bdd(), 'code' => $code];
}
//************************************************************************************************************
// Fonction : Informations sur une invitation à partir de son code (bandeau d'accueil d'un lien d'invitation).
// Reçoit : Le code (brut ou normalisé)
// Retour : Un tableau (ID, Statut, Email_invite, Prenom_invite, Nom_invite, Nom_bateau, Type_acces,
// Expiree [0/1]) ou null si le code est inconnu ou mal formé
//************************************************************************************************************
function Invitation_par_code($code)
{
$code = Normaliser_code_invitation($code);
if ($code === false) return null;
$colonne_type = (isset($_SESSION['langue']) && $_SESSION['langue'] == 'EN') ? 'Nom_en' : 'Nom_fr';
$requete = "SELECT L.ID, L.Statut, L.Email_invite, L.Prenom_invite, L.Nom_invite, B.Nom AS Nom_bateau,
T.".$colonne_type." AS Type_acces,
(L.Expiration_invitation IS NOT NULL AND L.Expiration_invitation <= NOW()) AS Expiree
FROM TB_Lien_bateau_shipmember L
JOIN TB_Bateaux B ON B.Cle_primaire = L.ID_Bateau
LEFT JOIN TB_Types_acces_bateau_shipmember T ON T.Cle_primaire = L.ID_Type_acces_bateau_shipmember
WHERE L.Code=?";
$reponse = Requete_preparee_bdd($requete, array(array('s', $code)), __FILE__, __LINE__);
$donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
Liberation_jeu_de_resultat_bdd($reponse);
return $donnees ?: null;
}
//************************************************************************************************************
// Fonction : Invitations en attente (non expirées) adressées à une adresse e-mail.
// Reçoit : L'adresse e-mail du ShipMember connecté
// Retour : Un tableau de lignes (ID, Nom_bateau, Type_acces, Expiration_invitation, Prenom_invitant, Nom_invitant)
//************************************************************************************************************
function Invitations_en_attente($email)
{
$invitations = [];
$colonne_type = (isset($_SESSION['langue']) && $_SESSION['langue'] == 'EN') ? 'Nom_en' : 'Nom_fr';
$requete = "SELECT L.ID, B.Nom AS Nom_bateau, T.".$colonne_type." AS Type_acces, L.Expiration_invitation,
S.Prenom AS Prenom_invitant, S.Nom AS Nom_invitant
FROM TB_Lien_bateau_shipmember L
JOIN TB_Bateaux B ON B.Cle_primaire = L.ID_Bateau
LEFT JOIN TB_Types_acces_bateau_shipmember T ON T.Cle_primaire = L.ID_Type_acces_bateau_shipmember
LEFT JOIN TB_Shipmembers S ON S.ID = L.Cree_par
WHERE L.Statut='en_attente' AND L.Email_invite=?
AND (L.Expiration_invitation IS NULL OR L.Expiration_invitation > NOW())
ORDER BY L.Date_creation";
$reponse = Requete_preparee_bdd($requete, array(array('s', strtolower(trim((string)$email)))), __FILE__, __LINE__);
while ($donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC)) $invitations[] = $donnees;
Liberation_jeu_de_resultat_bdd($reponse);
return $invitations;
}
//************************************************************************************************************
// Fonction : Accepte une invitation pour le ShipMember connecté.
// Toutes les conditions (en attente, adresse identique, non expirée) sont vérifiées DANS la requête de
// mise à jour elle-même : une invitation ne peut donc être acceptée qu'une fois, même par deux
// requêtes simultanées.
// Reçoit : L'ID de l'invitation, la clé primaire et l'adresse e-mail du ShipMember connecté
// Retour : ['ok'=>true] ou ['ok'=>false, 'raison'=>string]
//************************************************************************************************************
function Accepter_invitation($id_lien, $id_shipmember, $email)
{
if (!Compte_est_active($id_shipmember))
{
return ['ok' => false, 'raison' => t("Votre compte doit être activé (mot de passe provisoire reçu par e-mail) avant de pouvoir accepter une invitation.")];
}
$requete = "UPDATE TB_Lien_bateau_shipmember
SET Statut='actif', ID_Shipmember=?, Date_activation=NOW()
WHERE ID=? AND Statut='en_attente' AND Email_invite=?
AND (Expiration_invitation IS NULL OR Expiration_invitation > NOW())";
Requete_preparee_bdd($requete, array(
array('i', $id_shipmember), array('i', $id_lien), array('s', strtolower(trim((string)$email)))),
__FILE__, __LINE__);
if (Nb_lignes_modifiees() == 1) return ['ok' => true];
return ['ok' => false, 'raison' => t("Cette invitation n'est plus valable (déjà utilisée, expirée, révoquée, ou adressée à une autre adresse).")];
}
//************************************************************************************************************
// Fonction : Refuse une invitation (elle passe à 'revoque').
// Reçoit : L'ID de l'invitation et l'adresse e-mail du ShipMember connecté
// Retour : true si l'invitation a bien été refusée
//************************************************************************************************************
function Refuser_invitation($id_lien, $email)
{
$requete = "UPDATE TB_Lien_bateau_shipmember SET Statut='revoque'
WHERE ID=? AND Statut='en_attente' AND Email_invite=?";
Requete_preparee_bdd($requete, array(array('i', $id_lien), array('s', strtolower(trim((string)$email)))), __FILE__, __LINE__);
return (Nb_lignes_modifiees() == 1);
}
//************************************************************************************************************
// Fonction : Masque partiellement une adresse e-mail pour l'afficher à quelqu'un qui n'est pas encore connecté
// (ex : "pierre.durand@exemple.fr" -> "p•••@exemple.fr").
// Reçoit : L'adresse
// Retour : L'adresse masquée
//************************************************************************************************************
function Masquer_email($email)
{
$position = strpos((string)$email, '@');
if ($position === false || $position == 0) return "•••";
return mb_substr($email, 0, 1).'•••'.substr($email, $position);
}
//************************************************************************************************************
// Clé du type d'accès "Propriétaire" dans TB_Types_acces_bateau_shipmember (cf Devalidation_profils = 1 : tous
// les types SAUF Propriétaire).
//************************************************************************************************************
const TYPE_ACCES_PROPRIETAIRE = 2;
//************************************************************************************************************
// Fonction : Charge un accès (ou une invitation) À CONDITION qu'il appartienne au bateau indiqué - toute action
// sur un accès passe par là, pour qu'un identifiant envoyé par le navigateur ne puisse jamais viser
// l'accès d'un autre bateau.
// Reçoit : L'ID de l'accès et la clé primaire du bateau affiché
// Retour : La ligne (ID, Statut, Code, ID_Type_acces_bateau_shipmember, Expiration_invitation) ou null
//************************************************************************************************************
function Lien_du_bateau($id_lien, $id_bateau)
{
$requete = "SELECT ID, Statut, Code, ID_Type_acces_bateau_shipmember, Expiration_invitation
FROM TB_Lien_bateau_shipmember WHERE ID=? AND ID_Bateau=?";
$reponse = Requete_preparee_bdd($requete, array(array('i', $id_lien), array('i', $id_bateau)), __FILE__, __LINE__);
$donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC);
Liberation_jeu_de_resultat_bdd($reponse);
return $donnees ?: null;
}
//************************************************************************************************************
// Fonction : Indique si l'utilisateur peut suspendre, réactiver ou révoquer un accès d'un type donné.
// Administrateur : toujours. Devalidation_profils = 2 : tous les types. = 1 : tous sauf Propriétaire.
// Reçoit : Les droits de l'utilisateur sur le bateau ($acces_profil), s'il est administrateur, le type visé
// Retour : true si autorisé
//************************************************************************************************************
function Peut_devalider_acces($acces_profil, $est_administrateur, $id_type_cible)
{
if ($est_administrateur) return true;
$niveau = isset($acces_profil['Devalidation_profils']) ? (int)$acces_profil['Devalidation_profils'] : 0;
if ($niveau >= 2) return true;
if ($niveau == 1) return ((int)$id_type_cible != TYPE_ACCES_PROPRIETAIRE);
return false;
}
//************************************************************************************************************
// Fonction : Met à jour en session la liste des accès actifs du ShipMember connecté ($_SESSION['bateau_shipmember']
// et $_SESSION['acces']), à partir de la base - la liste est REMPLACÉE (l'ancien code y ajoutait des
// lignes sans la vider : des accès révoqués pouvaient y rester jusqu'à la déconnexion).
// Reçoit : La clé primaire du ShipMember
// Retour : Rien
//************************************************************************************************************
function Rafraichir_acces_session($id_shipmember)
{
$_SESSION['bateau_shipmember'] = Acces_actifs_shipmember($id_shipmember);
$_SESSION['acces'] = count($_SESSION['bateau_shipmember']);
}