<?php
/**
 * @version $Id$
 * @author Thomas Crespin <thomas.crespin@sesamath.net>
 * @copyright Thomas Crespin 2009-2022
 * 
 * ****************************************************************************************************
 * SACoche <https://sacoche.sesamath.net> - Suivi d’Acquisitions de Compétences
 * © Thomas Crespin pour Sésamath <https://www.sesamath.net> - Tous droits réservés.
 * Logiciel placé sous la licence libre Affero GPL 3 <https://www.gnu.org/licenses/agpl-3.0.html>.
 * ****************************************************************************************************
 * 
 * Ce fichier est une partie de SACoche.
 * 
 * SACoche est un logiciel libre ; vous pouvez le redistribuer ou le modifier suivant les termes 
 * de la “GNU Affero General Public License” telle que publiée par la Free Software Foundation :
 * soit la version 3 de cette licence, soit (à votre gré) toute version ultérieure.
 * 
 * SACoche est distribué dans l’espoir qu’il vous sera utile, mais SANS AUCUNE GARANTIE :
 * sans même la garantie implicite de COMMERCIALISABILITÉ ni d’ADÉQUATION À UN OBJECTIF PARTICULIER.
 * Consultez la Licence Publique Générale GNU Affero pour plus de détails.
 * 
 * Vous devriez avoir reçu une copie de la Licence Publique Générale GNU Affero avec SACoche ;
 * si ce n’est pas le cas, consultez : <http://www.gnu.org/licenses/>.
 * 
 */

class Outil
{

  // //////////////////////////////////////////////////
  // Méthodes publiques
  // //////////////////////////////////////////////////

  /**
   * Déterminer le nombre de fois où on peut appeler password_hash().
   * En 2020 un appel à crypter_mdp() ou verifier_mdp() prenait en moyenne 75ms, donc pour 100 itérations c'était 7.5s.
   * En 2025, suite à des dépassement de durée d’exécution, j’ai mesuré que 50 itérations prenaient un peu moins de 13s, d’où un passage à 30 itérations pour un peu moins de 8s.
   * La doc dit que depuis PHP 8.4 "la valeur par défaut de l’option cost de l’algorithme PASSWORD_BCRYPT a été augmentée de 10 à 12", je suppose que c’est l’explication.
   * Pour éviter de se faire avoir à l'avenir, plutôt que de fixer cette valeur en dur, on se base sur le temps d’une exécution.
   * 
   * @return int
   */
  public static function determiner_nb_hashages_par_appel()
  {
    $time_start = microtime(TRUE);
    Outil::crypter_mdp(str_shuffle('1234567890AZERTYUIOPqsdfghjklm~#{[|@]}*!§:%,.'));
    $time_end = microtime(TRUE);
    $time = $time_end - $time_start;
    $nombre_brut = 10 / $time;
    $nombre_dizaine = 10 * floor($nombre_brut/10);
    return max($nombre_dizaine,10);
  }

  public static $crypt_nb_iterations_script = 30; // Nombre pair requis

  /**
   * Fabriquer un login à partir de nom/prénom selon le format paramétré par l’administrateur (reste à tester sa disponibilité).
   * Cette fonction n’est appelée que depuis un espace administrateur ; $_SESSION['TAB_PROFILS_ADMIN']['LOGIN_MODELE'] est donc défini.
   * 
   * @param string $prenom
   * @param string $nom
   * @param string $profil_sigle
   * @return string
   */
  public static function fabriquer_login( $prenom , $nom , $profil_sigle )
  {
    $modele = $_SESSION['TAB_PROFILS_ADMIN']['LOGIN_MODELE'][$profil_sigle];
    $login_prenom = mb_substr( str_replace(array('.','-','_'),'',Clean::login($prenom)) , 0 , mb_substr_count($modele,'p') );
    $login_nom    = mb_substr( str_replace(array('.','-','_'),'',Clean::login($nom))    , 0 , mb_substr_count($modele,'n') );
    $login_separe = str_replace(array('p','n'),'',$modele);
    $login = ($modele[0]=='p') ? $login_prenom.$login_separe.$login_nom : $login_nom.$login_separe.$login_prenom ;
    return $login;
  }

  /**
   * Fabriquer un mot de passe.
   * Cette fonction peut être appelée par le webmestre ou à l’installation ; $_SESSION['TAB_PROFILS_ADMIN']['MDP_LONGUEUR_MINI'] n’est alors pas défini -> dans ce cas, on ne transmet pas de paramètre.
   * 
   * Certains caractères sont évités :
   * "e" sinon un tableur peut interpréter le mot de passe comme un nombre avec exposant
   * "i"j"1"l" pour éviter une confusion entre eux
   * "m"w" pour éviter la confusion avec "nn"vv"
   * "o"0" pour éviter une confusion entre eux
   * 
   * @param string|int $profil_sigle | $nb_chars
   * @return string
   */
  public static function fabriquer_mdp($profil_or_length=NULL)
  {
    if(is_string($profil_or_length))
    {
      $nb_chars =$_SESSION['TAB_PROFILS_ADMIN']['MDP_LONGUEUR_MINI'][$profil_or_length];
    }
    else
    {
      $nb_chars = (is_int($profil_or_length)) ? $profil_or_length : 8 ;
    }
    return mb_substr(str_shuffle('2345678923456789aaabcdfghknpqrstuuuvxyzAAABCDFGHJKLMNPRSTUUUVXYZ'), 0, $nb_chars);
  }

