507 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
			
		
		
	
	
			507 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
| <?php
 | |
| /**
 | |
|  * PHP Server Monitor
 | |
|  * Monitor your servers and websites.
 | |
|  *
 | |
|  * This file is part of PHP Server Monitor.
 | |
|  * PHP Server Monitor is free software: you can redistribute it and/or modify
 | |
|  * it under the terms of the GNU General Public License as published by
 | |
|  * the Free Software Foundation, either version 3 of the License, or
 | |
|  * (at your option) any later version.
 | |
|  *
 | |
|  * PHP Server Monitor is distributed in the hope that it will be useful,
 | |
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
|  * GNU General Public License for more details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU General Public License
 | |
|  * along with PHP Server Monitor.  If not, see <http://www.gnu.org/licenses/>.
 | |
|  *
 | |
|  * @package     phpservermon
 | |
|  * @author      Panique <https://github.com/panique/php-login-advanced/>
 | |
|  * @author      Pepijn Over <pep@peplab.net>
 | |
|  * @copyright   Copyright (c) 2008-2015 Pepijn Over <pep@peplab.net>
 | |
|  * @license     http://www.gnu.org/licenses/gpl.txt GNU GPL v3
 | |
|  * @version     Release: @package_version@
 | |
|  * @link        http://www.phpservermonitor.org/
 | |
|  * @since       phpservermon 3.0.0
 | |
|  **/
 | |
| 
 | |
| namespace psm\Service;
 | |
| use Symfony\Component\HttpFoundation\Session\Session;
 | |
| use Symfony\Component\HttpFoundation\Session\SessionInterface;
 | |
| 
 | |
| /**
 | |
|  * This is a heavily modified version of the php-login-advanced project by Panique.
 | |
|  *
 | |
|  * It uses the Session classes from the Symfony HttpFoundation component.
 | |
|  *
 | |
|  * @author Panique
 | |
|  * @author Pepijn Over
 | |
|  * @link http://www.php-login.net
 | |
|  * @link https://github.com/panique/php-login-advanced/
 | |
|  * @license http://opensource.org/licenses/MIT MIT License
 | |
|  */
 | |
