From e1230eb8ae15774ab892c19bfaa1b255b9713cc8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 8 May 2014 09:42:29 +0200 Subject: [PATCH 01/11] Add style and view scripts for first prototype refs #6136 --- application/controllers/InstallController.php | 45 +++++++++ application/views/scripts/install/index.phtml | 34 +++++++ .../scripts/install/index/installog.phtml | 1 + library/Icinga/Web/StyleSheet.php | 1 + public/css/icinga/install.less | 96 +++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 application/controllers/InstallController.php create mode 100644 application/views/scripts/install/index.phtml create mode 100644 application/views/scripts/install/index/installog.phtml create mode 100644 public/css/icinga/install.less diff --git a/application/controllers/InstallController.php b/application/controllers/InstallController.php new file mode 100644 index 000000000..dee397fd0 --- /dev/null +++ b/application/controllers/InstallController.php @@ -0,0 +1,45 @@ +register(); + +use \Mockery; +use Icinga\Web\Controller\ActionController; + +class InstallController extends ActionController +{ + /** + * Whether the controller requires the user to be authenticated + * + * The install wizard has its own authentication mechanism. + * + * @var bool + */ + protected $requiresAuthentication = false; + + public function indexAction() + { + $finished = false; + $this->view->installer = 'some log info, as html'; + $this->view->wizard = Mockery::mock(); + $this->view->wizard->shouldReceive('isFinished')->andReturn($finished) + ->shouldReceive('getTitle')->andReturn('Web') + ->shouldReceive('getPages')->andReturnUsing(function () { + $a = array(Mockery::mock(array('getTitle' => 'childTest', 'getChildPages' => array( + Mockery::mock(array('getTitle' => 'child1')), + Mockery::mock(array('getTitle' => 'child2')) + ), 'isActiveChild' => false))); for ($i=0;$i<10;$i++) { $a[] = Mockery::mock(array('getTitle' => 'title'.$i, 'getChildPages' => array())); } return $a; + }) + ->shouldReceive('isActivePage')->andReturnUsing(function ($p) { return $p->getTitle() == 'title4'; }) + ->shouldReceive('isCompletedPage')->andReturnUsing(function ($p) { return $p->getTitle() < 'title4'; }) + ->shouldReceive('getActivePage')->andReturnUsing(function () { + return Mockery::mock(array('getTitle' => 'title4', '__toString' => 'teh form elements')); + }); + } +} + +// @codeCoverageIgnoreEnd diff --git a/application/views/scripts/install/index.phtml b/application/views/scripts/install/index.phtml new file mode 100644 index 000000000..13a9e587a --- /dev/null +++ b/application/views/scripts/install/index.phtml @@ -0,0 +1,34 @@ +
+
+ +

getTitle(); ?>