  /**
   * N’est plus utilisé que pour des clefs de confirmation.
   * 
   * @param string $chaine
   * @return string
   */
  public static function clef_md5($chaine)
  {
    return md5('grain_de_sel'.$chaine);
  }

  /**
   * Crypter un mot de passe avant enregistrement dans la base.
   *
   * Attention, password_hash() prend du temps : 7 à 8 secondes pour 100 hashages.
   * Du coup, on peut vite dépasser un timeout de 20s ou 30s pour un enregistrement en masse de mdp.
   * Avant on pouvait se rabattre sur crypt() mais maintenant ce n’est plus le cas, tout utilise bcrypt qui est conçu pour être lent.
   * Du coup il faut boucler avec plusieurs appels, par paquets de 100 vérifications.
   *
   * @see https://www.php.net/manual/fr/faq.passwords.php
   * @see https://www.php.net/manual/fr/ref.password.php
   * @see https://www.php.net/password_hash
   * @see https://www.php.net/crypt
   * @see https://fr.wikipedia.org/wiki/%C3%89tirement_de_cl%C3%A9
   *
   * @param string $password
   * @return string
   */
  public static function crypter_mdp( $password )
  {
    if( function_exists('password_hash') )
    {
      // À généraliser mais requiert PHP 5.5
      return password_hash( $password , PASSWORD_DEFAULT );
    }
    else
    {
      // Sans le paramètre salt, crypt() crée un hash faible ; PHP 5.6 et suivants lancent dans ce cas une alerte de niveau E_NOTICE.
      return crypt( $password , Outil::fabriquer_mdp() );
    }
  }

  /**
   * Tester si le hash a le format d’un md5
   * 
   * @param string $password_hash
   * @return int|bool
   */
  public static function is_md5( $password_hash )
  {
    $masque_md5 = '#^[a-f0-9]{32}$#';
    return !empty($password_hash) ? preg_match( $masque_md5 , $password_hash ) : FALSE ;
  }

  /**
   * Vérifier le hash d’un mot de passe
   *
   * Attention, password_verify() ou hash_equals() prennent du temps : 7 à 8 secondes pour 100 hashages.
   * Du coup, on peut vite dépasser un timeout de 20s ou 30s pour une vérification en masse de mdp.
   * Avant on pouvait se rabattre sur crypt() mais maintenant ce n’est plus le cas, tout utilise bcrypt qui est conçu pour être lent.
   * Du coup il faut boucler avec plusieurs appels, par paquets de 100 vérifications.
   *
   * @see https://www.php.net/manual/fr/faq.passwords.php
   * @see https://www.php.net/manual/fr/ref.password.php
   * @see https://www.php.net/password_verify
   * @see https://www.php.net/hash_equals
   * @see https://www.php.net/crypt
   * @see https://fr.wikipedia.org/wiki/%C3%89tirement_de_cl%C3%A9
   *
   * @param string $password
   * @param string $password_hash
   * @return bool
   */
  public static function verifier_mdp( $password , $password_hash )
  {
    if(!Outil::is_md5($password_hash))
    {
      // nouvelle méthode
      if( function_exists('password_verify') )
      {
        // À généraliser mais requiert PHP 5.5
        return password_verify( $password , $password_hash );
      }
      else if( function_exists('hash_equals') )
      {
        // Recommandé sinon mais requiert PHP 5.6
        return hash_equals( $password_hash , crypt($password, $password_hash) );
      }
      else
      {
        return ( $password_hash == crypt($password, $password_hash) );
      }
    }
    else
    {
      // ancienne méthode
      return ( $password_hash == Outil::clef_md5($password) );
    }
  }

  /**
   * Tester si le hashage devrait être amélioré
   * 
   * @see http://php.net/manual/fr/faq.passwords.php
   * @see http://php.net/manual/fr/ref.password.php
   * @see http://php.net/manual/fr/function.crypt.php
   * 
   * @param string $password_hash
   * @return bool
   */
  public static function tester_mdp_hash_insuffisant( $password_hash )
  {
    if(!Outil::is_md5($password_hash))
    {
      // nouvelle méthode
      if( function_exists('password_needs_rehash') )
      {
        // Requiert PHP 5.5
        return password_needs_rehash( $password_hash , PASSWORD_DEFAULT );
      }
      else
      {
        return FALSE;
      }
    }
    else
    {
      // ancienne méthode
      return TRUE;
    }
  }

  /**
   * Récupérer le numéro de la dernière version de SACoche disponible auprès du serveur communautaire.
   * 
   * @return string 'AAAA-MM-JJi' ou message d’erreur
   */
  public static function recuperer_numero_derniere_version()
  {
    $requete_reponse = cURL::get_contents(SERVEUR_VERSION);
    return (preg_match('#^[0-9]{4}\-[0-9]{2}\-[0-9]{2}[a-z]?$#',$requete_reponse)) ? $requete_reponse : 'Dernière version non détectée&hellip;' ;
  }

