* @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
* @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 = "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 .= "\n"; $group = ""; } if (isset($field->group) && $field->group !== $group) { $res .= "
\n"; $res .= " $field->group\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 .= "
\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 .= "\n"; } $res .= "
\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); } }