diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 59f2eb3..95d9188 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,7 +22,7 @@ variables: # Configure mysql environment variables (https://hub.docker.com/r/_/mysql/) MYSQL_DATABASE: aauth_v3_ci4_testing MYSQL_ROOT_PASSWORD: root - CODEIGNITER_VERSION: '4.0.0-alpha.3' + CODEIGNITER_VERSION: '4.0.0-alpha.4' # Run our tests # If Xdebug was installed you can generate a coverage report and see code coverage metrics. diff --git a/application/Libraries/Aauth.php b/application/Libraries/Aauth.php index e60acc7..d2d9cbe 100644 --- a/application/Libraries/Aauth.php +++ b/application/Libraries/Aauth.php @@ -110,149 +110,441 @@ class Aauth $this->session = $session; } - //-------------------------------------------------------------------- - // User Functions - //-------------------------------------------------------------------- + //-------------------------------------------------------------------------- + // Login Functions + //-------------------------------------------------------------------------- /** - * Create user + * Login user * - * Creates a new user + * Check provided details against the database. Add items to error array on fail * - * @param string $email User's email address - * @param string $password User's password - * @param string|boolean $username User's username + * @param string $identifier Identifier + * @param string $password Password + * @param boolean $remember Whether to remember login + * @param string $totpCode TOTP Code * - * @return integer|boolean + * @return boolean */ - public function createUser(string $email, string $password, string $username = null) + public function login(string $identifier, string $password, bool $remember = null, string $totpCode = null) { - $userModel = new UserModel(); - - $data['email'] = $email; - $data['password'] = $password; + helper('cookie'); + delete_cookie('remember'); - if (! is_null($username)) - { - $data['username'] = $username; - } + $userModel = new UserModel(); + $loginAttemptModel = new LoginAttemptModel(); + $userVariableModel = new UserVariableModel(); - if (! $userId = $userModel->insert($data)) + if ($this->config->loginProtection && ! $loginAttemptModel->save()) { - $this->error(array_values($userModel->errors())); + $this->error(lang('Aauth.loginAttemptsExceeded')); return false; } - if ($this->config->userVerification) + // if ($this->config->ddos_protection && $this->config->recaptcha_active && $loginAttempts->get() > $this->config->recaptcha_login_attempts){ + // $this->CI->load->helper('recaptchalib'); + // $reCaptcha = new ReCaptcha( $this->config->recaptcha_secret); + // $resp = $reCaptcha->verifyResponse( $this->CI->input->server("REMOTE_ADDR"), $this->CI->input->post("g-recaptcha-response") ); + // if( ! $resp->success){ + // $this->error(lang('Aauth.aauth_error_recaptcha_not_correct')); + // return false; + // } + // } + + if ($this->config->loginUseUsername) { - $this->sendVerification($userId, $email); - $this->info(lang('Aauth.infoCreateVerification')); + if (! $identifier || strlen($password) < $this->config->passwordMin || strlen($password) > $this->config->passwordMax) + { + $this->error(lang('Aauth.loginFailedUsername')); - return $userId; + return false; + } + + if (! $user = $userModel->where('username', $identifier)->first()) + { + $this->error(lang('Aauth.notFoundUser')); + + return false; + } } + else + { + $validation = \Config\Services::validation(); - $this->info(lang('Aauth.infoCreateSuccess')); + if (! $validation->check($identifier, 'valid_email') || strlen($password) < $this->config->passwordMin || strlen($password) > $this->config->passwordMax) + { + $this->error(lang('Aauth.loginFailedEmail')); - return $userId; - } + return false; + } - /** - * Update user - * - * Updates existing user details - * - * @param integer $userId User id to update - * @param string|boolean $email User's email address, or FALSE if not to be updated - * @param string|boolean $password User's password, or FALSE if not to be updated - * @param string|boolean $username User's name, or FALSE if not to be updated - * - * @return boolean - */ - public function updateUser(int $userId, $email = null, string $password = null, string $username = null) - { - $userModel = new UserModel(); + if (! $user = $userModel->where('email', $identifier)->first()) + { + $this->error(lang('Aauth.notFoundUser')); - if (! $userModel->existsById($userId)) - { - $this->error(lang('Aauth.notFoundUser')); + return false; + } + } + if (! empty($userVariableModel->find($user['id'], 'verification_code', true))) + { + $this->error(lang('Aauth.notVerified')); return false; } - else if (is_null($email) && is_null($password) && is_null($username)) + else if ($user['banned']) { + $this->error(lang('Aauth.invalidUserBanned')); return false; } - $data['id'] = $userId; + // if ($this->config->totpEnabled && ! $this->config->totpOnIpChange && $this->config->totpLogin) + // { + // if ($this->config->totpLogin == true) + // { + // $this->session->set('totp_required', true); + // } - if (! is_null($email)) - { - $data['email'] = $email; - } + // $totp_secret = $userVariableModel->find($user['id'], 'totp_secret', true); + // if ( ! empty($totp_secret) && ! $totp_code) { + // $this->error(lang('Aauth.requiredTOTPCode')); + // return false; + // } else { + // if( ! empty($totp_secret)){ + // $this->CI->load->helper('googleauthenticator'); + // $ga = new PHPGangsta_GoogleAuthenticator(); + // $checkResult = $ga->verifyCode($totp_secret, $totp_code, 0); + // if ( ! $checkResult) { + // $this->error(lang('Aauth.invalidTOTPCode')); + // return false; + // } + // } + // } + // } + // else if ($this->config->totpEnabled && $this->config->totpOnIpChange) + // { + // $query = null; + // $query = $this->aauth_db->where($db_identifier, $identifier); + // $query = $this->aauth_db->get($this->config->users); + // $totp_secret = $query->row()->totp_secret; + // $ip_address = $query->row()->ip_address; + // $current_ip_address = $this->CI->input->ip_address(); + // if ($query->num_rows() > 0 AND !$totp_code) { + // if($ip_address != $current_ip_address ){ + // if($this->config->totpLogin == false){ + // $this->error(lang('Aauth.aauth_error_totp_code_required')); + // return false; + // } else if($this->config->totpLogin == true){ + // $this->session->set('totp_required', true); + // } + // } + // }else { + // if(!empty($totp_secret)){ + // if($ip_address != $current_ip_address ){ + // $this->CI->load->helper('googleauthenticator'); + // $ga = new PHPGangsta_GoogleAuthenticator(); + // $checkResult = $ga->verifyCode($totp_secret, $totp_code, 0); + // if (!$checkResult) { + // $this->error(lang('Aauth.aauth_error_totp_code_invalid')); + // return false; + // } + // } + // } + // } + // } - if (! is_null($password)) + if (password_verify($password, $user['password'])) { - $data['password'] = $password; - } + $data['id'] = $user['id']; + $data['username'] = $user['username']; + $data['email'] = $user['email']; + $data['loggedIn'] = true; + $this->session->set('user', $data); - if (! is_null($username)) - { - $data['username'] = $username; - } + if ($remember) + { + helper('text'); + $loginTokenModel = new LoginTokenModel(); + $expire = $this->config->loginRemember; + $userId = base64_encode($user['id']); + $randomString = random_string('alnum', 32); + $selectorString = random_string('alnum', 16); - if ($userModel->update($userId, $data)) - { - $this->info(lang('Aauth.infoUpdateSuccess')); + $cookieData['name'] = 'remember'; + $cookieData['value'] = $userId . ';' . $randomString . ';' . $selectorString; + $cookieData['expire'] = YEAR; + + $tokenData['user_id'] = $user['id']; + $tokenData['random_hash'] = password_hash($randomString, PASSWORD_DEFAULT); + $tokenData['selector_hash'] = password_hash($selectorString, PASSWORD_DEFAULT); + $tokenData['expires_at'] = date('Y-m-d H:i:s', strtotime($expire)); + + set_cookie($cookieData); + $loginTokenModel->insert($tokenData); + } + + $userModel->updateLastLogin($user['id']); + + if ($this->config->loginAttemptRemoveSuccessful) + { + $loginAttemptModel->delete(); + } return true; } + else + { + $this->error(lang('Aauth.loginFailedAll')); - $this->error(array_values($userModel->errors())); - - return false; + return false; + } } /** - * Delete user + * Logout * - * @param integer $userId User id to delete + * Deletes session and cookie * - * @return boolen + * @return void */ - public function deleteUser(int $userId) + public function logout() { - $userModel = new UserModel(); - - if (! $userModel->existsById($userId)) - { - $this->error(lang('Aauth.notFoundUser')); - - return false; - } - - return $userModel->delete($userId); + helper('cookie'); + set_cookie('remember', '', -3600); + $this->session->remove('user'); + @$this->session->destroy(); } /** - * List users + * Fast login * - * Return users as an object array + * Login with just a user id * - * @param integer $limit Limit of users to be returned - * @param integer $offset Offset for limited number of users - * @param boolean $includeBanneds Include banned users - * @param string $orderBy Order by MYSQL string (e.g. 'name ASC', 'email DESC') + * @param integer $userId User id * - * @return array Array of users + * @return boolean */ - public function listUsers(int $limit = 0, int $offset = 0, bool $includeBanneds = null, string $orderBy = null) + protected function loginFast(int $userId) { $userModel = new UserModel(); - $user = $userModel->limit($limit, $offset); + $userModel->select('id, email, username'); + $userModel->where('id', $userId); + $userModel->where('banned', 0); - $userModel->select('id, email, username, banned, created_at, updated_at, last_activity, last_ip_address, last_login'); - // eanbool $group_par = null, + if ($user = $userModel->get()->getFirstRow()) + { + $this->session->set('user', [ + 'id' => $user->id, + 'username' => $user->username, + 'email' => $user->email, + 'loggedIn' => true, + ]); + + return true; + } + + return false; + } + + //-------------------------------------------------------------------------- + // Access Functions + //-------------------------------------------------------------------------- + + /** + * Check user login + * + * Checks if user logged in, also checks remember. + * + * @return boolean + */ + public function isLoggedIn() + { + helper('cookie'); + + if (isset($this->session->get('user')['loggedIn'])) + { + return true; + } + else if ($cookie = get_cookie('remember')) + { + $cookie = explode(';', $cookie); + $cookie[0] = base64_decode($cookie[0]); + + if (! is_numeric($cookie[0]) || strlen($cookie[1]) !== 32 || strlen($cookie[2]) !== 16) + { + return false; + } + else + { + $loginTokenModel = new LoginTokenModel(); + $loginTokens = $loginTokenModel->findAllByUserId($cookie[0]); + + foreach ($loginTokens as $loginToken) + { + if (password_verify($cookie[1], $loginToken['random_hash']) && password_verify($cookie[2], $loginToken['selector_hash'])) + { + if (strtotime($loginToken['expires_at']) > strtotime('now')) + { + $loginTokenModel->update($loginToken['id']); + + return $this->loginFast($loginToken['user_id']); + } + else + { + $loginTokenModel->deleteExpired($cookie[0]); + delete_cookie('remember'); + } + } + } + } + } + + return false; + } + + //-------------------------------------------------------------------- + // User Functions + //-------------------------------------------------------------------- + + /** + * Create user + * + * Creates a new user + * + * @param string $email User's email address + * @param string $password User's password + * @param string|boolean $username User's username + * + * @return integer|boolean + */ + public function createUser(string $email, string $password, string $username = null) + { + $userModel = new UserModel(); + + $data['email'] = $email; + $data['password'] = $password; + + if (! is_null($username)) + { + $data['username'] = $username; + } + + if (! $userId = $userModel->insert($data)) + { + $this->error(array_values($userModel->errors())); + + return false; + } + + if ($this->config->userVerification) + { + $this->sendVerification($userId, $email); + $this->info(lang('Aauth.infoCreateVerification')); + + return $userId; + } + + $this->info(lang('Aauth.infoCreateSuccess')); + + return $userId; + } + + /** + * Update user + * + * Updates existing user details + * + * @param integer $userId User id to update + * @param string|boolean $email User's email address, or FALSE if not to be updated + * @param string|boolean $password User's password, or FALSE if not to be updated + * @param string|boolean $username User's name, or FALSE if not to be updated + * + * @return boolean + */ + public function updateUser(int $userId, $email = null, string $password = null, string $username = null) + { + $userModel = new UserModel(); + + if (! $userModel->existsById($userId)) + { + $this->error(lang('Aauth.notFoundUser')); + + return false; + } + else if (is_null($email) && is_null($password) && is_null($username)) + { + return false; + } + + $data['id'] = $userId; + + if (! is_null($email)) + { + $data['email'] = $email; + } + + if (! is_null($password)) + { + $data['password'] = $password; + } + + if (! is_null($username)) + { + $data['username'] = $username; + } + + if ($userModel->update($userId, $data)) + { + $this->info(lang('Aauth.infoUpdateSuccess')); + + return true; + } + + $this->error(array_values($userModel->errors())); + + return false; + } + + /** + * Delete user + * + * @param integer $userId User id to delete + * + * @return boolen + */ + public function deleteUser(int $userId) + { + $userModel = new UserModel(); + + if (! $userModel->existsById($userId)) + { + $this->error(lang('Aauth.notFoundUser')); + + return false; + } + + return $userModel->delete($userId); + } + + /** + * List users + * + * Return users as an object array + * + * @param integer $limit Limit of users to be returned + * @param integer $offset Offset for limited number of users + * @param boolean $includeBanneds Include banned users + * @param string $orderBy Order by MYSQL string (e.g. 'name ASC', 'email DESC') + * + * @return array Array of users + */ + public function listUsers(int $limit = 0, int $offset = 0, bool $includeBanneds = null, string $orderBy = null) + { + $userModel = new UserModel(); + $user = $userModel->limit($limit, $offset); + + $userModel->select('id, email, username, banned, created_at, updated_at, last_activity, last_ip_address, last_login'); + // eanbool $group_par = null, if (is_null($includeBanneds)) { @@ -297,9 +589,9 @@ class Aauth } return [ - 'users' => $userModel->paginate($limit), - 'pager' => $userModel->pager, - ]; + 'users' => $userModel->paginate($limit), + 'pager' => $userModel->pager, + ]; } /** @@ -410,7 +702,7 @@ class Aauth * * Get user id from email address, if par. not given, return current user's id * - * @param string|boolean $email Email address for user + * @param string|boolean $email Email address for user, * * @return object|boolean User information or false if user not found */ @@ -438,7 +730,7 @@ class Aauth /** * Is banned * - * @param integer $userId User id + * @param integer $userId User id, can be null to use session user * * @return boolean */ @@ -462,7 +754,7 @@ class Aauth /** * Ban User * - * @param integer $userId User id + * @param integer $userId User id, can be null to use session user * * @return boolean */ @@ -488,7 +780,7 @@ class Aauth /** * Unban User * - * @param integer $userId User id + * @param integer $userId User id, can be null to use session user * * @return boolean */ @@ -557,6 +849,7 @@ class Aauth return false; } + /** * Reset password * @@ -624,298 +917,147 @@ class Aauth return false; } - //-------------------------------------------------------------------------- - // Login Functions - //-------------------------------------------------------------------------- - /** - * Login user + * Set User Variable as key value + * if variable not set before, it will ve set + * if set, overwrites the value * - * Check provided details against the database. Add items to error array on fail - * - * @param string $identifier Identifier - * @param string $password Password - * @param boolean $remember Whether to remember login - * @param string $totpCode TOTP Code + * @param string $key + * @param string $value + * @param integer $userId User id, can be null to use session user * * @return boolean */ - public function login(string $identifier, string $password, bool $remember = null, string $totpCode = null) + public function setUserVar(string $key, string $value, int $userId = null) { - helper('cookie'); - delete_cookie('remember'); - - $userModel = new UserModel(); - $loginAttemptModel = new LoginAttemptModel(); - $userVariableModel = new UserVariableModel(); - - if ($this->config->loginProtection && ! $loginAttemptModel->save()) + if (! $userId) { - $this->error(lang('Aauth.loginAttemptsExceeded')); - - return false; + $userId = $this->session->user['id']; } - // if ($this->config->ddos_protection && $this->config->recaptcha_active && $loginAttempts->get() > $this->config->recaptcha_login_attempts){ - // $this->CI->load->helper('recaptchalib'); - // $reCaptcha = new ReCaptcha( $this->config->recaptcha_secret); - // $resp = $reCaptcha->verifyResponse( $this->CI->input->server("REMOTE_ADDR"), $this->CI->input->post("g-recaptcha-response") ); - // if( ! $resp->success){ - // $this->error(lang('Aauth.aauth_error_recaptcha_not_correct')); - // return false; - // } - // } + $userModel = new UserModel(); - if ($this->config->loginUseUsername) + if (! $userModel->existsById($userId)) { - if (! $identifier || strlen($password) < $this->config->passwordMin || strlen($password) > $this->config->passwordMax) - { - $this->error(lang('Aauth.loginFailedUsername')); - - return false; - } - - if (! $user = $userModel->where('username', $identifier)->first()) - { - $this->error(lang('Aauth.notFoundUser')); - - return false; - } + return false; } - else - { - $validation = \Config\Services::validation(); - - if (! $validation->check($identifier, 'valid_email') || strlen($password) < $this->config->passwordMin || strlen($password) > $this->config->passwordMax) - { - $this->error(lang('Aauth.loginFailedEmail')); - - return false; - } - if (! $user = $userModel->where('email', $identifier)->first()) - { - $this->error(lang('Aauth.notFoundUser')); + $userVariableModel = new UserVariableModel(); - return false; - } - } + return $userVariableModel->save($userId, $key, $value); + } - if (! empty($userVariableModel->find($user['id'], 'verification_code', true))) + /** + * Unset User Variable as key value + * + * @param string $key + * @param integer $userId User id, can be null to use session user + * + * @return boolean + */ + public function unsetUserVar(string $key, int $userId = null) + { + if (! $userId) { - $this->error(lang('Aauth.notVerified')); - return false; + $userId = $this->session->user['id']; } - else if ($user['banned']) + + $userModel = new UserModel(); + + if (! $userModel->existsById($userId)) { - $this->error(lang('Aauth.invalidUserBanned')); return false; } - // if ($this->config->totpEnabled && ! $this->config->totpOnIpChange && $this->config->totpLogin) - // { - // if ($this->config->totpLogin == true) - // { - // $this->session->set('totp_required', true); - // } + $userVariableModel = new UserVariableModel(); - // $totp_secret = $userVariableModel->find($user['id'], 'totp_secret', true); - // if ( ! empty($totp_secret) && ! $totp_code) { - // $this->error(lang('Aauth.requiredTOTPCode')); - // return false; - // } else { - // if( ! empty($totp_secret)){ - // $this->CI->load->helper('googleauthenticator'); - // $ga = new PHPGangsta_GoogleAuthenticator(); - // $checkResult = $ga->verifyCode($totp_secret, $totp_code, 0); - // if ( ! $checkResult) { - // $this->error(lang('Aauth.invalidTOTPCode')); - // return false; - // } - // } - // } - // } - // else if ($this->config->totpEnabled && $this->config->totpOnIpChange) - // { - // $query = null; - // $query = $this->aauth_db->where($db_identifier, $identifier); - // $query = $this->aauth_db->get($this->config->users); - // $totp_secret = $query->row()->totp_secret; - // $ip_address = $query->row()->ip_address; - // $current_ip_address = $this->CI->input->ip_address(); - // if ($query->num_rows() > 0 AND !$totp_code) { - // if($ip_address != $current_ip_address ){ - // if($this->config->totpLogin == false){ - // $this->error(lang('Aauth.aauth_error_totp_code_required')); - // return false; - // } else if($this->config->totpLogin == true){ - // $this->session->set('totp_required', true); - // } - // } - // }else { - // if(!empty($totp_secret)){ - // if($ip_address != $current_ip_address ){ - // $this->CI->load->helper('googleauthenticator'); - // $ga = new PHPGangsta_GoogleAuthenticator(); - // $checkResult = $ga->verifyCode($totp_secret, $totp_code, 0); - // if (!$checkResult) { - // $this->error(lang('Aauth.aauth_error_totp_code_invalid')); - // return false; - // } - // } - // } - // } - // } + return $userVariableModel->delete($userId, $key); + } - if (password_verify($password, $user['password'])) + /** + * Get User Variable by key + * + * @param string $key Variable Key + * @param integer $userId User id, can be null to use session user + * + * @return boolean|string FALSE if var is not set, the value of var if set + */ + public function getUserVar(string $key, int $userId = null) + { + if (! $userId) { - $data['id'] = $user['id']; - $data['username'] = $user['username']; - $data['email'] = $user['email']; - $data['loggedIn'] = true; - $this->session->set('user', $data); - - if ($remember) - { - helper('text'); - $loginTokenModel = new LoginTokenModel(); - $expire = $this->config->loginRemember; - $userId = base64_encode($user['id']); - $randomString = random_string('alnum', 32); - $selectorString = random_string('alnum', 16); - - $cookieData['name'] = 'remember'; - $cookieData['value'] = $userId . ';' . $randomString . ';' . $selectorString; - $cookieData['expire'] = YEAR; - - $tokenData['user_id'] = $user['id']; - $tokenData['random_hash'] = password_hash($randomString, PASSWORD_DEFAULT); - $tokenData['selector_hash'] = password_hash($selectorString, PASSWORD_DEFAULT); - $tokenData['expires_at'] = date('Y-m-d H:i:s', strtotime($expire)); + $userId = $this->session->user['id']; + } - set_cookie($cookieData); - $loginTokenModel->insert($tokenData); - } + $userModel = new UserModel(); - $userModel->updateLastLogin($user['id']); + if (! $userModel->existsById($userId)) + { + return false; + } - if ($this->config->loginAttemptRemoveSuccessful) - { - $loginAttemptModel->delete(); - } + $userVariableModel = new UserVariableModel(); - return true; - } - else + if ($variable = $userVariableModel->find($user['id'], 'verification_code', true)) { - $this->error(lang('Aauth.loginFailedAll')); - - return false; + return $variable; } - } - /** - * Logout - * - * Deletes session and cookie - * - * @return void - */ - public function logout() - { - helper('cookie'); - set_cookie('remember', '', -3600); - $this->session->remove('user'); - @$this->session->destroy(); + return false; } /** - * Fast login - * - * Login with just a user id + * Get User Variables by user id + * Return array with all user keys & variables * - * @param integer $userId User id - * - * @return boolean + * @param integer $user_id ; if not given current user + * @return boolean|array , FALSE if var is not set, the value of var if set */ - protected function loginFast(int $userId) + public function getUserVars(int $userId = null) { + if (! $userId) + { + $userId = $this->session->user['id']; + } + $userModel = new UserModel(); - $userModel->select('id, email, username'); - $userModel->where('id', $userId); - $userModel->where('banned', 0); - if ($user = $userModel->get()->getFirstRow()) + if (! $userModel->existsById($userId)) { - $this->session->set('user', [ - 'id' => $user->id, - 'username' => $user->username, - 'email' => $user->email, - 'loggedIn' => true, - ]); - - return true; + return false; } - return false; - } + $userVariableModel = new UserVariableModel(); - //-------------------------------------------------------------------------- - // Access Functions - //-------------------------------------------------------------------------- + return $userVariableModel->findAll(); + } /** - * Check user login - * - * Checks if user logged in, also checks remember. + * List User Variable Keys by UserID + * Return array of variable keys or FALSE * - * @return boolean + * @param integer $user_id ; if not given current user + * @return boolean|array */ - public function isLoggedIn() + public function list_user_var_keys($user_id = false) { - helper('cookie'); - - if (isset($this->session->get('user')['loggedIn'])) + if (! $userId) { - return true; + $userId = $this->session->user['id']; } - else if ($cookie = get_cookie('remember')) - { - $cookie = explode(';', $cookie); - $cookie[0] = base64_decode($cookie[0]); - if (! is_numeric($cookie[0]) || strlen($cookie[1]) !== 32 || strlen($cookie[2]) !== 16) - { - return false; - } - else - { - $loginTokenModel = new LoginTokenModel(); - $loginTokens = $loginTokenModel->findAllByUserId($cookie[0]); - - foreach ($loginTokens as $loginToken) - { - if (password_verify($cookie[1], $loginToken['random_hash']) && password_verify($cookie[2], $loginToken['selector_hash'])) - { - if (strtotime($loginToken['expires_at']) > strtotime('now')) - { - $loginTokenModel->update($loginToken['id']); + $userModel = new UserModel(); - return $this->loginFast($loginToken['user_id']); - } - else - { - $loginTokenModel->deleteExpired($cookie[0]); - delete_cookie('remember'); - } - } - } - } + if (! $userModel->existsById($userId)) + { + return false; } - return false; - } + $userVariableModel = new UserVariableModel(); + $userVariableModel->select('data_key as key'); + return $userVariableModel->findAll(); + } //-------------------------------------------------------------------------- // Error Functions //--------------------------------------------------------------------------