75 $class = str_replace(
'\\',
'/', str_replace(__NAMESPACE__ .
'\\',
'', $class));
76 $filePath = __DIR__ .
'/' . $class .
'.php';
77 $filePath = str_replace(
'..',
'', $filePath);
79 $resolved = realpath($filePath);
80 if ($resolved !==
false && file_exists($resolved)) {
81 require_once($resolved);
85spl_autoload_register(__NAMESPACE__ .
'\autoload');
128 'en' => [
'us',
'gb',
'ie',
'au',
'nz'],
129 'de' => [
'de',
'at',
'ch'],
130 'fr' => [
'fr',
'ch',
'be',
'lu',
'ca'],
206 private $currentStepId;
256 $this->isAutoSaveRequest = isset($_POST[
'formAutosave']) && $_POST[
'formAutosave'] ===
"true";
258 parent::__construct(
$name, $parameters, $this);
260 $this->url = parse_url($this->submitURL);
261 if (!empty($this->successURL)) {
262 $this->validateRedirectUrl($this->successURL,
true);
264 if (!empty($this->cancelURL)) {
265 $this->validateRedirectUrl($this->cancelURL,
true);
267 if (empty($this->successURL)) {
270 if (empty($this->cancelURL)) {
274 $this->currentStepId = isset($_GET[
'step']) ? $_GET[
'step'] : 0;
276 $this->startSession();
278 $this->valid = (isset($this->sessionSlot[
'formIsValid'])) ? $this->sessionSlot[
'formIsValid'] :
null;
281 if (!isset($this->sessionSlot[
'formCsrfToken'])) {
285 if (!isset($this->sessionSlot[
'formFinalPost'])) {
286 $this->sessionSlot[
'formFinalPost'] =
false;
290 $this->addHidden(
'formName')->setValue($this->name);
293 $this->addHidden(
'formStep')->setValue($this->currentStepId);
296 $this->addHidden(
'formCsrfToken')->setValue($this->sessionSlot[
'formCsrfToken']);
311 parent::setDefaults();
313 $this->defaults[
'label'] =
'submit';
314 $this->defaults[
'cancelLabel'] =
'';
315 $this->defaults[
'backLabel'] =
'';
316 $this->defaults[
'class'] =
'';
317 $this->defaults[
'method'] =
'post';
319 $this->defaults[
'submitURL'] = $_SERVER[
'REQUEST_URI'];
320 $this->defaults[
'successURL'] =
null;
321 $this->defaults[
'cancelURL'] =
null;
322 $this->defaults[
'validator'] =
null;
323 $this->defaults[
'ttl'] = 60 * 60;
324 $this->defaults[
'jsValidation'] =
'blur';
325 $this->defaults[
'jsAutosave'] =
'false';
336 private function startSession(): void
340 $params = session_get_cookie_params();
341 $sessionName = session_name();
343 session_set_cookie_params(
350 [
'SameSite' =>
'Lax'],
355 if (isset($_COOKIE[$sessionName])) {
358 $_COOKIE[$sessionName],
367 $this->sessionSlotName =
'htmlform-' . $this->name .
'-data';
370 $this->sessionExpiry();
380 private function sessionExpiry(): void
382 if (isset($this->ttl) && is_numeric($this->ttl)) {
386 isset($this->sessionSlot[
'formTimestamp'])
387 && ($timestamp - $this->sessionSlot[
'formTimestamp'] > $this->ttl)
393 $this->sessionSlot[
'formTimestamp'] = $timestamp;
403 return !isset($this->sessionSlot[
'formName']);
413 return base64_encode(openssl_random_pseudo_bytes(16));
431 $newElement = parent::addElement($type,
$name, $parameters);
433 if ($newElement instanceof
Elements\Step) {
434 $this->steps[] = $newElement;
436 if ($newElement instanceof
Abstracts\Input) {
454 if ($element->getName() ===
$name) {
464 private function getCurrentElements(): array
466 $currentElements = [];
468 foreach ($this->elements as $element) {
469 if ($element instanceof
Abstracts\Container) {
471 !($element instanceof
Elements\Step)
472 || (isset($this->steps[$this->currentStepId]) && ($element == $this->steps[$this->currentStepId]))
474 $currentElements = array_merge($currentElements, $element->getElements());
477 $currentElements[] = $element;
481 return $currentElements;
491 $this->namespaces[] = $namespace;
509 private function inCurrentStep(
string $name): bool
511 return in_array($this->
getElement($name), $this->getCurrentElements());
525 if (!is_null($step)) {
526 $this->currentStepId = $step;
528 if (!is_numeric($this->currentStepId)
529 || ($this->currentStepId > count($this->steps) - 1)
530 || ($this->currentStepId < 0)
555 return $this->currentStepId;
568 if (count($this->steps) > 0) {
569 foreach ($this->steps as $stepNumber => $step) {
570 if (!$step->validate()) {
580 return count($this->steps) - 1;
594 $url = isset($this->url[
'scheme']) ? $this->url[
'scheme'] .
'://' :
'';
595 $url .= $this->url[
'host'] ??
'';
596 $url .= isset($this->url[
'port']) ?
':' . $this->url[
'port'] :
'';
597 $url .= $this->url[
'path'] ??
'';
613 if (isset($this->url[
'query']) && $this->url[
'query'] !=
"") {
615 $query = html_entity_decode($this->url[
'query']);
618 parse_str($query, $queryParts);
621 foreach ($args as
$name => $value) {
623 $queryParts[
$name] = $value;
624 } elseif (isset($queryParts[
$name])) {
625 unset($queryParts[
$name]);
630 $query = http_build_query($queryParts);
656 $element->setSessionSlot($this->sessionSlot);
661 isset($_POST[
'formName']) && ($_POST[
'formName'] === $this->name)
662 && $this->inCurrentStep(
$name)
663 && isset($_POST[
'formCsrfToken']) && $_POST[
'formCsrfToken'] === $this->sessionSlot[
'formCsrfToken']
667 $oldValue = isset($this->sessionSlot[
$name]) ? $this->sessionSlot[
$name] :
null;
668 $this->sessionSlot[
$name] = $element->handleUploadedFiles($oldValue);
669 } elseif (!$element->getDisabled()) {
671 $value = isset($_POST[
$name]) ? $_POST[
$name] :
null;
672 $this->sessionSlot[
$name] = $element->setValue($value);
673 } elseif (!isset($this->sessionSlot[
$name])) {
675 $this->sessionSlot[
$name] = $element->setValue($element->getDefaultValue());
679 elseif (isset($this->sessionSlot[
$name])) {
680 $element->setValue($this->sessionSlot[
$name]);
693 $this->sessionSlot[
$name] = $element->clearValue();
705 public function populate(array|
object $data = []): void
708 $name = $element->name;
709 if (!in_array(
$name, $this->internalFields)) {
710 if (is_array($data) && isset($data[
$name])) {
711 $value = $data[
$name];
712 } elseif (is_object($data) && isset($data->$name)) {
713 $value = $data->$name;
717 $element->setDefaultValue($value);
718 if ($element->getDisabled() && !isset($this->sessionSlot[
$name])) {
719 $this->sessionSlot[
$name] = $value;
742 if (isset($_POST[
'formName']) && ($_POST[
'formName'] === $this->name)) {
744 $this->sessionSlot[
'formFinalPost'] = count($this->steps) == 0 || $_POST[
'formStep'] + 1 == count($this->steps)
747 if (!empty($this->cancelLabel) && isset($_POST[
'formSubmit']) && $_POST[
'formSubmit'] === $this->cancelLabel) {
751 } elseif ($this->isAutoSaveRequest) {
754 } elseif (!empty($this->backLabel) && isset($_POST[
'formSubmit']) && $_POST[
'formSubmit'] === $this->backLabel) {
758 $this->sessionSlot[
'formFinalPost'] =
false;
759 $prevStep = $this->currentStepId - 1;
763 $this->
redirect($this->
buildUrl([
'step' => ($prevStep <= 0) ?
'' : $prevStep]));
773 $nextStep = $this->currentStepId + 1;
775 if ($nextStep > $firstInvalidStep) {
776 $nextStep = $firstInvalidStep;
778 if ($nextStep > count($this->steps)) {
779 $nextStep = count($this->steps) - 1;
781 $this->
redirect($this->
buildUrl([
'step' => ($nextStep == 0) ?
'' : $nextStep]));
802 if ($this->valid && !is_null($this->validator)) {
803 if (is_callable($this->validator)) {
804 $this->valid = call_user_func($this->validator, $this, $this->
getValues());
806 throw new exceptions\validatorNotCallable(
"The validator paramater must be callable");
809 $this->valid = $this->valid && $this->sessionSlot[
'formFinalPost'];
855 if (isset($_POST[
'formCsrfToken'])) {
856 $hasCorrectToken = $_POST[
'formCsrfToken'] === $this->sessionSlot[
'formCsrfToken'];
857 $this->valid = $this->valid && $hasCorrectToken;
859 if (!$hasCorrectToken) {
861 $this->
log(
"HtmlForm: Requst invalid because of incorrect CsrfToken");
868 if ($this->isAutoSaveRequest
869 || (isset($this->sessionSlot[
'formIsAutosaved'])
870 && $this->sessionSlot[
'formIsAutosaved'] ===
true)
872 $this->valid =
false;
887 if (isset($this->sessionSlot)) {
889 return array_diff_key($this->sessionSlot, array_fill_keys($this->internalFields,
''));
903 $valuesWithLabel = [];
904 if (isset($values)) {
905 foreach ($values as $element => $value) {
909 $valuesWithLabel[$element] = [
911 "label" => $elem->getLabel(),
916 return $valuesWithLabel;
929 private function validateRedirectUrl(
string $url, $allowExternal =
false): void
935 $parsed = parse_url(
$url);
937 if ($parsed ===
false) {
938 throw new \InvalidArgumentException(
"Invalid redirect URL.");
941 if (isset($parsed[
'scheme']) && $parsed[
'scheme'] !==
'https') {
945 if (!isset($parsed[
'host'])) {
949 if (!$allowExternal) {
950 $allowedHosts = $_SERVER[
'HTTP_HOST'] ??
'';
951 $allowedHosts = array_map(
'trim', explode(
',', $allowedHosts));
952 if (!in_array($parsed[
'host'], $allowedHosts,
true)) {
953 throw new \InvalidArgumentException(
"Redirect to external host not allowed.");
964 private function validateRedirect(
string $url): void
967 throw new \InvalidArgumentException(
"Empty URL.");
970 if (preg_match(
'/[\r\n]/',
$url)) {
971 throw new \InvalidArgumentException(
"Invalid URL: newline characters detected.");
974 $parsed = parse_url(
$url);
975 if ($parsed ===
false) {
976 throw new \InvalidArgumentException(
"Invalid URL format.");
979 if (isset($parsed[
'scheme'])) {
980 $allowedSchemes = [
'http',
'https'];
981 if (!in_array(strtolower($parsed[
'scheme']), $allowedSchemes,
true)) {
982 throw new \InvalidArgumentException(
"Invalid URL scheme.");
993 $this->validateRedirect(
$url);
994 $safeUrl = htmlspecialchars(
$url, ENT_QUOTES | FILTER_FLAG_NO_ENCODE_QUOTES,
'UTF-8');
995 header(
'Location: ' . $safeUrl);
1005 http_response_code($code);
1016 if ($clearCsrfToken) {
1020 unset($_SESSION[$this->sessionSlotName]);
1021 unset($this->sessionSlot);
1025 if (!$element->getDisabled() && !in_array($element->name, $this->internalFields)) {
1026 unset($this->sessionSlot[$element->name]);
1039 $timestamp = time();
1041 if (empty($_SESSION)) {
1044 foreach ($_SESSION as $key => &$val) {
1045 if (preg_match($pattern, $key)
1046 && isset($val[
'formTimestamp'])
1047 && ($timestamp - $val[
'formTimestamp'] >
$ttl)
1049 unset($_SESSION[$key]);
1060 $this->dataAttr[
'jsautosave'] = $this->jsAutosave ===
true ?
"true" :
$this->jsAutosave;
1062 return parent::htmlDataAttributes();
1069 $step = $this->currentStepId != 0 ? $this->currentStepId :
'';
1083 $renderedElements =
'';
1087 $label = $this->htmlLabel();
1088 $cancellabel = $this->htmlCancelLabel();
1089 $backlabel = $this->htmlBackLabel();
1090 $class = $this->htmlClass();
1091 $method = $this->htmlMethod();
1094 $disabledAttr = $this->disabled ?
" disabled=\"disabled\"" :
"";
1096 foreach ($this->elementsAndHtml as $element) {
1098 if (!($element instanceof elements\step)
1099 || (isset($this->steps[$this->currentStepId]) && $this->steps[$this->currentStepId] == $element)
1101 $renderedElements .= $element;
1105 if (!empty($this->cancelLabel)) {
1106 $cancel =
"<p id=\"{$this->name}-cancel\" class=\"cancel\"><input type=\"submit\" name=\"formSubmit\" value=\"{$cancellabel}\"$disabledAttr></p>\n";
1108 if (!empty($this->backLabel) && $this->currentStepId > 0) {
1109 $back =
"<p id=\"{$this->name}-back\" class=\"back\"><input type=\"submit\" name=\"formSubmit\" value=\"{$backlabel}\"$disabledAttr></p>\n";
1111 if (!empty($this->label)) {
1112 $submit =
"<p id=\"{$this->name}-submit\" class=\"submit\"><input type=\"submit\" name=\"formSubmit\" value=\"{$label}\"$disabledAttr></p>\n";
1116 return "<form id=\"{$this->name}\" name=\"{$this->name}\" class=\"depage-form {$class}\" method=\"{$method}\" action=\"{$submitURL}\"{$dataAttr} enctype=\"multipart/form-data\">" .
"\n" .
container element base class
addChildElements()
Sub-element generator hook.
$form
Parent form object reference.
clearValue()
Deletes values of all child elements.
getElement(string $name, bool $includeFieldsets=false)
Gets subelement by name.
getElements(bool $includeFieldsets=false)
Returns containers subelements.
log(string $argument, ?string $type=null)
error & warning logger
$dataAttr
Extra information about the data that is saved inside the element.
htmlEscape(array|string $options=[])
Escapes HTML in strings and arrays of strings.
thrown when there are duplicate element names
$valid
Form validation result/status.
$isAutoSaveRequest
true if form request is from autosave call
registerNamespace(string $namespace)
Stores element namespaces for adding.
clearInputValue(string $name)
clearInputValue
$method
HTML form method attribute.
getCurrentStepId()
Returns the current step id.
$label
Contains the submit button label of the form.
validate()
Validates the forms subelements.
httpResponseCode(int $code)
Sets the HTTP response code.
$jsAutosave
Contains the javascript autosave type of the form.
$class
Contains the additional class value of the form.
$cancelURL
Specifies where the user is redirected to, once the form-data is cancelled.
getSteps()
Returns an array of steps.
__construct(string $name, array $parameters=[], HtmlForm|null $form=null)
HtmlForm class constructor.
$jsValidation
Contains the javascript validation type of the form.
getFirstInvalidStep()
Returns first step that didn't pass validation.
htmlDataAttributes()
Returns dataAttr escaped as attribute string.
$namespaces
Namespace strings for addible element classes.
onValidate()
Validation hook.
validateAutosave()
If the form is autosaving the validation property is defaulted to false.
$ttl
Time until session expiry (seconds)
getValues()
Gets form-data from current PHP session.
static clearOldSessions(int $ttl=3600, string $pattern="/^htmlform-.*/")
clearOldSessions
__toString()
Renders form to HTML.
process()
Calls form validation and handles redirects.
$submitURL
HTML form action attribute.
redirect(string $url)
Redirects Browser to a different URL.
checkElementName(string $name)
Checks for duplicate subelement names.
buildUrlQuery(array $args=[])
Adding step parameter to already existing query.
$successURL
Specifies where the user is redirected to, once the form-data is valid.
updateInputValue(string $name)
Updates the value of an associated input element.
$sessionSlot
PHP session handle.
clearSession(bool $clearCsrfToken=true)
Deletes the current forms' PHP session data.
$internalFields
List of internal fieldnames that are not part of the results.
$validator
Contains the validator function of the form.
$cancelLabel
Contains the cancel button label of the form.
populate(array|object $data=[])
Fills subelement values.
setCurrentStep(int|null $step=null)
Validates step number of GET request.
htmlSubmitURL()
Returns form url escaped as attribute string.
getValuesWithLabel()
Gets form-data from current PHP session but also contain elemnt labels.
setDefaults()
Collects initial values across subclasses.
$sessionSlotName
Contains the name of the array in the PHP session, holding the form-data.
$url
url of the current page
$backLabel
Contains the back button label of the form.
getNamespaces()
Returns list of registered namespaces.
isEmpty()
Returns wether form has been submitted before or not.
buildUrl(array $args=[])
Builds URL from parts.
getNewCsrfToken()
Returns new XSRF token.
addElement(string $type, string $name, array $parameters)
Adds input or fieldset elements to htmlform.
Abstract element classes.
Classes for HTML input-elements.
htmlform class and autoloader
autoload($class)
PHP autoloader.