Files
DomFramework/src/Form.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);
}
}