htmlform.php (2014-03-03 17:09:40 +0100, commit:e2a5b60)
Go to the documentation of this file.
1 <?php
158 namespace depage\htmlform;
159 
160 
166 function autoload($class)
167 {
168  $class = str_replace('\\', '/', str_replace(__NAMESPACE__ . '\\', '', $class));
169  $file = __DIR__ . '/' . $class . '.php';
170 
171  if (file_exists($file)) {
172  require_once($file);
173  }
174 }
175 
176 spl_autoload_register(__NAMESPACE__ . '\autoload');
177 
218 {
222  protected $method;
226  protected $submitURL;
230  protected $successURL;
234  protected $label;
238  protected $class;
242  protected $sessionSlotName;
246  protected $sessionSlot;
250  private $currentStepId;
254  private $steps = array();
258  protected $ttl;
262  public $valid;
263 
272  public function __construct($name, $parameters = array(), $form = null)
273  {
274  $this->url = parse_url($_SERVER['REQUEST_URI']);
275 
276  parent::__construct($name, $parameters, $this);
277 
278  $this->currentStepId = isset($_GET['step']) ? $_GET['step'] : 0;
279 
280  // check if there's an open session
281  if (!session_id()) {
282  session_set_cookie_params($this->ttl, "/");
283  session_start();
284  }
285  $this->sessionSlotName = $this->name . '-data';
286  $this->sessionSlot =& $_SESSION[$this->sessionSlotName];
287 
288  $this->sessionExpiry();
289 
290  $this->valid = (isset($this->sessionSlot['formIsValid'])) ? $this->sessionSlot['formIsValid'] : null;
291 
292  // set CSRF Token
293  if (!isset($this->sessionSlot['formCsrfToken'])) {
294  $this->sessionSlot['formCsrfToken'] = $this->getNewCsrfToken();
295  }
296 
297  if (!isset($this->sessionSlot['formFinalPost'])) {
298  $this->sessionSlot['formFinalPost'] = false;
299  }
300 
301  // create a hidden input to tell forms apart
302  $this->addHidden('formName')->setValue($this->name);
303 
304  // create hidden input for submitted step
305  $this->addHidden('formStep')->setValue($this->currentStepId);
306 
307  // create hidden input for CSRF token
308  $this->addHidden('formCsrfToken')->setValue($this->sessionSlot['formCsrfToken']);
309 
310  $this->addChildElements();
311  }
312 
322  protected function setDefaults()
323  {
324  parent::setDefaults();
325 
326  $this->defaults['label'] = 'submit';
327  $this->defaults['cancelLabel'] = null;
328  $this->defaults['backLabel'] = null;
329  $this->defaults['class'] = '';
330  $this->defaults['method'] = 'post';
331  // @todo adjust submit url for steps when used
332  $this->defaults['submitURL'] = $_SERVER['REQUEST_URI'];
333  $this->defaults['successURL'] = $_SERVER['REQUEST_URI'];
334  $this->defaults['cancelURL'] = $_SERVER['REQUEST_URI'];
335  $this->defaults['validator'] = null;
336  $this->defaults['ttl'] = 30 * 60; // 30 minutes
337  $this->defaults['jsValidation'] = 'blur';
338  $this->defaults['jsAutosave'] = 'false';
339  }
340 
349  private function sessionExpiry()
350  {
351  if (isset($this->ttl) && is_numeric($this->ttl)) {
352  $timestamp = time();
353 
354  if (
355  isset($this->sessionSlot['formTimestamp'])
356  && ($timestamp - $this->sessionSlot['formTimestamp'] > $this->ttl)
357  ) {
358  $this->clearSession();
359  $this->sessionSlot =& $_SESSION[$this->sessionSlotName];
360  }
361 
362  $this->sessionSlot['formTimestamp'] = $timestamp;
363  }
364  }
365 
377  protected function addElement($type, $name, $parameters)
378  {
379  $this->checkElementName($name);
380 
381  $newElement = parent::addElement($type, $name, $parameters);
382 
383  if ($newElement instanceof elements\step) { $this->steps[] = $newElement; }
384  if ($newElement instanceof abstracts\input) { $this->updateInputValue($name); }
385 
386  return $newElement;
387  }
388 
400  public function updateInputValue($name)
401  {
402  // if it's a post, take the value from there and save it to the session
403  if (
404  isset($_POST['formName']) && ($_POST['formName'] === $this->name)
405  && $this->inCurrentStep($name)
406  && isset($_POST['formCsrfToken']) && $_POST['formCsrfToken'] === $this->sessionSlot['formCsrfToken']
407  ) {
408  if ($this->getElement($name) instanceof elements\file) {
409  // handle uploaded file
410  $oldValue = isset($this->sessionSlot[$name]) ? $this->sessionSlot[$name] : null;
411  $this->sessionSlot[$name] = $this->getElement($name)->handleUploadedFiles($oldValue);
412  } else {
413  // save value
414  $value = isset($_POST[$name]) ? $_POST[$name] : null;
415  $this->sessionSlot[$name] = $this->getElement($name)->setValue($value);
416  }
417  }
418  // if it's not a post, try to get the value from the session
419  else if (isset($this->sessionSlot[$name])) {
420  $this->getElement($name)->setValue($this->sessionSlot[$name]);
421  }
422  }
423 
430  private function inCurrentStep($name)
431  {
432  return in_array($this->getElement($name), $this->getCurrentElements());
433  }
434 
445  private function setCurrentStep()
446  {
447  if (!is_numeric($this->currentStepId)
448  || ($this->currentStepId > count($this->steps) - 1)
449  || ($this->currentStepId < 0)
450  ) {
451  $this->currentStepId = $this->getFirstInvalidStep();
452  }
453  }
454 
460  public function getSteps()
461  {
462  return $this->steps;
463  }
464 
470  public function getCurrentStepId()
471  {
472  return $this->currentStepId;
473  }
474 
480  private function getCurrentElements()
481  {
482  $currentElements = array();
483 
484  foreach ($this->elements as $element) {
485  if ($element instanceof elements\fieldset) {
486  if (
487  !($element instanceof elements\step)
488  || (isset($this->steps[$this->currentStepId]) && ($element == $this->steps[$this->currentStepId]))
489  ) {
490  $currentElements = array_merge($currentElements, $element->getElements());
491  }
492  } else {
493  $currentElements[] = $element;
494  }
495  }
496 
497  return $currentElements;
498  }
499 
505  protected function getNewCsrfToken()
506  {
507  return base64_encode(openssl_random_pseudo_bytes(16));
508  }
509 
518  public function __toString()
519  {
520  $renderedElements = '';
521  $label = $this->htmlLabel();
522  $cancellabel = $this->htmlCancelLabel();
523  $backlabel = $this->htmlBackLabel();
524  $class = $this->htmlClass();
525  $method = $this->htmlMethod();
526  $submitURL = $this->htmlSubmitURL();
527  $jsValidation = $this->htmlJsValidation();
528  $jsAutosave = $this->jsAutosave === true ? "true" : $this->htmlJsAutosave();
529 
530  foreach ($this->elementsAndHtml as $element) {
531  // leave out inactive step elements
532  if (!($element instanceof elements\step)
533  || (isset($this->steps[$this->currentStepId]) && $this->steps[$this->currentStepId] == $element)
534  ) {
535  $renderedElements .= $element;
536  }
537  }
538 
539  if (!is_null($this->cancelLabel)) {
540  $cancel = "<p id=\"{$this->name}-cancel\" class=\"cancel\"><input type=\"submit\" name=\"formSubmit\" value=\"{$cancellabel}\"></p>\n";
541  } else {
542  $cancel = "";
543  }
544  if (!is_null($this->backLabel) && $this->currentStepId > 0) {
545  $back = "<p id=\"{$this->name}-back\" class=\"back\"><input type=\"submit\" name=\"formSubmit\" value=\"{$backlabel}\"></p>\n";
546  } else {
547  $back = "";
548  }
549 
550 
551  return "<form id=\"{$this->name}\" name=\"{$this->name}\" class=\"depage-form {$class}\" method=\"{$method}\" action=\"{$submitURL}\" data-jsvalidation=\"{$jsValidation}\" data-jsautosave=\"{$jsAutosave}\" enctype=\"multipart/form-data\">" . "\n" .
552  $renderedElements .
553  "<p id=\"{$this->name}-submit\" class=\"submit\"><input type=\"submit\" name=\"formSubmit\" value=\"{$label}\"></p>" . "\n" .
554  $cancel .
555  $back .
556  "</form>";
557  }
558 
570  public function process()
571  {
572  $this->setCurrentStep();
573  // if there's post-data from this form
574  if (isset($_POST['formName']) && ($_POST['formName'] === $this->name)) {
575  // save in session if submission was from last step
576  $this->sessionSlot['formFinalPost'] = count($this->steps) == 0 || $_POST['formStep'] + 1 == count($this->steps);
577 
578  if (!empty($this->cancelLabel) && isset($_POST['formSubmit']) && $_POST['formSubmit'] === $this->cancelLabel) {
579  // cancel button was pressed
580  $this->clearSession();
581  $this->redirect($this->cancelURL);
582  } elseif (!empty($this->backLabel) && isset($_POST['formSubmit']) && $_POST['formSubmit'] === $this->backLabel) {
583  // back button was pressed
584  $this->sessionSlot['formFinalPost'] = false;
585  $prevStep = $this->currentStepId - 1;
586  if ($prevStep < 0) {
587  $prevStep = 0;
588  }
589  $urlStepParameter = ($prevStep <= 0) ? $this->buildUrlQuery(array('step' => '')) : $this->buildUrlQuery(array('step' => $prevStep));
590  $this->redirect($this->url['path'] . $urlStepParameter);
591  } elseif ($this->validate()) {
592  // form was successfully submitted
593  $this->redirect($this->successURL);
594  } else {
595  // goto to next step or display first invalid step
596  $nextStep = $this->currentStepId + 1;
597  $firstInvalidStep = $this->getFirstInvalidStep();
598  if ($nextStep > $firstInvalidStep) {
599  $nextStep = $firstInvalidStep;
600  }
601  if ($nextStep > count($this->steps)) {
602  $nextStep = count($this->steps) - 1;
603  }
604  $urlStepParameter = ($nextStep == 0) ? $this->buildUrlQuery(array('step' => '')) : $this->buildUrlQuery(array('step' => $nextStep));
605  $this->redirect($this->url['path'] . $urlStepParameter);
606  }
607  }
608  }
609 
615  public function buildUrlQuery($args = array())
616  {
617  $query = '';
618  $queryParts = array();
619 
620  if (isset($this->url['query']) && $this->url['query'] != "") {
621  //decoding query string
622  $query = html_entity_decode($this->url['query']);
623 
624  //parsing the query into an array
625  parse_str($query, $queryParts);
626  }
627 
628  foreach ($args as $name => $value) {
629  if ($value != "") {
630  $queryParts[$name] = $value;
631  } elseif (isset($queryParts[$name])) {
632  unset($queryParts[$name]);
633  }
634  }
635 
636  // build the query again
637  $query = http_build_query($queryParts);
638 
639  if ($query != "") {
640  $query = "?" . $query;
641  }
642 
643  return $query;
644  }
645 
655  public function getFirstInvalidStep()
656  {
657  if (count($this->steps) > 0) {
658  foreach ($this->steps as $stepNumber => $step) {
659  if (!$step->validate()) {
660  return $stepNumber;
661  }
662  }
669  return count($this->steps) - 1;
670  } else {
671  return 0;
672  }
673  }
674 
680  public function redirect($url)
681  {
682  if (isset($_POST['formAutosave']) && $_POST['formAutosave'] === "true") {
683  // don't redirect > it's from ajax
684  } else {
685  header('Location: ' . $url);
686  die( "Tried to redirect you to <a href=\"$url\">$url</a>");
687  }
688  }
689 
700  public function validate()
701  {
702  // onValidate hook for custom required/validation rules
703  $this->valid = $this->onValidate();
704 
705  $this->valid = $this->valid && $this->validateAutosave();
706 
707  if ($this->valid && !is_null($this->validator)) {
708  if (is_callable($this->validator)) {
709  $this->valid = call_user_func($this->validator, $this, $this->getValues());
710  } else {
711  throw new exceptions\validatorNotCallable("The validator paramater must be callable");
712  }
713  }
714  $this->valid = $this->valid && $this->sessionSlot['formFinalPost'];
715 
716  // save validation-state in session
717  $this->sessionSlot['formIsValid'] = $this->valid;
718 
719  return $this->valid;
720  }
721 
731  public function validateAutosave()
732  {
733  parent::validate();
734 
735  if (isset($_POST['formCsrfToken'])) {
736  $this->valid = $this->valid && $_POST['formCsrfToken'] === $this->sessionSlot['formCsrfToken'];
737  }
738 
739  $partValid = $this->valid;
740 
741  // save data in session when autosaving but don't validate successfully
742  if ((isset($_POST['formAutosave']) && $_POST['formAutosave'] === "true")
743  || (isset($this->sessionSlot['formIsAutosaved'])
744  && $this->sessionSlot['formIsAutosaved'] === true)) {
745  $this->valid = false;
746  }
747 
748  // save whether form was autosaved the last time
749  $this->sessionSlot['formIsAutosaved'] = isset($_POST['formAutosave']) && $_POST['formAutosave'] === "true";
750 
751  return $partValid;
752  }
753 
759  public function isEmpty()
760  {
761  return !isset($this->sessionSlot['formName']);
762  }
763 
773  public function populate($data = array())
774  {
775  foreach ($data as $name => $value) {
776  $element = $this->getElement($name);
777  if ($element) {
778  $element->setDefaultValue($value);
779  }
780  }
781  }
782 
788  public function getValues()
789  {
790  if (isset($this->sessionSlot)) {
791  // remove internal attributes from values
792  return array_diff_key($this->sessionSlot, array(
793  'formIsValid' => '',
794  'formIsAutosaved' => '',
795  'formName' => '',
796  'formTimestamp' => '',
797  'formStep' => '',
798  'formFinalPost' => '',
799  'formCsrfToken' => '',
800  ));
801  } else {
802  return null;
803  }
804  }
805 
811  public function getValuesWithLabel()
812  {
813  //get values first
814  $values = $this->getValues();
815  $values_with_label =array();
816  if (isset($values)) {
817  foreach ($values as $element => $value) {
818  $elem = $this->getElement($element);
819  if ($elem) $values_with_label[$element] = array("value"=>$value, "label"=>$elem->getLabel());
820  }
821 
822  return $values_with_label;
823  } else {
824  return null;
825  }
826  }
827 
837  public function checkElementName($name)
838  {
839  foreach ($this->getElements(true) as $element) {
840  if ($element->getName() === $name) {
841  throw new exceptions\duplicateElementNameException("Element name \"{$name}\" already in use.");
842  }
843  }
844  }
845 
851  public function clearSession()
852  {
853  $this->clearValue();
854 
855  unset($_SESSION[$this->sessionSlotName]);
856  unset($this->sessionSlot);
857  }
858 
868  protected function onValidate()
869  {
870  return true;
871  }
872 }
873 
1021 /* vim:set ft=php sw=4 sts=4 fdm=marker et : */
getElements($includeFieldsets=false)
Returns containers subelements.
Definition: container.php:222
clearSession()
Deletes the current forms&#39; PHP session data.
Definition: htmlform.php:851
parent for element specific exceptions
$sessionSlotName
Contains the name of the array in the PHP session, holding the form-data.
Definition: htmlform.php:242
$class
Contains the additional class value of the form.
Definition: htmlform.php:238
$submitURL
HTML form action attribute.
Definition: htmlform.php:226
getSteps()
Returns an array of steps.
Definition: htmlform.php:460
__toString()
Renders form to HTML.
Definition: htmlform.php:518
$ttl
Time until session expiry (seconds)
Definition: htmlform.php:258
clearValue()
Deletes values of all child elements.
Definition: container.php:264
process()
Calls form validation and handles redirects.
Definition: htmlform.php:570
setDefaults()
Collects initial values across subclasses.
Definition: htmlform.php:322
$form
Parent form object reference.
Definition: container.php:34
$sessionSlot
PHP session handle.
Definition: htmlform.php:246
main interface to users
Definition: htmlform.php:217
getValues()
Gets form-data from current PHP session.
Definition: htmlform.php:788
checkElementName($name)
Checks for duplicate subelement names.
Definition: htmlform.php:837
onValidate()
Validation hook.
Definition: htmlform.php:868
__construct($name, $parameters=array(), $form=null)
htmlform class constructor
Definition: htmlform.php:272
$method
HTML form method attribute.
Definition: htmlform.php:222
updateInputValue($name)
Updates the value of an associated input element.
Definition: htmlform.php:400
populate($data=array())
Fills subelement values.
Definition: htmlform.php:773
getCurrentStepId()
Returns the current step id.
Definition: htmlform.php:470
validate()
Validates the forms subelements.
Definition: htmlform.php:700
getValuesWithLabel()
Gets form-data from current PHP session but also contain elemnt labels.
Definition: htmlform.php:811
$successURL
Specifies where the user is redirected to, once the form-data is valid.
Definition: htmlform.php:230
addElement($type, $name, $parameters)
Adds input or fieldset elements to htmlform.
Definition: htmlform.php:377
getNewCsrfToken()
Returns new XSRF token.
Definition: htmlform.php:505
isEmpty()
Returns wether form has been submitted before or not.
Definition: htmlform.php:759
container element base class
Definition: container.php:21
$label
Contains the submit button label of the form.
Definition: htmlform.php:234
$valid
Form validation result/status.
Definition: htmlform.php:262
getFirstInvalidStep()
Returns first step that didn&#39;t pass validation.
Definition: htmlform.php:655
redirect($url)
Redirects Browser to a different URL.
Definition: htmlform.php:680
buildUrlQuery($args=array())
Adding step parameter to already existing query.
Definition: htmlform.php:615
autoload($class)
PHP autoloader.
Definition: htmlform.php:166
getElement($name)
Gets subelement by name.
Definition: container.php:248
addChildElements()
Sub-element generator hook.
Definition: container.php:125