summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Web/Wizard.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Web/Wizard.php')
-rw-r--r--library/Icinga/Web/Wizard.php720
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();
+ }
+}