phpservermon/src/psm/Service/User.php

585 lines
20 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@mailbox.org>
* @copyright Copyright (c) 2008-2017 Pepijn Over <pep@mailbox.org>
* @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 (PSM_PUBLIC === true && PSM_PUBLIC_PAGE === true) {
$query_user = $this->db_connection->prepare('SELECT * FROM ' .
PSM_DB_PREFIX . 'users WHERE user_name = :user_name and level = :level');
$query_user->bindValue(':user_name', "__PUBLIC__", \PDO::PARAM_STR);
$query_user->bindValue(':level', PSM_USER_ANONYMOUS, \PDO::PARAM_STR);
$query_user->execute();
// get result row (as an object)
$this->setUserLoggedIn($query_user->fetchObject()->user_id);
}
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);
$ldapauthstatus = false;
if (empty($user_name) && empty($user_password)) {
return false;
}
$dirauthconfig = psm_get_conf('dirauth_status');
// LDAP auth enabled
if ($dirauthconfig === '1') {
$ldaplibpath = realpath(
PSM_PATH_SRC . '..' . DIRECTORY_SEPARATOR .
'vendor' . DIRECTORY_SEPARATOR .
'viharm' . DIRECTORY_SEPARATOR .
'psm-ldap-auth' . DIRECTORY_SEPARATOR .
'psmldapauth.php'
);
// If the library is found
if ($ldaplibpath) {
// Delegate the authentication to the PsmLDAPauth module.
// If LDAP auth fails or if library not found, fall back to native auth
include_once($ldaplibpath);
$ldapauthstatus = psmldapauth($user_name, $user_password, $GLOBALS['sm_config'], $this->db_connection);
}
}
$user = $this->getUserByUsername($user_name);
// Authenticated
if ($ldapauthstatus === true) {
// Remove password to prevent it from being saved in the DB.
// Otherwise, user may still be authenticated if LDAP is disabled later.
$user_password = null;
@fn_Debug('Authenticated', $user);
} else {
// 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;
} elseif (!password_verify($user_password, $user->password)) {
return false;
}
} // not authenticated
$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', random_bytes(64));
$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 (64 char string)
$user_password_reset_hash = hash('sha256', uniqid(random_bytes(64), 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|\PDOStatement $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;
}
}