| Current Path : /home/users/unlimited/www/eshop.codeskitter.site/application/models/ |
| Current File : /home/users/unlimited/www/eshop.codeskitter.site/application/models/Ion_auth_model.php |
<?php
/**
* Name: Ion Auth Model
* Author: Ben Edmunds
* ben.edmunds@gmail.com
* @benedmunds
*
* Added Awesomeness: Phil Sturgeon
*
* 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.6 or above
*
* @package CodeIgniter-Ion-Auth
* @author Ben Edmunds
* @link http://github.com/benedmunds/CodeIgniter-Ion-Auth
* @filesource
*/
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Class Ion Auth Model
* @property Ion_auth $ion_auth The Ion_auth library
*/
#[\AllowDynamicProperties]
class Ion_auth_model extends CI_Model
{
/**
* Max cookie lifetime constant
*/
const MAX_COOKIE_LIFETIME = 63072000; // 2 years = 60*60*24*365*2 = 63072000 seconds;
/**
* Max password size constant
*/
const MAX_PASSWORD_SIZE_BYTES = 4096;
/**
* Holds an array of tables used
*
* @var array
*/
public $tables = [];
/**
* activation code
*
* Set by deactivate() function
* Also set on register() function, if email_activation
* option is activated
*
* This is the value devs should give to the user
* (in an email, usually)
*
* It contains the *user* version of the activation code
* It's a value of the form "selector.validator"
*
* This is not the same activation_code as the one in DB.
* The DB contains a *hashed* version of the validator
* and a selector in another column.
*
* THe selector is not private, and only used to lookup
* the validator.
*
* The validator is private, and to be only known by the user
* So in case of DB leak, nothing could be actually used.
*
* @var string
*/
public $activation_code;
/**
* new password
*
* @var string
*/
public $new_password;
/**
* Identity
*
* @var string
*/
public $identity;
/**
* Where
*
* @var array
*/
public $_ion_where = [];
/**
* Select
*
* @var array
*/
public $_ion_select = [];
/**
* Like
*
* @var array
*/
public $_ion_like = [];
/**
* Limit
*
* @var string
*/
public $_ion_limit = NULL;
/**
* Offset
*
* @var string
*/
public $_ion_offset = NULL;
/**
* Order By
*
* @var string
*/
public $_ion_order_by = NULL;
/**
* Order
*
* @var string
*/
public $_ion_order = NULL;
/**
* Hooks
*
* @var object
*/
protected $_ion_hooks;
/**
* Response
*
* @var string
*/
protected $response = NULL;
/**
* message (uses lang file)
*
* @var string
*/
protected $messages;
/**
* error message (uses lang file)
*
* @var string
*/
protected $errors;
/**
* error start delimiter
*
* @var string
*/
protected $error_start_delimiter;
/**
* error end delimiter
*
* @var string
*/
protected $error_end_delimiter;
/**
* caching of users and their groups
*
* @var array
*/
public $_cache_user_in_group = [];
/**
* caching of groups
*
* @var array
*/
protected $_cache_groups = [];
/**
* Database object
*
* @var object
*/
protected $db;
public function __construct()
{
$this->config->load('ion_auth', TRUE);
$this->load->helper('cookie', 'date');
$this->lang->load('ion_auth');
// initialize the database
$group_name = $this->config->item('database_group_name', 'ion_auth');
if (empty($group_name)) {
// By default, use CI's db that should be already loaded
$CI = &get_instance();
$this->db = $CI->db;
} else {
// For specific group name, open a new specific connection
$this->db = $this->load->database($group_name, TRUE, TRUE);
}
// initialize db tables data
$this->tables = $this->config->item('tables', 'ion_auth');
// initialize data
$this->identity_column = $this->config->item('identity', 'ion_auth');
$this->join = $this->config->item('join', 'ion_auth');
// initialize hash method options (Bcrypt)
$this->hash_method = $this->config->item('hash_method', 'ion_auth');
// initialize messages and error
$this->messages = [];
$this->errors = [];
$delimiters_source = $this->config->item('delimiters_source', 'ion_auth');
// load the error delimeters either from the config file or use what's been supplied to form validation
if ($delimiters_source === 'form_validation') {
// load in delimiters from form_validation
// to keep this simple we'll load the value using reflection since these properties are protected
$this->load->library('form_validation');
$form_validation_class = new ReflectionClass("CI_Form_validation");
$error_prefix = $form_validation_class->getProperty("_error_prefix");
$error_prefix->setAccessible(TRUE);
$this->error_start_delimiter = $error_prefix->getValue($this->form_validation);
$this->message_start_delimiter = $this->error_start_delimiter;
$error_suffix = $form_validation_class->getProperty("_error_suffix");
$error_suffix->setAccessible(TRUE);
$this->error_end_delimiter = $error_suffix->getValue($this->form_validation);
$this->message_end_delimiter = $this->error_end_delimiter;
} else {
// use delimiters from config
$this->message_start_delimiter = $this->config->item('message_start_delimiter', 'ion_auth');
$this->message_end_delimiter = $this->config->item('message_end_delimiter', 'ion_auth');
$this->error_start_delimiter = $this->config->item('error_start_delimiter', 'ion_auth');
$this->error_end_delimiter = $this->config->item('error_end_delimiter', 'ion_auth');
}
// initialize our hooks object
$this->_ion_hooks = new stdClass;
$this->trigger_events('model_constructor');
}
/**
* Getter to the DB connection used by Ion Auth
* May prove useful for debugging
*
* @return object
*/
public function db()
{
return $this->db;
}
/**
* Hashes the password to be stored in the database.
*
* @param string $password
* @param string $identity
*
* @return false|string
* @author Mathew
*/
public function hash_password($password, $identity = NULL)
{
// Check for empty password, or password containing null char, or password above limit
// Null char may pose issue: http://php.net/manual/en/function.password-hash.php#118603
// Long password may pose DOS issue (note: strlen gives size in bytes and not in multibyte symbol)
if (
empty($password) || strpos($password, "\0") !== FALSE ||
strlen($password) > self::MAX_PASSWORD_SIZE_BYTES
) {
return FALSE;
}
$algo = $this->_get_hash_algo();
$params = $this->_get_hash_parameters($identity);
if ($algo !== FALSE && $params !== FALSE) {
return password_hash($password, $algo, $params);
}
return FALSE;
}
/**
* This function takes a password and validates it
* against an entry in the users table.
*
* @param string $password
* @param string $hash_password_db
* @param string $identity optional @deprecated only for BC SHA1
*
* @return bool
* @author Mathew
*/
public function verify_password($password, $hash_password_db, $identity = NULL)
{
// Check for empty id or password, or password containing null char, or password above limit
// Null char may pose issue: http://php.net/manual/en/function.password-hash.php#118603
// Long password may pose DOS issue (note: strlen gives size in bytes and not in multibyte symbol)
if (
empty($password) || empty($hash_password_db) || strpos($password, "\0") !== FALSE
|| strlen($password) > self::MAX_PASSWORD_SIZE_BYTES
) {
return FALSE;
}
// password_hash always starts with $
if (strpos($hash_password_db, '$') === 0) {
return password_verify($password, $hash_password_db);
} else {
// Handle legacy SHA1 @TODO to delete in later revision
return $this->_password_verify_sha1_legacy($identity, $password, $hash_password_db);
}
}
/**
* Check if password needs to be rehashed
* If true, then rehash and update it in DB
*
* @param string $hash
* @param string $identity
* @param string $password
*
*/
public function rehash_password_if_needed($hash, $identity, $password)
{
$algo = $this->_get_hash_algo();
$params = $this->_get_hash_parameters($identity);
if ($algo !== FALSE && $params !== FALSE) {
if (password_needs_rehash($hash, $algo, $params)) {
if ($this->_set_password_db($identity, $password)) {
$this->trigger_events(['rehash_password', 'rehash_password_successful']);
} else {
$this->trigger_events(['rehash_password', 'rehash_password_unsuccessful']);
}
}
}
}
/**
* Get a user by its activation code
*
* @param bool $user_code the activation code
* It's the *user* one, containing "selector.validator"
* the one you got in activation_code member
*
* @return bool|object
* @author Indigo
*/
public function get_user_by_activation_code($user_code)
{
// Retrieve the token object from the code
$token = $this->_retrieve_selector_validator_couple($user_code);
if ($token) {
// Retrieve the user according to this selector
$user = $this->where('activation_selector', $token->selector)->users()->row();
if ($user) {
// Check the hash against the validator
if ($this->verify_password($token->validator, $user->activation_code)) {
return $user;
}
}
}
return FALSE;
}
/**
* Validates and removes activation code.
*
* @param int|string $id the user identifier
* @param bool $code the *user* activation code
* if omitted, simply activate the user without check
*
* @return bool
* @author Mathew
*/
public function activate($id, $code = FALSE)
{
$this->trigger_events('pre_activate');
if ($code !== FALSE) {
$user = $this->get_user_by_activation_code($code);
}
// Activate if no code is given
// Or if a user was found with this code, and that it matches the id
if ($code === FALSE || ($user && $user->id === $id)) {
$data = [
'activation_selector' => NULL,
'activation_code' => NULL,
'active' => 1
];
$this->trigger_events('extra_where');
$this->db->update($this->tables['login_users'], $data, ['id' => $id]);
if ($this->db->affected_rows() === 1) {
$this->trigger_events(['post_activate', 'post_activate_successful']);
$this->set_message('activate_successful');
return TRUE;
}
}
$this->trigger_events(['post_activate', 'post_activate_unsuccessful']);
$this->set_error('activate_unsuccessful');
return FALSE;
}
/**
* Updates a users row with an activation code.
*
* @param int|string|null $id
*
* @return bool
* @author Mathew
*/
public function deactivate($id = NULL)
{
$token = $this->_generate_selector_validator_couple(20, 40);
$this->activation_code = $token->user_code;
$data = [
'activation_selector' => $token->selector,
'activation_code' => $token->validator_hashed,
'active' => 0
];
$this->trigger_events('extra_where');
$this->db->update($this->tables['login_users'], $data, ['id' => $id]);
$return = $this->db->affected_rows() == 1;
if ($return) {
$this->set_message('deactivate_successful');
} else {
$this->set_error('deactivate_unsuccessful');
}
return $return;
}
/**
* Clear the forgotten password code for a user
*
* @param string $identity
*
* @return bool Success
*/
public function clear_forgotten_password_code($identity)
{
if (empty($identity)) {
return FALSE;
}
$data = [
'forgotten_password_selector' => NULL,
'forgotten_password_code' => NULL,
'forgotten_password_time' => NULL
];
$this->db->update($this->tables['login_users'], $data, [$this->identity_column => $identity]);
return TRUE;
}
/**
* Clear the remember code for a user
*
* @param string $identity
*
* @return bool Success
*/
public function clear_remember_code($identity)
{
if (empty($identity)) {
return FALSE;
}
$data = [
'remember_selector' => NULL,
'remember_code' => NULL
];
$this->db->update($this->tables['login_users'], $data, [$this->identity_column => $identity]);
return TRUE;
}
/**
* Reset password
*
* @param string $identity
* @param string $new
*
* @return bool
* @author Mathew
*/
public function reset_password($identity, $new)
{
$this->trigger_events('pre_change_password');
if (!$this->identity_check($identity)) {
$this->trigger_events(['post_change_password', 'post_change_password_unsuccessful']);
return FALSE;
}
$return = $this->_set_password_db($identity, $new);
if ($return) {
$this->trigger_events(['post_change_password', 'post_change_password_successful']);
$this->set_message('password_change_successful');
} else {
$this->trigger_events(['post_change_password', 'post_change_password_unsuccessful']);
$this->set_error('password_change_unsuccessful');
}
return $return;
}
/**
* Change password
*
* @param string $identity
* @param string $old
* @param string $new
*
* @return bool
* @author Mathew
*/
public function change_password($identity, $old, $new)
{
$this->trigger_events('pre_change_password');
$this->trigger_events('extra_where');
$query = $this->db->select('id, password')
->where($this->identity_column, $identity)
->limit(1)
->order_by('id', 'desc')
->get($this->tables['login_users']);
if ($query->num_rows() !== 1) {
$this->trigger_events(['post_change_password', 'post_change_password_unsuccessful']);
$this->set_error('password_change_unsuccessful');
return FALSE;
}
$user = $query->row();
if ($this->verify_password($old, $user->password, $identity)) {
$result = $this->_set_password_db($identity, $new);
if ($result) {
$this->trigger_events(['post_change_password', 'post_change_password_successful']);
$this->set_message('password_change_successful');
} else {
$this->trigger_events(['post_change_password', 'post_change_password_unsuccessful']);
$this->set_error('password_change_unsuccessful');
}
return $result;
}
$this->set_error('password_change_unsuccessful');
return FALSE;
}
/**
* Checks username
*
* @param string $username
*
* @return bool
* @author Mathew
*/
public function username_check($username = '')
{
$this->trigger_events('username_check');
if (empty($username)) {
return FALSE;
}
$this->trigger_events('extra_where');
return $this->db->where('username', $username)
->limit(1)
->count_all_results($this->tables['login_users']) > 0;
}
/**
* Checks email
*
* @param string $email
*
* @return bool
* @author Mathew
*/
public function email_check($email = '')
{
$this->trigger_events('email_check');
if (empty($email)) {
return FALSE;
}
$this->trigger_events('extra_where');
return $this->db->where('email', $email)
->limit(1)
->count_all_results($this->tables['login_users']) > 0;
}
/**
* Identity check
*
* @param $identity string
*
* @return bool
* @author Mathew
*/
public function identity_check($identity = '')
{
$this->trigger_events('identity_check');
if (empty($identity)) {
return FALSE;
}
return $this->db->where($this->identity_column, $identity)
->limit(1)
->count_all_results($this->tables['login_users']) > 0;
}
/**
* Get user ID from identity
*
* @param $identity string
*
* @return bool|int
*/
public function get_user_id_from_identity($identity = '')
{
if (empty($identity)) {
return FALSE;
}
$query = $this->db->select('id')
->where($this->identity_column, $identity)
->limit(1)
->get($this->tables['login_users']);
if ($query->num_rows() !== 1) {
return FALSE;
}
$user = $query->row();
return $user->id;
}
/**
* Insert a forgotten password key.
*
* @param string $identity
*
* @return bool|string
* @author Mathew
* @updated Ryan
*/
public function forgotten_password($identity)
{
if (empty($identity)) {
$this->trigger_events(['post_forgotten_password', 'post_forgotten_password_unsuccessful']);
return FALSE;
}
// Generate random token: smaller size because it will be in the URL
$token = $this->_generate_selector_validator_couple(20, 80);
$update = [
'forgotten_password_selector' => $token->selector,
'forgotten_password_code' => $token->validator_hashed,
'forgotten_password_time' => time()
];
$this->trigger_events('extra_where');
$this->db->update($this->tables['login_users'], $update, [$this->identity_column => $identity]);
if ($this->db->affected_rows() === 1) {
$this->trigger_events(['post_forgotten_password', 'post_forgotten_password_successful']);
return $token->user_code;
} else {
$this->trigger_events(['post_forgotten_password', 'post_forgotten_password_unsuccessful']);
return FALSE;
}
}
/**
* Get a user from a forgotten password key.
*
* @param string $user_code
*
* @return bool|object
* @author Mathew
* @updated Ryan
*/
public function get_user_by_forgotten_password_code($user_code)
{
// Retrieve the token object from the code
$token = $this->_retrieve_selector_validator_couple($user_code);
if ($token) {
// Retrieve the user according to this selector
$user = $this->where('forgotten_password_selector', $token->selector)->users()->row();
if ($user) {
// Check the hash against the validator
if ($this->verify_password($token->validator, $user->forgotten_password_code)) {
return $user;
}
}
}
return FALSE;
}
/**
* Register
*
* @param string $identity
* @param string $password
* @param string $email
* @param array $additional_data
* @param array $groups
*
* @return bool
* @author Mathew
*/
public function register($identity, $password, $email, $additional_data = [], $groups = [])
{
$this->trigger_events('pre_register');
$manual_activation = $this->config->item('manual_activation', 'ion_auth');
if ($this->identity_check($identity)) {
$this->set_error('account_creation_duplicate_identity');
return FALSE;
} else if (!$this->config->item('default_group', 'ion_auth') && empty($groups)) {
$this->set_error('account_creation_missing_default_group');
return FALSE;
}
// check if the default set in config exists in database
$query = $this->db->get_where($this->tables['groups'], ['name' => $this->config->item('default_group', 'ion_auth')], 1)->row();
if (!isset($query->id) && empty($groups)) {
$this->set_error('account_creation_invalid_default_group');
return FALSE;
}
// capture default group details
$default_group = $query;
// IP Address
$ip_address = $this->input->ip_address();
// Do not pass $identity as user is not known yet so there is no need
$password = $this->hash_password($password);
if ($password === FALSE) {
$this->set_error('account_creation_unsuccessful');
return FALSE;
}
$this->identity_column = (isset($additional_data['type']) && $additional_data['type'] == 'phone') ? $this->identity_column : 'email';
// Users table.
$data = [
$this->identity_column => $identity,
'password' => $password,
'email' => $email,
'ip_address' => $ip_address,
'created_on' => time(),
'active' => ($manual_activation === FALSE ? 1 : 0)
];
// filter out any data passed that doesnt have a matching column in the users table
// and merge the set user data and the additional data
$user_data = array_merge($this->_filter_data($this->tables['login_users'], $additional_data), $data);
$this->trigger_events('extra_set');
$this->db->insert($this->tables['login_users'], $user_data);
$id = $this->db->insert_id($this->tables['login_users'] . '_id_seq');
// add in groups array if it doesn't exists and stop adding into default group if default group ids are set
if (isset($default_group->id) && empty($groups)) {
$groups[] = $default_group->id;
}
if (!empty($groups)) {
// add to groups
foreach ($groups as $group) {
$this->add_to_group($group, $id);
}
}
$this->trigger_events('post_register');
return (isset($id)) ? $id : FALSE;
}
/**
* login
*
* @param string $identity
* @param string $password
* @param bool $remember
*
* @return bool
* @author Mathew
*/
public function login($identity, $password, $remember = FALSE, $type = '')
{
$this->trigger_events('pre_login');
if (empty($identity) || empty($password)) {
$this->set_error('login_unsuccessful');
return FALSE;
}
$this->trigger_events('extra_where');
if (isset($type) && !empty($type) && $type != '' && $type != 'NULL') {
$query = $this->db->select($this->identity_column . ', email, id, password, active, last_login')
->where($this->identity_column, $identity)
->where('type', $type)
->limit(1)
->order_by('id', 'desc')
->get($this->tables['login_users']);
} else {
$query = $this->db->select($this->identity_column . ', email, id, password, active, last_login')
->where($this->identity_column, $identity)
->limit(1)
->order_by('id', 'desc')
->get($this->tables['login_users']);
}
if ($this->is_max_login_attempts_exceeded($identity)) {
// Hash something anyway, just to take up time
$this->hash_password($password);
$this->trigger_events('post_login_unsuccessful');
$this->set_error('login_timeout');
return FALSE;
}
if ($query->num_rows() === 1) {
$user = $query->row();
if ($type == 'phone') {
if ($this->verify_password($password, $user->password, $identity)) {
if ($user->active == 0) {
$this->trigger_events('post_login_unsuccessful');
$this->set_error('login_unsuccessful_not_active');
return FALSE;
}
$this->set_session($user);
$this->update_last_login($user->id);
$this->clear_login_attempts($identity);
$this->clear_forgotten_password_code($identity);
if ($this->config->item('remember_users', 'ion_auth')) {
if ($remember) {
$this->remember_user($identity);
} else {
$this->clear_remember_code($identity);
}
}
// Rehash if needed
$this->rehash_password_if_needed($user->password, $identity, $password);
// Regenerate the session (for security purpose: to avoid session fixation)
$this->session->sess_regenerate(FALSE);
$this->trigger_events(['post_login', 'post_login_successful']);
$this->set_message('login_successful');
return TRUE;
}
} else {
if ($user->active == 0) {
$this->trigger_events('post_login_unsuccessful');
$this->set_error('login_unsuccessful_not_active');
return FALSE;
}
$this->set_session($user);
$this->update_last_login($user->id);
$this->clear_login_attempts($identity);
$this->clear_forgotten_password_code($identity);
if ($this->config->item('remember_users', 'ion_auth')) {
if ($remember) {
$this->remember_user($identity);
} else {
$this->clear_remember_code($identity);
}
}
// Rehash if needed
$this->rehash_password_if_needed($user->password, $identity, $password);
// Regenerate the session (for security purpose: to avoid session fixation)
$this->session->sess_regenerate(FALSE);
$this->trigger_events(['post_login', 'post_login_successful']);
$this->set_message('login_successful');
return TRUE;
}
}
// Hash something anyway, just to take up time
$this->hash_password($password);
$this->increase_login_attempts($identity);
$this->trigger_events('post_login_unsuccessful');
$this->set_error('login_unsuccessful');
return FALSE;
}
/**
* Verifies if the session should be rechecked according to the configuration item recheck_timer. If it does, then
* it will check if the user is still active
* @return bool
*/
public function recheck_session()
{
if (empty($this->session->userdata('identity'))) {
return FALSE;
}
$recheck = (NULL !== $this->config->item('recheck_timer', 'ion_auth')) ? $this->config->item('recheck_timer', 'ion_auth') : 0;
if ($recheck !== 0) {
$last_login = $this->session->userdata('last_check');
if ($last_login + $recheck < time()) {
$query = $this->db->select('id')
->where([
$this->identity_column => $this->session->userdata('identity'),
'active' => '1'
])
->limit(1)
->order_by('id', 'desc')
->get($this->tables['login_users']);
if ($query->num_rows() === 1) {
$this->session->set_userdata('last_check', time());
} else {
$this->trigger_events('logout');
$identity = $this->config->item('identity', 'ion_auth');
$this->session->unset_userdata([$identity, 'id', 'user_id']);
return FALSE;
}
}
}
$session_hash = $this->session->userdata('ion_auth_session_hash');
return (bool)$session_hash && $session_hash === $this->config->item('session_hash', 'ion_auth');
}
/**
* is_max_login_attempts_exceeded
* Based on code from Tank Auth, by Ilya Konyukhov (https://github.com/ilkon/Tank-Auth)
*
* @param string $identity user's identity
* @param string|null $ip_address IP address
* Only used if track_login_ip_address is set to TRUE.
* If NULL (default value), the current IP address is used.
* Use get_last_attempt_ip($identity) to retrieve a user's last IP
*
* @return boolean
*/
public function is_max_login_attempts_exceeded($identity, $ip_address = NULL)
{
if ($this->config->item('track_login_attempts', 'ion_auth')) {
$max_attempts = $this->config->item('maximum_login_attempts', 'ion_auth');
if ($max_attempts > 0) {
$attempts = $this->get_attempts_num($identity, $ip_address);
return $attempts >= $max_attempts;
}
}
return FALSE;
}
/**
* Get number of login attempts for the given IP-address or identity
* Based on code from Tank Auth, by Ilya Konyukhov (https://github.com/ilkon/Tank-Auth)
*
* @param string $identity User's identity
* @param string|null $ip_address IP address
* Only used if track_login_ip_address is set to TRUE.
* If NULL (default value), the current IP address is used.
* Use get_last_attempt_ip($identity) to retrieve a user's last IP
*
* @return int
*/
public function get_attempts_num($identity, $ip_address = NULL)
{
if ($this->config->item('track_login_attempts', 'ion_auth')) {
$this->db->select('1', FALSE);
$this->db->where('login', $identity);
if ($this->config->item('track_login_ip_address', 'ion_auth')) {
if (!isset($ip_address)) {
$ip_address = $this->input->ip_address();
}
$this->db->where('ip_address', $ip_address);
}
$this->db->where('time >', time() - $this->config->item('lockout_time', 'ion_auth'), FALSE);
$qres = $this->db->get($this->tables['login_attempts']);
return $qres->num_rows();
}
return 0;
}
/**
* Get the last time a login attempt occurred from given identity
*
* @param string $identity User's identity
* @param string|null $ip_address IP address
* Only used if track_login_ip_address is set to TRUE.
* If NULL (default value), the current IP address is used.
* Use get_last_attempt_ip($identity) to retrieve a user's last IP
*
* @return int The time of the last login attempt for a given IP-address or identity
*/
public function get_last_attempt_time($identity, $ip_address = NULL)
{
if ($this->config->item('track_login_attempts', 'ion_auth')) {
$this->db->select('time');
$this->db->where('login', $identity);
if ($this->config->item('track_login_ip_address', 'ion_auth')) {
if (!isset($ip_address)) {
$ip_address = $this->input->ip_address();
}
$this->db->where('ip_address', $ip_address);
}
$this->db->order_by('id', 'desc');
$qres = $this->db->get($this->tables['login_attempts'], 1);
if ($qres->num_rows() > 0) {
return $qres->row()->time;
}
}
return 0;
}
/**
* Get the IP address of the last time a login attempt occurred from given identity
*
* @param string $identity User's identity
*
* @return string
*/
public function get_last_attempt_ip($identity)
{
if ($this->config->item('track_login_attempts', 'ion_auth') && $this->config->item('track_login_ip_address', 'ion_auth')) {
$this->db->select('ip_address');
$this->db->where('login', $identity);
$this->db->order_by('id', 'desc');
$qres = $this->db->get($this->tables['login_attempts'], 1);
if ($qres->num_rows() > 0) {
return $qres->row()->ip_address;
}
}
return '';
}
/**
* Based on code from Tank Auth, by Ilya Konyukhov (https://github.com/ilkon/Tank-Auth)
*
* Note: the current IP address will be used if track_login_ip_address config value is TRUE
*
* @param string $identity User's identity
*
* @return bool
*/
public function increase_login_attempts($identity)
{
if ($this->config->item('track_login_attempts', 'ion_auth')) {
$data = ['ip_address' => '', 'login' => $identity, 'time' => time()];
if ($this->config->item('track_login_ip_address', 'ion_auth')) {
$data['ip_address'] = $this->input->ip_address();
}
return $this->db->insert($this->tables['login_attempts'], $data);
}
return FALSE;
}
/**
* clear_login_attempts
* Based on code from Tank Auth, by Ilya Konyukhov (https://github.com/ilkon/Tank-Auth)
*
* @param string $identity User's identity
* @param int $old_attempts_expire_period In seconds, any attempts older than this value will be removed.
* It is used for regularly purging the attempts table.
* (for security reason, minimum value is lockout_time config value)
* @param string|null $ip_address IP address
* Only used if track_login_ip_address is set to TRUE.
* If NULL (default value), the current IP address is used.
* Use get_last_attempt_ip($identity) to retrieve a user's last IP
*
* @return bool
*/
public function clear_login_attempts($identity, $old_attempts_expire_period = 86400, $ip_address = NULL)
{
if ($this->config->item('track_login_attempts', 'ion_auth')) {
// Make sure $old_attempts_expire_period is at least equals to lockout_time
$old_attempts_expire_period = max($old_attempts_expire_period, $this->config->item('lockout_time', 'ion_auth'));
$this->db->where('login', $identity);
if ($this->config->item('track_login_ip_address', 'ion_auth')) {
if (!isset($ip_address)) {
$ip_address = $this->input->ip_address();
}
$this->db->where('ip_address', $ip_address);
}
// Purge obsolete login attempts
$this->db->or_where('time <', time() - $old_attempts_expire_period, FALSE);
return $this->db->delete($this->tables['login_attempts']);
}
return FALSE;
}
/**
* @param int $limit
*
* @return static
*/
public function limit($limit)
{
$this->trigger_events('limit');
$this->_ion_limit = $limit;
return $this;
}
/**
* @param int $offset
*
* @return static
*/
public function offset($offset)
{
$this->trigger_events('offset');
$this->_ion_offset = $offset;
return $this;
}
/**
* @param array|string $where
* @param null|string $value
*
* @return static
*/
public function where($where, $value = NULL)
{
$this->trigger_events('where');
if (!is_array($where)) {
$where = [$where => $value];
}
array_push($this->_ion_where, $where);
return $this;
}
/**
* @param string $like
* @param string|null $value
* @param string $position
*
* @return static
*/
public function like($like, $value = NULL, $position = 'both')
{
$this->trigger_events('like');
array_push($this->_ion_like, [
'like' => $like,
'value' => $value,
'position' => $position
]);
return $this;
}
/**
* @param array|string $select
*
* @return static
*/
public function select($select)
{
$this->trigger_events('select');
$this->_ion_select[] = $select;
return $this;
}
/**
* @param string $by
* @param string $order
*
* @return static
*/
public function order_by($by, $order = 'desc')
{
$this->trigger_events('order_by');
$this->_ion_order_by = $by;
$this->_ion_order = $order;
return $this;
}
/**
* @return object|mixed
*/
public function row()
{
$this->trigger_events('row');
$row = $this->response->row();
return $row;
}
/**
* @return array|mixed
*/
public function row_array()
{
$this->trigger_events(['row', 'row_array']);
$row = $this->response->row_array();
return $row;
}
/**
* @return mixed
*/
public function result()
{
$this->trigger_events('result');
$result = $this->response->result();
return $result;
}
/**
* @return array|mixed
*/
public function result_array()
{
$this->trigger_events(['result', 'result_array']);
$result = $this->response->result_array();
return $result;
}
/**
* @return int
*/
public function num_rows()
{
$this->trigger_events(['num_rows']);
$result = $this->response->num_rows();
return $result;
}
/**
* users
*
* @param array|null $groups
*
* @return static
* @author Ben Edmunds
*/
public function users($groups = NULL)
{
$this->trigger_events('login_users');
if (isset($this->_ion_select) && !empty($this->_ion_select)) {
foreach ($this->_ion_select as $select) {
$this->db->select($select);
}
$this->_ion_select = [];
} else {
// default selects
$this->db->select([
$this->tables['login_users'] . '.*',
$this->tables['login_users'] . '.id as id',
$this->tables['login_users'] . '.id as user_id'
]);
}
// filter by group id(s) if passed
if (isset($groups)) {
// build an array if only one group was passed
if (!is_array($groups)) {
$groups = [$groups];
}
// join and then run a where_in against the group ids
if (isset($groups) && !empty($groups)) {
$this->db->distinct();
$this->db->join(
$this->tables['users_groups'],
$this->tables['users_groups'] . '.' . $this->join['login_users'] . '=' . $this->tables['login_users'] . '.id',
'inner'
);
}
// verify if group name or group id was used and create and put elements in different arrays
$group_ids = [];
$group_names = [];
foreach ($groups as $group) {
if (is_numeric($group)) $group_ids[] = $group;
else $group_names[] = $group;
}
$or_where_in = (!empty($group_ids) && !empty($group_names)) ? 'or_where_in' : 'where_in';
// if group name was used we do one more join with groups
if (!empty($group_names)) {
$this->db->join($this->tables['groups'], $this->tables['users_groups'] . '.' . $this->join['groups'] . ' = ' . $this->tables['groups'] . '.id', 'inner');
$this->db->where_in($this->tables['groups'] . '.name', $group_names);
}
if (!empty($group_ids)) {
$this->db->{$or_where_in}($this->tables['users_groups'] . '.' . $this->join['groups'], $group_ids);
}
}
$this->trigger_events('extra_where');
// run each where that was passed
if (isset($this->_ion_where) && !empty($this->_ion_where)) {
foreach ($this->_ion_where as $where) {
$this->db->where($where);
}
$this->_ion_where = [];
}
if (isset($this->_ion_like) && !empty($this->_ion_like)) {
foreach ($this->_ion_like as $like) {
$this->db->or_like($like['like'], $like['value'], $like['position']);
}
$this->_ion_like = [];
}
if (isset($this->_ion_limit) && isset($this->_ion_offset)) {
$this->db->limit($this->_ion_limit, $this->_ion_offset);
$this->_ion_limit = NULL;
$this->_ion_offset = NULL;
} else if (isset($this->_ion_limit)) {
$this->db->limit($this->_ion_limit);
$this->_ion_limit = NULL;
}
// set the order
if (isset($this->_ion_order_by) && isset($this->_ion_order)) {
$this->db->order_by($this->_ion_order_by, $this->_ion_order);
$this->_ion_order = NULL;
$this->_ion_order_by = NULL;
}
$this->response = $this->db->get($this->tables['login_users']);
return $this;
}
/**
* user
*
* @param int|string|null $id
*
* @return static
* @author Ben Edmunds
*/
public function user($id = NULL)
{
$this->trigger_events('user');
// if no id was passed use the current users id
$id = isset($id) ? $id : $this->session->userdata('user_id');
$this->limit(1);
$this->order_by($this->tables['login_users'] . '.id', 'desc');
$this->where($this->tables['login_users'] . '.id', $id);
$this->users();
return $this;
}
/**
* get_users_groups
*
* @param int|string|bool $id
*
* @return CI_DB_result
* @author Ben Edmunds
*/
public function get_users_groups($id = FALSE)
{
$this->trigger_events('get_users_group');
// if no id was passed use the current users id
$id || $id = $this->session->userdata('user_id');
return $this->db->select($this->tables['users_groups'] . '.' . $this->join['groups'] . ' as id, ' . $this->tables['groups'] . '.name, ' . $this->tables['groups'] . '.description')
->where($this->tables['users_groups'] . '.' . $this->join['login_users'], $id)
->join($this->tables['groups'], $this->tables['users_groups'] . '.' . $this->join['groups'] . '=' . $this->tables['groups'] . '.id')
->get($this->tables['users_groups']);
}
/**
* @param int|string|array $check_group group(s) to check
* @param int|string|bool $id user id
* @param bool $check_all check if all groups is present, or any of the groups
*
* @return bool Whether the/all user(s) with the given ID(s) is/are in the given group
* @author Phil Sturgeon
**/
public function in_group($check_group, $id = FALSE, $check_all = FALSE)
{
$this->trigger_events('in_group');
$id || $id = $this->session->userdata('user_id');
if (!is_array($check_group)) {
$check_group = [$check_group];
}
if (isset($this->_cache_user_in_group[$id])) {
$groups_array = $this->_cache_user_in_group[$id];
} else {
$users_groups = $this->get_users_groups($id)->result();
$groups_array = [];
foreach ($users_groups as $group) {
$groups_array[$group->id] = $group->name;
}
$this->_cache_user_in_group[$id] = $groups_array;
}
foreach ($check_group as $key => $value) {
$groups = (is_numeric($value)) ? array_keys($groups_array) : $groups_array;
/**
* if !all (default), in_array
* if all, !in_array
*/
if (in_array($value, $groups) xor $check_all) {
/**
* if !all (default), true
* if all, false
*/
return !$check_all;
}
}
/**
* if !all (default), false
* if all, true
*/
return $check_all;
}
/**
* add_to_group
*
* @param array|int|float|string $group_ids
* @param bool|int|float|string $user_id
*
* @return int
* @author Ben Edmunds
*/
public function add_to_group($group_ids, $user_id = FALSE)
{
$this->trigger_events('add_to_group');
// if no id was passed use the current users id
$user_id || $user_id = $this->session->userdata('user_id');
if (!is_array($group_ids)) {
$group_ids = [$group_ids];
}
$return = 0;
// Then insert each into the database
foreach ($group_ids as $group_id) {
// Cast to float to support bigint data type
if ($this->db->insert(
$this->tables['users_groups'],
[
$this->join['groups'] => (float)$group_id,
$this->join['login_users'] => (float)$user_id
]
)) {
if (isset($this->_cache_groups[$group_id])) {
$group_name = $this->_cache_groups[$group_id];
} else {
$group = $this->group($group_id)->result();
$group_name = $group[0]->name;
$this->_cache_groups[$group_id] = $group_name;
}
$this->_cache_user_in_group[$user_id][$group_id] = $group_name;
// Return the number of groups added
$return++;
}
}
return $return;
}
/**
* remove_from_group
*
* @param array|int|float|string|bool $group_ids
* @param int|float|string|bool $user_id
*
* @return bool
* @author Ben Edmunds
*/
public function remove_from_group($group_ids = FALSE, $user_id = FALSE)
{
$this->trigger_events('remove_from_group');
// user id is required
if (empty($user_id)) {
return FALSE;
}
// if group id(s) are passed remove user from the group(s)
if (!empty($group_ids)) {
if (!is_array($group_ids)) {
$group_ids = [$group_ids];
}
foreach ($group_ids as $group_id) {
// Cast to float to support bigint data type
$this->db->delete(
$this->tables['users_groups'],
[$this->join['groups'] => (float)$group_id, $this->join['login_users'] => (float)$user_id]
);
if (isset($this->_cache_user_in_group[$user_id]) && isset($this->_cache_user_in_group[$user_id][$group_id])) {
unset($this->_cache_user_in_group[$user_id][$group_id]);
}
}
$return = TRUE;
}
// otherwise remove user from all groups
else {
// Cast to float to support bigint data type
if ($return = $this->db->delete($this->tables['users_groups'], [$this->join['login_users'] => (float)$user_id])) {
$this->_cache_user_in_group[$user_id] = [];
}
}
return $return;
}
/**
* groups
*
* @return static
* @author Ben Edmunds
*/
public function groups()
{
$this->trigger_events('groups');
// run each where that was passed
if (isset($this->_ion_where) && !empty($this->_ion_where)) {
foreach ($this->_ion_where as $where) {
$this->db->where($where);
}
$this->_ion_where = [];
}
if (isset($this->_ion_limit) && isset($this->_ion_offset)) {
$this->db->limit($this->_ion_limit, $this->_ion_offset);
$this->_ion_limit = NULL;
$this->_ion_offset = NULL;
} else if (isset($this->_ion_limit)) {
$this->db->limit($this->_ion_limit);
$this->_ion_limit = NULL;
}
// set the order
if (isset($this->_ion_order_by) && isset($this->_ion_order)) {
$this->db->order_by($this->_ion_order_by, $this->_ion_order);
}
$this->response = $this->db->get($this->tables['groups']);
return $this;
}
/**
* group
*
* @param int|string|null $id
*
* @return static
* @author Ben Edmunds
*/
public function group($id = NULL)
{
$this->trigger_events('group');
if (isset($id)) {
$this->where($this->tables['groups'] . '.id', $id);
}
$this->limit(1);
$this->order_by('id', 'desc');
return $this->groups();
}
/**
* update
*
* @param int|string $id
* @param array $data
*
* @return bool
* @author Phil Sturgeon
*/
public function update($id, array $data)
{
$this->trigger_events('pre_update_user');
$user = $this->user($id)->row();
$this->db->trans_begin();
if (array_key_exists($this->identity_column, $data) && $this->identity_check($data[$this->identity_column]) && $user->{$this->identity_column} !== $data[$this->identity_column]) {
$this->db->trans_rollback();
$this->set_error('account_creation_duplicate_identity');
$this->trigger_events(['post_update_user', 'post_update_user_unsuccessful']);
$this->set_error('update_unsuccessful');
return FALSE;
}
// Filter the data passed
$data = $this->_filter_data($this->tables['login_users'], $data);
if (array_key_exists($this->identity_column, $data) || array_key_exists('password', $data) || array_key_exists('email', $data)) {
if (array_key_exists('password', $data)) {
if (!empty($data['password'])) {
$data['password'] = $this->hash_password($data['password'], $user->{$this->identity_column});
if ($data['password'] === FALSE) {
$this->db->trans_rollback();
$this->trigger_events(['post_update_user', 'post_update_user_unsuccessful']);
$this->set_error('update_unsuccessful');
return FALSE;
}
} else {
// unset password so it doesn't effect database entry if no password passed
unset($data['password']);
}
}
}
$this->trigger_events('extra_where');
$this->db->update($this->tables['login_users'], $data, ['id' => $user->id]);
if ($this->db->trans_status() === FALSE) {
$this->db->trans_rollback();
$this->trigger_events(['post_update_user', 'post_update_user_unsuccessful']);
$this->set_error('update_unsuccessful');
return FALSE;
}
$this->db->trans_commit();
$this->trigger_events(['post_update_user', 'post_update_user_successful']);
$this->set_message('update_successful');
return TRUE;
}
/**
* delete_user
*
* @param int|string $id
*
* @return bool
* @author Phil Sturgeon
*/
public function delete_user($id)
{
$this->trigger_events('pre_delete_user');
$this->db->trans_begin();
// remove user from groups
$this->remove_from_group(NULL, $id);
// delete user from users table should be placed after remove from group
$this->db->delete($this->tables['login_users'], ['id' => $id]);
if ($this->db->trans_status() === FALSE) {
$this->db->trans_rollback();
$this->trigger_events(['post_delete_user', 'post_delete_user_unsuccessful']);
$this->set_error('delete_unsuccessful');
return FALSE;
}
$this->db->trans_commit();
$this->trigger_events(['post_delete_user', 'post_delete_user_successful']);
$this->set_message('delete_successful');
return TRUE;
}
/**
* update_last_login
*
* @param int|string $id
*
* @return bool
* @author Ben Edmunds
*/
public function update_last_login($id)
{
$this->trigger_events('update_last_login');
$this->load->helper('date');
$this->trigger_events('extra_where');
$this->db->update($this->tables['login_users'], ['last_login' => time()], ['id' => $id]);
return $this->db->affected_rows() == 1;
}
/**
* set_lang
*
* @param string $lang
*
* @return bool
* @author Ben Edmunds
*/
public function set_lang($lang = 'en')
{
$this->trigger_events('set_lang');
// if the user_expire is set to zero we'll set the expiration two years from now.
if ($this->config->item('user_expire', 'ion_auth') === 0) {
$expire = self::MAX_COOKIE_LIFETIME;
}
// otherwise use what is set
else {
$expire = $this->config->item('user_expire', 'ion_auth');
}
set_cookie([
'name' => 'lang_code',
'value' => $lang,
'expire' => $expire
]);
return TRUE;
}
/**
* set_session
*
* @param object $user
*
* @return bool
* @author jrmadsen67
*/
public function set_session($user)
{
$this->trigger_events('pre_set_session');
$session_data = [
'identity' => $user->{$this->identity_column},
$this->identity_column => $user->{$this->identity_column},
'email' => $user->email,
'user_id' => $user->id, //everyone likes to overwrite id so we'll use user_id
'old_last_login' => $user->last_login,
'last_check' => time(),
'ion_auth_session_hash' => $this->config->item('session_hash', 'ion_auth'),
];
$this->session->set_userdata($session_data);
$this->trigger_events('post_set_session');
return TRUE;
}
/**
* Set a user to be remembered
*
* Implemented as described in
* https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
*
* @param string $identity
*
* @return bool
* @author Ben Edmunds
*/
public function remember_user($identity)
{
$this->trigger_events('pre_remember_user');
if (!$identity) {
return FALSE;
}
// Generate random tokens
$token = $this->_generate_selector_validator_couple();
if ($token->validator_hashed) {
$this->db->update(
$this->tables['login_users'],
[
'remember_selector' => $token->selector,
'remember_code' => $token->validator_hashed
],
[$this->identity_column => $identity]
);
if ($this->db->affected_rows() > -1) {
// if the user_expire is set to zero we'll set the expiration two years from now.
if ($this->config->item('user_expire', 'ion_auth') === 0) {
$expire = self::MAX_COOKIE_LIFETIME;
}
// otherwise use what is set
else {
$expire = $this->config->item('user_expire', 'ion_auth');
}
set_cookie([
'name' => $this->config->item('remember_cookie_name', 'ion_auth'),
'value' => $token->user_code,
'expire' => $expire
]);
$this->trigger_events(['post_remember_user', 'remember_user_successful']);
return TRUE;
}
}
$this->trigger_events(['post_remember_user', 'remember_user_unsuccessful']);
return FALSE;
}
/**
* Login automatically a user with the "Remember me" feature
* Implemented as described in
* https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
*
* @return bool
* @author Ben Edmunds
*/
public function login_remembered_user()
{
$this->trigger_events('pre_login_remembered_user');
// Retrieve token from cookie
$remember_cookie = get_cookie($this->config->item('remember_cookie_name', 'ion_auth'));
$token = $this->_retrieve_selector_validator_couple($remember_cookie);
if ($token === FALSE) {
$this->trigger_events(['post_login_remembered_user', 'post_login_remembered_user_unsuccessful']);
return FALSE;
}
// get the user with the selector
$this->trigger_events('extra_where');
$query = $this->db->select($this->identity_column . ', id, email, remember_code, last_login')
->where('remember_selector', $token->selector)
->where('active', 1)
->limit(1)
->get($this->tables['login_users'])->row();
$query = empty($query) ? 0 : $query;
// Check that we got the user
if (!is_null($query) && count((array)$query) === 1) {
// Retrieve the information
$user = $query;
// Check the code against the validator
$identity = $user->{$this->identity_column};
if ($this->verify_password($token->validator, $user->remember_code, $identity)) {
$this->update_last_login($user->id);
$this->set_session($user);
$this->clear_forgotten_password_code($identity);
// extend the users cookies if the option is enabled
if ($this->config->item('user_extend_on_login', 'ion_auth')) {
$this->remember_user($identity);
}
// Regenerate the session (for security purpose: to avoid session fixation)
$this->session->sess_regenerate(FALSE);
$this->trigger_events(['post_login_remembered_user', 'post_login_remembered_user_successful']);
return TRUE;
}
}
delete_cookie($this->config->item('remember_cookie_name', 'ion_auth'));
$this->trigger_events(['post_login_remembered_user', 'post_login_remembered_user_unsuccessful']);
return FALSE;
}
/**
* create_group
*
* @param string|bool $group_name
* @param string $group_description
* @param array $additional_data
*
* @return int|bool The ID of the inserted group, or FALSE on failure
* @author aditya menon
*/
public function create_group($group_name = FALSE, $group_description = '', $additional_data = [])
{
// bail if the group name was not passed
if (!$group_name) {
$this->set_error('group_name_required');
return FALSE;
}
// bail if the group name already exists
$existing_group = $this->db->get_where($this->tables['groups'], ['name' => $group_name])->num_rows();
if ($existing_group !== 0) {
$this->set_error('group_already_exists');
return FALSE;
}
$data = ['name' => $group_name, 'description' => $group_description];
// filter out any data passed that doesnt have a matching column in the groups table
// and merge the set group data and the additional data
if (!empty($additional_data)) $data = array_merge($this->_filter_data($this->tables['groups'], $additional_data), $data);
$this->trigger_events('extra_group_set');
// insert the new group
$this->db->insert($this->tables['groups'], $data);
$group_id = $this->db->insert_id($this->tables['groups'] . '_id_seq');
// report success
$this->set_message('group_creation_successful');
// return the brand new group id
return $group_id;
}
/**
* update_group
*
* @param int|string|bool $group_id
* @param string|bool $group_name
* @param array $additional_data
*
* @return bool
* @author aditya menon
*/
public function update_group($group_id = FALSE, $group_name = FALSE, $additional_data = [])
{
if (empty($group_id)) {
return FALSE;
}
$data = [];
if (!empty($group_name)) {
// we are changing the name, so do some checks
// bail if the group name already exists
$existing_group = $this->db->get_where($this->tables['groups'], ['name' => $group_name])->row();
if (isset($existing_group->id) && $existing_group->id != $group_id) {
$this->set_error('group_already_exists');
return FALSE;
}
$data['name'] = $group_name;
}
// restrict change of name of the admin group
$group = $this->db->get_where($this->tables['groups'], ['id' => $group_id])->row();
if ($this->config->item('admin_group', 'ion_auth') === $group->name && $group_name !== $group->name) {
$this->set_error('group_name_admin_not_alter');
return FALSE;
}
// filter out any data passed that doesnt have a matching column in the groups table
// and merge the set group data and the additional data
if (!empty($additional_data)) {
$data = array_merge($this->_filter_data($this->tables['groups'], $additional_data), $data);
}
$this->db->update($this->tables['groups'], $data, ['id' => $group_id]);
$this->set_message('group_update_successful');
return TRUE;
}
/**
* delete_group
*
* @param int|string|bool $group_id
*
* @return bool
* @author aditya menon
*/
public function delete_group($group_id = FALSE)
{
// bail if mandatory param not set
if (!$group_id || empty($group_id)) {
return FALSE;
}
$group = $this->group($group_id)->row();
if ($group->name == $this->config->item('admin_group', 'ion_auth')) {
$this->trigger_events(['post_delete_group', 'post_delete_group_notallowed']);
$this->set_error('group_delete_notallowed');
return FALSE;
}
$this->trigger_events('pre_delete_group');
$this->db->trans_begin();
// remove all users from this group
$this->db->delete($this->tables['users_groups'], [$this->join['groups'] => $group_id]);
// remove the group itself
$this->db->delete($this->tables['groups'], ['id' => $group_id]);
if ($this->db->trans_status() === FALSE) {
$this->db->trans_rollback();
$this->trigger_events(['post_delete_group', 'post_delete_group_unsuccessful']);
$this->set_error('group_delete_unsuccessful');
return FALSE;
}
$this->db->trans_commit();
$this->trigger_events(['post_delete_group', 'post_delete_group_successful']);
$this->set_message('group_delete_successful');
return TRUE;
}
/**
* @param string $event
* @param string $name
* @param string $class
* @param string $method
* @param array $arguments
*/
public function set_hook($event, $name, $class, $method, $arguments)
{
$this->_ion_hooks->{$event}[$name] = new stdClass;
$this->_ion_hooks->{$event}[$name]->class = $class;
$this->_ion_hooks->{$event}[$name]->method = $method;
$this->_ion_hooks->{$event}[$name]->arguments = $arguments;
}
/**
* @param string $event
* @param string $name
*/
public function remove_hook($event, $name)
{
if (isset($this->_ion_hooks->{$event}[$name])) {
unset($this->_ion_hooks->{$event}[$name]);
}
}
/**
* @param string $event
*/
public function remove_hooks($event)
{
if (isset($this->_ion_hooks->$event)) {
unset($this->_ion_hooks->$event);
}
}
/**
* @param string $event
* @param string $name
*
* @return bool|mixed
*/
protected function _call_hook($event, $name)
{
if (isset($this->_ion_hooks->{$event}[$name]) && method_exists($this->_ion_hooks->{$event}[$name]->class, $this->_ion_hooks->{$event}[$name]->method)) {
$hook = $this->_ion_hooks->{$event}[$name];
return call_user_func_array([$hook->class, $hook->method], $hook->arguments);
}
return FALSE;
}
/**
* @param string|array $events
*/
public function trigger_events($events)
{
if (is_array($events) && !empty($events)) {
foreach ($events as $event) {
$this->trigger_events($event);
}
} else {
if (isset($this->_ion_hooks->$events) && !empty($this->_ion_hooks->$events)) {
foreach ($this->_ion_hooks->$events as $name => $hook) {
$this->_call_hook($events, $name);
}
}
}
}
/**
* set_message_delimiters
*
* Set the message delimiters
*
* @param string $start_delimiter
* @param string $end_delimiter
*
* @return true
* @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_error_delimiters
*
* Set the error delimiters
*
* @param string $start_delimiter
* @param string $end_delimiter
*
* @return true
* @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
*
* @param string $message The message
*
* @return string The given message
* @author Ben Edmunds
*/
public function set_message($message)
{
$this->messages[] = $message;
return $message;
}
/**
* messages
*
* Get the messages
*
* @return string
* @author Ben Edmunds
*/
public function messages()
{
$_output = '';
foreach ($this->messages as $message) {
$messageLang = $this->lang->line($message) ? $this->lang->line($message) : '##' . $message . '##';
$_output .= $this->message_start_delimiter . $messageLang . $this->message_end_delimiter;
}
return $_output;
}
/**
* messages as array
*
* Get the messages as an array
*
* @param bool $langify
*
* @return array
* @author Raul Baldner Junior
*/
public function messages_array($langify = TRUE)
{
if ($langify) {
$_output = [];
foreach ($this->messages as $message) {
$messageLang = $this->lang->line($message) ? $this->lang->line($message) : '##' . $message . '##';
$_output[] = $this->message_start_delimiter . $messageLang . $this->message_end_delimiter;
}
return $_output;
} else {
return $this->messages;
}
}
/**
* clear_messages
*
* Clear messages
*
* @return true
* @author Ben Edmunds
*/
public function clear_messages()
{
$this->messages = [];
return TRUE;
}
/**
* set_error
*
* Set an error message
*
* @param string $error The error to set
*
* @return string The given error
* @author Ben Edmunds
*/
public function set_error($error)
{
$this->errors[] = $error;
return $error;
}
/**
* errors
*
* Get the error message
*
* @return string
* @author Ben Edmunds
*/
public function errors()
{
$_output = '';
foreach ($this->errors as $error) {
$errorLang = $this->lang->line($error) ? $this->lang->line($error) : '##' . $error . '##';
$_output .= $this->error_start_delimiter . $errorLang . $this->error_end_delimiter;
}
return $_output;
}
/**
* errors as array
*
* Get the error messages as an array
*
* @param bool $langify
*
* @return array
* @author Raul Baldner Junior
*/
public function errors_array($langify = TRUE)
{
if ($langify) {
$_output = [];
foreach ($this->errors as $error) {
$errorLang = $this->lang->line($error) ? $this->lang->line($error) : '##' . $error . '##';
$_output[] = $this->error_start_delimiter . $errorLang . $this->error_end_delimiter;
}
return $_output;
} else {
return $this->errors;
}
}
/**
* clear_errors
*
* Clear Errors
*
* @return true
* @author Ben Edmunds
*/
public function clear_errors()
{
$this->errors = [];
return TRUE;
}
/**
* Internal function to set a password in the database
*
* @param string $identity
* @param string $password
*
* @return bool
*/
protected function _set_password_db($identity, $password)
{
$hash = $this->hash_password($password, $identity);
if ($hash === FALSE) {
return FALSE;
}
// When setting a new password, invalidate any other token
$data = [
'password' => $hash,
'remember_code' => NULL,
'forgotten_password_code' => NULL,
'forgotten_password_time' => NULL
];
$this->trigger_events('extra_where');
$this->db->update($this->tables['login_users'], $data, [$this->identity_column => $identity]);
return $this->db->affected_rows() == 1;
}
/**
* @param string $table
* @param array $data
*
* @return array
*/
protected function _filter_data($table, $data)
{
$filtered_data = [];
$columns = $this->db->list_fields($table);
if (is_array($data)) {
foreach ($columns as $column) {
if (array_key_exists($column, $data))
$filtered_data[$column] = $data[$column];
}
}
return $filtered_data;
}
/** Generate a random token
* Inspired from http://php.net/manual/en/function.random-bytes.php#118932
*
* @param int $result_length
* @return string
*/
protected function _random_token($result_length = 32)
{
if (!isset($result_length) || intval($result_length) <= 8) {
$result_length = 32;
}
// Try random_bytes: PHP 7
if (function_exists('random_bytes')) {
return bin2hex(random_bytes($result_length / 2));
}
// Try mcrypt
if (function_exists('mcrypt_create_iv')) {
return bin2hex(mcrypt_create_iv($result_length / 2, MCRYPT_DEV_URANDOM));
}
// Try openssl
if (function_exists('openssl_random_pseudo_bytes')) {
return bin2hex(openssl_random_pseudo_bytes($result_length / 2));
}
// No luck!
return FALSE;
}
/** Retrieve hash parameter according to options
*
* @param string $identity
*
* @return array|bool
*/
protected function _get_hash_parameters($identity = NULL)
{
// Check if user is administrator or not
$is_admin = FALSE;
if ($identity) {
$user_id = $this->get_user_id_from_identity($identity);
if ($user_id && $this->in_group($this->config->item('admin_group', 'ion_auth'), $user_id)) {
$is_admin = TRUE;
}
}
$params = FALSE;
switch ($this->hash_method) {
case 'bcrypt':
$params = [
'cost' => $is_admin ? $this->config->item('bcrypt_admin_cost', 'ion_auth')
: $this->config->item('bcrypt_default_cost', 'ion_auth')
];
break;
case 'argon2':
$params = $is_admin ? $this->config->item('argon2_admin_params', 'ion_auth')
: $this->config->item('argon2_default_params', 'ion_auth');
break;
default:
// Do nothing
}
return $params;
}
/** Retrieve hash algorithm according to options
*
* @return string|bool
*/
protected function _get_hash_algo()
{
$algo = FALSE;
switch ($this->hash_method) {
case 'bcrypt':
$algo = PASSWORD_BCRYPT;
break;
case 'argon2':
$algo = PASSWORD_ARGON2I;
break;
default:
// Do nothing
}
return $algo;
}
/**
* Generate a random selector/validator couple
* This is a user code
*
* @param $selector_size int size of the selector token
* @param $validator_size int size of the validator token
*
* @return object
* ->selector simple token to retrieve the user (to store in DB)
* ->validator_hashed token (hashed) to validate the user (to store in DB)
* ->user_code code to be used user-side (in cookie or URL)
*/
protected function _generate_selector_validator_couple($selector_size = 40, $validator_size = 128)
{
// The selector is a simple token to retrieve the user
$selector = $this->_random_token($selector_size);
// The validator will strictly validate the user and should be more complex
$validator = $this->_random_token($validator_size);
// The validator is hashed for storing in DB (avoid session stealing in case of DB leaked)
$validator_hashed = $this->hash_password($validator);
// The code to be used user-side
$user_code = "$selector.$validator";
return (object) [
'selector' => $selector,
'validator_hashed' => $validator_hashed,
'user_code' => $user_code,
];
}
/**
* Retrieve remember cookie info
*
* @param $user_code string A user code of the form "selector.validator"
*
* @return object
* ->selector simple token to retrieve the user in DB
* ->validator token to validate the user (check against hashed value in DB)
*/
protected function _retrieve_selector_validator_couple($user_code)
{
// Check code
if ($user_code) {
$tokens = explode('.', $user_code);
// Check tokens
if (count($tokens) === 2) {
return (object) [
'selector' => $tokens[0],
'validator' => $tokens[1]
];
}
}
return FALSE;
}
/**
* Handle legacy sha1 password
*
* We expect the configuration to still have:
* store_salt
* salt_length
*
* @TODO to be removed in later version
*
* @param string $identity
* @param string $password
* @param string $hashed_password_db
*
* @return bool
**/
protected function _password_verify_sha1_legacy($identity, $password, $hashed_password_db)
{
$this->trigger_events('pre_sha1_password_migration');
if ($this->config->item('store_salt', 'ion_auth')) {
// Salt is store at the side, retrieve it
$query = $this->db->select('salt')
->where($this->identity_column, $identity)
->limit(1)
->get($this->tables['login_users']);
$salt_db = $query->row();
if ($query->num_rows() !== 1) {
$this->trigger_events(['post_sha1_password_migration', 'post_sha1_password_migration_unsuccessful']);
return FALSE;
}
$hashed_password = sha1($password . $salt_db->salt);
} else {
// Salt is stored along with password
$salt_length = $this->config->item('salt_length', 'ion_auth');
if (!$salt_length) {
$this->trigger_events(['post_sha1_password_migration', 'post_sha1_password_migration_unsuccessful']);
return FALSE;
}
$salt = substr($hashed_password_db, 0, $salt_length);
$hashed_password = $salt . substr(sha1($salt . $password), 0, -$salt_length);
}
// Now we can compare them
if ($hashed_password === $hashed_password_db) {
// Password is good, migrate it to latest
$result = $this->_set_password_db($identity, $password);
if ($result) {
$this->trigger_events(['post_sha1_password_migration', 'post_sha1_password_migration_successful']);
} else {
$this->trigger_events(['post_sha1_password_migration', 'post_sha1_password_migration_unsuccessful']);
}
return $result;
} else {
// Password mismatch, we cannot migrate...
$this->trigger_events(['post_sha1_password_migration', 'post_sha1_password_migration_unsuccessful']);
return FALSE;
}
}
}