diff options
Diffstat (limited to 'library/Icinga/Web/Wizard.php')
-rw-r--r-- | library/Icinga/Web/Wizard.php | 720 |
1 files changed, 720 insertions, 0 deletions
diff --git a/library/Icinga/Web/Wizard.php b/library/Icinga/Web/Wizard.php new file mode 100644 index 0000000..9a1b8b6 --- /dev/null +++ b/library/Icinga/Web/Wizard.php @@ -0,0 +1,720 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Web; + +use Icinga\Forms\ConfigForm; +use Icinga\Module\Setup\Forms\ModulePage; +use LogicException; +use InvalidArgumentException; +use Icinga\Web\Session\SessionNamespace; +use Icinga\Web\Form\Decorator\ElementDoubler; + +/** + * Container and controller for form based wizards + */ +class Wizard +{ + /** + * An integer describing the wizard's forward direction + */ + const FORWARD = 0; + + /** + * An integer describing the wizard's backward direction + */ + const BACKWARD = 1; + + /** + * An integer describing that the wizard does not change its position + */ + const NO_CHANGE = 2; + + /** + * The name of the button to advance the wizard's position + */ + const BTN_NEXT = 'btn_next'; + + /** + * The name of the button to rewind the wizard's position + */ + const BTN_PREV = 'btn_prev'; + + /** + * The name and id of the element for showing the user an activity indicator when advancing the wizard + */ + const PROGRESS_ELEMENT = 'wizard_progress'; + + /** + * This wizard's parent + * + * @var Wizard + */ + protected $parent; + + /** + * The name of the wizard's current page + * + * @var string + */ + protected $currentPage; + + /** + * The pages being part of this wizard + * + * @var array + */ + protected $pages = array(); + + /** + * Initialize a new wizard + */ + public function __construct() + { + $this->init(); + } + + /** + * Run additional initialization routines + * + * Should be implemented by subclasses to add pages to the wizard. + */ + protected function init() + { + } + + /** + * Return this wizard's parent or null in case it has none + * + * @return Wizard|null + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set this wizard's parent + * + * @param Wizard $wizard The parent wizard + * + * @return $this + */ + public function setParent(Wizard $wizard) + { + $this->parent = $wizard; + return $this; + } + + /** + * Return the pages being part of this wizard + * + * In case this is a nested wizard a flattened array of all contained pages is returned. + * + * @return array + */ + public function getPages() + { + $pages = array(); + foreach ($this->pages as $page) { + if ($page instanceof self) { + $pages = array_merge($pages, $page->getPages()); + } else { + $pages[] = $page; + } + } + + return $pages; + } + + /** + * Return the page with the given name + * + * Note that it's also possible to retrieve a nested wizard's page by using this method. + * + * @param string $name The name of the page to return + * + * @return ModulePage|Form|null The page or null in case there is no page with the given name + */ + public function getPage($name) + { + foreach ($this->getPages() as $page) { + if ($name === $page->getName()) { + return $page; + } + } + } + + /** + * Add a new page or wizard to this wizard + * + * @param Form|Wizard $page The page or wizard to add to the wizard + * + * @return $this + */ + public function addPage($page) + { + if (! $page instanceof Form && ! $page instanceof self) { + throw new InvalidArgumentException( + 'The $page argument must be an instance of Icinga\Web\Form ' + . 'or Icinga\Web\Wizard but is of type: ' . get_class($page) + ); + } elseif ($page instanceof self) { + $page->setParent($this); + } + + $this->pages[] = $page; + return $this; + } + + /** + * Add multiple pages or wizards to this wizard + * + * @param array $pages The pages or wizards to add to the wizard + * + * @return $this + */ + public function addPages(array $pages) + { + foreach ($pages as $page) { + $this->addPage($page); + } + + return $this; + } + + /** + * Assert that this wizard has any pages + * + * @throws LogicException In case this wizard has no pages + */ + protected function assertHasPages() + { + $pages = $this->getPages(); + if (count($pages) < 2) { + throw new LogicException("Although Chuck Norris can advance a wizard with less than two pages, you can't."); + } + } + + /** + * Return the current page of this wizard + * + * @return Form + * + * @throws LogicException In case the name of the current page currently being set is invalid + */ + public function getCurrentPage() + { + if ($this->parent) { + return $this->parent->getCurrentPage(); + } + + if ($this->currentPage === null) { + $this->assertHasPages(); + $pages = $this->getPages(); + $this->currentPage = $this->getSession()->get('current_page', $pages[0]->getName()); + } + + if (($page = $this->getPage($this->currentPage)) === null) { + throw new LogicException(sprintf('No page found with name "%s"', $this->currentPage)); + } + + return $page; + } + + /** + * Set the current page of this wizard + * + * @param Form $page The page to set as current page + * + * @return $this + */ + public function setCurrentPage(Form $page) + { + $this->currentPage = $page->getName(); + $this->getSession()->set('current_page', $this->currentPage); + return $this; + } + + /** + * Setup the given page that is either going to be displayed or validated + * + * Implement this method in a subclass to populate default values and/or other data required to process the form. + * + * @param Form $page The page to setup + * @param Request $request The current request + */ + public function setupPage(Form $page, Request $request) + { + } + + /** + * Process the given request using this wizard + * + * Validate the request data using the current page, update the wizard's + * position and redirect to the page's redirect url upon success. + * + * @param Request $request The request to be processed + * + * @return Request The request supposed to be processed + */ + public function handleRequest(Request $request = null) + { + $page = $this->getCurrentPage(); + + if (($wizard = $this->findWizard($page)) !== null) { + return $wizard->handleRequest($request); + } + + if ($request === null) { + $request = $page->getRequest(); + } + + $this->setupPage($page, $request); + $requestData = $this->getRequestData($page, $request); + if ($page->wasSent($requestData)) { + if (($requestedPage = $this->getRequestedPage($requestData)) !== null) { + $isValid = false; + $direction = $this->getDirection($request); + if ($direction === static::FORWARD && $page->isValid($requestData)) { + $isValid = true; + if ($this->isLastPage($page)) { + $this->setIsFinished(); + } + } elseif ($direction === static::BACKWARD) { + $page->populate($requestData); + $isValid = true; + } + + if ($isValid) { + $pageData = & $this->getPageData(); + $pageData[$page->getName()] = ConfigForm::transformEmptyValuesToNull($page->getValues()); + $this->setCurrentPage($this->getNewPage($requestedPage, $page)); + $page->getResponse()->redirectAndExit($page->getRedirectUrl()); + } + } elseif ($page->getValidatePartial()) { + $page->isValidPartial($requestData); + } else { + $page->populate($requestData); + } + } elseif (($pageData = $this->getPageData($page->getName())) !== null) { + $page->populate($pageData); + } + + return $request; + } + + /** + * Return the wizard for the given page or null if its not part of a wizard + * + * @param Form $page The page to return its wizard for + * + * @return Wizard|null + */ + protected function findWizard(Form $page) + { + foreach ($this->getWizards() as $wizard) { + if ($wizard->getPage($page->getName()) === $page) { + return $wizard; + } + } + } + + /** + * Return this wizard's child wizards + * + * @return array + */ + protected function getWizards() + { + $wizards = array(); + foreach ($this->pages as $pageOrWizard) { + if ($pageOrWizard instanceof self) { + $wizards[] = $pageOrWizard; + } + } + + return $wizards; + } + + /** + * Return the request data based on given form's request method + * + * @param Form $page The page to fetch the data for + * @param Request $request The request to fetch the data from + * + * @return array + */ + protected function getRequestData(Form $page, Request $request) + { + if (strtolower($request->getMethod()) === $page->getMethod()) { + return $request->{'get' . ($request->isPost() ? 'Post' : 'Query')}(); + } + + return array(); + } + + /** + * Return the name of the requested page + * + * @param array $requestData The request's data + * + * @return null|string The name of the requested page or null in case no page has been requested + */ + protected function getRequestedPage(array $requestData) + { + if ($this->parent) { + return $this->parent->getRequestedPage($requestData); + } + + if (isset($requestData[static::BTN_NEXT])) { + return $requestData[static::BTN_NEXT]; + } elseif (isset($requestData[static::BTN_PREV])) { + return $requestData[static::BTN_PREV]; + } + } + + /** + * Return the direction of this wizard using the given request + * + * @param Request $request The request to use + * + * @return int The direction @see Wizard::FORWARD @see Wizard::BACKWARD @see Wizard::NO_CHANGE + */ + protected function getDirection(Request $request = null) + { + if ($this->parent) { + return $this->parent->getDirection($request); + } + + $currentPage = $this->getCurrentPage(); + + if ($request === null) { + $request = $currentPage->getRequest(); + } + + $requestData = $this->getRequestData($currentPage, $request); + if (isset($requestData[static::BTN_NEXT])) { + return static::FORWARD; + } elseif (isset($requestData[static::BTN_PREV])) { + return static::BACKWARD; + } + + return static::NO_CHANGE; + } + + /** + * Return the new page to set as current page + * + * Permission is checked by verifying that the requested page or its previous page has page data available. + * The requested page is automatically permitted without any checks if the origin page is its previous + * page or one that occurs later in order. + * + * @param string $requestedPage The name of the requested page + * @param Form $originPage The origin page + * + * @return Form The new page + * + * @throws InvalidArgumentException In case the requested page does not exist or is not permitted yet + */ + protected function getNewPage($requestedPage, Form $originPage) + { + if ($this->parent) { + return $this->parent->getNewPage($requestedPage, $originPage); + } + + if (($page = $this->getPage($requestedPage)) !== null) { + $permitted = true; + + $pages = $this->getPages(); + if (! $this->hasPageData($requestedPage) && ($index = array_search($page, $pages, true)) > 0) { + $previousPage = $pages[$index - 1]; + if ($originPage === null || ($previousPage->getName() !== $originPage->getName() + && array_search($originPage, $pages, true) < $index)) { + $permitted = $this->hasPageData($previousPage->getName()); + } + } + + if ($permitted) { + return $page; + } + } + + throw new InvalidArgumentException( + sprintf('"%s" is either an unknown page or one you are not permitted to view', $requestedPage) + ); + } + + /** + * Return the next or previous page based on the given one + * + * @param Form $page The page to skip + * + * @return Form + */ + protected function skipPage(Form $page) + { + if ($this->parent) { + return $this->parent->skipPage($page); + } + + if ($this->hasPageData($page->getName())) { + $pageData = & $this->getPageData(); + unset($pageData[$page->getName()]); + } + + $pages = $this->getPages(); + if ($this->getDirection() === static::FORWARD) { + $nextPage = $pages[array_search($page, $pages, true) + 1]; + $newPage = $this->getNewPage($nextPage->getName(), $page); + } else { // $this->getDirection() === static::BACKWARD + $previousPage = $pages[array_search($page, $pages, true) - 1]; + $newPage = $this->getNewPage($previousPage->getName(), $page); + } + + return $newPage; + } + + /** + * Return whether the given page is this wizard's last page + * + * @param Form $page The page to check + * + * @return bool + */ + protected function isLastPage(Form $page) + { + if ($this->parent) { + return $this->parent->isLastPage($page); + } + + $pages = $this->getPages(); + return $page->getName() === end($pages)->getName(); + } + + /** + * Return whether all of this wizard's pages were visited by the user + * + * The base implementation just verifies that the very last page has page data available. + * + * @return bool + */ + public function isComplete() + { + $pages = $this->getPages(); + return $this->hasPageData($pages[count($pages) - 1]->getName()); + } + + /** + * Set whether this wizard has been completed + * + * @param bool $state Whether this wizard has been completed + * + * @return $this + */ + public function setIsFinished($state = true) + { + $this->getSession()->set('isFinished', $state); + return $this; + } + + /** + * Return whether this wizard has been completed + * + * @return bool + */ + public function isFinished() + { + return $this->getSession()->get('isFinished', false); + } + + /** + * Return the overall page data or one for a particular page + * + * Note that this method returns by reference so in order to update the + * returned array set this method's return value also by reference. + * + * @param string $pageName The page for which to return the data + * + * @return array + */ + public function & getPageData($pageName = null) + { + $session = $this->getSession(); + + if (false === isset($session->page_data)) { + $session->page_data = array(); + } + + $pageData = & $session->getByRef('page_data'); + if ($pageName !== null) { + $data = null; + if (isset($pageData[$pageName])) { + $data = & $pageData[$pageName]; + } + + return $data; + } + + return $pageData; + } + + /** + * Return whether there is any data for the given page + * + * @param string $pageName The name of the page to check + * + * @return bool + */ + public function hasPageData($pageName) + { + return $this->getPageData($pageName) !== null; + } + + /** + * Return a session to be used by this wizard + * + * @return SessionNamespace + */ + public function getSession() + { + if ($this->parent) { + return $this->parent->getSession(); + } + + return Session::getSession()->getNamespace(get_class($this)); + } + + /** + * Clear the session being used by this wizard + */ + public function clearSession() + { + $this->getSession()->clear(); + } + + /** + * Add buttons to the given page based on its position in the page-chain + * + * @param Form $page The page to add the buttons to + */ + protected function addButtons(Form $page) + { + $pages = $this->getPages(); + $index = array_search($page, $pages, true); + if ($index === 0) { + $page->addElement( + 'button', + static::BTN_NEXT, + array( + 'class' => 'control-button btn-primary', + 'type' => 'submit', + 'value' => $pages[1]->getName(), + 'label' => t('Next'), + 'decorators' => array('ViewHelper', 'Spinner') + ) + ); + } elseif ($index < count($pages) - 1) { + $page->addElement( + 'button', + static::BTN_PREV, + array( + 'class' => 'control-button', + 'type' => 'submit', + 'value' => $pages[$index - 1]->getName(), + 'label' => t('Back'), + 'decorators' => array('ViewHelper'), + 'formnovalidate' => 'formnovalidate' + ) + ); + $page->addElement( + 'button', + static::BTN_NEXT, + array( + 'class' => 'control-button btn-primary', + 'type' => 'submit', + 'value' => $pages[$index + 1]->getName(), + 'label' => t('Next'), + 'decorators' => array('ViewHelper') + ) + ); + } else { + $page->addElement( + 'button', + static::BTN_PREV, + array( + 'class' => 'control-button', + 'type' => 'submit', + 'value' => $pages[$index - 1]->getName(), + 'label' => t('Back'), + 'decorators' => array('ViewHelper'), + 'formnovalidate' => 'formnovalidate' + ) + ); + $page->addElement( + 'button', + static::BTN_NEXT, + array( + 'class' => 'control-button btn-primary', + 'type' => 'submit', + 'value' => $page->getName(), + 'label' => t('Finish'), + 'decorators' => array('ViewHelper') + ) + ); + } + + $page->setAttrib('data-progress-element', static::PROGRESS_ELEMENT); + $page->addElement( + 'note', + static::PROGRESS_ELEMENT, + array( + 'order' => 99, // Ensures that it's shown on the right even if a sub-class adds another button + 'decorators' => array( + 'ViewHelper', + array('Spinner', array('id' => static::PROGRESS_ELEMENT)) + ) + ) + ); + + $page->addDisplayGroup( + array(static::BTN_PREV, static::BTN_NEXT, static::PROGRESS_ELEMENT), + 'buttons', + array( + 'decorators' => array( + 'FormElements', + new ElementDoubler(array( + 'double' => static::BTN_NEXT, + 'condition' => static::BTN_PREV, + 'placement' => ElementDoubler::PREPEND, + 'attributes' => array('tabindex' => -1, 'class' => 'double') + )), + array('HtmlTag', array('tag' => 'div', 'class' => 'buttons')) + ) + ) + ); + } + + /** + * Return the current page of this wizard with appropriate buttons being added + * + * @return Form + */ + public function getForm() + { + $form = $this->getCurrentPage(); + $form->create(); // Make sure that buttons are displayed at the very bottom + $this->addButtons($form); + return $form; + } + + /** + * Return the current page of this wizard rendered as HTML + * + * @return string + */ + public function __toString() + { + return (string) $this->getForm(); + } +} |