| /** | |
| * Class implementing the Page Controller pattern for multipage forms | |
| * | |
| * PHP version 5 | |
| * | |
| * LICENSE: | |
| * | |
| * Copyright (c) 2006-2010, Alexey Borzov <avb@php.net>, | |
| * Bertrand Mansion <golgote@mamasam.com> | |
| * All rights reserved. | |
| * | |
| * Redistribution and use in source and binary forms, with or without | |
| * modification, are permitted provided that the following conditions | |
| * are met: | |
| * | |
| * * Redistributions of source code must retain the above copyright | |
| * notice, this list of conditions and the following disclaimer. | |
| * * Redistributions in binary form must reproduce the above copyright | |
| * notice, this list of conditions and the following disclaimer in the | |
| * documentation and/or other materials provided with the distribution. | |
| * * The names of the authors may not be used to endorse or promote products | |
| * derived from this software without specific prior written permission. | |
| * | |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS | |
| * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, | |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR | |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
| * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
| * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| * | |
| * @category HTML | |
| * @package HTML_QuickForm2 | |
| * @author Alexey Borzov <avb@php.net> | |
| * @author Bertrand Mansion <golgote@mamasam.com> | |
| * @license http://opensource.org/licenses/bsd-license.php New BSD License | |
| * @version SVN: $Id: Controller.php 295963 2010-03-08 14:33:43Z avb $ | |
| * @link http://pear.php.net/package/HTML_QuickForm2 | |
| */ | |
| /** The class representing a page of a multipage form */ | |
| // require_once 'HTML/QuickForm2/Controller/Page.php'; | |
| /** Object wrapping around session variable used to store controller data */ | |
| // require_once 'HTML/QuickForm2/Controller/SessionContainer.php'; | |
| /** Class presenting the values stored in session by Controller as submitted ones */ | |
| // require_once 'HTML/QuickForm2/DataSource/Session.php'; | |
| /** | |
| * Class implementing the Page Controller pattern for multipage forms | |
| * | |
| * This class keeps track of pages and (default) action handlers for the form, | |
| * it manages $_SESSION container for the form values, allows setting | |
| * DataSources for the form as a whole and getting its value. | |
| * | |
| * @category HTML | |
| * @package HTML_QuickForm2 | |
| * @author Alexey Borzov <avb@php.net> | |
| * @author Bertrand Mansion <golgote@mamasam.com> | |
| * @version Release: @package_version@ | |
| */ | |
| class HTML_QuickForm2_Controller implements IteratorAggregate | |
| { | |
| /** | |
| * Key in $_REQUEST array that contains the ID of the Controller | |
| */ | |
| const KEY_ID = '_qfc_id'; | |
| /** | |
| * Key in $_SESSION array that contains the Controller data (needs ID substituted via sprintf()) | |
| */ | |
| const KEY_CONTAINER = '_%s_container'; | |
| /** | |
| * Whether the form is a wizard | |
| * @var boolean | |
| */ | |
| protected $wizard = true; | |
| /** | |
| * Whether Controller ID should be sent in GET and POST parameters | |
| * @var boolean | |
| */ | |
| protected $propagate = true; | |
| /** | |
| * Controller ID | |
| * @var string | |
| */ | |
| protected $id = null; | |
| /** | |
| * Contains the pages (instances of HTML_QuickForm2_Controller_Page) of the multipage form | |
| * @var array | |
| */ | |
| protected $pages = array(); | |
| /** | |
| * Contains the mapping of action names to handlers (objects implementing HTML_QuickForm2_Controller_Action) | |
| * @var array | |
| */ | |
| protected $handlers = array(); | |
| /** | |
| * The action extracted from HTTP request: array('page', 'action') | |
| * @var array | |
| */ | |
| protected $actionName = null; | |
| /** | |
| * A wrapper around session variable used to store form data | |
| * @var HTML_QuickForm2_Controller_SessionContainer | |
| */ | |
| protected $sessionContainer = null; | |
| /** | |
| * Finds a controller name in $_REQUEST | |
| * | |
| * @return string|null Returns nulle if either a KEY_ID is not present | |
| * in $_REQUEST or KEY_CONTAINER is not present in | |
| * $_SESSION | |
| */ | |
| public static function findControllerID() | |
| { | |
| if (empty($_REQUEST[self::KEY_ID]) | |
| || empty($_SESSION[sprintf(self::KEY_CONTAINER, $_REQUEST[self::KEY_ID])]) | |
| ) { | |
| return null; | |
| } else { | |
| return $_REQUEST[self::KEY_ID]; | |
| } | |
| } | |
| /** | |
| * Class constructor | |
| * | |
| * Sets the form ID, whether to send this ID in POST and GET parameters, | |
| * wizard / non-wizard behaviour. | |
| * | |
| * Different forms should be given different IDs, as they are used to store | |
| * values in session. If $id is empty, the controller will try to find it | |
| * in $_REQUEST, throwing the exception if this fails. | |
| * | |
| * Wizard forms only allow going to the next page if all the previous ones | |
| * are valid. | |
| * | |
| * @param string Form ID | |
| * @param boolean Whether the form is a wizard | |
| * @param boolean Whether form's ID should be sent with GET and POST parameters | |
| * @throws HTML_QuickForm2_NotFoundException if ID is not given and cannot | |
| * be found in $_REQUEST, or session container is empty | |
| */ | |
| public function __construct($id = null, $wizard = true, $propagateId = false) | |
| { | |
| if (empty($id)) { | |
| $propagateId = true; | |
| $id = self::findControllerID(); | |
| } | |
| if (empty($id)) { | |
| throw new HTML_QuickForm2_NotFoundException( | |
| 'Controller ID not available in $_REQUEST or session ' . | |
| 'container is empty, please provide ID to constructor' | |
| ); | |
| } | |
| $this->id = $id; | |
| $this->wizard = (bool)$wizard; | |
| $this->propagate = (bool)$propagateId; | |
| } | |
| /** | |
| * Returns whether the form is a wizard | |
| * | |
| * @return boolean | |
| */ | |
| public function isWizard() | |
| { | |
| return $this->wizard; | |
| } | |
| /** | |
| * Returns the form ID | |
| * | |
| * @return string | |
| */ | |
| public function getId() | |
| { | |
| return $this->id; | |
| } | |
| /** | |
| * Returns whether to send form id with GET and POST parameters | |
| * | |
| * @return boolean | |
| */ | |
| public function propagateId() | |
| { | |
| return $this->propagate; | |
| } | |
| /** | |
| * Returns the session container with the controller data | |
| * | |
| * @return HTML_QuickForm2_Controller_SessionContainer | |
| */ | |
| public function getSessionContainer() | |
| { | |
| if (empty($this->sessionContainer)) { | |
| $this->sessionContainer = new HTML_QuickForm2_Controller_SessionContainer($this); | |
| } | |
| return $this->sessionContainer; | |
| } | |
| /** | |
| * Removes the session variable containing the controller data | |
| */ | |
| public function destroySessionContainer() | |
| { | |
| unset($_SESSION[sprintf(self::KEY_CONTAINER, $this->id)]); | |
| $this->sessionContainer = null; | |
| } | |
| /** | |
| * Extracts the name of the page and the action to perform with it from HTTP request data | |
| * | |
| * @return array first element is page name, second is action name | |
| */ | |
| public function getActionName() | |
| { | |
| if (is_array($this->actionName)) { | |
| return $this->actionName; | |
| } | |
| if (empty($this->pages)) { | |
| throw new HTML_QuickForm2_NotFoundException('No pages added to the form'); | |
| } | |
| $names = array_map('preg_quote', array_keys($this->pages)); | |
| $regex = '/^_qf_(' . implode('|', $names) . ')_(.+?)(_x)?$/'; | |
| foreach (array_keys($_REQUEST) as $key) { | |
| if (preg_match($regex, $key, $matches)) { | |
| $this->actionName = array($matches[1], $matches[2]); | |
| break; | |
| } | |
| } | |
| if (!is_array($this->actionName)) { | |
| reset($this->pages); | |
| $this->actionName = array(key($this->pages), 'display'); | |
| } | |
| return $this->actionName; | |
| } | |
| /** | |
| * Processes the request | |
| * | |
| * This finds the page, the action to perform with it and passes the action | |
| * to the page's handle() method. | |
| * | |
| * @throws HTML_QuickForm2_Exception | |
| */ | |
| public function run() | |
| { | |
| list($page, $action) = $this->getActionName(); | |
| return $this->pages[$page]->handle($action); | |
| } | |
| /** | |
| * Adds a handler for a specific action | |
| * | |
| * @param string action name | |
| * @param HTML_QuickForm2_Controller_Action the handler for the action | |
| */ | |
| public function addHandler($actionName, HTML_QuickForm2_Controller_Action $action) | |
| { | |
| $this->handlers[$actionName] = $action; | |
| } | |
| /** | |
| * Handles an action | |
| * | |
| * This will be called if the page itself does not have a handler for a | |
| * specific action. The method also loads and uses default handlers for | |
| * common actions, if specific ones were not added. | |
| * | |
| * @param HTML_QuickForm2_Controller_Page form page | |
| * @param string action name | |
| * @throws HTML_QuickForm2_NotFoundException if handler for an action is missing | |
| */ | |
| public function handle(HTML_QuickForm2_Controller_Page $page, $actionName) | |
| { | |
| if (!isset($this->handlers[$actionName]) | |
| && in_array($actionName, array('next', 'back', 'submit', 'display', 'jump')) | |
| ) { | |
| $className = 'HTML_QuickForm2_Controller_Action_' . ucfirst($actionName); | |
| if (!class_exists($className)) { | |
| HTML_QuickForm2_Loader::loadClass($className); | |
| } | |
| $this->addHandler($actionName, new $className()); | |
| } | |
| if (isset($this->handlers[$actionName])) { | |
| return $this->handlers[$actionName]->perform($page, $actionName); | |
| } else { | |
| throw new HTML_QuickForm2_NotFoundException( | |
| "Unhandled action '{$actionName}' for page '{$page->getForm()->getId()}'" | |
| ); | |
| } | |
| } | |
| /** | |
| * Adds a new page to the form | |
| * | |
| * @param HTML_QuickForm2_Controller_Page | |
| */ | |
| public function addPage(HTML_QuickForm2_Controller_Page $page) | |
| { | |
| $pageId = $page->getForm()->getId(); | |
| if (!empty($this->pages[$pageId])) { | |
| throw new HTML_QuickForm2_InvalidArgumentException( | |
| "Duplicate page ID '{$pageId}'" | |
| ); | |
| } | |
| $page->setController($this); | |
| $this->pages[$pageId] = $page; | |
| } | |
| /** | |
| * Returns a page | |
| * | |
| * @param string Page ID | |
| * @return HTML_QuickForm2_Controller_Page | |
| * @throws HTML_QuickForm2_NotFoundException if there is no page with | |
| * the given ID | |
| */ | |
| public function getPage($pageId) | |
| { | |
| if (!empty($this->pages[$pageId])) { | |
| return $this->pages[$pageId]; | |
| } else { | |
| throw new HTML_QuickForm2_NotFoundException( | |
| "Unknown page '{$pageId}'" | |
| ); | |
| } | |
| } | |
| /** | |
| * Returns the page preceding the given one | |
| * | |
| * @param HTML_QuickForm2_Controller_Page | |
| * @return HTML_QuickForm2_Controller_Page|null | |
| */ | |
| public function previousPage(HTML_QuickForm2_Controller_Page $reference) | |
| { | |
| $previous = null; | |
| foreach ($this->pages as $page) { | |
| if ($page === $reference) { | |
| return $previous; | |
| } | |
| $previous = $page; | |
| } | |
| return null; | |
| } | |
| /** | |
| * Returns the page following the given one | |
| * | |
| * @param HTML_QuickForm2_Controller_Page | |
| * @return HTML_QuickForm2_Controller_Page|null | |
| */ | |
| public function nextPage(HTML_QuickForm2_Controller_Page $reference) | |
| { | |
| $previous = null; | |
| foreach ($this->pages as $page) { | |
| if ($previous === $reference) { | |
| return $page; | |
| } | |
| $previous = $page; | |
| } | |
| return null; | |
| } | |
| /** | |
| * Checks whether the pages of the controller are valid | |
| * | |
| * @param HTML_QuickForm2_Controller_Page If given, check only the pages | |
| * before (not including) that page | |
| * @return bool | |
| */ | |
| public function isValid(?HTML_QuickForm2_Controller_Page $reference = null) | |
| { | |
| $container = $this->getSessionContainer(); | |
| foreach ($this->pages as $id => $page) { | |
| if ($reference === $page) { | |
| return true; | |
| } | |
| if (!$container->getValidationStatus($id)) { | |
| // We should handle the possible situation when the user has never | |
| // seen a page of a non-modal multipage form | |
| if (!$this->isWizard() | |
| && null === $container->getValidationStatus($id) | |
| ) { | |
| // Empty Session datasource makes the form look submitted | |
| $page->getForm()->setDatasources(array_merge( | |
| $container->getDatasources(), | |
| array(new HTML_QuickForm2_DataSource_Session(array())) | |
| )); | |
| // This will store the "submitted" values in session and | |
| // return validation status | |
| if ($page->storeValues()) { | |
| continue; | |
| } | |
| } | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /** | |
| * Returns the first page that failed validation | |
| * | |
| * @return HTML_QuickForm2_Controller_Page|null | |
| */ | |
| public function getFirstInvalidPage() | |
| { | |
| foreach ($this->pages as $id => $page) { | |
| if (!$this->getSessionContainer()->getValidationStatus($id)) { | |
| return $page; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Adds a new data source to the Controller | |
| * | |
| * Note that Controller data sources are stored in session, so your data source | |
| * implementation should properly handle its (un)serialization. | |
| * | |
| * @param HTML_QuickForm2_DataSource Data source | |
| */ | |
| public function addDataSource(HTML_QuickForm2_DataSource $datasource) | |
| { | |
| $this->getSessionContainer()->storeDatasources( | |
| array_merge($this->getSessionContainer()->getDatasources(), | |
| array($datasource)) | |
| ); | |
| } | |
| /** | |
| * Returns the form's values | |
| * | |
| * @return array | |
| */ | |
| public function getValue() | |
| { | |
| $values = array(); | |
| foreach (array_keys($this->pages) as $id) { | |
| $pageValues = $this->getSessionContainer()->getValues($id); | |
| // skip elements representing actions | |
| foreach ($pageValues as $key => $value) { | |
| if (0 !== strpos($key, '_qf')) { | |
| if (isset($values[$key]) && is_array($value)) { | |
| $values[$key] = self::arrayMerge($values[$key], $value); | |
| } else { | |
| $values[$key] = $value; | |
| } | |
| } | |
| } | |
| } | |
| return $values; | |
| } | |
| /** | |
| * Merges two arrays | |
| * | |
| * Merges two arrays like the PHP function array_merge_recursive does, | |
| * the difference being that existing integer keys will not be renumbered. | |
| * | |
| * @param array | |
| * @param array | |
| * @return array resulting array | |
| */ | |
| protected static function arrayMerge($a, $b) | |
| { | |
| foreach ($b as $k => $v) { | |
| if (!is_array($v) || isset($a[$k]) && !is_array($a[$k])) { | |
| $a[$k] = $v; | |
| } else { | |
| $a[$k] = self::arrayMerge(isset($a[$k])? $a[$k]: array(), $v); | |
| } | |
| } | |
| return $a; | |
| } | |
| /** | |
| * Returns an Iterator for the form's pages | |
| * | |
| * @return ArrayIterator | |
| */ | |
| public function getIterator(): ArrayIterator | |
| { | |
| return new ArrayIterator($this->pages); | |
| } | |
| } | |