包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、JS空集合保护、 会话过期体验优化(403 JSON)、display_errors 关闭、 固定 key 根治 Integrity check failed 等全部改动 注意:config.inc.php/.htaccess/.user.ini 含敏感信息, 通过 .gitignore 排除,勿推送到公开仓库。
496 lines
14 KiB
PHP
496 lines
14 KiB
PHP
<?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);
|
|
}
|
|
|
|
}
|