diff --git a/application/controllers/InstallController.php b/application/controllers/InstallController.php new file mode 100644 index 000000000..da320a6b1 --- /dev/null +++ b/application/controllers/InstallController.php @@ -0,0 +1,111 @@ +createWizard(); + + 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 + $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( +// 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' + ) + ); + + 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(); + } +} + +// @codeCoverageIgnoreEnd diff --git a/application/views/scripts/install/index.phtml b/application/views/scripts/install/index.phtml new file mode 100644 index 000000000..baf0608f0 --- /dev/null +++ b/application/views/scripts/install/index.phtml @@ -0,0 +1,40 @@ +
+
+ +

getTitle(); ?>

+
+ +
+isFinished()): ?> + partial('install/index/installog.phtml', array('installer' => $installer)); ?> + + + +
+
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/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() diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php index 7cd3ad746..5d5c3da85 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(); } @@ -590,10 +599,24 @@ 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'); } return $this; } + + public function hideButtons() + { + $this->buttonsHidden = true; + } + + /** + * q&d solution to be able to recreate a form + */ + public function reset() + { + $this->created = false; + $this->clearElements(); + } } 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/library/Icinga/Web/Wizard/Page.php b/library/Icinga/Web/Wizard/Page.php new file mode 100644 index 000000000..82e064745 --- /dev/null +++ b/library/Icinga/Web/Wizard/Page.php @@ -0,0 +1,93 @@ +wizard = $wizard; + } + + /** + * Overwrite this to initialize this wizard page + */ + public function init() + { + + } + + /** + * 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 + * + * @param string $title The title to set + */ + public function setTitle($title) + { + $this->title = $title; + } + + /** + * Return the title of this wizard page + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Return a config containing all values provided by the user + * + * @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..3fc5fa49d --- /dev/null +++ b/library/Icinga/Web/Wizard/Wizard.php @@ -0,0 +1,385 @@ +pages, function ($page) { return $page->isRequired(); }); + } + + /** + * 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 + * + * @param Page $page The page to add + */ + public function addPage(Page $page) + { + if (!($pageName = $page->getName())) { + throw new ProgrammingError('Wizard page "' . get_class($page) . '" has no unique name'); + } + + $wizardConfig = $this->getConfig(); + if ($wizardConfig->get($pageName) === null) { + $wizardConfig->{$pageName} = new Zend_Config(array(), true); + } + + $page->setConfiguration($wizardConfig->{$pageName}); + $page->setRequest($this->getRequest()); + $page->setTokenDisabled(); // Usually default for pages, but not for wizards + $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($this); + $wizard->setTitle($title); + $this->addPage($wizard); + $wizard->addPages($pageClassOrArray); + } else { + $page = new $pageClassOrArray($this); + $page->setTitle($title); + $this->addPage($page); + } + } + } + + /** + * 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); + $this->reset(); + } + } + } + } 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); + $this->reset(); + } + } + } + + $config = $this->getConfig(); + $config->{$currentPage->getName()} = $currentPage->getConfig(); + } + + /** + * 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(); + + $page->buildForm(); // Needs to get called manually as it's nothing that Zend knows about + $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); + + $currentSubPage->buildForm(); // Needs to get called manually as it's nothing that Zend knows about + $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() + ) + ); + } +} 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; + } +}