  /**
   * Tester si une adresse de courriel semble normale.
   * 
   * Utilisé pour une récupération via un CSV parce que pour un champ de saisie javascript fait déjà le ménage.
   * http://fr2.php.net/manual/fr/function.preg-match.php#96910
   * 
   * @param string   $courriel
   * @return bool
   */
  public static function tester_courriel($courriel)
  {
    return !empty($courriel) && preg_match('/^[^@]+@[a-zA-Z0-9._-]+\.[a-zA-Z]+$/',$courriel) ? TRUE : FALSE;
  }

  /**
   * Vérifier que le domaine du serveur mail peut recevoir des mails, c’est à dire qu’il a un serveur de mail déclaré dans les DNS).
   * Ça évite tous les domaines avec une coquille du genre @gmaill.com, @hoatmail.com, @gmaol.com, @laoste.net, etc.
   *
   * @param string              $mail_adresse
   * @return array(string,bool) le domaine + TRUE|FALSE
   */
  public static function tester_domaine_courriel_valide($mail_adresse)
  {
    $mail_domaine = mb_substr( $mail_adresse , mb_strpos($mail_adresse,'@')+1 );
    $hasMx = function_exists('getmxrr') && getmxrr($mail_domaine, $tab_mxhosts);
    $isPort25open = FALSE;
    if (!$hasMx)
    {
      $res = @fsockopen($mail_domaine, 25, $errno, $errstr, 5);
      if ($res)
      {
        fclose($res);
        $isPort25open = TRUE;
      }
    }
    $is_domaine_valide = $hasMx || $isPort25open ;
    return array( $mail_domaine , $is_domaine_valide );
  }

  /**
   * Tester si un numéro UAI est valide.
   * 
   * Utilisé pour une récupération via un CSV parce que pour un champ de saisie javascript fait déjà le ménage.
   * 
   * @param string   $uai
   * @return bool
   */
  public static function tester_UAI($uai)
  {
    if(empty($uai))
    {
      return FALSE;
    }
    $alphabet = 'ABCDEFGHJKLMNPRSTUVWXYZ';
    $uai_lettre = substr($uai, -1);
    // Base RAMSESE (établissement du système éducatif français) : 7 chiffres suivis d’une lettre de contrôle basée sur le modulo 23 du nombre
    if(preg_match('#^[0-9]{7}['.$alphabet.']{1}$#', $uai))
    {
      $reste = substr($uai, 0, 7) % 23 ;
      return ( $uai_lettre == substr($alphabet, $reste, 1) ) ? TRUE : FALSE ;
    }
    // Base RHODES (organismes de détachement hors éducation) : 6 chiffres suivis du caractère "X" puis d’une lettre de contrôle basée sur le modulo 23 du ( nombre * 8 + 1 )
    if(preg_match('#^[0-9]{6}X['.$alphabet.']{1}$#', $uai))
    {
      $reste = (substr($uai, 0, 6) * 8 + 1 ) % 23 ;
      return ( $uai_lettre == substr($alphabet, $reste, 1) ) ? TRUE : FALSE ;
    }
    return FALSE;
  }

  /**
   * Tester si une date est valide : format AAAA-MM-JJ par exemple.
   * 
   * Utilisé pour une récupération via un CSV parce que pour un champ de saisie javascript fait déjà le ménage.
   * 
   * @param string   $date
   * @return bool
   */
  public static function tester_date($date)
  {
    $date_unix = strtotime($date);
    return ( ($date_unix!==FALSE) && ($date_unix!==-1) ) ? TRUE : FALSE ;
  }

