<?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/>.
 *
 */

if(!defined('SACoche')) {exit('Ce fichier ne peut être appelé directement !');}
$TITRE = 'Connexion SSO'; // Pas de traduction car pas de choix de langue à ce niveau.

/*
 * Cette page n’est pas (plus en fait) appelée directement.
 * Elle est appelée lors d’un lien direct vers une page nécessitant une identification :
 * - si des paramètres dans l’URL indiquent explicitement un SSO (nouvelle connexion, appel depuis un service tiers...)
 * - ou si des informations en cookies indiquent un SSO (session perdue mais tentative de reconnexion automatique)
 *
 * En cas d’installation de type multi-structures, SACoche doit connaître la structure concernée AVANT de lancer SAML ou CAS pour savoir si l’établissement l’a configuré ou pas, et avec quels paramètres !
 * Si on ne sait pas de quel établissement il s’agit, on ne peut pas savoir s’il y a un CAS, un SAML-GEPI, et si oui quelle URL appeler, etc.
 * (sur un même serveur il peut y avoir un SACoche avec authentification reliée à l’ENT de Nantes, un SACoche relié à un LCS, un SACoche relié à un SAML-GEPI, ...)
 * D'autre part on ne peut pas se fier à une éventuelle info transmise par SAML ou CAS ; non seulement car elle arrive trop tard comme je viens de l’expliquer, mais aussi car ce n’est pas le même schéma partout.
 * (CAS, par exemple, peut renvoyer le RNE en attribut APRES authentification à une appli donnée, dans une acad donnée, mais pas pour autant à une autre appli, ou dans une autre acad)
 *
 * Normalement on passe en GET le numéro de la base, mais il se peut qu’une connection directe ne puisse être établie qu’avec l’UAI (connu de l’ENT) et non avec le numéro de la base SACoche (inconnu de l’ENT).
 * Dans ce cas, on récupère le numéro de la base et on le remplace dans les variables PHP, pour ne pas avoir à recommencer ce petit jeu à chaque échange avec le serveur SSO pendant l’authentification.
 *
 * URL directe mono-structure             : http://adresse.com/?sso
 * URL directe multi-structures normale   : http://adresse.com/?sso&base=[BASE] | http://adresse.com/?sso&id=[BASE] | http://adresse.com/?sso=[BASE]
 * URL directe multi-structures spéciale  : http://adresse.com/?sso&uai=[UAI]   | http://adresse.com/?sso=[UAI]
 *
 * URL profonde mono-structure            : http://adresse.com/?page=...&sso
 * URL profonde multi-structures normale  : http://adresse.com/?page=...&sso&base=[BASE] | http://adresse.com/?page=...&sso&id=[BASE] | http://adresse.com/?page=...&sso=[BASE]
 * URL profonde multi-structures spéciale : http://adresse.com/?page=...&sso&uai=[UAI]   | http://adresse.com/?page=...&sso=[UAI]
 */

// ////////////////////////////////////////////////////////////////////////////////////////////////////
// En cas de multi-structures, il faut savoir dans quelle base récupérer les informations.
// Un UAI ou un id de base doit être transmis, même s’il est toléré de le retrouver dans un cookie.
// ////////////////////////////////////////////////////////////////////////////////////////////////////

$BASE = 0;
$UAI = '';

if(HEBERGEUR_INSTALLATION=='multi-structures')
{
  // Lecture d’un cookie sur le poste client servant à retenir le dernier établissement sélectionné si identification avec succès
  $BASE = isset($_COOKIE[COOKIE_STRUCTURE]) ? Clean::entier($_COOKIE[COOKIE_STRUCTURE]) : 0 ;
  // Test si id d’établissement transmis dans l’URL
  // Historiquement "id" si connexion normale et "base" si connexion SSO
  // Nouveauté 07/2014 : pouvoir passer l’info de l’établissement comme valeur du paramètre SSO
  $BASE = ( isset($_GET['sso']) && ctype_digit((string)$_GET['sso']) ) ? Clean::get('sso', 'entier') : $BASE ;
  $BASE = Clean::get('id'  , 'entier', $BASE);
  $BASE = Clean::get('base', 'entier', $BASE);
  // Test si UAI d’établissement transmis dans l’URL
  // Nouveauté 07/2014 : pouvoir passer l’UAI de l’établissement comme valeur du paramètre SSO
  $UAI = Clean::get('uai', 'uai', Clean::get('sso', 'uai') );
  $is_UAI = Outil::tester_UAI($UAI);
  $BASE = ($is_UAI) ? DB_WEBMESTRE_PUBLIC::DB_recuperer_structure_id_base_for_UAI($UAI) : $BASE ;
  if(!$BASE)
  {
    if($is_UAI)
    {
      exit_error( 'Paramètre incorrect' /*titre*/ , 'Le numéro UAI transmis '.$UAI.' n’est pas référencé sur cette installation de SACoche : vérifiez son exactitude et si cet établissement est bien inscrit sur ce serveur.' /*contenu*/ );
    }
    else
    {
      exit_error( 'Donnée manquante' /*titre*/ , 'Référence de base manquante (le paramètre "base" ou "id" ou "sso" n’a pas été transmis ou n’est pas un entier et n’a pas non plus été trouvé dans un Cookie).' /*contenu*/ );
    }
  }
  DBextra::charger_parametres_sql_supplementaires($BASE);
}

