<?php
/**
 * Define a Serveur active directory
 * @author Jean CARRUESCO
 * @category Plugin
 * @license MIT
 */
class AdServer extends Entity{

	public $id;
	public $ip; //IP (Texte)
	public $port = 389; //Port (Nombre Entier)
	public $sslPort = 636; //Port SSL (Nombre Entier)
	public $protocolVersion = 3; //Version de protocole ldap (Texte)
	public $domain; //Domaine (Texte)
	public $userRoot; //Racine des comptes utilisateurs (Texte Long)
	public $groupRoot; //Racine des groupes utilisateurs (Texte Long)
	public $readonlyLogin; //Identifiant du compte lecture seule (Texte)
	public $readonlyPassword; //Mot de passe du compte lecture seule (Mot de passe)
	public $adminLogin; //Identifiant compte administrateur (Texte)
	public $adminPassword; //Mot de passe compte administrateur (Mot de passe)
	public $mapping; //Plan de liaisons des champs (Texte Long)
	public $authenticationMode = 'samaccountname'; //Authentication mode (Texte)

	private $datasource; //Ressource AD

	protected $TABLE_NAME = 'activedirectory_ad_server';
	public $entityLabel = 'Serveur active directory';
	public $fields = array(
		'id' => array('type'=>'key', 'label' => 'Identifiant'),
		'ip' => array('type'=>'text','label' => 'IP'),
		'port' => array('type'=>'integer','label' => 'Port'),
		'sslPort' => array('type'=>'integer','label' => 'Port SSL'),
		'protocolVersion' => array('type'=>'text','label' => 'Version de protocole ldap'),
		'domain' => array('type'=>'text','label' => 'Domaine'),
		'userRoot' => array('type'=>'textarea','label' => 'Racine des comptes utilisateurs'),
		'groupRoot' => array('type'=>'textarea','label' => 'Racine des groupes utilisateurs'),
		'readonlyLogin' => array('type'=>'text','label' => 'Identifiant du compte lecture seule'),
		'readonlyPassword' => array('type'=>'password','label' => 'Mot de passe du compte lecture seule'),
		'adminLogin' => array('type'=>'text','label' => 'Identifiant compte administrateur'),
		'adminPassword' => array('type'=>'password','label' => 'Mot de passe compte administrateur'),
		'mapping' => array('type'=>'textarea','label' => 'Plan de liaisons des champs'),
		'authenticationMode' => array('type'=>'text','label' => 'Authentication mode'),

	);

	//retourne le mapping champ ad -> champ ustilisateur de base
	function default_mapping(){
		$mapping = array(
			'sn'=>array('label'=>'Nom','field'=>'name','source'=>'active_directory'),
			'givenname'=>array('label'=>'Prénom','field'=>'firstname','source'=>'active_directory'),
			'mail'=>array('label'=>'E-mail','field'=>'mail','source'=>'active_directory'),
			'telephonenumber'=>array('label'=>'Téléphone fixe','field'=>'phone','source'=>'active_directory'),
			'mobile'=>array('label'=>'Téléphone mobile','field'=>'mobile','source'=>'active_directory'),
			'title'=>array('label'=>'Fonction','field'=>'function','source'=>'active_directory'),
			//prend le samaccountname ou le userprincipalname en fct de la conf
			'samaccountname'=>array('label'=>'samaccountname','field'=>function(&$user,$infos){
				$user->login = str_replace($this->domain,'',mb_strtolower($infos[$this->authenticationMode][0]));
			},'source'=>'active_directory'),
			'origin'=>array('label'=>'samaccountname','field'=>'origin','value'=>'active_directory','source'=>'hardcoded'),
			'department'=>array('label'=>'Nom','field'=>'service','source'=>'active_directory'),
			'thumbnailphoto'=>array('label'=>'Photo 1','field'=>function(&$user,$infos){
				$user->meta['ldap_avatar'] = base64_encode($infos['thumbnailphoto'][0]);
			},'source'=>'active_directory'),
			'jpegphoto'=>array('label'=>'Photo 2','field'=>function(&$user,$infos){
				$user->meta['ldap_avatar'] = base64_encode($infos['jpegphoto'][0]);
			},'source'=>'active_directory'),
			'accountexpires'=>array('label'=>'Expiration','field'=>function(&$user,$infos){
				$seconds = (float)($infos['accountexpires'][0] / 10000000);
				//Convertion d'un timestamp AD en timestamp UNIX
				$timestamp = round($seconds - (((1970-1601) * 365.242190) * 86400));
				$user->meta['expiration'] = $timestamp;
			},'source'=>'active_directory'),
			//on traite memberof de manière particuliere donc pas de field
			'memberof'=>array('label'=>'Membre de','field'=>'','source'=>'active_directory'),
			'manager'=>array('label'=>'Manager','field'=>'manager','source'=>'active_directory'),
			'userprincipalname'=>array('label'=>'userprincipalname','field'=>'','source'=>'active_directory'),
			'whencreated'=>array('label'=>'Date création','field'=>function(&$user,$infos){
				if(!isset($infos['whencreated'][0]) || strlen($infos['whencreated'][0])<12 ) return;
				$created = substr($infos['whencreated'][0],0,8).' '.substr($infos['whencreated'][0],8,2).':'.substr($infos['whencreated'][0],10,2);
				$user->created = strtotime($created);
			},'source'=>'active_directory')

		);

		return $mapping;
	}