  /**
   * Renvoyer les balises images à afficher et la chaine solution à mettre en session.
   * 
   * @return array   [$html_imgs,$captcha_soluce]
   */
  public static function captcha()
  {
    $tab_base64 = array(
      0 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAI4hI+pwe3fDJxPQjStWxV0HxzdpoXliYJMSqpmC0euKM/2TcYrqpv2t3O9PrTcpWikUEpKouKJKAAAOw==',
      1 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAIzhI+pwe3fDJxPznNBtlV3DHHMEnql6YipOqKr21JgJ7OBfdv4vNZw7ssES5Qi0LhRKBMFADs=',
      2 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAI4hI+pwe3fDJxP1gRtVOA6xIDbpwUHSW3dmIrhOrlvLM9XiZJ4rqYm/APSdBxesMjyZYTKF+eJKAAAOw==',
      3 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAI3hI+pwe3fDJxPQjStWxV0HxzdpoXliYJMebHsio1tHKmZa4t1zvfkB7OZZrof8UZJDpU3hVNRAAA7',
      4 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAI2hI+pwe3fDJxPTgRtPvviCngcuH2OFiXdKY0sGLopGwYygyqmXeN63iutREMhiYJEIWG/JqAAADs=',
      5 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAI4hI+pwe3fDJxPOmUjqJjzTQUZc4QiSI7T2EFse6EnWsbmbJt1Gsv72QP9gMEhsbiS3TRLF+aZKAAAOw==',
      6 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAI5hI+pwe3fDJxPQjStU6D2bR0e+DFiVAYnpaZc5r4jGmpmjIPkjvJ37PmsbpdEEWbrsSjJJY0DPRQAADs=',
      7 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAI2hI+pwe3fDJxPRnWcZThfegGaCAYeR1InWq5mCrkvKG+V/U3yrONs7PvdYKMeMVcCJo2dJqAAADs=',
      8 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAI4hI+pwe3fDJxPQjStWxV0HxzdpoXliYJMaYIieSVxjI2ky6Y3HOn9t8qpejXeZ/ijKFnKm+KZKAAAOw==',
      9 => 'R0lGODlhFAAUAIAAAGZm/93d/yH5BAEKAAAALAAAAAAUABQAAAI4hI+pwe3fDJxPQjStWxV0HxzdpoXliYJMSqrmt4qRaypuOSckle/9H4PVRh+Zr5ih8Y7Lou1pKAAAOw==',
    );
    $rand_keys   = str_shuffle('0123456789');
    $rand_values = str_shuffle('abcdefghijklmnopqrstuvxyz');
    $tab_values = array();
    $html_imgs = '';
    for( $i=1 ; $i<=6 ; $i++ )
    {
      $key   = $rand_keys[$i];
      $value = $rand_values[$i];
      $tab_values[$key] = $value;
      $html_imgs .= '<img class="captcha" id="cap_'.$value.'" src="data:image/gif;base64,'.$tab_base64[$key].'">';
    }
    ksort($tab_values);
    $captcha_soluce = implode('',$tab_values);
    return array($html_imgs,$captcha_soluce);
  }

  /**
   * Tester si on est dans la période de rentrée
   * Par défaut : 01/08 -> 30/09
   *
   * @return bool
   */
  public static function test_periode_rentree()
  {
    $mois_actuel    = date('n');
    $mois_bascule   = $_SESSION['MOIS_BASCULE_ANNEE_SCOLAIRE'] ; // par défaut août
    $mois_rentree   = ($mois_bascule<12) ? $mois_bascule+1 : 1 ; // par défaut septembre
    return ($mois_actuel==$mois_bascule) || ($mois_actuel==$mois_rentree) ;
  }

  /**
   * Tester si on est dans la période de sortie
   * Par défaut : 01/06 -> 31/07
   *
   * @return bool
   */
  public static function test_periode_sortie()
  {
    $mois_actuel    = date('n');
    $mois_bascule   = $_SESSION['MOIS_BASCULE_ANNEE_SCOLAIRE'] ; // par défaut août
    $mois_sortie    = ($mois_bascule>1) ? $mois_bascule-1 : 12 ; // par défaut juillet
    $mois_fin_annee = ($mois_sortie>1)  ? $mois_sortie-1  : 12 ; // par défaut juin
    return ($mois_actuel==$mois_sortie) || ($mois_actuel==$mois_fin_annee) ;
  }

  /*
   * Vérifier qu’une liste d’items récupérés via un formulaire sont bien dans la base
   * 
   * @param array
   * @return void
   */
  public static function verif_items_base( $tab_item_id )
  {
    $nb_items_transmis = count($tab_item_id);
    if($nb_items_transmis)
    {
      $DB_TAB = DB_STRUCTURE_PROFESSEUR::DB_lister_id_items( implode(',',$tab_item_id) );
      $nb_items_trouves = count($DB_TAB);
      if( $nb_items_trouves != $nb_items_transmis )
      {
        $tab_item_inconnu = array_diff( $tab_item_id , $DB_TAB );
        $s = ( count($tab_item_id) > 1 ) ? 's' : '' ;
        Json::end( FALSE , 'Item'.$s.' '.implode(',',$tab_item_id).' introuvable'.$s.' en base !' );
      }
    }
  }

  /*
   * Vérifier qu’une liste d’élèves récupérés via un formulaire sont bien ceux d’un professeur
   * 
   * @param array
   * @return void
   */
  public static function verif_eleves_prof( $tab_eleve_id )
  {
    $tab_eleves_non_rattaches = array_diff( $tab_eleve_id , $_SESSION['PROF_TAB_ELEVES'] );
    if(!empty($tab_eleves_non_rattaches))
    {
      // On vérifie de nouveau, au cas où l’admin viendrait d’ajouter une affectation
      Session::_set('PROF_TAB_ELEVES' , DB_STRUCTURE_PROFESSEUR::DB_lister_ids_eleves_professeur( $_SESSION['USER_ID'] , $_SESSION['USER_JOIN_GROUPES'] , 'array' /*format_retour*/ ) );
      $tab_eleves_non_rattaches = array_diff( $tab_eleve_id , $_SESSION['PROF_TAB_ELEVES'] );
      if(!empty($tab_eleves_non_rattaches))
      {
        Json::end( FALSE , 'Élève(s) non rattaché(s) à votre compte enseignant !' );
      }
    }
  }

