622 lines
20 KiB
PHP
622 lines
20 KiB
PHP
<?php
|
|
|
|
/**
|
|
* DomFramework
|
|
* @package domframework
|
|
* @author Dominique Fournier <dominique@fournier38.fr>
|
|
* @license BSD
|
|
*/
|
|
|
|
namespace Domframework;
|
|
|
|
/**
|
|
* This class permit to create easily some forms to HTML (or text mode in
|
|
* future).
|
|
* Each field can be checked in AJAX or HTML.
|
|
*/
|
|
class Form
|
|
{
|
|
/**
|
|
* All the fields
|
|
*/
|
|
private $fields = null;
|
|
/**
|
|
* The name of the form
|
|
*/
|
|
private $formName;
|
|
/**
|
|
* Allow to debug the PHP
|
|
*/
|
|
public $debug = 0;
|
|
/**
|
|
* CSRF protection
|
|
* By default, the CSRF protection is active if a SESSION is active too.
|
|
* It can be disabled if needed. An Exception is raised if the form is send
|
|
* back without the token
|
|
*/
|
|
public $csrf = true;
|
|
/**
|
|
* Name of the CSRF hidden field in HTML page
|
|
*/
|
|
public $csrfField = "CSRF_TOKEN";
|
|
/**
|
|
* The CSRF token value
|
|
*/
|
|
private $csrfToken = "";
|
|
|
|
/**
|
|
* The method used to send the values
|
|
*/
|
|
private $method = "post";
|
|
|
|
/**
|
|
* The Bootstrap width of the column of titles
|
|
*/
|
|
public $titlewidth = 2;
|
|
/**
|
|
* The Bootstrap width of the column of fields
|
|
*/
|
|
public $fieldwidth = 10;
|
|
|
|
/**
|
|
* Define a class for form object
|
|
*/
|
|
public $formClass = "form-horizontal";
|
|
|
|
/**
|
|
* The logging callable method
|
|
*/
|
|
private $loggingCallable = null;
|
|
/**
|
|
* The logging basemsg
|
|
*/
|
|
private $loggingBasemsg = "";
|
|
|
|
/**
|
|
* Form template (Bootstrap3 by default)
|
|
*/
|
|
private $formTemplate = "Bootstrap3";
|
|
|
|
/**
|
|
* Create a form
|
|
* @param string|null $formName The form name
|
|
*/
|
|
public function __construct($formName = "form")
|
|
{
|
|
$this->formName = $formName;
|
|
}
|
|
|
|
// The setters of the properties
|
|
/**
|
|
* Set the debug level
|
|
* @param integer $val The debug value
|
|
*/
|
|
public function debug($val): self
|
|
{
|
|
$this->debug = $val;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the csrf enable
|
|
* @param integer $val The csrf check
|
|
*/
|
|
public function csrf($val): self
|
|
{
|
|
$this->csrf = !! $val;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the method
|
|
* @param string $val The method to use
|
|
*/
|
|
public function method($val): self
|
|
{
|
|
$this->method = strtolower($val);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the csrf token name
|
|
* @param integer $val The csrf token name
|
|
*/
|
|
public function csrfField($val): self
|
|
{
|
|
$this->csrfField = $val;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the titlewidth
|
|
* @param integer $val The titlewidth
|
|
*/
|
|
public function titlewidth($val): self
|
|
{
|
|
$this->titlewidth = $val;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the fieldwidth
|
|
* @param integer $val The fieldwidth
|
|
*/
|
|
public function fieldwidth($val): self
|
|
{
|
|
$this->fieldwidth = $val;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the formClass
|
|
* @param integer $val The formClass
|
|
*/
|
|
public function formClass($val): self
|
|
{
|
|
$this->formClass = $val;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set logging class an method
|
|
* @param callable $loggingCallable The callable function. This method will
|
|
* receive two params : the LOG level (LOG_ERROR...) and the message
|
|
* @param string|null $loggingBasemsg The basemsg added at the beginning of
|
|
* the log
|
|
*/
|
|
public function logging($loggingCallable, $loggingBasemsg = ""): void
|
|
{
|
|
$this->loggingCallable = $loggingCallable;
|
|
$this->loggingBasemsg = $loggingBasemsg;
|
|
}
|
|
|
|
/**
|
|
* Set the Form Templating to use.
|
|
* Can be : Bootstrap3, Bootstrap4 (later Bulma)
|
|
* @param string $formTemplate The template to use
|
|
*/
|
|
public function formTemplate($formTemplate): self
|
|
{
|
|
if (
|
|
! in_array(
|
|
$formTemplate,
|
|
["Bootstrap3", "Bootstrap4"],
|
|
true
|
|
)
|
|
) {
|
|
throw new \Exception("Unknown formTemplate provided", 500);
|
|
}
|
|
$this->formTemplate = $formTemplate;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* The private method to log if the $this->loggingCallable is defined
|
|
* @param integer $prio The priority of the message
|
|
* @param string $msg The message to store
|
|
*/
|
|
private function loggingCallable($prio, $msg): void
|
|
{
|
|
if (! is_callable($this->loggingCallable)) {
|
|
return;
|
|
}
|
|
$base = "";
|
|
if ($this->loggingBasemsg !== "") {
|
|
$base = $this->loggingBasemsg . " ";
|
|
}
|
|
call_user_func($this->loggingCallable, $prio, $base . $msg);
|
|
}
|
|
|
|
/**
|
|
* Save the array of fields into the structure.
|
|
* Available :
|
|
* - name : name of the field in the HTML page
|
|
* - label : label written to the describe the field
|
|
* - [titles] : text written in radio/checkboxes
|
|
* - [defaults] : default values. Must be array for checkbox/select, and
|
|
* string for others
|
|
* - [type] : text, password, hidden, checkbox, select, radio, submit,
|
|
* textarea
|
|
* text by default
|
|
* - [help] : The Help message (written below the field). Overwrited in
|
|
* case of error
|
|
* - [multiple] : Multiple selection are possible (if the type supports it)
|
|
* - [group] : define a fieldset and define the title with groupe name
|
|
* Warning : all the elements of the same group must be
|
|
* consecutive !
|
|
* - [readonly] : put a read-only flag on the field (the user see it but
|
|
* can't interract on it. The value will be sent to next
|
|
* page
|
|
* - [mandatory] : boolean to add a red star at end of label
|
|
* - [hidden] : hide the field (add a style='display:hidden' to the field)
|
|
* - [maxlength] : the maximum length of the content of the field in chars
|
|
* - [rows] : Number of rows
|
|
* - [cols] : Number of columns
|
|
* - [placeholder] : The text to be displayed in the placeholder
|
|
*
|
|
* @param array $fields The fields to be displayed
|
|
*/
|
|
public function fields($fields): void
|
|
{
|
|
$this->fields = $fields;
|
|
}
|
|
|
|
/**
|
|
* Add a field to the form. For the details of a field, see the description
|
|
* in fields method
|
|
* @param object $field The field to add
|
|
*/
|
|
public function addfield($field): void
|
|
{
|
|
$this->fields[] = $field;
|
|
}
|
|
|
|
/**
|
|
* Return the values provided by the user. Test the CSRF before continue
|
|
* NEVER read the values from $_POST in your codes or CSRF will not be
|
|
* checked
|
|
*/
|
|
public function values(): array
|
|
{
|
|
$values = [];
|
|
if ($this->method === "post") {
|
|
if (isset($_POST[$this->formName])) {
|
|
$values = $_POST[$this->formName];
|
|
}
|
|
} elseif ($this->method === "get") {
|
|
if (isset($_GET[$this->formName])) {
|
|
$values = $_GET[$this->formName];
|
|
}
|
|
} else {
|
|
$this->loggingCallable(
|
|
LOG_ERR,
|
|
"Unknown FORM method (GET or POST allowed)"
|
|
);
|
|
throw new \Exception(dgettext(
|
|
"domframework",
|
|
"Unknown FORM method (GET or POST allowed)"
|
|
));
|
|
}
|
|
|
|
if (count($values) !== 0 && $this->csrf === true) {
|
|
// CSRF protection
|
|
try {
|
|
$this->checkToken($values[$this->csrfField]);
|
|
} catch (\Exception $e) {
|
|
$this->loggingCallable(LOG_ERR, $e->getMessage());
|
|
throw new \Exception(dgettext(
|
|
"domframework",
|
|
"Can not read the data from the form : " .
|
|
"Expired or missing CSRF Token"
|
|
), 500);
|
|
}
|
|
// Remove the field CSRF : can not be used outside the form
|
|
unset($values[$this->csrfField]);
|
|
}
|
|
if (isset($_SESSION["domframework"]["form"][$this->formName]["fields"])) {
|
|
foreach (
|
|
$_SESSION["domframework"]["form"][$this->formName]["fields"] as $field
|
|
) {
|
|
if (
|
|
$field->type === "hidden" ||
|
|
($field->readonly !== null && $field->readonly !== false)
|
|
) {
|
|
if (isset($field->values)) {
|
|
$values[$field->name] = $field->values;
|
|
} elseif (isset($field->defaults)) {
|
|
$values[$field->name] = $field->defaults;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Return the fields in HTML code. If $values is provided, use it in place
|
|
* of default values. In case of select boxes, $values are the selected
|
|
* elements
|
|
* $method is the method written in method field of <form>
|
|
* @param string|null $method The method to use to transmit the form (POST,
|
|
* GET)
|
|
* @param array|null $values The default values of the fields
|
|
* @param array|null $errors The fields to put in error with the associated
|
|
* message
|
|
*/
|
|
public function printHTML(
|
|
$method = 'post',
|
|
$values = null,
|
|
$errors = []
|
|
): string {
|
|
if (count($this->fields) === 0) {
|
|
$this->loggingCallable(
|
|
LOG_ERR,
|
|
"Can't display a form without defined field"
|
|
);
|
|
throw new \Exception("Can't display a form without defined field", 500);
|
|
}
|
|
if (isset($_SESSION)) {
|
|
$_SESSION["domframework"]["form"][$this->formName]["fields"] =
|
|
$this->fields;
|
|
}
|
|
$this->method = strtolower($method);
|
|
$res = "";
|
|
$res = "<form action='#' method='$method'";
|
|
// If a file field will be displayed, the form must have a
|
|
// enctype="multipart/form-data" parameter
|
|
foreach ($this->fields as $field) {
|
|
if ($field->type === "file") {
|
|
$res .= "enctype='multipart/form-data'";
|
|
break;
|
|
}
|
|
}
|
|
if ($this->formName != "") {
|
|
$res .= " id='$this->formName'";
|
|
}
|
|
$res .= " class='" . $this->formClass . "'>\n";
|
|
$group = "";
|
|
if (isset($_SESSION["domframework"]["form"][$this->formName]["values"])) {
|
|
$values = $_SESSION["domframework"]["form"][$this->formName]["values"];
|
|
$errors = $_SESSION["domframework"]["form"][$this->formName]["errors"];
|
|
unset($_SESSION["domframework"]["form"][$this->formName]["values"]);
|
|
unset($_SESSION["domframework"]["form"][$this->formName]["errors"]);
|
|
}
|
|
foreach ($this->fields as $field) {
|
|
$field->formName = $this->formName;
|
|
if (
|
|
isset($field->group) && $field->group !== $group && $group !== "" ||
|
|
!isset($field->group) && $group !== ""
|
|
) {
|
|
$res .= "</fieldset>\n";
|
|
$group = "";
|
|
}
|
|
if (isset($field->group) && $field->group !== $group) {
|
|
$res .= "<fieldset>\n";
|
|
$res .= " <legend>$field->group</legend>\n";
|
|
$group = $field->group;
|
|
}
|
|
|
|
$res .= " ";
|
|
if (
|
|
isset($values[$field->name]) &&
|
|
$values[$field->name] !== "unset"
|
|
) {
|
|
$field->values = $values[$field->name];
|
|
}
|
|
if (
|
|
isset($errors[$field->name]) &&
|
|
$errors[$field->name] !== "unset"
|
|
) {
|
|
if (is_array($errors[$field->name])) {
|
|
$field->errors = $errors[$field->name];
|
|
} else {
|
|
$field->errors = ["error", $errors[$field->name]];
|
|
}
|
|
if ($field->type === "hidden") {
|
|
$field->type = "text";
|
|
$field->readonly = true;
|
|
}
|
|
}
|
|
$field->titlewidth = $this->titlewidth;
|
|
$field->fieldwidth = $this->fieldwidth;
|
|
$field->formTemplate = $this->formTemplate;
|
|
$res .= $field->display();
|
|
}
|
|
|
|
if ($group !== "") {
|
|
$res .= "</fieldset>\n";
|
|
$group = "";
|
|
}
|
|
|
|
if ($this->csrf === true) {
|
|
$csrf = new Csrf();
|
|
$csrf->field = $this->formName . "[" . $this->csrfField . "]";
|
|
$res .= $csrf->displayFormCSRF();
|
|
$this->csrfToken = $csrf->getToken();
|
|
}
|
|
|
|
// Manage the focus. On the first visible element if there is no error, on
|
|
// the first error fields when there is one
|
|
$focusElement = null;
|
|
foreach ($this->fields as $field) {
|
|
if ($field->type === "hidden" || $field->readonly === true) {
|
|
continue;
|
|
}
|
|
if ($field->titles) {
|
|
$focusElement = $field->name . "_" . key($field->titles);
|
|
} else {
|
|
$focusElement = $field->name;
|
|
}
|
|
break;
|
|
}
|
|
if (count($errors) > 0) {
|
|
foreach ($errors as $fieldErr => $error) {
|
|
// If the field is numeric, it is a global error, and not an error due
|
|
// to a field: skip it !
|
|
foreach ($this->fields as $field) {
|
|
if ($field->name === $fieldErr) {
|
|
$focusElement = $field->name;
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($focusElement !== null) {
|
|
$res .= "<script>document.getElementById('" . $this->formName . "_" .
|
|
$focusElement . "').focus();" .
|
|
"var formFocusElement='" . $this->formName . "_" .
|
|
$focusElement . "';</script>\n";
|
|
}
|
|
$res .= "</form>\n";
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Check the token from the user
|
|
* @param string $tokenFromUser The value form the user's token
|
|
*/
|
|
public function checkToken($tokenFromUser): void
|
|
{
|
|
$csrf = new Csrf();
|
|
$csrf->field = $this->csrfField;
|
|
// The checkThenDeleteToken method check the token and except if there is a
|
|
// problem. If there is no problem, it delete the token
|
|
$csrf->checkThenDeleteToken($tokenFromUser);
|
|
}
|
|
|
|
/**
|
|
* Return the token generated in form
|
|
*/
|
|
public function getToken(): string
|
|
{
|
|
if ($this->csrfToken === "") {
|
|
$csrf = new Csrf();
|
|
$this->csrfToken = $csrf->newToken();
|
|
}
|
|
return $this->csrfToken;
|
|
}
|
|
|
|
/**
|
|
* Check if the parameters are correct with the defined fields
|
|
* Need the session !
|
|
* @param array $values The values to check
|
|
* @param array|null $fields The fields definition (or use the session
|
|
* stored one if the value is null)
|
|
* @return array containing the errors
|
|
*/
|
|
public function verify($values, $fields = []): array
|
|
{
|
|
if (count($fields) === 0) {
|
|
if (! isset($_SESSION["domframework"]["form"]["fields"])) {
|
|
return [];
|
|
}
|
|
$fields = $_SESSION["domframework"]["form"]["fields"];
|
|
}
|
|
$errors = [];
|
|
foreach ($fields as $field) {
|
|
if (
|
|
$field->mandatory !== null &&
|
|
(! array_key_exists($field->name, $values) ||
|
|
trim($values[$field->name]) === "")
|
|
) {
|
|
$errors[$field->name] = dgettext(
|
|
"domframework",
|
|
"Field mandatory and not provided"
|
|
);
|
|
}
|
|
}
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* If there is at least one error reported in $errors, save the old values
|
|
* and the errors in the session, and redirect to the provided url.
|
|
* If there is no error, do nothing
|
|
* @param array $values The values of the fields filled by the user
|
|
* @param array $errors The errors detected by a verify
|
|
* @param object $route the route object
|
|
* @param string|null $url The URL to redirect. If not provided, use the
|
|
* $route->requestURL () method to found the calling page
|
|
*
|
|
* Example :
|
|
* $form = new \Domframework\form ();
|
|
* $form->logging (array ('\apps\general\controllers\logging', 'log'),
|
|
* $authHTML["email"]);
|
|
* $values = $form->values ();
|
|
* $errors = $spaceObj->verify ($values);
|
|
* $form->redirectIfError ($values, $errors, $route, "/admin/space/");
|
|
* $spaceuuid = $spaceObj->spaceCreateConceal ($values["spacename"]);
|
|
* $route->redirect ("/admin/space/");
|
|
*/
|
|
public function redirectIfError($values, $errors, $route, $url = ""): void
|
|
{
|
|
$this->saveValuesErrors($values, $errors);
|
|
if ($url === "") {
|
|
$url = "/" . $route->requestURL();
|
|
}
|
|
if (count($errors)) {
|
|
$route->redirect($url);
|
|
}
|
|
$this->saveValuesErrorsReset();
|
|
}
|
|
|
|
/**
|
|
* Save the values and errors to be displayed in the next page if the session
|
|
* is available
|
|
* Need the session to work
|
|
* @param array $values The values of the fields filled by the user
|
|
* @param array|null $errors The errors detected by a verify
|
|
*/
|
|
public function saveValuesErrors($values, $errors = []): void
|
|
{
|
|
if (isset($_SESSION)) {
|
|
$_SESSION["domframework"]["form"][$this->formName]["values"] = $values;
|
|
$_SESSION["domframework"]["form"][$this->formName]["errors"] = $errors;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset the saved values to provide a clean form next page
|
|
* Need the session to work
|
|
*/
|
|
public function saveValuesErrorsReset(): void
|
|
{
|
|
unset($_SESSION["domframework"]["form"][$this->formName]["values"]);
|
|
unset($_SESSION["domframework"]["form"][$this->formName]["errors"]);
|
|
}
|
|
|
|
/**
|
|
* Get the stored values if there is one. If there is no stored values,
|
|
* return the values provided as parameter
|
|
* @param array $values The values returned if there is no stored values
|
|
* @return array The values to use
|
|
*/
|
|
public function getOldValues($values): array
|
|
{
|
|
if (isset($_SESSION["domframework"]["form"][$this->formName]["values"])) {
|
|
$values = $_SESSION["domframework"]["form"][$this->formName]["values"];
|
|
unset($_SESSION["domframework"]["form"][$this->formName]["values"]);
|
|
}
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Get the stored errors if there is one. If there is no sorted errors,
|
|
* return the errors provided as parameter
|
|
* @param array $errors The values returned if there is no stored values
|
|
* @return array The errors to use
|
|
*/
|
|
public function getOldErrors($errors): array
|
|
{
|
|
if (isset($_SESSION["domframework"]["form"][$this->formName]["errors"])) {
|
|
$errors = $_SESSION["domframework"]["form"][$this->formName]["errors"];
|
|
unset($_SESSION["domframework"]["form"][$this->formName]["errors"]);
|
|
}
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* Convert Date received in one format to another.
|
|
* If the provided string is not corresponding to the format, don't change
|
|
* anything.
|
|
* Format used http://php.net/manual/en/datetime.createfromformat.php
|
|
* @param string $inputDate The date to modify
|
|
* @param string $inputFormat The input format of the date
|
|
* @param string $outputFormat The output format of the date
|
|
* @return string
|
|
*/
|
|
public function convertDate($inputDate, $inputFormat, $outputFormat): string
|
|
{
|
|
$date = \DateTime::CreateFromFormat($inputFormat, $inputDate);
|
|
if ($date === false) {
|
|
return $inputDate;
|
|
}
|
|
$errors = $date->getLastErrors();
|
|
if (is_array($errors) && ($errors["warning_count"] > 0 || $errors["error_count"] > 0)) {
|
|
return $inputDate;
|
|
}
|
|
return $date->format($outputFormat);
|
|
}
|
|
}
|