包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementA extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('a');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementButton extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('button');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementDatalist extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('datalist');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementDiv extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('div');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class ElementFieldset extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('fieldset');
|
||||
}
|
||||
|
||||
|
||||
// $legend can be
|
||||
// (a) an ElementLegend object, or
|
||||
// (b) another element, or
|
||||
// (c) a string
|
||||
// If it is (b) or (c) then it is wrapped inside a Legend element.
|
||||
public function addLegend($legend) : ElementFieldset
|
||||
{
|
||||
if (is_object($legend) &&
|
||||
(__NAMESPACE__ . "\\ElementLegend" == get_class($legend)))
|
||||
{
|
||||
$element = $legend;
|
||||
}
|
||||
else
|
||||
{
|
||||
$element = new ElementLegend();
|
||||
|
||||
if (is_string($legend))
|
||||
{
|
||||
$element->setText($legend);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assumed to be an object of class 'Element' if it is not a string
|
||||
$element->addElement($legend);
|
||||
}
|
||||
}
|
||||
|
||||
$this->addElement($element);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementImg extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('img', true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
abstract class ElementInput extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('input', true);
|
||||
$this->setAttribute('type', 'text');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputCheckbox extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'checkbox');
|
||||
}
|
||||
|
||||
|
||||
public function setChecked($checked=true): ElementInputCheckbox
|
||||
{
|
||||
if ($checked)
|
||||
{
|
||||
$this->setAttribute('checked');
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->removeAttribute('checked');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputDatalist extends ElementInput
|
||||
{
|
||||
private static $list_prefix = "mrbs_";
|
||||
private static $list_number = 1;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// One problem with using a datalist with an input element is the way different browsers
|
||||
// handle autocomplete. If you have autocomplete on, and also an id or name attribute, then some
|
||||
// browsers, eg Edge, will bring the history up on top of the datalist options so that you can't
|
||||
// see the first few options. But if you have autocomplete off, then other browsers, eg Chrome,
|
||||
// will not present the datalist options at all. This can be fixed in JavaScript by having a second,
|
||||
// hidden, input which holds the actual form value and mirrors the visible input. Because we can't
|
||||
// rely on JavaScript being enabled we will create the basic HTML using autocomplete on, ie the default,
|
||||
// which is the least bad alternative. One disadvantage of this method is that the label is no longer
|
||||
// tied to the visible input, but this isn't as important for a text input as it is, say, for a checkbox
|
||||
// or radio button.
|
||||
parent::__construct();
|
||||
|
||||
// Provide a unique id to link the list with the input.
|
||||
// Doesn't matter what it is as it won't be used elsewhere.
|
||||
$list_id = self::$list_prefix . self::$list_number;
|
||||
self::$list_number++;
|
||||
|
||||
$this->setAttributes(array('type' => 'text',
|
||||
'list' => $list_id));
|
||||
|
||||
$datalist = new ElementDatalist();
|
||||
$datalist->setAttribute('id', $list_id);
|
||||
|
||||
$this->next($datalist);
|
||||
}
|
||||
|
||||
|
||||
public function addDatalistOptions(array $options, ?bool $associative=null): ElementInputDatalist
|
||||
{
|
||||
// Put a <select> wrapper around the options so that browsers that don't
|
||||
// support <datalist> will still have the options in their DOM and then
|
||||
// the JavaScript polyfill can find them and do something with them
|
||||
$select = new ElementSelect();
|
||||
$select->addClass('none');
|
||||
$select->addSelectOptions($options, null, $associative, true);
|
||||
|
||||
$this->next($this->next()->addElement($select));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputDate extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'date');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputEmail extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'email');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputFile extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'file');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputHidden extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct(?string $name=null, $value=null)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'hidden');
|
||||
|
||||
if (isset($name))
|
||||
{
|
||||
$this->setAttribute('name', $name);
|
||||
}
|
||||
|
||||
if (isset($value))
|
||||
{
|
||||
if (is_bool($value))
|
||||
{
|
||||
$value = ($value) ? 1 : 0;
|
||||
}
|
||||
$this->setAttribute('value', $value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputImage extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'image');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputNumber extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttributes(array('type' => 'number',
|
||||
'step' => '1'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputPassword extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'password');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputRadio extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'radio');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputSearch extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'search');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputSubmit extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'submit');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputText extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'text');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputTime extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'time');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementInputUrl extends ElementInput
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('type', 'url');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementLabel extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('label');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementLegend extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('legend');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class ElementOptgroup extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('optgroup');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class ElementOption extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('option');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementP extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('p');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementSelect extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('select');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementSpan extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('span');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
class ElementTextarea extends Element
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('textarea');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
// Fields consider of a 'label' element, ie a <label>, and a control
|
||||
// element, eg an <input> or <select>, all wrapped in a <div>. For example
|
||||
// <div>
|
||||
// <label></label>
|
||||
// <input>
|
||||
// </div>
|
||||
|
||||
abstract class Field extends Element
|
||||
{
|
||||
// $is_group records whether the field consists of a group of controls
|
||||
// (eg radio buttons) or just a single control, in which case a label
|
||||
// can be associated with it.
|
||||
protected $is_group = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Wrap all fields in a <div> ... </div>
|
||||
parent::__construct('div');
|
||||
$this->addElement(new ElementLabel(), 'label');
|
||||
}
|
||||
|
||||
|
||||
public function addControl(Element $element): Field
|
||||
{
|
||||
$this->addElement($element, 'control');
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function getControl(): Element
|
||||
{
|
||||
return $this->getElement('control');
|
||||
}
|
||||
|
||||
|
||||
public function setControl(Element $element): Field
|
||||
{
|
||||
$this->setElement('control', $element);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function removeControl(): Field
|
||||
{
|
||||
$this->removeElement('control');
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// If $raw is true then the text will not be put through escape_html(). Only to
|
||||
// be used for trusted text.
|
||||
public function setLabel($text, bool $text_at_start=false, bool $raw=false): Field
|
||||
{
|
||||
$label = $this->getElement('label');
|
||||
$label->setText($text, $text_at_start, $raw);
|
||||
$this->setElement('label', $label);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Sets an attribute for the field control. Also takes care of the label
|
||||
// by associating the label with the control using a 'for' attribute, by
|
||||
// using the 'id' if one is given, or if not, by assuming that the 'id'
|
||||
// is the same as the 'name' (unless $add_id is FALSE, in which case an id
|
||||
// won't be added).
|
||||
public function setControlAttribute(string $name, $value=true, bool $add_id=true): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
|
||||
// If this is the name attribute and we haven't yet got an id, then
|
||||
// make the id the same as the name - unless $add_id is FALSE
|
||||
if ($add_id && ($name == 'name') && (null === $elements['control']->getAttribute('id')))
|
||||
{
|
||||
$this->setControlAttribute('id', $value);
|
||||
}
|
||||
|
||||
// If this is an id and it's not a group field, then associate the
|
||||
// label with the id
|
||||
if (!$this->is_group && ($name == 'id'))
|
||||
{
|
||||
$elements['label']->setAttribute('for', $value);
|
||||
}
|
||||
|
||||
$elements['control']->setAttribute($name, $value);
|
||||
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Sets the attributes for the field control.
|
||||
public function setControlAttributes(array $attributes, bool $add_id=true): Field
|
||||
{
|
||||
foreach ($attributes as $key => $value)
|
||||
{
|
||||
$this->setControlAttribute($key, $value, $add_id);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function addControlClass(string $class): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
$elements['control']->addClass($class);
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function addLabelClass(string $class): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
$elements['label']->addClass($class);
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function setControlChecked($checked=true): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
$elements['control']->setChecked($checked);
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function setControlText(string $text): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
$elements['control']->setText($text);
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function addControlElement(Element $element): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
$elements['control']->addElement($element);
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function addLabelElement(Element $element): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
$elements['label']->addElement($element);
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function setLabelAttribute(string $name, $value=true): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
$elements['label']->setAttribute($name, $value);
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Sets the attributes for the field label. No need to do
|
||||
// the 'for' attribute, as that is done automatically when you
|
||||
// set the 'id' in the control attributes.
|
||||
public function setLabelAttributes(array $attributes): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
|
||||
foreach ($attributes as $key => $value)
|
||||
{
|
||||
$elements['label']->setAttribute($key, $value);
|
||||
}
|
||||
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function removeLabelAttribute(string $name): Field
|
||||
{
|
||||
$elements = $this->getElements();
|
||||
$elements['label']->removeAttribute($name);
|
||||
$this->setElements($elements);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Adds a hidden input to the form
|
||||
public function addHiddenInput(string $name, $value) : Field
|
||||
{
|
||||
$element = new ElementInputHidden($name, $value);
|
||||
$this->addElement($element);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldButton extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementButton());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldDiv extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementDiv());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputCheckbox extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputCheckbox());
|
||||
}
|
||||
|
||||
public function setChecked($checked=true)
|
||||
{
|
||||
$control = $this->getControl();
|
||||
$control->setChecked($checked);
|
||||
$this->setControl($control);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputCheckboxGroup extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementDiv())
|
||||
->setControlAttributes(array('class' => 'group'));
|
||||
$this->is_group = true;
|
||||
}
|
||||
|
||||
|
||||
public function addCheckboxOptions(array $options, string $name, $checked=null, $associative=null, bool $disabled=false): Element
|
||||
{
|
||||
$element = $this->getControl();
|
||||
$element->addCheckboxOptions($options, $name, $checked, $associative, $disabled);
|
||||
$this->setControl($element);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputDatalist extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputDatalist());
|
||||
}
|
||||
|
||||
|
||||
public function addDatalistOptions(array $options, ?bool $associative=null) : FieldInputDatalist
|
||||
{
|
||||
$datalist = $this->getControl();
|
||||
$datalist->addDatalistOptions($options, $associative);
|
||||
$this->setControl($datalist);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputDate extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputDate());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputEmail extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputEmail());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputFile extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputFile());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
// Defaults to step="1"
|
||||
class FieldInputNumber extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputNumber());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputPassword extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputPassword());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputRadioGroup extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementDiv())
|
||||
->setControlAttributes(array('class' => 'group'));
|
||||
$this->is_group = true;
|
||||
}
|
||||
|
||||
|
||||
public function addRadioOptions(array $options, string $name, $checked=null, $associative=null, bool $disabled=false): Element
|
||||
{
|
||||
$element = $this->getControl();
|
||||
$element->addRadioOptions($options, $name, $checked, $associative, $disabled);
|
||||
$this->setControl($element);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputSearch extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputSearch());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputSubmit extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputSubmit());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputText extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputText());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputTime extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputTime());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldInputUrl extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementInputUrl());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldSelect extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementSelect());
|
||||
}
|
||||
|
||||
|
||||
public function addSelectOptions(array $options, $selected=null, ?bool $associative=null, bool $for_datalist=false): Element
|
||||
{
|
||||
$select = $this->getControl();
|
||||
$select->addSelectOptions($options, $selected, $associative);
|
||||
$this->setControl($select);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldSpan extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addControl(new ElementSpan());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
class FieldTextarea extends Field
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setAttribute('class', 'field_text_area')
|
||||
->addControl(new ElementTextarea());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
|
||||
use function MRBS\toTimeString;
|
||||
|
||||
class FieldTimeWithUnits extends FieldDiv
|
||||
{
|
||||
|
||||
// Constructs a field that has an enabling checkbox and then inputs for
|
||||
// the quantity and units of time.
|
||||
//
|
||||
// $param_names An array of the parameter names, indexed by
|
||||
// 'enabler', 'quantity' and 'seconds'
|
||||
// $enabled The current value of the enabling checkbox
|
||||
// $seconds The current value of the field, in seconds
|
||||
// $suffix Optional text that can appear after the units
|
||||
// $input_attributes Optional array of additional attributes for the input
|
||||
public function __construct(array $param_names, $enabled, $seconds, $suffix=null, ?array $input_attributes=null)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Convert the raw seconds into as large a unit as possible
|
||||
$duration = $seconds;
|
||||
toTimeString($duration, $units);
|
||||
|
||||
// The checkbox, which enables or disables the field
|
||||
$checkbox = new ElementInputCheckbox();
|
||||
$checkbox->setAttributes(array('name' => $param_names['enabler'],
|
||||
'class' => 'enabler'))
|
||||
->setChecked($enabled);
|
||||
$this->addControlElement($checkbox);
|
||||
|
||||
// The quantity element
|
||||
$input = new ElementInputNumber();
|
||||
$attributes = array('name' => $param_names['quantity'],
|
||||
'value' => $duration);
|
||||
if (isset($input_attributes))
|
||||
{
|
||||
$attributes = array_merge($attributes, $input_attributes);
|
||||
}
|
||||
$input->setAttributes($attributes);
|
||||
$this->addControlElement($input);
|
||||
|
||||
// The select element for the units
|
||||
$options = Form::getTimeUnitOptions();
|
||||
$select = new ElementSelect();
|
||||
$select->setAttribute('name', $param_names['units'])
|
||||
->addSelectOptions($options, array_search($units, $options), true);
|
||||
$this->addControlElement($select);
|
||||
|
||||
// The suffix
|
||||
if (isset($suffix))
|
||||
{
|
||||
$span = new ElementSpan();
|
||||
$span->setText($suffix);
|
||||
$this->addControlElement($span);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
use MRBS\Errors\Errors;
|
||||
use MRBS\Exception;
|
||||
use function MRBS\generate_token;
|
||||
use function MRBS\get_form_var;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\session;
|
||||
|
||||
|
||||
class Form extends Element
|
||||
{
|
||||
public const METHOD_GET = 0;
|
||||
public const METHOD_POST = 1;
|
||||
private const TOKEN_NAME = 'csrf_token';
|
||||
|
||||
private static $token = null;
|
||||
private static $cookie_set = false;
|
||||
|
||||
|
||||
// Creates a new form, automatically adding a CSRF token and a MAX_FILE_SIZE value
|
||||
// as hidden inputs if the method is POST. The GET method should only be used for
|
||||
// navigating between pages and not for submitting form data which could change the
|
||||
// database contents or reveal data to which the user is not entitled.
|
||||
public function __construct(int $method=self::METHOD_GET)
|
||||
{
|
||||
parent::__construct('form');
|
||||
$this->setMethod($method);
|
||||
}
|
||||
|
||||
|
||||
// Sets the form method. If the method is POST then a CSRF token and a MAX_FILE_SIZE
|
||||
// value are also added as hidden inputs. If the method is GET then these hidden
|
||||
// inputs are removed because, in the case of the CSRF token we don't want it to
|
||||
// appear in the URL for security reasons, and in the case of MAX_FILE_SIZE it should
|
||||
// only be needed for POST requests.
|
||||
// TODO: if this method is called after the constructor then make sure the MAX_FILE_SIZE
|
||||
// TODO: hidden input is at the beginning, before any possible file input elements.
|
||||
private function setMethod(int $method) : Element
|
||||
{
|
||||
if ($method === self::METHOD_GET)
|
||||
{
|
||||
$this->removeHiddenInput('MAX_FILE_SIZE');
|
||||
$this->removeHiddenInput(self::TOKEN_NAME);
|
||||
}
|
||||
|
||||
elseif ($method === self::METHOD_POST)
|
||||
{
|
||||
// Add a MAX_FILE_SIZE hidden input for use by forms that have a file
|
||||
// upload input. This hidden input must come before the file input
|
||||
// element if it is to be used by PHP. Although at the time of writing
|
||||
// it is not used by any browsers, we can add some JavaScript to check
|
||||
// the file size when it is selected and thus save a failed upload attempt.
|
||||
$max_file_size = ini_get('upload_max_filesize');
|
||||
if ($max_file_size !== false)
|
||||
{
|
||||
$max_file_size = self::convertToBytes($max_file_size);
|
||||
$this->addHiddenInput('MAX_FILE_SIZE', $max_file_size, 'MAX_FILE_SIZE');
|
||||
}
|
||||
// Add a CSRF token
|
||||
$this->addCSRFToken();
|
||||
}
|
||||
|
||||
return parent::setAttribute('method', self::methodToString($method));
|
||||
}
|
||||
|
||||
|
||||
// Converts a method string (eg 'get' or 'GET') to a method constant (eg self::METHOD_GET)
|
||||
private static function methodToInt(string $string) : int
|
||||
{
|
||||
if (strcasecmp($string, 'get') === 0)
|
||||
{
|
||||
return self::METHOD_GET;
|
||||
}
|
||||
|
||||
if (strcasecmp($string, 'post') === 0)
|
||||
{
|
||||
return self::METHOD_POST;
|
||||
}
|
||||
|
||||
throw new Exception("Unsupported method $string");
|
||||
}
|
||||
|
||||
|
||||
// Converts a method constant (eg self::METHOD_GET) to a method string (eg 'get')
|
||||
private static function methodToString(int $int) : string
|
||||
{
|
||||
if ($int === self::METHOD_GET)
|
||||
{
|
||||
return 'get';
|
||||
}
|
||||
|
||||
if ($int === self::METHOD_POST)
|
||||
{
|
||||
return 'post';
|
||||
}
|
||||
|
||||
throw new Exception("Unsupported method constant $int");
|
||||
}
|
||||
|
||||
|
||||
// Sets a form attribute, taking special action in the case of the
|
||||
// method attribute to set/unset the CSRF token and MAX_FILE_SIZE
|
||||
// hidden inputs. Can cope with either string or integer method values.
|
||||
public function setAttribute(string $name, $value=true): Element
|
||||
{
|
||||
if (strcasecmp($name, 'method') === 0)
|
||||
{
|
||||
if (is_string($value))
|
||||
{
|
||||
$value = self::methodToInt($value);
|
||||
}
|
||||
if ($value === self::METHOD_POST)
|
||||
{
|
||||
$message = "Changing the form method after the form has been created may result " .
|
||||
"in the MAX_FILE_SIZE hidden input coming after a file input, thus " .
|
||||
"making it unusable by the server.";
|
||||
trigger_error($message, E_USER_WARNING);
|
||||
}
|
||||
return $this->setMethod($value);
|
||||
}
|
||||
|
||||
return parent::setAttribute($name, $value);
|
||||
}
|
||||
|
||||
|
||||
// Adds a hidden input to the form. Optionally give the element a key
|
||||
// so that it can be removed later using the same key.
|
||||
public function addHiddenInput(string $name, $value, ?string $key=null) : Form
|
||||
{
|
||||
$element = new ElementInputHidden($name, $value);
|
||||
$this->addElement($element, $key);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Adds an array of hidden inputs to the form
|
||||
public function addHiddenInputs(array $hidden_inputs) : Form
|
||||
{
|
||||
foreach ($hidden_inputs as $key => $value)
|
||||
{
|
||||
$this->addHiddenInput($key, $value);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Removes a hidden input from the form.
|
||||
private function removeHiddenInput(string $key) : Form
|
||||
{
|
||||
$this->removeElement($key);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Returns the HTML for a hidden field containing a CSRF token
|
||||
public static function getTokenHTML() : string
|
||||
{
|
||||
$element = new ElementInputHidden();
|
||||
$element->setAttributes(array('name' => self::TOKEN_NAME,
|
||||
'value' => self::getToken()));
|
||||
return $element->toHTML();
|
||||
}
|
||||
|
||||
|
||||
// Checks the CSRF token against the stored value and dies with a fatal error
|
||||
// if they do not match. Note that:
|
||||
// (1) The CSRF token is always looked for in the POST data, never anywhere else.
|
||||
// GET requests should only be used for operations that do not modify data or
|
||||
// grant access.
|
||||
// (2) Forms should normally use a POST method.
|
||||
// (3) Actions should normally be taken by handler pages which are not designed to be
|
||||
// accessed directly by the user and are only expecting POST requests. These pages
|
||||
// will look for the CSRF token however they are requested. If they are requested via
|
||||
// GET then they will still look for the token in the POST data and so fail.
|
||||
// (4) There are some MRBS pages that can be accessed either via a URL with query string,
|
||||
// or via a POST request. These pages should not take any action, but as a matter of
|
||||
// good practice should check the token anyway if they have been requested by a POST.
|
||||
// To cater for these pages the $post_only parameter should be set to TRUE.
|
||||
public static function checkToken(bool $post_only=false) : void
|
||||
{
|
||||
global $server;
|
||||
|
||||
if ($post_only && ($server['REQUEST_METHOD'] != 'POST'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$token = get_form_var(self::TOKEN_NAME, 'string', null, INPUT_POST);
|
||||
$stored_token = self::getStoredToken();
|
||||
|
||||
if (!self::compareTokens($stored_token, $token))
|
||||
{
|
||||
if (isset($stored_token))
|
||||
{
|
||||
// Only report a possible CSRF attack if the stored token exists. If it doesn't
|
||||
// it's normally because the user session has expired in between the form being
|
||||
// displayed and submitted.
|
||||
trigger_error('Possible CSRF attack from IP address ' . $server['REMOTE_ADDR'], E_USER_NOTICE);
|
||||
}
|
||||
|
||||
if (method_exists(session(), 'logoffUser'))
|
||||
{
|
||||
session()->logoffUser();
|
||||
}
|
||||
|
||||
Errors::fatalError(get_vocab("session_expired"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// $max_unit can be set to 'seconds', 'minutes', 'hours', etc. and
|
||||
// can be used to specify the maximum unit to return.
|
||||
public static function getTimeUnitOptions($max_unit=null) : array
|
||||
{
|
||||
$options = array();
|
||||
$units = array('seconds', 'minutes', 'hours', 'days', 'weeks');
|
||||
|
||||
foreach ($units as $unit)
|
||||
{
|
||||
$options[$unit] = get_vocab($unit);
|
||||
if (isset($max_unit) && ($max_unit == $unit))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
|
||||
private function addCSRFToken() : Form
|
||||
{
|
||||
$this->addHiddenInput(self::TOKEN_NAME, self::getToken(), self::TOKEN_NAME);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Get a CSRF token
|
||||
public static function getToken() : string
|
||||
{
|
||||
$token_length = 32;
|
||||
|
||||
if (!isset(self::$token))
|
||||
{
|
||||
$stored_token = self::getStoredToken();
|
||||
// The test below should really be isset() rather than !empty(). However occasionally MRBS has the
|
||||
// value 0 stored in the session variable. It's not clear how or why this is happening. Until the
|
||||
// root cause is found we test for empty() and if the token is set but empty we generate a new token.
|
||||
// Update: it seems that when the token is 0, so are all the other session variables. So the problem
|
||||
// is probably not in the form code, but elsewhere.
|
||||
if (!empty($stored_token))
|
||||
{
|
||||
self::$token = $stored_token;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isset($stored_token))
|
||||
{
|
||||
// The token is set but empty
|
||||
$message = "Stored token is '$stored_token'. This should not be possible. " .
|
||||
"Generating a new token.";
|
||||
trigger_error($message,E_USER_WARNING);
|
||||
}
|
||||
self::$token = generate_token($token_length);
|
||||
self::storeToken(self::$token);
|
||||
}
|
||||
}
|
||||
|
||||
return self::$token;
|
||||
}
|
||||
|
||||
|
||||
// Compare two tokens in a timing attack safe manner.
|
||||
// Returns true if they are equal, otherwise false.
|
||||
// Note: it is important to provide the user-supplied string as the
|
||||
// second parameter, rather than the first.
|
||||
private static function compareTokens($known_token, $user_token) : bool
|
||||
{
|
||||
if (is_null($known_token) || is_null($user_token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (function_exists('hash_equals'))
|
||||
{
|
||||
return hash_equals($known_token, $user_token);
|
||||
}
|
||||
|
||||
// Could do fancier things here to give a timing attack safe comparison,
|
||||
// For example https://github.com/indigophp/hash-compat
|
||||
return ($known_token === $user_token);
|
||||
}
|
||||
|
||||
|
||||
private static function storeToken($token) : void
|
||||
{
|
||||
session()->set(self::TOKEN_NAME, $token);
|
||||
}
|
||||
|
||||
|
||||
private static function getStoredToken() : ?string
|
||||
{
|
||||
$result = session()->get(self::TOKEN_NAME);
|
||||
|
||||
// For some unknown reason the integer value 0 is sometimes stored in the session
|
||||
// variable. It's not clear how this can happen.
|
||||
if (isset($result) && !is_string($result))
|
||||
{
|
||||
trigger_error("Stored token is of type " . gettype($result) . ", value $result", E_USER_WARNING);
|
||||
$result = strval($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// Convert a file size to bytes
|
||||
// See https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
|
||||
private static function convertToBytes(string $size) : int
|
||||
{
|
||||
// Split the size into value and units (if any)
|
||||
$values = preg_split('/(?<=[0-9])(?=[^0-9]+)/i', $size);
|
||||
|
||||
if (count($values) == 2)
|
||||
{
|
||||
$result = intval($values[0]);
|
||||
switch ($values[1])
|
||||
{
|
||||
case 'G':
|
||||
$result = 1024 * $result;
|
||||
// Fall through
|
||||
case 'M':
|
||||
$result = 1024 * $result;
|
||||
// Fall through
|
||||
case 'K':
|
||||
$result = 1024 * $result;
|
||||
return $result;
|
||||
break;
|
||||
default:
|
||||
// Unrecognised suffix
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return intval($size);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user