	//retourn les mapping champ ad->champ user par défaut + customs
	function getMapping(){
		$mapping = array();

		$jsonmapping = json_decode($this->mapping,true) ;
		if(is_array($jsonmapping)){
			foreach(json_decode($this->mapping,true) as $json){
				if(!isset($json['slug'])) continue;
				$json['source'] = 'active_directory';
				$mapping[$json['slug']]= $json;
			}
		}

		$mapping = array_merge(self::default_mapping(),$mapping);

		return $mapping;
	}

	//Gestion des modes d'authentification possibles
	function authenticationModes($key=null){
		$items = array(
			'samaccountname' => array('label'=>'SamAccountName','filter'=>function($server,$login){
				return "(&(samaccountname=".$login."))(objectClass=user)(objectCategory=person))";
			}),
			'userprincipalname' => array('label'=>'UserPrincipalName','filter'=>function($server,$login){
				return "(&(userprincipalname=".$login.$server->domain.")(objectClass=user)(objectCategory=person))";
			}),
		);
		if(!isset($key)) return $items;
		return isset($items[$key]) ? $items[$key] : array('label'=>'Non définis','filter'=>function($server,$login){});
	}

	//connexion ad
	function login($options = array()){

		$options = array_merge(array(
			'mode' => 'readonly',
			'ssl' => false
		),$options);

		putenv('LDAPTLS_REQCERT=never');


		$port = $options['ssl'] ? $this->sslPort : $this->port ;
		$ip = $this->ip;
		$ip = 'ldap'.($options['ssl']? 's' :'').'://'.$ip;
		$ip .= ':'.$port;

		if($ip==null || $port==null || $this->userRoot==null) throw new Exception('Paramètres de connexion manquants',400);
		$this->datasource = ldap_connect($ip);
		if(!$this->datasource) throw new Exception('Connexion échouée', 404);

		ldap_set_option($this->datasource,LDAP_OPT_PROTOCOL_VERSION,$this->protocolVersion);
		ldap_set_option($this->datasource, LDAP_OPT_REFERRALS, 0);

		switch($options['mode']){
			case 'admin':
				$login = $this->adminLogin;
				$password = decrypt($this->adminPassword);
			break;
			case 'readonly':
				$login = $this->readonlyLogin;
				$password = decrypt($this->readonlyPassword);
			break;
			case 'login':
				$login = $options['login'];
				$password = $options['password'];
			break;
		}

		try{
			if(@ldap_bind($this->datasource,$login,$password) == false)
				throw new Exception(ldap_error($this->datasource), 403);

		}catch(Exception $e){
			if(ldap_errno($this->datasource)==49){
				throw new Exception('Identifiant ou mot de passe incorrect', 403);
			}else{
				throw new Exception('Connexion échouée :'.ldap_error($this->datasource), 404);
			}

		}
	}

	/**
	 * Recherche des valeurs dans la base de données en fonction du filter
	 * @param <String> Racine contexte de la recherche
	 * @param <String> Filtre contenant les éléments a rechercher
	 * @param <Array> Liste des attributs à récupérer
	 * @return <Array> tableau contenant les objets correspondants a la recherche
	 */
	public function search($dns, $filter=null, $attributes=array(),$limit=0){

		$searches = ldap_search( array_fill(0, count($dns), $this->datasource) , $dns,$filter,$attributes,0,$limit);
		$infos = array();
		foreach($searches as $search){
			if($search === false) continue;
	    	$infos = array_merge($infos ,ldap_get_entries($this->datasource, $search));
		}
		return $infos;
	}