  /*
   * Vérifier qu’un élève transmis via un formulaire est bien celui d’un professeur
   * 
   * @param int
   * @return void
   */
  public static function verif_eleve_prof( $eleve_id )
  {
    if(!in_array($eleve_id, $_SESSION['PROF_TAB_ELEVES']))
    {
      // On vérifie de nouveau, au cas où l’admin viendrait d’ajouter une affectation
      Session::_set('PROF_TAB_ELEVES' , DB_STRUCTURE_PROFESSEUR::DB_lister_ids_eleves_professeur( $_SESSION['USER_ID'] , $_SESSION['USER_JOIN_GROUPES'] , 'array' /*format_retour*/ ) );
      if(!in_array($eleve_id, $_SESSION['PROF_TAB_ELEVES']))
      {
        Json::end( FALSE , 'Élève non rattaché à votre compte enseignant !' );
      }
    }
  }

  /*
   * Vérifier qu’un enfant transmis via un formulaire est bien celui d’un parent
   * 
   * @param int
   * @return void
   */
  public static function verif_enfant_parent( $eleve_id )
  {
    $is_enfant_legitime = FALSE;
    foreach($_SESSION['OPT_PARENT_ENFANTS'] as $DB_ROW)
    {
      if($DB_ROW['valeur']==$eleve_id)
      {
        $is_enfant_legitime = TRUE;
        break;
      }
    }
    if(!$is_enfant_legitime)
    {
      Json::end( FALSE , 'Enfant non rattaché à votre compte parent !' );
    }
  }

  /**
   * Tester si un droit d’accès spécifique comporte une restriction aux PP ou aux coordonnateurs
   *
   * @param string $listing_droits_sigles
   * @param string $restriction   'ONLY_PP' | 'ONLY_COORD'
   * @return bool
   */
  public static function test_droit_specifique_restreint( $listing_droits_sigles , $restriction )
  {
    return (strpos($listing_droits_sigles,$restriction)!==FALSE);
  }

  /**
   * Tester si on a un droit d’accès spécifique
   *
   * @param string $listing_droits_sigles
   * @param int    $matiere_coord_or_groupe_pp_connu   si le droit comporte une restriction aux coordonnateurs matières | professeurs principaux, on peut déja connaitre et transmettre l’info (soit pour au moins une matière | classe, soit pour une matière | classe donnée)
   * @param int    $matiere_id_or_groupe_id_a_tester   si le droit comporte une restriction aux coordonnateurs matières | professeurs principaux, et si $matiere_coord_or_groupe_pp_connu n’est pas transmis, on peut chercher si le droit est bon soit pour une matière | classe donnée, soit pour au moins une matière | classe
   * @param string $forcer_profil                      pour forcer à tester un profil donné au lieu du profil de l’utilisateur
   * @return bool
   */
  public static function test_user_droit_specifique( $listing_droits_sigles , $matiere_coord_or_groupe_pp_connu=NULL , $matiere_id_or_groupe_id_a_tester=0 , $forcer_profil=NULL )
  {
    if( $forcer_profil=='TUT' )
    {
      // profil parent forcé pour des impressions de bilans officiels
      $user_profil_sigle = 'TUT' ;
      $user_profil_type  = 'parent' ;
    }
    elseif( $_SESSION['USER_PROFIL_SIGLE']=='ADM' )
    {
      // cas d’un admin qui consulte ou archive des données d’un bilan officiel
      $user_profil_sigle = 'DIR' ;
      $user_profil_type  = 'directeur' ;
    }
    else
    {
      $user_profil_sigle = $_SESSION['USER_PROFIL_SIGLE'];
      $user_profil_type  = $_SESSION['USER_PROFIL_TYPE'];
    }
    $tableau_droits_sigles = explode(',',$listing_droits_sigles);
    $test_droit = in_array($user_profil_sigle,$tableau_droits_sigles);
    if( $test_droit && ($user_profil_type=='professeur') && ($_SESSION['USER_JOIN_GROUPES']=='config') && Outil::test_droit_specifique_restreint($listing_droits_sigles,'ONLY_PP') )
    {
      return ($matiere_coord_or_groupe_pp_connu!==NULL) ? (bool)$matiere_coord_or_groupe_pp_connu : DB_STRUCTURE_PROFESSEUR::DB_tester_prof_principal($_SESSION['USER_ID'],$matiere_id_or_groupe_id_a_tester) ;
    }
    if( $test_droit && ($user_profil_type=='professeur') && ($_SESSION['USER_JOIN_MATIERES']=='config') && Outil::test_droit_specifique_restreint($listing_droits_sigles,'ONLY_COORD') )
    {
      return ($matiere_coord_or_groupe_pp_connu!==NULL) ? (bool)$matiere_coord_or_groupe_pp_connu : DB_STRUCTURE_PROFESSEUR::tester_prof_coordonnateur($_SESSION['USER_ID'],$matiere_id_or_groupe_id_a_tester) ;
    }
    return $test_droit;
  }