// ////////////////////////////////////////////////////////////////////////////////////////////////////
// Connexion à la base pour charger les paramètres du SSO demandé
// ////////////////////////////////////////////////////////////////////////////////////////////////////

// Mettre à jour la base si nécessaire
DBextra::maj_base_structure_si_besoin($BASE);

// Récupérer les infos utiles de l’établissement pour la connexion
$tab_parametres = array(
  '"connexion_departement"',
  '"connexion_mode"',
  '"connexion_nom"',
  '"cas_serveur_host"',
  '"cas_serveur_port"',
  '"cas_serveur_root"',
  '"cas_serveur_url_login"',
  '"cas_serveur_url_logout"',
  '"cas_serveur_url_validate"',
  '"cas_serveur_verif_certif_ssl"',
);
$DB_TAB = DB_STRUCTURE_PARAMETRE::DB_lister_parametres( implode(',',$tab_parametres) );
foreach($DB_TAB as $DB_ROW)
{
  ${$DB_ROW['parametre_nom']} = $DB_ROW['parametre_valeur'];
}
if($connexion_mode=='normal')
{
  exit_error( 'Configuration manquante' /*titre*/ , 'Établissement non paramétré par l’administrateur pour utiliser un service d’authentification externe.<br>Un administrateur doit renseigner cette configuration dans le menu [Paramétrages][Mode&nbsp;d’identification].' /*contenu*/ , 'contact' , $BASE );
}

// ////////////////////////////////////////////////////////////////////////////////////////////////////
// Identification avec le protocole CAS
// ////////////////////////////////////////////////////////////////////////////////////////////////////