	//Déconnexion ad
	function logout(){
		if($this->datasource!=null) @ldap_close($this->datasource);
	}

	//Récuperation des utilisateurs
	function users($filters = array(),$options = array()){
		require_once(__DIR__.SLASH.'AdFirmRank.class.php');
		$mapping = $this->getMapping();
		$options = array_merge(array(
			'limit' => 0, // limit de users (0= pas de limite)
			'select' => array_keys($mapping), //Colonnes a cibler
			'group' => true, // charger les groupes ad
			'rank' => true, // charger les rank et les firms
			'manager' => false, // charger les managers en tant qu'objet user
			'activeOnly' => true //n'afficher que les comptes non expirés
		),$options);


		$groups = $options['group'] ? $this->groups() : array();
		$ranks = array();

		if($options['rank'] ){
			foreach(AdFirmRank::loadAll( array(), null,  null,  array('*'), 1)  as $firmRank){
				if(!isset($ranks[$firmRank->group])) $ranks[$firmRank->group] = array();
				$ranks[$firmRank->group][] = $firmRank;
			}
		}

		$users = array();

		$default_filters = array();
		$filterChain = '';

		$default_filters['objectClass'] = 'user';
		$default_filters['objectCategory'] = 'person';

		$filters = array_merge($default_filters,$filters);

		if(count($filters)>1) $filterChain ='(&';

		foreach($filters as $key=>$value)
			$filterChain .= '('.$key.'='.$value.')';

		if(count($filters)>1) $filterChain .= ')';

		$entries = $this->search(explode(PHP_EOL,$this->userRoot),$filterChain,$options['select'],$options['limit']);

		if(count($entries)==0 || $entries['count']==0)  return $users;
		if(!isset($entries[0])) return $users;

		$usersDn = array();
		for($i=0; $i<$entries['count']; $i++){
			$entry = $entries[$i];

			//Vérifie que le compte n'est pas expiré (nb : 0 et 9223372036854775807 sont les deux valeurs possibles pour un n'expire jamais (allez comprendre la logique microsoft...))
			if($options['activeOnly'] && isset($entry['accountexpires'][0]) && $entry['accountexpires'][0]!=0 && $entry['accountexpires'][0]!=9223372036854775807){
				//Convertion en seconds
				$seconds = (float)($entry['accountexpires'][0] / 10000000);
				//Convertion d'un timestamp AD en timestamp UNIX
				$timestamp = round($seconds - (((1970-1601) * 365.242190) * 86400));
			    if($timestamp <= time()) continue;
		    }

			$user = self::user_object($entry,$mapping,$options,$groups,$ranks);
			//on stocke l'id de l'ad ayant provisionné ce user dans les méta pour réutilisations ulterieure (ex save du mdp)

			$user->meta['activedirectory_server'] = $this->id;
			$user->meta = json_encode($user->meta);

			if($options['manager']) $usersDn[$entry['dn']] = $user;
			$users[$user->login] = $user;
		}

		//Chargement des managers
		if($options['manager']){

			foreach($users as $key=>$user){
				if(empty($user->manager)) continue;

				if(isset($usersDn[$user->manager])){
					$user->manager = $usersDn[$user->manager];
				}else{
					$manager = $this->users(array(
					'dn' => $user->manager
					),array(
						'limit' => 1, // limit de users (0= pas de limite)
						'group' => true, // charger les groupes ad
						'rank' => true, // charger les rank et les firms
						'rights' => false, // charger les rank et les firms
						'manager' => true, // charger les managers en tant qu'objet user
						'activeOnly' => true //n'afficher que les comptes non expirés
					));
					$user->manager = array_shift($manager);
				}
				$users[$key] = $user;
			}
		}

		return $users;
	}

	//Remplis et return un objet user en fonction d'une entrée ad
	static function user_object($entry,$mapping,$options = array(),$groups=array(),$ranks=array()){
		$user = new User();

		//récuperation des attributs mappés (défaut et custom)
		foreach($mapping as $ldap=>$attribute){
			$field = $attribute['field'];

			if(empty($field )) continue;
			//si le champ doit contenir une valeur en dur (et non ldap ex : origin : active_directory)
			if($attribute['source']=='hardcoded' && !empty($attribute['value'])){
				$user->$field = $attribute['value'];
				continue;
			}

			//si aucune correspondance trouvée dans l'entrée ldap on passe au suivant
			if(empty($entry[$ldap]) || empty($entry[$ldap][0])) continue;


			//si le field définit est une méthode anonyme
			if(!is_string($field)){
				$field($user,$entry);
			//si le field définit est un simple attribut
			}else{

				if(property_exists($user,$field)){
					$user->$field = $entry[$ldap][0];
				}else{
					$user->meta[$field] = $entry[$ldap][0];
				}
			}
		}



		//Récuperation des groupes direct et indirects
		$user->groups = array();
		if(isset($entry['memberof'])){
			for($i=0; $i<count($entry['memberof'])-1; ++$i){
				if(empty($groups[$entry['memberof'][$i]])) continue;
				self::group_recursive_fill($user,$groups,$entry['memberof'][$i],$ranks);
			}
		}
		return $user;
	}