  /**
   * Afficher les profils ayant un droit d’accès spécifique
   *
   * @param string $listing_droits_sigles
   * @param string $format   "li" | "br"
   * @return bool
   */
  public static function afficher_profils_droit_specifique( $listing_droits_sigles , $format )
  {
    $tab_profils = array();
    $texte_testriction_pp    = Outil::test_droit_specifique_restreint($listing_droits_sigles,'ONLY_PP')    ? ' restreint aux professeurs principaux'  : '' ;
    $texte_testriction_coord = Outil::test_droit_specifique_restreint($listing_droits_sigles,'ONLY_COORD') ? ' restreint aux coordonnateurs matières' : '' ;
    $tableau_droits_sigles = explode(',',$listing_droits_sigles);
    foreach($tableau_droits_sigles as $droit_sigle)
    {
      if(isset($_SESSION['TAB_PROFILS_DROIT']['TYPE'][$droit_sigle]))
      {
        $profil_nom  = $_SESSION['TAB_PROFILS_DROIT']['NOM_LONG_PLURIEL'][$droit_sigle];
        $profil_nom .= ( ($_SESSION['TAB_PROFILS_DROIT']['TYPE'][$droit_sigle]=='professeur') && ($_SESSION['TAB_PROFILS_DROIT']['JOIN_GROUPES'][$droit_sigle]=='config')  ) ? $texte_testriction_pp    : '' ;
        $profil_nom .= ( ($_SESSION['TAB_PROFILS_DROIT']['TYPE'][$droit_sigle]=='professeur') && ($_SESSION['TAB_PROFILS_DROIT']['JOIN_MATIERES'][$droit_sigle]=='config') ) ? $texte_testriction_coord : '' ;
        $tab_profils[] = $profil_nom;
      }
    }
    if(!count($tab_profils))
    {
      $tab_profils[] = 'aucun profil autorisé';
    }
    return ($format=='li') ? '<ul class="puce">'.NL.'<li>'.implode('</li>'.NL.'<li>',$tab_profils).'</li>'.NL.'</ul>'.NL : implode(BRJS,$tab_profils) ;
  }

  /**
   * Afficher un texte tronqué au dela d’un certain nombre de caractères
   *
   * @param string $texte
   * @param int    $longueur_max
   * @return string
   */
  public static function afficher_texte_tronque( $texte , $longueur_max )
  {
    if( is_null($texte) || ( mb_strlen($texte) < $longueur_max ) )
    {
      return $texte;
    }
    $pos_espace = mb_strpos( $texte , ' ' , $longueur_max-10 );
    $chaine_de_fin = ' [...]';
    if($pos_espace!==FALSE)
    {
      return mb_substr( $texte , 0 , $pos_espace ).$chaine_de_fin;
    }
    return mb_substr( $texte , 0 , $longueur_max-5 ).$chaine_de_fin;
  }

  /**
   * Calculer la distance Levenshtein entre 2 chaines et retourner la réponse sous forme de pourcentage
   * Autre méthode dénichée mais non essayée : http://tonyarchambeau.com/blog/400-php-coefficient-de-dice/
   *
   * Attention, levenshtein() requiert des arguments de moins de 256 caractères.
   * De plus, cela compte double les caractères accentués dans la réponse, et 256 caractères Unicode provoquent un dépassement.
   * On convertit donc d'abord dans un encodage sur 1 bit (http://php.net/manual/fr/function.levenshtein.php#113702)
   *
   * @see http://fr.php.net/levenshtein
   * @param string $string1
   * @param string $string2
   * @return floor
   */
  private static function utf8_to_extended_ascii($str, &$map)
  {
    // find all multibyte characters (cf. utf-8 encoding specs)
    $matches = array();
    if (!preg_match_all('/[\xC0-\xF7][\x80-\xBF]+/', $str, $matches))
      return $str; // plain ascii string
    // update the encoding map with the characters not already met
    foreach ($matches[0] as $mbc)
      if (!isset($map[$mbc]))
        $map[$mbc] = chr(128 + count($map));
    // finally remap non-ascii characters
    return strtr($str, $map);
  }
  public static function pourcentage_commun( $string1 , $string2 )
  {
    $charMap = array();
    $string1 = Outil::utf8_to_extended_ascii($string1, $charMap);
    $string2 = Outil::utf8_to_extended_ascii($string2, $charMap);
    // levenshtein() est sensible à la casse
    $string1 = Clean::lower($string1);
    $string2 = Clean::lower($string2);
    // levenshtein() requiert des arguments < 256 caractères (renvoie -1 sinon)
    $string1_longueur = min( mb_strlen($string1) , 255 );
    if($string1_longueur==255)
    {
      $string1 = mb_substr($string1,0,255);
    }
    $string2_longueur = min( mb_strlen($string2) , 255 );
    if($string2_longueur==255)
    {
      $string2 = mb_substr($string2,0,255);
    }
    // on compare les chaînes tronquées
    $nb_differences = levenshtein( $string1 , $string2 );
    // on calcule le pourcentage en commun ; max(*,0) car levenshtein() compte double les caractères accentués
    $longueur = max( $string1_longueur , $string2_longueur );
    $pourcent_commun = 100 * max( $longueur - $nb_differences , 0 ) / $longueur ;
    return $pourcent_commun;
  }

