MRBS 1.12.2 等保2.0二级整改完整提交

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

注意:config.inc.php/.htaccess/.user.ini 含敏感信息,
通过 .gitignore 排除,勿推送到公开仓库。
This commit is contained in:
人事系统开发
2026-09-09 16:55:02 +08:00
commit 1ba6efd8ed
2151 changed files with 528780 additions and 0 deletions
+495
View File
@@ -0,0 +1,495 @@
<?php
declare(strict_types=1);
namespace MRBS\Form;
// This is a limited implementation of the HTML5 DOM and is optimised for use by
// MRBS forms. It makes a number of simplifications and restrictions. In particular
// it assumes that:
// (a) an element can only contain one text node, and
// (b) that text node either comes before or after all the element nodes that it contains
//
// In the full DOM an element can contain multiple text nodes, for example
// <p>Some text<b>a bold bit</b>some more text</p>
//
// If structures like these are required ten they can usually be achieved by wrapping the raw
// text nodes in a <span>.
use function MRBS\escape_html;
use function MRBS\is_assoc;
class Element
{
private $tag;
private $self_closing;
private $attributes = array();
private $text = null;
private $raw = false;
private $text_at_start = false;
private $elements = [];
private $next = null;
private $prev = null;
public function __construct(string $tag, bool $self_closing=false)
{
$this->tag = $tag;
$this->self_closing = $self_closing;
}
// If $raw is true then the text will not be put through escape_html(). Only to
// be used for trusted text.
public function setText(string $text, bool $text_at_start=false, bool $raw=false) : Element
{
if ($this->self_closing)
{
throw new \Exception("A self closing element cannot contain text.");
}
$this->text = $text;
$this->text_at_start = $text_at_start;
$this->raw = $raw;
return $this;
}
public function getAttribute(string $name)
{
return (isset($this->attributes[$name])) ? $this->attributes[$name] : null;
}
// A value of true allows for the setting of boolean attributes such as
// 'required' and 'disabled'
public function setAttribute(string $name, $value=true) : Element
{
$this->attributes[$name] = $value;
return $this;
}
public function setAttributes(array $attributes) : Element
{
foreach ($attributes as $name => $value)
{
$this->setAttribute($name, $value);
}
return $this;
}
public function removeAttribute(string $name) : Element
{
unset($this->attributes[$name]);
return $this;
}
public function getElement(string $key) : Element
{
return $this->elements[$key];
}
public function setElement(string $key, Element $element) : Element
{
$this->elements[$key] = $element;
return $this;
}
public function getElements() : array
{
return $this->elements;
}
public function setElements(array $elements) : Element
{
$this->elements = $elements;
return $this;
}
public function addElement(?Element $element=null, ?string $key=null) : Element
{
if (isset($element))
{
if (isset($key))
{
$this->elements[$key] = $element;
}
else
{
$this->elements[] = $element;
}
}
return $this;
}
public function addElements(array $elements) : Element
{
foreach ($elements as $element)
{
$this->addElement($element);
}
return $this;
}
public function removeElement(string $key) : Element
{
unset($this->elements[$key]);
return $this;
}
public function next(?Element $element=null) : ?Element
{
if (isset($element))
{
$this->next = $element;
return $this;
}
elseif (isset($this->next))
{
return $this->next;
}
else
{
return null;
}
}
public function prev(?Element $element=null): ?Element
{
if (isset($element))
{
$this->prev = $element;
return $this;
}
elseif (isset($this->prev))
{
return $this->prev;
}
else
{
return null;
}
}
public function addClass(string $class) : Element
{
$classes = $this->getAttribute('class');
$classes = (isset($classes)) ? explode(' ', $classes) : array();
if (!in_array($class, $classes))
{
$classes[] = $class;
$this->setAttribute('class', implode(' ', $classes));
}
return $this;
}
// Add a set of select options to an element, eg to a <select> or <datalist> element.
// $options An array of options for the select element. Can be a one- or two-dimensional
// array. If it's two-dimensional then the keys of the outer level represent
// <optgroup> labels. The inner level can be a simple array or an associative
// array with value => text members for each <option> in the <select> element.
// $selected The value(s) of the option(s) that are selected. Can be a single value
// or an array of values.
// $associative Whether to treat the options as a simple or an associative array. (This
// parameter is necessary because if you index an array with strings that look
// like integers then PHP casts the keys to integers and the array becomes a
// simple array). Can take the following values:
// true treat as an associative array
// false treat as a simple array
// null auto-detect
// $for_datalist Whether the options are intended for use in a <datalist>.
public function addSelectOptions(array $options, $selected=null, ?bool $associative=null, bool $for_datalist=false) : Element
{
// Trivial case
if (empty($options))
{
return $this;
}
if (!isset($associative))
{
$associative = is_assoc($options);
}
// It's possible to have multiple options selected
if (!is_array($selected))
{
$selected = array($selected);
}
// Test whether $options is a one-dimensional or two-dimensional array.
// If two-dimensional then we need to use <optgroup>s.
if (is_array(reset($options))) // cannot use $options[0] because $options may be associative
{
if ($for_datalist)
{
throw new \InvalidArgumentException("Datalists cannot have <optgroup> elements.");
}
foreach ($options as $group => $group_options)
{
$optgroup = new ElementOptgroup();
$optgroup->setAttribute('label', $group)
->addSelectOptions($group_options, $selected, $associative);
$this->addElement($optgroup);
}
}
else
{
foreach ($options as $key => $text)
{
$option = new ElementOption();
// We need to be careful about strings with multiple consecutive spaces in them. If we have a <select> element
// with simple options of the form <option>Joe Smith</option>, then the text "Joe Smith" will be contracted
// to "Joe Smith". This means that (a) it will look wrong on the form and (b) the wrong value will be passed
// through to the form handler. One way of getting round this would be to substitute the extra spaces with
// non-breaking space characters. However, although this will now look correct on the form, the value that is
// passed through to the form handler will contain non-breaking spaces instead of ordinary spaces. A query
// using this value to find a row in the database will rely on the database engine treating ordinary and non-
// breaking spaces as equivalent. While this will be true for many collations, it won't always be so.
//
// The solution is to place the text as is in the value attribute, but to replace the spaces in the text node
// with non-breaking spaces, eg <option value="Joe Smith">Joe Smith</option>. Because extra spaces in
// attributes are preserved, the correct string will be passed through to the form handler. And because
// there are now non-breaking spaces in the text node, the string is displayed properly in the browser. If the
// options are in the form of an associative array with values and text, then the value will be used for the
// value attribute and the text can have its spaces replaced.
//
// This solution doesn't work though for <datalist> elements, at least not when using Chrome or Safari.
// According to https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/datalist "Each <option>
// element should have a value attribute, which represents a suggestion to be entered into the input. It can
// also have a label attribute, or, missing that, some text content, which may be displayed by the browser
// instead of value (Firefox), or in addition to value (Chrome and Safari, as supplemental text). The exact
// content of the drop-down menu depends on the browser, but when clicked, content entered into control field
// will always come from the value attribute." In other words, if the value and text are different - even in
// the type of space character used - then, when using Chrome and Safari, they are both displayed by the
// browser, which looks a little odd, when they are essentially the same string.
//
// So for <datalist> elements, which are never associative anyway, we just use the value attribute and have an
// empty text node, eg <option value="Joe Smith"></option>.
//
// See also https://github.com/meeting-room-booking-system/mrbs-code/issues/3871 and
// https://stackoverflow.com/questions/79629259/does-mysql-distinguish-betwen-ordinary-and-non-breaking-spaces-in-a-query
$value = ($associative) ? $key : $text;
$option->setAttribute('value', $value);
if (!$for_datalist || $associative)
{
// If it's a string replace the second and subsequent spaces with non-breaking spaces
$text = (is_string($text)) ? preg_replace('/(?<= ) /', "\xc2\xa0", $text) : strval($text);
$option->setText($text);
}
if (!$for_datalist && in_array($value, $selected))
{
$option->setAttribute('selected');
}
$this->addElement($option);
}
}
return $this;
}
// $checked is either a scalar or an array of keys that are checked
public function addCheckboxOptions(array $options, string $name, $checked=null, $associative=null, bool $disabled=false): Element
{
// Trivial case
if (empty($options))
{
return $this;
}
if (is_scalar($checked))
{
$checked = array($checked);
}
if (!isset($associative))
{
$associative = is_assoc($options);
}
foreach ($options as $key => $value)
{
if (!$associative)
{
$key = $value;
}
$checkbox = new ElementInputCheckbox();
$checkbox->setAttributes(array('name' => $name,
'value' => $key));
if (isset($checked) && (in_array($key, $checked)))
{
$checkbox->setChecked(true);
}
if ($disabled)
{
$checkbox->setAttribute('disabled', true);
}
$label = new ElementLabel();
$label->setText(strval($value))
->addElement($checkbox);
$this->addElement($label);
}
return $this;
}
public function addRadioOptions(array $options, string $name, $checked=null, $associative=null, bool $disabled=false): Element
{
// Trivial case
if (empty($options))
{
return $this;
}
if (!isset($associative))
{
$associative = is_assoc($options);
}
foreach ($options as $key => $value)
{
if (!$associative)
{
$key = $value;
}
$radio = new ElementInputRadio();
$radio->setAttributes(array('name' => $name,
'value' => $key,
'disabled' => $disabled));
if (isset($checked) && ($key == $checked))
{
$radio->setAttribute('checked');
}
$label = new ElementLabel();
$label->setText(strval($value))
->addElement($radio);
$this->addElement($label);
}
return $this;
}
public function render() : void
{
echo $this->toHTML();
}
// Turns the form into HTML. HTML escaping is done here.
// If $no_whitespace is true, then don't put any whitespace after opening or
// closing tags. This is useful for structures such as
// <label><input>text</label> where whitespace after the <input> tag would
// affect what the browser displays on the screen.
public function toHTML(bool $no_whitespace=false): string
{
$html = "";
$prev = $this->prev();
if (isset($prev))
{
$html .= $prev->toHTML();
}
$terminator = ($no_whitespace) ? '' : "\n";
$html .= "<" . $this->tag;
foreach ($this->attributes as $key => $value)
{
if (!isset($value) || ($value === false))
{
// a boolean attribute, or else an empty attribute, that should be omitted.
// We allow the empty string, '', because that can be used, for example, in
// 'value=""' as an attribute for the <option> element in a <select> element
// that has the 'required' attribute set.
continue;
}
$html .= " $key";
if ($value !== true)
{
// boolean attributes, eg 'required', don't need a value
$html .= '="' . escape_html($value) . '"';
}
}
$html .= ">";
if ($this->self_closing)
{
$html .= $terminator;
}
else
{
if (isset($this->text) && $this->text_at_start)
{
$html .= self::escapeText($this->text, $this->raw);
}
if (!empty($this->elements))
{
// If this element contains text, then don't use a terminator, otherwise
// unwanted whitespace will be introduced.
if (!isset($this->text))
{
$html .= $terminator;
}
foreach ($this->elements as $element)
{
$html .= $element->toHTML(isset($this->text));
}
}
if (isset($this->text) && !$this->text_at_start)
{
$html .= self::escapeText($this->text, $this->raw);
}
$html .= "</" . $this->tag . ">$terminator";
}
$next = $this->next();
if (isset($next))
{
$html .= $next->toHTML();
}
return $html;
}
private static function escapeText($text, bool $raw=false)
{
return ($raw) ? $text : escape_html($text);
}
}