<?php

/**
 * Define an application user.
 * @author valentin carruesco
 * @category Core
 * @license MIT
 */
class User extends Entity {
    public $id,$login,$password,$function,$name,$firstname,$mail,$state,$rights,$firms,$superadmin,$token,$preferences,$phone,$mobile,$groups,$ranks,$manager,$service,$origin,$meta;
    public $entityLabel = 'Utilisateur';
    public $fields = array(
        'id' => 'key',
        'login' => array('label'=>'Identifiant','type'=>'string'),
        'password' => array('label'=>'Mot de passe','type'=>'password'),
        'name' => array('label'=>'Nom','type'=>'string'),
        'function' => array('label'=>'Fonction','type'=>'string'),
        'firstname' => array('label'=>'Prénom','type'=>'text'),
        'token' => array('label'=>'Token','type'=>'string'),
        'mail' => array('label'=>'E-mail','type'=>'mail'),
        'state' => array('label'=>'Etat','type'=>'string'),
        'phone' => array('label'=>'Téléphone','type'=>'phone'),
        'mobile' => array('label'=>'Téléphone portable','type'=>'phone'),
        'manager' => array('label'=>'Manager','type'=>'user'),
        'origin' => array('label'=>'Origine','type'=>'text'),
        'service' => array('label'=>'Service','type'=>'text'),
        'meta' => array('label'=>'Meta informations','type'=>'text')
    );

    function __construct(){
        global $myUser, $myFirm, $conf;
        parent::__construct();
        $this->token = self::generateToken();
        $this->meta = array();
        $this->ranks = array();
        $this->firms = array();
    }