if($connexion_mode=='cas')
{
  /**
   * Si la bufferisation est active et contient la sortie de phpCAS sur une CAS_Exception,
   * récupère le contenu et l’affiche dans notre template (sinon lance un exit sans rien faire).
   *
   * Une cause rencontrée (peut-être pas la seule)
   * est que le XML renvoyé par le serveur CAS est syntaxiquement invalide.
   * En général car il contient un caractère parmi & < >
   *
   * Quand c’est un &, avant l’erreur fatale on a un warning : DOMDocument::loadXML(): xmlParseEntityRef: no name in Entity...
   * Quand c’est un <, avant l’erreur fatale on a un warning : DOMDocument::loadXML(): StartTag: invalid element name...
   * Quand c’est un >, avant l’erreur fatale on a un warning : DOMDocument::loadXML(): Start tag expected, '<' not found in Entity...
   * L’ENT doit s’arranger pour envoyer un XML valide, donc :
   * - soit convertir ces caractères en entités HTML (&amp; &lt; &gt;)
   * - soit retirer ces caractères ou les remplacer par d’autres
   * - soit utiliser des sections CDATA : <![CDATA[some text & some more text]]>
   *
   * Par ailleurs, il est tout de même dommage que phpCas ne renvoie pas un message plus causant
   * (genre xml parse error, ou à défaut invalid Response).
   *
   * @author Daniel Caillibaud <daniel.caillibaud@sesamath.net>
   * @param string $contenu_erreur_phpcas   La page d’erreur toute moche renvoyée par phpCAS
   * @param string $msg_supplementaire      Du contenu supplémentaire ajouté juste avant le </body> (mettre les <p>)
   */
  function exit_CAS_Exception( $contenu_erreur_phpcas , $msg_supplementaire )
  {
    global $BASE;
    if ($contenu_erreur_phpcas)
    {
      // on ne veut pas afficher ça mais notre jolie page
      // cf CAS/Client.php:printHTMLHeader()
      $pattern = '/<html><head><title>([^<]*)<\/title><\/head><body><h1>[^<]*<\/h1>(.*)<\/body><\/html>/';
      $matches = array();
      preg_match($pattern, $contenu_erreur_phpcas, $matches);
      if (!empty($matches[1]))
      {
        exit_error( $matches[1] /*titre*/ , $matches[2].$msg_supplementaire /*contenu*/ , 'contact' , $BASE );
      }
    }
    // peut-on vraiment passer par là ?
    else
    {
      exit_error( 'Problème d’authentification CAS' /*titre*/ , $msg_supplementaire /*contenu*/ , 'contact' , $BASE );
    }
    exit();
  }
  /**
   * Renvoie les traces d’une exception sous forme d’une chaîne
   *
   * @author Daniel Caillibaud <daniel.caillibaud@sesamath.net>
   * @param  Exception $e L’exception dont on veut les traces
   * @return string Les traces (liste à puces)
   * @throws Exception
   */
  function get_string_traces($e)
  {
    $tab_traces = $e->getTrace();
    $str_traces = '<ul>';
    $indice = 0;
    foreach ($tab_traces as $trace)
    {
      // init
      $str_traces .= '<li>'.$indice.' &rArr; ';
      // class
      if (isset($trace['class']))
      {
        $str_traces .= $trace['class'].' &rarr; ';
        unset($trace['class']);
      }
      // function
      if (isset($trace['function']))
      {
        // le nom de la fct concernée
        $str_traces .= $trace['function'];
        unset($trace['function']);
        // et ses arguments
        if (isset($trace['args']))
        {
          // faut ajouter les traces, mais $trace['args'] peut contenir des objets impossible à afficher
          // on pourrait récupérer la sortie du dump mais ça peut être gros, on affichera donc que
          // la classe des objets ou bien "array"
          $args_aff = array();
          foreach ($trace['args'] as $arg)
          {
            if (is_scalar($arg))
            {
              $args_aff[] = html(str_replace(CHEMIN_DOSSIER_SACOCHE,'',$arg));
            }
            elseif (is_array($arg))
            {
              $args_aff[] = '[array ' .count($arg) .' elts]';
            }
            elseif (is_object($arg))
            {
              $args_aff[] = 'obj ' .get_class($arg);
            }
            else
            {
              $args_aff[] = 'type ' .gettype($arg);
            }
          }
          // reste que des strings, on ajoute à la trace globale
          $str_traces .= '( ' .implode(' , ', $args_aff) .' )';
          unset($trace['args']);
        }
        else
        {
          // pas d’args, on ajoute les () pour mieux voir que c’est une fct
          $str_traces .= '()';
        }
      }
      // line
      if (isset($trace['line']))
      {
        $str_traces .= ' ligne '.$trace['line'];
        unset($trace['line']);
      }
      // file
      if (isset($trace['file']))
      {
        $str_traces .= ' dans '.str_replace(CHEMIN_DOSSIER_SACOCHE,'',$trace['file']);
        unset($trace['file']);
      }
      // type
      if (isset($trace['type']))
      {
        if ( ($trace['type']!='') && ($trace['type']!='->') && ($trace['type']!='::') )
        {
          $str_traces .= ' type : '.$trace['type'];
        }
        unset($trace['type']);
      }
      // si jamais il reste des trucs...
      if (count($trace))
      {
        $str_traces .= ' autres infos :';
        foreach ($trace as $key => $value)
        {
          $str_traces .= " ".$key.' : ';
          if (is_scalar($value))
          {
            $str_traces .= $value.' ; ';
          }
          else
          {
            $str_traces .= print_r($value, TRUE).' ; ';
          }
        }
      }
      $indice++;
      $str_traces .= '</li>'."\n";
    }
    return $str_traces;
  }
  $id_ENT = NULL;
  $phpCASattributes = NULL ;
  try
  {
    // Pour la méthode error() de phpCAS qui comporte un echo
    ob_start();
    // Appeler getVersion() est juste une ruse pour charger l’autoload de phpCAS avant l’appel client()
    $phpCAS_version = phpCAS::getVersion();
    // Maintenant que l’autoload est chargé on peut appeler cette méthode avant l’appel client()
    CAS_GracefullTerminationException::throwInsteadOfExiting();
    // Si besoin, cette méthode statique crée un fichier de log sur ce qui se passe avec CAS
    if(DEBUG_PHPCAS)
    {
      if( (HEBERGEUR_INSTALLATION=='mono-structure') || !PHPCAS_LOGS_ETABL_LISTING || (strpos(PHPCAS_LOGS_ETABL_LISTING,','.$BASE.',')!==FALSE) )
      {
        $fichier_nom_debut = 'debugcas_'.$BASE;
        $fichier_nom_fin   = FileSystem::generer_fin_nom_fichier__pseudo_alea($fichier_nom_debut);
        phpCAS::setDebug(PHPCAS_LOGS_CHEMIN.$fichier_nom_debut.'_'.$fichier_nom_fin.'.txt');
      }
    }
    // Initialiser la connexion avec CAS ; le premier argument est la version du protocole CAS ; le dernier argument indique qu’on utilise la session existante
    // Depuis la version 1.6.0 il faut indiquer en 5e paramètre l’URL de base du serveur appelant.
    if(version_compare($phpCAS_version,'1.6','<'))
    {
      phpCAS::client(CAS_VERSION_2_0, $cas_serveur_host, (int)$cas_serveur_port, $cas_serveur_root, FALSE);
    }
    else
    {
      phpCAS::client(CAS_VERSION_2_0, $cas_serveur_host, (int)$cas_serveur_port, $cas_serveur_root, URL_BASE, FALSE);
    }
    phpCAS::setLang(PHPCAS_LANG_FRENCH);
    // Surcharge éventuelle des URL
    if ($cas_serveur_url_login)    { phpCAS::setServerLoginURL($cas_serveur_url_login); }
    if ($cas_serveur_url_logout)   { phpCAS::setServerLogoutURL($cas_serveur_url_logout); }
    if ($cas_serveur_url_validate) { phpCAS::setServerServiceValidateURL($cas_serveur_url_validate); }
    // Suite à des attaques DDOS, Kosmos a décidé en avril 2015 de filtrer les requêtes en bloquant toutes celles sans User-Agent.
    // C’est idiot car cette valeur n’est pas fiable, n’importe qui peut présenter n’importe quel User-Agent !
    // En attendant qu’ils appliquent un remède plus intelligent, et au cas où un autre prestataire aurait la même mauvaise idée, on envoie un User-Agent bidon (défini dans le loader)...
    phpCAS::setExtraCurlOption(CURLOPT_USERAGENT , CURL_AGENT);
    // 10 secondes maximum pour éviter d’avoir des processus PHP en attente inutilement
    phpCAS::setExtraCurlOption(CURLOPT_CONNECTTIMEOUT , 10);
    phpCAS::setExtraCurlOption(CURLOPT_TIMEOUT , 10);
    // Appliquer un proxy si défini par le webmestre ; voir cURL::get_contents() pour les commentaires.
    if( defined('SERVEUR_PROXY_USED') && SERVEUR_PROXY_USED )
    {
      phpCAS::setExtraCurlOption(CURLOPT_PROXY     , SERVEUR_PROXY_NAME);
      phpCAS::setExtraCurlOption(CURLOPT_PROXYPORT , (int)SERVEUR_PROXY_PORT);
      phpCAS::setExtraCurlOption(CURLOPT_PROXYTYPE , constant(SERVEUR_PROXY_TYPE));
      if(SERVEUR_PROXY_AUTH_USED)
      {
        phpCAS::setExtraCurlOption(CURLOPT_PROXYAUTH    , constant(SERVEUR_PROXY_AUTH_METHOD));
        phpCAS::setExtraCurlOption(CURLOPT_PROXYUSERPWD , SERVEUR_PROXY_AUTH_USER.':'.SERVEUR_PROXY_AUTH_PASS);
      }
    }
    // On indique qu’il faut vérifier la validité du certificat SSL, sauf exception paramétrée, mais alors dans ce cas ça ne sert à rien d’utiliser une connexion sécurisée.
    if($cas_serveur_verif_certif_ssl)
    {
      phpCAS::setCasServerCACert(CHEMIN_FICHIER_CA_CERTS_FILE);
    }
    else
    {
      phpCAS::setNoCasServerValidation();
    }
    // Gestion du single sign-out
    phpCAS::handleLogoutRequests(FALSE);
    // Demander à CAS d’aller interroger le serveur
    // Cette méthode permet de forcer CAS à demander au client de s’authentifier s’il ne trouve aucun client d’authentifié.
    // (redirige vers le serveur d’authentification si aucun utilisateur authentifié n’a été trouvé par le client CAS)
    phpCAS::forceAuthentication();
    // A partir de là, l’utilisateur est forcément authentifié sur son CAS.
    // Récupérer l’identifiant (login ou numéro interne...) de l’utilisateur authentifié pour le traiter dans l’application
    // Transmis via la balise <cas:user></cas:user>
    $id_ENT = phpCAS::getUser();
    // Exception pour un ENT, il faut aller chercher dans les attributs...
    if( ($connexion_nom=='docaposte_oze_enc92') && phpCAS::hasAttribute('user') )
    {
      $id_ENT = phpCAS::getAttribute('user');
    }
    // Pour plus tard en cas d’utilisateur non reconnu
    $phpCASattributes = phpCAS::hasAttributes() ? phpCAS::getAttributes() : NULL ;
    // Pour mettre fin au ob_start() ; cas 1/2 où il n’y a pas eu d’erreur.
    ob_end_clean();
  }
  catch(CAS_Exception $e)
  {
    // Pour mettre fin au ob_start() ; cas 2/2 où il y a eu une erreur.
    $contenu_erreur_phpcas = ob_get_clean();
    // @author Daniel Caillibaud <daniel.caillibaud@sesamath.net>
    // on ajoute les traces
    $msg_supplementaire = '<p>Cette erreur peut être due à un certificat expiré, à un ticket obsolète, à un blocage des communications sortantes, ou à des données invalides renvoyées par le serveur CAS.</p>'
                        . '<p>Vous pouvez <a href="https://www.sslshopper.com/ssl-checker.html#hostname='.html($cas_serveur_host).'" target="_blank" rel="noopener noreferrer">vérifier la chaîne de certificats SSL</a> du domaine '.html($cas_serveur_host).'.</p>'
                        . get_string_traces($e);
    if (is_a($e, 'CAS_AuthenticationException'))
    {
      // $e->getMessage() ne contient rien...
      /*
       * error_log() retiré car il on récupère en nombre des choses genre :
       * <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationFailure code="INVALID_SERVICE">Service https://sacoche.sesamath.net/sacoche/?sso=6317&cookie invalid for the ticket found</cas:authenticationFailure></cas:serviceResponse>
       * qui génèrent aussi "PHP Warning:  DOMDocument::loadXML(): EntityRef: expecting ';' in Entity" à cause de l’éperluette dans l’adresse.
       */
      // error_log('SACoche - Erreur phpCAS sur l’ENT "'.$connexion_nom.'" (serveur '.$cas_serveur_host.') pour l’établissement n°'.$BASE.'.');
      exit_CAS_Exception( $contenu_erreur_phpcas , $msg_supplementaire );
    }
    else
    {
      // On passe ici visiblement en cas de simple redirection si l’utilisateur n’est pas déjà connecté ; dans ce cas on a :
      // $e->getMessage() = "Terminate Gracefully"
      // get_parent_class($e) = "RuntimeException"
    }
  }
  // Forcer à réinterroger le serveur CAS en cas de nouvel appel à cette page pour être certain que c’est toujours le même utilisateur qui est connecté au CAS.
  Session::_unset('phpCAS');
  // Comparer avec les données de la base
  list( $auth_SUCCESS , $auth_DATA ) = SessionUser::tester_authentification_utilisateur( $BASE , $id_ENT /*login*/ , FALSE /*password*/ , 'cas:user' /*mode_connection*/ );
  if($auth_SUCCESS!==TRUE)
  {
    // On essaye à partir des attributs s'ils sont présents
    if($phpCASattributes)
    {
      // id SIECLE (élèves uniquement)
      if(isset($phpCASattributes['ENTEleveStructRattachId']))
      {
        $cas_sconet_id = Clean::entier($phpCASattributes['ENTEleveStructRattachId']);
      }
      // nom
      if(isset($phpCASattributes['sn']))
      {
        $cas_nom = Clean::nom($phpCASattributes['sn']);
      }
      else if(isset($phpCASattributes['lastName']))
      {
        $cas_nom = Clean::nom($phpCASattributes['lastName']);
      }
      // prénom
      if(isset($phpCASattributes['givenName']))
      {
        $cas_prenom = Clean::prenom($phpCASattributes['givenName']);
      }
      else if(isset($phpCASattributes['firstName']))
      {
        $cas_prenom = Clean::prenom($phpCASattributes['firstName']);
      }
      // date de naissance (au format JJ/MM/AAAA)
      if(isset($phpCASattributes['ENTPersonDateNaissance']))
      {
        $cas_birth_date = Clean::jour($phpCASattributes['ENTPersonDateNaissance']);
      }
      // profil
      if(isset($phpCASattributes['ENTPersonNationalProfil']))
      {
        $cas_profil = Clean::ent_profil($phpCASattributes['ENTPersonNationalProfil']);
      }
      else if(isset($phpCASattributes['ENTPersonProfils']))
      {
        $cas_profil = Clean::ent_profil($phpCASattributes['ENTPersonProfils']);
      }
      else if(isset($phpCASattributes['PROFIL_NATIONAL']))
      {
        $cas_profil = Clean::ent_profil($phpCASattributes['PROFIL_NATIONAL']);
      }
      else if(isset($phpCASattributes['categories']))
      {
        $cas_profil = Clean::ent_profil($phpCASattributes['categories']);
      }
      if(!empty($cas_sconet_id))
      {
        list( $auth_SUCCESS , $auth_DATA ) = SessionUser::tester_authentification_utilisateur( $BASE , $cas_sconet_id /*login*/ , FALSE /*password*/ , 'cas:sconet' /*mode_connection*/ );
      }
      if( ($auth_SUCCESS!==TRUE) && !empty($cas_nom) && !empty($cas_prenom) )
      {
        $cas_profil     = !empty($cas_profil)     ? $cas_profil     : NULL ;
        $cas_birth_date = !empty($cas_birth_date) ? $cas_birth_date : NULL ;
        list( $auth_SUCCESS , $auth_DATA ) = SessionUser::tester_authentification_utilisateur( $BASE , $id_ENT /*login*/ , FALSE /*password*/ , 'cas:attributes' /*mode_connection*/ , $cas_nom , $cas_prenom , $cas_profil , $cas_birth_date );
      }
      // Mémoriser le <cas:user>
      if($auth_SUCCESS===TRUE)
      {
        DB_STRUCTURE_ADMINISTRATEUR::DB_modifier_user( $auth_DATA['user_id'] , array(':id_ent'=>$id_ENT) );
      }
    }
    if($auth_SUCCESS!==TRUE)
    {
      if($phpCASattributes)
      {
        // Ajout des attributs transmis aux message d’erreur
        $auth_DATA .= '<br>Attributs transmis pour information :<ul>';
        foreach ($phpCASattributes as $attribute_name => $attribute_value)
        {
          $auth_DATA .= '<li>'.html($attribute_name).' &rarr; '.html( is_array($attribute_value) ? implode(' / ',$attribute_value) : $attribute_value ).'</li>';
        }
        $auth_DATA .= '</ul>';
      }
      exit_error( 'Problème d’authentification CAS' /*titre*/ , $auth_DATA /*contenu*/ , 'contact' , $BASE );
    }
  }
  // Vérifier la présence d’une convention valide si besoin,
  // sauf pour les administrateurs qui doivent pouvoir accéder à leur espace pour régulariser la situation (même s’il leur est toujours possible d’utiliser une authentification locale),
  // et sauf pour les établissements destinés à tester les connecteurs ENT en PROD
  if( IS_HEBERGEMENT_SESAMATH && (SERVEUR_TYPE=='PROD') && CONVENTION_ENT_REQUISE && (CONVENTION_ENT_START_DATE_SQL<=TODAY_SQL) && ($auth_DATA['user_profil_type']!='administrateur') && ($BASE<CONVENTION_ENT_ID_ETABL_MAXI) )
  {
    // Vérifier que les paramètres de la base n’ont pas été trafiqués (via une sauvegarde / restauration de la base avec modification intermédiaire) pour passer outre : nom de connexion mis à perso ou modifié etc.
    $connexion_ref = $connexion_departement.'|'.$connexion_nom;
    require(CHEMIN_DOSSIER_INCLUDE.'tableau_sso.php');
    if(!isset($tab_connexion_info[$connexion_mode][$connexion_ref]))
    {
      exit_error( 'Paramètres CAS anormaux' /*titre*/ , 'Les paramètres CAS sont anormaux (connexion_mode vaut "'.$connexion_mode.'" ; connexion_departement vaut "'.$connexion_departement.'" ; connexion_nom vaut "'.$connexion_nom.'") !<br>Un administrateur doit sélectionner l’ENT concerné depuis son menu [Paramétrage&nbsp;établissement] [Mode&nbsp;d’identification].' /*contenu*/ , 'contact' , $BASE );
    }
    $tab_info = $tab_connexion_info[$connexion_mode][$connexion_ref];
    if($connexion_nom!='perso')
    {
      if(  (strpos($cas_serveur_host,$tab_info['serveur_host_domain'])===FALSE)
        || ( ($tab_info['serveur_port']!=$cas_serveur_port) && ($tab_info['serveur_port']!='*') )
        || ($tab_info['serveur_root']!=$cas_serveur_root)
        || ($tab_info['serveur_url_login']!=$cas_serveur_url_login)
        || ($tab_info['serveur_url_logout']!=$cas_serveur_url_logout)
        || ($tab_info['serveur_url_validate']!=$cas_serveur_url_validate)
      )
      {
        exit_error( 'Paramètres CAS anormaux' /*titre*/ , 'Les paramètres CAS enregistrés ne correspondent pas à ceux attendus pour la référence "'.$connexion_ref.'" !<br>Un administrateur doit revalider la sélection depuis son menu [Paramétrage&nbsp;établissement] [Mode&nbsp;d’identification].' /*contenu*/ , 'contact' , $BASE );
      }
    }
    // Normalement les hébergements académiques ne sont pas concernés
    activeSesamathLoader(false);
    if( isset(EntConventions::$tab_connecteurs_hebergement[$connexion_ref]) )
    {
      exit_error( 'Mode d’authentification anormal' /*titre*/ , 'Le mode d’authentification sélectionné ('.$connexion_nom.') doit être utilisé sur l’hébergement académique dédié (département '.$connexion_departement.') !' /*contenu*/ , 'contact' , $BASE );
    }
    // Pas besoin de vérification si convention signée à un plus haut niveau
    if (EntConventions::has_ent_convention_active($connexion_ref))
    {
      // Cas d’une convention signée par un partenaire ENT => Mettre en session l’affichage de sa communication en page d’accueil.
      $partenaire_id = DB_WEBMESTRE_PUBLIC::DB_recuperer_id_partenaire_for_connecteur($connexion_ref);
      $fichier_chemin = 'info_'.$partenaire_id.'.php';
      if( $partenaire_id && is_file(CHEMIN_DOSSIER_PARTENARIAT.$fichier_chemin) )
      {
        require(CHEMIN_DOSSIER_PARTENARIAT.$fichier_chemin);
        $logo_url   = ($partenaire_logo_actuel_filename) ? URL_DIR_PARTENARIAT.$partenaire_logo_actuel_filename : URL_DIR_IMG.'auto.gif' ;
        $lien_open  = ($partenaire_adresse_web) ? '<a href="'.html($partenaire_adresse_web).'" target="_blank" rel="noopener noreferrer">' : '' ;
        $lien_close = ($partenaire_adresse_web) ? '</a>' : '' ;
        $html = $lien_open.'<span id="partenaire_logo"><img src="'.html($logo_url).'"></span><span id="partenaire_message">'.nl2br(html($partenaire_message)).'</span>'.$lien_close.'<hr id="partenaire_hr">';
        Session::_set('CONVENTION_PARTENAIRE_ENT_COMMUNICATION' , $html );
      }
    }
    else
    {
      // pas de convention de collectivité (ou inactive)
      if(!DB_WEBMESTRE_PUBLIC::DB_tester_convention_active( $BASE , $connexion_nom ))
      {
        // pas de convention ent ni de convention étab
        $texte = EntConventions::get_ent_convention_texte($connexion_ref); // chaîne vide si y'a pas de convention ent
        $message_introduction = ($texte)
            ? $texte.'<br>L’usage de ce service sur ce serveur est donc désormais soumis à la signature et au règlement d’une convention avec l’établissement.'
            : 'L’usage de ce service sur ce serveur est soumis à la signature et au règlement d’une convention.' ;
        $message_explication  = '<br>Un administrateur doit effectuer les démarches depuis son menu [Paramétrage&nbsp;établissement] [Mode&nbsp;d’identification].<br>Veuillez consulter <a href="'.SERVEUR_GUIDE_ENT.'#toggle_partenariats" target="_blank" rel="noopener noreferrer">cette documentation pour davantage d’explications</a> et <a href="'.SERVEUR_GUIDE_ENT.'#toggle_gestion_convention" target="_blank" rel="noopener noreferrer">cette documentation pour la marche à suivre</a>.' ;
        exit_error( 'Absence de convention valide' /*titre*/ , $message_introduction.$message_explication /*contenu*/ , 'contact' , $BASE );
      }
    }
  }
  // Connecter l’utilisateur
  SessionUser::initialiser_utilisateur( $BASE , $auth_DATA );
  // Pas de redirection (passage possible d’infos en POST à conserver), on peut laisser le code se poursuivre.
  return; // Ne pas exécuter la suite de ce fichier inclus.
}

