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 @@
+
+
+
+
+isFinished()): ?>
+ = $this->partial('install/index/installog.phtml', array('installer' => $installer)); ?>
+
+ = $wizard; ?>
+
+
+
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 @@
+= $installer; ?>
\ 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;
+ }
+}