<?php

if (!defined('BASEPATH'))
    exit('No direct script access allowed');

/**
 * Name:  Ion Auth
 *
 * Author: Ben Edmunds
 * 		  ben.edmunds@gmail.com
 *         @benedmunds
 *
 * Added Awesomeness: Phil Sturgeon
 *
 * Location: http://github.com/benedmunds/CodeIgniter-Ion-Auth
 *
 * Created:  10.01.2009
 *
 * Description:  Modified auth system based on redux_auth with extensive customization.  This is basically what Redux Auth 2 should be.
 * Original Author name has been kept but that does not mean that the method has not been modified.
 *
 * Requirements: PHP5 or above
 *
 */
class Ion_auth
{

    /**
     * CodeIgniter global
     *
     * @var string
     * */
    protected $ci;

    /**
     * account status ('not_activated', etc ...)
     *
     * @var string
     * */
    protected $status;

    /**
     * message (uses lang file)
     *
     * @var string
     * */
    protected $messages;

    /**
     * error message (uses lang file)
     *
     * @var string
     * */
    protected $errors = array();

    /**
     * error start delimiter
     *
     * @var string
     * */
    protected $error_start_delimiter;

    /**
     * error end delimiter
     *
     * @var string
     * */
    protected $error_end_delimiter;

    /**
     * extra where
     *
     * @var array
     * */
    public $_extra_where = array();

    /**
     * extra set
     *
     * @var array
     * */
    public $_extra_set = array();
    public $mode = 'internal';
    public $restClient = null;
    private $admin_role = 'admin';
    
    const TWO_FACTOR_UNSUCCESSFUL_ERROR_MESSAGE = 'Invalid two-factor authentication code';
    const TWO_FACTOR_UNSUCCESSFUL_ERROR = 'two_factor_unsuccessful';

    /**
     * __construct
     *
     * @return void
     * @author Mathew
     * */
    public function __construct()
    {
        $this->ci = & get_instance();
        $this->ci->load->config('ion_auth', TRUE);
        $this->ci->load->config('appsettings', TRUE);
        $this->ci->load->library('email');
        $this->ci->load->library('session');



        // $this->ci->load->library('Auth_Ldap');
        $this->ci->load->library('encryption');
        $this->ci->lang->load('ion_auth');
        $this->ci->load->model(array('astrolabe_model','api_rest_model','authentication_model','authentication_model_pgsql','settings_rest_model'));
        $this->ci->load->helper('cookie');


        $this->messages = array();
        $this->errors = array();
        $this->infos = array();
        $this->email=$this->ci->config->item('mission_portal_email', 'ion_auth');
        $this->message_start_delimiter = $this->ci->config->item('message_start_delimiter', 'ion_auth');
        $this->message_end_delimiter = $this->ci->config->item('message_end_delimiter', 'ion_auth');
        $this->error_start_delimiter = $this->ci->config->item('error_start_delimiter', 'ion_auth');
        $this->error_end_delimiter = $this->ci->config->item('error_end_delimiter', 'ion_auth');
        $this->info_start_delimiter = $this->ci->config->item('info_start_delimiter', 'ion_auth');
        $this->info_end_delimiter = $this->ci->config->item('info_end_delimiter', 'ion_auth');
        //load the mode of authentication


        $this->auth_model_pgsql=$this->ci->authentication_model_pgsql;
        $this->auth_model = $this->ci->authentication_model;


        if (!$this->mode)
        {
            $this->set_error('backend_error');
            log_message('info', 'cannot find any mode switching to internal database');
            $this->mode = 'internal';
            //return FALSE;
        }

        if ($this->ci->session->userdata('mode') !== FALSE)
        {
            $this->mode = $this->ci->session->userdata('mode');
        }


    }

    function setRestClient($restClient)
    {
        $this->restClient = $restClient;
        $this->auth_model->setRestClient($this->getRestClient());
    }

    function getRestClient()
    {
        return $this->restClient;
    }

    /**
     * __call
     *
     * Acts as a simple way to call model methods without loads of stupid alias'
     *
     * */
    public function __call($method, $arguments)
    {
        if (!method_exists($this->ci->authentication_model, $method))
        {
            throw new Exception('Undefined method Ion_auth::' . $method . '() called');
        }

        return call_user_func_array(array($this->ci->authentication_model, $method), $arguments);
    }



