Files
DomFramework/src/Form.php

598 lines
18 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)
{
$this->debug = $val;
return $this;
}
/** Set the csrf enable
* @param integer $val The csrf check
*/
public function csrf ($val)
{
$this->csrf = !! $val;
return $this;
}
/** Set the method
* @param string $val The method to use
*/
public function method ($val)
{
$this->method = strtolower ($val);
return $this;
}
/** Set the csrf token name
* @param integer $val The csrf token name
*/
public function csrfField ($val)
{
$this->csrfField = $val;
return $this;
}
/** Set the titlewidth
* @param integer $val The titlewidth
*/
public function titlewidth ($val)
{
$this->titlewidth = $val;
return $this;
}
/** Set the fieldwidth
* @param integer $val The fieldwidth
*/
public function fieldwidth ($val)
{
$this->fieldwidth = $val;
return $this;
}
/** Set the formClass
* @param integer $val The formClass
*/
public function formClass ($val)
{
$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 = "")
{
$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)
{
if (! in_array ($formTemplate,
array ("Bootstrap3", "Bootstrap4")))
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)
// {{{
{
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)
// {{{
{
$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)
// {{{
{
$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 ()
// {{{
{
$values = array ();
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 = array())
// {{{
{
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 = array ("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)
// {{{
{
$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 ()
// {{{
{
if ($this->csrfToken === "")
$this->createToken ();
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 array ();
$fields = $_SESSION["domframework"]["form"]["fields"];
}
$errors = array ();
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 = "")
// {{{
{
$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=array ())
// {{{
{
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 ()
// {{{
{
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)
// {{{
{
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)
// {{{
{
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)
// {{{
{
$date = \DateTime::CreateFromFormat ($inputFormat, $inputDate);
if ($date === false)
return $inputDate;
$errors = $date->getLastErrors();
if ($errors["warning_count"] > 0 || $errors["error_count"] > 0)
return $inputDate;
return $date->format ($outputFormat);
}
// }}}
}