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(); } }