// ////////////////////////////////////////////////////////////////////////////////////////////////////
// Authentification assurée par Shibboleth
// La redirection est effectuée en amont (configuration du serveur web qui est "shibbolisé"), l’utilisateur doit donc être authentifié à ce stade.
// Attention : SACoche comportant une partie publique ne requérant pas d’authentification, et un accès possible avec une authentification locale, toute l’application n’est pas à shibboliser.
// @see https://services.renater.fr/federation/docs/fiches/shibbolisation
// ////////////////////////////////////////////////////////////////////////////////////////////////////

/*

>>> extrait conf shibboleth2.xml

<RequestMapper type="Native">
 <RequestMap applicationId="default">
  <Host name="vm-iozone3.in.ac-bordeaux.fr">
   <Path name="sacoche">
    <Query name="sso" authType="shibboleth" requireSession="true">
   </Path>
  </Host>
 </RequestMap>
</RequestMapper>

>>> extrait httpd.conf

Redirect permanent /sacoche /sacoche/
ProxyPass /sacoche/ https://ent2d.ac-bordeaux.fr/sacoche/
ProxyPassReverse /sacoche/ https://ent2d.ac-bordeaux.fr/sacoche/

*/

if($connexion_mode=='shibboleth')
{
  // Récupération dans les variables serveur de l’identifiant de l’utilisateur authentifié.
  if( empty($_SERVER['HTTP_UID']) && empty($_SERVER['HTTP_FREDUCTREFID']) && empty($_SERVER['HTTP_FREDUVECTEUR']) )
  {
    $http_uid          = isset($_SERVER['HTTP_UID'])          ? 'vaut "'.html($_SERVER['HTTP_UID']).'"'          : 'n’est pas définie' ;
    $http_freductrefid = isset($_SERVER['HTTP_FREDUCTREFID']) ? 'vaut "'.html($_SERVER['HTTP_FREDUCTREFID']).'"' : 'n’est pas définie' ;
    $http_freduvecteur = isset($_SERVER['HTTP_FREDUVECTEUR']) ? 'vaut "'.html($_SERVER['HTTP_FREDUVECTEUR']).'"' : 'n’est pas définie' ;
    $infos = '- la variable $_SERVER["HTTP_UID"] '.$http_uid.'<br>'
           . '- la variable $_SERVER["HTTP_FREDUCTREFID"] '.$http_freductrefid.'<br>'
           . '- la variable $_SERVER["HTTP_FREDUVECTEUR"] '.$http_freduvecteur;
    if( !isset($_SERVER['HTTP_UID']) && !isset($_SERVER['HTTP_FREDUCTREFID']) && !isset($_SERVER['HTTP_FREDUVECTEUR']) )
    {
      $contenu = 'Ce serveur ne semble pas disposer d’une authentification Shibboleth, ou bien celle ci n’a pas été mise en &oelig;uvre :<br>'.$infos;
    }
    else
    {
      $contenu = 'Les données d’authentification Shibboleth n’ont pas été trouvées dans les variables du serveur :<br>'
               . $infos.'<br>'
               . 'Cela vient très probablement d’une session expirée (page laissée ouverte trop longtemps).<br>'
               . 'Dans ce cas il vous suffit de repasser par l’ENT pour vous reconnecter à SACoche.';
    }
    exit_error( 'Problème d’authentification Shibboleth' /*titre*/ , $contenu , 'contact' , $BASE );
  }
  // Comparer avec les données de la base.
  $auth_SUCCESS = FALSE;
  $auth_DATA = 'Données serveurs accessibles insuffisantes pour authentifier un utilisateur !';
  $auth_info = '';
  //
  // [1/4] On commence par regarder HTTP_FREDUCTREFID, disponible pour les ÉLÈVES et les PARENTS utilisant ÉDUCONNECT.
  //
  if( ($auth_SUCCESS===FALSE) && !empty($_SERVER['HTTP_FREDUCTREFID']) )
  {
    if( is_file(CHEMIN_FICHIER_WS_BORDEAUX) )
    {
      require(CHEMIN_FICHIER_WS_BORDEAUX); // Charge EntBordeaux::GetUidFromEduVecteur()
      // Appelle le serveur LDAP et retourne un tableau [ ['nom'][i] , ['prenom'][i] , ['id_ent'][i] ]
      $tab_users_ENT = EntBordeaux::GetUidFromEduVecteur($_SERVER['HTTP_FREDUCTREFID']);
      // Pour les parents comme pour les élèves le vecteur d’identité peut être multivalué.
      if(!empty($tab_users_ENT['id_ent']))
      {
        foreach($tab_users_ENT['id_ent'] as $key => $id_ENT)
        {
          list( $auth_SUCCESS , $auth_DATA ) = SessionUser::tester_authentification_utilisateur( $BASE , $id_ENT /*login*/ , FALSE /*password*/ , 'shibboleth' /*mode_connection*/ );
          if($auth_SUCCESS!==FALSE)
          {
            // Normalement il ne devrait pas exister de compte multivalué dans un même établissement.
            // Si jamais c’était le cas, c’est une erreur de l’établissement dans SIECLE.
            // Du coup, même en cas de succès, on teste les autres comptes afin de pouvoir recevoir une alerte à ce sujet.
            $key_max = count($tab_users_ENT['id_ent']) - 1;
            if( $key < $key_max )
            {
              $key_min = $key + 1;
              for( $key = $key_min ; $key <= $key_max ; $key++ )
              {
                $id_ENT = $tab_users_ENT['id_ent'][$key];
                list( $other_SUCCESS , $other_DATA ) = SessionUser::tester_authentification_utilisateur( $BASE , $id_ENT /*login*/ , FALSE /*password*/ , 'shibboleth' /*mode_connection*/ );
                if($other_SUCCESS!==FALSE)
                {
                  list( $auth_SUCCESS , $auth_DATA ) = array( FALSE , 'Identification réussie mais votre compte est en doublon dans l’établissement.' );
                  $auth_info .= '<br>Veuillez contacter son secrétariat afin de demander la fusion de vos comptes dans SIECLE.'
                              . '<br>Cette modification prendra effet le lendemain dans l’annuaire fédérateur (pour votre connexion à l’ENT).'
                              . '<br>Enfin, signalez qu’un administrateur SACoche devra répercuter la modification en y ré-important les comptes responsables.';
                }
              }
            }
            break;
          }
          else
          {
            $auth_info .= '<br>Donnée associée récupérée : '.$id_ENT.' '.$tab_users_ENT['nom'][$key].' '.$tab_users_ENT['prenom'][$key].'.';
          }
        }
      }
      else
      {
        $auth_info .= '<br>Aucune donnée trouvée pour le FrEduCtRefId '.$_SERVER['HTTP_FREDUCTREFID'].'.';
      }
    }
  }
  //
  // [2/4] On regarde ensuite HTTP_UID, disponible en particulier pour les enseignants et personnels.
  //
  if( ($auth_SUCCESS===FALSE) && !empty($_SERVER['HTTP_UID']) )
  {
    // A cause du chainage réalisé depuis Shibboleth entre différents IDP pour compléter les attributs exportés, l’UID peut arriver en double séparé par un « ; ».
    $tab_http_uid = explode( ';' , $_SERVER['HTTP_UID'] );
    $id_ENT = $tab_http_uid[0];
    if($id_ENT)
    {
      list( $auth_SUCCESS , $auth_DATA ) = SessionUser::tester_authentification_utilisateur( $BASE , $id_ENT /*login*/ , FALSE /*password*/ , 'shibboleth' /*mode_connection*/ );
    }
  }
  //
  // [3/4] On peut regarder HTTP_TSSCONETID ou HTTP_ENTELEVESTRUCTRATTACHID, disponible pour les élèves utilisant ATEN (TÉLÉSERVICES).
  // TODO : à supprimer, normalement obsolète à compter de septembre 2021
  //
  if( ($auth_SUCCESS===FALSE) && ( !empty($_SERVER['HTTP_TSSCONETID']) || !empty($_SERVER['HTTP_ENTELEVESTRUCTRATTACHID']) ) )
  {
    $eleve_sconet_id = (!empty($_SERVER['HTTP_TSSCONETID'])) ? (int)$_SERVER['HTTP_TSSCONETID'] : ( (!empty($_SERVER['HTTP_ENTELEVESTRUCTRATTACHID'])) ? (int)$_SERVER['HTTP_ENTELEVESTRUCTRATTACHID'] : 0 ) ;
    if($eleve_sconet_id)
    {
      list( $auth_SUCCESS , $auth_DATA ) = SessionUser::tester_authentification_utilisateur( $BASE , $eleve_sconet_id /*login*/ , FALSE /*password*/ , 'siecle' /*mode_connection*/ );
    }
  }
  //
  // [4/4] On peut enfin regarder HTTP_FREDUVECTEUR, disponible pour les élèves et les parents utilisant ATEN (TÉLÉSERVICES).
  // TODO : à supprimer, normalement obsolète à compter de septembre 2021
  //
  if( ($auth_SUCCESS===FALSE) && !empty($_SERVER['HTTP_FREDUVECTEUR']) )
  {
    // Pour les parents, il peut être multivalué, les différentes valeurs étant alors séparées par un « ; » ; on ne peut pas se contenter de tester la 1ère valeur au cas où il y a d’autres enfants dans d’autres établissement...
    $fr_edu_vecteur = (!empty($_SERVER['HTTP_FREDUVECTEUR'])) ? $_SERVER['HTTP_FREDUVECTEUR'] : '' ;
    $tab_vecteur = explode( ';' , $fr_edu_vecteur );
    foreach($tab_vecteur as $fr_edu_vecteur)
    {
      list( $vecteur_profil , $vecteur_nom , $vecteur_prenom , $vecteur_eleve_id , $vecteur_uai ) = explode('|',$fr_edu_vecteur ) + array_fill(0,5,NULL) ; // Evite des NOTICE en initialisant les valeurs manquantes
      if( in_array($vecteur_profil,array(3,4)) && ($vecteur_eleve_id) && ($vecteur_eleve_id!=$eleve_sconet_id) ) // cas d’un élève
      {
        list( $auth_SUCCESS , $auth_DATA ) = SessionUser::tester_authentification_utilisateur( $BASE , $vecteur_eleve_id /*login*/ , FALSE /*password*/ , 'siecle' /*mode_connection*/ );
      }
      elseif( in_array($vecteur_profil,array(1,2)) && ($vecteur_eleve_id) ) // cas d’un parent
      {
        if( $vecteur_nom && $vecteur_prenom )
        {
          list( $auth_SUCCESS , $auth_DATA ) = SessionUser::tester_authentification_utilisateur( $BASE , $vecteur_eleve_id /*login*/ , FALSE /*password*/ , 'vecteur_parent' /*mode_connection*/ , $vecteur_nom , $vecteur_prenom );
        }
        else
        {
          list( $auth_SUCCESS , $auth_DATA ) = array( FALSE , 'Identification réussie mais vecteur d’identité parent "' .$fr_edu_vecteur.'" incomplet, ce qui empêche de rechercher le compte SACoche correspondant.' );
        }
        if($auth_SUCCESS!==FALSE)
        {
          // Pour un parent, si ok pour l’enfant considéré, on s’arrête, sinon on teste avec l’enfant suivant
          break;
        }
        else
        {
          $auth_info .= '<br>FrEduVecteur récupéré : "'.$fr_edu_vecteur.'".';
        }
      }
    }
  }
  // Tous les modes de recherches ont désormais été passés en revue.
  if($auth_SUCCESS===FALSE)
  {
    exit_error( 'Problème d’authentification Shibboleth' /*titre*/ , $auth_DATA.$auth_info , 'contact' , $BASE );
  }
  // Connecter l’utilisateur
  SessionUser::initialiser_utilisateur( $BASE , $auth_DATA );
  // Pas de redirection (passage possible d’infos en POST à conserver), on peut laisser le code se poursuivre.
  return; // Ne pas exécuter la suite de ce fichier inclus.
}

?>