| class User {
 | |
| 
 | |
|     /**
 | |
| 	 * The database connection
 | |
|      * @var \PDO $db_connection
 | |
|      */
 | |
|     protected $db_connection = null;
 | |
| 
 | |
| 	/**
 | |
| 	 * Local cache of user data
 | |
| 	 * @var array $user_data
 | |
| 	 */
 | |
| 	protected $user_data = array();
 | |
| 
 | |
| 	/**
 | |
| 	 * Session object
 | |
| 	 * @var \Symfony\Component\HttpFoundation\Session\Session $session
 | |
| 	 */
 | |
| 	protected $session;
 | |
| 
 | |
|     /**
 | |
| 	 * Current user id
 | |
|      * @var int $user_id
 | |
|      */
 | |
|     protected $user_id;
 | |
| 
 | |
| 	/**
 | |
| 	 *Current user preferences
 | |
| 	 * @var array $user_preferences
 | |
| 	 */
 | |
| 	protected $user_preferences;
 | |
| 
 | |
| 	/**
 | |
| 	 * The user's login status
 | |
|      * @var boolean $user_is_logged_in
 | |
|      */
 | |
|     protected $user_is_logged_in = false;
 | |
| 
 | |
|     /**
 | |
|      * Open a new user service
 | |
| 	 *
 | |
| 	 * @param \psm\Service\Database $db
 | |
| 	 * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session if NULL, one will be created
 | |
|      */
 | |
|     public function __construct(Database $db, SessionInterface $session = null) {
 | |
| 		$this->db_connection = $db->pdo();
 | |
| 
 | |
| 		if(!psm_is_cli()) {
 | |
| 			if($session == null) {
 | |
| 				$session = new Session();
 | |
| 				$session->start();
 | |
| 			}
 | |
| 			$this->session = $session;
 | |
| 
 | |
| 			if((!defined('PSM_INSTALL') || !PSM_INSTALL)) {
 | |
| 				// check the possible login actions:
 | |
| 				// 1. login via session data (happens each time user opens a page on your php project AFTER he has successfully logged in via the login form)
 | |
| 				// 2. login via cookie
 | |
| 
 | |
| 				// if user has an active session on the server
 | |
| 				if(!$this->loginWithSessionData()) {
 | |
| 					$this->loginWithCookieData();
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get user by id, or get current user.
 | |
| 	 * @param int $user_id if null it will attempt current user id
 | |
| 	 * @param boolean $flush if TRUE it will query db regardless of whether we already have the data
 | |
|      * @return object|boolean FALSE if user not found, object otherwise
 | |
|      */
 | |
|     public function getUser($user_id = null, $flush = false) {
 | |
| 		if($user_id == null) {
 | |
| 			if(!$this->isUserLoggedIn()) {
 | |
| 				return false;
 | |
| 			} else {
 | |
| 				$user_id = $this->getUserId();
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if(!isset($this->user_data[$user_id]) || $flush) {
 | |
| 			$query_user = $this->db_connection->prepare('SELECT * FROM '.PSM_DB_PREFIX.'users WHERE user_id = :user_id');
 | |
| 			$query_user->bindValue(':user_id', $user_id, \PDO::PARAM_INT);
 | |
| 			$query_user->execute();
 | |
| 			// get result row (as an object)
 | |
| 			$this->user_data[$user_id] = $query_user->fetchObject();
 | |
| 		}
 | |
| 		return $this->user_data[$user_id];
 | |
| 	}
 | |
| 
 | |
|     /**
 | |
|      * Search into database for the user data of user_name specified as parameter
 | |
|      * @return object|boolean user data as an object if existing user
 | |
|      */
 | |
|     public function getUserByUsername($user_name) {
 | |
| 		// database query, getting all the info of the selected user
 | |
| 		$query_user = $this->db_connection->prepare('SELECT * FROM '.PSM_DB_PREFIX.'users WHERE user_name = :user_name');
 | |
| 		$query_user->bindValue(':user_name', $user_name, \PDO::PARAM_STR);
 | |
| 		$query_user->execute();
 | |
| 		// get result row (as an object)
 | |
| 		return $query_user->fetchObject();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Logs in with SESSION data.
 | |
| 	 *
 | |
| 	 * @return boolean
 | |
|      */
 | |
|     protected function loginWithSessionData() {
 | |
| 		if(!$this->session->has('user_id')) {
 | |
| 			return false;
 | |
| 		}
 | |
| 		$user = $this->getUser($this->session->get('user_id'));
 | |
| 
 | |
| 		if(!empty($user)) {
 | |
| 			$this->setUserLoggedIn($user->user_id);
 | |
| 			return true;
 | |
| 		} else {
 | |
| 			// user no longer exists in database
 | |
| 			// call logout to clean up session vars
 | |
| 			$this->doLogout();
 | |
| 			return false;
 | |
| 		}
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Logs in via the Cookie
 | |
|      * @return bool success state of cookie login
 | |
|      */
 | |
|     private function loginWithCookieData() {
 | |
|         if (isset($_COOKIE['rememberme'])) {
 | |
|             // extract data from the cookie
 | |
|             list ($user_id, $token, $hash) = explode(':', $_COOKIE['rememberme']);
 | |
|             // check cookie hash validity
 | |
|             if($hash == hash('sha256', $user_id . ':' . $token . PSM_LOGIN_COOKIE_SECRET_KEY) && !empty($token)) {
 | |
|                 // cookie looks good, try to select corresponding user
 | |
| 				// get real token from database (and all other data)
 | |
| 				$user = $this->getUser($user_id);
 | |
| 
 | |
| 				if(!empty($user) && $token === $user->rememberme_token) {
 | |
| 					$this->setUserLoggedIn($user->user_id, true);
 | |
| 
 | |
| 					// Cookie token usable only once
 | |
| 					$this->newRememberMeCookie();
 | |
| 					return true;
 | |
| 				}
 | |
|             }
 | |
| 			// call logout to remove invalid cookie
 | |
| 			$this->doLogout();
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Logs in with the data provided in $_POST, coming from the login form
 | |
|      * @param string $user_name
 | |
|      * @param string $user_password
 | |
|      * @param boolean $user_rememberme
 | |
| 	 * @return boolean
 | |
|      */
 | |
|     public function loginWithPostData($user_name, $user_password, $user_rememberme = false) {
 | |
| 		$user_name = trim($user_name);
 | |
| 		$user_password = trim($user_password);
 | |
| 
 | |
| 		if(empty($user_name) && empty($user_password)) {
 | |
| 			return false;
 | |
| 		}
 | |
| 		$user = $this->getUserByUsername($user_name);
 | |
| 
 | |
| 		// using PHP 5.5's password_verify() function to check if the provided passwords fits to the hash of that user's password
 | |
| 		if(!isset($user->user_id)) {
 | |
| 			password_verify($user_password, 'dummy_call_against_timing');
 | |
| 			return false;
 | |
| 		} else if(!password_verify($user_password, $user->password)) {
 | |
| 			return false;
 | |
| 		}
 | |
| 
 | |
| 		$this->setUserLoggedIn($user->user_id, true);
 | |
| 
 | |
| 		// if user has check the "remember me" checkbox, then generate token and write cookie
 | |
| 		if($user_rememberme) {
 | |
| 			$this->newRememberMeCookie();
 | |
| 		}
 | |
| 
 | |
| 		// recalculate the user's password hash
 | |
| 		// DELETE this if-block if you like, it only exists to recalculate users's hashes when you provide a cost factor,
 | |
| 		// by default the script will use a cost factor of 10 and never change it.
 | |
| 		// check if the have defined a cost factor in config/hashing.php
 | |
| 		if(defined('PSM_LOGIN_HASH_COST_FACTOR')) {
 | |
| 			// check if the hash needs to be rehashed
 | |
| 			if(password_needs_rehash($user->password, PASSWORD_DEFAULT, array('cost' => PSM_LOGIN_HASH_COST_FACTOR))) {
 | |
| 				$this->changePassword($user->user_id, $user_password);
 | |
| 			}
 | |
| 		}
 | |
| 		return true;
 | |
|     }
 | |
| 
 | |
| 	/**
 | |
| 	 * Set the user logged in
 | |
| 	 * @param int $user_id
 | |
| 	 * @param boolean $regenerate regenerate session id against session fixation?
 | |
| 	 */
 | |
| 	protected function setUserLoggedIn($user_id, $regenerate = false) {
 | |
| 		if($regenerate) {
 | |
| 			$this->session->invalidate();
 | |
| 		}
 | |
| 		$this->session->set('user_id', $user_id);
 | |
| 		$this->session->set('user_logged_in', 1);
 | |
| 
 | |
| 		// declare user id, set the login status to true
 | |
| 		$this->user_id = $user_id;
 | |
| 		$this->user_is_logged_in = true;
 | |
| 	}
 | |
| 
 | |
|     /**
 | |
|      * Create all data needed for remember me cookie connection on client and server side
 | |
|      */
 | |
|     protected function newRememberMeCookie() {
 | |
| 		// generate 64 char random string and store it in current user data
 | |
| 		$random_token_string = hash('sha256', mt_rand());
 | |
| 		$sth = $this->db_connection->prepare('UPDATE '.PSM_DB_PREFIX.'users SET rememberme_token = :user_rememberme_token WHERE user_id = :user_id');
 | |
| 		$sth->execute(array(':user_rememberme_token' => $random_token_string, ':user_id' => $this->getUserId()));
 | |
| 
 | |
| 		// generate cookie string that consists of userid, randomstring and combined hash of both
 | |
| 		$cookie_string_first_part = $this->getUserId() . ':' . $random_token_string;
 | |
| 		$cookie_string_hash = hash('sha256', $cookie_string_first_part . PSM_LOGIN_COOKIE_SECRET_KEY);
 | |
| 		$cookie_string = $cookie_string_first_part . ':' . $cookie_string_hash;
 | |
| 
 | |
| 		// set cookie
 | |
| 		setcookie('rememberme', $cookie_string, time() + PSM_LOGIN_COOKIE_RUNTIME, "/", PSM_LOGIN_COOKIE_DOMAIN);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Delete all data needed for remember me cookie connection on client and server side
 | |
|      */
 | |
|     protected function deleteRememberMeCookie() {
 | |
| 		// Reset rememberme token
 | |
| 		if($this->session->has('user_id')) {
 | |
| 			$sth = $this->db_connection->prepare('UPDATE '.PSM_DB_PREFIX.'users SET rememberme_token = NULL WHERE user_id = :user_id');
 | |
| 			$sth->execute(array(':user_id' => $this->session->get('user_id')));
 | |
| 		}
 | |
| 
 | |
|         // set the rememberme-cookie to ten years ago (3600sec * 365 days * 10).
 | |
|         // that's obivously the best practice to kill a cookie via php
 | |
|         // @see http://stackoverflow.com/a/686166/1114320
 | |
|         setcookie('rememberme', false, time() - (3600 * 3650), '/', PSM_LOGIN_COOKIE_DOMAIN);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Perform the logout, resetting the session
 | |
|      */
 | |
|     public function doLogout() {
 | |
|         $this->deleteRememberMeCookie();
 | |
| 
 | |
| 		$this->session->clear();
 | |
| 		$this->session->invalidate();
 | |
| 
 | |
|         $this->user_is_logged_in = false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Simply return the current state of the user's login
 | |
|      * @return bool user's login status
 | |
|      */
 | |
|     public function isUserLoggedIn() {
 | |
|         return $this->user_is_logged_in;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sets a random token into the database (that will verify the user when he/she comes back via the link
 | |
|      * in the email) and returns it
 | |
| 	 * @param int $user_id
 | |
| 	 * @return string|boolean FALSE on error, string otherwise
 | |
|      */
 | |
|     public function generatePasswordResetToken($user_id) {
 | |
|         $user_id = intval($user_id);
 | |
| 
 | |
|         if($user_id == 0) {
 | |
|             return false;
 | |
|         }
 | |
| 		// generate timestamp (to see when exactly the user (or an attacker) requested the password reset mail)
 | |
| 		$temporary_timestamp = time();
 | |
| 		// generate random hash for email password reset verification (40 char string)
 | |
| 		$user_password_reset_hash = sha1(uniqid(mt_rand(), true));
 | |
| 
 | |
| 		$query_update = $this->db_connection->prepare('UPDATE '.PSM_DB_PREFIX.'users SET password_reset_hash = :user_password_reset_hash,
 | |
| 													   password_reset_timestamp = :user_password_reset_timestamp
 | |
| 													   WHERE user_id = :user_id');
 | |
| 		$query_update->bindValue(':user_password_reset_hash', $user_password_reset_hash, \PDO::PARAM_STR);
 | |
| 		$query_update->bindValue(':user_password_reset_timestamp', $temporary_timestamp, \PDO::PARAM_INT);
 | |
| 		$query_update->bindValue(':user_id', $user_id, \PDO::PARAM_INT);
 | |
| 		$query_update->execute();
 | |
| 
 | |
| 		// check if exactly one row was successfully changed:
 | |
| 		if ($query_update->rowCount() == 1) {
 | |
| 			return $user_password_reset_hash;
 | |
| 		} else {
 | |
| 			return false;
 | |
| 		}
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Checks if the verification string in the account verification mail is valid and matches to the user.
 | |
| 	 *
 | |
| 	 * Please note it is valid for 1 hour.
 | |
| 	 * @param int $user_id
 | |
| 	 * @param string $token
 | |
| 	 * @return boolean
 | |
|      */
 | |
|     public function verifyPasswordResetToken($user_id, $token) {
 | |
|         $user_id = intval($user_id);
 | |
| 
 | |
|         if(empty($user_id) || empty($token)) {
 | |
|             return false;
 | |
|         }
 | |
| 		$user = $this->getUser($user_id);
 | |
| 
 | |
| 		if(isset($user->user_id) && $user->password_reset_hash == $token) {
 | |
| 			$runtime = (defined('PSM_LOGIN_RESET_RUNTIME')) ? PSM_LOGIN_RESET_RUNTIME : 3600;
 | |
| 			$timestamp_max_interval = time() - $runtime;
 | |
| 
 | |
| 			if($user->password_reset_timestamp > $timestamp_max_interval) {
 | |
| 				return true;
 | |
| 			}
 | |
| 		}
 | |
| 		return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Change the password of a user
 | |
| 	 * @param int $user_id
 | |
| 	 * @param string $password
 | |
| 	 * @return boolean TRUE on success, FALSE on failure
 | |
|      */
 | |
|     public function changePassword($user_id, $password) {
 | |
|         $user_id = intval($user_id);
 | |
| 
 | |
|         if(empty($user_id) || empty($password)) {
 | |
|             return false;
 | |
|         }
 | |
| 		// now it gets a little bit crazy: check if we have a constant PSM_LOGIN_HASH_COST_FACTOR defined (in src/includes/psmconfig.inc.php),
 | |
| 		// if so: put the value into $hash_cost_factor, if not, make $hash_cost_factor = null
 | |
| 		$hash_cost_factor = (defined('PSM_LOGIN_HASH_COST_FACTOR') ? PSM_LOGIN_HASH_COST_FACTOR : null);
 | |
| 
 | |
| 		// crypt the user's password with the PHP 5.5's password_hash() function, results in a 60 character hash string
 | |
| 		// the PASSWORD_DEFAULT constant is defined by the PHP 5.5, or if you are using PHP 5.3/5.4, by the password hashing
 | |
| 		// compatibility library. the third parameter looks a little bit shitty, but that's how those PHP 5.5 functions
 | |
| 		// want the parameter: as an array with, currently only used with 'cost' => XX.
 | |
| 		$user_password_hash = password_hash($password, PASSWORD_DEFAULT, array('cost' => $hash_cost_factor));
 | |
| 
 | |
| 		// write users new hash into database
 | |
| 		$query_update = $this->db_connection->prepare('UPDATE '.PSM_DB_PREFIX.'users SET password = :user_password_hash,
 | |
| 													   password_reset_hash = NULL, password_reset_timestamp = NULL
 | |
| 													   WHERE user_id = :user_id');
 | |
| 		$query_update->bindValue(':user_password_hash', $user_password_hash, \PDO::PARAM_STR);
 | |
| 		$query_update->bindValue(':user_id', $user_id, \PDO::PARAM_STR);
 | |
| 		$query_update->execute();
 | |
| 
 | |
| 		// check if exactly one row was successfully changed:
 | |
| 		if ($query_update->rowCount() == 1) {
 | |
| 			return true;
 | |
| 		} else {
 | |
| 			return false;
 | |
| 		}
 | |
|     }
 | |
| 
 | |
| 	/**
 | |
| 	 * Gets the user id
 | |
| 	 * @return int
 | |
| 	 */
 | |
| 	public function getUserId() {
 | |
| 		return $this->user_id;
 | |
| 	}
 | |
| 
 | |
|     /**
 | |
|      * Gets the username
 | |
|      * @return string
 | |
|      */
 | |
|     public function getUsername() {
 | |
|         $user = $this->getUser();
 | |
| 		return (isset($user->user_name) ? $user->user_name : null);
 | |
|     }
 | |
| 
 | |
| 	/**
 | |
| 	 * Gets the user level
 | |
| 	 * @return int
 | |
| 	 */
 | |
| 	public function getUserLevel() {
 | |
| 		$user = $this->getUser();
 | |
| 
 | |
| 		if(isset($user->level)) {
 | |
| 			return $user->level;
 | |
| 		} else {
 | |
| 			return PSM_USER_ANONYMOUS;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * read current user preferences from the database
 | |
| 	 * @return boolean return false is user not connected
 | |
| 	 */
 | |
| 	protected function loadPreferences() {
 | |
| 		if($this->user_preferences === null) {
 | |
| 			if(!$this->getUser()) {
 | |
| 				return false;
 | |
| 			}
 | |
| 
 | |
| 			$this->user_preferences = array();
 | |
| 			foreach($this->db_connection->query('SELECT `key`,`value` FROM `' . PSM_DB_PREFIX . 'users_preferences` WHERE `user_id` = ' . $this->user_id) as $row) {
 | |
| 				$this->user_preferences[$row['key']] = $row['value'];
 | |
| 			}
 | |
| 		}
 | |
| 		return true;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get a user preference value
 | |
| 	 * @param string $key
 | |
| 	 * @param mixed $default
 | |
| 	 * @return mixed
 | |
| 	 */
 | |
| 	public function getUserPref($key, $default = '') {
 | |
| 		if(!$this->loadPreferences() || !isset($this->user_preferences[$key])) {
 | |
| 			return $default;
 | |
| 		}
 | |
| 
 | |
| 		$value = $this->user_preferences[$key];
 | |
| 		settype($value, gettype($default));
 | |
| 		return $value;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Set a user preference value
 | |
| 	 * @param string $key
 | |
| 	 * @param mixed $value
 | |
| 	 */
 | |
| 	public function setUserPref($key, $value) {
 | |
| 		if($this->loadPreferences()) {
 | |
| 			if(isset($this->user_preferences[$key])) {
 | |
| 				if($this->user_preferences[$key] == $value) {
 | |
| 					return;		// no change
 | |
| 				}
 | |
| 				$sql = 'UPDATE `' . PSM_DB_PREFIX . 'users_preferences` SET `key` = ?, `value` = ? WHERE `user_id` = ?';
 | |
| 			} else{
 | |
| 				$sql = 'INSERT INTO `' . PSM_DB_PREFIX . 'users_preferences` SET `key` = ?, `value` = ?, `user_id` = ?';
 | |
| 			}
 | |
| 			$sth = $this->db_connection->prepare($sql);
 | |
| 			$sth->execute(array($key, $value, $this->user_id));
 | |
| 			$this->user_preferences[$key] = $value;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get session object
 | |
| 	 * @return \Symfony\Component\HttpFoundation\Session\SessionInterface
 | |
| 	 */
 | |
| 	public function getSession() {
 | |
| 		return $this->session;
 | |
| 	}
 | |
| }
 |