    /**
     * Change password.
     *
     * @return void
     * @author Mathew
     * */
    public function change_password($identity, $old, $new, $twoFaCode = '')
    {
     try
        {
            if ($this->auth_model->change_password($identity, $old, $new, $twoFaCode))
            {
                $this->set_message('password_change_successful');
                return TRUE;
            }

            $this->set_error('password_change_unsuccessful');
            return FALSE;
        }
        catch (Exception $e)
        {
              $this->set_error($e->getMessage());
              return FALSE;
        }
    }
    
    /**
     * register
     *
     * @return void
     * @author Mathew
     * */
    public function register($data) //need to test email activation
    {
       try{
            $id = $this->auth_model->createUser($data);
            if ($id !== FALSE)
            {
                $this->set_message('account_creation_successful');
                return TRUE;
            }
            else
            {
                $this->set_error('account_creation_unsuccessful');
                return FALSE;
            }
       }catch(Exception $e)
       {
            $this->set_error($e->getMessage());
            return FALSE;
       }
    }

    /**
     * login
     *
     * @return bool
     * @author Mathew
     * */
    public function login(string $username, string $password, bool $remember = false, ?string $twoFaCode = '')
    {
        $return = false;
        try
        {
            $accessTokenLifeTime = $remember ? 
                $this->ci->config->item('remember_user_expire', 'ion_auth') :
                $this->ci->config->item('user_expire', 'ion_auth');
            
            $val = $this->auth_model->login($username, $password, life_time: $accessTokenLifeTime, twoFaCode: $twoFaCode);
            if (is_array($val) && !empty($val))
            {
                $data = $this->ci->api_rest_model->get_api_details();
	            $isLdapEnabled = $this->ci->settings_rest_model->app_settings_get_item('ldapEnabled');
	            $this->mode = $isLdapEnabled ? 'external' : 'internal';
                $this->ci->session->set_userdata('username',$data['userId']);
                if (isset($data['timezone'])) {
                    $this->ci->session->set_userdata('timezone', $data['timezone']);
                }
                $this->ci->session->set_userdata('access_token',$val['access_token']);
                $this->ci->session->set_userdata('mode', $this->mode);
                $this->on_login_successful($data['userId']);
                $return = true;
            }
        } catch (HttpClient_TooManyRequests|HttpClient_Forbidden $e) {
            $this->set_error($e->getMessage());
        } catch (HttpClient_NotFound $e) {
            $this->set_error('cannot_contact_rest_server');
        } catch (HttpClient_Unauthorized $e) {
            if (str_contains($e->getMessage(), self::TWO_FACTOR_UNSUCCESSFUL_ERROR_MESSAGE)) {
                $this->set_error(self::TWO_FACTOR_UNSUCCESSFUL_ERROR);
            } else {
                $this->set_error('login_unsuccessful');
            }
        } catch (Exception $e) {
            log_message(log_level_for_exception($e), $e->getMessage());
            $this->set_error('login_unsuccessful_unknown_reason');
        }
        return $return;
    }

    /**
     * basic rest login
     *
     * @return void
     * @author Mathew
     * */
    public function basic_rest_login($headers)
    {
        try
        {
            if(!isset($headers['Authorization']) || empty($headers['Authorization'])) {
                log_message('debug', 'Basic auth login failure. Authorization header is not set.');
                return false;
            }

            $authorizationHeader = $headers['Authorization'];
            $credentials = str_replace('Basic ', '', $authorizationHeader);
            $credentials = explode(':', base64_decode($credentials), 2);
            $val = $this->auth_model->login($credentials[0], $credentials[1]);
            if (is_array($val) && !empty($val))
            {
                // get username form api details and set roles and isAdmin flag
                $data = $this->ci->api_rest_model->get_api_details();
                $user = [
                    'id' => $data['userId'],
                    'role' => $this->get_user_role($data['userId']),
                    'isAdmin' =>  in_array($this->admin_role, $this->get_user_role($data['userId']))
                ];

                return $user;
            }

            return false;
        }
        catch (Exception $e)
        {
            log_message(log_level_for_exception($e),$e->getMessage());
            if ($e instanceof HttpClient_NotFound)
            {
                $this->set_error('cannot_contact_rest_server');
            }
            elseif($e instanceof HttpClient_Unauthorized)
            {
                $this->set_error('login_unsuccessful');
            }else{
                $this->set_error('login_unsuccessful_unknown_reason');
            }
            return false;
        }
    }

