<?php

/**
 * Authentication backend using Shibboleth
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Dominique Launay <dominique.launay AT cru.fr>
 * @author     Olivier Salaun <olivier.salaun AT cru.fr>
 * @author     Yoann Lecuyer <yoann.lecuyer AT cru.fr> 
 * Version: 1.2
 * last modified: 2009-11-27
 *
 * Work based on :
 *    ssl authentication backend:
 *       @author     Dominique Launay <dominique.launay AT cru.fr>
 *    Sympa Soap server authentication backend:
 *       @author     David Pepin<sympa-authors AT cru.fr>
 *
 * The nusoap library is required
 *
 **/


define('DOKU_AUTH', dirname(__FILE__));
require_once(DOKU_AUTH.'/basic.class.php');
define('AUTH_USERFILE',DOKU_CONF.'users.auth.php');

class auth_shibboleth extends auth_basic {
  var $users = null;
  var $sympaDefaultGroup = null;
  var $sympaSoapService = null;
  var $soap_client = null;
  var $log_file = null;

  /**
   *
   * Constructor
   *
   * @author  Yoann Lecuyer <yoann.lecuyer AT cru.fr>
   *
   **/
  // This function is run for every web access to the wiki
  // Most of the Shibboleth authentication code is run here because we never run the do_login action 
  // With Shibboleth, dokuwiki never collects user password
  function auth_shibboleth() {
    global $conf;
    //msg('auth_shibboleth');
    $this->cando['external'] = true;
    //$this->cando['getGroups']    = true;


    if (method_exists($this, 'auth_basic')){
      parent::auth_basic();
    }
    // check if the server configuration has correctly been done
    if (!isset($conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']) || empty($conf['plugin']['shibbolethauth']['shibbolethEmailAttribute'])) {
      $this->success = false;
      return;
    }
    if (isset($conf['plugin']['shibbolethauth']['useSympa']) && $conf['plugin']['shibbolethauth']['useSympa'] == 1 && empty($conf['plugin']['shibbolethauth']['sympaSoapService'])) {
      $this->success = false;
      return;
    }
    if(!isset($conf['plugin']['shibbolethauth']['shibbolethLoginURL']) || empty($conf['plugin']['shibbolethauth']['shibbolethLoginURL'])) {
      $this->success = false;
      return;  
    }
    if(!isset($conf['plugin']['shibbolethauth']['shibbolethLogoutURL']) || empty($conf['plugin']['shibbolethauth']['shibbolethLogoutURL'])) {
      $this->success = false;
      return;  
    }

    if (isset($conf['defaultgroup'])) {
      $this->sympaDefaultGroup = $conf['defaultgroup'];
    }
    
    
    $this->success = true;
    return;
  }

  /**
   * Do all authentication [ OPTIONAL ]
   *
   * Set $this->cando['external'] = true when implemented
   *
   * If this function is implemented it will be used to
   * authenticate a user - all other DokuWiki internals
   * will not be used for authenticating, thus
   * implementing the functions below becomes optional.
   *
   * The function can be used to authenticate against third
   * party cookies or Apache auth mechanisms and replaces
   * the auth_login() function
   *
   * The function will be called with or without a set
   * username. If the Username is given it was called
   * from the login form and the given credentials might
   * need to be checked. If no username was given it
   * the function needs to check if the user is logged in
   * by other means (cookie, environment).
   *
   * The function needs to set some globals needed by
   * DokuWiki like auth_login() does.
   *
   * @see auth_login()
   * @author  Andreas Gohr <andi@splitbrain.org>
   * @author  Olivier Salaün <olivier.salaun AT cru.fr>
   * @author  Dominique Launay <dominique.launay AT cru.fr>
   *
   * @param   string  $user    Username
   * @param   string  $pass    Cleartext Password
   * @param   bool    $sticky  Cookie should not expire
   * @return  bool             true on successful auth
   */
  function trustExternal($user,$pass,$sticky=false){
    //msg('trust_external');
    // some example:
    
    global $USERINFO;
    global $conf;
    $sticky ? $sticky = true : $sticky = false; //sanity check
    // do the checking here
    //see if we got cookie
    if(isset($_SESSION['DW_SHIB'])){
      // case logout
      if($_REQUEST['do'] == 'logout'){
	unset($_SESSION['DW_SHIB']);
	unset($_SESSION[DOKU_COOKIE]['auth']['user']);
	unset($_SESSION[DOKU_COOKIE]['auth']['pass']);
	unset($_SESSION[DOKU_COOKIE]['auth']['info']);
	error_log('shibbolethauth : authenticated user redirected for logout to '.$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']);
	header("Location: ".$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']."?return=".urlencode($_SERVER['HTTP_REFERER']));
	exit;
      }else{
	// fill vars with cookie vars
	$_SESSION[DOKU_COOKIE]['auth']['user'] = $user = $_SESSION['DW_SHIB']['user'];
	$_SESSION[DOKU_COOKIE]['auth']['pass'] = $pass = $_SESSION['DW_SHIB']['pass'];
       	$_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO = $_SESSION['DW_SHIB']['info'];
	$_SERVER['REMOTE_USER'] =  $_SESSION['DW_SHIB']['user'];
	return true;
      }
    }else{
      // save the Email done by a Shibboleth attribute if this attribute is set
      // we need to check if the identityProvider attribute is set, otherwise we can't distinguish 
      // normal unauthenticated access from authenticated access with a missing email address
      if(isset($_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]) && $_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]!=""){
	// if user wants to log out
	if($_REQUEST['do'] == 'logout'){
	  unset($_SESSION['DW_SHIB']);
	  error_log('shibbolethauth : authenticated user redirected for logout to '.$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']);
	  header("Location: ".$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']."?return=".urlencode($_SERVER['HTTP_REFERER']));
	  exit;
	}else{
	  //attributes set fill convenient variables
	  $USERINFO['name'] = $user = $_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']];
	  $USERINFO['mail'] = $mail = $_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']];
	  
	  
	  // load the groups of users contained on the file users.auth.php
	  if($this->users === null) $this->_loadUserData();
	  // groups the person belongs to
	  $grps = array();
	  // Test if $user is an email or not. If it is not, it is an user id
	  if(strpbrk($user, '@') != false){
	    // We are in a Shibboteth context, and we get the datas for the autorization
	    // Test if we use the autorisation with Sympa server
	    $sympaIsOK = true;
	    if(isset($conf['plugin']['shibbolethauth']['useSympa']) && $conf['plugin']['shibbolethauth']['useSympa'] == 1){
	      // Get the groups list on the Sympa server
	      //create soap client
	      try{
		$this->soap_client = new SoapClient($conf['plugin']['shibbolethauth']['sympaSoapService']);
	      }catch(SoapFault $fault){
		$sympaIsOK =  false;
		error_log("shibbolethauth : unable to establish a SOAP connection with ".$conf['plugin']['shibbolethauth']['sympaSoapService']);
		msg("No valid SOAP server",0);
	      }
	      // get the groups list
	      if($sympaIsOK){
		try{
		  $res = $this->soap_client->authenticateRemoteAppAndRun($conf['plugin']['shibbolethauth']['sympaApplicationId'],$conf['plugin']['shibbolethauth']['sympaApplicationPwd'], 'USER_EMAIL='.$user,'complexWhich');      
		}catch(SoapFault $fault){
		  $sympaIsOK =  false;
		  msg("dokuwiki groups only",0);
		  error_log("shibbolethauth : unable to authenticate on ".$conf['plugin']['shibbolethauth']['sympaSoapService']);
		}
		if($sympaIsOK){
		  if (isset($res) && gettype($res) == 'array') {
		    foreach ($res as $list) {
		      if (empty($grps[0])) { 
			$grps[0] = $list->listAddress;
		      }else{
			array_unshift($grps,$list->listAddress);
		      }
		      if($list->isOwner == 'true'){
			$listrequest = preg_replace('/@/','-request@',$list->listAddress);
			array_unshift($grps,$listrequest);
		      }
		      if ($list->isEditor == 'true') {
			$listeditor = preg_replace('/@/','-editor@',$list->listAddress);
			array_unshift($grps,$listeditor);
		      }      
		    }
		  }
		}
	      }
	    }
	    // Collect the DokuWiki's data with the Sympa's data (if we are in Sympa context)
	    $users = $this->setUserGroups($mail,$grps);
	  }
	  // set the globals if authed
	  $USERINFO['grps'] = $users['grps'];
	  $_SERVER['REMOTE_USER'] = $user;
	  $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
	  $_SESSION[DOKU_COOKIE]['auth']['pass'] = $user;
	  $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
	  $_SESSION['DW_SHIB']['user']= $user;
	  $_SESSION['DW_SHIB']['pass']= $user;
	  $_SESSION['DW_SHIB']['info'] = $USERINFO;
	  error_log('shibbolethauth : authenticated user');
	  return true;
	}
	// if this attribute is not set, we log off the system
      }else{
	if($_REQUEST['do'] == 'login'){
	  error_log('shibbolethauth: redirect user for login to '.$conf['plugin']['shibbolethauth']['shibbolethLoginURL']);
	  header("Location: ".$conf['plugin']['shibbolethauth']['shibbolethLoginURL']."?target=".urlencode($_SERVER['HTTP_REFERER']));
	  exit;
	}else{
	  error_log('shibbolethauth: no email address to log');
	  unset($_SESSION[DOKU_COOKIE]['auth']['user']);
	  unset($_SESSION[DOKU_COOKIE]['auth']['pass']);
	  unset($_SESSION[DOKU_COOKIE]['auth']['info']);
	  return false;
	}
      }
    }
  }
  
  
  /**
   * Checks if the Email attibute of Shibboleth is correctly set
   *
   * @author  Yoann Lecuyer <yoann.lecuyer AT cru.fr>
   *
   **/

  function checkPass($user,$pass){
  }

  /**
   * Return user info
   *
   * Accessing to sympa is done through the nusoap library
   * for the autorization part
   *
   * Reading the user's group on DokuWiki's files
   * for the user manager part
   *
   * The returned field are :
   *
   * name string  full name of the user 
   * mail string  email addres of the user 
   * groups array   list of groups the user is in
   *
   * @author  Yoann Lecuyer <yoann.lecuyer AT cru.fr>
   *
   **/

  function getUserData( $user ) {
    $userData = false;
    $userData['name'] = $user;
    $userData['mail'] = $user;
    $userData['grps'] = $this->getUserGroups($user);
    return $userData;
  }

  /**
   * Return user groups
   *
   **/

  function getUserGroups($user) {
    global $conf;
    $mail=$user;
    // load the groups of users contained on the file users.auth.php
    if($this->users === null) $this->_loadUserData();
    // groups the person belongs to
    $grps = array();
    // Test if $user is an email or not. If it is not, it is an user id
    if(strpbrk($user, '@') != false){
      // We are in a Shibboteth context, and we get the datas for the autorization
      // Test if we use the autorisation with Sympa server
      $sympaIsOK = true;
      if(isset($conf['plugin']['shibbolethauth']['useSympa']) && $conf['plugin']['shibbolethauth']['useSympa'] == 1){
	// Get the groups list on the Sympa server
	//create soap client
	try{
	  $this->soap_client = new SoapClient($conf['plugin']['shibbolethauth']['sympaSoapService']);
	}catch(SoapFault $fault){
	  $sympaIsOK =  false;
	  error_log("shibbolethauth : unable to establish a SOAP connection with ".$conf['plugin']['shibbolethauth']['sympaSoapService']);
	}
	// get the groups list
	if($sympaIsOK){
	  try{
	    $res = $this->soap_client->authenticateRemoteAppAndRun($conf['plugin']['shibbolethauth']['sympaApplicationId'],$conf['plugin']['shibbolethauth']['sympaApplicationPwd'], 'USER_EMAIL='.$user,'complexWhich');      
	  }catch(SoapFault $fault){
	    $sympaIsOK =  false;
	    msg("dokuwiki groups only",0);
	    error_log("shibbolethauth : unable to authenticate on ".$conf['plugin']['shibbolethauth']['sympaSoapService']);
	  }
	  if($sympaIsOK){
	  if (isset($res) && gettype($res) == 'array') {
	      foreach ($res as $list) {
		if (empty($grps[0])) { 
		  $grps[0] = $list->listAddress;
		}else{
		  array_unshift($grps,$list->listAddress);
		}
		if($list->isOwner == 'true'){
		  $listrequest = preg_replace('/@/','-request@',$list->listAddress);
		  array_unshift($grps,$listrequest);
		}
		if ($list->isEditor == 'true') {
		  $listeditor = preg_replace('/@/','-editor@',$list->listAddress);
		  array_unshift($grps,$listeditor);
		}      
	      }
	    }
	  }
	}
      }
      
      // Collect the DokuWiki's data with the Sympa's data (if we are in Sympa context)
      $users = $this->setUserGroups($mail,$grps);
    }


    // set the globals if authed
    return $users['grps'];
  }


  /**
   * Set the DokuWiki's groups and the Sympa's groups together
   *
   * @author  Yoann Lecuyer <yoann.lecuyer AT cru.fr>
   *
   **/

  function setUserGroups($mail,$grps){
    foreach ($this->users as $user){
      if($user['mail'] == $mail){
        // Add or set the groups of the user
        if(!empty($grps[0])){
          foreach($grps as $group){
            array_unshift($user['grps'],$group);
          }
        }
        // Verify if the table contains the sympaDefaultGroup value
        $defaultgroupset = false;
        foreach ($user['grps'] as $group){
          if($group == $this->sympaDefaultGroup) $defaultgroupset = true;
        }
        // Put the sympaDefaultGroup value if it is not on the table
        if(!$defaultgroupset) array_unshift($user['grps'],$this->sympaDefaultGroup);
        return $user;
      }
    }
    // the mail is not on the table of DokuWiki's users, we create an other user

    // Verify if the table contains the sympaDefaultGroup value
    $defaultgrpset = false;
    foreach ($grps as $grp){
      if($grp == $this->sympaDefaultGroup) $defaultgrpset = true;
    }
    // Put the sympaDefaultGroup value if it is not on the table
    if(!$defaultgrpset) array_unshift($grps,$this->sympaDefaultGroup);

    $this->users[$mail]['name'] = $mail;
    $this->users[$mail]['mail'] = $mail;
    $this->users[$mail]['grps'] = $grps;
    return $this->users[$mail];
  }

  /**
   * Logoff user : 
   *
   * everything that was about the session is unset
   *
   * @author  Yoann Lecuyer <yoann.lecuyer AT cru.fr>
   *
   **/

  function logOff() {
    if(isset($_SESSION[DOKU_COOKIE]['auth']['info'])) {
      unset($_SESSION[DOKU_COOKIE]['auth']['info']);
    }
    unset($this->soap_client);
    unset($_REQUEST['u']);
    return;
  }

  function server() {
    return($this->sympaSoapService);
  }

  /**
   * Create a new User
   *
   * Returns false if the user already exists, null when an error
   * occured and true if everything went well.
   *
   * The new user will be added to the default group by this
   * function if grps are not specified (default behaviour).
   *
   * @author  Andreas Gohr <andi@splitbrain.org>
   * @author  Chris Smith <chris@jalakai.co.uk>
   */
  function createUser($user,$pwd,$name,$mail,$grps=null){
    global $conf;

    // user mustn't already exist
    if ($this->getUserData($user) !== false) return false;

    $pass = auth_cryptPassword($pwd);
    $name = $user;
    // set default group if no groups specified
    if (!is_array($grps)) $grps = array($conf['defaultgroup']);

    // prepare user line
    $groups = join(',',$grps);
    $userline = join(':',array($user,$pass,$name,$mail,$groups))."\n";

    if (io_saveFile(AUTH_USERFILE,$userline,true)) {
      $this->users[$user] = compact('pass','name','mail','grps');
      return $pass;
    }

    msg('The '.AUTH_USERFILE.' file is not writable. Please inform the Wiki-Admin',-1);
    return null;
  }

  /**
   * Modify user data
   *
   * @author  Chris Smith <chris@jalakai.co.uk>
   * @param   $user      nick of the user to be changed
   * @param   $changes   array of field/value pairs to be changed (password will be clear text)
   * @return  bool
   */

  function modifyUser($user, $changes) {
    global $conf;
    global $ACT;
    global $INFO;

    // sanity checks, user must already exist and there must be something to change
    if (($userinfo = $this->getUserData($user)) === false) return false;
    if (!is_array($changes) || !count($changes)) return true;

    // update userinfo with new data, remembering to encrypt any password
    $newuser = $user;
    foreach ($changes as $field => $value) {
      if ($field == 'user') {
        $newuser = $value;
        continue;
      }
      if ($field == 'pass') $value = auth_cryptPassword($value);
      $userinfo[$field] = $value;
    }

    $groups = join(',',$userinfo['grps']);
    $userline = join(':',array($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $groups))."\n";

    if (!$this->deleteUsers(array($user))) {
      msg('Unable to modify user data. Please inform the Wiki-Admin',-1);
      return false;
    }

    if (!io_saveFile(AUTH_USERFILE,$userline,true)) {
      msg('There was an error modifying your user data. You should register again.',-1);
      // FIXME, user has been deleted but not recreated, should force a logout and redirect to login page
      $ACT == 'register';
      return false;
    }

    $this->users[$newuser] = $userinfo;
    return true;
  }

  /**
   *  Remove one or more users from the list of registered users
   *
   *  @author  Christopher Smith <chris@jalakai.co.uk>
   *  @param   array  $users   array of users to be deleted
   *  @return  int             the number of users deleted
   */

  function deleteUsers($users) {

    if (!is_array($users) || empty($users)) return 0;

    if ($this->users === null) $this->_loadUserData();

    $deleted = array();
    foreach ($users as $user) {
      if (isset($this->users[$user])) $deleted[] = preg_quote($user,'/');
    }

    if (empty($deleted)) return 0;

    $pattern = '/^('.join('|',$deleted).'):/';

    if (io_deleteFromFile(AUTH_USERFILE,$pattern,true)) {
      foreach ($deleted as $user) unset($this->users[$user]);
      return count($deleted);
    }

    // problem deleting, reload the user list and count the difference
    $count = count($this->users());
    $this->_loadUserData();
    $count -= $count($this->users());
    return $count;
  }

  /**
   * Return a count of the number of user which meet $filter criteria
   *
   * @author  Chris Smith <chris@jalakai.co.uk>
   */

  function getUserCount($filter=array()) {

    if($this->users === null) $this->_loadUserData();

    if (!count($filter)) return count($this->users);

    $count = 0;
    $this->_constructPattern($filter);

    foreach ($this->users as $user => $info) {
      $count += $this->_filter($user, $info);
    }

    return $count;
  }

  /**
   * Bulk retrieval of user data
   *
   * @author  Chris Smith <chris@jalakai.co.uk>
   * @param   start     index of first user to be returned
   * @param   limit     max number of users to be returned
   * @param   filter    array of field/pattern pairs
   * @return  array of userinfo (refer getUserData for internal userinfo details)
   */
  function retrieveUsers($start=0,$limit=0,$filter=array()) {

    if ($this->users === null) $this->_loadUserData();

    ksort($this->users);

    $i = 0;
    $count = 0;
    $out = array();
    $this->_constructPattern($filter);

    foreach ($this->users as $user => $info) {
      if ($this->_filter($user, $info)) {
        if ($i >= $start) {
          $out[$user] = $info;
          $count++;
          if (($limit > 0) && ($count >= $limit)) break;
        }
        $i++;
      }
    }

    return $out;
  }

  /**
   * Load all user data
   *
   * loads the user file into a datastructure
   *
   * @author  Andreas Gohr <andi@splitbrain.org>
   */

  function _loadUserData(){
    $this->users = array();

    if(!@file_exists(AUTH_USERFILE)) return;

    $lines = file(AUTH_USERFILE);
    foreach($lines as $line){
      $line = preg_replace('/#.*$/','',$line); //ignore comments
      $line = trim($line);
      if(empty($line)) continue;

      $row    = split(":",$line,5);
      $groups = split(",",$row[4]);
      $this->users[$row[0]]['pass'] = $row[1];
      $this->users[$row[0]]['name'] = urldecode($row[2]);
      $this->users[$row[0]]['mail'] = $row[3];
      $this->users[$row[0]]['grps'] = $groups;
    }
  }

  /**
   * return 1 if $user + $info match $filter criteria, 0 otherwise
   *
   * @author   Chris Smith <chris@jalakai.co.uk>
   */

  function _filter($user, $info) {
    // FIXME
    foreach ($this->_pattern as $item => $pattern) {
      if ($item == 'user') {
        if (!preg_match($pattern, $user)) return 0;
      } else if ($item == 'grps') {
        if (!count(preg_grep($pattern, $info['grps']))) return 0;
      } else {
        if (!preg_match($pattern, $info[$item])) return 0;
      }
    }
    return 1;
  }

  function _constructPattern($filter) {
    $this->_pattern = array();
    foreach ($filter as $item => $pattern) {
//      $this->_pattern[$item] = '/'.preg_quote($pattern,"/").'/';          // don't allow regex characters
      $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/';    // allow regex characters
    }
  }
}
