HtmlForm.php
Go to the documentation of this file.
1<?php
63namespace Depage\HtmlForm;
64
65
71function autoload($class)
72{
73 $class = str_replace('\\', '/', str_replace(__NAMESPACE__ . '\\', '', $class));
74 $file = __DIR__ . '/' . $class . '.php';
75
76 if (file_exists($file)) {
77 require_once($file);
78 }
79}
80
81spl_autoload_register(__NAMESPACE__ . '\autoload');
82
120{
122 'en' => ['us','gb','ie','au','nz'],
123 'de' => ['de','at','ch'],
124 'fr' => ['fr','ch','be','lu','ca'],
125 'it' => ['it','ch'],
126 ];
130 protected $method;
131
135 protected $url;
136
140 protected $submitURL;
141
145 protected $successURL;
146
150 protected $cancelURL;
151
155 protected $label;
156
160 protected $backLabel;
161
165 protected $cancelLabel;
166
170 protected $class;
171
175 protected $validator;
176
180 protected $jsValidation;
181
185 protected $jsAutosave;
186
191
195 protected $sessionSlot;
196
200 private $currentStepId;
201
205 private $steps = array();
206
210 protected $ttl;
211
215 public $valid;
216
220 public $isAutoSaveRequest = false;
221
225 protected $internalFields = array(
226 'formIsValid',
227 'formIsAutosaved',
228 'formName',
229 'formTimestamp',
230 'formStep',
231 'formFinalPost',
232 'formCsrfToken',
233 'formCaptcha',
234 );
235
239 protected $namespaces = array('\\Depage\\HtmlForm\\Elements');
248 public function __construct($name, $parameters = array(), $form = null)
249 {
250 $this->isAutoSaveRequest = isset($_POST['formAutosave']) && $_POST['formAutosave'] === "true";
251
252 $this->url = parse_url($_SERVER['REQUEST_URI']);
253
254 parent::__construct($name, $parameters, $this);
255
256 $this->url = parse_url($this->submitURL);
257 if (empty($this->successURL)) {
258 $this->successURL = $this->submitURL;
259 }
260 if (empty($this->cancelURL)) {
261 $this->cancelURL = $this->submitURL;
262 }
263
264 $this->currentStepId = isset($_GET['step']) ? $_GET['step'] : 0;
265
266 $this->startSession();
267
268 $this->valid = (isset($this->sessionSlot['formIsValid'])) ? $this->sessionSlot['formIsValid'] : null;
269
270 // set CSRF Token
271 if (!isset($this->sessionSlot['formCsrfToken'])) {
272 $this->sessionSlot['formCsrfToken'] = $this->getNewCsrfToken();
273 }
274
275 if (!isset($this->sessionSlot['formFinalPost'])) {
276 $this->sessionSlot['formFinalPost'] = false;
277 }
278
279 // create a hidden input to tell forms apart
280 $this->addHidden('formName')->setValue($this->name);
281
282 // create hidden input for submitted step
283 $this->addHidden('formStep')->setValue($this->currentStepId);
284
285 // create hidden input for CSRF token
286 $this->addHidden('formCsrfToken')->setValue($this->sessionSlot['formCsrfToken']);
287
288 $this->addChildElements();
289 }
299 protected function setDefaults()
300 {
301 parent::setDefaults();
302
303 $this->defaults['label'] = 'submit';
304 $this->defaults['cancelLabel'] = null;
305 $this->defaults['backLabel'] = null;
306 $this->defaults['class'] = '';
307 $this->defaults['method'] = 'post';
308 // @todo adjust submit url for steps when used
309 $this->defaults['submitURL'] = $_SERVER['REQUEST_URI'];
310 $this->defaults['successURL'] = null;
311 $this->defaults['cancelURL'] = null;
312 $this->defaults['validator'] = null;
313 $this->defaults['ttl'] = 60 * 60; // 60 minutes
314 $this->defaults['jsValidation'] = 'blur';
315 $this->defaults['jsAutosave'] = 'false';
316 }
317
326 private function startSession()
327 {
328 // check if there's an open session
329 if (!session_id()) {
330 $params = session_get_cookie_params();
331 $sessionName = session_name();
332
333 session_set_cookie_params(
334 $this->ttl,
335 $params['path'],
336 $params['domain'],
337 $params['secure'],
338 $params['httponly']
339 );
340 session_start();
341
342 // Extend the expiration time upon page load
343 if (isset($_COOKIE[$sessionName])) {
344 setcookie(
345 $sessionName,
346 $_COOKIE[$sessionName],
347 time() + $this->ttl,
348 $params['path'],
349 $params['domain'],
350 $params['secure'],
351 $params['httponly']
352 );
353 }
354 }
355 $this->sessionSlotName = 'htmlform-' . $this->name . '-data';
356 $this->sessionSlot =& $_SESSION[$this->sessionSlotName];
357
358 $this->sessionExpiry();
359 }
368 private function sessionExpiry()
369 {
370 if (isset($this->ttl) && is_numeric($this->ttl)) {
371 $timestamp = time();
372
373 if (
374 isset($this->sessionSlot['formTimestamp'])
375 && ($timestamp - $this->sessionSlot['formTimestamp'] > $this->ttl)
376 ) {
377 $this->clearSession();
378 $this->sessionSlot =& $_SESSION[$this->sessionSlotName];
379 }
380
381 $this->sessionSlot['formTimestamp'] = $timestamp;
382 }
383 }
389 public function isEmpty()
390 {
391 return !isset($this->sessionSlot['formName']);
392 }
393
399 protected function getNewCsrfToken()
400 {
401 return base64_encode(openssl_random_pseudo_bytes(16));
402 }
403
415 protected function addElement($type, $name, $parameters)
416 {
417 $this->checkElementName($name);
418
419 $newElement = parent::addElement($type, $name, $parameters);
420
421 if ($newElement instanceof Elements\Step) { $this->steps[] = $newElement; }
422 if ($newElement instanceof Abstracts\Input) { $this->updateInputValue($name); }
423
424 return $newElement;
425 }
435 public function checkElementName($name)
436 {
437 foreach ($this->getElements(true) as $element) {
438 if ($element->getName() === $name) {
439 throw new Exceptions\DuplicateElementNameException("Element name \"{$name}\" already in use.");
440 }
441 }
442 }
448 private function getCurrentElements()
449 {
450 $currentElements = array();
451
452 foreach ($this->elements as $element) {
453 if ($element instanceof Abstracts\Container) {
454 if (
455 !($element instanceof Elements\Step)
456 || (isset($this->steps[$this->currentStepId]) && ($element == $this->steps[$this->currentStepId]))
457 ) {
458 $currentElements = array_merge($currentElements, $element->getElements());
459 }
460 } else {
461 $currentElements[] = $element;
462 }
463 }
464
465 return $currentElements;
466 }
473 public function registerNamespace($namespace)
474 {
475 $this->namespaces[] = $namespace;
476 }
482 public function getNamespaces()
483 {
484 return $this->namespaces;
485 }
486
493 private function inCurrentStep($name)
494 {
495 return in_array($this->getElement($name), $this->getCurrentElements());
496 }
507 public function setCurrentStep($step = null)
508 {
509 if (!is_null($step)) {
510 $this->currentStepId = $step;
511 }
512 if (!is_numeric($this->currentStepId)
513 || ($this->currentStepId > count($this->steps) - 1)
514 || ($this->currentStepId < 0)
515 ) {
516 $this->currentStepId = $this->getFirstInvalidStep();
517 }
518 }
524 public function getSteps()
525 {
526 return $this->steps;
527 }
533 public function getCurrentStepId()
534 {
535 return $this->currentStepId;
536 }
546 public function getFirstInvalidStep()
547 {
548 if (count($this->steps) > 0) {
549 foreach ($this->steps as $stepNumber => $step) {
550 if (!$step->validate()) {
551 return $stepNumber;
552 }
553 }
560 return count($this->steps) - 1;
561 } else {
562 return 0;
563 }
564 }
570 public function buildUrlQuery($args = array())
571 {
572 $query = '';
573 $queryParts = array();
574
575 if (isset($this->url['query']) && $this->url['query'] != "") {
576 //decoding query string
577 $query = html_entity_decode($this->url['query']);
578
579 //parsing the query into an array
580 parse_str($query, $queryParts);
581 }
582
583 foreach ($args as $name => $value) {
584 if ($value != "") {
585 $queryParts[$name] = $value;
586 } elseif (isset($queryParts[$name])) {
587 unset($queryParts[$name]);
588 }
589 }
590
591 // build the query again
592 $query = http_build_query($queryParts);
593
594 if ($query != "") {
595 $query = "?" . $query;
596 }
597
598 return $query;
599 }
600
612 public function updateInputValue($name)
613 {
614 $element = $this->getElement($name);
615
616 // handle captcha phrase
617 if ($this->getElement($name) instanceof Elements\Captcha) {
618 $element->setSessionSlot($this->sessionSlot);
619 }
620
621 // if it's a post, take the value from there and save it to the session
622 if (
623 isset($_POST['formName']) && ($_POST['formName'] === $this->name)
624 && $this->inCurrentStep($name)
625 && isset($_POST['formCsrfToken']) && $_POST['formCsrfToken'] === $this->sessionSlot['formCsrfToken']
626 ) {
627 if ($this->getElement($name) instanceof Elements\File) {
628 // handle uploaded file
629 $oldValue = isset($this->sessionSlot[$name]) ? $this->sessionSlot[$name] : null;
630 $this->sessionSlot[$name] = $element->handleUploadedFiles($oldValue);
631 } else if (!$element->getDisabled()) {
632 // save value
633 $value = isset($_POST[$name]) ? $_POST[$name] : null;
634 $this->sessionSlot[$name] = $element->setValue($value);
635 } else if (!isset($this->sessionSlot[$name])) {
636 // set default value for disabled elements
637 $this->sessionSlot[$name] = $element->setValue($element->getDefaultValue());
638 }
639 }
640 // if it's not a post, try to get the value from the session
641 else if (isset($this->sessionSlot[$name])) {
642 $element->setValue($this->sessionSlot[$name]);
643 }
644 }
651 public function clearInputValue($name)
652 {
653 $element = $this->getElement($name);
654
655 $this->sessionSlot[$name] = $element->clearValue();
656 }
657
667 public function populate($data = array())
668 {
669 foreach ($this->getElements() as $element) {
670 $name = $element->name;
671 if (!in_array($name, $this->internalFields)) {
672 if (is_array($data) && isset($data[$name])) {
673 $value = $data[$name];
674 } else if (is_object($data) && isset($data->$name)) {
675 $value = $data->$name;
676 }
677
678 if (isset($value)) {
679 $element->setDefaultValue($value);
680 if ($element->getDisabled() && !isset($this->sessionSlot[$name])) {
681 $this->sessionSlot[$name] = $value;
682 }
683 }
684
685 unset($value);
686 }
687 }
688 }
700 public function process()
701 {
702 $this->setCurrentStep();
703 // if there's post-data from this form
704 if (isset($_POST['formName']) && ($_POST['formName'] === $this->name)) {
705 // save in session if submission was from last step
706 $this->sessionSlot['formFinalPost'] = count($this->steps) == 0 || $_POST['formStep'] + 1 == count($this->steps)
708
709 if (!empty($this->cancelLabel) && isset($_POST['formSubmit']) && $_POST['formSubmit'] === $this->cancelLabel) {
710 // cancel button was pressed
711 $this->clearSession();
712 $this->redirect($this->cancelURL);
713 } elseif ($this->isAutoSaveRequest) {
714 // do not redirect -> is autosave
715 $this->onPost();
716 } elseif (!empty($this->backLabel) && isset($_POST['formSubmit']) && $_POST['formSubmit'] === $this->backLabel) {
717 // back button was pressed
718 $this->onPost();
719
720 $this->sessionSlot['formFinalPost'] = false;
721 $prevStep = $this->currentStepId - 1;
722 if ($prevStep < 0) {
723 $prevStep = 0;
724 }
725 $urlStepParameter = ($prevStep <= 0) ? $this->buildUrlQuery(array('step' => '')) : $this->buildUrlQuery(array('step' => $prevStep));
726 $this->redirect($this->url['path'] . $urlStepParameter);
727 } elseif ($this->validate()) {
728 // form was successfully submitted
729 $this->onPost();
730
731 $this->redirect($this->successURL);
732 } else {
733 // goto to next step or display first invalid step
734 $this->onPost();
735
736 $nextStep = $this->currentStepId + 1;
737 $firstInvalidStep = $this->getFirstInvalidStep();
738 if ($nextStep > $firstInvalidStep) {
739 $nextStep = $firstInvalidStep;
740 }
741 if ($nextStep > count($this->steps)) {
742 $nextStep = count($this->steps) - 1;
743 }
744 $urlStepParameter = ($nextStep == 0) ? $this->buildUrlQuery(array('step' => '')) : $this->buildUrlQuery(array('step' => $nextStep));
745 $this->redirect($this->url['path'] . $urlStepParameter);
746 }
747 }
748 }
759 public function validate()
760 {
761 // onValidate hook for custom required/validation rules
762 $this->valid = $this->onValidate();
763
764 $this->valid = $this->valid && $this->validateAutosave();
765
766 if ($this->valid && !is_null($this->validator)) {
767 if (is_callable($this->validator)) {
768 $this->valid = call_user_func($this->validator, $this, $this->getValues());
769 } else {
770 throw new exceptions\validatorNotCallable("The validator paramater must be callable");
771 }
772 }
773 $this->valid = $this->valid && $this->sessionSlot['formFinalPost'];
774
775 // save validation-state in session
776 $this->sessionSlot['formIsValid'] = $this->valid;
777
778 return $this->valid;
779 }
789 protected function onPost()
790 {
791 return true;
792 }
802 protected function onValidate()
803 {
804 return true;
805 }
815 public function validateAutosave()
816 {
817 parent::validate();
818
819 if (isset($_POST['formCsrfToken'])) {
820 $hasCorrectToken = $_POST['formCsrfToken'] === $this->sessionSlot['formCsrfToken'];
821 $this->valid = $this->valid && $hasCorrectToken;
822
823 if (!$hasCorrectToken) {
824 http_response_code(400);
825 $this->log("HtmlForm: Requst invalid because of incorrect CsrfToken");
826 }
827 }
828
829 $partValid = $this->valid;
830
831 // save data in session when autosaving but don't validate successfully
832 if ($this->isAutoSaveRequest
833 || (isset($this->sessionSlot['formIsAutosaved'])
834 && $this->sessionSlot['formIsAutosaved'] === true)
835 ) {
836 $this->valid = false;
837 }
838
839 // save whether form was autosaved the last time
840 $this->sessionSlot['formIsAutosaved'] = $this->isAutoSaveRequest;
841
842 return $partValid;
843 }
849 public function getValues()
850 {
851 if (isset($this->sessionSlot)) {
852 // remove internal attributes from values
853 return array_diff_key($this->sessionSlot, array_fill_keys($this->internalFields, ''));
854 } else {
855 return null;
856 }
857 }
863 public function getValuesWithLabel()
864 {
865 //get values first
866 $values = $this->getValues();
867 $valuesWithLabel = array();
868 if (isset($values)) {
869 foreach ($values as $element => $value) {
870 $elem = $this->getElement($element);
871
872 if ($elem) $valuesWithLabel[$element] = array(
873 "value" => $value,
874 "label" => $elem->getLabel()
875 );
876 }
877
878 return $valuesWithLabel;
879 } else {
880 return null;
881 }
882 }
888 public function redirect($url)
889 {
890 header('Location: ' . $url);
891 die( "Tried to redirect you to <a href=\"$url\">$url</a>");
892 }
900 public function clearSession($clearCsrfToken = true)
901 {
902 if ($clearCsrfToken) {
903 // clear everything
904 $this->clearValue();
905
906 unset($_SESSION[$this->sessionSlotName]);
907 unset($this->sessionSlot);
908 } else {
909 // clear everything except internal fields
910 foreach ($this->getElements(false) as $element) {
911 if (!$element->getDisabled() && !in_array($element->name, $this->internalFields)) {
912 unset($this->sessionSlot[$element->name]);
913 }
914 }
915 }
916 }
923 public static function clearOldSessions($ttl = 3600, $pattern = "/^htmlform-.*/")
924 {
925 $timestamp = time();
926
927 if (empty($_SESSION)) {
928 return;
929 }
930 foreach ($_SESSION as $key => &$val) {
931 if (preg_match($pattern, $key)
932 && isset($val['formTimestamp'])
933 && ($timestamp - $val['formTimestamp'] > $ttl)
934 ) {
935 unset($_SESSION[$key]);
936 }
937 }
938 }
939
943 protected function htmlDataAttributes()
944 {
945 $this->dataAttr['jsvalidation'] = $this->jsValidation;
946 $this->dataAttr['jsautosave'] = $this->jsAutosave === true ? "true" : $this->jsAutosave;
947
948 return parent::htmlDataAttributes();
949 }
953 protected function htmlSubmitURL()
954 {
955 $scheme = isset($this->form->url['scheme']) ? $this->form->url['scheme'] . '://' : '';
956 $host = isset($this->form->url['host']) ? $this->form->url['host'] : '';
957 $port = isset($this->form->url['port']) ? ':' . $this->form->url['port'] : '';
958 $path = isset($this->form->url['path']) ? $this->form->url['path'] : '';
959 $baseUrl = "$scheme$host$port$path";
960 $step = $this->currentStepId != 0 ? $this->currentStepId : '';
961
962 return $this->htmlEscape($baseUrl . $this->form->buildUrlQuery(['step' => $step]));
963 }
972 public function __toString()
973 {
974 $renderedElements = '';
975 $submit = '';
976 $cancel = '';
977 $back = '';
978 $label = $this->htmlLabel();
979 $cancellabel = $this->htmlCancelLabel();
980 $backlabel = $this->htmlBackLabel();
981 $class = $this->htmlClass();
982 $method = $this->htmlMethod();
983 $submitURL = $this->htmlSubmitURL();
984 $dataAttr = $this->htmlDataAttributes();
985 $disabledAttr = $this->disabled ? " disabled=\"disabled\"" : "";
986
987 foreach ($this->elementsAndHtml as $element) {
988 // leave out inactive step elements
989 if (!($element instanceof elements\step)
990 || (isset($this->steps[$this->currentStepId]) && $this->steps[$this->currentStepId] == $element)
991 ) {
992 $renderedElements .= $element;
993 }
994 }
995
996 if (!is_null($this->cancelLabel)) {
997 $cancel = "<p id=\"{$this->name}-cancel\" class=\"cancel\"><input type=\"submit\" name=\"formSubmit\" value=\"{$cancellabel}\"$disabledAttr></p>\n";
998 }
999 if (!is_null($this->backLabel) && $this->currentStepId > 0) {
1000 $back = "<p id=\"{$this->name}-back\" class=\"back\"><input type=\"submit\" name=\"formSubmit\" value=\"{$backlabel}\"$disabledAttr></p>\n";
1001 }
1002 if (!empty($this->label)) {
1003 $submit = "<p id=\"{$this->name}-submit\" class=\"submit\"><input type=\"submit\" name=\"formSubmit\" value=\"{$label}\"$disabledAttr></p>\n";
1004 }
1005
1006
1007 return "<form id=\"{$this->name}\" name=\"{$this->name}\" class=\"depage-form {$class}\" method=\"{$method}\" action=\"{$submitURL}\"{$dataAttr} enctype=\"multipart/form-data\">" . "\n" .
1008 $renderedElements .
1009 $submit .
1010 $cancel .
1011 $back .
1012 "</form>";
1013 }
1014}
1015
1163/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
container element base class
Definition Container.php:22
getElements($includeFieldsets=false)
Returns containers subelements.
addChildElements()
Sub-element generator hook.
$form
Parent form object reference.
Definition Container.php:34
clearValue()
Deletes values of all child elements.
getElement($name, $includeFieldsets=false)
Gets subelement by name.
log($argument, $type=null)
error & warning logger
Definition Element.php:242
$dataAttr
Extra information about the data that is saved inside the element.
Definition Element.php:68
htmlEscape($options=array())
Escapes HTML in strings and arrays of strings.
Definition Element.php:267
main interface to users
Definition HtmlForm.php:120
$valid
Form validation result/status.
Definition HtmlForm.php:215
$isAutoSaveRequest
true if form request is from autosave call
Definition HtmlForm.php:220
addElement($type, $name, $parameters)
Adds input or fieldset elements to htmlform.
Definition HtmlForm.php:415
$method
HTML form method attribute.
Definition HtmlForm.php:130
getCurrentStepId()
Returns the current step id.
Definition HtmlForm.php:533
$label
Contains the submit button label of the form.
Definition HtmlForm.php:155
validate()
Validates the forms subelements.
Definition HtmlForm.php:759
setCurrentStep($step=null)
Validates step number of GET request.
Definition HtmlForm.php:507
$jsAutosave
Contains the javascript autosave type of the form.
Definition HtmlForm.php:185
$class
Contains the additional class value of the form.
Definition HtmlForm.php:170
$cancelURL
Specifies where the user is redirected to, once the form-data is cancelled.
Definition HtmlForm.php:150
updateInputValue($name)
Updates the value of an associated input element.
Definition HtmlForm.php:612
__construct($name, $parameters=array(), $form=null)
HtmlForm class constructor.
Definition HtmlForm.php:248
getSteps()
Returns an array of steps.
Definition HtmlForm.php:524
$jsValidation
Contains the javascript validation type of the form.
Definition HtmlForm.php:180
getFirstInvalidStep()
Returns first step that didn't pass validation.
Definition HtmlForm.php:546
checkElementName($name)
Checks for duplicate subelement names.
Definition HtmlForm.php:435
htmlDataAttributes()
Returns dataAttr escaped as attribute string.
Definition HtmlForm.php:943
clearInputValue($name)
clearInputValue
Definition HtmlForm.php:651
$namespaces
Namespace strings for addible element classes.
Definition HtmlForm.php:239
onValidate()
Validation hook.
Definition HtmlForm.php:802
validateAutosave()
If the form is autosaving the validation property is defaulted to false.
Definition HtmlForm.php:815
$ttl
Time until session expiry (seconds)
Definition HtmlForm.php:210
getValues()
Gets form-data from current PHP session.
Definition HtmlForm.php:849
__toString()
Renders form to HTML.
Definition HtmlForm.php:972
populate($data=array())
Fills subelement values.
Definition HtmlForm.php:667
process()
Calls form validation and handles redirects.
Definition HtmlForm.php:700
$submitURL
HTML form action attribute.
Definition HtmlForm.php:140
$successURL
Specifies where the user is redirected to, once the form-data is valid.
Definition HtmlForm.php:145
$sessionSlot
PHP session handle.
Definition HtmlForm.php:195
$internalFields
List of internal fieldnames that are not part of the results.
Definition HtmlForm.php:225
$validator
Contains the validator function of the form.
Definition HtmlForm.php:175
registerNamespace($namespace)
Stores element namespaces for adding.
Definition HtmlForm.php:473
$cancelLabel
Contains the cancel button label of the form.
Definition HtmlForm.php:165
htmlSubmitURL()
Returns dataAttr escaped as attribute string.
Definition HtmlForm.php:953
getValuesWithLabel()
Gets form-data from current PHP session but also contain elemnt labels.
Definition HtmlForm.php:863
setDefaults()
Collects initial values across subclasses.
Definition HtmlForm.php:299
static clearOldSessions($ttl=3600, $pattern="/^htmlform-.*/")
clearOldSessions
Definition HtmlForm.php:923
$sessionSlotName
Contains the name of the array in the PHP session, holding the form-data.
Definition HtmlForm.php:190
redirect($url)
Redirects Browser to a different URL.
Definition HtmlForm.php:888
$url
url of the current page
Definition HtmlForm.php:135
buildUrlQuery($args=array())
Adding step parameter to already existing query.
Definition HtmlForm.php:570
$backLabel
Contains the back button label of the form.
Definition HtmlForm.php:160
getNamespaces()
Returns list of registered namespaces.
Definition HtmlForm.php:482
isEmpty()
Returns wether form has been submitted before or not.
Definition HtmlForm.php:389
getNewCsrfToken()
Returns new XSRF token.
Definition HtmlForm.php:399
clearSession($clearCsrfToken=true)
Deletes the current forms' PHP session data.
Definition HtmlForm.php:900
htmlform class and autoloader
autoload($class)
PHP autoloader.
Definition HtmlForm.php:71