MRBS 1.12.2 等保2.0二级整改完整提交
Docker image / push (push) Canceled after 0s

包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
人事系统开发
2026-09-08 21:19:47 +08:00
commit 48092cab42
2221 changed files with 659586 additions and 0 deletions
@@ -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']));
}