    protected function on_login_successful($username)
    {
        $isFirstLogin = $this->auth_model_pgsql->is_first_login($username, $this->mode);

        if ($isFirstLogin)
        {
            $newdata = array(
                                'firstLogin' => TRUE
                            );
            $this->ci->session->set_userdata($newdata);
            $this->ci->astrolabe_model->add_builtin_profiles($username);
            $this->auth_model_pgsql->add_last_login($username, $this->mode);
        }
        else
        {
            $this->ci->session->unset_userdata('firstLogin');
            $this->auth_model_pgsql->update_last_login($username);
        }


        $session_data = array(
            'roles' => $this->get_user_role($username)
        );
        $this->ci->session->set_userdata($session_data);
    }
    

    /**
     * logout
     *
     * @return void
     * @author Mathew
     * */
    public function logout()
    {
        $token = $this->ci->session->userdata('access_token');
        if ($token) {
            $this->getRestClient()->setupOauthAuth($token);
            $this->auth_model->unsetToken($token);
        }

        $this->ci->session->unset_userdata('username');
        $this->ci->session->unset_userdata('role');
        $this->ci->session->unset_userdata('id');
        $this->ci->session->unset_userdata('user_id');

        //delete the remember me cookies if they exist
        if (get_cookie('identity'))
        {
            delete_cookie('identity');
        }
        if (get_cookie('remember_code'))
        {
            delete_cookie('remember_code');
        }
         if (get_cookie('mode'))
        {
            delete_cookie('mode');
        }

        $this->ci->session->sess_destroy();

        $this->set_message('logout_successful');
        return TRUE;
    }

    /**
     * logged_in
     *
     * @return bool
     * @author Mathew
     * */
    public function logged_in()
    {
        return (bool) $this->ci->session->userdata('username');
    }

    /**
     * is_admin
     *
     * @return bool
     * @author Ben Edmunds
     * */
    public function is_admin($check_real_assigned_roles = false)
    {

        if ($check_real_assigned_roles == false)
        {
            $user_role = $this->ci->session->userdata('roles');
        }
        else
        {
            $user_role = $this->get_user_rolelist($this->ci->session->userdata('username'));

        }

        if ($user_role === False || empty($user_role))
        {
            return false;
        }

        return in_array($this->admin_role, $user_role);
    }


    /**
     * is_role
     *
     * @return bool
     * @author Phil Sturgeon
     * */
    public function is_role($check_role)
    {
        $user_role = $this->ci->session->userdata('role');

        if (is_array($check_role))
        {
            return in_array($user_role, $check_role);
        }

        return $user_role == $check_role;
    }


    public function get_user_role($username)
    {
        return $this->auth_model->getRolesForUser($username);
    }

    public function get_user_rolelist($id)
    {
        $roles = $this->get_user_role($id);
        if (!empty($roles) && $roles !== False)
        {
            return $roles;
        }
        return array();
    }

   /**
    *
    * @return array of arrays
    */
    public function get_users_array($page,$count,$usertype,$returnWithMeta=false)
    {
        $page = is_int($page) ? $page : 1;
        $count = is_int($count) ? $count : 25;
        return $this->auth_model->getAllUsers($page, $count, $usertype, $returnWithMeta);
    }

    public function get_total_users($usertype){
        $users=$this->auth_model->getAllUsers(1,1,$usertype,true);
        return $users['total'];
    }

    public function search_user($usertype,$expression,$returnWithMeta=false){
        $users=$this->auth_model->searchUser($usertype,$expression,$returnWithMeta);
        return $users;
    }

    /**
     *
     * @param type $id
     * @return associative array
     */
    public function get_user($id=false)
    {
        try
        {
            return $this->auth_model->getUserDetails($id);
        }
        catch (Exception $e)
        {
            return false;
        }
    }