    public static function getAll($options = array()){

        $users = array();

        $options = array_merge(array(
            'right' => true,
            'manager' => true,
            'force' =>false
        ),$options);


        if(isset($_SESSION['users_'.($options['right']?'rights':'norights')]))
            $users = unserialize($_SESSION['users_'.($options['right']?'rights':'norights')]);

        if(empty($users) || $options['force']){
            $users = array();
            //récuperation des users non db (plugin ad ...)
            Plugin::callHook('user_get_all',array(&$users,$options));

            $logins = array();

            //Récuperation des users db
            foreach (self::loadAll(array('state'=>User::ACTIVE), array(' name ASC ')) as $baseUser)
                $users[$baseUser->login] = $baseUser;

            //récuperation des logins concernés pour requettage
            foreach($users as $key=>$user){
                //chargement des managers pour les origines db
                if($options['manager'] && empty($user->origin) && !empty($user->manager) && isset($users[$user->manager])){
                  $users[$key]->manager = $users[$user->manager];
                }
                $logins[] = $user->login;
            }

            if($options['right']){
                $ranksId = array();

                //Ajout des rangs depuis les liens user/firm/rank
                foreach (UserFirmRank::loadAll(array('user:IN'=>$logins ), null,  null,  array('*'), 1) as $firmRank) {
                    $rank = $firmRank->join('rank');
                    $firm = $firmRank->join('firm');
                    $ranksId[] = $rank->id;
                    $users[$firmRank->user]->firms[$firm->id] = $firm;
                    if(!isset($users[$firmRank->user]->ranks[$firmRank->firm])) $users[$firmRank->user]->ranks[$firmRank->firm] = array();
                    $users[$firmRank->user]->ranks[$firmRank->firm][$rank->id] = $rank;
                    if(empty($users[$firmRank->user]->superadmin) && $rank->superadmin) $users[$firmRank->user]->superadmin = boolval($rank->superadmin);
                }

                //récuperation des droits ciblés sur user et rangs pour les user et rang user concernés
                $rights = Right::staticQuery('SELECT * FROM {{table}} WHERE
                    (targetScope="user" AND targetUid IN ('.implode(',',array_fill(0,count($logins),'?')).')) OR (targetScope="rank" AND targetUid IN('.implode(',',array_fill(0,count($ranksId),'?')).')) ',array_merge($logins,$ranksId),true);

                $rightsForRank = array();
                foreach($rights as $right):

                    //pour les rangs on remplis un tableau de mapping qu'on redistribuera plus tard
                    if($right->targetScope =='rank'){
                        if(!isset($rightsForRank[$right->targetUid])) $rightsForRank[$right->targetUid] = array();
                        $rightsForRank[$right->targetUid][]= $right;

                    //pour les users
                    }else{

                        //Pour le premier rang qui aborde cette scope on met tous les droits à false
                        if(!isset($users[$right->targetUid]->rights[$right->scope][$right->firm][$right->uid])){
                            $users[$right->targetUid]->rights[$right->scope][$right->firm][$right->uid] = array(
                                'read' => false,
                                'edit' => false,
                                'delete' => false,
                                'recursive' => false,
                                'configure' => false
                            );
                        }
                        //Puis on complete uniquement les droits à true sur la scope pour chaques rangs additionnels
                        if($right->read) $users[$right->targetUid]->rights[$right->scope][$right->firm][$right->uid]['read'] = true;
                        if($right->edit) $users[$right->targetUid]->rights[$right->scope][$right->firm][$right->uid]['edit'] = true;
                        if($right->delete) $users[$right->targetUid]->rights[$right->scope][$right->firm][$right->uid]['delete'] = true;
                        if($right->configure) $users[$right->targetUid]->rights[$right->scope][$right->firm][$right->uid]['configure'] = true;
                    }
                endforeach;

                //pour chaque user
                foreach($users as $key=>$user){
                    //onrecupere les firms
                    foreach($user->ranks as $firmrank){
                        //on liste les rangs du user pour cette firm
                        foreach($firmrank as $rank){
                            //si un de ces rangs possede des droits liés
                            if(!isset($rightsForRank[$rank->id])) continue;

                            //pour chaque droit liées on les applique au user
                            foreach($rightsForRank[$rank->id] as $right){
                               //Pour le premier rang qui aborde cette scope on met tous les droits à false
                                if(!isset($users[$key]->rights[$right->scope][$right->firm][$right->uid])){
                                    $users[$key]->rights[$right->scope][$right->firm][$right->uid] = array(
                                        'read' => false,
                                        'edit' => false,
                                        'delete' => false,
                                        'recursive' => false,
                                        'configure' => false
                                    );
                                }
                                //Puis on complete uniquement les droits à true sur la scope pour chaques rangs additionnels
                                if($right->read) $users[$key]->rights[$right->scope][$right->firm][$right->uid]['read'] = true;
                                if($right->edit) $users[$key]->rights[$right->scope][$right->firm][$right->uid]['edit'] = true;
                                if($right->delete) $users[$key]->rights[$right->scope][$right->firm][$right->uid]['delete'] = true;
                                if($right->configure) $users[$key]->rights[$right->scope][$right->firm][$right->uid]['configure'] = true;
                            }
                        }
                    }
                }
            }

            //sort des users par noms
            uasort($users, function($a, $b){ return strcmp($a->name, $b->name); });

            $_SESSION['users_'.($options['right']?'rights':'norights')] = serialize($users);
        }

        return $users;
    }

    public function __sleep(){
        $fields = $this->toArray();
        unset($fields['fullName'], $fields['initials']);
        return array_merge(array('superadmin','rights','groups','ranks','firms','preferences'),array_keys($fields));
    }

    //Surchage de toArray pour prendre en compte le fullName et les initials régulierement utilisé en toArray
    public function toArray($decoded=false) {
        $fields = parent::toArray($decoded);
        $fields['fullName'] = $this->fullName();
        $fields['initials'] = $this->initials();
        unset($fields['password']);
        unset($fields['token']);
        if(isset($fields['manager']) && is_object($fields['manager'])) $fields['manager'] = $fields['manager']->toArray();
        if($decoded){
            $fields['fullName'] = html_entity_decode($fields['fullName']);
            $fields['initials'] = html_entity_decode($fields['initials']);
        }
        return $fields;
    }

    // $this->rights[$right->scope][$right->uid][$right->firm]
    public function can($scope,$right,$uid=0,$options = array()){
        if($this->superadmin == 1) return true;
        global $myFirm;

        $firm = is_object($myFirm) && $myFirm->id !=0 ? $myFirm->id : 0;

        // Recherche dans les droits ciblé sur la firm courante
        if(isset($this->rights[$scope][$firm][$uid][$right]))
            return $this->rights[$scope][$firm][$uid][$right]==1;

        // Recherche dans les droits non ciblés sur une firm
        if(isset($this->rights[$scope][0][$uid][$right]))
            return $this->rights[$scope][0][$uid][$right]==1;

        if(isset($options['contains']) && $options['contains'] == true){
            if(isset($this->rights[$scope])) {
                $rightArray = array();
                if(isset($this->rights[$scope][0]))
                    $rightArray =$this->rights[$scope][0];
                if(isset($this->rights[$scope][$myFirm->id]))
                    $rightArray = $this->rights[$scope][$myFirm->id];
                foreach($rightArray as $uid=>$rights){
                    if(!isset($rights[$right]) || !$rights[$right]) continue;
                    return true;
                }
            }
        }

        return false;
    }

    //Lance les exception appropriées en fonction du droit ou des droits spécifiés
    // ex : User::check_access('document','configure');
    public static function check_access($scope,$right,$uid=0){
        global $myUser;
        if(!isset($myUser) || !is_object($myUser) || !$myUser->connected()) throw new Exception("Contrôle d'accès - Vous devez être connecté",401);
        if(!$myUser->can($scope,$right,$uid)) throw new Exception("Contrôle d'accès - Permissions insuffisantes ('".$scope.".".$right."')",403);
    }

    //Vérifie que l'utilisateur courant est lié a l'id du rang spécifié
    public function hasRank($rankId){
        if($this->superadmin) return true;
        $rankIds = array();
        global $myFirm;
        if(empty($this->ranks) || !isset($this->ranks[$myFirm->id])) return false;
        foreach ($this->ranks[$myFirm->id] as $rank)
            $rankIds[$rank->id] = true;
        return isset($rankIds[$rankId]);
    }

    public function preference($key=null, $value=null){
        global $myUser;
        if(!isset($key) && !isset($value)) return $this->preferences;
        if(isset($key) && !isset($value)) return isset($this->preferences[$key])?$this->preferences[$key]:'';

        if(isset($key) && isset($value)){
            $this->preferences[$key] = $value;
            $preference = UserPreference::load(array('key'=>$key,'user'=>$this->login));
            if(!$preference) $preference = new UserPreference();
            $preference->key = $key;
            $preference->value = $value;
            $preference->user = $this->login;
            $preference->save();
            if($myUser->login == $this->login) $_SESSION['currentUser'] = serialize($this);
        }
    }

    public function loadRanks(){
        //Ajout des rangs depuis les liens user/firm/rank
        foreach (UserFirmRank::loadAll(array('user'=>$this->login), null,  null,  array('*'), 1) as $firmRank) {
            $rank = $firmRank->join('rank');
            $firm = $firmRank->join('firm');
            $this->firms[$firm->id] = $firm;
            if(!isset($this->ranks[$firmRank->firm])) $this->ranks[$firmRank->firm] = array();
            $this->ranks[$firmRank->firm][$rank->id] = $rank;
            if(empty($this->superadmin) && $rank->superadmin) $this->superadmin = boolval($rank->superadmin);
        }
    }

    public function loadPreferences(){
        $this->preferences = array();
        foreach(UserPreference::loadAll(array('user'=>$this->login)) as $line):
            $this->preferences[$line->key] = $line->value;
        endforeach;
    }

     //@TODO: Ajouter paramètre $firm pour cibler la récup
    //des droits pour une firm en particulier et plus $myFirm
    public function loadRights(){
        global $myFirm;
        $this->rights = array();
        if($this->superadmin) return;

        if(!isset($this->ranks)) $this->ranks = array();
        if(!isset($myFirm) || !isset($this->ranks[$myFirm->id]) || count($this->ranks[$myFirm->id])==0) return;

        $ranksId = array();
        foreach($this->ranks[$myFirm->id] as $rank){
            if(!isset($rank->id) || !is_numeric($rank->id)) continue;
            $ranksId[] = $rank->id;
        }
        if(count($ranksId)==0) return;

        $rights = Right::staticQuery('SELECT * FROM {{table}} WHERE (targetScope="user" AND targetUid=?) OR (targetScope="rank" AND targetUid IN('.str_repeat('?,', count($ranksId) - 1) . '?'.'))',array_merge(array($this->login), $ranksId),true);
        foreach($rights as $right):
            //Pour le premier rang qui aborde cette scope on met tous les droits à false
            if(!isset($this->rights[$right->scope][$right->firm][$right->uid])){
                $this->rights[$right->scope][$right->firm][$right->uid] = array(
                    'read' => false,
                    'edit' => false,
                    'delete' => false,
                    'recursive' => false,
                    'configure' => false
                );
            }
            //Puis on complete uniquement les droits à true sur la scope pour chaques rangs additionnels
            if($right->read) $this->rights[$right->scope][$right->firm][$right->uid]['read'] = true;
            if($right->edit) $this->rights[$right->scope][$right->firm][$right->uid]['edit'] = true;
            if($right->delete) $this->rights[$right->scope][$right->firm][$right->uid]['delete'] = true;
            if($right->configure) $this->rights[$right->scope][$right->firm][$right->uid]['configure'] = true;
        endforeach;

        /*
        $permissions = Right::staticQuery('SELECT * FROM {{table}} WHERE (targetScope="user" AND targetUid=?) OR (targetScope="rank" AND targetUid IN('.str_repeat('?,', count($ranksId) - 1) . '?'.'))',array_merge(array($this->login), $ranksId),true);
        $this->rights['permission'] = array();
        foreach ($permissions as $permission) {
            if(!isset($this->rights['permission'][$permission->scope])) $this->rights['permission'][$permission->scope] = array();
            if(!isset($this->rights['permission'][$permission->scope][$permission->uid])){
                $this->rights['permission'][$permission->scope][$permission->uid] = array(
                    'read' => false,
                    'edit' => false,
                    'delete' => false,
                    'recursive' => false,
                    'configure' => false
                );
            }

            if($permission->read) $this->rights['permission'][$permission->scope][$permission->uid]['read'] = true;
            if($permission->edit) $this->rights['permission'][$permission->scope][$permission->uid]['edit'] = true;
            if($permission->delete) $this->rights['permission'][$permission->scope][$permission->uid]['delete'] = true;
            if($permission->recursive) $this->rights['permission'][$permission->scope][$permission->uid]['recursive'] = true;
            if($permission->configure) $this->rights['permission'][$permission->scope][$permission->uid]['configure'] = true;
        }
        */
    }

    public function getFirms(){
        $this->firms = array();
        foreach(Firm::staticQuery('SELECT f.* FROM {{table}} f LEFT JOIN '.UserFirmRank::tableName().' uf ON uf.firm=f.id WHERE uf.user=?',array($this->login),true) as $firm):

            $this->firms[$firm->id] = $firm;
        endforeach;
    }

    //Vérifie que l'utilisateur courant est lié à l'id établissement spécifié
    //tags : hasFirm
    public function haveFirm($id){
        return in_array($id, array_keys($this->firms));
    }

    public function getAvatar($getPath = false){
        $avatar = 'img/default-avatar.png';
        if(!$this->check_avatar_path_length()) return $avatar;
        $files = glob(__ROOT__.FILE_PATH.AVATAR_PATH.self::format_avatar_name($this->login).self::get_avatar_extension_brace(),GLOB_BRACE);

        if(count($files)>0){
            if($getPath) return $files[0];
            preg_match("/\.(\w{3,4})$|\?/m", $files[0], $extension);
            $avatar = 'action.php?action=core_account_avatar_download&user='.urlencode($this->login).'&extension='.$extension[1];
        }
        return $avatar;
    }

    public function check_avatar_path_length(){
        return strlen($this->login) <= OS_path_max_length() - strlen(__ROOT__.FILE_PATH.AVATAR_PATH) - strlen(User::get_avatar_extension_brace());
    }

    public static function format_avatar_name($text){
        return iconv('utf-8','windows-1256//IGNORE', $text);
    }

    public static function get_avatar_extension_brace(){
        return ".{jpg,png,jpeg,gif}";
    }

    public static function initialization(&$user, $loadRight){
        global $myFirm,$conf;

        //Ajout du check de l'état du user
        if(!empty($user->state) && $user->state!=User::ACTIVE) return new self;
        $user->ranks = empty($user->ranks) ? array() : $user->ranks;
        $user->firms = empty($user->firms) ? array() : $user->firms;
        if(isset($user->manager) && !empty($user->manager) && !is_object($user->manager)) $user->manager = self::byLogin($user->manager);
        $user->loadRanks();
        $user->loadPreferences();

        //Suppression des rangs et de la firm Anonyme
        if(isset($user->ranks[-1])) unset($user->ranks[-1]);
        if(isset($user->firms[-1])) unset($user->firms[-1]);

        //Set des rangs et droits pour le SuperAdmin
        if($user->superadmin == 1){
            foreach(Firm::loadAll() as $firm) {
                $user->firms[$firm->id] = $firm;

                foreach (Rank::loadAll() as $rank)
                    $user->ranks[$firm->id][$rank->id] = $rank;
            }
        }

        //Set de l'établissement par défaut
        if(!empty($user->firms)){
            $defaultFirm = !empty($user->preference('default_firm')) ? $user->preferences['default_firm'] : key($user->firms);
            $myFirm = isset($user->firms[$defaultFirm]) ? $user->firms[$defaultFirm] : reset($user->firms);
            if(!isset($user->firms[$defaultFirm])) $user->preference('default_firm',$myFirm->id);

            //Ajout des rang par défaut user connecté
            if(!empty($conf->get('connected_default_rank'))) {
                foreach(Rank::loadAll(array('id:IN'=>$conf->get('connected_default_rank'))) as $rank){
                    foreach ($user->firms as $firm) {
                        if(!isset($user->ranks[$firm->id])) $user->ranks[$firm->id] = array();
                        $user->ranks[$firm->id][$rank->id] = $rank;
                    }
                }
            }
        }
        if($loadRight) $user->loadRights();
    }

    public static function check($login, $password, $loadRight=true){
        global $myFirm,$conf;
        //Load from db
        $user = self::load(array('login'=>$login));
        if($user && !password_verify($password, $user->password)) $user = false;

        //Load from plugins
        Plugin::callHook("user_login", array(&$user,htmlspecialchars_decode($login),htmlspecialchars_decode($password),$loadRight));

        //init user
        if($user!=false) self::initialization($user, $loadRight);

        $user = is_object($user) ? $user : new self;
        return $user;
    }

    //Connecte unn utilisateur sans recourir au mot de passe
    public static function connectLogin($login){
        global $myFirm,$conf;

        //Load from db
        $user = self::load(array('login'=>$login,'state'=>User::ACTIVE));

        //Load from plugins
        Plugin::callHook("user_login", array(&$user,htmlspecialchars_decode($login),null,true,true,true));

        if($user!=false)
            self::initialization($user, true);

        $user = is_object($user) ? $user : new self;
        return $user;
    }

    public static function byLogin($login, $loadRight=true, $force=false){
        foreach(User::getAll(array('right'=>$loadRight, 'force'=>$force)) as $user){
            if($user->login != $login) continue;
            return $user;
        }
        return new User();
    }

    public function lastname(){
        return mb_strtoupper(htmlspecialchars_decode(mb_strtolower($this->name)));
    }

    public function firstname(){
        return mb_ucfirst(htmlspecialchars_decode(mb_strtolower($this->firstname)));
    }

    public function fullName(){
        $fullName = $this->firstname().' '.$this->lastname();
        return trim($fullName) != '' ? $fullName : $this->login;
    }

    public function initials(){
        $fullname = str_replace(array(' ','\''),'-',implode(' ',array($this->firstname.' '.$this->name)));
        $fullname = explode('-',$fullname);

        $result = '';
        foreach($fullname as $value)
            $result.= mb_strtoupper(mb_substr($value,0,1));

        return $result;
    }

    public function subordinates(){
        $subordinates = array();
        foreach (User::getAll() as $user) {
            if(is_object($user->manager) && $user->manager->login == $this->login) $subordinates[] = $user;
        }
        return $subordinates;
    }

    public static function generate_password($length=16,$iteration=0){
        global $conf;

        if($length<0) $length = 16;
        $chars =  'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.
                '0123456789`-=~!@#$%^&*()_+,./<>?;:[]{}\|';
        //Suppression des caractères interdits en conf de la chaîne de traitement
        $chars = str_replace(preg_split('//u', htmlspecialchars_decode($conf->get('password_forbidden_char')), null, PREG_SPLIT_NO_EMPTY), '', $chars);

        $str = '';
        $max = strlen($chars) - 1;

        for ($i=0; $i<$length; $i++)
        $str .= $chars[mt_rand(0, $max)];

        //Génère un password tant que ne correspond pas aux règles définies
        //(maximum 100 itérations pour éviter boucle infinie)
        if(!empty(self::check_password_format($str)) && $iteration<100) $str = self::generate_password($length,++$iteration);

        return $str;
    }

    public static function password_formats(){
        $formats = array(
            array('pattern'=>'|[0-9]|i','label'=>'Le mot de passe doit comporter au minimum 1 chiffre (norme ANSSI)'),
            array('pattern'=>'|[A-Z]|','label'=>'Le mot de passe doit comporter au minimum 1 majuscule (norme ANSSI)'),
            array('pattern'=>'|[^A-Za-z0-9éèêëàäâïîöôûüù]|i','label'=>'Le mot de passe doit comporter au minimum 1 caractère spécial (norme ANSSI)'),
            array('pattern'=>'|.{6,}|','label'=>'Le mot de passe doit comporter au minimum 6 caractères'),
            array('pattern'=>'|.{12,}|','label'=>'Le mot de passe doit comporter au minimum 12 caractères (norme ANSSI)'),
        );
        return $formats;
    }

    public static function check_password_format($password){
        global $conf;
        $errors = array();
        $formats = array();
        foreach (self::password_formats() as $format) {
            $formats[$format['pattern']] = $format;
        }
        $selectedFormats = json_decode($conf->get('password_format'),true);

        if(is_array($selectedFormats)){
            foreach($selectedFormats as $pattern){
                if(!isset($formats[$pattern])) continue;
                $format = $formats[$pattern];
                if(!preg_match($pattern, $password)) $errors[] = $format['label'];
            }
        }
        return $errors;
    }

    public static function password_encrypt($password){
        return password_hash($password, PASSWORD_DEFAULT);
    }

    public function connected(){
        return !empty($this->login);
    }

    public function disconnect($redirect=null){
        global $conf;
        if(isset($this->login)) $this->preference('cookie','');

        $url = 'index.php';
        if(!is_null($redirect)) $url = base64_decode($redirect);
        //Permet la redirection vers une url spécifique lors de la déco
        if(isset($_SESSION['logout_redirect'])) $url = $_SESSION['logout_redirect'];

        unset($_SESSION['currentUser']);
        unset($_SESSION['firm']);
        session_destroy();
        unset($_COOKIE[COOKIE_NAME]);
        setcookie(COOKIE_NAME, null, -1, '/');

        return $url;
    }

    public static function generateToken(){
        return substr(md5(uniqid(rand(), true)),0,10);
    }

    public function getGroups(){
        return (is_array($this->groups) ? $this->groups : array());
    }

    public function setFirms($firms){
        if(empty($firms)) return;
        $this->firms = $firms;
    }

    public function getRanksId($firmId=null){
        if(empty($this->ranks)) return array();

        $ranks = array();
        if(!empty($firmId)) {
            //On ne rassemble pas les conditions pour ne pas passer dans le else si la sous-condition est fausse et donc garder un array vide
            if(!empty($this->ranks[$firmId])) $ranks = array_keys($this->ranks[$firmId]);
        } else {
            foreach($this->ranks as $fId => $userRanks)
                $ranks[$fId] = array_keys($userRanks);
        }
        return $ranks;
    }

    //Retourne un objet manager (User vide si pas de manager) quel que soit le provider d'entré (ad : objet, db: login)
    public function manager(){
        $manager = new User();
        if(!isset($this->manager)) return $manager;
        if(is_object($this->manager)) $manager = $this->manager;
        if(is_string($this->manager) && !empty($this->manager)) $manager = User::byLogin($this->manager);

        return is_object($manager) ? $manager: new User();
    }
}