	//ajout les groupes a l'utilisateur de manière récursive (un groupe peut être parent d'un autre)
	static function group_recursive_fill(&$user,$groups,$dn,$ranks){
		if(!isset($groups[$dn])) return;
		$groupItem = $groups[$dn];
		$user->groups[] = $groupItem['label'];

		if(isset($ranks[$groupItem['label']])){

			foreach($ranks[$groupItem['label']] as $firmrank){
				$user->ranks[$firmrank->firm][$firmrank->rank] = $firmrank->join('rank');
				$user->firms[$firmrank->firm] = $firmrank->join('firm');
			}

		}

		foreach($groupItem['memberof'] as $parentDn){
			self::group_recursive_fill($user,$groups,$parentDn,$ranks);
		}
	}

	function userChange($login,$changes){
		switch($this->authenticationMode){
			case 'samaccountname':
				$filter = "(".$this->authenticationMode."=".$login.")";
			break;
			case 'userprincipalname':
			default:
				$filter= "(".$this->authenticationMode."=".$login.$this->domain.")";
			break;
		}

		$filter = '(&'.$filter.'(objectClass=user)(objectCategory=person))';

		$entries= $this->search(array($this->userRoot),$filter);
		$cn =  (isset($entries[0])) ? $entries[0]['dn'] : false;
		if($cn == false) throw new Exception('Impossible de trouver l\'utilisateur sur ce serveur');


		putenv('LDAPTLS_REQCERT=never');
		foreach($changes as $adfield=>$value){
			if($adfield == 'password') continue;
			if($value != ''){
				$attributes[$adfield][0] = $value;
				return ldap_modify($this->datasource,$cn,$attributes);
			}else{
				//evite le crash si l'attribut n'existe pas lors d'une supression
				$attributes[$adfield] ='0';
				ldap_modify($this->datasource,$cn,$attributes);
				$attributes[$adfield] = array();
				return ldap_mod_del($this->datasource,$cn,$attributes);
			}
		}

		if(isset($changes['password'])){
			if (!ldap_mod_replace($this->datasource, $cn ,  self::encrypt_password($changes['password']))) throw new Exception("Impossible de modifier le mot de passe : ".ldap_error($this->datasource));
		}
	}

	public static function encrypt_password($newPassword){
		$newPassword = "\"" . $newPassword . "\"";
		$len = strlen( $newPassword );
		$newPassw = "";
		for ( $i = 0; $i < $len; $i++ )
			$newPassw .= $newPassword[$i]."\000";
		return array("unicodePwd" => $newPassw);
	}


	//Récuperation des groupes ad
	function groups($filters = array()){
		$groups = array();

		$default_filters = array();
		$filterChain = '';

		$default_filters['objectClass'] = 'group';

		$filters = array_merge($default_filters,$filters);

		if(count($filters)>1) $filterChain ='(&';

		foreach($filters as $key=>$value)
			$filterChain .= '('.$key.'='.$value.')';

		if(count($filters)>1) $filterChain .= ')';


		$entries = $this->search(explode(PHP_EOL,$this->groupRoot),$filterChain,array('name','memberOf'));



		for($i=0; $i<$entries['count']; $i++){
			$info = $entries[$i];

			$group = array(
				'label' => $info['name'][0],
				'dn' => $info['dn'],
				'memberof' => array()
			);
			//on récupere les groupes parents
			if(isset($info['memberof'])){
				for($u=0; $u<count($info['memberof'])-1; ++$u){
					//on ne prend que les groupes parent qui font partie de la racine groupe
					if(strrpos($info['memberof'][$u],$this->groupRoot)===false) continue;
					$group['memberof'][] = $info['memberof'][$u];
				}
			}
			$groups[$group['dn']] = $group;
		}
		uasort($groups,function($a,$b){
			return $a['label'] > $b['label'];
		});
		return $groups;
	}

}
?>