    /**
     * update_user
     *
     * @param <string> $id user id (name right now)
     * @param <data>   $data user data
     * @return boolean
     *
     * */
    public function update_user($id, $data, $twoFaCode = '')
    {
        $roles = $this->get_user_rolelist($id);
        $admin_role = 'admin';

        /*$count = $this->count_users_in_role($admin_role);
        if ($count <= 1 && in_array($admin_role, $roles) && !in_array($admin_role, $data['roles']))
        {
            $this->set_error('one_admin_required');
            return FALSE;
        }*/

        try
        {
            if ($this->auth_model->update_user($id, $data, [Cf_RestInstance::CF_2FA_TOKEN_HEADER => $twoFaCode]))
            {
                $this->set_message('update_successful');
                return TRUE;
            }
        }
        catch (Exception $e)
        {
            log_message(log_level_for_exception($e),sprintf('Update unsuccessful for user %s with data %s. Error:: %s ',$id,json_encode($data),$e->getMessage()));
            $this->set_error('update_unsuccessful');
            return FALSE;
        }
    }


    /*public function count_users_in_role($role)
    {
        $users = $this->get_users_array();
        $count = 0;
        foreach ((array) $users as $user)
        {
            if (key_exists('roles',$user) && in_array($role, $user['roles']))
            {
                $count++;
            }
        }
        return $count;
    }*/

    /**
     * delete_user
     *
     * @return void
     * @author Phil Sturgeon
     * */
    public function delete_user($id)
    {
        $roles = $this->get_user_rolelist($id);
        $admin_role = 'admin';

        try
        {
            //delete from phpcfengine users table
            if (!$this->auth_model_pgsql->delete_user_from_logged_in_table($id))
            {
                $this->set_error('user_delete_unsuccessful');
                return FALSE;
            }


            if ($this->auth_model->deleteUser($id))
            {

                $this->ci->astrolabe_model->profile_delete_all($id);

                $this->set_message('user_delete_successful');
                return TRUE;
            }
            $this->set_error('user_delete_unsuccessful');
            return FALSE;
        }
        catch (Exception $e)
        {
            $this->set_error($e->getMessage());
            return FALSE;
        }
    }

    /**
     * Delete role
     *
     * @param type $name role name
     * @return type bool
     */
    public function delete_role($rolename)
    {
        try
        {
            if ($this->auth_model->deleteRole($rolename))
            {
                $this->set_message('role_delete_successful');
                return TRUE;
            }
            $this->set_error('role_delete_unsuccessful');
            return FALSE;
        }
        catch (Exception $e)
        {
            $this->set_error($e->getMessage());
            return false;
        }
    }



    /**
     * set_message_delimiters
     *
     * Set the message delimiters
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function set_message_delimiters($start_delimiter, $end_delimiter)
    {
        $this->message_start_delimiter = $start_delimiter;
        $this->message_end_delimiter = $end_delimiter;

        return TRUE;
    }

    /**
     * set_info_delimiters
     */
    public function set_info_delimiters($start_delimiter, $end_delimiter)
    {
        $this->info_start_delimiter = $start_delimiter;
        $this->info_end_delimiter = $end_delimiter;
        return TRUE;
    }

    /**
     * set_error_delimiters
     *
     * Set the error delimiters
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function set_error_delimiters($start_delimiter, $end_delimiter)
    {
        $this->error_start_delimiter = $start_delimiter;
        $this->error_end_delimiter = $end_delimiter;

        return TRUE;
    }

    /**
     * set_message
     *
     * Set a message
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function set_message($message)
    {
        $this->messages[] = $message;

        return $message;
    }

    /**
     * messages
     *
     * Get the messages
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function messages()
    {
        $_output = '';
        foreach ($this->messages as $message)
        {
         if ($this->ci->lang->line($message, log_errors: false) != '')
               $_output .= $this->message_start_delimiter . $this->ci->lang->line($message) . $this->message_end_delimiter;
          else
               $_output .= $this->error_start_delimiter . $message . $this->error_end_delimiter;
        }

        return $_output;
    }

    /**
     * set_error
     *
     * Set an error message
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function set_error($error)
    {
        $this->errors[] = $error;

        return $error;
    }

    /**
     * errors
     *
     * Get the error message
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function errors()
    {
        $_output = '';
        foreach ($this->errors as $error)
        {
            if ($this->ci->lang->line($error, log_errors: false) != '')
                $_output .= $this->error_start_delimiter . $this->ci->lang->line($error) . $this->error_end_delimiter;
            else
                $_output .= $this->error_start_delimiter . $error . $this->error_end_delimiter;
        }

        return $_output;
    }

    /**
     * To set the information to be displayed to user
     * @param <type> $info
     * @return <type>
     */
    public function set_info($info)
    {
        $this->infos[] = $info;
        return $info;
    }

