包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
@@ -0,0 +1,848 @@
|
||||
<?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
|
||||
{
|
||||
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];
|
||||
// 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.
|
||||
$sql = "SELECT password_hash, name
|
||||
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();
|
||||
|
||||
if (!isset($row['password_hash']))
|
||||
{
|
||||
// No user found with that name
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($this->checkPassword($pass, $row['password_hash'], 'name', $row['name'])) ? $row['name'] : false;
|
||||
}
|
||||
|
||||
|
||||
/* 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.
|
||||
foreach($users as $user)
|
||||
{
|
||||
if (isset($user['password_hash']) &&
|
||||
$this->checkPassword($pass, $user['password_hash'], 'email', $email))
|
||||
{
|
||||
$valid_usernames[] = $user['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return $valid_usernames;
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
$sql = "UPDATE " . _tbl('users') . "
|
||||
SET password_hash=:password_hash,
|
||||
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),
|
||||
':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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Session;
|
||||
|
||||
use MRBS\Form\ElementA;
|
||||
use MRBS\Form\ElementFieldset;
|
||||
use MRBS\Form\ElementP;
|
||||
use MRBS\Form\FieldDiv;
|
||||
use MRBS\Form\FieldInputPassword;
|
||||
use MRBS\Form\FieldInputSubmit;
|
||||
use MRBS\Form\FieldInputText;
|
||||
use MRBS\Form\Form;
|
||||
use function MRBS\auth;
|
||||
use function MRBS\get_form_var;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\location_header;
|
||||
use function MRBS\multisite;
|
||||
use function MRBS\print_footer;
|
||||
use function MRBS\print_header;
|
||||
use function MRBS\this_page;
|
||||
|
||||
/**
|
||||
* An abstract class for those session schemes that implement a login form.
|
||||
*/
|
||||
abstract class SessionWithLogin extends Session
|
||||
{
|
||||
protected $form = array();
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Get the non-standard form variables
|
||||
$vars = [
|
||||
'action' => 'string',
|
||||
'username' => 'string',
|
||||
'password' => 'string',
|
||||
'returl' => 'url_local'
|
||||
];
|
||||
|
||||
foreach ($vars as $var => $type)
|
||||
{
|
||||
$this->form[$var] = get_form_var($var, $type, null, INPUT_POST);
|
||||
}
|
||||
|
||||
// Allow the target_url to be a GET or POST value to help password managers (the target_url can
|
||||
// be stored as a query string parameter in the password manager).
|
||||
$this->form['target_url'] = get_form_var('target_url', 'url_local');
|
||||
|
||||
if (isset($this->form['username']))
|
||||
{
|
||||
// It's easy for extra spaces to appear, especially on a mobile device
|
||||
$this->form['username'] = trim($this->form['username']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Gets the username and password. Returns: Nothing
|
||||
//
|
||||
// $target_url The URL to go to after successful login
|
||||
// $returl The URL to return to eventually
|
||||
public function authGet(?string $target_url=null, ?string $returl=null, ?string $error=null, bool $raw=false) : void
|
||||
{
|
||||
if (!isset($target_url))
|
||||
{
|
||||
$target_url = $this->form['target_url'] ?? this_page(true);
|
||||
}
|
||||
|
||||
// Omit the Login link in the header when we're on the login page itself
|
||||
print_header(null, false, true);
|
||||
$action = multisite(this_page());
|
||||
$this->printLoginForm($action, $target_url, $returl, $error, $raw);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Returns the parameters ('method', 'action' and 'hidden_inputs') for a
|
||||
// Logon form. Returns an array.
|
||||
public function getLogonFormParams() : ?array
|
||||
{
|
||||
return array(
|
||||
'action' => multisite('admin.php'),
|
||||
'method' => Form::METHOD_POST,
|
||||
'hidden_inputs' => array('target_url' => this_page(true),
|
||||
'action' => 'QueryName')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Returns the parameters ('method', 'action' and 'hidden_inputs') for a
|
||||
// logoff form. Returns an array of parameters, or null if no form is to be
|
||||
// shown.
|
||||
public function getLogoffFormParams() : ?array
|
||||
{
|
||||
return array(
|
||||
'action' => multisite('admin.php'),
|
||||
'method' => Form::METHOD_POST,
|
||||
'hidden_inputs' => array('target_url' => this_page(true),
|
||||
'action' => 'SetName',
|
||||
'username' => '',
|
||||
'password' => '')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function processForm() : void
|
||||
{
|
||||
if (isset($this->form['action']))
|
||||
{
|
||||
// Target of the form with sets the URL argument "action=QueryName".
|
||||
// Will eventually return to URL argument "target_url=whatever".
|
||||
if ($this->form['action'] == 'QueryName')
|
||||
{
|
||||
$this->authGet($this->form['target_url']);
|
||||
exit(); // unnecessary because authGet() exits, but just included for clarity
|
||||
}
|
||||
|
||||
// Target of the form with sets the URL argument "action=SetName".
|
||||
// Will eventually return to URL argument "target_url=whatever".
|
||||
if ($this->form['action'] == 'SetName')
|
||||
{
|
||||
// First make sure the password is valid
|
||||
if (!isset($this->form['username']) || ($this->form['username'] == ''))
|
||||
{
|
||||
$this->logoffUser();
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we're going to do something then check the CSRF token first.
|
||||
// (Don't check the token before logging off the user because if the session has
|
||||
// expired due to inactivity, the token will be invalid, but that won't matter because
|
||||
// the result will be the same anyway - logging off the user - and we avoid
|
||||
// generating an unnecessary CSRF error message.)
|
||||
Form::checkToken();
|
||||
|
||||
// Get a valid user
|
||||
$valid_username = $this->getValidUser($this->form['username'], $this->form['password']);
|
||||
|
||||
// Successful login. You can't get out of getValidUser() without a valid username and password
|
||||
$this->logonUser($valid_username);
|
||||
|
||||
if (!empty($this->form['returl']))
|
||||
{
|
||||
// check to see whether there's a query string already
|
||||
$this->form['target_url'] .= (mb_strpos($this->form['target_url'], '?') === false) ? '?' : '&';
|
||||
$this->form['target_url'] .= 'returl=' . urlencode($this->form['returl']);
|
||||
}
|
||||
}
|
||||
|
||||
location_header($this->form['target_url']); // Redirect browser to initial page
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Can only return a valid username. If the username and password are not valid it will ask for new ones.
|
||||
protected function getValidUser(
|
||||
#[\SensitiveParameter]
|
||||
?string $username,
|
||||
#[\SensitiveParameter]
|
||||
?string $password) : string
|
||||
{
|
||||
if (!isset($this->form['password']) ||
|
||||
(($valid_username = auth()->validateUser($this->form['username'], $this->form['password'])) === false))
|
||||
{
|
||||
$this->authGet($this->form['target_url'], $this->form['returl'], get_vocab('unknown_user'));
|
||||
exit(); // unnecessary because authGet() exits, but just included for clarity
|
||||
}
|
||||
|
||||
return $valid_username;
|
||||
}
|
||||
|
||||
|
||||
protected function logonUser(string $username) : void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function logoffUser() : void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
// Displays the login form.
|
||||
// Will eventually return to $target_url with query string returl=$returl
|
||||
// If $error is set then an $error is printed.
|
||||
// If $raw is true then the message is not HTML escaped
|
||||
private function printLoginForm(string $action, ?string $target_url, ?string $returl, ?string $error=null, bool $raw=false) : void
|
||||
{
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
$form->setAttributes(array('class' => 'standard',
|
||||
'id' => 'logon',
|
||||
'action' => $action));
|
||||
|
||||
// Hidden inputs
|
||||
$hidden_inputs = array('returl' => $returl,
|
||||
'target_url' => $target_url,
|
||||
'action' => 'SetName');
|
||||
$form->addHiddenInputs($hidden_inputs);
|
||||
|
||||
// Now for the visible fields
|
||||
if (isset($error))
|
||||
{
|
||||
$p = new ElementP();
|
||||
$p->setText($error, false, $raw);
|
||||
$form->addElement($p);
|
||||
}
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('please_login'));
|
||||
|
||||
// The username field
|
||||
if (auth()->canValidateByEmail() && auth()->canValidateByUsername())
|
||||
{
|
||||
$tag = 'username_or_email';
|
||||
}
|
||||
elseif (auth()->canValidateByUsername())
|
||||
{
|
||||
$tag = 'users.name';
|
||||
}
|
||||
else
|
||||
{
|
||||
$tag = 'users.email';
|
||||
}
|
||||
|
||||
$placeholder = get_vocab($tag);
|
||||
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('user'))
|
||||
->setLabelAttributes(array('title' => $placeholder))
|
||||
->setControlAttributes(array('id' => 'username',
|
||||
'name' => 'username',
|
||||
'placeholder' => $placeholder,
|
||||
'required' => true,
|
||||
'autofocus' => true,
|
||||
'autocomplete' => 'username'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// The password field
|
||||
$field = new FieldInputPassword();
|
||||
$field->setLabel(get_vocab('users.password'))
|
||||
->setControlAttributes(array('id' => 'password',
|
||||
'name' => 'password',
|
||||
'autocomplete' => 'current-password'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
$form->addElement($fieldset);
|
||||
|
||||
// The submit button
|
||||
$fieldset = new ElementFieldset();
|
||||
$field = new FieldInputSubmit();
|
||||
$field->setControlAttributes(array('value' => get_vocab('login')));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
$form->addElement($fieldset);
|
||||
|
||||
if (auth()->canResetPassword())
|
||||
{
|
||||
$fieldset = new ElementFieldset();
|
||||
$field = new FieldDiv();
|
||||
$a = new ElementA();
|
||||
$a->setAttribute('href', multisite('reset_password.php'))
|
||||
->setText(get_vocab('lost_password'));
|
||||
$field->addControl($a);
|
||||
$fieldset->addElement($field);
|
||||
$form->addElement($fieldset);
|
||||
}
|
||||
|
||||
$form->render();
|
||||
|
||||
|
||||
|
||||
// Print footer and exit
|
||||
print_footer(true);
|
||||
}
|
||||
|
||||
|
||||
// Check we've got the right authentication type for the session scheme.
|
||||
// To be called for those session schemes which require the same
|
||||
// authentication type
|
||||
protected function checkTypeMatchesSession() : void
|
||||
{
|
||||
global $auth;
|
||||
|
||||
if ($auth['type'] !== $auth['session'])
|
||||
{
|
||||
$class = get_called_class();
|
||||
$message = "MRBS configuration error: $class needs \$auth['type'] set to '" . $auth['session'] . "'";
|
||||
die($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,491 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Auth\AuthFactory;
|
||||
use MRBS\Session\Session;
|
||||
use MRBS\Session\SessionFactory;
|
||||
|
||||
|
||||
// Convenience wrapper function to provide access to an Auth object
|
||||
function auth()
|
||||
{
|
||||
global $auth;
|
||||
|
||||
static $auth_obj = null;
|
||||
|
||||
if (is_null($auth_obj))
|
||||
{
|
||||
$auth_obj = AuthFactory::create($auth['type']);
|
||||
}
|
||||
|
||||
return $auth_obj;
|
||||
}
|
||||
|
||||
|
||||
// Convenience wrapper function to provide access to a Session object
|
||||
function session() : Session
|
||||
{
|
||||
global $auth;
|
||||
|
||||
static $session_obj = null;
|
||||
|
||||
if (is_null($session_obj))
|
||||
{
|
||||
$session_obj = SessionFactory::create($auth['session']);
|
||||
}
|
||||
|
||||
return $session_obj;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Checks if a page is open to users, using the config variables
|
||||
// $auth['only_admin_can_book'] and $auth['only_admin_can_book_before']
|
||||
function booking_level() : int
|
||||
{
|
||||
global $auth;
|
||||
|
||||
if ($auth['allow_anonymous_booking'])
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($auth['only_admin_can_book'])
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
elseif ($auth['only_admin_can_book_before'])
|
||||
{
|
||||
$go_live = strtotime($auth['only_admin_can_book_before']);
|
||||
if ($go_live === false)
|
||||
{
|
||||
$message = "Could not calculate time from '" . $auth['only_admin_can_book_before'] . "'.";
|
||||
trigger_error($message);
|
||||
return 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (time() >= $go_live) ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
// Gets the minimum user level required to access a page
|
||||
function get_page_level($page)
|
||||
{
|
||||
global $auth, $max_level;
|
||||
|
||||
// If you're resetting your password you won't be logged in and $auth['deny_public_access']
|
||||
// should not apply.
|
||||
if (in_array($page, array('reset_password.php', 'reset_password_handler.php')))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Otherwise ...
|
||||
switch ($page)
|
||||
{
|
||||
// These pages are open to the public by default as they only contain
|
||||
// read features.
|
||||
case 'help.php':
|
||||
case 'index.php':
|
||||
$result = 0;
|
||||
break;
|
||||
|
||||
// These pages reveal usernames, which could be of assistance to someone trying to
|
||||
// break into the system, so users are required to be logged in before viewing them.
|
||||
case 'search.php':
|
||||
$result = 1;
|
||||
break;
|
||||
|
||||
case 'view_entry.php':
|
||||
$result = ($auth['allow_anonymous_booking']) ? 0 : 1;
|
||||
break;
|
||||
|
||||
// These pages are set to have a minimum access level of 1 as ordinary users
|
||||
// should be able to access them because they will have read access and in some
|
||||
// cases write access for their own entries. Where necessary further checks are
|
||||
// made within the page to prevent ordinary users gaining access to admin features.
|
||||
case 'admin.php':
|
||||
case 'approve_entry_handler.php': // Ordinary users are allowed to remind admins
|
||||
case 'edit_message.php': // Booking admins can edit messages
|
||||
case 'edit_message_handler.php': // Booking admins can edit messages
|
||||
case 'edit_room.php': // Ordinary users can view room details
|
||||
case 'edit_users.php': // Ordinary users can edit their own details
|
||||
case 'pending.php': // Ordinary users can view their own entries
|
||||
case 'registration_handler.php': // Ordinary users can register for an event
|
||||
case 'usernames.php': // Ajax page for getting a list of users (booking admins can use this)
|
||||
$result = 1;
|
||||
break;
|
||||
|
||||
// These pages allow users to create and delete entries
|
||||
case 'check_slot.php': // Ajax page used by edit_entry.php
|
||||
case 'del_entry.php':
|
||||
case 'edit_entry.php':
|
||||
case 'edit_entry_handler.php':
|
||||
return booking_level();
|
||||
break;
|
||||
|
||||
// Everything else is for admins only
|
||||
default:
|
||||
$result = (isset($max_level)) ? $max_level : 2;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($auth['deny_public_access'])
|
||||
{
|
||||
$result = max($result, 1);
|
||||
}
|
||||
|
||||
// Can always access index.php when in kiosk mode
|
||||
if ($page == 'index.php' && is_kiosk_mode())
|
||||
{
|
||||
$result = 0;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/* getAuthorised($level)
|
||||
*
|
||||
* Check to see if the current user has a certain level of rights
|
||||
*
|
||||
* $level - The access level required
|
||||
* $returl - The URL to return to eventually
|
||||
*
|
||||
* Returns:
|
||||
* false - The user does not have the required access
|
||||
* true - The user has the required access
|
||||
*/
|
||||
function getAuthorised($level, $returl) : bool
|
||||
{
|
||||
// If the minimum level is zero (or not set) then they are
|
||||
// authorised, whoever they are
|
||||
if (empty($level))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise we need to check who they are
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
if(!isset($mrbs_user))
|
||||
{
|
||||
// Ask them to authenticate, if the session scheme supports it
|
||||
if (method_exists(session(), 'authGet'))
|
||||
{
|
||||
session()->authGet(null, $returl);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($mrbs_user->level >= $level);
|
||||
}
|
||||
|
||||
|
||||
/* checkAuthorised()
|
||||
*
|
||||
* Checks to see that a user is authorised to access a page.
|
||||
* If they are not, then shows an "Access Denied" message and exits.
|
||||
*
|
||||
*/
|
||||
function checkAuthorised($page, $just_check=false)
|
||||
{
|
||||
global $view, $view_all, $year, $month, $day, $area, $room;
|
||||
global $returl;
|
||||
|
||||
// Get the minimum authorisation level for this page
|
||||
$required_level = get_page_level($page);
|
||||
|
||||
if ($just_check)
|
||||
{
|
||||
if ($required_level == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
return (isset($mrbs_user) && ($mrbs_user->level >= $required_level));
|
||||
}
|
||||
|
||||
// Check that the user has this level
|
||||
if (getAuthorised($required_level, $returl))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we don't know the right date then use today's
|
||||
if (!isset($day) or !isset($month) or !isset($year))
|
||||
{
|
||||
$day = date('d');
|
||||
$month = date('m');
|
||||
$year = date('Y');
|
||||
}
|
||||
|
||||
if (empty($area))
|
||||
{
|
||||
$area = get_default_area();
|
||||
}
|
||||
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area, isset($room) ? $room : null);
|
||||
exit();
|
||||
}
|
||||
|
||||
|
||||
/* getWritable($creator, $room)
|
||||
*
|
||||
* Determines if the current user is able to modify an entry
|
||||
*
|
||||
* $creator - The creator of the entry
|
||||
* $rooms - The id(s) of the room(s) that the entries are in. Can
|
||||
* be a scalar or an array.
|
||||
* $all - Whether to check that the creator has write access
|
||||
* for all ($all=true) or just some ($all=false) of the
|
||||
* rooms.
|
||||
*
|
||||
* Returns:
|
||||
* false - The user does not have the required access
|
||||
* true - The user has the required access
|
||||
*/
|
||||
function getWritable($creator, $rooms=null, $all=true) : bool
|
||||
{
|
||||
if (is_array($rooms) && (count($rooms) > 0))
|
||||
{
|
||||
if ($all)
|
||||
{
|
||||
// We want the user to have write access for all the rooms,
|
||||
// so if for any one room they are not, then return false.
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
if (!getWritable($creator, $room))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want the user to have write access for at least one room,
|
||||
// so if there are no rooms for which they do, then return false.
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
if (getWritable($creator, $room))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($rooms) && !$all)
|
||||
{
|
||||
// Not yet supported. Could support it but need to decide what $rooms=null means.
|
||||
// Does it mean all rooms in the system or just all rooms in the current area?
|
||||
throw new \Exception('$rooms===null and $all===false not yet supported.');
|
||||
}
|
||||
|
||||
// You can't make bookings in rooms which are invisible
|
||||
if (!is_visible($rooms))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always allowed to modify your own stuff
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
if (isset($mrbs_user) && isset($creator) && (compare_usernames($creator, $mrbs_user->username) === 0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise you have to be a (booking) admin for this room
|
||||
if (is_book_admin($rooms))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unauthorised access
|
||||
return false;
|
||||
}
|
||||
|
||||
/* showAccessDenied()
|
||||
*
|
||||
* Displays an appropriate message when access has been denied
|
||||
*
|
||||
* Returns: Nothing
|
||||
*/
|
||||
function showAccessDenied($view=null, $view_all=null, $year=null, $month=null, $day=null, $area=null, $room=null)
|
||||
{
|
||||
global $server;
|
||||
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => isset($room) ? $room : null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
|
||||
// Wrap the contents in a <div> to help with styling. Not a very nice solution, but anyway.
|
||||
echo "<div>\n";
|
||||
echo "<h1>" . get_vocab("accessdenied") . "</h1>\n";
|
||||
echo "<p>" . get_vocab("norights") . "</p>\n";
|
||||
$referrer = session()->getReferrer();
|
||||
if (isset($referrer))
|
||||
{
|
||||
echo "<p>\n";
|
||||
echo "<a href=\"" . escape_html($referrer) . "\">\n" . get_vocab("returnprev") . "</a>\n";
|
||||
echo "</p>\n";
|
||||
}
|
||||
echo "</div>\n";
|
||||
|
||||
// Print footer and exit
|
||||
print_footer(true);
|
||||
}
|
||||
|
||||
|
||||
// Checks whether the current user has admin rights
|
||||
function is_admin() : bool
|
||||
{
|
||||
global $max_level;
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
$required_level = (isset($max_level) ? $max_level : 2);
|
||||
|
||||
return (isset($mrbs_user) && ($mrbs_user->level >= $required_level));
|
||||
}
|
||||
|
||||
|
||||
// Checks whether the current user has booking administration rights
|
||||
// for $rooms - ie is allowed to modify and delete other people's bookings
|
||||
// and to approve bookings.
|
||||
//
|
||||
// $rooms can be either a single scalar value or an array of room ids. The default
|
||||
// value for $rooms is all rooms. (At the moment $room is ignored, but is passed here
|
||||
// so that later MRBS can be enhanced to provide fine-grained permissions.)
|
||||
//
|
||||
// $all specifies whether the user must be a booking for all $rooms, or just some of
|
||||
// them, ie at least one.
|
||||
//
|
||||
// Returns: TRUE if the user is allowed has booking admin rights for
|
||||
// the room(s); otherwise FALSE
|
||||
function is_book_admin($rooms=null, $all=true) : bool
|
||||
{
|
||||
global $min_booking_admin_level;
|
||||
|
||||
if (is_array($rooms) && (count($rooms) > 0))
|
||||
{
|
||||
if ($all)
|
||||
{
|
||||
// We want the user to be a booking admin for all the rooms,
|
||||
// so if for any one room they are not, then return false.
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
if (!is_book_admin($room))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want the user to be a booking admin for at least one room,
|
||||
// so if there are no rooms for which they are, then return false.
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
if (is_book_admin($room))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($rooms) && !$all)
|
||||
{
|
||||
// Not yet supported. Could support it but need to decide what $rooms=null means.
|
||||
// Does it mean all rooms in the system or just all rooms in the current area?
|
||||
throw new \Exception('$rooms===null and $all===false not yet supported.');
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
return (isset($mrbs_user) && ($mrbs_user->level >= $min_booking_admin_level));
|
||||
}
|
||||
|
||||
|
||||
// Checks whether the current user has user editing rights
|
||||
function is_user_admin() : bool
|
||||
{
|
||||
global $min_user_editing_level;
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
return (isset($mrbs_user) && ($mrbs_user->level >= $min_user_editing_level));
|
||||
}
|
||||
|
||||
|
||||
// Checks whether a room is visible to the current user
|
||||
// Doesn't do anything at the moment, but allows for customisation or future development
|
||||
function is_visible($room) : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Checks whether a user is allowed to register other users for events
|
||||
function can_register_others($room_id=null) : bool
|
||||
{
|
||||
global $auth;
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
if (!isset($mrbs_user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return $auth['users_can_register_others'] || is_book_admin($room_id);
|
||||
}
|
||||
|
||||
|
||||
// Checks whether the current user can see others' email addresses
|
||||
function can_see_email_addresses() : bool
|
||||
{
|
||||
global $auth, $is_private_field;
|
||||
|
||||
// Admins can see everything
|
||||
if (is_admin())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
// Don't expose email addresses to the public
|
||||
if (!isset($mrbs_user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// MRBS must be configured for logged-in users to see others' details
|
||||
if ($auth['only_admin_can_see_other_users'])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise the email field in the users table must not be private
|
||||
return (!auth()->canCreateUsers() || empty($is_private_field['users.email']));
|
||||
}
|
||||
Reference in New Issue
Block a user