  /**
   * Formater les liens selon un code perso
   *
   * En attendant un éventuel textarea enrichi pour la saisie des messages (mais est-ce que ça ne risquerait pas de faire une page d’accueil folklorique ?), une petite fonction pour fabriquer des liens...
   * Format attendu : [descriptif|adresse] ou [descriptif|adresse|target]
   *
   * @param string $texte
   * @param int    $longueur_max
   * @param string $contexte   html | mail
   * @return string
   */
  public static function make_lien( $texte , $contexte )
  {
    $masque_recherche = '#\['.'([^\|\]]+)'.'\|'.'([^\|\]]+)'.'\|'.'([^\|\]]+)'.'\]#' ;
    $masque_remplacement = ($contexte=='html') ? '<a href="$2" target="$3">$1</a>' : '$1 [$2]' ;
    $texte = preg_replace( $masque_recherche , $masque_remplacement , $texte );
    $masque_recherche = '#\['.'([^\|\]]+)'.'\|'.'([^\|\]]+)'.'\]#' ;
    $masque_remplacement = ($contexte=='html') ? '<a href="$2">$1</a>' : '$1 [$2]' ;
    $texte = preg_replace( $masque_recherche , $masque_remplacement , $texte );
    return str_replace('javascript:','',$texte);
  }

  /**
   * Ajout d’un log PHP dans le fichier error-log du serveur Web
   * 
   * @param string $log_objet       objet du log
   * @param string $log_contenu     contenu du log
   * @param string $log_fichier     transmettre __FILE__
   * @param string $log_ligne       transmettre __LINE__
   * @param bool   $only_sesamath   [TRUE] pour un log uniquement sur le serveur Sésamath, [FALSE] sinon
   * @return void
   */
  public static function ajouter_log_PHP( $log_objet , $log_contenu , $log_fichier , $log_ligne , $only_sesamath )
  {
    if( (!$only_sesamath) || (strpos(URL_INSTALL_SACOCHE,SERVEUR_PROJET)===0) )
    {
      $SEP = ' ║ ';
      $log_intro = ($only_sesamath) ? 'SACoche DEBUG' : 'SACoche INFO' ;
      $log_base = !empty($_SESSION['BASE'])    ? $SEP . 'base '.$_SESSION['BASE'] : '' ;
      $log_user = !empty($_SESSION['USER_ID']) ? $SEP . 'base '.$_SESSION['USER_ID'] : '' ;
      error_log( $log_intro . $SEP . $log_objet . $log_base . $log_user . $SEP . basename($log_fichier).' '.$log_ligne . $SEP . $log_contenu , 0 );
    }
  }

  /**
   * Récupérer et mettre en session les seuils pour les degrés de maîtrise du livret
   * 
   * @param string $livret_page_ref
   * @param string $livret_page_colonne facultatif et requis uniquement pour 6e 5e 4e 3e où il peut y avoir position ou objectif
   * @return void
   */
  public static function recuperer_seuils_livret( $livret_page_ref , $livret_page_colonne=NULL )
  {
    $DB_TAB = DB_STRUCTURE_LIVRET::DB_lister_page_seuils_infos( $livret_page_ref , $livret_page_colonne );
    foreach($DB_TAB as $DB_ROW)
    {
      $id = $DB_ROW['livret_colonne_id'] % 10 ; // 1 2 3 et 4 sauf pour le cycle 1
      Session::_set('LIVRET',$id,'USED'      , TRUE);
      Session::_set('LIVRET',$id,'SEUIL_MIN' , $DB_ROW['livret_seuil_min']);
      Session::_set('LIVRET',$id,'SEUIL_MAX' , $DB_ROW['livret_seuil_max']);
      Session::_set('LIVRET',$id,'LEGENDE'   , $DB_ROW['livret_colonne_legende']);
    }
    if($livret_page_ref=='cycle1')
    {
      Session::_set('LIVRET',4,'USED' , FALSE);
    }
  }

