包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
@@ -0,0 +1,735 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use MRBS\Form\ElementInputDate;
|
||||
use MRBS\Form\ElementInputSearch;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
|
||||
function print_head(bool $simple=false) : void
|
||||
{
|
||||
global $refresh_rate;
|
||||
|
||||
echo "<head>\n";
|
||||
|
||||
echo "<meta charset=\"" . Language::MRBS_CHARSET . "\">\n";
|
||||
|
||||
// Set IE=edge so that IE10 will display MRBS properly, even if compatibility mode is used
|
||||
// on the browser. If we don't do this then MRBS will treat IE10 as an unsupported browser
|
||||
// when compatibility mode is turned on, potentially confusing users who may have forgotten
|
||||
// that they are using compatibility mode. Unfortunately we can't set IE=edge in the header,
|
||||
// which is where we would normally do it, because then we won't be able to detect IE9 using
|
||||
// conditional comments. So we have to do it in a <meta> tag, after the conditional comments
|
||||
// around the <html> tags.
|
||||
echo "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n";
|
||||
|
||||
// Improve scaling on mobile devices
|
||||
echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n";
|
||||
|
||||
if (!$simple)
|
||||
{
|
||||
// Add the CSRF token so that JavaScript can use it
|
||||
echo "<meta name=\"csrf_token\" content=\"" . escape_html(Form::getToken()) . "\">\n";
|
||||
}
|
||||
|
||||
echo "<meta name=\"robots\" content=\"noindex, nofollow, noarchive\">\n";
|
||||
|
||||
if (($refresh_rate != 0) && (this_page(false, '.php') == 'index'))
|
||||
{
|
||||
// If we're using JavaScript we'll do the refresh by getting a new
|
||||
// table using Ajax requests, which means we only have to download
|
||||
// the table not the whole page each time
|
||||
echo "<noscript>\n";
|
||||
echo "<meta http-equiv=\"Refresh\" content=\"$refresh_rate\">\n";
|
||||
echo "</noscript>\n";
|
||||
}
|
||||
|
||||
echo "<title>" . get_vocab("mrbs") . "</title>\n";
|
||||
|
||||
require_once MRBS_ROOT . "/style.inc";
|
||||
|
||||
if (!$simple)
|
||||
{
|
||||
require_once MRBS_ROOT . "/js.inc";
|
||||
}
|
||||
|
||||
echo "</head>\n";
|
||||
}
|
||||
|
||||
|
||||
// Print the basic site information. This function is used for all headers, including
|
||||
// the simple header, and so mustn't require any database access.
|
||||
function print_header_site_info() : void
|
||||
{
|
||||
global $mrbs_company,
|
||||
$mrbs_company_url,
|
||||
$mrbs_company_logo,
|
||||
$mrbs_company_more_info;
|
||||
|
||||
// Company logo, with a link to the company
|
||||
if (!empty($mrbs_company_logo))
|
||||
{
|
||||
echo "<div class=\"logo\">\n";
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo '<a href="' . escape_html($mrbs_company_url) . '">';
|
||||
}
|
||||
// Suppress error messages in case the logo is a URL, in which case getimagesize() can
|
||||
// fail for any number of reasons, eg (a) allow_url_fopen is not enabled in php.ini or
|
||||
// (b) "SSL operation failed with code 1. OpenSSL Error messages: error:1416F086:SSL
|
||||
// routines:tls_process_server_certificate:certificate verify failed". As the image
|
||||
// size is not essential we'll just carry on.
|
||||
$logo_size = @getimagesize($mrbs_company_logo);
|
||||
echo '<img src="' . $mrbs_company_logo . '"';
|
||||
echo ' alt="' . escape_html($mrbs_company) . '"';
|
||||
if (is_array($logo_size))
|
||||
{
|
||||
echo ' ' . $logo_size[3];
|
||||
}
|
||||
echo '>';
|
||||
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo "</a>\n";
|
||||
}
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
// Company name, any extra company info and MRBS
|
||||
echo "<div class=\"company\">\n";
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo '<a href="' . escape_html($mrbs_company_url) . '">';
|
||||
}
|
||||
echo '<span>' . escape_html($mrbs_company) . '</span>';
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo "</a>\n";
|
||||
}
|
||||
if (!empty($mrbs_company_more_info))
|
||||
{
|
||||
// Do not put $mrbs_company_more_info through escape_html() as it is
|
||||
// trusted and allowed to contain HTML.
|
||||
echo "<span class=\"company_more_info\">$mrbs_company_more_info</span>\n";
|
||||
}
|
||||
echo '<a href="' . escape_html(multisite('index.php')) . '">' . get_vocab('mrbs') . "</a>\n";
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
function print_goto_date(array $context) : void
|
||||
{
|
||||
global $multisite, $site;
|
||||
|
||||
if (!checkAuthorised('index.php', true))
|
||||
{
|
||||
// Don't show the goto box if the user isn't allowed to view the calendar
|
||||
return;
|
||||
}
|
||||
|
||||
$form = new Form();
|
||||
|
||||
$form_id = 'form_nav';
|
||||
|
||||
$form->setAttributes(array('id' => $form_id,
|
||||
'class' => 'js_hidden',
|
||||
'action' => multisite('index.php')))
|
||||
->addHiddenInput('view', $context['view']);
|
||||
|
||||
if (isset($context['area']))
|
||||
{
|
||||
$form->addHiddenInput('area', $context['area']);
|
||||
}
|
||||
|
||||
if (isset($room))
|
||||
{
|
||||
$form->addHiddenInput('room', $context['room']);
|
||||
}
|
||||
|
||||
if ($multisite && isset($site) && ($site !== ''))
|
||||
{
|
||||
$form->addHiddenInput('site', $site);
|
||||
}
|
||||
|
||||
$input = new ElementInputDate();
|
||||
// Add the 'navigation' class so that the JavaScript knows it can use hidden days
|
||||
$input->setAttributes(array(
|
||||
'name' => 'page_date',
|
||||
'value' => format_iso_date($context['year'], $context['month'], $context['day']),
|
||||
'class' => 'navigation',
|
||||
'aria-label' => get_vocab('goto'),
|
||||
'required' => true,
|
||||
'data-submit' => $form_id)
|
||||
);
|
||||
|
||||
$form->addElement($input);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttribute('value', get_vocab('goto'));
|
||||
$form->addElement($submit);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function print_outstanding(string $query) : void
|
||||
{
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
if (!isset($mrbs_user))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Provide a link to the list of bookings awaiting approval
|
||||
// (if there are any enabled areas where we require bookings to be approved)
|
||||
$approval_somewhere = some_area('approval_enabled', TRUE);
|
||||
if ($approval_somewhere && ($mrbs_user->level > 0))
|
||||
{
|
||||
$n_outstanding = get_entries_n_outstanding($mrbs_user);
|
||||
|
||||
$class = 'notification';
|
||||
|
||||
if ($n_outstanding > 0)
|
||||
{
|
||||
$class .= ' attention';
|
||||
}
|
||||
|
||||
$url = 'pending.php';
|
||||
if ($query !== '')
|
||||
{
|
||||
$url .= "?$query";
|
||||
}
|
||||
echo '<a href="' . escape_html(multisite($url)) . '"' .
|
||||
" class=\"$class\"" .
|
||||
' title="' . get_vocab('outstanding', $n_outstanding) .
|
||||
"\">$n_outstanding</a>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function print_menu_items(string $query) : void
|
||||
{
|
||||
global $auth, $kiosk_mode_enabled;
|
||||
|
||||
$menu_items = array('help' => 'help.php',
|
||||
'report' => 'report.php',
|
||||
'import' => 'import.php');
|
||||
|
||||
if ($kiosk_mode_enabled)
|
||||
{
|
||||
$menu_items['kiosk'] = 'kiosk.php';
|
||||
}
|
||||
|
||||
$menu_items['rooms'] = 'admin.php';
|
||||
|
||||
if (auth()->canCreateUsers())
|
||||
{
|
||||
$menu_items['user_list'] = 'edit_users.php';
|
||||
}
|
||||
|
||||
// 等保整改:自助修改密码入口(对已登录及未登录用户均显示)
|
||||
$menu_items['change_password'] = 'change_password.php';
|
||||
|
||||
foreach ($menu_items as $token => $page)
|
||||
{
|
||||
// Only print menu items for which the user is allowed to access the page
|
||||
if (checkAuthorised($page, true))
|
||||
{
|
||||
$url = $page;
|
||||
if ($query !== '')
|
||||
{
|
||||
$url .= "?$query";
|
||||
}
|
||||
echo '<a href="' . escape_html(multisite($url)) . '">' . get_vocab($token) . "</a>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function print_search(array $context) : void
|
||||
{
|
||||
if (!checkAuthorised('search.php', true))
|
||||
{
|
||||
// Don't show the search box if the user isn't allowed to search
|
||||
return;
|
||||
}
|
||||
|
||||
echo "<div>\n";
|
||||
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$form->setAttributes(array(
|
||||
'id' => 'header_search',
|
||||
'action' => multisite('search.php'))
|
||||
)
|
||||
->addHiddenInputs(array(
|
||||
'view' => $context['view'],
|
||||
'year' => $context['year'],
|
||||
'month' => $context['month'],
|
||||
'day' => $context['day'],
|
||||
'from_date' => format_iso_date($context['year'], $context['month'], $context['day'])
|
||||
)
|
||||
);
|
||||
|
||||
if (!empty($context['area']))
|
||||
{
|
||||
$form->addHiddenInput('area', $context['area']);
|
||||
}
|
||||
if (!empty($context['room']))
|
||||
{
|
||||
$form->addHiddenInput('room', $context['room']);
|
||||
}
|
||||
|
||||
$input = new ElementInputSearch();
|
||||
$search_vocab = get_vocab('search');
|
||||
|
||||
$input->setAttributes(array('name' => 'search_str',
|
||||
'placeholder' => $search_vocab,
|
||||
'aria-label' => $search_vocab,
|
||||
'required' => true));
|
||||
|
||||
$form->addElement($input);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array('value' => get_vocab('search_button'),
|
||||
'class' => 'js_none'));
|
||||
$form->addElement($submit);
|
||||
|
||||
$form->render();
|
||||
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
// Generate the username link, which gives a report on the user's upcoming bookings.
|
||||
function print_report_link(User $user) : void
|
||||
{
|
||||
// If possible, provide a link to the Report page, otherwise the Search page
|
||||
// and if that's not possible just print the username with no link. (Note that
|
||||
// the Search page isn't the perfect solution because it searches for any bookings
|
||||
// containing the search string, not just those created by the user.)
|
||||
if (checkAuthorised('report.php', true))
|
||||
{
|
||||
$attributes = array('action' => multisite('report.php'));
|
||||
$hidden_inputs = array('phase' => '2',
|
||||
'creatormatch' => $user->username);
|
||||
}
|
||||
elseif (checkAuthorised('search.php', true))
|
||||
{
|
||||
$attributes = array('action' => multisite('search.php'));
|
||||
$date_now = new DateTime();
|
||||
$hidden_inputs = array(
|
||||
'search_str' => $user->username,
|
||||
'from_date' => $date_now->getISODate()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
echo '<span>' . escape_html($user->display_name) . '</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
// We're authorised for either Report or Search so print the form.
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes['id'] = 'show_my_entries';
|
||||
$form->setAttributes($attributes)
|
||||
->addHiddenInputs($hidden_inputs);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array('title' => get_vocab('show_my_entries'),
|
||||
'value' => $user->display_name));
|
||||
$form->addElement($submit);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function print_logonoff_button(array $params, string $value) : void
|
||||
{
|
||||
$form = new Form($params['method']);
|
||||
$form->setAttributes(array('action' => $params['action']));
|
||||
|
||||
// A Get method will replace the query string in the action URL with a query
|
||||
// string made up of the hidden inputs. So put any parameters in the action
|
||||
// query string into hidden inputs.
|
||||
if ($params['method'] == Form::METHOD_GET)
|
||||
{
|
||||
$query_string = parse_url($params['action'], PHP_URL_QUERY);
|
||||
if (isset($query_string))
|
||||
{
|
||||
parse_str($query_string, $query_parameters);
|
||||
$form->addHiddenInputs($query_parameters);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the hidden fields
|
||||
if (isset($params['hidden_inputs']))
|
||||
{
|
||||
$form->addHiddenInputs($params['hidden_inputs']);
|
||||
}
|
||||
|
||||
// The submit button
|
||||
$element = new ElementInputSubmit();
|
||||
$element->setAttribute('value', $value);
|
||||
$form->addElement($element);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function print_logon() : void
|
||||
{
|
||||
if (method_exists(session(), 'getLogonFormParams'))
|
||||
{
|
||||
$form_params = session()->getLogonFormParams();
|
||||
if (isset($form_params))
|
||||
{
|
||||
print_logonoff_button($form_params, get_vocab('login'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function print_logoff() : void
|
||||
{
|
||||
if (method_exists(session(), 'getLogoffFormParams'))
|
||||
{
|
||||
$form_params = session()->getLogoffFormParams();
|
||||
if (isset($form_params))
|
||||
{
|
||||
print_logonoff_button($form_params, get_vocab('logoff'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// $context is an associative array indexed by 'view', 'view_all', 'year', 'month', 'day', 'area' and 'room'.
|
||||
// When $omit_login is true the Login link is omitted.
|
||||
function print_banner(?array $context, $simple=false, $omit_login=false) : void
|
||||
{
|
||||
global $kiosk_QR_code, $auth;
|
||||
|
||||
echo '<header class="banner' . (($simple) ? ' simple' : '') . "\">\n";
|
||||
|
||||
$vars = array();
|
||||
|
||||
if (isset($context['view']))
|
||||
{
|
||||
$vars['view'] = $context['view'];
|
||||
}
|
||||
if (isset($context['year']) && isset($context['month']) && isset($context['day']))
|
||||
{
|
||||
$vars['page_date'] = format_iso_date($context['year'], $context['month'], $context['day']);
|
||||
}
|
||||
if (isset($context['area']))
|
||||
{
|
||||
$vars['area'] = $context['area'];
|
||||
}
|
||||
if (isset($context['room']))
|
||||
{
|
||||
$vars['room'] = $context['room'];
|
||||
}
|
||||
|
||||
$query = http_build_query($vars, '', '&');
|
||||
|
||||
print_header_site_info();
|
||||
|
||||
if (!$simple)
|
||||
{
|
||||
echo "<nav class=\"container\">\n";
|
||||
|
||||
echo "<nav>\n";
|
||||
|
||||
echo "<nav class=\"menu\">\n";
|
||||
print_menu_items($query);
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "<nav class=\"logon\">\n";
|
||||
print_outstanding($query);
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
// The empty string username is a special case when using anonymous booking
|
||||
if (isset($mrbs_user) && (!$auth['allow_anonymous_booking'] || ($mrbs_user->username !== '')))
|
||||
{
|
||||
print_report_link($mrbs_user);
|
||||
print_logoff();
|
||||
}
|
||||
elseif (!$omit_login)
|
||||
{
|
||||
print_logon();
|
||||
}
|
||||
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "<nav>\n";
|
||||
print_goto_date($context);
|
||||
print_search($context);
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "</nav>\n";
|
||||
|
||||
// Add in a QR code for kiosk mode
|
||||
// (The QR code library requires PHP 7.4 or greater and the mbstring extension)
|
||||
if (isset($context['kiosk']) &&
|
||||
$kiosk_QR_code &&
|
||||
(version_compare(PHP_VERSION, '7.4') >= 0) &&
|
||||
//Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
|
||||
defined('MB_CASE_UPPER'))
|
||||
{
|
||||
$url = multisite(url_base() . "/index.php?$query");
|
||||
echo '<nav class="qr" title="' . escape_html($url) . "\">\n";
|
||||
$options = new QROptions([
|
||||
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
|
||||
'imageBase64' => false,
|
||||
]);
|
||||
$qrcode = new QRCode($options);
|
||||
echo $qrcode->render($url);
|
||||
echo "</nav>\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</header>\n";
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Print a message which will only be displayed (thanks to CSS) if the user is
|
||||
// using an unsupported browser.
|
||||
function print_unsupported_message(?array $context) : void
|
||||
{
|
||||
echo "<div class=\"unsupported_message\">\n";
|
||||
print_banner($context, true);
|
||||
echo "<div class=\"contents\">\n";
|
||||
echo "<p>" . get_vocab('browser_not_supported', get_vocab('mrbs_abbr')) . "</p>\n";
|
||||
echo "</div>\n";
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
// Print the page header
|
||||
// $context is an associative array indexed by 'view', 'view_all', 'year', 'month', 'day', 'area' and 'room',
|
||||
// any of which can be NULL.
|
||||
// If $simple is true, then just print a simple header that doesn't require any database
|
||||
// access or JavaScript (useful for fatal errors and database upgrades).
|
||||
// When $omit_login is true the Login link is omitted.
|
||||
function print_theme_header(?array $context=null, bool $simple=false, bool $omit_login=false) : void
|
||||
{
|
||||
global $multisite, $site, $default_view, $default_view_all, $view_week_number, $style_weekends, $watermark_enabled;
|
||||
|
||||
if ($simple)
|
||||
{
|
||||
$data = array();
|
||||
$classes = array();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the context values if they haven't been given
|
||||
if (!isset($context))
|
||||
{
|
||||
$context = array();
|
||||
}
|
||||
|
||||
if (empty($context['area']))
|
||||
{
|
||||
$context['area'] = get_default_area();
|
||||
}
|
||||
|
||||
if (empty($context['room']))
|
||||
{
|
||||
$context['room'] = get_default_room($context['area']);
|
||||
}
|
||||
|
||||
if (!isset($context['view']))
|
||||
{
|
||||
$context['view'] = (isset($default_view)) ? $default_view : 'day';
|
||||
}
|
||||
|
||||
if (!isset($context['view_all']))
|
||||
{
|
||||
$context['view_all'] = (isset($default_view_all)) ? $default_view_all : true;
|
||||
}
|
||||
|
||||
// Need to set the timezone before we can use date()
|
||||
// This will set the correct timezone for the area
|
||||
get_area_settings($context['area']);
|
||||
|
||||
// If we don't know the right date then use today's
|
||||
if (!isset($context['year']))
|
||||
{
|
||||
$context['year'] = (int) date('Y');
|
||||
}
|
||||
|
||||
if (!isset($context['month']))
|
||||
{
|
||||
$context['month'] = (int) date('m');
|
||||
}
|
||||
|
||||
if (!isset($context['day']))
|
||||
{
|
||||
$context['day'] = (int) date('d');
|
||||
}
|
||||
|
||||
// Get the form token now, before any headers are sent, in case we are using the 'cookie'
|
||||
// session scheme. Otherwise we won't be able to store the Form token.
|
||||
Form::getToken();
|
||||
|
||||
$page = this_page(false, '.php');
|
||||
|
||||
// Put some data attributes in the body element for the benefit of JavaScript. Note that we
|
||||
// cannot use these PHP variables directly in the JavaScript files as those files are cached.
|
||||
|
||||
// Get the language preferences
|
||||
$lang_preferences = Language::getInstance()->getPreferences();
|
||||
// Add to the beginning of the list the best fit locale (which may not necessarily have been the first choice)
|
||||
array_unshift($lang_preferences, mb_strtolower(Language::getInstance()->getWebLocale()));
|
||||
// Remove duplicates and renumber keys
|
||||
$lang_preferences = array_values(array_unique($lang_preferences));
|
||||
|
||||
$data = [
|
||||
'view' => $context['view'],
|
||||
'view_all' => $context['view_all'],
|
||||
'area' => $context['area'],
|
||||
'room' => $context['room'],
|
||||
'page' => $page,
|
||||
'page-date' => format_iso_date($context['year'], $context['month'], $context['day']),
|
||||
'is-admin' => (is_admin()) ? 'true' : 'false',
|
||||
'is-book-admin' => (is_book_admin()) ? 'true' : 'false',
|
||||
'lang-prefs' => json_encode($lang_preferences)
|
||||
];
|
||||
|
||||
if ($multisite && isset($site) && ($site !== ''))
|
||||
{
|
||||
$data['site'] = $site;
|
||||
}
|
||||
|
||||
if (isset($context['kiosk']))
|
||||
{
|
||||
$data['kiosk'] = $context['kiosk'];
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
if (isset($mrbs_user))
|
||||
{
|
||||
$data['username'] = $mrbs_user->username;
|
||||
}
|
||||
|
||||
// We need $timetohighlight for the day and week views
|
||||
$timetohighlight = get_form_var('timetohighlight', 'int');
|
||||
if (isset($timetohighlight))
|
||||
{
|
||||
$data['timetohighlight'] = $timetohighlight;
|
||||
}
|
||||
|
||||
// Put the filename in as a class to aid styling.
|
||||
$classes[] = $page;
|
||||
// And if the user is logged in, add another class to aid styling
|
||||
if (isset($mrbs_user))
|
||||
{
|
||||
$classes[] = 'logged_in';
|
||||
}
|
||||
|
||||
// To help styling
|
||||
if ($view_week_number)
|
||||
{
|
||||
$classes[] = 'view_week_number';
|
||||
}
|
||||
if ($style_weekends)
|
||||
{
|
||||
$classes[] = 'style_weekends';
|
||||
}
|
||||
|
||||
// ===== 等保整改:口令到期 / 首次登录 → 强制跳转改密页(改密页自身除外) =====
|
||||
if (isset($mrbs_user) && !empty($_SESSION['mrbs_force_pwd_change']) &&
|
||||
($page !== 'change_password'))
|
||||
{
|
||||
// 关闭会话写入后重定向,确保强制改密标记已持久化
|
||||
session_write_close();
|
||||
location_header('change_password.php?target_url=' . urlencode(this_page(true)));
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$headers = array("Content-Type: text/html; charset=" . Language::MRBS_CHARSET);
|
||||
http_headers($headers);
|
||||
|
||||
echo DOCTYPE . "\n";
|
||||
|
||||
// We produce two <html> tags: one for versions of IE that we don't support and one for all
|
||||
// other browsers. This enables us to use CSS to hide and show the appropriate text.
|
||||
$mrbs_lang = Language::getInstance()->getWebLang();
|
||||
echo "<!--[if lte IE 9]>\n";
|
||||
echo "<html lang=\"" . escape_html($mrbs_lang) . "\" class=\"unsupported_browser\">\n";
|
||||
echo "<![endif]-->\n";
|
||||
echo "<!--[if (!IE)|(gt IE 9)]><!-->\n";
|
||||
echo "<html lang=\"" . escape_html($mrbs_lang) . "\">\n";
|
||||
echo "<!--<![endif]-->\n";
|
||||
|
||||
print_head($simple);
|
||||
|
||||
echo '<body class="' . escape_html(implode(' ', $classes)) . '"';
|
||||
foreach ($data as $key => $value)
|
||||
{
|
||||
if (isset($value))
|
||||
{
|
||||
// Convert booleans to 0 or 1
|
||||
if (is_bool($value))
|
||||
{
|
||||
$value = (int)$value;
|
||||
}
|
||||
echo " data-$key=\"" . escape_html($value) . '"';
|
||||
}
|
||||
}
|
||||
echo ">\n";
|
||||
|
||||
// ===== 等保整改:屏幕水印(防截图/拍照泄密溯源;config.inc.php $watermark_enabled 控制开关) =====
|
||||
if (!$simple && isset($data['username']) && !empty($watermark_enabled))
|
||||
{
|
||||
$wm_user = htmlspecialchars((string)$data['username']);
|
||||
$wm_ip = htmlspecialchars((string)($_SERVER['REMOTE_ADDR'] ?? '-'));
|
||||
echo <<<WM_EOT
|
||||
<div id="screen_watermark" data-user="{$wm_user}" data-ip="{$wm_ip}" aria-hidden="true"></div>
|
||||
<style>
|
||||
#screen_watermark{position:fixed;inset:0;z-index:99999;pointer-events:none;overflow:hidden;opacity:.09}
|
||||
#screen_watermark span{position:absolute;font-size:15px;line-height:1;color:#000;white-space:nowrap;user-select:none;transform:rotate(-28deg);letter-spacing:1px}
|
||||
</style>
|
||||
<script>
|
||||
(function(){
|
||||
var w=document.getElementById('screen_watermark');if(!w)return;
|
||||
var u=w.getAttribute('data-user')||'';var ip=w.getAttribute('data-ip')||'';if(!u)return;
|
||||
function pad(n){return (n<10)?'0'+n:''+n;}
|
||||
function ts(){var d=new Date();return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+' '+pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds());}
|
||||
function tile(){return u+' '+ip+' '+ts();}
|
||||
function draw(){
|
||||
var cw=Math.max(6,Math.ceil(window.innerWidth/320)),ch=Math.max(4,Math.ceil(window.innerHeight/160));
|
||||
var t=tile();
|
||||
for(var r=0;r<ch;r++){for(var c=0;c<cw;c++){var s=document.createElement('span');s.textContent=t;
|
||||
s.style.left=(c*320+((r%2)*80))+'px';s.style.top=(r*160)+'px';w.appendChild(s);}}
|
||||
}
|
||||
draw();
|
||||
setInterval(function(){var t=tile();var s=w.querySelectorAll('span');for(var i=0;i<s.length;i++){s[i].textContent=t;}},1000);
|
||||
})();
|
||||
</script>
|
||||
WM_EOT;
|
||||
}
|
||||
|
||||
print_unsupported_message($context);
|
||||
|
||||
print_banner($context, $simple, $omit_login);
|
||||
|
||||
// This <div> should really be moved out of here so that we can always see
|
||||
// the matching closing </div>
|
||||
echo "<div class=\"contents\">\n";
|
||||
|
||||
|
||||
} // end of print_theme_header()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# 等保整改:审计日志目录禁止 Web 直接访问
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</IfModule>
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Audit;
|
||||
use MRBS\Form\Element;
|
||||
use MRBS\Form\ElementFieldset;
|
||||
use MRBS\Form\ElementP;
|
||||
use MRBS\Form\FieldDiv;
|
||||
use MRBS\Form\FieldInputPassword;
|
||||
use MRBS\Form\FieldInputSubmit;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
require "defaultincludes.inc";
|
||||
|
||||
|
||||
// ===== 等保2.0二级整改:自助修改密码页 =====
|
||||
// - 仅限已登录用户(未登录自动引导到登录页,登录后回到本页)
|
||||
// - 校验当前密码 → 新密码复杂度($pwd_policy)→ 两次一致 → 新旧不同
|
||||
// - 成功后写 password_changed_at(供 90 天有效期计算)并清除强制改密标记
|
||||
// - 所有成功/失败动作写入安全审计日志(Audit)
|
||||
|
||||
|
||||
function generate_change_password_form(?string $error = null, string $target_url = 'index.php') : void
|
||||
{
|
||||
global $pwd_policy;
|
||||
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
$form->setAttributes(array(
|
||||
'class' => 'standard',
|
||||
'id' => 'change_password',
|
||||
'action' => multisite('change_password.php')
|
||||
));
|
||||
|
||||
$form->addHiddenInputs(array(
|
||||
'action' => 'change_password',
|
||||
'target_url' => $target_url
|
||||
));
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('change_password'));
|
||||
|
||||
// 顶部提示 / 错误消息
|
||||
$field = new FieldDiv();
|
||||
$p = new ElementP();
|
||||
|
||||
if (isset($error))
|
||||
{
|
||||
switch ($error)
|
||||
{
|
||||
case 'old_pwd_invalid':
|
||||
$p->setText(get_vocab('old_pwd_invalid'));
|
||||
break;
|
||||
case 'pwd_not_match':
|
||||
$p->setText(get_vocab('passwords_not_eq'));
|
||||
break;
|
||||
case 'pwd_same':
|
||||
$p->setText(get_vocab('pwd_same_as_old'));
|
||||
break;
|
||||
case 'pwd_invalid':
|
||||
$p->setText(get_vocab('password_invalid'));
|
||||
break;
|
||||
default:
|
||||
$p->setText(get_vocab('unknown_user'));
|
||||
break;
|
||||
}
|
||||
$p->setAttribute('class', 'error');
|
||||
$field->addControlElement($p);
|
||||
|
||||
// 策略不满足时列出具体规则
|
||||
if (($error == 'pwd_invalid') && isset($pwd_policy))
|
||||
{
|
||||
$ul = new Element('ul');
|
||||
$ul->setAttribute('class', 'error');
|
||||
foreach ($pwd_policy as $rule => $value)
|
||||
{
|
||||
if ($value != 0)
|
||||
{
|
||||
$li = new Element('li');
|
||||
$li->setText(get_vocab('policy_' . $rule, $value));
|
||||
$ul->addElement($li);
|
||||
}
|
||||
}
|
||||
$field->addControlElement($ul);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 提示(强制改密或常规自助修改)
|
||||
$p->setText(get_vocab('pwd_expired_msg'));
|
||||
$field->addControlElement($p);
|
||||
}
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// 当前密码
|
||||
$field = new FieldInputPassword();
|
||||
$field->setLabel(get_vocab('current_password'))
|
||||
->setControlAttributes(array('id' => 'password_old',
|
||||
'name' => 'password_old',
|
||||
'autocomplete' => 'current-password',
|
||||
'required' => true,
|
||||
'autofocus' => true));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// 新密码(输入两次)
|
||||
$labels = array(get_vocab('new_password'), get_vocab('confirm_password'));
|
||||
for ($i = 0; $i < 2; $i++)
|
||||
{
|
||||
$field = new FieldInputPassword();
|
||||
$field->setLabel($labels[$i])
|
||||
->setControlAttributes(array('id' => "password$i",
|
||||
'name' => "password$i",
|
||||
'autocomplete' => 'new-password',
|
||||
'required' => true));
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
// 口令策略说明
|
||||
if (isset($pwd_policy))
|
||||
{
|
||||
$field = new FieldDiv();
|
||||
$p = new ElementP();
|
||||
$p->setText(get_vocab('pwd_must_contain'));
|
||||
$field->addControlElement($p);
|
||||
$ul = new Element('ul');
|
||||
$ul->setAttribute('id', 'pwd_policy');
|
||||
foreach ($pwd_policy as $rule => $value)
|
||||
{
|
||||
if ($value != 0)
|
||||
{
|
||||
$li = new Element('li');
|
||||
$li->setText(get_vocab('policy_' . $rule, $value));
|
||||
$ul->addElement($li);
|
||||
}
|
||||
}
|
||||
$field->addControlElement($ul);
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
$form->addElement($fieldset);
|
||||
|
||||
// 提交按钮
|
||||
$fieldset = new ElementFieldset();
|
||||
$field = new FieldInputSubmit();
|
||||
$field->setControlAttributes(array('value' => get_vocab('change_password')));
|
||||
$fieldset->addElement($field);
|
||||
$form->addElement($fieldset);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function generate_change_password_success(string $target_url) : void
|
||||
{
|
||||
echo "<h2>" . get_vocab('change_password') . "</h2>\n";
|
||||
echo "<p class=\"notice\">" . get_vocab('password_changed') . "</p>\n";
|
||||
echo '<p><a href="' . htmlspecialchars(multisite($target_url)) . '">' . get_vocab('back') . "</a></p>\n";
|
||||
}
|
||||
|
||||
|
||||
// ===== 主流程 =====
|
||||
|
||||
// 必须是已登录用户
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
if (!isset($mrbs_user))
|
||||
{
|
||||
// 未登录:引导到登录页,登录成功后回到本页
|
||||
session()->authGet(null, 'change_password.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 跳转目标(仅允许站内相对 URL)
|
||||
$target_url = get_form_var('target_url', 'url_local', null, INPUT_GET);
|
||||
if (!isset($target_url) || ($target_url == ''))
|
||||
{
|
||||
$target_url = 'index.php';
|
||||
}
|
||||
// 防止把改密页自身作为跳转目标(避免循环)
|
||||
if ($target_url == 'change_password.php')
|
||||
{
|
||||
$target_url = 'index.php';
|
||||
}
|
||||
|
||||
// 处理提交(action 只从 POST 读取,且必须通过 CSRF 校验)
|
||||
$action = get_form_var('action', 'string', null, INPUT_POST);
|
||||
|
||||
if (isset($action) && ($action == 'change_password'))
|
||||
{
|
||||
Form::checkToken();
|
||||
|
||||
$old_password = get_form_var('password_old', 'string', null, INPUT_POST);
|
||||
$password0 = get_form_var('password0', 'string', null, INPUT_POST);
|
||||
$password1 = get_form_var('password1', 'string', null, INPUT_POST);
|
||||
|
||||
$post_target = get_form_var('target_url', 'url_local', null, INPUT_POST);
|
||||
if (isset($post_target) && ($post_target != ''))
|
||||
{
|
||||
$target_url = $post_target;
|
||||
}
|
||||
|
||||
$error = null;
|
||||
|
||||
// 1. 校验当前密码
|
||||
// (注意:validateUser 失败会计入失败次数;连续 5 次错误当前账号将被临时锁定,
|
||||
// 与登录通道行为一致,属预期安全设计)
|
||||
if (($old_password === null) || ($old_password === '') ||
|
||||
!auth()->validateUser($mrbs_user->username, $old_password))
|
||||
{
|
||||
$error = 'old_pwd_invalid';
|
||||
Audit::log('PWD_CHANGE_FAIL', $mrbs_user->username, 'old password incorrect');
|
||||
}
|
||||
// 2. 两次输入一致
|
||||
elseif ($password0 !== $password1)
|
||||
{
|
||||
$error = 'pwd_not_match';
|
||||
}
|
||||
// 3. 符合复杂度策略
|
||||
elseif (($password0 === null) || ($password0 === '') ||
|
||||
!auth()->validatePassword($password0))
|
||||
{
|
||||
$error = 'pwd_invalid';
|
||||
}
|
||||
// 4. 新旧密码不同
|
||||
elseif ($password0 === $old_password)
|
||||
{
|
||||
$error = 'pwd_same';
|
||||
}
|
||||
else
|
||||
{
|
||||
// 成功:更新口令并记录修改时间
|
||||
auth()->updatePassword($mrbs_user->username, $password0);
|
||||
Audit::log('PWD_CHANGE', $mrbs_user->username, 'self-service change');
|
||||
|
||||
// 清除“强制改密”标记(须在 session_write_close 前完成)
|
||||
unset($_SESSION['mrbs_force_pwd_change']);
|
||||
session_write_close();
|
||||
|
||||
location_header('change_password.php?result=ok&target_url=' . urlencode($target_url));
|
||||
exit;
|
||||
}
|
||||
|
||||
// 校验失败:回到表单显示错误(PRG 模式,防止表单重复提交)
|
||||
location_header('change_password.php?error=' . urlencode($error) . '&target_url=' . urlencode($target_url));
|
||||
exit;
|
||||
}
|
||||
|
||||
// ===== 渲染页面 =====
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => isset($area) ? $area : null,
|
||||
'room' => isset($room) ? $room : null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
|
||||
$result = get_form_var('result', 'string', null, INPUT_GET);
|
||||
$error = get_form_var('error', 'string', null, INPUT_GET);
|
||||
|
||||
echo "<div class=\"contents\">\n";
|
||||
|
||||
if (isset($result) && ($result == 'ok'))
|
||||
{
|
||||
generate_change_password_success($target_url);
|
||||
}
|
||||
else
|
||||
{
|
||||
generate_change_password_form($error, $target_url);
|
||||
}
|
||||
|
||||
echo "</div>\n";
|
||||
|
||||
print_footer();
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MRBS;
|
||||
|
||||
use IntlDateFormatter;
|
||||
|
||||
require_once 'lib/autoload.inc';
|
||||
|
||||
/**************************************************************************
|
||||
* MRBS 配置文件(精简完整版)
|
||||
* 仅保留必要配置 + 您的自定义设置
|
||||
**************************************************************************/
|
||||
|
||||
/**********
|
||||
* 时区与语言
|
||||
**********/
|
||||
$timezone = "Asia/Shanghai";
|
||||
$override_locale = 'zh-CN';
|
||||
|
||||
/*******************
|
||||
* 数据库设置
|
||||
*******************/
|
||||
$dbsys = "mysql";
|
||||
$db_host = "localhost";
|
||||
$db_database = "hotel";
|
||||
$db_login = "hotel";
|
||||
$db_password = 'i4wt5yn2'; // ← 请替换为实际数据库密码
|
||||
$db_tbl_prefix = "mrbs_";
|
||||
$db_persist = false;
|
||||
|
||||
/* ====================== 以下为自定义配置 ====================== */
|
||||
$mrbs_company = "LZOLJ";
|
||||
$vocab_override['zh']['mrbs'] = "会议预定系统V1.11.6";
|
||||
|
||||
/**********************************************
|
||||
* 邮件设置(企业163 SMTP)
|
||||
**********************************************/
|
||||
$mail_settings = [
|
||||
'from' => 'admin@hi-luzhou-lj.com',
|
||||
'use_from_for_all_mail' => true,
|
||||
'use_reply_to' => true,
|
||||
'organizer' => 'admin@hi-luzhou-lj.com',
|
||||
'recipients' => 'admin@hi-luzhou-lj.com',
|
||||
'cc' => '',
|
||||
'treat_cc_as_to' => false,
|
||||
|
||||
'admin_on_bookings' => false,
|
||||
'area_admin_on_bookings'=> true,
|
||||
'room_admin_on_bookings'=> true,
|
||||
'booker' => false, // 改成 true 可让预订者本人也收到邮件
|
||||
'on_new' => true,
|
||||
'on_change' => false,
|
||||
'on_delete' => false,
|
||||
|
||||
'allow_no_mail' => false,
|
||||
'no_mail_default' => false,
|
||||
'details' => false,
|
||||
'html' => false,
|
||||
'icalendar' => false,
|
||||
|
||||
'admin_lang' => 'zh',
|
||||
'admin_backend' => 'smtp',
|
||||
];
|
||||
|
||||
/*******************
|
||||
* SMTP 设置
|
||||
*******************/
|
||||
$smtp_settings = [
|
||||
'host' => 'smtphz.qiye.163.com',
|
||||
'port' => 465,
|
||||
'auth' => true,
|
||||
'secure' => 'ssl',
|
||||
'username' => 'admin@hi-luzhou-lj.com',
|
||||
'password' => 'NwxJ%fNmLguah%k2', // ← 请替换为实际 SMTP 密码
|
||||
'hostname' => '',
|
||||
'helo' => '',
|
||||
'disable_opportunistic_tls' => false,
|
||||
'ssl_verify_peer' => true,
|
||||
'ssl_verify_peer_name' => true,
|
||||
'ssl_allow_self_signed' => false,
|
||||
];
|
||||
|
||||
/* ====================== 推荐附加设置 ====================== */
|
||||
// 认证方式(最常用)
|
||||
$auth['type'] = 'db';
|
||||
|
||||
// 默认显示区域和房间(根据您实际的 area_id 和 room_id 修改)
|
||||
$default_area = 1;
|
||||
$default_room = 1;
|
||||
|
||||
// 最大重复预订天数(1年)
|
||||
$max_rep_interval = 365;
|
||||
|
||||
// 其他常用优化(可按需取消注释)
|
||||
// $refresh_rate = 0; // 关闭自动刷新
|
||||
// $enable_periods = false; // 使用时间段模式(而非分钟)
|
||||
|
||||
/* ====================== 等保二级整改配置(2026-09-08) ====================== */
|
||||
|
||||
// ---- C1: 口令复杂度策略 ----
|
||||
// MRBS 1.12 内建校验框架:管理端设密(edit_users.php)、自助重置均自动执行本策略
|
||||
$pwd_policy = [
|
||||
'length' => 8, // 最小长度 8 位
|
||||
'lower' => 1, // 至少 1 个小写字母
|
||||
'upper' => 1, // 至少 1 个大写字母
|
||||
'numeric' => 1, // 至少 1 个数字
|
||||
'special' => 1, // 至少 1 个特殊字符
|
||||
];
|
||||
|
||||
// ---- C2: 会话超时(登录连接超时自动退出)----
|
||||
// 使用默认 'php' session 方案($auth['session'] 未单独设置)
|
||||
$auth['session_php']['session_name'] = 'MRBS_SESSID'; // 会话名
|
||||
$auth['session_php']['session_expire_time'] = 12 * 60 * 60; // 绝对过期:12 小时(原默认 30 天)
|
||||
$auth['session_php']['inactivity_expire_time'] = 30 * 60; // 空闲 30 分钟自动退出(原默认 0 = 永不)
|
||||
|
||||
// ---- C6: 禁止匿名访问(全站必须登录)----
|
||||
// 官方机制:所有页面最低访问级别提升为需登录;忘记密码流程除外;需确认 kiosk 模式未启用
|
||||
$auth['deny_public_access'] = true;
|
||||
|
||||
// ---- 屏幕水印开关(防截图泄密溯源;配合 Themes/default/header.inc 输出)----
|
||||
$watermark_enabled = true;
|
||||
|
||||
// ---- 代码改造参数(登录锁定 / 口令有效期 / 审计日志)----
|
||||
// 配套代码:lib/MRBS/Auth/AuthDb.php、lib/MRBS/Session/SessionWithLogin.php、
|
||||
// lib/MRBS/Audit.php、change_password.php
|
||||
$login_lock_threshold = 5; // 连续登录失败阈值(次),达到后临时锁定
|
||||
$login_lock_duration = 15 * 60; // 锁定时间(秒)= 15 分钟
|
||||
$pwd_max_age = 90 * 24 * 60 * 60; // 口令最长有效期(秒)= 90 天
|
||||
$audit_log_file = __DIR__ . '/audit/security_audit.log'; // 安全审计日志文件
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,745 @@
|
||||
<?php // -*-mode: PHP; coding:utf-8;-*-
|
||||
|
||||
// This file contains PHP code that specifies language specific strings
|
||||
// The default strings come from lang.en, and anything in a locale
|
||||
// specific file will overwrite the default. This is a US/UK English file.
|
||||
//
|
||||
//
|
||||
//
|
||||
// This file is PHP code. Treat it as such.
|
||||
|
||||
// Used in style.inc
|
||||
$vocab["mrbs"] = "Meeting Room Booking System";
|
||||
$vocab["mrbs_abbr"] = "MRBS";
|
||||
|
||||
// Used in functions.inc
|
||||
$vocab["report"] = "Report";
|
||||
$vocab["admin"] = "Admin";
|
||||
$vocab["help"] = "Help";
|
||||
$vocab["search"] = "Search";
|
||||
$vocab["outstanding"] = "%d pending bookings";
|
||||
|
||||
// Used in index.php
|
||||
$vocab["bookingsfor"] = "Bookings for";
|
||||
$vocab["bookingsforpost"] = ""; // Goes after the date
|
||||
$vocab["areas"] = "Areas";
|
||||
$vocab["now_day"] = "Today";
|
||||
$vocab["now_week"] = "Today";
|
||||
$vocab["now_month"] = "Today";
|
||||
$vocab["now_year"] = "Today";
|
||||
$vocab["daybefore"] = "Go to day before";
|
||||
$vocab["dayafter"] = "Go to day after";
|
||||
$vocab["gototoday"] = "Go to today";
|
||||
$vocab["goto"] = "Go to";
|
||||
$vocab["nav_day"] = "Day";
|
||||
$vocab["nav_week"] = "Week";
|
||||
$vocab["nav_month"] = "Month";
|
||||
$vocab["nav_year"] = "Year";
|
||||
$vocab["highlight_line"] = "Highlight this line";
|
||||
$vocab["click_to_reserve"] = "Click on the cell to make a reservation.";
|
||||
$vocab["timezone"] = "Timezone";
|
||||
$vocab["weekbefore"] = "Go to week before";
|
||||
$vocab["weekafter"] = "Go to week after";
|
||||
$vocab["gotothisweek"] = "Go to this week";
|
||||
$vocab["monthbefore"] = "Go to month before";
|
||||
$vocab["monthafter"] = "Go to month after";
|
||||
$vocab["gotothismonth"] = "Go to this month";
|
||||
$vocab['yearbefore'] = "Go to year before";
|
||||
$vocab['yearafter'] = "Go to year after";
|
||||
$vocab['gotothisyear'] = "Go to this year";
|
||||
$vocab["no_rooms_for_area"] = "No rooms defined for this area";
|
||||
$vocab["loading"] = "Loading";
|
||||
$vocab["saving"] = "Saving";
|
||||
$vocab["week_number"] = "Week %s | ";
|
||||
$vocab["create_new_booking"] = "Create a new booking";
|
||||
$vocab["select_area"] = "Select area";
|
||||
$vocab["select_room"] = "Select room";
|
||||
$vocab["all"] = "<all>";
|
||||
$vocab["registration_level_limited"] = ' [%1$d/%2$d]';
|
||||
$vocab["registration_level_limited_with_names"] = ' [%1$d/%2$d: %3$s]';
|
||||
$vocab["registration_level_unlimited"] = ' [%1$d]';
|
||||
$vocab["registration_level_unlimited_with_names"] = ' [%1$d: %2$s]';
|
||||
$vocab["exit_kiosk_mode_confirm"] = "Exit kiosk mode?";
|
||||
$vocab["close"] = "Close";
|
||||
$vocab["ok"] = "OK";
|
||||
$vocab["cancel"] = "Cancel";
|
||||
|
||||
// Used in trailer.inc
|
||||
$vocab["viewday"] = "View Day";
|
||||
$vocab["viewweek"] = "View Week";
|
||||
$vocab["viewmonth"] = "View Month";
|
||||
$vocab["viewyear"] = "View Year";
|
||||
$vocab["ppreview"] = "Print Preview";
|
||||
|
||||
// Used in edit_entry.php
|
||||
$vocab["addentry"] = "Add Entry";
|
||||
$vocab["editentry"] = "Edit Entry";
|
||||
$vocab["copyentry"] = "Copy Entry";
|
||||
$vocab["editseries"] = "Edit Series";
|
||||
$vocab["copyseries"] = "Copy Series";
|
||||
$vocab["namebooker"] = "Brief description";
|
||||
$vocab["fulldescription"] = "Full description";
|
||||
$vocab["date"] = "Date";
|
||||
$vocab["start"] = "Start";
|
||||
$vocab["end"] = "End";
|
||||
$vocab["start_date"] = "Start time";
|
||||
$vocab["end_date"] = "End time";
|
||||
$vocab["time"] = "Time";
|
||||
$vocab["period"] = "Period";
|
||||
$vocab["unknown_period"] = "<Period %d>";
|
||||
$vocab["duration"] = "Duration";
|
||||
$vocab["second_lc"] = "second";
|
||||
$vocab["seconds"] = "seconds";
|
||||
$vocab["minute_lc"] = "minute";
|
||||
$vocab["minutes"] = "minutes";
|
||||
$vocab["hour_lc"] = "hour";
|
||||
$vocab["hours"] = "hours";
|
||||
$vocab["day"] = "day";
|
||||
$vocab["days"] = "days";
|
||||
$vocab["week"] = "week";
|
||||
$vocab["weeks"] = "weeks";
|
||||
$vocab["month"] = "month";
|
||||
$vocab["months"] = "months";
|
||||
$vocab["year_lc"] = "year";
|
||||
$vocab["years"] = "years";
|
||||
$vocab["period_lc"] = "period";
|
||||
$vocab["periods"] = "periods";
|
||||
$vocab["all_day"] = "All day";
|
||||
$vocab["area"] = "Area";
|
||||
$vocab["type"] = "Type";
|
||||
$vocab["allow_registration"] = "Allow registration";
|
||||
$vocab["registrant_limit_enabled"] = "Set event capacity";
|
||||
$vocab["registrant_limit"] = "Event capacity";
|
||||
$vocab["registration_opens"] = "Registration opens";
|
||||
$vocab["registration_closes"] = "Registration closes";
|
||||
$vocab["in_advance"] = "in advance";
|
||||
$vocab["in_advance_periods"] = "before %s"; // a time, eg 09:00 or 9.00am
|
||||
$vocab["n_registered"] = "Currently registered";
|
||||
$vocab["save"] = "Save";
|
||||
$vocab["rep_type"] = "Repeat type";
|
||||
$vocab["rep_type_0"] = "None";
|
||||
$vocab["rep_type_1"] = "Daily";
|
||||
$vocab["rep_type_2"] = "Weekly";
|
||||
$vocab["rep_type_3"] = "Monthly";
|
||||
$vocab["rep_type_4"] = "Yearly";
|
||||
$vocab["ord_1"] = "first";
|
||||
$vocab["ord_2"] = "second";
|
||||
$vocab["ord_3"] = "third";
|
||||
$vocab["ord_4"] = "fourth";
|
||||
$vocab["ord_5"] = "fifth";
|
||||
$vocab["ord_-1"] = "last";
|
||||
$vocab["ord_-2"] = "second last";
|
||||
$vocab["ord_-3"] = "third last";
|
||||
$vocab["ord_-4"] = "fourth last";
|
||||
$vocab["ord_-5"] = "fifth last";
|
||||
$vocab["rep_end_date"] = "Repeat end date";
|
||||
$vocab["rep_rep_day"] = "Repeat day";
|
||||
$vocab["rep_interval"] = "Repeat every";
|
||||
$vocab["month_absolute"] = "On day";
|
||||
$vocab["month_relative"] = "On the";
|
||||
$vocab["skip_conflicts"] = "Skip past conflicts";
|
||||
$vocab["no_mail"] = "Do not send email";
|
||||
$vocab["ctrl_click"] = "Use Control-Click to select more than one room";
|
||||
$vocab["entryid"] = "Entry ID ";
|
||||
$vocab["repeat_id"] = "Repeat ID ";
|
||||
$vocab["brief_description"] = "Brief description.";
|
||||
$vocab["status"] = "Status";
|
||||
$vocab["public"] = "Public";
|
||||
$vocab["private"] = "Private";
|
||||
$vocab["unavailable"] = "[Private]";
|
||||
$vocab["is_mandatory_field"] = "is a mandatory field, please supply a value.";
|
||||
$vocab["missing_mandatory_field"] = "You have not supplied a value for the mandatory field";
|
||||
$vocab["confirmed"] = "Confirmed";
|
||||
$vocab["start_after_end"] = "Start day after end day";
|
||||
$vocab["start_after_end_long"] = "Error: the start day cannot be after the end day.";
|
||||
$vocab["invalid_rep_interval"] = "The repeat interval must be greater than zero.";
|
||||
$vocab["confirm_rep_end_date"] = "It looks like you may have forgotten to set the repeat " .
|
||||
"end date. Do you want to continue anyway?";
|
||||
$vocab["rep_end_date_before_start_date"] = "The repeat end date must not be before the start date.";
|
||||
$vocab["type_reserved_for_admins"] = "The type '%s' is reserved for administrators";
|
||||
$vocab["multiroom_not_allowed"] = "You are not allowed to make a booking for multiple rooms.";
|
||||
$vocab["edit_entry_nonexistent_room"] = "You are trying to create or edit an entry for a room " .
|
||||
"that doesn't exist. This might be because you are using " .
|
||||
"a bookmark that is no longer valid. It is not generally " .
|
||||
"recommended to use bookmarks that take you straight to " .
|
||||
"the booking form. Instead it is better to get to the " .
|
||||
"booking form from the calendar view.";
|
||||
$vocab["home"] = "Home";
|
||||
|
||||
// Used in view_entry.php
|
||||
$vocab["description"] = "Description";
|
||||
$vocab["room"] = "Room";
|
||||
$vocab["createdby"] = "Created by";
|
||||
$vocab["modifiedby"] = "Modified by";
|
||||
$vocab["lastupdate"] = "Last updated";
|
||||
$vocab["repeat_on"] = "Repeat day";
|
||||
$vocab["deleteentry"] = "Delete Entry";
|
||||
$vocab["deleteseries"] = "Delete Series";
|
||||
$vocab["exportentry"] = "Export Entry";
|
||||
$vocab["exportseries"] = "Export Series";
|
||||
$vocab["confirmdel"] = "Are you sure you want to delete this entry?";
|
||||
$vocab["confirmdel_series"] = "Are you sure you want to delete this series?";
|
||||
$vocab["confirm_edit_series"] = "WARNING! Users have registered for one or more events in this series and " .
|
||||
"editing the series will cause their names to be lost. If you want " .
|
||||
"to keep their names then you should edit the series one entry at a " .
|
||||
"time using 'Edit Entry'. Are you sure you want to continue?";
|
||||
$vocab["returnprev"] = "Return to previous page";
|
||||
$vocab["invalid_entry_id"] = "Invalid entry id.";
|
||||
$vocab["invalid_series_id"] = "Invalid series id.";
|
||||
$vocab["confirmation_status"] = "Confirmation status";
|
||||
$vocab["tentative"] = "Tentative";
|
||||
$vocab["approval_status"] = "Approval status";
|
||||
$vocab["approved"] = "Approved";
|
||||
$vocab["awaiting_approval"] = "Awaiting approval";
|
||||
$vocab["approve"] = "Approve";
|
||||
$vocab["reject"] = "Reject";
|
||||
$vocab["more_info"] = "More Info";
|
||||
$vocab["remind_admin"] = "Remind Admin";
|
||||
$vocab["series"] = "Series";
|
||||
$vocab["request_more_info"] = "Please list the extra information you require";
|
||||
$vocab["reject_reason"] = "Please give a reason for your rejection of this reservation request";
|
||||
$vocab["send"] = "Send";
|
||||
$vocab["approve_failed"] = "The reservation could not be approved.";
|
||||
$vocab["no_request_yet"] = "No request has been sent yet"; // Used for the title tooltip on More Info button
|
||||
$vocab["last_request"] = "Last request sent at"; // Used for the title tooltip on More Info button
|
||||
$vocab["by"] = "by"; // Used for the title tooltip on More Info button
|
||||
$vocab["sent_at"] = "Sent at ";
|
||||
$vocab["yes"] = "Yes";
|
||||
$vocab["no"] = "No";
|
||||
$vocab["event_registration"] = "Event registration";
|
||||
$vocab["event_details"] = "Event details";
|
||||
$vocab["already_registered"] = "You have registered for this event";
|
||||
$vocab["register"] = "Register";
|
||||
$vocab["cancel_registration"] = "Cancel registration";
|
||||
$vocab["registered_by"] = "Registered by";
|
||||
$vocab["registered_on"] = "Registered on";
|
||||
$vocab["event_full"] = "This event is full.";
|
||||
$vocab["confirm_del_registrant"] = "Are you sure you want to delete the registration of '%s'?";
|
||||
|
||||
// Used in edit_entry_handler.php
|
||||
$vocab["error"] = "Error";
|
||||
$vocab["sched_conflict"] = "Scheduling Conflict";
|
||||
$vocab["conflict"] = "The new booking will conflict with the following entries:";
|
||||
$vocab["no_conflicts"] = "No scheduling conflicts";
|
||||
$vocab["rules_broken"] = "The new booking will conflict with the following policies:";
|
||||
$vocab["rules_broken_notices"] = "[Information only] The new booking would conflict with the following policies:";
|
||||
$vocab["no_rules_broken"] = "No policy conflicts";
|
||||
$vocab["schedule"] = "Schedule";
|
||||
$vocab["policy"] = "Policy";
|
||||
$vocab["conflicts_with_self"] = "This series contains entries which overlap each other. Check the entry end " .
|
||||
"date and the repeat end date.";
|
||||
$vocab["too_many_entries"] = "A series cannot consist of more than %d entries";
|
||||
$vocab["returncal"] = "Return to calendar view";
|
||||
$vocab["failed_to_acquire"] = "Failed to acquire exclusive database access";
|
||||
$vocab["invalid_booking"] = "Invalid booking";
|
||||
$vocab["must_set_description"] = "You must set a brief description for the booking. Please go back and enter one.";
|
||||
$vocab["no_rooms_selected"] = "You must select a room.";
|
||||
$vocab["mail_subject_approved"] = "Entry approved for %s MRBS"; // $mrbs_company
|
||||
$vocab["mail_subject_rejected"] = "Entry rejected for %s MRBS"; // $mrbs_company
|
||||
$vocab["mail_subject_more_info"] = "%s MRBS: more information requested"; // $mrbs_company
|
||||
$vocab["mail_subject_reminder"] = "Reminder for %s MRBS"; // $mrbs_company
|
||||
$vocab["mail_body_approved"] = "An entry has been approved by %s; here are the details:";
|
||||
$vocab["mail_body_rej_entry"] = "An entry has been rejected by %s, here are the details:";
|
||||
$vocab["mail_body_more_info"] = "%s requires more information about an entry; here are the details:";
|
||||
$vocab["mail_body_reminder"] = "Reminder - an entry is awaiting approval; here are the details:";
|
||||
$vocab["mail_body_repeats_booked"] = "The following dates were booked:";
|
||||
$vocab["mail_body_repeats_deleted"] = "The following bookings were deleted:";
|
||||
$vocab["mail_body_exceptions"] = "The following dates could not be booked due to conflicts:";
|
||||
$vocab["mail_subject_new_entry"] = "Entry added for %s MRBS"; // $mrbs_company
|
||||
$vocab["mail_subject_changed_entry"] = "Entry changed for %s MRBS"; // $mrbs_company
|
||||
$vocab["mail_subject_delete"] = "Entry deleted for %s MRBS"; // $mrbs_company
|
||||
$vocab["mail_body_new_entry"] = "A new entry has been booked by %s, here are the details:";
|
||||
$vocab["mail_body_changed_entry"] = "An entry has been modified by %s, here are the details:";
|
||||
$vocab["mail_body_del_entry"] = "An entry has been deleted by %s, here are the details:";
|
||||
$vocab["new_value"] = "New";
|
||||
$vocab["old_value"] = "Old";
|
||||
$vocab["reason"] = "Reason";
|
||||
$vocab["info_requested"] = "Information requested";
|
||||
$vocab["cannot_change_approved_bookings"] = "Bookings that have been approved cannot be edited or deleted";
|
||||
$vocab["no_simultaneous_bookings"] = "You cannot have bookings for more than %s room(s) at the same time";
|
||||
$vocab["no_bookings_on_holidays"] = "You have tried to make a booking on a holiday (%s)";
|
||||
$vocab["no_bookings_on_weekends"] = "You have tried to make a booking on a weekend day (%s)";
|
||||
$vocab["type_not_allowed"] = 'Bookings of type %1$s are not allowed in %2$s';
|
||||
$vocab["type_not_allowed_day"] = 'Bookings of type %1$s are not allowed on %2$ss';
|
||||
$vocab["min_create_time_before"] = 'You cannot create a booking which starts in less than %1$d %2$s';
|
||||
$vocab["max_create_time_before"] = 'You cannot create a booking which ends in more than %1$d %2$s';
|
||||
$vocab["max_create_time_before_type"] = 'You cannot create a booking of type %3$s which ends in more than %1$d %2$s';
|
||||
$vocab["max_create_time_before_start"] = 'You cannot create a booking which starts in more than %1$d %2$s';
|
||||
$vocab["max_create_time_before_start_type"] = 'You cannot create a booking of type %3$s which starts in more than %1$d %2$s';
|
||||
$vocab["min_delete_time_before"] = 'You cannot edit or delete a booking which starts in less than %1$d %2$s';
|
||||
$vocab["max_delete_time_before"] = 'You cannot edit or delete a booking which ends in more than %1$d %2$s';
|
||||
$vocab["max_delete_time_before_start"] = 'You cannot edit or delete a booking which starts in more than %1$d %2$s';
|
||||
$vocab["earliest_booking_date"] = "The earliest booking date is %s";
|
||||
$vocab["latest_booking_date"] = "The latest booking date is %s";
|
||||
$vocab["booking_opens_at"] = "Bookings open at %s each day";
|
||||
$vocab["max_booking_duration"] = 'The maximum duration of a booking is %1$d %2$s';
|
||||
$vocab["max_booking_duration_type"] = 'The maximum duration of a booking of type %3$s is %1$d %2$s';
|
||||
$vocab["max_per_day_global"] = "The maximum number of bookings per day per user across the whole system is";
|
||||
$vocab["max_per_week_global"] = "The maximum number of bookings per week per user across the whole system is";
|
||||
$vocab["max_per_month_global"] = "The maximum number of bookings per month per user across the whole system is";
|
||||
$vocab["max_per_year_global"] = "The maximum number of bookings per year per user across the whole system is";
|
||||
$vocab["max_per_future_global"] = "The maximum number of outstanding bookings per user across the whole system is";
|
||||
$vocab["max_per_day_area"] = "The maximum number of bookings per day per user in this area is";
|
||||
$vocab["max_per_week_area"] = "The maximum number of bookings per week per user in this area is";
|
||||
$vocab["max_per_month_area"] = "The maximum number of bookings per month per user in this area is";
|
||||
$vocab["max_per_year_area"] = "The maximum number of bookings per year per user in this area is";
|
||||
$vocab["max_per_future_area"] = "The maximum number of outstanding bookings per user in this area is";
|
||||
$vocab["max_secs_per_day_global"] = 'The maximum total length of bookings per day per user across the whole system is %1$s %2$s'; // eg 2 hours
|
||||
$vocab["max_secs_per_week_global"] = 'The maximum total length of bookings per week per user across the whole system is %1$s %2$s';
|
||||
$vocab["max_secs_per_month_global"] = 'The maximum total length of bookings per month per user across the whole system is %1$s %2$s';
|
||||
$vocab["max_secs_per_year_global"] = 'The maximum total length of bookings per year per user across the whole system is %1$s %2$s';
|
||||
$vocab["max_secs_per_future_global"] = 'The maximum total length of outstanding bookings per user across the whole system is %1$s %2$s';
|
||||
$vocab["max_secs_per_day_area"] = 'The maximum total length of bookings per day per user in this area is %1$s %2$s';
|
||||
$vocab["max_secs_per_week_area"] = 'The maximum total length of bookings per week per user in this area is %1$s %2$s';
|
||||
$vocab["max_secs_per_month_area"] = 'The maximum total length of bookings per month per user in this area is %1$s %2$s';
|
||||
$vocab["max_secs_per_year_area"] = 'The maximum total length of bookings per year per user in this area is %1$s %2$s';
|
||||
$vocab["max_secs_per_future_area"] = 'The maximum total length of outstanding bookings per user in this area is %1$s %2$s';
|
||||
$vocab["skip_and_book"] = "Skip and book";
|
||||
$vocab["skip_and_book_note"] = "Carry on with the booking, skipping past the conflicting entries";
|
||||
|
||||
// Used in edit_message.php
|
||||
$vocab["edit_message"] = "Edit message";
|
||||
$vocab["message"] = "System message";
|
||||
$vocab["display_from"] = "Display from the start of";
|
||||
$vocab["display_until"] = "Display until the end of";
|
||||
|
||||
// Used in kiosk.php
|
||||
$vocab["enter"] = "Enter";
|
||||
$vocab["exit"] = "Exit";
|
||||
$vocab["enter_kiosk_intro"] = "Set a password which will be needed to exit kiosk mode.";
|
||||
$vocab["exit_kiosk_intro"] = "Enter the password you set when you entered kiosk mode.";
|
||||
$vocab["enter_kiosk_mode"] = "Enter Kiosk Mode";
|
||||
$vocab["exit_kiosk_mode"] = "Exit Kiosk Mode";
|
||||
$vocab["kiosk"] = "Kiosk";
|
||||
$vocab["kiosk_password"] = "Kiosk password";
|
||||
|
||||
// Used in pending.php
|
||||
$vocab["pending"] = "Bookings awaiting approval";
|
||||
$vocab["none_outstanding"] = "You have no bookings awaiting approval.";
|
||||
|
||||
// Authentication stuff
|
||||
$vocab["accessdenied"] = "Access Denied";
|
||||
$vocab["norights"] = "You do not have the necessary rights to view this page.";
|
||||
$vocab["please_login"] = "Please log in";
|
||||
$vocab["users.name"] = "Username";
|
||||
$vocab["users.display_name"] = "Name";
|
||||
$vocab["users.password"] = "Password";
|
||||
$vocab["users.level"] = "Rights";
|
||||
$vocab["users.timestamp"] = "Last updated";
|
||||
$vocab["users.last_login"] = "Last login";
|
||||
$vocab["unknown_user"] = "Unknown user";
|
||||
$vocab["login"] = "Log in";
|
||||
$vocab["logoff"] = "Log off";
|
||||
$vocab["username_or_email"] = "Username or email address";
|
||||
$vocab["lost_password"] = "Lost your password?";
|
||||
$vocab["get_new_password"] = "Get new password";
|
||||
$vocab["password_reset"] = "Password reset";
|
||||
$vocab["password_reset_subject"] = "Password reset request";
|
||||
$vocab["password_reset_body"] = 'Someone has generated a password reset request for %3$s. ' .
|
||||
'If this wasn\'t you then you can ignore this email. Otherwise ' .
|
||||
'you should follow the link below to reset your password. ' .
|
||||
'The link will expire in %1$d %2$s.';
|
||||
$vocab["enter_username"] = "Please enter your username here.";
|
||||
$vocab["enter_username_or_email"] = "Please enter your username or email address here.";
|
||||
$vocab["will_be_sent_instructions"] = "You will be sent an email message with instructions on how to reset your password. " .
|
||||
"If you don't have an email address you will need to contact your administrator.";
|
||||
$vocab["reset_password"] = "Reset password";
|
||||
$vocab["pwd_check_email"] = "Thank you. If there's a user account corresponding to those details then you " .
|
||||
"will shortly receive an email with instructions for resetting your password. " .
|
||||
"Don't forget to check your spam/junk folder if it doesn't arrive.";
|
||||
$vocab["invalid_link"] = "Invalid link";
|
||||
$vocab["pwd_request_failed"] = "An email could not be sent to this user.";
|
||||
$vocab["link_invalid"] = "The password reset link is invalid or has expired.";
|
||||
$vocab["enter_new_password"] = "Enter your new password twice below.";
|
||||
$vocab["pwd_must_contain"] = "The password must contain at least:";
|
||||
$vocab["pwd_reset_success"] = "Your password has successfully been reset.";
|
||||
|
||||
// Database upgrade code
|
||||
$vocab["database_login"] = "Database login";
|
||||
$vocab["upgrade_required"] = "To complete the upgrade the database now needs to be upgraded. Please backup your database before proceeding.";
|
||||
$vocab["supply_userpass"] = "Please supply a database username and password that has admin rights.";
|
||||
$vocab["contact_admin"] = "If you are not the MRBS administrator please contact %s."; // $mrbs_admin
|
||||
$vocab["upgrading_site"] = "Upgrading site '%s'"; // site name
|
||||
$vocab["upgrading_main_site"] = "Upgrading the main site";
|
||||
$vocab["no_tables_found"] = "No tables found";
|
||||
$vocab["already_at_version"] = "Already at version %d.";
|
||||
$vocab["upgrade_to_version"] = "Upgrading to database version";
|
||||
$vocab["upgrade_to_local_version"] = "Upgrading to database local version";
|
||||
$vocab["upgrade_summary"] = "Upgrade summary";
|
||||
$vocab["upgrade_completed"] = "Database upgrade successfully completed.";
|
||||
$vocab["no_connection"] = "A database connection could not be established.";
|
||||
$vocab["main_site_failed"] = "The main site could not be upgraded.";
|
||||
$vocab["failed_sites"] = "The following sub-sites could not be upgraded:";
|
||||
$vocab["retry_from_failing"] = "If a database connection could not be established for a site, please launch MRBS " .
|
||||
"from that site and, when prompted, enter the database credentials for that site.";
|
||||
|
||||
// User access levels
|
||||
$vocab["level_0"] = "none";
|
||||
$vocab["level_1"] = "user";
|
||||
$vocab["level_2"] = "admin";
|
||||
$vocab["level_3"] = "user admin";
|
||||
|
||||
// Authentication database
|
||||
$vocab["user_list"] = "Users";
|
||||
$vocab["edit_user"] = "Edit user";
|
||||
$vocab["delete_user"] = "Delete this user";
|
||||
//$vocab["users.name"] = Use the same as above, for consistency.
|
||||
//$vocab["users.password"] = Use the same as above, for consistency.
|
||||
$vocab["users.email"] = "Email address";
|
||||
$vocab["password_twice"] = "If you wish to change the password, please type the new password twice";
|
||||
$vocab["passwords_not_eq"] = "The passwords did not match!";
|
||||
$vocab["password_invalid"] = "The password does not conform to the policy. It must contain at least:";
|
||||
$vocab["policy_length"] = "%d character(s)";
|
||||
$vocab["policy_alpha"] = "%d letter(s)";
|
||||
$vocab["policy_lower"] = "%d lower-case letter(s)";
|
||||
$vocab["policy_upper"] = "%d upper-case letter(s)";
|
||||
$vocab["policy_numeric"] = "%d numeric character(s)";
|
||||
$vocab["policy_special"] = "%d special character(s)";
|
||||
$vocab["add_new_user"] = "Add a new user";
|
||||
$vocab["action"] = "Action";
|
||||
$vocab["user"] = "User";
|
||||
$vocab["administrator"] = "Administrator";
|
||||
$vocab["unknown"] = "Unknown";
|
||||
$vocab["ok"] = "OK";
|
||||
$vocab["show_my_entries"] = "Click to display my upcoming entries";
|
||||
$vocab["no_users_initial"] = "Welcome to MRBS!";
|
||||
$vocab["no_users_create_first_admin"] = "Before you can do anything else you need to create an admin " .
|
||||
"user. Then login as that user and create more users and create " .
|
||||
"areas and rooms.";
|
||||
$vocab["warning_last_admin"] = "Warning! This is the last admin and so you cannot delete this user or remove admin rights, otherwise you will be locked out of the system.";
|
||||
$vocab["copy_email_addresses"] = "Copy email addresses";
|
||||
|
||||
// Used in search.php
|
||||
$vocab["invalid_search"] = "Empty or invalid search string.";
|
||||
$vocab["search_results"] = 'Search results for \'%1$s\' from %2$s';
|
||||
$vocab["search_results_unlimited"] = "Search results for '%s'";
|
||||
$vocab["nothing_found"] = "No matching entries found.";
|
||||
$vocab["records"] = "Records ";
|
||||
$vocab["through"] = " through ";
|
||||
$vocab["of"] = " of ";
|
||||
$vocab["previous"] = "Previous";
|
||||
$vocab["next"] = "Next";
|
||||
$vocab["entry"] = "Entry";
|
||||
$vocab["search_button"] = "Search";
|
||||
$vocab["search_for"] = "Search for";
|
||||
$vocab["from"] = "From";
|
||||
$vocab["export_as_ics"] = ".ics";
|
||||
|
||||
// Used in report.php
|
||||
$vocab["report_on"] = "Report on Meetings";
|
||||
$vocab["report_start"] = "Report start date";
|
||||
$vocab["report_end"] = "Report end date";
|
||||
$vocab["match_area"] = "Match area";
|
||||
$vocab["match_room"] = "Match room";
|
||||
$vocab["match_type"] = "Match type";
|
||||
$vocab["ctrl_click_type"] = "Use Control-Click to select more than one type";
|
||||
$vocab["match_entry"] = "Match brief description";
|
||||
$vocab["match_descr"] = "Match full description";
|
||||
$vocab["output"] = "Output";
|
||||
$vocab["summary"] = "Summary";
|
||||
$vocab["format"] = "Format";
|
||||
$vocab["html"] = "HTML";
|
||||
$vocab["csv"] = "CSV";
|
||||
$vocab["ical"] = "iCalendar (.ics file)";
|
||||
$vocab["combination_not_supported"] = "This output is not supported in this format";
|
||||
$vocab["summarize_by"] = "Summarize by";
|
||||
$vocab["sum_by_descrip"] = "Brief description";
|
||||
$vocab["sum_by_creator"] = "Creator";
|
||||
$vocab["sum_by_type"] = "Type";
|
||||
$vocab["entry_found"] = "entry found";
|
||||
$vocab["entries_found"] = "entries found";
|
||||
$vocab["summary_header"] = "Summary of (Entries) Hours";
|
||||
$vocab["summary_header_per"] = "Summary of (Entries) Periods";
|
||||
$vocab["summary_header_both"] = "Summary of (Entries) Hours/Periods";
|
||||
$vocab["entries"] = "entries";
|
||||
$vocab["total"] = "Total";
|
||||
$vocab["submitquery"] = "Run Report";
|
||||
$vocab["sort_rep"] = "Sort report by";
|
||||
$vocab["sort_rep_time"] = "Start date/time";
|
||||
$vocab["sort_room"] = "Room";
|
||||
$vocab["fulldescription_short"] = "Full Description";
|
||||
$vocab["both"] = "All";
|
||||
$vocab["with"] = "With";
|
||||
$vocab["without"] = "Without";
|
||||
$vocab["privacy_status"] = "Privacy status";
|
||||
$vocab["search_criteria"] = "Search criteria";
|
||||
$vocab["presentation_options"] = "Output options";
|
||||
$vocab["delete_entries"] = "Delete entries";
|
||||
$vocab["delete_entries_warning"] = "WARNING! This will delete all the entries matching " .
|
||||
"the search criteria. The operation cannot be undone. Are " .
|
||||
"you sure you want to continue?\n\n" .
|
||||
"Total number of entries that will be deleted: %s";
|
||||
$vocab["deleting_n_entries"] = "Deleting %d entries ...";
|
||||
$vocab["delete_entries_failed"] = "The entries could not be deleted.";
|
||||
$vocab["cancel"] = "Cancel";
|
||||
$vocab["registered"] = "Registered";
|
||||
$vocab["na"] = "[N/A]";
|
||||
$vocab["compound_name"] = '%1$s (%2$s)'; // 1: username, 2: display name
|
||||
$vocab["registrant_registered_by"] = '%1$s (by %2$s)'; // 1: Registrant, 2: Registered by
|
||||
$vocab["registrant_username_and_registered_by"] = '%1$s (%2$s) (by %3$s)'; // 2: Registrant username, 2: Registrant display name, 3: Registered by
|
||||
$vocab["unique_addresses_copied"] = "%d unique email addresses copied to the clipboard.";
|
||||
$vocab["clipboard_copy_failed"] = "Clipboard copy failed.";
|
||||
|
||||
// Used in admin.php
|
||||
$vocab["no_message"] = "There is currently no message to display above the calendar.";
|
||||
$vocab["this_message"] = "The message below will be displayed above the calendar.";
|
||||
$vocab["this_message_from"] = "The message below will be displayed above the calendar from %s.";
|
||||
$vocab["this_message_until"] = "The message below will be displayed above the calendar until %s.";
|
||||
$vocab["this_message_from_until"] = 'The message below will be displayed above the calendar from %1$s until %2$s.';
|
||||
$vocab["edit"] = "Edit";
|
||||
$vocab["delete"] = "Delete";
|
||||
$vocab["rooms"] = "Rooms";
|
||||
$vocab["in"] = "in";
|
||||
$vocab["noareas"] = "No areas have been defined.";
|
||||
$vocab["noareas_enabled"] = "No areas have been enabled.";
|
||||
$vocab["addarea"] = "Add Area";
|
||||
$vocab["name"] = "Name";
|
||||
$vocab["noarea"] = "No area selected";
|
||||
$vocab["browserlang"] = "Your browser is set with the following language preference order";
|
||||
$vocab["addroom"] = "Add Room";
|
||||
$vocab["capacity"] = "Capacity";
|
||||
$vocab["norooms"] = "No rooms have been defined.";
|
||||
$vocab["norooms_enabled"] = "No rooms have been enabled.";
|
||||
$vocab["administration"] = "Room Details";
|
||||
$vocab["invalid_area_name"] = "That area name has already been used!";
|
||||
$vocab["empty_name"] = "You have not entered a name!";
|
||||
|
||||
// Used in edit_area.php and/or edit_room.php
|
||||
$vocab["editarea"] = "Edit Area";
|
||||
$vocab["change"] = "Change";
|
||||
$vocab["editroom"] = "Edit Room";
|
||||
$vocab["viewroom"] = "View Room";
|
||||
$vocab["not_found"] = " not found";
|
||||
$vocab["room_admin_email"] = "Notification emails";
|
||||
$vocab["area_admin_email"] = "Notification emails";
|
||||
$vocab["area_first_slot_start"] = "Start of first slot";
|
||||
$vocab["area_last_slot_start"] = "Start of last slot";
|
||||
$vocab["area_res_mins"] = "Resolution (minutes)";
|
||||
$vocab["area_def_duration_mins"] = "Default duration (minutes)";
|
||||
$vocab["times_along_top"] = "Times along the top";
|
||||
$vocab["invalid_area"] = "Invalid area!";
|
||||
$vocab["invalid_room"] = "Invalid room!";
|
||||
$vocab["invalid_room_name"] = "This room name has already been used in the area!";
|
||||
$vocab["invalid_email"] = "Invalid email address!";
|
||||
$vocab["invalid_time_format"] = "Times must be in the format 'hh:mm'";
|
||||
$vocab["invalid_resolution"] = "Invalid combination of first slot, last slot and resolution!";
|
||||
$vocab["general_settings"] = "General";
|
||||
$vocab["time_settings"] = "Slot times";
|
||||
$vocab["period_settings"] = "Period names";
|
||||
$vocab["add_period"] = "Add period";
|
||||
$vocab["confirmation_settings"] = "Confirmation settings";
|
||||
$vocab["allow_confirmation"] = "Allow tentative bookings";
|
||||
$vocab["default_settings_conf"] = "Default setting";
|
||||
$vocab["default_confirmed"] = "Confirmed";
|
||||
$vocab["default_tentative"] = "Tentative";
|
||||
$vocab["approval_settings"] = "Approval settings";
|
||||
$vocab["enable_approval"] = "Require bookings to be approved";
|
||||
$vocab["enable_reminders"] = "Allow users to remind admins";
|
||||
$vocab["private_settings"] = "Privacy settings";
|
||||
$vocab["allow_private"] = "Allow private bookings";
|
||||
$vocab["force_private"] = "Force private bookings";
|
||||
$vocab["default_settings"] = "Default/forced settings";
|
||||
$vocab["default_private"] = "Private";
|
||||
$vocab["default_public"] = "Public";
|
||||
$vocab["private_display"] = "Privacy settings (display)";
|
||||
$vocab["private_display_label"] = "How should private bookings be displayed?";
|
||||
$vocab["private_display_caution"] = "CAUTION: think carefully about the privacy implications before changing these settings!";
|
||||
$vocab["treat_respect"] = "Respect the privacy setting of the booking";
|
||||
$vocab["treat_private"] = "Treat all bookings as private, ignoring their privacy settings";
|
||||
$vocab["treat_public"] = "Treat all bookings as public, ignoring their privacy settings";
|
||||
$vocab["sort_key"] = "Sort key";
|
||||
$vocab["sort_key_note"] = "This is the key used for ordering rooms";
|
||||
$vocab["booking_policies"] = "Booking policies";
|
||||
$vocab["booking_creation"] = "Booking creation";
|
||||
$vocab["booking_deletion"] = "Booking deletion";
|
||||
$vocab["booking_limits"] = "Limits on the number of bookings per user";
|
||||
$vocab["booking_limits_secs"] = "Limits on the total length of bookings per user (times mode only)";
|
||||
$vocab["booking_durations"] = "Limits on the duration of bookings";
|
||||
$vocab["max_duration"] = "Maximum duration";
|
||||
$vocab["min_book_ahead"] = "Advance booking - minimum";
|
||||
$vocab["max_book_ahead"] = "Advance booking - maximum";
|
||||
$vocab["this_area"] = "This area";
|
||||
$vocab["whole_system"] = "Whole system";
|
||||
$vocab["whole_system_note"] = "The values for the whole system are set in the config file";
|
||||
$vocab["max_per_day"] = "Maximum number per day";
|
||||
$vocab["max_per_week"] = "Maximum number per week";
|
||||
$vocab["max_per_month"] = "Maximum number per month";
|
||||
$vocab["max_per_year"] = "Maximum number per year";
|
||||
$vocab["max_per_future"] = "Maximum number in the future";
|
||||
$vocab["max_secs_per_day"] = "Maximum time per day";
|
||||
$vocab["max_secs_per_week"] = "Maximum time per week";
|
||||
$vocab["max_secs_per_month"] = "Maximum time per month";
|
||||
$vocab["max_secs_per_year"] = "Maximum time per year";
|
||||
$vocab["max_secs_per_future"] = "Maximum time in the future";
|
||||
$vocab["custom_html"] = "Custom HTML";
|
||||
$vocab["custom_html_note"] = "This field can be used for displaying your own HTML, for example an embedded Google map";
|
||||
$vocab["email_list_note"] = "Enter a list of email addresses separated by commas";
|
||||
$vocab["mode"] = "Mode";
|
||||
$vocab["mode_periods"] = "Periods";
|
||||
$vocab["mode_times"] = "Times";
|
||||
$vocab["times_only"] = "Times mode only";
|
||||
$vocab["enabled"] = "Enabled";
|
||||
$vocab["disabled"] = "Disabled";
|
||||
$vocab["disabled_area_note"] = "If this area is disabled, it will not appear in the calendar views " .
|
||||
"and it will not be possible to book rooms in it. However existing bookings " .
|
||||
"will be preserved and will be visible in Search and Report results.";
|
||||
$vocab["disabled_room_note"] = "If this room is disabled, it will not appear in the calendar views " .
|
||||
"and it will not be possible to book it. However existing bookings " .
|
||||
"will be preserved and will be visible in Search and Report results.";
|
||||
$vocab["book_ahead_note_periods"] = "When using periods, book ahead times are rounded down to the nearest whole day.";
|
||||
$vocab["invalid_types"] = "Invalid types";
|
||||
$vocab["invalid_types_note"] = "These are types that are not allowed to be used in this room.";
|
||||
$vocab["select_note"] = "Use Control-Click to select/deselect an option";
|
||||
$vocab["use_period_times"] = "Set period times";
|
||||
$vocab["invalid_period_start_time"] = 'The start time \'%1$s\' for period \'%2$s\' is invalid.';
|
||||
$vocab["invalid_period_end_time"] = 'The end time \'%1$s\' for period \'%2$s\' is invalid.';
|
||||
$vocab["invalid_period_time"] = "The period '%s' has an invalid start or end time.";
|
||||
$vocab["period_start_before_last_end"] = "The start of period '%s' is before the end of the previous period.";
|
||||
$vocab["period_must_have_positive_duration"] = "Period '%s' must have a positive duration.";
|
||||
$vocab["period_start_before_previous_end"] = "The start time cannot be before the previous period's end time.";
|
||||
$vocab["period_end_must_be_after_start"] = "The period end time must be after the start time.";
|
||||
|
||||
// Used in edit_users.php
|
||||
$vocab["name_empty"] = "You must enter a name.";
|
||||
$vocab["name_not_unique"] = "already exists. Please choose another name.";
|
||||
$vocab["invalid_date"] = "The field '%s' must contain a valid date in the format YYYY-MM-DD.";
|
||||
$vocab["confirm_delete_user"] = "Are you sure you want to delete this user?";
|
||||
$vocab["confirm_delete_user_plus"] = "Are you sure you want to delete this user? They are involved in " .
|
||||
"bookings as a creator, modifier or registrant and deleting the user " .
|
||||
"will lose information such as their display name and email address.";
|
||||
|
||||
// Used in del.php
|
||||
$vocab["deletefollowing"] = "This will delete the following bookings";
|
||||
$vocab["and_n_more"] = "and %s more"; // %s rather than %d because n is the output of NumberFormatter::format()
|
||||
$vocab["sure"] = "Are you sure?";
|
||||
$vocab["YES"] = "YES";
|
||||
$vocab["NO"] = "NO";
|
||||
$vocab["delarea"] = "You must delete all rooms in this area before you can delete it<p>";
|
||||
|
||||
// Used in help.php
|
||||
$vocab["about_mrbs"] = "About MRBS";
|
||||
$vocab["mrbs_version"] = "MRBS version";
|
||||
$vocab["db_schema_version"] = "Database schema version";
|
||||
$vocab["db_local_schema_version"] = "Database local schema version";
|
||||
$vocab["config_details"] = "Configuration details";
|
||||
$vocab["server_details"] = "Server details";
|
||||
$vocab["database"] = "Database";
|
||||
$vocab["system"] = "System";
|
||||
$vocab["servertime"] = "Server time";
|
||||
$vocab["server_software"] = "Server software";
|
||||
$vocab["extensions"] = "Extensions";
|
||||
$vocab["please_contact"] = "Please contact %s for any questions that aren't answered here.";
|
||||
|
||||
// Used in import.php
|
||||
$vocab["import_icalendar"] = "Import an iCalendar file";
|
||||
$vocab["source"] = "Source";
|
||||
$vocab["area_room_settings"] = "Areas and rooms";
|
||||
$vocab["other_settings"] = "Other settings";
|
||||
$vocab["import_intro"] = "This form allows you to import an RFC 5545 compliant iCalendar " .
|
||||
"into MRBS from a file or URL. Only those repeating events " .
|
||||
"that have a recurrence rule with an equivalent repeat type in " .
|
||||
"MRBS will be imported.";
|
||||
$vocab["supported_file_types"] = "The following file types are supported:";
|
||||
$vocab["import_text_file"] = "uncompressed iCalendar files";
|
||||
$vocab["import_zip"] = "zip archives, including multiple files in an archive";
|
||||
$vocab["import_gzip"] = "gzip files";
|
||||
$vocab["import_bzip2"] = "bzip2 files";
|
||||
$vocab["source_type"] = "Source type";
|
||||
$vocab["file"] = "File";
|
||||
$vocab["file_name"] = "File";
|
||||
$vocab["url"] = "URL";
|
||||
$vocab["invalid_url"] = "Invalid URL";
|
||||
$vocab["import"] = "Import";
|
||||
$vocab["upload_failed"] = "Upload failed";
|
||||
$vocab["max_allowed_file_size"] = "The maximum allowed file size is %s bytes";
|
||||
$vocab["no_file"] = "No file was uploaded";
|
||||
$vocab["could_not_process"] = "Import failed: could not process file";
|
||||
$vocab["badly_formed_ics"] = "Badly formed VCALENDAR file";
|
||||
$vocab["default_room"] = "Default room";
|
||||
$vocab["default_room_note"] = "The room to be used if no LOCATION property is specified";
|
||||
$vocab["ignore_location"] = "Use the default room instead of LOCATION";
|
||||
$vocab["add_location"] = "Add the LOCATION to the";
|
||||
$vocab["expanded_name"] = '%1$s [%2$s]'; // 1$ - SUMMARY, 2$ - LOCATION
|
||||
$vocab["expanded_empty_name"] = '[%s]'; // LOCATION
|
||||
$vocab["expanded_description"] = '%1$s [%2$s]'; // 1$ - DESCRIPTION, 2$ - LOCATION
|
||||
$vocab["expanded_empty_description"] = '[%s]'; // LOCATION
|
||||
$vocab["area_room_order"] = "Order";
|
||||
$vocab["area_room_order_note"] = "The order of the area and room names in the LOCATION property";
|
||||
$vocab["area_room"] = "Area-Room";
|
||||
$vocab["room_area"] = "Room-Area";
|
||||
$vocab["area_room_delimiter"] = "Delimiter";
|
||||
$vocab["area_room_delimiter_note"] = "The string separating the area and room names in the LOCATION property. " .
|
||||
"If no delimiter is found MRBS will look for a unique room with the same " .
|
||||
"name as the LOCATION";
|
||||
$vocab["area_room_create"] = "Create rooms if necessary";
|
||||
$vocab["derive_creator_from"] = "Derive creator from";
|
||||
$vocab["organizer_email_address"] = "ORGANIZER email address";
|
||||
$vocab["organizer_mrbs_username"] = "ORGANIZER MRBS username";
|
||||
$vocab["import_past"] = "Import past bookings";
|
||||
$vocab["default_type"] = "Default type";
|
||||
$vocab["room_does_not_exist_no_area"] = "room does not exist and cannot be added - no area given";
|
||||
$vocab["room_not_unique_no_area"] = "room name is not unique. Cannot choose which one without an area.";
|
||||
$vocab["area_does_not_exist"] = "Non-existent area:";
|
||||
$vocab["room_does_not_exist"] = "Non-existent room:";
|
||||
$vocab["creating_new_area"] = "Creating new area:";
|
||||
$vocab["creating_new_room"] = "Creating new room:";
|
||||
$vocab["could_not_create_area"] = "Could not create area";
|
||||
$vocab["could_not_create_room"] = "Could not create room";
|
||||
$vocab["could_not_find_room"] = "Could not find room";
|
||||
$vocab["could_not_import"] = 'Could not import \'%1$s\' (UID: %2$s)'; // 1$ name, 2$ UID
|
||||
$vocab["import_no_SUMMARY"] = "Imported event - no SUMMARY available"; // This string cannot be empty
|
||||
$vocab["no_LOCATION"] = "The VEVENT did not include a LOCATION property";
|
||||
$vocab["invalid_RRULE"] = "Invalid RRULE: missing FREQ part";
|
||||
$vocab["invalid_RFC5545_day"] = "Invalid day '%s'";
|
||||
$vocab["more_than_one_BYDAY"] = "MRBS does not support more than one BYDAY value when FREQ=";
|
||||
$vocab["BYDAY_equals_5"] = "MRBS does not support a BYDAY value of 5";
|
||||
$vocab["unsupported_FREQ"] = "MRBS does not support FREQ=";
|
||||
$vocab["unsupported_COUNT"] = "COUNT not yet supported by MRBS";
|
||||
$vocab["no_indefinite_repeats"] = "Indefinite repeats not yet supported by MRBS";
|
||||
$vocab["bad_timezone"] = "Unknown or bad timezone '%s'";
|
||||
$vocab["event_created_from_periods"] = "The event was exported from an area that uses periods.";
|
||||
$vocab["events_imported"] = "events imported";
|
||||
$vocab["events_not_imported"] = "events not imported";
|
||||
|
||||
// Used in EntryInterval.php
|
||||
$vocab["range_separator"] = " - "; // For separating two dates/times in a range
|
||||
$vocab["year_range_separator"] = "/"; // For separating two years in a range, eg 2025/26
|
||||
$vocab["date_time_separator"] = ", "; // For separating a date from a time
|
||||
|
||||
// Used in DataTables
|
||||
$vocab["dt_all"] = "All";
|
||||
$vocab["show_hide_columns"] = "Show / hide columns";
|
||||
$vocab["copy"] = "Copy";
|
||||
// "csv" already defined above
|
||||
$vocab["excel"] = "Excel";
|
||||
$vocab["pdf"] = "PDF";
|
||||
$vocab["print"] = "Print";
|
||||
$vocab["restore_original"] = "Restore original";
|
||||
|
||||
// Entry types
|
||||
$vocab["type."] = "Please select a type";
|
||||
$vocab["type.I"] = "Internal";
|
||||
$vocab["type.E"] = "External";
|
||||
|
||||
// General
|
||||
$vocab["fatal_error"] = "Whoops! Unfortunately MRBS has encountered a fatal error. Please consult your system administrator.";
|
||||
$vocab["resize_error"] = "Whoops! Unfortunately MRBS encountered an error while trying to modify this booking. If this " .
|
||||
"continues to happen please consult your system administrator.";
|
||||
$vocab["fatal_db_error"] = "Fatal error: unfortunately the database is not available at the moment.";
|
||||
$vocab["fatal_db_ext_error"] = "Fatal error: unfortunately the external database is not available at the moment.";
|
||||
$vocab["fatal_no_tables"] = "Fatal error: the MRBS tables do not exist or cannot be accessed.";
|
||||
$vocab["session_expired"] = "Your session has expired.";
|
||||
$vocab["browser_not_supported"] = "Unfortunately your browser isn't supported by %s. You will need to upgrade to a more " .
|
||||
"recent version, or else use another browser.";
|
||||
$vocab["back"] = "Back";
|
||||
// New vocabulary for the security upgrade
|
||||
$vocab["account_locked"] = "Too many failed login attempts. Account temporarily locked for about %d minutes. Please try again later.";
|
||||
$vocab["change_password"] = "Change password";
|
||||
$vocab["current_password"] = "Current password";
|
||||
$vocab["new_password"] = "New password";
|
||||
$vocab["confirm_password"] = "Confirm new password";
|
||||
$vocab["password_changed"] = "Your password has been changed successfully.";
|
||||
$vocab["old_pwd_invalid"] = "Your current password is incorrect.";
|
||||
$vocab["pwd_same_as_old"] = "The new password must be different from the current password.";
|
||||
$vocab["pwd_expired_msg"] = "Your password has expired or is still the initial password. Please change it before continuing.";
|
||||
|
||||
@@ -0,0 +1,614 @@
|
||||
<?php // -*-mode: PHP; coding:utf-8;-*-
|
||||
// $Id$
|
||||
|
||||
// This file contains PHP code that specifies language specific strings
|
||||
// The default strings come from lang.en, and anything in a locale
|
||||
// specific file will overwrite the default. This is a Simplified Chinese file.
|
||||
//
|
||||
//
|
||||
// This file is PHP code. Treat it as such.
|
||||
|
||||
// Used in style.inc
|
||||
$vocab["mrbs"] = "会议室预约系统";
|
||||
|
||||
// Used in functions.inc
|
||||
$vocab["report"] = "报表";
|
||||
$vocab["admin"] = "系统管理";
|
||||
$vocab["help"] = "帮助";
|
||||
$vocab["search"] = "搜索";
|
||||
$vocab["outstanding"] = "%d 未完成的预约";
|
||||
|
||||
// Used in index.php
|
||||
$vocab["bookingsfor"] = "预约:";
|
||||
$vocab["bookingsforpost"] = "";
|
||||
$vocab["areas"] = "区域";
|
||||
$vocab["now_day"] = "今天";
|
||||
$vocab["now_week"] = "今天";
|
||||
$vocab["now_month"] = "今天";
|
||||
$vocab["now_year"] = "今天";
|
||||
$vocab["daybefore"] = "前一天";
|
||||
$vocab["dayafter"] = "后一天";
|
||||
$vocab["gototoday"] = "今天";
|
||||
$vocab["goto"] = "前往";
|
||||
$vocab["nav_day"] = "日";
|
||||
$vocab["nav_week"] = "周";
|
||||
$vocab["nav_month"] = "月";
|
||||
$vocab["highlight_line"] = "高亮显示这一行";
|
||||
$vocab["click_to_reserve"] = "点击单元格进行预约";
|
||||
$vocab["timezone"] = "时区";
|
||||
$vocab["weekbefore"] = "前一周";
|
||||
$vocab["weekafter"] = "后一周";
|
||||
$vocab["gotothisweek"] = "本周";
|
||||
$vocab["monthbefore"] = "上个月";
|
||||
$vocab["monthafter"] = "下个月";
|
||||
$vocab["gotothismonth"] = "本月";
|
||||
$vocab["no_rooms_for_area"] = "这个区域没有定义房间";
|
||||
$vocab["loading"] = "载入中";
|
||||
$vocab["saving"] = "保存中";
|
||||
$vocab["week_number"] = "周 %s | ";
|
||||
$vocab["create_new_booking"] = "新建一个预约";
|
||||
$vocab["select_area"] = "选择区域";
|
||||
$vocab["select_room"] = "选择会议房间";
|
||||
$vocab["all"] = "<所有>";
|
||||
|
||||
// Used in trailer.inc
|
||||
$vocab["viewday"] = "日视图";
|
||||
$vocab["viewweek"] = "周视图";
|
||||
$vocab["viewmonth"] = "月视图";
|
||||
$vocab["ppreview"] = "打印预览";
|
||||
|
||||
// Used in edit_entry.php
|
||||
$vocab["addentry"] = "新增条目";
|
||||
$vocab["editentry"] = "修改条目";
|
||||
$vocab["copyentry"] = "复制条目";
|
||||
$vocab["editseries"] = "修改例会";
|
||||
$vocab["copyseries"] = "复制例会";
|
||||
$vocab["namebooker"] = "简要说明";
|
||||
$vocab["fulldescription"] = "完整说明";
|
||||
$vocab["date"] = "日期";
|
||||
$vocab["start"] = "起始";
|
||||
$vocab["end"] = "结束";
|
||||
$vocab["start_date"] = "起始时间";
|
||||
$vocab["end_date"] = "结束时间";
|
||||
$vocab["time"] = "时间";
|
||||
$vocab["period"] = "期间";
|
||||
$vocab["duration"] = "持续时间";
|
||||
$vocab["second_lc"] = "秒";
|
||||
$vocab["seconds"] = "秒";
|
||||
$vocab["minute_lc"] = "分";
|
||||
$vocab["minutes"] = "分";
|
||||
$vocab["hour_lc"] = "小时";
|
||||
$vocab["hours"] = "小时";
|
||||
$vocab["day"] = "天";
|
||||
$vocab["days"] = "天";
|
||||
$vocab["week"] = "星期";
|
||||
$vocab["weeks"] = "星期";
|
||||
$vocab["month"] = "月";
|
||||
$vocab["months"] = "月";
|
||||
$vocab["year_lc"] = "年";
|
||||
$vocab["years"] = "年";
|
||||
$vocab["period_lc"] = "期间";
|
||||
$vocab["periods"] = "期间";
|
||||
$vocab["all_day"] = "整天";
|
||||
$vocab["area"] = "区域";
|
||||
$vocab["type"] = "类型";
|
||||
$vocab["allow_registration"] = "允许注册";
|
||||
$vocab["enable_registrant_limit"] = "设置事件限制";
|
||||
$vocab["registrant_limit"] = "事件限制";
|
||||
$vocab["n_registered"] = "当前已注册";
|
||||
$vocab["save"] = "保存";
|
||||
$vocab["rep_type"] = "重复类型";
|
||||
$vocab["rep_type_0"] = "不重复";
|
||||
$vocab["rep_type_1"] = "每天";
|
||||
$vocab["rep_type_2"] = "每周";
|
||||
$vocab["rep_type_3"] = "每月";
|
||||
$vocab["rep_type_4"] = "每年";
|
||||
$vocab["ord_1"] = "首个";
|
||||
$vocab["ord_2"] = "第二";
|
||||
$vocab["ord_3"] = "第三";
|
||||
$vocab["ord_4"] = "第四";
|
||||
$vocab["ord_5"] = "第五";
|
||||
$vocab["ord_-1"] = "最后";
|
||||
$vocab["ord_-2"] = "倒数第二";
|
||||
$vocab["ord_-3"] = "倒数第三";
|
||||
$vocab["ord_-4"] = "倒数第四";
|
||||
$vocab["ord_-5"] = "倒数第五";
|
||||
$vocab["rep_end_date"] = "结束重复的日期";
|
||||
$vocab["rep_rep_day"] = "重复的星期";
|
||||
$vocab["rep_interval"] = "每...重复";
|
||||
$vocab["month_absolute"] = "于天";
|
||||
$vocab["month_relative"] = "于此日";
|
||||
$vocab["skip_conflicts"] = "跳过以前的冲突";
|
||||
$vocab["ctrl_click"] = "用Ctrl+鼠标点击可以多选房间";
|
||||
$vocab["no_mail"] = "不发送邮件";
|
||||
$vocab["entryid"] = "条目编号 ";
|
||||
$vocab["repeat_id"] = "例会编号 ";
|
||||
$vocab["brief_description"] = "简要说明.";
|
||||
$vocab["status"] = "状态";
|
||||
$vocab["public"] = "公开";
|
||||
$vocab["private"] = "私有";
|
||||
$vocab["unavailable"] = "[私有]";
|
||||
$vocab["is_mandatory_field"] = "是一个必填项, 请提供一个值.";
|
||||
$vocab["missing_mandatory_field"] = "您尚未为必填项提供值";
|
||||
$vocab["confirmed"] = "确定";
|
||||
$vocab["start_after_end"] = "开始日期晚于结束日期";
|
||||
$vocab["start_after_end_long"] = "错误: 开始日期不能晚于结束日期.";
|
||||
$vocab["invalid_rep_interval"] = "重复间隔必须大于0";
|
||||
$vocab["type_reserved_for_admins"] = "类型 '%s' 是为管理员保留。";
|
||||
|
||||
// Used in view_entry.php
|
||||
$vocab["description"] = "说明";
|
||||
$vocab["room"] = "房间";
|
||||
$vocab["createdby"] = "预约人";
|
||||
$vocab["modifiedby"] = "修改人";
|
||||
$vocab["lastupdate"] = "最后更新";
|
||||
$vocab["repeat_on"] = "重复日";
|
||||
$vocab["deleteentry"] = "删除条目";
|
||||
$vocab["deleteseries"] = "删除例会";
|
||||
$vocab["exportentry"] = "导出条目";
|
||||
$vocab["exportseries"] = "导出例会";
|
||||
$vocab["confirmdel"] = "你确定要删除此条目?";
|
||||
$vocab["confirm_edit_series"] = "是否确定继续?";
|
||||
$vocab["returnprev"] = "回前一页";
|
||||
$vocab["invalid_entry_id"] = "条目编号错误.";
|
||||
$vocab["invalid_series_id"] = "序列编号错误.";
|
||||
$vocab["confirmation_status"] = "确定状态";
|
||||
$vocab["tentative"] = "暂定";
|
||||
$vocab["approval_status"] = "批准状态";
|
||||
$vocab["approved"] = "已批准";
|
||||
$vocab["awaiting_approval"] = "等待批准";
|
||||
$vocab["approve"] = "批准";
|
||||
$vocab["reject"] = "拒绝";
|
||||
$vocab["more_info"] = "需要更多信息";
|
||||
$vocab["remind_admin"] = "提醒管理员";
|
||||
$vocab["series"] = "例会";
|
||||
$vocab["request_more_info"] = "请列出你需要的额外信息";
|
||||
$vocab["reject_reason"] = "请给出你拒绝这个预约申请的原因";
|
||||
$vocab["send"] = "发送";
|
||||
$vocab["approve_failed"] = "该预约不能被批准.";
|
||||
$vocab["no_request_yet"] = "尚未发出申请"; // Used for the title tooltip on More Info button
|
||||
$vocab["last_request"] = "最后发送的申请"; // Used for the title tooltip on More Info button
|
||||
$vocab["by"] = "由"; // Used for the title tooltip on More Info button
|
||||
$vocab["sent_at"] = "发送于 ";
|
||||
$vocab["yes"] = "是";
|
||||
$vocab["no"] = "否";
|
||||
$vocab["event_registration"] = "事件注册";
|
||||
$vocab["event_details"] = "事件详情";
|
||||
$vocab["already_registered"] = "已注册过";
|
||||
$vocab["register"] = "注册人";
|
||||
$vocab["cancel_registration"] = "取消注册";
|
||||
$vocab["registered_by"] = "由...注册";
|
||||
$vocab["registered_on"] = "注册于";
|
||||
$vocab["event_full"] = "该事件已满.";
|
||||
$vocab["confirm_del_registrant"] = "请确认是否删除 '%s' 注册?";
|
||||
|
||||
// Used in edit_entry_handler.php
|
||||
$vocab["error"] = "错误";
|
||||
$vocab["sched_conflict"] = "时段冲突";
|
||||
$vocab["conflict"] = "该新的预约将会与下列条目冲突";
|
||||
$vocab["no_conflicts"] = "无计划冲突";
|
||||
$vocab["rules_broken"] = "该新的预约将会与下列策略冲突";
|
||||
$vocab["rules_broken_notices"] = "新预约可以与以下策略冲突:";
|
||||
$vocab["no_rules_broken"] = "无规则冲突";
|
||||
$vocab["schedule"] = "计划";
|
||||
$vocab["policy"] = "策略";
|
||||
$vocab["too_many_entries"] = "这些选项会创建太多的条目.<br>请重新选择!";
|
||||
$vocab["returncal"] = "返回到日历视图";
|
||||
$vocab["failed_to_acquire"] = "获取独占式数据库访问失败";
|
||||
$vocab["failed_to_acquire"] = "获取独占数据库访问失败";
|
||||
$vocab["invalid_booking"] = "无效预约";
|
||||
$vocab["must_set_description"] = "你必须设置一个简要说明. 请返回重新输入";
|
||||
$vocab["no_rooms_selected"] = "你必须选择一个房间.";
|
||||
$vocab["mail_subject_approved"] = "已为 %s 会议室预约系统批准了条目"; // $mrbs_company
|
||||
$vocab["mail_subject_rejected"] = "已为 %s 会议室预约系统拒绝了条目"; // $mrbs_company
|
||||
$vocab["mail_subject_more_info"] = "%s 会议室预约系统: 申请的更多信息"; // $mrbs_company
|
||||
$vocab["mail_subject_reminder"] = "%s 会议室预约系统的提醒"; // $mrbs_company
|
||||
$vocab["mail_body_approved"] = "管理员批准了一个条目; 详细情况如下:";
|
||||
$vocab["mail_body_rej_entry"] = "管理员拒绝了一个条目; 详细情况如下:";
|
||||
$vocab["mail_body_more_info"] = "管理员需要有关该条目的更多信息; 详细情况如下:";
|
||||
$vocab["mail_body_reminder"] = "提醒 - 有一个条目等待审批; 详细情况如下:";
|
||||
$vocab["mail_body_repeats_booked"] = "下面的日期已被预约:";
|
||||
$vocab["mail_body_repeats_deleted"] = "下面的预约已被删除:";
|
||||
$vocab["mail_body_exceptions"] = "下面的日期因冲突的原因不能被预约:";
|
||||
$vocab["mail_subject_new_entry"] = "已增加新条目 %s 会议室预约系统"; // $mrbs_company
|
||||
$vocab["mail_subject_changed_entry"] = "条目已修改 %s 会议室预约系统"; // $mrbs_company
|
||||
$vocab["mail_subject_delete"] = "条目已删除 %s 会议室预约系统"; // $mrbs_company
|
||||
$vocab["mail_body_new_entry"] = "一个新条目被预约, 详细情况如下:";
|
||||
$vocab["mail_body_changed_entry"] = "一个条目被修改, 详细情况如下:";
|
||||
$vocab["mail_body_del_entry"] = "一个条目被删除, 详细情况如下:";
|
||||
$vocab["new_value"] = "新";
|
||||
$vocab["old_value"] = "旧";
|
||||
$vocab["reason"] = "原因";
|
||||
$vocab["info_requested"] = "申请信息";
|
||||
$vocab["min_time_before"] = "现在与预约起始之间的最小时间间隔为";
|
||||
$vocab["max_time_before"] = "现在与预约起始之间的最大小时间间隔为";
|
||||
$vocab["earliest_booking_date"] = "最早的预约日期是 %s";
|
||||
$vocab["latest_booking_date"] = "最近的预约日期是 %s";
|
||||
$vocab["max_booking_duration"] = '预约的最长时间长度为 %1$d %2$s';
|
||||
$vocab["max_per_day_global"] = "全系统中每个用户每天最大预约数为";
|
||||
$vocab["max_per_week_global"] = "全系统中每个用户每周最大预约数为";
|
||||
$vocab["max_per_month_global"] = "全系统中每个用户每月最大预约数为";
|
||||
$vocab["max_per_year_global"] = "全系统中每个用户每年最大预约数为";
|
||||
$vocab["max_per_future_global"] = "全系统中每个用户每天最大重要预约数为";
|
||||
$vocab["max_per_day_area"] = "该区域中每个用户每天最大预约数为";
|
||||
$vocab["max_per_week_area"] = "该区域中每个用户每周最大预约数为";
|
||||
$vocab["max_per_month_area"] = "该区域中每个用户每月最大预约数为";
|
||||
$vocab["max_per_year_area"] = "该区域中每个用户每年最大预约数为";
|
||||
$vocab["max_per_future_area"] = "该区域中每个用户每天最大重要预约数为";
|
||||
$vocab["max_per_day_area"] = "该区域中每个用户每天最大预约数为";
|
||||
$vocab["max_per_week_area"] = "该区域中每个用户每周最大预约数为";
|
||||
$vocab["max_per_month_area"] = "该区域中每个用户每月最大预约数为";
|
||||
$vocab["max_per_year_area"] = "该区域中每个用户每年最大预约数为";
|
||||
$vocab["max_per_future_area"] = "该区域中每个用户的最大未完成预订数为";
|
||||
$vocab["max_secs_per_day_global"] = '全系统中每用户每天最大总预约时长为 %1$s %2$s'; // 如2小时
|
||||
$vocab["max_secs_per_week_global"] = '全系统中每用户每周最大总预约时长为 %1$s %2$s';
|
||||
$vocab["max_secs_per_month_global"] = '全系统中每用户每月最大总预约时长为 %1$s %2$s';
|
||||
$vocab["max_secs_per_year_global"] = '全系统中每用户每年最大总预约时长为 %1$s %2$s';
|
||||
$vocab["max_secs_per_future_global"] = '全系统中每用户未完成最大总预约时长为 %1$s %2$s';
|
||||
$vocab["max_secs_per_day_area"] = '该区域中每用户每天最大总预约时长为 %1$s %2$s';
|
||||
$vocab["max_secs_per_week_area"] = '该区域中每用户每周最大总预约时长为 %1$s %2$s';
|
||||
$vocab["max_secs_per_month_area"] = '该区域中每用户每月最大总预约时长为 %1$s %2$s';
|
||||
$vocab["max_secs_per_year_area"] = '该区域中每用户每年最大总预约时长为 %1$s %2$s';
|
||||
$vocab["max_secs_per_future_area"] = '该区域中每用户未完成最大总预约时长为 %1$s %2$s';
|
||||
$vocab["skip_and_book"] = "忽略并预约";
|
||||
$vocab["skip_and_book_note"] = "继续预约, 忽略以前的有冲突的条目";
|
||||
|
||||
// Used in pending.php
|
||||
$vocab["pending"] = "待批准的预约";
|
||||
$vocab["none_outstanding"] = "您没有待批准的预约.";
|
||||
|
||||
// Authentication stuff
|
||||
|
||||
$vocab["accessdenied"] = "访问被拒绝";
|
||||
$vocab["norights"] = "您无权限访问该页面!";
|
||||
$vocab["please_login"] = "请登录";
|
||||
$vocab["users.name"] = "用户登录名";
|
||||
$vocab["users.display_name"] = "用户姓名";
|
||||
$vocab["users.password"] = "密码";
|
||||
$vocab["users.level"] = "权限";
|
||||
$vocab["users.timestamp"] = "最近更新日期";
|
||||
$vocab["users.last_login"] = "最近登录";
|
||||
$vocab["unknown_user"] = "用户不存在或密码错误";
|
||||
$vocab["login"] = "登录";
|
||||
$vocab["logoff"] = "注销";
|
||||
$vocab["username_or_email"] = "用户名或电子邮件地址";
|
||||
$vocab["lost_password"] = "忘记密码?";
|
||||
$vocab["get_new_password"] = "获取新密码";
|
||||
$vocab["password_reset"] = "密码重置";
|
||||
$vocab["password_reset_subject"] = "密码重置请求";
|
||||
$vocab["password_reset_body"] = "有人为此邮箱发起了一个密码重置请求。 " .
|
||||
"如果不是你本人操作请忽略. " .
|
||||
"否则请按照下面的链接指引重置密码。 " .
|
||||
'该链接有效期: %1$d %2$s.';
|
||||
$vocab["enter_username"] = "请输入用户登录名.";
|
||||
$vocab["enter_username_or_email"] = "请输入用户登录名或邮件地址.";
|
||||
$vocab["will_be_sent_instructions"] = "你将会收到一封邮件指引你如何重置密码。";
|
||||
$vocab["reset_password"] = "重置密码";
|
||||
$vocab["pwd_check_email"] = "谢谢。如果用户账号符合" .
|
||||
"你很快会收到一封指引你重置密码的邮件。";
|
||||
$vocab["invalid_link"] = "无效链接";
|
||||
$vocab["pwd_request_failed"] = "邮件无法发送至该用户。";
|
||||
$vocab["link_invalid"] = "密码重置链接无效或过期。";
|
||||
$vocab["enter_new_password"] = "在下面输入两次新密码";
|
||||
$vocab["pwd_must_contain"] = "密码至少包含:";
|
||||
$vocab["pwd_reset_success"] = "密码重置成功。";
|
||||
// Database upgrade code
|
||||
$vocab["database_login"] = "登录数据库";
|
||||
$vocab["upgrade_required"] = "数据库需要升级. 在继续之前请先备份您的数据库.";
|
||||
$vocab["supply_userpass"] = "请提供一个有管理员权限的数据库用户名和密码.";
|
||||
$vocab["contact_admin"] = "如果您不是会议室预约系统的管理员, 请联系 %s."; // $mrbs_admin
|
||||
$vocab["upgrade_to_version"] = "正在升级到数据库版本";
|
||||
$vocab["upgrade_to_local_version"] = "正在升级到数据库本地版本";
|
||||
$vocab["upgrade_completed"] = "数据库升级完成.";
|
||||
$vocab["upgrade_to_version"] = "正在升级到数据库版本";
|
||||
$vocab["upgrade_to_local_version"] = "正在升级到数据库本地版本";
|
||||
$vocab["upgrade_completed"] = "数据库升级完成.";
|
||||
|
||||
// User access levels
|
||||
$vocab["level_0"] = "无";
|
||||
$vocab["level_1"] = "用户";
|
||||
$vocab["level_2"] = "管理员";
|
||||
$vocab["level_3"] = "用户管理员";
|
||||
|
||||
// Authentication database
|
||||
$vocab["user_list"] = "用户清单";
|
||||
$vocab["edit_user"] = "编辑用户";
|
||||
$vocab["delete_user"] = "删除用户";
|
||||
//$vocab["users.name"] = Use the same as above, for consistency.
|
||||
//$vocab["users.password"] = Use the same as above, for consistency.
|
||||
$vocab["users.email"] = "Email 地址";
|
||||
$vocab["password_twice"] = "如果你想修改密码, 请输入两次新密码";
|
||||
$vocab["passwords_not_eq"] = "密码不匹配.";
|
||||
$vocab["password_invalid"] = "密码不符合策略. 它必须包含至少:";
|
||||
$vocab["policy_length"] = "%d 字符";
|
||||
$vocab["policy_alpha"] = "%d 字母";
|
||||
$vocab["policy_lower"] = "%d 小写字母";
|
||||
$vocab["policy_upper"] = "%d 大写字母";
|
||||
$vocab["policy_numeric"] = "%d 数字";
|
||||
$vocab["policy_special"] = "%d 特殊字符";
|
||||
$vocab["add_new_user"] = "新增用户";
|
||||
$vocab["action"] = "动作";
|
||||
$vocab["user"] = "用户";
|
||||
$vocab["administrator"] = "管理员";
|
||||
$vocab["unknown"] = "未知的";
|
||||
$vocab["ok"] = "OK";
|
||||
$vocab["show_my_entries"] = "显示全部我的预约";
|
||||
$vocab["no_users_initial"] = "在数据库中没有用户, 允许创建初始用户";
|
||||
$vocab["no_users_create_first_admin"] = "创建一个用户配置为管理员, 然后你可以登录并创建其他用户.";
|
||||
$vocab["warning_last_admin"] = "警告! 这是最后一个管理员, 所以你不能删除这个用户或者移除其管理权限, 否则你就被锁在系统门外了";
|
||||
$vocab["copy_email_addresses"] = "复制邮箱地址";
|
||||
|
||||
// Used in search.php
|
||||
$vocab["invalid_search"] = "空的或非法的搜索字符.";
|
||||
$vocab["search_results"] = "搜索结果:";
|
||||
$vocab["nothing_found"] = "未找到符合条件的条目.";
|
||||
$vocab["records"] = "第 ";
|
||||
$vocab["through"] = " 至 ";
|
||||
$vocab["of"] = " 共 ";
|
||||
$vocab["previous"] = "前一页";
|
||||
$vocab["next"] = "下一页";
|
||||
$vocab["entry"] = "条目";
|
||||
$vocab["search_button"] = "搜索";
|
||||
$vocab["search_for"] = "搜索";
|
||||
$vocab["from"] = "从";
|
||||
|
||||
// Used in report.php
|
||||
$vocab["report_on"] = "会议室报表";
|
||||
$vocab["report_start"] = "报表起始日期";
|
||||
$vocab["report_end"] = "报表结束日期";
|
||||
$vocab["match_area"] = "区域";
|
||||
$vocab["match_room"] = "房间";
|
||||
$vocab["match_type"] = "类型";
|
||||
$vocab["ctrl_click_type"] = "使用Ctrl键+鼠标点击选取一个以上的类型";
|
||||
$vocab["match_entry"] = "简要说明";
|
||||
$vocab["match_descr"] = "完整说明";
|
||||
$vocab["output"] = "输出";
|
||||
$vocab["summary"] = "汇总";
|
||||
$vocab["format"] = "格式";
|
||||
$vocab["html"] = "HTML";
|
||||
$vocab["csv"] = "CSV";
|
||||
$vocab["ical"] = "生成 iCalendar (.ics) 格式的报表 - 除去期间";
|
||||
$vocab["combination_not_supported"] = "不支持这个格式的输出";
|
||||
$vocab["summarize_by"] = "汇总方式";
|
||||
$vocab["sum_by_descrip"] = "按简要说明汇总";
|
||||
$vocab["sum_by_creator"] = "按预约人汇总";
|
||||
$vocab["sum_by_type"] = "按类型汇总";
|
||||
$vocab["entry_found"] = "个预约被找到";
|
||||
$vocab["entries_found"] = "找到预约";
|
||||
$vocab["summary_header"] = "总共预约(小时)";
|
||||
$vocab["summary_header_per"] = "总共预约(次)";
|
||||
$vocab["summary_header_both"] = "总共预约小时/期间数";
|
||||
$vocab["entries"] = "条目";
|
||||
$vocab["total"] = "全部";
|
||||
$vocab["submitquery"] = "生成报表";
|
||||
$vocab["sort_rep"] = "排序";
|
||||
$vocab["sort_rep_time"] = "起始日期/时间";
|
||||
$vocab["sort_room"] = "房间名";
|
||||
$vocab["fulldescription_short"] = "完整描述";
|
||||
$vocab["both"] = "所有";
|
||||
$vocab["with"] = "有";
|
||||
$vocab["without"] = "无";
|
||||
$vocab["privacy_status"] = "隐私状态";
|
||||
$vocab["search_criteria"] = "搜索条件";
|
||||
$vocab["presentation_options"] = "输出选项";
|
||||
$vocab["delete_entries"] = "删除条目";
|
||||
$vocab["delete_entries_warning"] = "警告! 这将会删除所有符合搜索条件的条目." .
|
||||
"该操作不能回退. 你确定要继续吗?\n\n" .
|
||||
"将要删除的条目数: %s";
|
||||
|
||||
$vocab["delete_entries_failed"] = "不能删除这些条目.";
|
||||
|
||||
// Used in admin.php
|
||||
$vocab["edit"] = "编辑";
|
||||
$vocab["delete"] = "删除";
|
||||
$vocab["rooms"] = "房间";
|
||||
$vocab["in"] = "在";
|
||||
$vocab["noareas"] = "没有区域";
|
||||
$vocab["noareas_enabled"] = "没有启用任何区域.";
|
||||
$vocab["addarea"] = "新增区域";
|
||||
$vocab["name"] = "名称";
|
||||
$vocab["noarea"] = "还没选择区域";
|
||||
$vocab["browserlang"] = "你的浏览器设为下列语言顺序";
|
||||
$vocab["addroom"] = "新增房间";
|
||||
$vocab["capacity"] = "容纳人数";
|
||||
$vocab["norooms"] = "没有定义任何房间.";
|
||||
$vocab["norooms_enabled"] = "没有启用任何房间.";
|
||||
$vocab["administration"] = "房间明细";
|
||||
$vocab["invalid_area_name"] = "这个区域名已被使用!";
|
||||
$vocab["empty_name"] = "你还没有输入一个名称!";
|
||||
|
||||
// Used in edit_area.php and/or edit_room.php
|
||||
$vocab["editarea"] = "修改区域";
|
||||
$vocab["change"] = "修改";
|
||||
$vocab["editroom"] = "修改房间";
|
||||
$vocab["viewroom"] = "查看房间";
|
||||
$vocab["not_found"] = "找不到";
|
||||
$vocab["room_admin_email"] = "会议室管理员 Email";
|
||||
$vocab["area_admin_email"] = "区域管理员 Email";
|
||||
$vocab["area_first_slot_start"] = "第一时段的起始时间(上班时间)";
|
||||
$vocab["area_last_slot_start"] = "最后时段的起始时间(下班时间)";
|
||||
$vocab["area_res_mins"] = "分辨率/时段时长 (分钟)";
|
||||
$vocab["area_def_duration_mins"] = "默认会议时长 (分钟)";
|
||||
$vocab["times_along_top"] = "时间置顶部";
|
||||
$vocab["invalid_area"] = "无效的区域!";
|
||||
$vocab["invalid_room"] = "无效的房间!";
|
||||
$vocab["invalid_room_name"] = "房间名在该区域内已被使用!";
|
||||
$vocab["invalid_email"] = "无效的 Email 地址!";
|
||||
$vocab["invalid_resolution"] = "第一时段、最后时段和分辨率的组合非法!";
|
||||
$vocab["invalid_resolution"] = "无效组合!";
|
||||
$vocab["general_settings"] = "一般设置";
|
||||
$vocab["time_settings"] = "时段设置";
|
||||
$vocab["confirmation_settings"] = "确定设置";
|
||||
$vocab["period_settings"] = "时间区间名称";
|
||||
$vocab["add_period"] = "添加时间区间";
|
||||
$vocab["allow_confirmation"] = "允许暂定预约";
|
||||
$vocab["default_settings_conf"] = "默认设置";
|
||||
$vocab["default_confirmed"] = "确定";
|
||||
$vocab["default_tentative"] = "暂定";
|
||||
$vocab["approval_settings"] = "批准设置";
|
||||
$vocab["enable_approval"] = "预约需要批准";
|
||||
$vocab["enable_reminders"] = "允许用户提醒管理员";
|
||||
$vocab["private_settings"] = "隐私设置";
|
||||
$vocab["allow_private"] = "允许私有预约";
|
||||
$vocab["force_private"] = "强制私有预约";
|
||||
$vocab["default_settings"] = "默认/强制设置";
|
||||
$vocab["default_private"] = "私有";
|
||||
$vocab["default_public"] = "公共";
|
||||
$vocab["private_display"] = "隐私设置 (显示)";
|
||||
$vocab["private_display_label"] = "私有预约该怎样显示?";
|
||||
$vocab["private_display_caution"] = "注意: 在更改这些设置前, 请认真考虑关于隐私的含义!";
|
||||
$vocab["treat_respect"] = "遵守预约中的隐私设置";
|
||||
$vocab["treat_private"] = "强制为私有预约, 忽略预约中的隐私设置";
|
||||
$vocab["treat_public"] = "强制为公共预约, 忽略预约中的隐私设置";
|
||||
$vocab["sort_key"] = "排序字段内容";
|
||||
$vocab["sort_key_note"] = "这是用来对房间排序的字段内容";
|
||||
$vocab["booking_policies"] = "预约策略";
|
||||
$vocab["min_book_ahead"] = "高级预约 - 最少";
|
||||
$vocab["max_book_ahead"] = "高级预约 - 最多";
|
||||
$vocab["booking_creation"] = "预约创建";
|
||||
$vocab["booking_deletion"] = "预约删除";
|
||||
$vocab["booking_limits"] = "预约数量限制";
|
||||
$vocab["booking_limits_secs"] = "预约总时长限制";
|
||||
$vocab["booking_durations"] = "预约区间限制";
|
||||
$vocab["max_duration"] = "最大区间";
|
||||
$vocab["this_area"] = "本区域";
|
||||
$vocab["whole_system"] = "整个系统";
|
||||
$vocab["whole_system_note"] = "用于整个系统的取值已在配置文件中设置";
|
||||
$vocab["max_per_day"] = "每天最大数";
|
||||
$vocab["max_per_week"] = "每周最大数";
|
||||
$vocab["max_per_month"] = "每月最大数";
|
||||
$vocab["max_per_year"] = "每年最大数";
|
||||
$vocab["max_per_future"] = "未来最大数";
|
||||
$vocab["max_secs_per_day"] = "每天最长时间";
|
||||
$vocab["max_secs_per_week"] = "每周最长时间";
|
||||
$vocab["max_secs_per_month"] = "每月最长时间";
|
||||
$vocab["max_secs_per_year"] = "每年最长时间";
|
||||
$vocab["max_secs_per_future"] = "未来最长时间";
|
||||
$vocab["custom_html"] = "自定义 HTML";
|
||||
$vocab["custom_html_note"] = "这一项可以用来显示你自己的 HTML, 比如说一个嵌入的 Google 地图";
|
||||
$vocab["email_list_note"] = "输入一个以逗号或者换行分割的 Email 地址列表";
|
||||
$vocab["mode"] = "模式";
|
||||
$vocab["mode_periods"] = "期间";
|
||||
$vocab["mode_times"] = "时段";
|
||||
$vocab["times_only"] = "仅时段模式";
|
||||
$vocab["enabled"] = "启用";
|
||||
$vocab["disabled"] = "禁用";
|
||||
$vocab["disabled_area_note"] = "如果这个区域被禁用, 它他就不会在日历视图中显示出来, " .
|
||||
"并且将不能预约该区域内的房间. 然而已存在的预约" .
|
||||
"将会保留并且能在搜索和报表结果中显示.";
|
||||
$vocab["disabled_room_note"] = "如果这个房间被禁用, 它他就不会在日历视图中显示出来, " .
|
||||
"并且它也不能用来预约. 然而已存在的预约" .
|
||||
"将会保留并且能在搜索和报表结果中显示.";
|
||||
$vocab["book_ahead_note_periods"] = "当使用期间时, 预约前时间往下舍入到最接近的整天.";
|
||||
$vocab["invalid_types"] = "无效类型";
|
||||
$vocab["invalid_types_note"] = "有些类型不允许在该房间中使用。";
|
||||
|
||||
// Used in edit_users.php
|
||||
$vocab["name_empty"] = "你必须输入一个名字.";
|
||||
$vocab["name_not_unique"] = "已存在. 请选用另外一个名字.";
|
||||
|
||||
// Used in del.php
|
||||
$vocab["deletefollowing"] = "这个操作会删除下列的预约";
|
||||
$vocab["and_n_more"] = "且多 %s "; // %s rather than %d because n is the output of NumberFormatter::format()
|
||||
$vocab["sure"] = "确定吗?";
|
||||
$vocab["YES"] = "是";
|
||||
$vocab["NO"] = "否";
|
||||
$vocab["delarea"] = "在删除这个区域前, 你必须先删除在这个区域里的所有房间<p>";
|
||||
|
||||
// Used in help.php
|
||||
$vocab["about_mrbs"] = "关于会议室预约系统";
|
||||
$vocab["mrbs_version"] = "系统版本";
|
||||
$vocab["db_schema_version"] = "数据库版本";
|
||||
$vocab["db_local_schema_version"] = "数据库本地版本";
|
||||
$vocab["config_details"] = "配置详情";
|
||||
$vocab["server_details"] = "服务器详情";
|
||||
$vocab["database"] = "数据库";
|
||||
$vocab["system"] = "操作系统";
|
||||
$vocab["servertime"] = "服务器时间";
|
||||
$vocab["server_software"] = "服务器软件";
|
||||
$vocab["extensions"] = "扩展";
|
||||
$vocab["please_contact"] = "请联系%s,关于任何无法在此解决的问题.";
|
||||
|
||||
// Used in import.php
|
||||
$vocab["import_icalendar"] = "导入一个 iCalendar 文件";
|
||||
$vocab["area_room_settings"] = "区域和房间";
|
||||
$vocab["other_settings"] = "其他设置";
|
||||
$vocab["import_intro"] = "这个表单允许你将一个与RFC 5545兼容的 " .
|
||||
"iCalendar 文件导入到系统. 只有那些" .
|
||||
"重复规则带有与MRBS内等价的重复类型的" .
|
||||
"循环事件才会被导入.";
|
||||
$vocab["supported_file_types"] = "支持以下文件类型:";
|
||||
$vocab["import_text_file"] = "非压缩 iCalendar 文件";
|
||||
$vocab["import_zip"] = "zip 文档";
|
||||
$vocab["import_gzip"] = "gzip 文件";
|
||||
$vocab["import_bzip2"] = "bzip2 文件";
|
||||
$vocab["file_name"] = "文件名";
|
||||
$vocab["import"] = "导入";
|
||||
$vocab["upload_failed"] = "上传失败";
|
||||
$vocab["max_allowed_file_size"] = "允许的最大文件大小 %s";
|
||||
$vocab["no_file"] = "没有文件被上传";
|
||||
$vocab["could_not_process"] = "导入失败:无法处理文件";
|
||||
|
||||
$vocab["badly_formed_ics"] = "格式错误的 VCALENDAR 文件";
|
||||
$vocab["default_room"] = "默认房间";
|
||||
$vocab["default_room_note"] = "要使用的房间未指定位置属性";
|
||||
$vocab["area_room_order"] = "顺序";
|
||||
$vocab["area_room_order_note"] = "LOCATION 属性里的区域和房间名的顺序";
|
||||
$vocab["area_room"] = "区域-房间";
|
||||
$vocab["room_area"] = "房间-区域";
|
||||
$vocab["area_room_delimiter"] = "分隔符";
|
||||
$vocab["area_room_delimiter_note"] = "将 LOCATION 属性内的区域名和房间名分隔的字符串. " .
|
||||
"如果没有找到分隔符, MRBS将会找一个与 " .
|
||||
"LOCATION 名称相同的唯一的房间";
|
||||
$vocab["area_room_create"] = "如果需要,则创建房间";
|
||||
$vocab["default_type"] = "默认类型";
|
||||
$vocab["room_does_not_exist_no_area"] = "房间不存在,不能被添加 - 没有指定区域";
|
||||
$vocab["room_not_unique_no_area"] = "房间名不唯一. 在不指定区域的情况下,不知道该选哪个.";
|
||||
$vocab["area_does_not_exist"] = "不存在的区域:";
|
||||
$vocab["room_does_not_exist"] = "不存在的房间:";
|
||||
$vocab["creating_new_area"] = "正在创建区域:";
|
||||
$vocab["creating_new_room"] = "正在创建房间:";
|
||||
$vocab["could_not_create_area"] = "不能创建区域";
|
||||
$vocab["could_not_create_room"] = "不能创建房间";
|
||||
$vocab["could_not_find_room"] = "找不到房间";
|
||||
$vocab["could_not_import"] = "不能导入";
|
||||
$vocab["no_LOCATION"] = "VEVENT 没有包含 LOCATION 属性";
|
||||
$vocab["invalid_RRULE"] = "RRULE 无效: 缺少 FREQ 部分";
|
||||
$vocab["more_than_one_BYDAY"] = "当 FREQ= 时, 不支持多个 BYDAY 值";
|
||||
$vocab["BYDAY_equals_5"] = "MRBS不支持 BYDAY 数值为 5";
|
||||
$vocab["unsupported_FREQ"] = "MRBS不支持 FREQ=";
|
||||
$vocab["unsupported_COUNT"] = "MRBS尚不支持 COUNT";
|
||||
$vocab["no_indefinite_repeats"] = "MRBS尚不支持无限重复";
|
||||
$vocab["events_imported"] = "事件已被导入";
|
||||
$vocab["events_not_imported"] = "事件未被导入";
|
||||
|
||||
// Used in DataTables
|
||||
$vocab["show_hide_columns"] = "显示/隐藏列";
|
||||
$vocab["copy"] = "复制";
|
||||
// "csv" already defined above
|
||||
$vocab["excel"] = "Excel";
|
||||
$vocab["pdf"] = "PDF";
|
||||
$vocab["print"] = "Print";
|
||||
$vocab["restore_original"] = "恢复原样";
|
||||
|
||||
// Entry types
|
||||
$vocab["type.I"] = "内部使用";
|
||||
$vocab["type.E"] = "外部使用";
|
||||
|
||||
// General
|
||||
$vocab["fatal_error"] = "遇到了一个致命的错误。请联系管理员.";
|
||||
$vocab["fatal_db_error"] = "致命错误: 非常不幸, 数据库现在不可用.";
|
||||
$vocab["fatal_no_tables"] = "致命错误:数据库表不存在或无法访问.";
|
||||
$vocab["session_expired"] = "会话已过期.";
|
||||
$vocab["browser_not_supported"] = "你的浏览器不支持 %s. 需要升级到最近版本或使用其他浏览器 ";
|
||||
$vocab["back"] = "后退";
|
||||
// 等保整改新增词条
|
||||
$vocab["account_locked"] = "登录失败次数过多,账号已被临时锁定,请约 %d 分钟后重试。";
|
||||
$vocab["change_password"] = "修改密码";
|
||||
$vocab["current_password"] = "当前密码";
|
||||
$vocab["new_password"] = "新密码";
|
||||
$vocab["confirm_password"] = "确认新密码";
|
||||
$vocab["password_changed"] = "密码修改成功。";
|
||||
$vocab["old_pwd_invalid"] = "当前密码不正确。";
|
||||
$vocab["pwd_same_as_old"] = "新密码不能与当前密码相同。";
|
||||
$vocab["pwd_expired_msg"] = "您的密码已过期或为初始密码,请先修改密码后再继续使用系统。";
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MRBS;
|
||||
|
||||
/**
|
||||
* 等保2.0二级整改:安全审计日志(JSON Lines,文件追加)
|
||||
*
|
||||
* MRBS 自带 Logger 仅是 PSR-3 外壳(notice/info/debug 只触发 E_USER_NOTICE,
|
||||
* 无法落盘),因此这里自行实现一个轻量文件审计器:
|
||||
* - 配置项 $audit_log_file 指定日志路径(默认 <web>/audit/security_audit.log)
|
||||
* - 超过 20MB 自动轮转一次(保留一份 .old)
|
||||
* - 写失败静默(不影响业务),避免审计故障拖垮登录流程
|
||||
*
|
||||
* 事件类型(event):
|
||||
* LOGIN_OK / LOGIN_FAIL / LOGIN_BLOCKED / PWD_CHANGE / PWD_CHANGE_FAIL
|
||||
* PWD_RESET / PWD_ADMIN_SET / LOGOUT
|
||||
*
|
||||
* 日志格式(每行一个 JSON 对象,便于 grep / 导入 SIEM):
|
||||
* {"ts":"...","ip":"...","user":"...","event":"...","detail":"..."}
|
||||
*/
|
||||
class Audit
|
||||
{
|
||||
public static function log(string $event, ?string $user = null, string $detail = '') : void
|
||||
{
|
||||
global $audit_log_file, $timezone;
|
||||
|
||||
$file = $audit_log_file;
|
||||
if (empty($file))
|
||||
{
|
||||
$file = dirname(__DIR__, 2) . '/audit/security_audit.log';
|
||||
}
|
||||
|
||||
// 确保日志目录存在(部署时可能尚未手工创建 audit/ 目录)
|
||||
$dir = dirname($file);
|
||||
if (!is_dir($dir))
|
||||
{
|
||||
@mkdir($dir, 0775, true);
|
||||
}
|
||||
|
||||
// 防审计文件无限增长:超过 20MB 轮转一次(保留一份 .old)
|
||||
if (is_file($file) && (filesize($file) > 20 * 1024 * 1024))
|
||||
{
|
||||
@rename($file, $file . '.old');
|
||||
}
|
||||
|
||||
// 统一按 MRBS 配置时区记录时间戳(登录处理发生在 init_area() 之前,
|
||||
// 彼时 PHP 默认时区可能仍是 UTC,会导致同一日志两种时区混写)
|
||||
$old_tz = date_default_timezone_get();
|
||||
if (!empty($timezone))
|
||||
{
|
||||
@date_default_timezone_set($timezone);
|
||||
}
|
||||
$ts = date('c');
|
||||
if (!empty($old_tz))
|
||||
{
|
||||
@date_default_timezone_set($old_tz);
|
||||
}
|
||||
|
||||
$record = array(
|
||||
'ts' => $ts,
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? '-',
|
||||
'user' => $user ?? '-',
|
||||
'event' => $event,
|
||||
'detail' => $detail
|
||||
);
|
||||
|
||||
@file_put_contents($file, json_encode($record, JSON_UNESCAPED_UNICODE) . "\n",
|
||||
FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,981 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?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 MRBS\Audit;
|
||||
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
|
||||
|
||||
// ===== 等保整改:登录成功审计 =====
|
||||
Audit::log('LOGIN_OK', $valid_username);
|
||||
|
||||
// ===== 等保整改:口令到期 / 存量账号首登 → 强制改密 =====
|
||||
// 必须在 logonUser()(其内部调用 session_write_close())之前写入会话,否则会丢失
|
||||
$auth_obj = auth();
|
||||
if (method_exists($auth_obj, 'needsPasswordChange') &&
|
||||
$auth_obj->needsPasswordChange($valid_username))
|
||||
{
|
||||
$_SESSION['mrbs_force_pwd_change'] = 1;
|
||||
}
|
||||
|
||||
$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))
|
||||
{
|
||||
// ===== 等保整改:登录失败 / 账号锁定 审计与提示区分 =====
|
||||
$login_name = $this->form['username'] ?? '';
|
||||
$auth_obj = auth();
|
||||
$error = get_vocab('unknown_user');
|
||||
|
||||
if (method_exists($auth_obj, 'getLoginBlocked') && $auth_obj->getLoginBlocked())
|
||||
{
|
||||
global $login_lock_duration;
|
||||
$minutes = (int)ceil(($login_lock_duration ?? 900) / 60);
|
||||
$error = get_vocab('account_locked', $minutes);
|
||||
Audit::log('LOGIN_BLOCKED', $login_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
Audit::log('LOGIN_FAIL', $login_name);
|
||||
}
|
||||
|
||||
$this->authGet($this->form['target_url'], $this->form['returl'], $error);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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. (change_password.php 同样放行:页面自身强制要求已登录,未登录会引导登录)
|
||||
if (in_array($page, array('reset_password.php', 'reset_password_handler.php', 'change_password.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