    /**
     * get all the information to be displayed
     * @return string
     */
    public function infos()
    {
        $_output = '';
        foreach ($this->infos as $info)
        {
            // do not log errors if line not found, otherwise private information can be leaked
            if ($this->ci->lang->line($info, log_errors: false) != '')
               $_output .= $this->message_start_delimiter . $this->ci->lang->line($info) . $this->message_end_delimiter;
          else
               $_output .= $this->error_start_delimiter . $info . $this->error_end_delimiter;
        }
        return $_output;
    }


    public function get_roles()
    {
        return $this->auth_model->getAllRoles();
    }

    public function get_default_role()
    {
        return $this->auth_model->getDefaultRole();
    }

    public function set_default_role($name)
    {
        return $this->auth_model->setDefaultRole($name);
    }

    /**
     * Returns the details of a role in arrAY.
     * @param type $rolename
     * @return type
     */
    public function get_role_detail($rolename)
    {
        try
        {
           $data=$this->auth_model->getRoleDetails($rolename);
           if(is_array($data))
           {
             return $data;
           }
           $this->set_error('error_fetching_details');
           return false;
        }
        catch(Exception $e)
        {
            $this->set_error($e->getMessage());
        }
    }


    public function create_role($data)
    {
        if ($this->auth_model->createRole($data))
        {
            $this->set_message('role_creation_successful');
            return true;
        }
        else
        {
            $this->set_error('role_creation_unsuccessful');
            return FALSE;
        }
    }


    public function update_role($rolename, $data)
    {

        try
        {
            if ($this->auth_model->updateRole($rolename, $data))
            {
                $this->set_message('role_update_successful');
                return TRUE;
            }

            $this->set_error('role_update_unsuccessful');
            return FALSE;
        }
        catch (Exception $e)
        {
            $this->set_error($e->getMessage());
            return FALSE;
        }
    }

    public function set_mode($mode)
    {
        $this->mode = $mode;
    }

    public function reset_password($username)
    {
       try
        {
         $newPassword=$this->auth_model->resetPassword($username);
        }
        catch (Exception $e)
        {
            if (str_contains($e->getMessage(), self::TWO_FACTOR_UNSUCCESSFUL_ERROR_MESSAGE)) {
                $this->set_error($e->getMessage());
            } else {
                $this->set_error('password_reset_unsucessful');
            }
          return false;
        }

       if($newPassword !== FALSE)
       {
           $status=$this->send_email($username, $newPassword);
           if($status)
           {
             $this->set_message('password_reset_emailed');
             return true;
           }
           else
           {
             $this->set_info(' <strong>Warning!</strong> New password could not be emailed to user, the generated password is <b>'.$newPassword.'</b>. Please mail it manually to the user');
             return true;
           }
       }
       else
       {
           $this->set_error('password_reset_unsucessful');
           return false;
       }

    }

    public function send_email($username, $newpassword)
    {
            $user=$this->get_user($username);

            if(!key_exists('email', $user))
            {
                return FALSE;
            }

            $data = array(
                'identity' => $username,
                'new_password' => $newpassword,
                'expiration' => $this->ci->config->item("RESET_PASSWORD_EXPIRE_AFTER_HOURS")
            );
            $message = $this->ci->load->view($this->ci->config->item('email_templates', 'ion_auth') . $this->ci->config->item('email_forgot_password_complete', 'ion_auth'), $data, true);
            $this->ci->email->clear();
            $config['mailtype'] = $this->ci->config->item('email_type', 'ion_auth');
            $this->ci->email->initialize($config);
            $this->ci->email->set_newline("\r\n");
            $this->ci->email->from($this->email, $this->ci->config->item('site_title', 'ion_auth'));
            $this->ci->email->to($user['email']);
            $this->ci->email->subject($this->ci->config->item('site_title', 'ion_auth') . ' - New Password');
            $this->ci->email->message($message);
                if ($this->ci->email->send())
                {
                    return TRUE;
                }
                else
                {
                    return FALSE;
                }
    }

}