  /**
   * Récupèrer le placement des élèves, positionner au passage les élèves s’ils ne l’ont pas été, corriger d’éventuelles anomalies
   * 
   * @param int   $plan_id
   * @param int   $nb_rangees
   * @param int   $nb_colonnes
   * @param array $tab_eleve
   * @return array
   */
  public static function recuperer_ajuster_places_eleves( $plan_id , $nb_rangees , $nb_colonnes , $tab_eleve )
  {
    // Tableaux utilisés (le premier sera retourné)
    $tab_places_occupees = array_fill(0, $nb_rangees, array_fill(0, $nb_colonnes, NULL) );
    $tab_eleve_a_deplacer = array();
    $tab_places_libres = array();
    for( $num_rangee=0 ; $num_rangee<$nb_rangees ; $num_rangee++ )
    {
      for( $num_colonne=0 ; $num_colonne<$nb_colonnes ; $num_colonne++ )
      {
        $tab_places_libres[$num_rangee.'x'.$num_colonne] = TRUE;
      }
    }
    $DB_TAB = DB_STRUCTURE_PROFESSEUR_PLAN::DB_lister_plan_eleves( $plan_id );
    if(!empty($DB_TAB))
    {
      foreach($DB_TAB as $DB_ROW)
      {
        $eleve_id = $DB_ROW['eleve_id'];
        if(!isset($tab_eleve[$eleve_id]))
        {
          // élève à retirer du plan de classe
          DB_STRUCTURE_PROFESSEUR_PLAN::DB_supprimer_plan_eleve($plan_id , $eleve_id);
        }
        elseif( ($DB_ROW['jointure_rangee']>=$nb_rangees) || ($DB_ROW['jointure_colonne']>=$num_colonne) || !is_null($tab_places_occupees[$DB_ROW['jointure_rangee']][$DB_ROW['jointure_colonne']]) )
        {
          // élève à replacer (ultérieurement) car en dehors du plan de classe ou à une place déjà prise
          $tab_eleve_a_deplacer[$eleve_id] = array(
            'ordre'  => $DB_ROW['jointure_ordre'],
            'equipe' => $DB_ROW['jointure_equipe'],
            'role'   => $DB_ROW['jointure_role'],
          );
        }
        else
        {
          $tab_places_occupees[$DB_ROW['jointure_rangee']][$DB_ROW['jointure_colonne']] = $tab_eleve[$eleve_id] + array(
            'id'     => $eleve_id,
            'ordre'  => $DB_ROW['jointure_ordre'],
            'equipe' => $DB_ROW['jointure_equipe'],
            'role'   => $DB_ROW['jointure_role'],
          );
          unset( $tab_eleve[$eleve_id] , $tab_places_libres[$DB_ROW['jointure_rangee'].'x'.$DB_ROW['jointure_colonne']] );
        }
      }
    }
    if(!empty($tab_eleve_a_deplacer))
    {
      foreach($tab_eleve_a_deplacer as $eleve_id => $tab_eleve_data)
      {
        // élève à déplacer sur le plan de classe
        $clef = key($tab_places_libres);
        list($jointure_rangee,$jointure_colonne) = explode('x',$clef);
        DB_STRUCTURE_PROFESSEUR_PLAN::DB_modifier_plan_eleve( $plan_id , $eleve_id , $jointure_rangee , $jointure_colonne , $tab_eleve_data['ordre'] , $tab_eleve_data['equipe'] , $tab_eleve_data['role'] );
        $tab_places_occupees[$jointure_rangee][$jointure_colonne] = $tab_eleve[$eleve_id] + array(
          'id'     => $eleve_id,
          'ordre'  => $tab_eleve_data['ordre'],
          'equipe' => $tab_eleve_data['equipe'],
          'role'   => $tab_eleve_data['role'],
        );
        unset( $tab_eleve[$eleve_id] , $tab_places_libres[$clef] );
      }
    }
    if(!empty($tab_eleve))
    {
      foreach($tab_eleve as $eleve_id => $tab)
      {
        // élève à ajouter au plan de classe
        $clef = key($tab_places_libres);
        list($jointure_rangee,$jointure_colonne) = explode('x',$clef);
        DB_STRUCTURE_PROFESSEUR_PLAN::DB_ajouter_plan_eleve( $plan_id , $eleve_id , $jointure_rangee , $jointure_colonne , 0 /*jointure_ordre*/ , '' /*jointure_equipe*/ , '' /*jointure_role*/ );
        $tab_places_occupees[$jointure_rangee][$jointure_colonne] = $tab + array(
          'id'    => $eleve_id,
          'ordre' => 0,
          'equipe' => '',
          'role'   => '',
        );
        unset( $tab_places_libres[$clef] );
      }
    }
    // retour
    return $tab_places_occupees;
  }

  /**
   * Méthode pour déterminer la date de début à prendre en compte pour récupérer les évaluations selon la prise en compte des évaluations antérieures
   * 
   * @param string   $retroactif   oui | non | annuel | auto
   * @param string   $date_sql_debut
   * @param string   $date_sql_debut_annee_scolaire
   * @return string|bool
   */
  public static function date_sql_start( $retroactif , $date_sql_debut , $date_sql_debut_annee_scolaire )
  {
    if($retroactif=='non')
    {
      return $date_sql_debut;
    }
    if($retroactif=='annuel')
    {
      return min($date_sql_debut,$date_sql_debut_annee_scolaire);
    }
    // 'oui' | 'auto' ; en 'auto' il faut faire le tri après
    return FALSE;
  }

  /**
   * Méthode pour déterminer si une note doit être gardée en cas de prise en compte des évaluations antérieures automatique donc dépendant du référentiel
   * 
   * @param string   $retroactif   oui | non | annuel | auto
   * @param string   $date_sql_debut
   * @param string   $date_sql_debut_annee_scolaire
   * @return bool
   */
  public static function is_note_a_garder( $retroactif , $item_retro , $item_date , $date_sql_debut , $date_sql_debut_annee_scolaire )
  {
    return ($retroactif!='auto')
        || ($item_retro=='oui')
        || ( ($item_retro=='non') && ( $item_date >= $date_sql_debut ) )
        || ( ($item_retro=='annuel') && ( $item_date >= min($date_sql_debut,$date_sql_debut_annee_scolaire) ) ) ;
  }



}
?>