Files
人事系统开发 1ba6efd8ed MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、JS空集合保护、
会话过期体验优化(403 JSON)、display_errors 关闭、
固定 key 根治 Integrity check failed 等全部改动

注意:config.inc.php/.htaccess/.user.ini 含敏感信息,
通过 .gitignore 排除,勿推送到公开仓库。
2026-09-09 16:55:02 +08:00

982 lines
29 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace MRBS\Auth;
use MRBS\DB\DB;
use MRBS\Language;
use MRBS\MailQueue;
use MRBS\User;
use PHPMailer\PHPMailer\PHPMailer;
use function MRBS\_tbl;
use function MRBS\auth;
use function MRBS\db;
use function MRBS\format_compound_name;
use function MRBS\generate_token;
use function MRBS\get_vocab;
use function MRBS\multisite;
use function MRBS\parse_email;
use function MRBS\row_cast_columns;
use function MRBS\toTimeString;
use function MRBS\url_base;
class AuthDb extends AuthDbAbstract
{
// 等保整改:登录状态标记(供 SessionWithLogin 区分“锁定 / 密码错误 / 用户不存在”)
// @var bool
private $loginBlocked = false;
private $loginFailed = false;
public function __construct()
{
$this->db_table = _tbl('users');
$this->column_name_username = 'name';
$this->column_name_display_name = 'display_name';
}
/**
* @param string|null $user a username or email address
*/
public function validateUser(
#[\SensitiveParameter]
?string $user,
#[\SensitiveParameter]
?string $pass)
{
// The string $user that the user logged on with could be either a username or
// an email address, or even possibly just the local part of an email address.
// So it's just possible that there is more than one user with this password and
// username | email address | local-part. If we get more than one, then we don't
// know which user it is, so we return false.
$valid_usernames = array();
if (($valid_username = $this->validateUsername($user, $pass)) !== false)
{
$valid_usernames[] = $valid_username;
}
$valid_usernames = array_merge($valid_usernames, $this->validateEmail($user, $pass));
$valid_usernames = array_unique($valid_usernames);
if (count($valid_usernames) == 1)
{
$result = $valid_usernames[0];
// 登录成功:复位状态标记(避免残留锁定标记影响后续判断)
$this->loginBlocked = false;
$this->loginFailed = false;
// Update the database with this login, but don't change the timestamp
$now = time();
$sql = "UPDATE " . _tbl('users') . "
SET last_login=?, timestamp=timestamp
WHERE name=?";
$sql_params = array($now, $result);
$this->connection()->command($sql, $sql_params);
return $result;
}
else
{
return false;
}
}
protected function connection() : ?DB
{
return db();
}
/* validateUsername($user, $pass)
*
* Checks if the specified username/password pair are valid
*
* $user - The user name
* $pass - The password
*
* Returns:
* false - The pair are invalid or do not exist
* string - The validated username
*/
private function validateUsername(
#[\SensitiveParameter]
?string $user,
#[\SensitiveParameter]
?string $pass)
{
$sql_params = array();
// We use syntax_casesensitive_equals() rather than just '=' because '=' in MySQL
// permits trailing spacings, eg 'john' = 'john '. We could use LIKE, but that then
// permits wildcards, so we could use a combination of LIKE and '=' but that's a bit
// messy. We could use STRCMP, but that's MySQL only.
// Usernames are unique in the users table, so we only look for one.
// 等保整改:额外取出 failed_logins / locked_until 用于登录失败锁定
$sql = "SELECT password_hash, name, failed_logins, locked_until
FROM " . _tbl('users') . "
WHERE " . $this->connection()->syntax_casesensitive_equals('name', mb_strtolower($user), $sql_params) . "
LIMIT 1";
$res = $this->connection()->query($sql, $sql_params);
$row = $res->next_row_keyed();
// 复位状态标记(一次登录尝试至多设置其一)
$this->loginBlocked = false;
$this->loginFailed = false;
if (!isset($row['password_hash']))
{
// No user found with that name. 故意不计数、不区分提示,避免用户名枚举。
return false;
}
// 等保整改:账号临时锁定检查(连续失败达阈值后 locked_until > now
if ((int)$row['locked_until'] > time())
{
$this->loginBlocked = true;
return false;
}
if (!$this->checkPassword($pass, $row['password_hash'], 'name', $row['name']))
{
$this->loginFailed = true;
$this->registerLoginFailure($row['name'], (int)$row['failed_logins']);
return false;
}
// 密码正确:清零失败计数并解除锁定
$this->clearLoginFailures($row['name']);
return $row['name'];
}
/* authValidateEmail($email, $pass)
*
* Checks if the specified email/password pair are valid
*
* $email - The email address
* $pass - The password
*
* Returns:
* array - An array of valid usernames, empty if none found
*/
private function validateEmail(
#[\SensitiveParameter]
string $email,
#[\SensitiveParameter]
string $pass) : array
{
$valid_usernames = array();
// Email addresses are not unique in the users table, so we need to find all of them.
$users = self::getUsersByEmail($email);
// Check all the users that have this email address and password hash.
// 等保整改:同一邮箱多账号时逐个做锁定检查 / 失败计数(封堵 email 登录通道的暴力破解)
foreach($users as $user)
{
if (isset($user['password_hash']))
{
if ((int)$user['locked_until'] > time())
{
$this->loginBlocked = true;
continue;
}
if ($this->checkPassword($pass, $user['password_hash'], 'email', $email))
{
$valid_usernames[] = $user['name'];
$this->clearLoginFailures($user['name']);
}
else
{
$this->loginFailed = true;
$this->registerLoginFailure($user['name'], (int)$user['failed_logins']);
}
}
}
return $valid_usernames;
}
// ===== 等保整改:登录失败锁定 / 口令有效期 / 自助改密 =====
// 上次登录尝试是否因账号锁定被拒
public function getLoginBlocked() : bool
{
return $this->loginBlocked;
}
// 上次登录尝试是否因密码错误被拒(区别于“用户不存在”)
public function getLoginFailed() : bool
{
return $this->loginFailed;
}
// 登录失败计数:达到阈值则设置 locked_until。
// 若账号曾因达到阈值被锁(现已到期解锁),从 1 重新计数,避免“永远差一次就锁”。
private function registerLoginFailure(string $username, int $current_failures) : void
{
global $login_lock_threshold, $login_lock_duration;
$threshold = $login_lock_threshold ?? 5;
$duration = $login_lock_duration ?? (15 * 60);
if ($current_failures >= $threshold)
{
$current_failures = 0;
}
$new_failures = $current_failures + 1;
$locked_until = ($new_failures >= $threshold) ? (time() + $duration) : 0;
$sql = "UPDATE " . _tbl('users') . "
SET failed_logins=?, locked_until=?
WHERE name=?";
$this->connection()->command($sql, array($new_failures, $locked_until, $username));
}
// 登录成功:清零失败计数与锁定时间
private function clearLoginFailures(string $username) : void
{
$sql = "UPDATE " . _tbl('users') . "
SET failed_logins=0, locked_until=0
WHERE name=?";
$this->connection()->command($sql, array($username));
}
// 口令是否需要强制修改(初始口令 / 已超过 90 天有效期)
public function needsPasswordChange(string $username) : bool
{
global $pwd_max_age;
$max_age = $pwd_max_age ?? (90 * 24 * 60 * 60);
$sql = "SELECT password_changed_at
FROM " . _tbl('users') . "
WHERE name=?
LIMIT 1";
$res = $this->connection()->query($sql, array($username));
$row = $res->next_row_keyed();
if (!isset($row))
{
return false;
}
$changed_at = (int)$row['password_changed_at'];
return ($changed_at == 0) || ((time() - $changed_at) > $max_age);
}
// 自助改密:更新口令并记录修改时间、清零失败计数并解除锁定
public function updatePassword(string $username, string $password) : void
{
$sql = "UPDATE " . _tbl('users') . "
SET password_hash=:password_hash,
password_changed_at=:password_changed_at,
failed_logins=0,
locked_until=0
WHERE name=:name";
$sql_params = array(
':password_hash' => password_hash($password, PASSWORD_DEFAULT),
':password_changed_at' => time(),
':name' => $username
);
$this->connection()->command($sql, $sql_params);
}
protected function getUserFresh(string $username) : ?User
{
$row = $this->getUserByUsername($username);
// The username doesn't exist - return NULL
if (!isset($row))
{
return null;
}
// The username does exist - return a User object
$user = new User($username);
// $user->level and $user->display_name will be set as part of this
foreach ($row as $key => $value)
{
if ($key == 'name')
{
// This has already been set as the 'username' property;
continue;
}
$user->$key = $value;
}
return $user;
}
// Return an array of all users
public function getUsers() : array
{
// Add in an extra column, last_updated, which is the SQL timestamp converted to a UNIX
// timestamp. We do the conversion in the SQL query so that it is converted using the
// same timezone that it was stored with.
$sql = "SELECT *, ". $this->connection()->syntax_timestamp_to_unix("timestamp") . " AS last_updated
FROM " . _tbl('users') . "
ORDER BY name";
$res = $this->connection()->query($sql);
$result = $res->all_rows_keyed();
foreach ($result as &$row)
{
row_cast_columns($row, 'users');
// Turn the last_updated column into an int (some MySQL drivers will return a string,
// and it won't have been caught by row_cast_columns() as it's a derived result).
$row['last_updated'] = intval($row['last_updated']);
}
return $result;
}
/**
*/
public function canCreateUsers() : bool
{
return true;
}
/**
*/
public function canValidateByEmail() : bool
{
return true;
}
/**
*/
public function canResetPassword() : bool
{
return true;
}
/**
*/
public function canResetByEmail() : bool
{
// We allow resetting by email, even if there are multiple users with the
// same email address.
return $this->canValidateByEmail();
}
public function requestPassword(?string $login) : bool
{
if (!isset($login) || ($login === ''))
{
return false;
}
// Get the possible users given this login, which could be a username or email address.
// However all the possible users must have the same email address, so check the email
// addresses at the same time.
$possible_users = array();
$user = $this->getUserByUsername($login);
// Users must have an email address otherwise we won't be able to mail a reset link
if (isset($user) && isset($user['email']) && ($user['email'] !== ''))
{
$possible_users[] = $user;
}
if ($this->canValidateByEmail())
{
$users = $this->getUsersByEmail($login);
if (!empty($users))
{
// Check that the email addresses are the same
if (!empty($possible_users) &&
(mb_strtolower($possible_users[0]['email']) !== mb_strtolower($login)))
{
return false;
}
foreach ($users as $user)
{
$possible_users[] = $user;
}
}
}
if (!empty($possible_users))
{
// Generate a key
$key = generate_token(32);
// Update the database
if ($this->setResetKey($possible_users, $key))
{
// Email the user
return $this->notifyUser($possible_users, $key);
}
}
return false;
}
public function resetPassword(
#[\SensitiveParameter]
?string $username,
?string $key,
#[\SensitiveParameter]
?string $password) : bool
{
// Check that we've got a password and we're allowed to reset the password
if (!isset($password) || !auth()->isValidReset($username, $key))
{
return false;
}
// Set the new password and clear the reset key
// 等保整改:记录口令修改时间(password_changed_at),供 90 天有效期计算
$sql = "UPDATE " . _tbl('users') . "
SET password_hash=:password_hash,
password_changed_at=:password_changed_at,
reset_key_hash=NULL,
reset_key_expiry=0
WHERE name=:name"; // PostgreSQL does not support LIMIT with UPDATE
$sql_params = array(
':password_hash' => password_hash($password, PASSWORD_DEFAULT),
':password_changed_at' => time(),
':name' => $username
);
$this->connection()->command($sql, $sql_params);
return true;
}
public function isValidReset(?string $user, ?string $key) : bool
{
if (!isset($user) || !isset($key) || ($user === '') || ($key === ''))
{
return false;
}
$sql = "SELECT reset_key_hash, reset_key_expiry
FROM " . _tbl('users') . "
WHERE name=:name
LIMIT 1";
$sql_params = array(':name' => $user);
$res = $this->connection()->query($sql,$sql_params);
// Check we've found a row
if ($res->count() == 0)
{
return false;
}
$row = $res->next_row_keyed();
// Check that the reset hasn't expired
if (time() > $row['reset_key_expiry'])
{
return false;
}
// Check we've got the correct key
return password_verify($key, $row['reset_key_hash']);
}
// Returns an unsorted array of registrants display names
protected function getRegistrantsDisplayNamesUnsortedWithout(int $id, bool $with_registrant_username) : array
{
// For the 'db' auth type we can improve performance by doing a single query
// on the participants table joined with the users table. (Actually it's two
// queries in a UNION: one getting the rows where there isn't an entry in the
// users table and another the rows where there is.)
$sql = "SELECT P.username as username,
P.username as display_name
FROM " . _tbl('participants') . " P
LEFT JOIN " . _tbl('users') . " U
ON P.username=U.name
WHERE P.entry_id=:entry_id
AND (U.display_name IS NULL OR U.display_name='')
UNION
SELECT U.name as username,
U.display_name as display_name
FROM " . _tbl('participants') . " P
LEFT JOIN " . _tbl('users') . " U
ON P.username=U.name
WHERE P.entry_id=:entry_id
AND U.display_name IS NOT NULL AND U.display_name!=''";
$result = array();
$res = $this->connection()->query($sql, array(':entry_id' => $id));
while (false !== ($row = $res->next_row_keyed()))
{
$result[] = ($with_registrant_username) ? format_compound_name($row['username'], $row['display_name']) : $row['display_name'];
}
return $result;
}
// Returns an unsorted array of registrants display names, including, if
// different, the display name of the person that registered them.
protected function getRegistrantsDisplayNamesUnsortedWith(int $id, bool $with_registrant_username) : array
{
// For the 'db' auth type we can improve performance by doing a single query
// on the participants table joined with the users table. (Actually it's four
// queries in a UNION: one getting the rows where there isn't an entry in the
// users table and another the rows where there is, etc. for both the registrant
// and the person that registered them.)
$sql = "SELECT P.username as registrant_username,
P.username as registrant_display_name,
P.create_by as create_by_username,
P.create_by as create_by_display_name
FROM " . _tbl('participants') . " P
LEFT JOIN " . _tbl('users') . " U1
ON P.username=U1.name
LEFT JOIN " . _tbl('users') . " U2
ON P.create_by=U2.name
WHERE P.entry_id=:entry_id
AND (U1.display_name IS NULL OR U1.display_name='')
AND (U2.display_name IS NULL OR U2.display_name='')
UNION
SELECT P.username as registrant_username,
P.username as registrant_display_name,
P.create_by as create_by_username,
U2.display_name as registrant_display_name
FROM " . _tbl('participants') . " P
LEFT JOIN " . _tbl('users') . " U1
ON P.username=U1.name
LEFT JOIN " . _tbl('users') . " U2
ON P.create_by=U2.name
WHERE P.entry_id=:entry_id
AND (U1.display_name IS NULL OR U1.display_name='')
AND U2.display_name IS NOT NULL AND U2.display_name!=''
UNION
SELECT P.username as registrant_username,
U1.display_name as registrant_display_name,
P.create_by as create_by_username,
P.create_by as registrant_display_name
FROM " . _tbl('participants') . " P
LEFT JOIN " . _tbl('users') . " U1
ON P.username=U1.name
LEFT JOIN " . _tbl('users') . " U2
ON P.create_by=U2.name
WHERE P.entry_id=:entry_id
AND U1.display_name IS NOT NULL AND U1.display_name!=''
AND (U2.display_name IS NULL OR U2.display_name='')
UNION
SELECT P.username as registrant_username,
U1.display_name as registrant_display_name,
P.create_by as create_by_username,
U2.display_name as registrant_display_name
FROM " . _tbl('participants') . " P
LEFT JOIN " . _tbl('users') . " U1
ON P.username=U1.name
LEFT JOIN " . _tbl('users') . " U2
ON P.create_by=U2.name
WHERE P.entry_id=:entry_id
AND U1.display_name IS NOT NULL AND U1.display_name!=''
AND U2.display_name IS NOT NULL AND U2.display_name!=''";
$result = array();
$res = $this->connection()->query($sql, array(':entry_id' => $id));
while (false !== ($row = $res->next_row_keyed()))
{
if ($row['registrant_username'] === $row['create_by_username'])
{
if ($with_registrant_username)
{
$result[] = format_compound_name($row['registrant_username'], $row['registrant_display_name']);
}
else
{
$result[] = $row['registrant_display_name'];
}
}
else
{
if ($with_registrant_username && ($row['registrant_username'] !== $row['registrant_display_name']))
{
$result[] = get_vocab('registrant_username_and_registered_by',
$row['registrant_username'],
$row['registrant_display_name'],
$row['create_by_display_name']);
}
else
{
$result[] = get_vocab('registrant_registered_by',
$row['registrant_display_name'],
$row['create_by_display_name']);
}
}
}
return $result;
}
/**
* @return string[]
*/
protected function getRegistrantsDisplayNamesUnsorted(int $id, bool $with_registered_by, $with_registrant_username) : array
{
if ($with_registered_by)
{
return $this->getRegistrantsDisplayNamesUnsortedWith($id, $with_registrant_username);
}
else
{
return $this->getRegistrantsDisplayNamesUnsortedWithout($id, $with_registrant_username);
}
}
private function notifyUser(array $users, string $key) : bool
{
global $auth, $mail_settings;
if (empty($users) || !isset($users[0]['email']) || ($users[0]['email'] === ''))
{
return false;
}
$expiry_time = $auth['db']['reset_key_expiry'];
toTimeString($expiry_time, $expiry_units, true, 'hours');
$addresses = array(
'from' => $mail_settings['from']
);
// Add the To address, using the display name if possible (ie if it exists and there's
// only one user).
// Also get a name to use in the message body
if ((count($users) == 1) &&
isset($users[0]['display_name']) &&
($users[0]['display_name'] !== ''))
{
$mailer = new PHPMailer();
$mailer->CharSet = Language::MAIL_CHARSET;
// Note that addrFormat() returns a MIME-encoded address
$addresses['to'] = $mailer->addrFormat(array($users[0]['email'], $users[0]['display_name']));
$name = $users[0]['display_name'];
}
else
{
$addresses['to'] = $users[0]['email'];
// If there's only one user we can use the username, otherwise we have to use the
// email address which is the same for all users.
$name = (count($users) == 1) ? $users[0]['name'] : $users[0]['email'];
}
$subject = get_vocab('password_reset_subject');
$body = '<p>';
$body .= get_vocab('password_reset_body', intval($expiry_time), $expiry_units, $name);
$body .= "</p>\n";
// Construct and add in the link
$usernames = array();
foreach ($users as $user)
{
$usernames[] = $user['name'];
}
$usernames = array_unique($usernames);
$vars = array(
'action' => 'reset',
'usernames' => $usernames,
'key' => $key
);
$query = http_build_query($vars, '', '&');
$href = url_base() . multisite("reset_password.php?$query");
$body .= "<p><a href=\"$href\">" . get_vocab('reset_password') . "</a>.</p>";
MailQueue::add(
$addresses,
$subject,
strip_tags($body),
$body,
null,
Language::MAIL_CHARSET
);
return true;
}
private function setResetKey(array $users, string $key) : bool
{
global $auth;
if (empty($users))
{
return false;
}
$ids = array();
foreach($users as $user)
{
// Use intval to make sure the string is safe for the SQL query
$ids[] = intval($user['id']);
}
$sql = "UPDATE " . _tbl('users') . "
SET reset_key_hash=:reset_key_hash,
reset_key_expiry=:reset_key_expiry
WHERE id IN (" . implode(',', $ids) . ")";
$sql_params = array(
':reset_key_hash' => password_hash($key, PASSWORD_DEFAULT),
':reset_key_expiry' => time() + $auth['db']['reset_key_expiry']
);
$this->connection()->command($sql, $sql_params);
return true;
}
private function getUserByUsername(string $username) : ?array
{
$sql = "SELECT *
FROM " . _tbl('users') . "
WHERE name=:name
LIMIT 1";
$result = $this->connection()->query($sql, array(':name' => $username));
// The username doesn't exist - return NULL
if ($result->count() === 0)
{
return null;
}
return $result->next_row_keyed();
}
public function getUserByUserId(int $id) : ?User
{
$sql = "SELECT *
FROM " . _tbl('users') . "
WHERE id=:id
LIMIT 1";
$result = $this->connection()->query($sql, array(':id' => $id));
// The username doesn't exist - return NULL
if ($result->count() === 0)
{
return null;
}
// The username does exist - return a User object
$user = new User();
$row = $result->next_row_keyed();
// $user->level and $user->display_name will be set as part of this
foreach ($row as $key => $value)
{
if ($key == 'name')
{
$user->username = $value;
}
$user->$key = $value;
}
return $user;
}
public function getUsernameByEmail(string $email) : ?string
{
$sql = "SELECT name
FROM " . _tbl('users') . "
WHERE email=?";
$res = $this->connection()->query($sql, array($email));
if ($res->count() == 0)
{
return null;
}
if ($res->count() > 1)
{
// Could maybe do something better here
trigger_error("Email address not unique", E_USER_NOTICE);
}
$row = $res->next_row_keyed();
return $row['name'];
}
// Returns an array of rows for all users with the email address $email.
// Assumes that email addresses are case insensitive.
// Allows equivalent Gmail addresses, ie ignores dots in the local part and
// treats gmail.com and googlemail.com as equivalent domains.
private function getUsersByEmail(string $email) : array
{
global $auth;
$result = array();
// For the moment we will assume that email addresses are case-insensitive. Whilst it is true
// on most systems, it isn't always true. The domain is case-insensitive but the local-part can
// be case-sensitive. But before we can take account of this, the email addresses in the database
// need to be normalised so that all the domain names are stored in lower case. Then it will be
// possible to do a case-sensitive comparison.
if (mb_strpos($email, '@') === false)
{
if (!empty($auth['allow_local_part_email']))
{
// We're just checking the local-part of the email address
$sql_params = array($email);
$condition = "LOWER(?)=LOWER(" . $this->connection()->syntax_simple_split('email', '@', 1, $sql_params) .")";
}
else
{
return $result;
}
}
else
{
$address = parse_email($email);
// Invalid email address
if ($address === false)
{
return $result;
}
// Special case for Gmail addresses: ignore dots in the local part and treat gmail.com and
// googlemail.com as equivalent domains.
elseif (in_array(mb_strtolower($address['domain']), array('gmail.com', 'googlemail.com')))
{
$sql_params = array(str_replace('.', '', $address['local']));
$sql_params[] = $sql_params[0];
$condition = "(LOWER(?) = REPLACE(TRIM(TRAILING '@gmail.com' FROM LOWER(email)), '.', '')) OR " .
"(LOWER(?) = REPLACE(TRIM(TRAILING '@googlemail.com' FROM LOWER(email)), '.', ''))";
}
// Everything else: check the complete email address
else
{
$sql_params = array($email);
$condition = "LOWER(?)=LOWER(email)";
}
}
$sql = "SELECT *
FROM " . _tbl('users') . "
WHERE $condition";
$res = $this->connection()->query($sql, $sql_params);
while (false !== ($row = $res->next_row_keyed()))
{
$result[] = $row;
}
return $result;
}
private function rehash(
#[\SensitiveParameter]
string $password,
string $column_name,
string $column_value) : void
{
$sql_params = array(password_hash($password, PASSWORD_DEFAULT));
switch ($column_name)
{
case 'name':
$condition = $this->connection()->syntax_casesensitive_equals($column_name, mb_strtolower($column_value), $sql_params);
break;
case 'email':
// For the moment we will assume that email addresses are case insensitive. Whilst it is true
// on most systems, it isn't always true. The domain is case insensitive but the local-part can
// be case sensitive. But before we can take account of this, the email addresses in the database
// need to be normalised so that all the domain names are stored in lower case. Then it will be possible
// to do a case sensitive comparison.
$sql_params[] = $column_value;
$condition = "LOWER($column_name)=LOWER(?)";
break;
default:
trigger_error("Unsupported column name '$column_name'.", E_USER_NOTICE);
return;
break;
}
$sql = "UPDATE " . _tbl('users') . "
SET password_hash=?
WHERE $condition";
$this->connection()->command($sql, $sql_params);
}
// Checks $password against $password_hash for the row in the user table
// where $column_name=$column_value. Typically $column_name will be either
// 'name' or 'email'.
// Returns a boolean: true if they match, otherwise false.
private function checkPassword(
#[\SensitiveParameter]
string $password,
string $password_hash,
string $column_name,
string $column_value) : bool
{
$result = false;
$do_rehash = false;
/* If the hash starts '$' it's a PHP password hash */
if (substr($password_hash, 0, 1) == '$')
{
if (password_verify($password, $password_hash))
{
$result = true;
if (password_needs_rehash($password_hash, PASSWORD_DEFAULT))
{
$do_rehash = true;
}
}
}
/* Otherwise it's a legacy MD5 hash */
else
{
if (md5($password) == $password_hash)
{
$result = true;
$do_rehash = true;
}
}
if ($do_rehash)
{
$this->rehash($password, $column_name, $column_value);
}
return $result;
}
}