675 lines
19 KiB
PHP
675 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* @brief Authentication and user credentials management
|
|
*
|
|
* dcAuth is a class used to handle everything related to user authentication
|
|
* and credentials. Object is provided by dcCore $auth property.
|
|
*
|
|
* @package Dotclear
|
|
* @subpackage Core
|
|
*
|
|
* @copyright Olivier Meunier & Association Dotclear
|
|
* @copyright GPL-2.0-only
|
|
*/
|
|
|
|
if (!defined('DC_RC_PATH')) {return;}
|
|
|
|
class dcAuth
|
|
{
|
|
/** @var dcCore dcCore instance */
|
|
protected $core;
|
|
/** @var connection Database connection object */
|
|
protected $con;
|
|
|
|
/** @var string User table name */
|
|
protected $user_table;
|
|
/** @var string Perm table name */
|
|
protected $perm_table;
|
|
|
|
/** @var string Current user ID */
|
|
protected $user_id;
|
|
/** @var array Array with user information */
|
|
protected $user_info = [];
|
|
/** @var array Array with user options */
|
|
protected $user_options = [];
|
|
/** @var boolean User must change his password after login */
|
|
protected $user_change_pwd;
|
|
/** @var boolean User is super admin */
|
|
protected $user_admin;
|
|
/** @var array Permissions for each blog */
|
|
protected $permissions = [];
|
|
/** @var boolean User can change its password */
|
|
protected $allow_pass_change = true;
|
|
/** @var array List of blogs on which the user has permissions */
|
|
protected $blogs = [];
|
|
/** @var integer Count of user blogs */
|
|
public $blog_count = null;
|
|
|
|
/** @var array Permission types */
|
|
protected $perm_types;
|
|
|
|
/** @var dcPrefs dcPrefs object */
|
|
public $user_prefs;
|
|
|
|
/**
|
|
* Class constructor. Takes dcCore object as single argument.
|
|
*
|
|
* @param dcCore $core dcCore object
|
|
*/
|
|
public function __construct($core)
|
|
{
|
|
$this->core = &$core;
|
|
$this->con = &$core->con;
|
|
$this->blog_table = $core->prefix . 'blog';
|
|
$this->user_table = $core->prefix . 'user';
|
|
$this->perm_table = $core->prefix . 'permissions';
|
|
|
|
$this->perm_types = [
|
|
'admin' => __('administrator'),
|
|
'usage' => __('manage their own entries and comments'),
|
|
'publish' => __('publish entries and comments'),
|
|
'delete' => __('delete entries and comments'),
|
|
'contentadmin' => __('manage all entries and comments'),
|
|
'categories' => __('manage categories'),
|
|
'media' => __('manage their own media items'),
|
|
'media_admin' => __('manage all media items')
|
|
];
|
|
}
|
|
|
|
/// @name Credentials and user permissions
|
|
//@{
|
|
/**
|
|
* Checks if user exists and can log in. <var>$pwd</var> argument is optionnal
|
|
* while you may need to check user without password. This method will create
|
|
* credentials and populate all needed object properties.
|
|
*
|
|
* @param string $user_id User ID
|
|
* @param string $pwd User password
|
|
* @param string $user_key User key check
|
|
* @param boolean $check_blog checks if user is associated to a blog or not.
|
|
* @return boolean
|
|
*/
|
|
public function checkUser($user_id, $pwd = null, $user_key = null, $check_blog = true)
|
|
{
|
|
# Check user and password
|
|
$strReq = 'SELECT user_id, user_super, user_pwd, user_change_pwd, ' .
|
|
'user_name, user_firstname, user_displayname, user_email, ' .
|
|
'user_url, user_default_blog, user_options, ' .
|
|
'user_lang, user_tz, user_post_status, user_creadt, user_upddt ' .
|
|
'FROM ' . $this->con->escapeSystem($this->user_table) . ' ' .
|
|
"WHERE user_id = '" . $this->con->escape($user_id) . "' ";
|
|
|
|
try {
|
|
$rs = $this->con->select($strReq);
|
|
} catch (Exception $e) {
|
|
$err = $e->getMessage();
|
|
return false;
|
|
}
|
|
|
|
if ($rs->isEmpty()) {
|
|
sleep(rand(2, 5));
|
|
return false;
|
|
}
|
|
|
|
$rs->extend('rsExtUser');
|
|
|
|
if ($pwd != '') {
|
|
$rehash = false;
|
|
if (password_verify($pwd, $rs->user_pwd)) {
|
|
// User password ok
|
|
if (password_needs_rehash($rs->user_pwd, PASSWORD_DEFAULT)) {
|
|
$rs->user_pwd = $this->crypt($pwd);
|
|
$rehash = true;
|
|
}
|
|
} else {
|
|
// Check if pwd still stored in old fashion way
|
|
$ret = password_get_info($rs->user_pwd);
|
|
if (is_array($ret) && isset($ret['algo']) && $ret['algo'] == 0) {
|
|
// hash not done with password_hash() function, check by old fashion way
|
|
if (crypt::hmac(DC_MASTER_KEY, $pwd, DC_CRYPT_ALGO) == $rs->user_pwd) {
|
|
// Password Ok, need to store it in new fashion way
|
|
$rs->user_pwd = $this->crypt($pwd);
|
|
$rehash = true;
|
|
} else {
|
|
// Password KO
|
|
sleep(rand(2, 5));
|
|
return false;
|
|
}
|
|
} else {
|
|
// Password KO
|
|
sleep(rand(2, 5));
|
|
return false;
|
|
}
|
|
}
|
|
if ($rehash) {
|
|
// Store new hash in DB
|
|
$cur = $this->con->openCursor($this->user_table);
|
|
$cur->user_pwd = (string) $rs->user_pwd;
|
|
$cur->update("WHERE user_id = '" . $rs->user_id . "'");
|
|
}
|
|
} elseif ($user_key != '') {
|
|
if (http::browserUID(DC_MASTER_KEY . $rs->user_id . $this->cryptLegacy($rs->user_id)) != $user_key) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$this->user_id = $rs->user_id;
|
|
$this->user_change_pwd = (boolean) $rs->user_change_pwd;
|
|
$this->user_admin = (boolean) $rs->user_super;
|
|
|
|
$this->user_info['user_pwd'] = $rs->user_pwd;
|
|
$this->user_info['user_name'] = $rs->user_name;
|
|
$this->user_info['user_firstname'] = $rs->user_firstname;
|
|
$this->user_info['user_displayname'] = $rs->user_displayname;
|
|
$this->user_info['user_email'] = $rs->user_email;
|
|
$this->user_info['user_url'] = $rs->user_url;
|
|
$this->user_info['user_default_blog'] = $rs->user_default_blog;
|
|
$this->user_info['user_lang'] = $rs->user_lang;
|
|
$this->user_info['user_tz'] = $rs->user_tz;
|
|
$this->user_info['user_post_status'] = $rs->user_post_status;
|
|
$this->user_info['user_creadt'] = $rs->user_creadt;
|
|
$this->user_info['user_upddt'] = $rs->user_upddt;
|
|
|
|
$this->user_info['user_cn'] = dcUtils::getUserCN($rs->user_id, $rs->user_name,
|
|
$rs->user_firstname, $rs->user_displayname);
|
|
|
|
$this->user_options = array_merge($this->core->userDefaults(), $rs->options());
|
|
|
|
$this->user_prefs = new dcPrefs($this->core, $this->user_id);
|
|
|
|
# Get permissions on blogs
|
|
if ($check_blog && ($this->findUserBlog() === false)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* This method crypt given string (password, session_id, …).
|
|
*
|
|
* @param string $pwd string to be crypted
|
|
* @return string crypted value
|
|
*/
|
|
public function crypt($pwd)
|
|
{
|
|
return password_hash($pwd, PASSWORD_DEFAULT);
|
|
}
|
|
|
|
/**
|
|
* This method crypt given string (password, session_id, …).
|
|
*
|
|
* @param string $pwd string to be crypted
|
|
* @return string crypted value
|
|
*/
|
|
public function cryptLegacy($pwd)
|
|
{
|
|
return crypt::hmac(DC_MASTER_KEY, $pwd, DC_CRYPT_ALGO);
|
|
}
|
|
|
|
/**
|
|
* This method only check current user password.
|
|
*
|
|
* @param string $pwd User password
|
|
* @return boolean
|
|
*/
|
|
public function checkPassword($pwd)
|
|
{
|
|
if (!empty($this->user_info['user_pwd'])) {
|
|
return password_verify($pwd, $this->user_info['user_pwd']);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This method checks if user session cookie exists
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function sessionExists()
|
|
{
|
|
return isset($_COOKIE[DC_SESSION_NAME]);
|
|
}
|
|
|
|
/**
|
|
* This method checks user session validity.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function checkSession($uid = null)
|
|
{
|
|
$this->core->session->start();
|
|
|
|
# If session does not exist, logout.
|
|
if (!isset($_SESSION['sess_user_id'])) {
|
|
$this->core->session->destroy();
|
|
return false;
|
|
}
|
|
|
|
# Check here for user and IP address
|
|
$this->checkUser($_SESSION['sess_user_id']);
|
|
$uid = $uid ?: http::browserUID(DC_MASTER_KEY);
|
|
|
|
$user_can_log = $this->userID() !== null && $uid == $_SESSION['sess_browser_uid'];
|
|
|
|
if (!$user_can_log) {
|
|
$this->core->session->destroy();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if user must change his password in order to login.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function mustChangePassword()
|
|
{
|
|
return $this->user_change_pwd;
|
|
}
|
|
|
|
/**
|
|
* Checks if user is super admin
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isSuperAdmin()
|
|
{
|
|
return $this->user_admin;
|
|
}
|
|
|
|
/**
|
|
* Checks if user has permissions given in <var>$permissions</var> for blog
|
|
* <var>$blog_id</var>. <var>$permissions</var> is a coma separated list of
|
|
* permissions.
|
|
*
|
|
* @param string $permissions Permissions list
|
|
* @param string $blog_id Blog ID
|
|
* @return boolean
|
|
*/
|
|
public function check($permissions, $blog_id)
|
|
{
|
|
if ($this->user_admin) {
|
|
return true;
|
|
}
|
|
|
|
$p = array_map('trim', explode(',', $permissions));
|
|
$b = $this->getPermissions($blog_id);
|
|
|
|
if ($b != false) {
|
|
if (isset($b['admin'])) {
|
|
return true;
|
|
}
|
|
|
|
foreach ($p as $v) {
|
|
if (isset($b[$v])) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns true if user is allowed to change its password.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function allowPassChange()
|
|
{
|
|
return $this->allow_pass_change;
|
|
}
|
|
//@}
|
|
|
|
/// @name User code handlers
|
|
//@{
|
|
public function getUserCode()
|
|
{
|
|
$code =
|
|
pack('a32', $this->userID()) .
|
|
pack('H*', $this->crypt($this->getInfo('user_pwd')));
|
|
return bin2hex($code);
|
|
}
|
|
|
|
public function checkUserCode($code)
|
|
{
|
|
$code = @pack('H*', $code);
|
|
|
|
$user_id = trim(@pack('a32', substr($code, 0, 32)));
|
|
$pwd = @unpack('H*hex', substr($code, 32));
|
|
|
|
if ($user_id === false || $pwd === false) {
|
|
return false;
|
|
}
|
|
|
|
$pwd = $pwd['hex'];
|
|
|
|
$strReq = 'SELECT user_id, user_pwd ' .
|
|
'FROM ' . $this->user_table . ' ' .
|
|
"WHERE user_id = '" . $this->con->escape($user_id) . "' ";
|
|
|
|
$rs = $this->con->select($strReq);
|
|
|
|
if ($rs->isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->crypt($rs->user_pwd) != $pwd) {
|
|
return false;
|
|
}
|
|
|
|
return $rs->user_id;
|
|
}
|
|
//@}
|
|
|
|
/// @name Sudo
|
|
//@{
|
|
/**
|
|
* Calls $f function with super admin rights.
|
|
* Returns the function result.
|
|
*
|
|
* @param callback $f Callback function
|
|
* @return mixed
|
|
*/
|
|
public function sudo($f, ...$args)
|
|
{
|
|
if (!is_callable($f)) {
|
|
throw new Exception($f . ' function doest not exist');
|
|
}
|
|
|
|
if ($this->user_admin) {
|
|
$res = call_user_func_array($f, $args);
|
|
} else {
|
|
$this->user_admin = true;
|
|
try {
|
|
$res = call_user_func_array($f, $args);
|
|
$this->user_admin = false;
|
|
} catch (Exception $e) {
|
|
$this->user_admin = false;
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
//@}
|
|
|
|
/// @name User information and options
|
|
//@{
|
|
/**
|
|
* Returns user permissions for a blog as an array which looks like:
|
|
*
|
|
* - [blog_id]
|
|
* - [permission] => true
|
|
* - ...
|
|
*
|
|
* @param string $blog_id Blog ID
|
|
* @return array
|
|
*/
|
|
public function getPermissions($blog_id)
|
|
{
|
|
if (isset($this->blogs[$blog_id])) {
|
|
return $this->blogs[$blog_id];
|
|
}
|
|
|
|
if ($this->user_admin) {
|
|
$strReq = 'SELECT blog_id ' .
|
|
'from ' . $this->blog_table . ' ' .
|
|
"WHERE blog_id = '" . $this->con->escape($blog_id) . "' ";
|
|
$rs = $this->con->select($strReq);
|
|
|
|
$this->blogs[$blog_id] = $rs->isEmpty() ? false : ['admin' => true];
|
|
|
|
return $this->blogs[$blog_id];
|
|
}
|
|
|
|
$strReq = 'SELECT permissions ' .
|
|
'FROM ' . $this->perm_table . ' ' .
|
|
"WHERE user_id = '" . $this->con->escape($this->user_id) . "' " .
|
|
"AND blog_id = '" . $this->con->escape($blog_id) . "' " .
|
|
"AND (permissions LIKE '%|usage|%' OR permissions LIKE '%|admin|%' OR permissions LIKE '%|contentadmin|%') ";
|
|
$rs = $this->con->select($strReq);
|
|
|
|
$this->blogs[$blog_id] = $rs->isEmpty() ? false : $this->parsePermissions($rs->permissions);
|
|
|
|
return $this->blogs[$blog_id];
|
|
}
|
|
|
|
public function getBlogCount()
|
|
{
|
|
if ($this->blog_count === null) {
|
|
$this->blog_count = $this->core->getBlogs([], true)->f(0);
|
|
}
|
|
|
|
return $this->blog_count;
|
|
}
|
|
|
|
public function findUserBlog($blog_id = null)
|
|
{
|
|
if ($blog_id && $this->getPermissions($blog_id) !== false) {
|
|
return $blog_id;
|
|
} else {
|
|
if ($this->user_admin) {
|
|
$strReq = 'SELECT blog_id ' .
|
|
'FROM ' . $this->blog_table . ' ' .
|
|
'ORDER BY blog_id ASC ' .
|
|
$this->con->limit(1);
|
|
} else {
|
|
$strReq = 'SELECT P.blog_id ' .
|
|
'FROM ' . $this->perm_table . ' P, ' . $this->blog_table . ' B ' .
|
|
"WHERE user_id = '" . $this->con->escape($this->user_id) . "' " .
|
|
"AND P.blog_id = B.blog_id " .
|
|
"AND (permissions LIKE '%|usage|%' OR permissions LIKE '%|admin|%' OR permissions LIKE '%|contentadmin|%') " .
|
|
"AND blog_status >= 0 " .
|
|
'ORDER BY P.blog_id ASC ' .
|
|
$this->con->limit(1);
|
|
}
|
|
|
|
$rs = $this->con->select($strReq);
|
|
if (!$rs->isEmpty()) {
|
|
return $rs->blog_id;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns current user ID
|
|
*
|
|
* @return string
|
|
*/
|
|
public function userID()
|
|
{
|
|
return $this->user_id;
|
|
}
|
|
|
|
/**
|
|
* Returns information about a user .
|
|
*
|
|
* @param string $n Information name
|
|
* @return string
|
|
*/
|
|
public function getInfo($n)
|
|
{
|
|
if (isset($this->user_info[$n])) {
|
|
return $this->user_info[$n];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Returns a specific user option
|
|
*
|
|
* @param string $n Option name
|
|
* @return string
|
|
*/
|
|
public function getOption($n)
|
|
{
|
|
if (isset($this->user_options[$n])) {
|
|
return $this->user_options[$n];
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Returns all user options in an associative array.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getOptions()
|
|
{
|
|
return $this->user_options;
|
|
}
|
|
//@}
|
|
|
|
/// @name Permissions
|
|
//@{
|
|
/**
|
|
* Returns an array with permissions parsed from the string <var>$level</var>
|
|
*
|
|
* @param string $level Permissions string
|
|
* @return array
|
|
*/
|
|
public function parsePermissions($level)
|
|
{
|
|
$level = preg_replace('/^\|/', '', $level);
|
|
$level = preg_replace('/\|$/', '', $level);
|
|
|
|
$res = [];
|
|
foreach (explode('|', $level) as $v) {
|
|
$res[$v] = true;
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Returns <var>perm_types</var> property content.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getPermissionsTypes()
|
|
{
|
|
return $this->perm_types;
|
|
}
|
|
|
|
/**
|
|
* Adds a new permission type.
|
|
*
|
|
* @param string $name Permission name
|
|
* @param string $title Permission title
|
|
*/
|
|
public function setPermissionType($name, $title)
|
|
{
|
|
$this->perm_types[$name] = $title;
|
|
}
|
|
//@}
|
|
|
|
/// @name Password recovery
|
|
//@{
|
|
/**
|
|
* Add a recover key to a specific user identified by its email and
|
|
* password.
|
|
*
|
|
* @param string $user_id User ID
|
|
* @param string $user_email User Email
|
|
* @return string
|
|
*/
|
|
public function setRecoverKey($user_id, $user_email)
|
|
{
|
|
$strReq = 'SELECT user_id ' .
|
|
'FROM ' . $this->user_table . ' ' .
|
|
"WHERE user_id = '" . $this->con->escape($user_id) . "' " .
|
|
"AND user_email = '" . $this->con->escape($user_email) . "' ";
|
|
|
|
$rs = $this->con->select($strReq);
|
|
|
|
if ($rs->isEmpty()) {
|
|
throw new Exception(__('That user does not exist in the database.'));
|
|
}
|
|
|
|
$key = md5(uniqid('', true));
|
|
|
|
$cur = $this->con->openCursor($this->user_table);
|
|
$cur->user_recover_key = $key;
|
|
|
|
$cur->update("WHERE user_id = '" . $this->con->escape($user_id) . "'");
|
|
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Creates a new user password using recovery key. Returns an array:
|
|
*
|
|
* - user_email
|
|
* - user_id
|
|
* - new_pass
|
|
*
|
|
* @param string $recover_key Recovery key
|
|
* @return array
|
|
*/
|
|
public function recoverUserPassword($recover_key)
|
|
{
|
|
$strReq = 'SELECT user_id, user_email ' .
|
|
'FROM ' . $this->user_table . ' ' .
|
|
"WHERE user_recover_key = '" . $this->con->escape($recover_key) . "' ";
|
|
|
|
$rs = $this->con->select($strReq);
|
|
|
|
if ($rs->isEmpty()) {
|
|
throw new Exception(__('That key does not exist in the database.'));
|
|
}
|
|
|
|
$new_pass = crypt::createPassword();
|
|
|
|
$cur = $this->con->openCursor($this->user_table);
|
|
$cur->user_pwd = $this->crypt($new_pass);
|
|
$cur->user_recover_key = null;
|
|
$cur->user_change_pwd = 1; // User will have to change this temporary password at next login
|
|
|
|
$cur->update("WHERE user_recover_key = '" . $this->con->escape($recover_key) . "'");
|
|
|
|
return ['user_email' => $rs->user_email, 'user_id' => $rs->user_id, 'new_pass' => $new_pass];
|
|
}
|
|
//@}
|
|
|
|
/** @name User management callbacks
|
|
This 3 functions only matter if you extend this class and use
|
|
DC_AUTH_CLASS constant.
|
|
These are called after core user management functions.
|
|
Could be useful if you need to add/update/remove stuff in your
|
|
LDAP directory or other third party authentication database.
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* Called after core->addUser
|
|
* @see dcCore::addUser
|
|
* @param cursor $cur User cursor
|
|
*/
|
|
public function afterAddUser($cur)
|
|
{}
|
|
|
|
/**
|
|
* Called after core->updUser
|
|
* @see dcCore::updUser
|
|
* @param string $id User ID
|
|
* @param cursor $cur User cursor
|
|
*/
|
|
public function afterUpdUser($id, $cur)
|
|
{}
|
|
|
|
/**
|
|
* Called after core->delUser
|
|
* @see dcCore::delUser
|
|
* @param string $id User ID
|
|
*/
|
|
public function afterDelUser($id)
|
|
{}
|
|
//@}
|
|
}
|