+
+ +
+isFinished()): ?> + partial('install/index/installog.phtml', array('installer' => $installer)); ?> + + getActivePage(); ?> + +
+
diff --git a/application/views/scripts/install/index/installog.phtml b/application/views/scripts/install/index/installog.phtml new file mode 100644 index 000000000..d4b9f7375 --- /dev/null +++ b/application/views/scripts/install/index/installog.phtml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php index b247f32e9..e62a861ca 100644 --- a/library/Icinga/Web/StyleSheet.php +++ b/library/Icinga/Web/StyleSheet.php @@ -22,6 +22,7 @@ class StyleSheet 'css/icinga/monitoring-colors.less', 'css/icinga/selection-toolbar.less', 'css/icinga/login.less', + 'css/icinga/install.less', ); public static function compileForPdf() diff --git a/public/css/icinga/install.less b/public/css/icinga/install.less new file mode 100644 index 000000000..421d7fdbd --- /dev/null +++ b/public/css/icinga/install.less @@ -0,0 +1,96 @@ +div.wizard { + div.header { + padding: 0.6em 0 0 1em; + height: 3em; + position: fixed; + top: 0; + left: 0; + right: 0; + + color: #eee; + background-color: #555; + background-image: linear-gradient(top, #777, #555); + background-image: -o-linear-gradient(top, #777, #555); + background-image: -ms-linear-gradient(top, #777, #555); + background-image: -webkit-linear-gradient(top, #777, #555); + + h1 { + margin: 0 3.5em; + display: inline-block; + + font-size: 2em; + } + } + + div.sidebar { + width: 13em; + position: fixed; + top: 3.6em; + left: 0; + bottom: 0; + + background-color: #999; + box-shadow: inset -0.5em 0 0.5em -0.5em #555; + -moz-box-shadow: inset -0.5em 0 0.5em -0.5em #555; + -webkit-box-shadow: inset -0.5em 0 0.5em -0.5em #555; + + & > ul { + margin: 0; + padding: 0; + list-style: none; + + & > li { + color: #f5f5f5; + font-size: 1.1em; + padding: 0.5em; + margin-left: 0.5em; + text-shadow: #555 -1px 1px 0px; + border-bottom: 1px solid #888; + + &.active { + color: black; + margin-left: 0; + padding-left: 1em; + text-shadow: none; + background-color: white; + } + + &.complete { + color: green; + } + + &.pending { + color: red; + } + + &.install { + border-bottom: 0; + } + + ul { + margin: 0; + padding: 0; + list-style: none; + + li.child { + font-size: 0.9em; + padding: 0.4em 0.8em 0; + + &.active { + font-weight: bold; + } + } + } + } + } + } + + div.panel { + padding: 1em; + position: fixed; + top: 3.6em; + left: 13em; + right: 0; + bottom: 0; + } +} From 4cf5fe6fddd155c4bb5fd7ed9014c0f953f2b658 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 13 May 2014 14:24:51 +0200 Subject: [PATCH 02/11] Add multistep form refs #6136 --- application/controllers/InstallController.php | 98 ++++- application/views/scripts/install/index.phtml | 18 +- library/Icinga/Web/Wizard/Page.php | 54 +++ library/Icinga/Web/Wizard/Wizard.php | 379 ++++++++++++++++++ 4 files changed, 522 insertions(+), 27 deletions(-) create mode 100644 library/Icinga/Web/Wizard/Page.php create mode 100644 library/Icinga/Web/Wizard/Wizard.php diff --git a/application/controllers/InstallController.php b/application/controllers/InstallController.php index dee397fd0..f2b0398e2 100644 --- a/application/controllers/InstallController.php +++ b/application/controllers/InstallController.php @@ -3,11 +3,9 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -require_once 'Mockery/Loader.php'; -$mockeryLoader = new \Mockery\Loader; -$mockeryLoader->register(); - -use \Mockery; +use Icinga\Web\Session; +use Icinga\Web\Wizard\Page; +use Icinga\Web\Wizard\Wizard; use Icinga\Web\Controller\ActionController; class InstallController extends ActionController @@ -21,24 +19,82 @@ class InstallController extends ActionController */ protected $requiresAuthentication = false; + /** + * Show the wizard and run the installation once its finished + */ public function indexAction() { - $finished = false; - $this->view->installer = 'some log info, as html'; - $this->view->wizard = Mockery::mock(); - $this->view->wizard->shouldReceive('isFinished')->andReturn($finished) - ->shouldReceive('getTitle')->andReturn('Web') - ->shouldReceive('getPages')->andReturnUsing(function () { - $a = array(Mockery::mock(array('getTitle' => 'childTest', 'getChildPages' => array( - Mockery::mock(array('getTitle' => 'child1')), - Mockery::mock(array('getTitle' => 'child2')) - ), 'isActiveChild' => false))); for ($i=0;$i<10;$i++) { $a[] = Mockery::mock(array('getTitle' => 'title'.$i, 'getChildPages' => array())); } return $a; - }) - ->shouldReceive('isActivePage')->andReturnUsing(function ($p) { return $p->getTitle() == 'title4'; }) - ->shouldReceive('isCompletedPage')->andReturnUsing(function ($p) { return $p->getTitle() < 'title4'; }) - ->shouldReceive('getActivePage')->andReturnUsing(function () { - return Mockery::mock(array('getTitle' => 'title4', '__toString' => 'teh form elements')); - }); + $wizard = $this->createWizard(); + $wizard->navigate(); // Needs to be called before isSubmittedAndValid() as this creates the form + + if ($wizard->isSubmittedAndValid() && $wizard->isFinished()) { + // TODO: Run the installer (Who creates an installer? How do we handle module installers?) + $this->dropConfiguration(); // TODO: Should only be done if the installation has been successfully completed + $this->view->installer = ''; + } else { + $this->storeConfiguration($wizard->getConfig()); + } + + $this->view->wizard = $wizard; + } + + /** + * Create the wizard and register all pages + * + * @return Wizard + */ + protected function createWizard() + { + $wizard = new Wizard(); + $wizard->setTitle('Web'); + $wizard->setRequest($this->getRequest()); + $wizard->setConfiguration($this->loadConfiguration()); + $wizard->addPages( + array( + '1st step' => new Page(), + '2nd step' => new Page(), + '3rd step' => new Page(), + 'a wizard' => array( + '4th step' => new Page(), + '5th step' => new Page() + ), + 'last step' => new Page() + ) + ); + + return $wizard; + } + + /** + * Store the given configuration values + * + * @param Zend_Config $config The configuration + */ + protected function storeConfiguration(Zend_Config $config) + { + $session = Session::getSession(); + $session->getNamespace('WebWizard')->setAll($config->toArray(), true); + $session->write(); + } + + /** + * Load all configuration values + * + * @return Zend_Config + */ + protected function loadConfiguration() + { + return new Zend_Config(Session::getSession()->getNamespace('WebWizard')->getAll(), true); + } + + /** + * Clear all stored configuration values + */ + protected function dropConfiguration() + { + $session = Session::getSession(); + $session->removeNamespace('WebWizard'); + $session->write(); } } diff --git a/application/views/scripts/install/index.phtml b/application/views/scripts/install/index.phtml index 13a9e587a..baf0608f0 100644 --- a/application/views/scripts/install/index.phtml +++ b/application/views/scripts/install/index.phtml @@ -6,17 +6,23 @@ diff --git a/library/Icinga/Web/Wizard/Page.php b/library/Icinga/Web/Wizard/Page.php new file mode 100644 index 000000000..ae839d58a --- /dev/null +++ b/library/Icinga/Web/Wizard/Page.php @@ -0,0 +1,54 @@ +title = $title; + } + + /** + * Return the title of this wizard page + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Return a config containing all values of this wizard page + * + * @return Zend_Config + */ + public function getConfig() + { + return $this->getConfiguration(); + } +} diff --git a/library/Icinga/Web/Wizard/Wizard.php b/library/Icinga/Web/Wizard/Wizard.php new file mode 100644 index 000000000..6efba7665 --- /dev/null +++ b/library/Icinga/Web/Wizard/Wizard.php @@ -0,0 +1,379 @@ +pages; + } + + /** + * Add a new page to this wizard + * + * @param Page $page The page to add + */ + public function addPage(Page $page) + { + $ident = $this->generatePageIdentifier($page); + $wizardConfig = $this->getConfig(); + if ($wizardConfig->get($ident) === null) { + $wizardConfig->{$ident} = new Zend_Config(array(), true); + } + + $page->setTokenDisabled(); // Usually default for pages, but not for wizards + $page->setConfiguration($wizardConfig->{$ident}); + $page->setRequest($this->getRequest()); + $page->setName($ident); + $this->pages[] = $page; + } + + /** + * Add multiple pages to this wizard + * + * The given array's keys are titles and its values are class names to add + * as wizard pages. An array as value causes a sub-wizard being added. + * + * @param array $pages The pages to add to the wizard + */ + public function addPages(array $pages) + { + foreach ($pages as $title => $pageClassOrArray) { + if (is_array($pageClassOrArray)) { + $wizard = new static(); + $wizard->setTitle($title); + $this->addPage($wizard); + $wizard->addPages($pageClassOrArray); + } elseif (is_string($pageClassOrArray)) { + $page = new $pageClassOrArray(); + $page->setTitle($title); + $this->addPage($page); + } else { + $pageClassOrArray->setTitle($title); + $this->addPage($pageClassOrArray); + } + } + } + + /** + * Return this wizard's progress + * + * @param int $default The step to return in case this wizard has no progress information yet + * + * @return int The current step + */ + public function getProgress($default = 0) + { + return $this->getConfig()->get('progress', $default); + } + + /** + * Set this wizard's progress + * + * @param int $step The current step + */ + public function setProgress($step) + { + $config = $this->getConfig(); + $config->progress = $step; + } + + /** + * Return the current page + * + * @return Page + * + * @throws ProgrammingError In case there are not any pages registered + */ + public function getCurrentPage() + { + $pages = $this->getPages(); + + if (empty($pages)) { + throw new ProgrammingError('This wizard has no pages'); + } + + return $pages[$this->getProgress()]; + } + + /** + * Return whether the given page is the current page + * + * @param Page $page The page to check + * + * @return bool + */ + public function isCurrentPage(Page $page) + { + return $this->getCurrentPage() === $page; + } + + /** + * Return whether the given page is the first page in the wizard + * + * @param Page $page The page to check + * + * @return bool + * + * @throws ProgrammingError In case there are not any pages registered + */ + public function isFirstPage(Page $page) + { + $pages = $this->getPages(); + + if (empty($pages)) { + throw new ProgrammingError('This wizard has no pages'); + } + + return $pages[0] === $page; + } + + /** + * Return whether the given page has been completed + * + * @param Page $page The page to check + * + * @return bool + * + * @throws ProgrammingError In case there are not any pages registered + */ + public function isCompletedPage(Page $page) + { + $pages = $this->getPages(); + + if (empty($pages)) { + throw new ProgrammingError('This wizard has no pages'); + } + + return $this->isFinished() || array_search($page, $pages, true) < $this->getProgress(); + } + + /** + * Return whether the given page is the last page in the wizard + * + * @param Page $page The page to check + * + * @return bool + * + * @throws ProgrammingError In case there are not any pages registered + */ + public function isLastPage(Page $page) + { + $pages = $this->getPages(); + + if (empty($pages)) { + throw new ProgrammingError('This wizard has no pages'); + } + + return $pages[count($pages) - 1] === $page; + } + + /** + * Return whether this wizard has been completed + * + * @return bool + */ + public function isFinished() + { + return $this->finished && $this->isLastPage($this->getCurrentPage()); + } + + /** + * Return whether the given page is a wizard + * + * @param Page $page The page to check + * + * @return bool + */ + public function isWizard(Page $page) + { + return $page instanceof self; + } + + /** + * Return whether either the back- or next-button was clicked + * + * @see Form::isSubmitted() + */ + public function isSubmitted() + { + $checkData = $this->getRequest()->getParams(); + return isset($checkData['btn_return']) || isset($checkData['btn_advance']); + } + + /** + * Update the wizard's progress + * + * @param bool $lastStepIsLast Whether the last step of this wizard is actually the very last one + */ + public function navigate($lastStepIsLast = true) + { + $currentPage = $this->getCurrentPage(); + if (($pageName = $this->getRequest()->getParam('btn_advance'))) { + if (!$this->isWizard($currentPage) || $currentPage->navigate(false) || $currentPage->isFinished()) { + if ($this->isLastPage($currentPage) && (!$lastStepIsLast || $pageName === 'install')) { + $this->finished = true; + } else { + $pages = $this->getPages(); + $newStep = $this->getProgress() + 1; + if (isset($pages[$newStep]) && $pages[$newStep]->getName() === $pageName) { + $this->setProgress($newStep); + } + } + } + } elseif (($pageName = $this->getRequest()->getParam('btn_return'))) { + if ($this->isWizard($currentPage) && $currentPage->getProgress() > 0) { + $currentPage->navigate(false); + } elseif (!$this->isFirstPage($currentPage)) { + $pages = $this->getPages(); + $newStep = $this->getProgress() - 1; + if ($pages[$newStep]->getName() === $pageName) { + $this->setProgress($newStep); + } + } + } + + $config = $this->getConfig(); + $config->{$currentPage->getName()} = $currentPage->getConfig(); + } + + /** + * Return a unique identifier for the given page + * + * @param Page $page The page for which to return the identifier + * + * @return string The page's unique identifier + */ + protected function generatePageIdentifier(Page $page) + { + if (($name = $page->getName())) { + return $name; + } + + $pageClass = get_class($page); + $wizardConfig = $this->getConfig(); + + if ($wizardConfig->get('page_names') === null || $wizardConfig->page_names->get($pageClass) === null) { + $wizardConfig->page_names = $wizardConfig->get('page_names', new Zend_Config(array(), true)); + $wizardConfig->page_names->{$pageClass} = sprintf('%06x', mt_rand(0, 0xffffff)); + } + + return $wizardConfig->page_names->{$pageClass}; + } + + /** + * Setup the current wizard page + */ + protected function create() + { + $currentPage = $this->getCurrentPage(); + if ($this->isWizard($currentPage)) { + $this->createWizard($currentPage); + } else { + $this->createPage($currentPage); + } + } + + /** + * Display the given page as this wizard's current page + * + * @param Page $page The page + */ + protected function createPage(Page $page) + { + $pages = $this->getPages(); + $currentStep = $this->getProgress(); + + $this->addSubForm($page, $page->getName()); + + if (!$this->isFirstPage($page)) { + $this->addElement( + 'button', + 'btn_return', + array( + 'type' => 'submit', + 'label' => t('Previous'), + 'value' => $pages[$currentStep - 1]->getName() + ) + ); + } + + $this->addElement( + 'button', + 'btn_advance', + array( + 'type' => 'submit', + 'label' => $this->isLastPage($page) ? t('Install') : t('Next'), + 'value' => $this->isLastPage($page) ? 'install' : $pages[$currentStep + 1]->getName() + ) + ); + } + + /** + * Display the current page of the given wizard as this wizard's current page + * + * @param Wizard $wizard The wizard + */ + protected function createWizard(Wizard $wizard) + { + $isFirstPage = $this->isFirstPage($wizard); + $isLastPage = $this->isLastPage($wizard); + $currentSubPage = $wizard->getCurrentPage(); + $isFirstSubPage = $wizard->isFirstPage($currentSubPage); + $isLastSubPage = $wizard->isLastPage($currentSubPage); + + $this->addSubForm($currentSubPage, $currentSubPage->getName()); + + if (!$isFirstPage || !$isFirstSubPage) { + $pages = $isFirstSubPage ? $this->getPages() : $wizard->getPages(); + $currentStep = $isFirstSubPage ? $this->getProgress() : $wizard->getProgress(); + $this->addElement( + 'button', + 'btn_return', + array( + 'type' => 'submit', + 'label' => t('Previous'), + 'value' => $pages[$currentStep - 1]->getName() + ) + ); + } + + $pages = $isLastSubPage ? $this->getPages() : $wizard->getPages(); + $currentStep = $isLastSubPage ? $this->getProgress() : $wizard->getProgress(); + $this->addElement( + 'button', + 'btn_advance', + array( + 'type' => 'submit', + 'label' => $isLastPage && $isLastSubPage ? t('Install') : t('Next'), + 'value' => $isLastPage && $isLastSubPage ? 'install' : $pages[$currentStep + 1]->getName() + ) + ); + } +} From 88e451402f3c369ef19e84603021e33f2888fcc1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 13 May 2014 14:27:30 +0200 Subject: [PATCH 03/11] Redirect to /install when the setup.token exists but no config.ini refs #6136 --- application/controllers/InstallController.php | 9 +++ .../Web/Controller/ActionController.php | 64 ++++++++++++++----- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/application/controllers/InstallController.php b/application/controllers/InstallController.php index f2b0398e2..f476a568e 100644 --- a/application/controllers/InstallController.php +++ b/application/controllers/InstallController.php @@ -19,6 +19,15 @@ class InstallController extends ActionController */ protected $requiresAuthentication = false; + /** + * Whether the controller requires configuration + * + * The install wizard does not require any configuration. + * + * @var bool + */ + protected $requiresConfiguration = false; + /** * Show the wizard and run the installation once its finished */ diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php index 5b42f8730..5893a3c3f 100644 --- a/library/Icinga/Web/Controller/ActionController.php +++ b/library/Icinga/Web/Controller/ActionController.php @@ -46,6 +46,7 @@ use Icinga\File\Pdf; use Icinga\Exception\ProgrammingError; use Icinga\Web\Session; use Icinga\Session\SessionNamespace; +use Icinga\Exception\NotReadableError; /** * Base class for all core action controllers @@ -61,6 +62,13 @@ class ActionController extends Zend_Controller_Action */ protected $requiresAuthentication = true; + /** + * Whether the controller requires configuration + * + * @var bool + */ + protected $requiresConfiguration = true; + private $config; private $configs = array(); @@ -114,26 +122,24 @@ class ActionController extends Zend_Controller_Action $this->_helper = new Zend_Controller_Action_HelperBroker($this); $this->_helper->addPath('../application/controllers/helpers'); - // when noInit is set (e.g. for testing), authentication and init is skipped - if (isset($invokeArgs['noInit'])) { - // TODO: Find out whether this still makes sense? - return; - } - if ($this->_request->isXmlHttpRequest()) { $this->windowId = $this->_request->getHeader('X-Icinga-WindowId', null); } - if ($this->requiresLogin() === false) { - $this->view->tabs = new Tabs(); - $this->init(); - } else { - $url = $this->getRequestUrl(); - if ($url === 'default/index/index') { - // TODO: We need our own router :p - $url = 'dashboard'; + if ($this->requiresConfig() === false) { + if ($this->requiresLogin() === false) { + $this->view->tabs = new Tabs(); + $this->init(); + } else { + $url = $this->getRequestUrl(); + if ($url === 'default/index/index') { + // TODO: We need our own router :p + $url = 'dashboard'; + } + $this->redirectToLogin($url); } - $this->redirectToLogin($url); + } else { + $this->redirectNow(Url::fromPath('install')); } } @@ -224,11 +230,39 @@ class ActionController extends Zend_Controller_Action } } + /** + * Check whether the controller requires configuration. That is when no configuration + * is available and when it is possible to setup the configuration + * + * @return bool + * + * @see requiresConfiguration + */ + protected function requiresConfig() + { + if (!$this->requiresConfiguration) { + return false; + } + + if (file_exists(Config::$configDir . '/setup.token')) { + try { + $config = Config::app()->toArray(); + } catch (NotReadableError $e) { + return true; + } + + return empty($config); + } else { + return false; + } + } + /** * Check whether the controller requires a login. That is when the controller requires authentication and the * user is currently not authenticated * * @return bool + * * @see requiresAuthentication */ protected function requiresLogin() From de169c7bf58ca62285787473b2f60d23de56e5b3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 13 May 2014 14:32:08 +0200 Subject: [PATCH 04/11] Make it possible to disable form submit buttons forcefully --- library/Icinga/Web/Form.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php index 7cd3ad746..384ca0848 100644 --- a/library/Icinga/Web/Form.php +++ b/library/Icinga/Web/Form.php @@ -127,6 +127,15 @@ class Form extends Zend_Form */ protected $last_note_id = 0; + /** + * Whether buttons are shown or not + * + * This is just a q&d solution and MUST NOT survive any refactoring! + * + * @var bool + */ + protected $buttonsHidden = false; + /** * Getter for the session ID * @@ -279,11 +288,11 @@ class Form extends Zend_Form $this->initCsrfToken(); $this->create(); - if ($this->submitLabel) { + if (!$this->buttonsHidden && $this->submitLabel) { $this->addSubmitButton(); } - if ($this->cancelLabel) { + if (!$this->buttonsHidden && $this->cancelLabel) { $this->addCancelButton(); } @@ -596,4 +605,9 @@ class Form extends Zend_Form return $this; } + + public function hideButtons() + { + $this->buttonsHidden = true; + } } From 51bac035ac1bdee2c63f680ec66120beb1081af3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 13 May 2014 15:28:27 +0200 Subject: [PATCH 05/11] Fix subForm functionality --- library/Icinga/Web/Form.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php index 384ca0848..3822062e0 100644 --- a/library/Icinga/Web/Form.php +++ b/library/Icinga/Web/Form.php @@ -599,7 +599,7 @@ class Form extends Zend_Form $decorators = $this->getDecorators(); if (empty($decorators)) { $this->addDecorator('FormElements') - //->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form')) + ->addDecorator('HtmlTag', array('tag' => 'div')) // Quickfix to get subForms to work ->addDecorator('Form'); } From 429e09aae2e9f8fd63dfad1e5e16b0e22f90dfdc Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 14 May 2014 09:01:24 +0200 Subject: [PATCH 06/11] Multistep pages should not have random generated names refs #6136 --- application/controllers/InstallController.php | 31 +++++++------- library/Icinga/Web/Wizard/Page.php | 8 ++++ library/Icinga/Web/Wizard/Wizard.php | 40 +++++-------------- 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/application/controllers/InstallController.php b/application/controllers/InstallController.php index f476a568e..9755f99d7 100644 --- a/application/controllers/InstallController.php +++ b/application/controllers/InstallController.php @@ -4,7 +4,6 @@ // {{{ICINGA_LICENSE_HEADER}}} use Icinga\Web\Session; -use Icinga\Web\Wizard\Page; use Icinga\Web\Wizard\Wizard; use Icinga\Web\Controller\ActionController; @@ -36,12 +35,14 @@ class InstallController extends ActionController $wizard = $this->createWizard(); $wizard->navigate(); // Needs to be called before isSubmittedAndValid() as this creates the form - if ($wizard->isSubmittedAndValid() && $wizard->isFinished()) { - // TODO: Run the installer (Who creates an installer? How do we handle module installers?) - $this->dropConfiguration(); // TODO: Should only be done if the installation has been successfully completed - $this->view->installer = ''; - } else { - $this->storeConfiguration($wizard->getConfig()); + if ($wizard->isSubmittedAndValid()) { + if ($wizard->isFinished()) { + // TODO: Run the installer (Who creates an installer? How do we handle module installers?) + $this->dropConfiguration(); // TODO: Should only be done if the installation has been successfully completed + $this->view->installer = ''; + } else { + $this->storeConfiguration($wizard->getConfig()); + } } $this->view->wizard = $wizard; @@ -60,14 +61,14 @@ class InstallController extends ActionController $wizard->setConfiguration($this->loadConfiguration()); $wizard->addPages( array( - '1st step' => new Page(), - '2nd step' => new Page(), - '3rd step' => new Page(), - 'a wizard' => array( - '4th step' => new Page(), - '5th step' => new Page() - ), - 'last step' => new Page() +// t('Welcome') => 'Icinga\Form\Install\WelcomePage', +// t('Requirements') => 'Icinga\Form\Install\RequirementsPage', +// t('Authentication') => 'Icinga\Form\Install\AuthenticationPage', +// t('Administration') => 'Icinga\Form\Install\AdministrationPage', +// t('Preferences') => 'Icinga\Form\Install\PreferencesPage', + t('Logging') => 'Icinga\Form\Install\LoggingPage', +// t('Database Setup') => 'Icinga\Form\Install\DatabasePage', +// t('Summary') => 'Icinga\Form\Install\SummaryPage' ) ); diff --git a/library/Icinga/Web/Wizard/Page.php b/library/Icinga/Web/Wizard/Page.php index ae839d58a..53383fd1a 100644 --- a/library/Icinga/Web/Wizard/Page.php +++ b/library/Icinga/Web/Wizard/Page.php @@ -22,6 +22,14 @@ class Page extends Form */ protected $title = ''; + /** + * Overwrite this to initialize this wizard page + */ + public function init() + { + + } + /** * Set the title for this wizard page * diff --git a/library/Icinga/Web/Wizard/Wizard.php b/library/Icinga/Web/Wizard/Wizard.php index 6efba7665..301149f08 100644 --- a/library/Icinga/Web/Wizard/Wizard.php +++ b/library/Icinga/Web/Wizard/Wizard.php @@ -41,16 +41,18 @@ class Wizard extends Page */ public function addPage(Page $page) { - $ident = $this->generatePageIdentifier($page); - $wizardConfig = $this->getConfig(); - if ($wizardConfig->get($ident) === null) { - $wizardConfig->{$ident} = new Zend_Config(array(), true); + if (!($pageName = $page->getName())) { + throw new ProgrammingError('Wizard page "' . get_class($page) . '" has no unique name'); } - $page->setTokenDisabled(); // Usually default for pages, but not for wizards - $page->setConfiguration($wizardConfig->{$ident}); + $wizardConfig = $this->getConfig(); + if ($wizardConfig->get($pageName) === null) { + $wizardConfig->{$pageName} = new Zend_Config(array(), true); + } + + $page->setConfiguration($wizardConfig->{$pageName}); $page->setRequest($this->getRequest()); - $page->setName($ident); + $page->setTokenDisabled(); // Usually default for pages, but not for wizards $this->pages[] = $page; } @@ -263,30 +265,6 @@ class Wizard extends Page $config->{$currentPage->getName()} = $currentPage->getConfig(); } - /** - * Return a unique identifier for the given page - * - * @param Page $page The page for which to return the identifier - * - * @return string The page's unique identifier - */ - protected function generatePageIdentifier(Page $page) - { - if (($name = $page->getName())) { - return $name; - } - - $pageClass = get_class($page); - $wizardConfig = $this->getConfig(); - - if ($wizardConfig->get('page_names') === null || $wizardConfig->page_names->get($pageClass) === null) { - $wizardConfig->page_names = $wizardConfig->get('page_names', new Zend_Config(array(), true)); - $wizardConfig->page_names->{$pageClass} = sprintf('%06x', mt_rand(0, 0xffffff)); - } - - return $wizardConfig->page_names->{$pageClass}; - } - /** * Setup the current wizard page */ From 1637a19c237872ad81c6b068afa9c2a09a73c302 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 14 May 2014 11:01:01 +0200 Subject: [PATCH 07/11] Fix multistep form creation refs #6136 --- application/controllers/InstallController.php | 2 +- library/Icinga/Web/Wizard/Wizard.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/application/controllers/InstallController.php b/application/controllers/InstallController.php index 9755f99d7..da320a6b1 100644 --- a/application/controllers/InstallController.php +++ b/application/controllers/InstallController.php @@ -33,9 +33,9 @@ class InstallController extends ActionController public function indexAction() { $wizard = $this->createWizard(); - $wizard->navigate(); // Needs to be called before isSubmittedAndValid() as this creates the form if ($wizard->isSubmittedAndValid()) { + $wizard->navigate(); if ($wizard->isFinished()) { // TODO: Run the installer (Who creates an installer? How do we handle module installers?) $this->dropConfiguration(); // TODO: Should only be done if the installation has been successfully completed diff --git a/library/Icinga/Web/Wizard/Wizard.php b/library/Icinga/Web/Wizard/Wizard.php index 301149f08..2129dac79 100644 --- a/library/Icinga/Web/Wizard/Wizard.php +++ b/library/Icinga/Web/Wizard/Wizard.php @@ -288,6 +288,7 @@ class Wizard extends Page $pages = $this->getPages(); $currentStep = $this->getProgress(); + $page->buildForm(); // Needs to get called manually as it's nothing that Zend knows about $this->addSubForm($page, $page->getName()); if (!$this->isFirstPage($page)) { @@ -326,6 +327,7 @@ class Wizard extends Page $isFirstSubPage = $wizard->isFirstPage($currentSubPage); $isLastSubPage = $wizard->isLastPage($currentSubPage); + $currentSubPage->buildForm(); // Needs to get called manually as it's nothing that Zend knows about $this->addSubForm($currentSubPage, $currentSubPage->getName()); if (!$isFirstPage || !$isFirstSubPage) { From 33d41426643595e8f1beb1ea5ab94e0c0c33727d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 14 May 2014 11:02:11 +0200 Subject: [PATCH 08/11] Make it possible to recreate a form --- library/Icinga/Web/Form.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php index 3822062e0..5d5c3da85 100644 --- a/library/Icinga/Web/Form.php +++ b/library/Icinga/Web/Form.php @@ -610,4 +610,13 @@ class Form extends Zend_Form { $this->buttonsHidden = true; } + + /** + * q&d solution to be able to recreate a form + */ + public function reset() + { + $this->created = false; + $this->clearElements(); + } } From 1f942acd38a4ef5a1655cc17badc639aecbe2d24 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 14 May 2014 11:03:16 +0200 Subject: [PATCH 09/11] Fix multistep page creation refs #6136 --- library/Icinga/Web/Wizard/Wizard.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/Icinga/Web/Wizard/Wizard.php b/library/Icinga/Web/Wizard/Wizard.php index 2129dac79..6be831ae8 100644 --- a/library/Icinga/Web/Wizard/Wizard.php +++ b/library/Icinga/Web/Wizard/Wizard.php @@ -246,6 +246,7 @@ class Wizard extends Page $newStep = $this->getProgress() + 1; if (isset($pages[$newStep]) && $pages[$newStep]->getName() === $pageName) { $this->setProgress($newStep); + $this->reset(); } } } @@ -257,6 +258,7 @@ class Wizard extends Page $newStep = $this->getProgress() - 1; if ($pages[$newStep]->getName() === $pageName) { $this->setProgress($newStep); + $this->reset(); } } } From 754f854dd03c381a40d79210a8a3e9ac34caddd0 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 14 May 2014 12:42:22 +0200 Subject: [PATCH 10/11] Multistep pages should know about their multistep form refs #6136 --- library/Icinga/Web/Wizard/Page.php | 19 ++++++++++++++++++ library/Icinga/Web/Wizard/Wizard.php | 30 ++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/library/Icinga/Web/Wizard/Page.php b/library/Icinga/Web/Wizard/Page.php index 53383fd1a..fdc0fd171 100644 --- a/library/Icinga/Web/Wizard/Page.php +++ b/library/Icinga/Web/Wizard/Page.php @@ -15,6 +15,13 @@ class Page extends Form */ protected $tokenDisabled = true; + /** + * The wizard this page is part of + * + * @var Wizard + */ + protected $wizard; + /** * The title of this wizard page * @@ -22,6 +29,18 @@ class Page extends Form */ protected $title = ''; + /** + * Create a new wizard page + * + * @param Wizard $wizard The wizard this page is part of + * @param mixed $options Zend_Form options + */ + public function __construct(Wizard $wizard = null, $options = null) + { + parent::__construct($options); + $this->wizard = $wizard; + } + /** * Overwrite this to initialize this wizard page */ diff --git a/library/Icinga/Web/Wizard/Wizard.php b/library/Icinga/Web/Wizard/Wizard.php index 6be831ae8..64e9e5f24 100644 --- a/library/Icinga/Web/Wizard/Wizard.php +++ b/library/Icinga/Web/Wizard/Wizard.php @@ -34,6 +34,27 @@ class Wizard extends Page return $this->pages; } + /** + * Return a page by its name or null if it's not found + * + * @param string $pageName The name of the page + * + * @return Page|null + */ + public function getPage($pageName) + { + $candidates = array_filter( + $this->pages, // Cannot use getPages() here because I might get called as part of Page::isRequired() + function ($page) use ($pageName) { return $page->getName() === $pageName; } + ); + + if (!empty($candidates)) { + return array_shift($candidates); + } elseif ($this->wizard !== null) { + return $this->wizard->getPage($pageName); + } + } + /** * Add a new page to this wizard * @@ -68,17 +89,14 @@ class Wizard extends Page { foreach ($pages as $title => $pageClassOrArray) { if (is_array($pageClassOrArray)) { - $wizard = new static(); + $wizard = new static($this); $wizard->setTitle($title); $this->addPage($wizard); $wizard->addPages($pageClassOrArray); - } elseif (is_string($pageClassOrArray)) { - $page = new $pageClassOrArray(); + } else { + $page = new $pageClassOrArray($this); $page->setTitle($title); $this->addPage($page); - } else { - $pageClassOrArray->setTitle($title); - $this->addPage($pageClassOrArray); } } } From ef7bb0e79485976d3321b0885ab60743c715b4cd Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 14 May 2014 12:44:12 +0200 Subject: [PATCH 11/11] It is a multistep page's responsibility whether it is shown to the user refs #6136 --- library/Icinga/Web/Wizard/Page.php | 14 +++++++++++++- library/Icinga/Web/Wizard/Wizard.php | 8 +++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Web/Wizard/Page.php b/library/Icinga/Web/Wizard/Page.php index fdc0fd171..82e064745 100644 --- a/library/Icinga/Web/Wizard/Page.php +++ b/library/Icinga/Web/Wizard/Page.php @@ -49,6 +49,18 @@ class Page extends Form } + /** + * Return whether this page needs to be shown to the user + * + * Overwrite this to add page specific handling + * + * @return bool + */ + public function isRequired() + { + return true; + } + /** * Set the title for this wizard page * @@ -70,7 +82,7 @@ class Page extends Form } /** - * Return a config containing all values of this wizard page + * Return a config containing all values provided by the user * * @return Zend_Config */ diff --git a/library/Icinga/Web/Wizard/Wizard.php b/library/Icinga/Web/Wizard/Wizard.php index 64e9e5f24..3fc5fa49d 100644 --- a/library/Icinga/Web/Wizard/Wizard.php +++ b/library/Icinga/Web/Wizard/Wizard.php @@ -8,6 +8,12 @@ use Zend_Config; use Icinga\Web\Form; use Icinga\Exception\ProgrammingError; +/** + * Multistep form with support for nesting and dynamic behaviour + * + * @todo Pages that were displayed initially and filled out by the user remain + * currently in the configuration returned by Wizard::getConfig() + */ class Wizard extends Page { /** @@ -31,7 +37,7 @@ class Wizard extends Page */ public function getPages() { - return $this->pages; + return array_filter($this->pages, function ($page) { return $page->isRequired(); }); } /**