summaryrefslogtreecommitdiffstats
path: root/modules/setup/application
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/setup/application/clicommands/ConfigCommand.php188
-rw-r--r--modules/setup/application/clicommands/TokenCommand.php89
-rw-r--r--modules/setup/application/controllers/IndexController.php91
-rw-r--r--modules/setup/application/forms/AdminAccountPage.php431
-rw-r--r--modules/setup/application/forms/AuthBackendPage.php274
-rw-r--r--modules/setup/application/forms/AuthenticationPage.php69
-rw-r--r--modules/setup/application/forms/DatabaseCreationPage.php209
-rw-r--r--modules/setup/application/forms/DbResourcePage.php183
-rw-r--r--modules/setup/application/forms/GeneralConfigPage.php41
-rw-r--r--modules/setup/application/forms/LdapDiscoveryConfirmPage.php133
-rw-r--r--modules/setup/application/forms/LdapDiscoveryPage.php115
-rw-r--r--modules/setup/application/forms/LdapResourcePage.php152
-rw-r--r--modules/setup/application/forms/ModulePage.php108
-rw-r--r--modules/setup/application/forms/RequirementsPage.php68
-rw-r--r--modules/setup/application/forms/SummaryPage.php84
-rw-r--r--modules/setup/application/forms/UserGroupBackendPage.php147
-rw-r--r--modules/setup/application/forms/WelcomePage.php45
-rw-r--r--modules/setup/application/views/scripts/form/setup-modules.phtml33
-rw-r--r--modules/setup/application/views/scripts/form/setup-requirements.phtml48
-rw-r--r--modules/setup/application/views/scripts/form/setup-summary.phtml40
-rw-r--r--modules/setup/application/views/scripts/form/setup-welcome.phtml120
-rw-r--r--modules/setup/application/views/scripts/index/index.phtml224
-rw-r--r--modules/setup/application/views/scripts/index/parts/finish.phtml34
-rw-r--r--modules/setup/application/views/scripts/index/parts/wizard.phtml1
24 files changed, 2927 insertions, 0 deletions
diff --git a/modules/setup/application/clicommands/ConfigCommand.php b/modules/setup/application/clicommands/ConfigCommand.php
new file mode 100644
index 0000000..e50333e
--- /dev/null
+++ b/modules/setup/application/clicommands/ConfigCommand.php
@@ -0,0 +1,188 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Clicommands;
+
+use Icinga\Application\Logger;
+use Icinga\Cli\Command;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Setup\Webserver;
+
+class ConfigCommand extends Command
+{
+ /**
+ * Create Icinga Web 2's configuration directory
+ *
+ * USAGE:
+ *
+ * icingacli setup config directory [options]
+ *
+ * OPTIONS:
+ *
+ * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
+ *
+ * --mode=<mode> The access mode to use [2770]
+ *
+ * --group=<group> Owner group for the configuration directory [icingaweb2]
+ *
+ * EXAMPLES:
+ *
+ * icingacli setup config directory
+ *
+ * icingacli setup config directory --mode=2775 --config=/opt/icingaweb2/etc
+ */
+ public function directoryAction()
+ {
+ $configDir = trim($this->params->get('config', $this->app->getConfigDir()));
+ if (strlen($configDir) === 0) {
+ $this->fail($this->translate(
+ 'The argument --config expects a path to Icinga Web 2\'s configuration files'
+ ));
+ }
+
+ $group = trim($this->params->get('group', 'icingaweb2'));
+ if (strlen($group) === 0) {
+ $this->fail($this->translate(
+ 'The argument --group expects a owner group for the configuration directory'
+ ));
+ }
+
+ $mode = trim($this->params->get('mode', '2770'));
+ if (strlen($mode) === 0) {
+ $this->fail($this->translate(
+ 'The argument --mode expects an access mode for the configuration directory'
+ ));
+ }
+
+ if (! file_exists($configDir) && ! @mkdir($configDir, 0755, true)) {
+ $e = error_get_last();
+ $this->fail(sprintf(
+ $this->translate('Can\'t create configuration directory %s: %s'),
+ $configDir,
+ $e['message']
+ ));
+ }
+
+ if (! @chmod($configDir, octdec($mode))) {
+ $e = error_get_last();
+ $this->fail(sprintf(
+ $this->translate('Can\'t change the mode of the configuration directory to %s: %s'),
+ $mode,
+ $e['message']
+ ));
+ }
+
+ if (! @chgrp($configDir, $group)) {
+ $e = error_get_last();
+ $this->fail(sprintf(
+ $this->translate('Can\'t change the group of %s to %s: %s'),
+ $configDir,
+ $group,
+ $e['message']
+ ));
+ }
+
+ printf($this->translate('Successfully created configuration directory %s') . PHP_EOL, $configDir);
+ }
+
+ /**
+ * Create webserver configuration
+ *
+ * USAGE:
+ *
+ * icingacli setup config webserver <apache|nginx> [options]
+ *
+ * OPTIONS:
+ *
+ * --path=<urlpath> The URL path to Icinga Web 2 [/icingaweb2]
+ *
+ * --root|--document-root=<directory> The directory from which the webserver will serve files
+ * [/path/to/icingaweb2/public]
+ *
+ * --enable-fpm Enable FPM handler for Apache (Nginx is always enabled)
+ *
+ * --fpm-uri=<uri> Address or path where to pass requests to FPM [127.0.0.1:9000]
+ *
+ * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
+ *
+ * --file=<filename> Write configuration to file [stdout]
+ *
+ * EXAMPLES:
+ *
+ * icingacli setup config webserver apache
+ *
+ * icingacli setup config webserver apache \
+ * --path=/icingaweb2 \
+ * --document-root=/usr/share/icingaweb2/public \
+ * --config=/etc/icingaweb2
+ *
+ * icingacli setup config webserver apache \
+ * --file=/etc/apache2/conf.d/icingaweb2.conf
+ *
+ * icingacli setup config webserver nginx \
+ * --root=/usr/share/icingaweb2/public \
+ * --fpm-uri=unix:/var/run/php5-fpm.sock
+ */
+ public function webserverAction()
+ {
+ if (($type = $this->params->getStandalone()) === null) {
+ $this->fail($this->translate('Argument type is mandatory.'));
+ }
+
+ $webserver = null;
+ try {
+ $webserver = Webserver::createInstance($type);
+ } catch (ProgrammingError $e) {
+ $this->fail($this->translate('Unknown type') . ': ' . $type);
+ }
+ $urlPath = trim($this->params->get('path', $webserver->getUrlPath()));
+ if (strlen($urlPath) === 0) {
+ $this->fail($this->translate('The argument --path expects a URL path'));
+ }
+ $documentRoot = trim(
+ $this->params->get('root', $this->params->get('document-root', $webserver->getDocumentRoot()))
+ );
+ if (strlen($documentRoot) === 0) {
+ $this->fail($this->translate(
+ 'The argument --root/--document-root expects a directory from which the webserver will serve files'
+ ));
+ }
+ $configDir = trim($this->params->get('config', $webserver->getConfigDir()));
+ if (strlen($configDir) === 0) {
+ $this->fail($this->translate(
+ 'The argument --config expects a path to Icinga Web 2\'s configuration files'
+ ));
+ }
+
+ $enableFpm = $this->params->shift('enable-fpm', $webserver->getEnableFpm());
+
+ $fpmUri = trim($this->params->get('fpm-uri', $webserver->getFpmUri()));
+ if (empty($fpmUri)) {
+ $this->fail($this->translate(
+ 'The argument --fpm-uri expects an address or path where to pass requests to FPM'
+ ));
+ }
+ $webserver
+ ->setDocumentRoot($documentRoot)
+ ->setConfigDir($configDir)
+ ->setUrlPath($urlPath)
+ ->setEnableFpm($enableFpm)
+ ->setFpmUri($fpmUri);
+ $config = $webserver->generate() . "\n";
+ if (($file = $this->params->get('file')) !== null) {
+ if (file_exists($file) === true) {
+ $this->fail(sprintf($this->translate('File %s already exists. Please delete it first.'), $file));
+ }
+ Logger::info($this->translate('Write %s configuration to file: %s'), $type, $file);
+ $re = file_put_contents($file, $config);
+ if ($re === false) {
+ $this->fail($this->translate('Could not write to file') . ': ' . $file);
+ }
+ Logger::info($this->translate('Successfully written %d bytes to file'), $re);
+ return true;
+ }
+ echo $config;
+ return true;
+ }
+}
diff --git a/modules/setup/application/clicommands/TokenCommand.php b/modules/setup/application/clicommands/TokenCommand.php
new file mode 100644
index 0000000..f1c30d1
--- /dev/null
+++ b/modules/setup/application/clicommands/TokenCommand.php
@@ -0,0 +1,89 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Clicommands;
+
+use Icinga\Cli\Command;
+
+/**
+ * Maintain the setup wizard's authentication
+ *
+ * The token command allows you to display the current setup token or to create a new one.
+ *
+ * Usage: icingacli setup token <action>
+ */
+class TokenCommand extends Command
+{
+ /**
+ * Display the current setup token
+ *
+ * Shows you the current setup token used to authenticate when setting up Icinga Web 2 using the web-based wizard.
+ *
+ * USAGE:
+ *
+ * icingacli setup token show [options]
+ *
+ * OPTIONS:
+ *
+ * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
+ */
+ public function showAction()
+ {
+ $configDir = $this->params->get('config', $this->app->getConfigDir());
+ if (! is_string($configDir) || strlen(trim($configDir)) === 0) {
+ $this->fail($this->translate(
+ 'The argument --config expects a path to Icinga Web 2\'s configuration files'
+ ));
+ }
+
+ $token = file_get_contents($configDir . '/setup.token');
+ if (! $token) {
+ $this->fail(
+ $this->translate('Nothing to show. Please create a new setup token using the generateToken action.')
+ );
+ }
+
+ printf($this->translate("The current setup token is: %s\n"), $token);
+ }
+
+ /**
+ * Create a new setup token
+ *
+ * Re-generates the setup token used to authenticate when setting up Icinga Web 2 using the web-based wizard.
+ *
+ * USAGE:
+ *
+ * icingacli setup token create [options]
+ *
+ * OPTIONS:
+ *
+ * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
+ */
+ public function createAction()
+ {
+ $configDir = $this->params->get('config', $this->app->getConfigDir());
+ if (! is_string($configDir) || strlen(trim($configDir)) === 0) {
+ $this->fail($this->translate(
+ 'The argument --config expects a path to Icinga Web 2\'s configuration files'
+ ));
+ }
+
+ $file = $configDir . '/setup.token';
+
+ if (function_exists('openssl_random_pseudo_bytes')) {
+ $token = bin2hex(openssl_random_pseudo_bytes(8));
+ } else {
+ $token = substr(md5(mt_rand()), 16);
+ }
+
+ if (false === file_put_contents($file, $token)) {
+ $this->fail(sprintf($this->translate('Cannot write setup token "%s" to disk.'), $file));
+ }
+
+ if (! chmod($file, 0660)) {
+ $this->fail(sprintf($this->translate('Cannot change access mode of "%s" to %o.'), $file, 0660));
+ }
+
+ printf($this->translate("The newly generated setup token is: %s\n"), $token);
+ }
+}
diff --git a/modules/setup/application/controllers/IndexController.php b/modules/setup/application/controllers/IndexController.php
new file mode 100644
index 0000000..b75643c
--- /dev/null
+++ b/modules/setup/application/controllers/IndexController.php
@@ -0,0 +1,91 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Controllers;
+
+use Icinga\Module\Setup\WebWizard;
+use Icinga\Web\Controller;
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class IndexController extends Controller
+{
+ /**
+ * Whether the controller requires the user to be authenticated
+ *
+ * FALSE as the wizard uses token authentication
+ *
+ * @var bool
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $innerLayout = 'inline';
+
+ /**
+ * Show the web wizard and run the configuration once finished
+ */
+ public function indexAction()
+ {
+ $wizard = new WebWizard();
+
+ if ($wizard->isFinished()) {
+ $setup = $wizard->getSetup();
+ $success = $setup->run();
+ if ($success) {
+ $wizard->clearSession();
+ } else {
+ $wizard->setIsFinished(false);
+ }
+
+ $this->view->success = $success;
+ $this->view->report = $setup->getReport();
+ } else {
+ $wizard->handleRequest();
+
+ $restartForm = new Form();
+ $restartForm->setUidDisabled();
+ $restartForm->setName('setup_restart_form');
+ $restartForm->setAction(Url::fromPath('setup/index/restart'));
+ $restartForm->setAttrib('class', 'restart-form');
+ $restartForm->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'type' => 'submit',
+ 'value' => 'btn_submit',
+ 'escape' => false,
+ 'label' => $this->view->icon('reply-all'),
+ 'title' => $this->translate('Restart the setup'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->view->restartForm = $restartForm;
+ }
+
+ $this->view->wizard = $wizard;
+ $this->view->title = $this->translate('Setup') . ' :: ' . $this->view->defaultTitle;
+ }
+
+ /**
+ * Reset session and restart the wizard
+ */
+ public function restartAction()
+ {
+ $this->assertHttpMethod('POST');
+
+ $form = new Form(array(
+ 'onSuccess' => function () {
+ $wizard = new WebWizard();
+ $wizard->clearSession(false);
+ }
+ ));
+ $form->setUidDisabled();
+ $form->setRedirectUrl('setup');
+ $form->setSubmitLabel('btn_submit');
+ $form->handleRequest();
+ }
+}
diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php
new file mode 100644
index 0000000..b33749e
--- /dev/null
+++ b/modules/setup/application/forms/AdminAccountPage.php
@@ -0,0 +1,431 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Authentication\User\ExternalBackend;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\User\DbUserBackend;
+use Icinga\Authentication\User\LdapUserBackend;
+use Icinga\Authentication\UserGroup\UserGroupBackend;
+use Icinga\Authentication\UserGroup\LdapUserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Data\Selectable;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Protocol\Ldap\LdapQuery;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define the initial administrative account
+ */
+class AdminAccountPage extends Form
+{
+ /**
+ * The resource configuration to use
+ *
+ * @var array
+ */
+ protected $resourceConfig;
+
+ /**
+ * The user backend configuration to use
+ *
+ * @var array
+ */
+ protected $backendConfig;
+
+ /**
+ * The user group backend configuration to use
+ *
+ * @var array
+ */
+ protected $groupConfig;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_admin_account');
+ $this->setTitle($this->translate('Administration', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Now it\'s time to configure your first administrative account or group for Icinga Web 2.'
+ ));
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $this->resourceConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Set the user backend configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setBackendConfig(array $config)
+ {
+ $this->backendConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Set the user group backend configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setGroupConfig(array $config = null)
+ {
+ $this->groupConfig = $config;
+ return $this;
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $choices = array();
+ $groups = [];
+ if ($this->backendConfig['backend'] !== 'db') {
+ $choices['by_name'] = $this->translate('By Name', 'setup.admin');
+ $choice = isset($formData['user_type']) ? $formData['user_type'] : 'by_name';
+
+ if (in_array($this->backendConfig['backend'], array('ldap', 'msldap'))) {
+ $groups = $this->fetchGroups();
+ if (! empty($groups)) {
+ $choices['user_group'] = $this->translate('User Group', 'setup.admin');
+ }
+ }
+ } else {
+ $choices['new_user'] = $this->translate('New User', 'setup.admin');
+ $choice = isset($formData['user_type']) ? $formData['user_type'] : 'new_user';
+ }
+
+ $users = [];
+ if (in_array($this->backendConfig['backend'], array('db', 'ldap', 'msldap'))) {
+ $users = $this->fetchUsers();
+ if (! empty($users)) {
+ $choices['existing_user'] = $this->translate('Existing User', 'setup.admin');
+ }
+ }
+
+ if (count($choices) > 1) {
+ $this->addElement(
+ 'select',
+ 'user_type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Type Of Definition'),
+ 'description' => $this->translate('Choose how to define the desired account.'),
+ 'multiOptions' => $choices,
+ 'value' => $choice
+ )
+ );
+ } else {
+ $this->addElement(
+ 'hidden',
+ 'user_type',
+ array(
+ 'required' => true,
+ 'value' => key($choices)
+ )
+ );
+ }
+
+ if ($choice === 'by_name') {
+ $this->addElement(
+ 'text',
+ 'by_name',
+ array(
+ 'required' => true,
+ 'value' => $this->getUsername(),
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'Define the initial administrative account by providing a username that reflects'
+ . ' a user created later or one that is authenticated using external mechanisms.'
+ )
+ )
+ );
+ }
+
+ if ($choice === 'user_group') {
+ $this->addElement(
+ 'select',
+ 'user_group',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Group Name'),
+ 'description' => $this->translate(
+ 'Choose a user group reported by the LDAP backend'
+ . ' to permit its members administrative access.',
+ 'setup.admin'
+ ),
+ 'multiOptions' => array_combine($groups, $groups)
+ )
+ );
+ }
+
+ if ($choice === 'existing_user') {
+ $this->addElement(
+ 'select',
+ 'existing_user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => sprintf(
+ $this->translate(
+ 'Choose a user reported by the %s backend as the initial administrative account.',
+ 'setup.admin'
+ ),
+ $this->backendConfig['backend'] === 'db'
+ ? $this->translate('database', 'setup.admin.authbackend')
+ : 'LDAP'
+ ),
+ 'multiOptions' => array_combine($users, $users)
+ )
+ );
+ }
+
+ if ($choice === 'new_user') {
+ $this->addElement(
+ 'text',
+ 'new_user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'Enter the username to be used when creating an initial administrative account.'
+ )
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_user_password',
+ array(
+ 'required' => true,
+ 'renderPassword' => true,
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate(
+ 'Enter the password to assign to the newly created account.'
+ ),
+ 'autocomplete' => 'new-password'
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_user_2ndpass',
+ array(
+ 'required' => true,
+ 'renderPassword' => true,
+ 'label' => $this->translate('Repeat password'),
+ 'description' => $this->translate(
+ 'Please repeat the password given above to avoid typing errors.'
+ ),
+ 'validators' => array(
+ array('identical', false, array('new_user_password'))
+ )
+ )
+ );
+ }
+ }
+
+ /**
+ * Validate the given request data and ensure that any new user does not already exist
+ *
+ * @param array $data The request data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+
+ if ($data['user_type'] === 'new_user' && $this->hasUser($data['new_user'])) {
+ $this->getElement('new_user')->addError($this->translate('Username already exists.'));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the name of the externally authenticated user
+ *
+ * @return string
+ */
+ protected function getUsername()
+ {
+ list($name, $_) = ExternalBackend::getRemoteUserInformation();
+ if ($name === null) {
+ return '';
+ }
+
+ if (isset($this->backendConfig['strip_username_regexp']) && $this->backendConfig['strip_username_regexp']) {
+ // No need to silence or log anything here because the pattern has
+ // already been successfully compiled during backend configuration
+ $name = preg_replace($this->backendConfig['strip_username_regexp'], '', $name);
+ }
+
+ return $name;
+ }
+
+ /**
+ * Return the names of all users the user backend currently provides
+ *
+ * @return array
+ */
+ protected function fetchUsers()
+ {
+ try {
+ $query = $this
+ ->createUserBackend()
+ ->select(array('user_name'))
+ ->order('user_name', 'asc', true);
+ if (in_array($this->backendConfig['backend'], array('ldap', 'msldap'))) {
+ /** @var LdapQuery $ldapQuery */
+ $ldapQuery = $query->getQuery();
+ $ldapQuery->setUsePagedResults();
+ }
+
+ return $query->fetchColumn();
+ } catch (Exception $_) {
+ // No need to handle anything special here. Error means no users found.
+ return array();
+ }
+ }
+
+ /**
+ * Return whether the user backend provides a user with the given name
+ *
+ * @param string $username
+ *
+ * @return bool
+ */
+ protected function hasUser($username)
+ {
+ try {
+ return $this
+ ->createUserBackend()
+ ->select()
+ ->where('user_name', $username)
+ ->count() > 1;
+ } catch (Exception $_) {
+ return false;
+ }
+ }
+
+ /**
+ * Create and return the user backend
+ *
+ * @return DbUserBackend|LdapUserBackend
+ */
+ protected function createUserBackend()
+ {
+ $resourceConfig = new Config();
+ $resourceConfig->setSection($this->resourceConfig['name'], $this->resourceConfig);
+ ResourceFactory::setConfig($resourceConfig);
+
+ $config = new ConfigObject($this->backendConfig);
+ $config->resource = $this->resourceConfig['name'];
+ return UserBackend::create(null, $config);
+ }
+
+ /**
+ * Return the names of all user groups the user group backend currently provides
+ *
+ * @return array
+ */
+ protected function fetchGroups()
+ {
+ try {
+ $query = $this
+ ->createUserGroupBackend()
+ ->select(array('group_name'));
+ if (in_array($this->backendConfig['backend'], array('ldap', 'msldap'))) {
+ /** @var LdapQuery $ldapQuery */
+ $ldapQuery = $query->getQuery();
+ $ldapQuery->setUsePagedResults();
+ }
+
+ return $query->fetchColumn();
+ } catch (Exception $_) {
+ // No need to handle anything special here. Error means no groups found.
+ return array();
+ }
+ }
+
+ /**
+ * Return whether the user group backend provides a user group with the given name
+ *
+ * @param string $groupname
+ *
+ * @return bool
+ */
+ protected function hasGroup($groupname)
+ {
+ try {
+ return $this
+ ->createUserGroupBackend()
+ ->select()
+ ->where('group_name', $groupname)
+ ->count() > 1;
+ } catch (Exception $_) {
+ return false;
+ }
+ }
+
+ /**
+ * Create and return the user group backend
+ *
+ * @return LdapUserGroupBackend
+ */
+ protected function createUserGroupBackend()
+ {
+ $resourceConfig = new Config();
+ $resourceConfig->setSection($this->resourceConfig['name'], $this->resourceConfig);
+ ResourceFactory::setConfig($resourceConfig);
+
+ $backendConfig = new Config();
+ $backendConfig->setSection($this->backendConfig['name'], array_merge(
+ $this->backendConfig,
+ array('resource' => $this->resourceConfig['name'])
+ ));
+ UserBackend::setConfig($backendConfig);
+
+ if (empty($this->groupConfig)) {
+ $groupConfig = new ConfigObject(array(
+ 'backend' => $this->backendConfig['backend'], // _Should_ be "db" or "msldap"
+ 'resource' => $this->resourceConfig['name'],
+ 'user_backend' => $this->backendConfig['name'] // Gets ignored if 'backend' is "db"
+ ));
+ } else {
+ $groupConfig = new ConfigObject($this->groupConfig);
+ }
+
+ $backend = UserGroupBackend::create(null, $groupConfig);
+ if (! $backend instanceof Selectable) {
+ throw new NotImplementedError('Unsupported, until #9772 has been resolved');
+ }
+
+ return $backend;
+ }
+}
diff --git a/modules/setup/application/forms/AuthBackendPage.php b/modules/setup/application/forms/AuthBackendPage.php
new file mode 100644
index 0000000..88c77e6
--- /dev/null
+++ b/modules/setup/application/forms/AuthBackendPage.php
@@ -0,0 +1,274 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Application\Config;
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\Config\UserBackendConfigForm;
+use Icinga\Forms\Config\UserBackend\DbBackendForm;
+use Icinga\Forms\Config\UserBackend\LdapBackendForm;
+use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define authentication backend specific details
+ */
+class AuthBackendPage extends Form
+{
+ /**
+ * The resource configuration to use
+ *
+ * @var array
+ */
+ protected $config;
+
+ /**
+ * Default values for the subform's elements suggested by a previous step
+ *
+ * @var string[]
+ */
+ protected $suggestions = array();
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_authentication_backend');
+ $this->setTitle($this->translate('Authentication Backend', 'setup.page.title'));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $resourceConfig = new Config();
+ $resourceConfig->setSection($config['name'], $config);
+ ResourceFactory::setConfig($resourceConfig);
+
+ $this->config = $config;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ $this->addSkipValidationCheckbox();
+ }
+
+ $backendForm = null;
+ if (! isset($this->config) || $this->config['type'] === 'external') {
+ $backendForm = new ExternalBackendForm();
+ $backendForm->create($formData);
+ $this->addDescription($this->translate(
+ 'You\'ve chosen to authenticate using a web server\'s mechanism so it may be necessary'
+ . ' to adjust usernames before any permissions, restrictions, etc. are being applied.'
+ ));
+ } elseif ($this->config['type'] === 'db') {
+ $this->setRequiredCue(null);
+ $backendForm = new DbBackendForm();
+ $backendForm->setRequiredCue(null);
+ $backendForm->create($formData)->removeElement('resource');
+ $this->addDescription($this->translate(
+ 'As you\'ve chosen to use a database for authentication all you need '
+ . 'to do now is defining a name for your first authentication backend.'
+ ));
+ } elseif ($this->config['type'] === 'ldap') {
+ $type = null;
+ if (! isset($formData['type'])) {
+ if (isset($formData['backend'])) {
+ $formData['type'] = $type = $formData['backend'];
+ } elseif (isset($this->suggestions['backend'])) {
+ $formData['type'] = $type = $this->suggestions['backend'];
+ }
+ }
+
+ $backendForm = new LdapBackendForm();
+ $backendForm->setSuggestions($this->suggestions);
+ $backendForm->setResources(array($this->config['name']));
+ $backendForm->create($formData);
+ $backendForm->getElement('resource')->setIgnore(true);
+ $this->addDescription($this->translate(
+ 'Before you are able to authenticate using the LDAP connection defined earlier you need to'
+ . ' provide some more information so that Icinga Web 2 is able to locate account details.'
+ ));
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate(
+ 'The type of the resource being used for this authenticaton provider'
+ ),
+ 'multiOptions' => array(
+ 'ldap' => 'LDAP',
+ 'msldap' => 'ActiveDirectory'
+ ),
+ 'value' => $type
+ )
+ );
+ }
+
+ $backendForm->getElement('name')->setValue('icingaweb2');
+ $this->addSubForm($backendForm, 'backend_form');
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * @param bool $suppressArrayNotation Ignored
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues();
+ $values = array_merge($values, $values['backend_form']);
+ unset($values['backend_form']);
+ return $values;
+ }
+
+ /**
+ * Validate the given form data and check whether it's possible to authenticate using the configured backend
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (! parent::isValid($data)) {
+ return false;
+ }
+
+ if (isset($this->config)) {
+ if ($this->config['type'] === 'ldap' && (
+ ! isset($data['skip_validation']) || $data['skip_validation'] == 0)
+ ) {
+ $self = clone $this;
+ $self->getSubForm('backend_form')->getElement('resource')->setIgnore(false);
+ $inspection = UserBackendConfigForm::inspectUserBackend($self);
+ if ($inspection && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ $self = clone $this;
+ if (($resourceElement = $self->getSubForm('backend_form')->getElement('resource')) !== null) {
+ $resourceElement->setIgnore(false);
+ }
+
+ $inspection = UserBackendConfigForm::inspectUserBackend($self);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_string($e) ? $e : join("\n", array_map($join, $e));
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ } elseif (isset($formData['discovery_btn']) || isset($formData['btn_discover_domain'])) {
+ return parent::isValidPartial($formData);
+ } elseif (! isset($formData['backend_validation'])) {
+ // This is usually done by isValid(Partial), but as we're not calling any of these...
+ $this->populate($formData);
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a checkbox to this form by which the user can skip the authentication validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'required' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate('Check this to not to validate authentication using this backend')
+ )
+ );
+ }
+
+ /**
+ * Get default values for the subform's elements suggested by a previous step
+ *
+ * @return string[]
+ */
+ public function getSuggestions()
+ {
+ return $this->suggestions;
+ }
+
+ /**
+ * Set default values for the subform's elements suggested by a previous step
+ *
+ * @param string[] $suggestions
+ *
+ * @return $this
+ */
+ public function setSuggestions(array $suggestions)
+ {
+ $this->suggestions = $suggestions;
+
+ return $this;
+ }
+}
diff --git a/modules/setup/application/forms/AuthenticationPage.php b/modules/setup/application/forms/AuthenticationPage.php
new file mode 100644
index 0000000..52e3c66
--- /dev/null
+++ b/modules/setup/application/forms/AuthenticationPage.php
@@ -0,0 +1,69 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Authentication\User\ExternalBackend;
+use Icinga\Web\Form;
+use Icinga\Application\Platform;
+
+/**
+ * Wizard page to choose an authentication backend
+ */
+class AuthenticationPage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setRequiredCue(null);
+ $this->setName('setup_authentication_type');
+ $this->setTitle($this->translate('Authentication', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Please choose how you want to authenticate when accessing Icinga Web 2.'
+ . ' Configuring backend specific details follows in a later step.'
+ ));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ if (isset($formData['type']) && $formData['type'] === 'external') {
+ list($username, $_) = ExternalBackend::getRemoteUserInformation();
+ if ($username === null) {
+ $this->info(
+ $this->translate(
+ 'You\'re currently not authenticated using any of the web server\'s authentication '
+ . 'mechanisms. Make sure you\'ll configure such, otherwise you\'ll not be able to '
+ . 'log into Icinga Web 2.'
+ ),
+ false
+ );
+ }
+ }
+
+ $backendTypes = array();
+ if (Platform::hasMysqlSupport() || Platform::hasPostgresqlSupport()) {
+ $backendTypes['db'] = $this->translate('Database');
+ }
+ if (Platform::extensionLoaded('ldap')) {
+ $backendTypes['ldap'] = 'LDAP';
+ }
+ $backendTypes['external'] = $this->translate('External');
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Authentication Type'),
+ 'description' => $this->translate('The type of authentication to use when accessing Icinga Web 2'),
+ 'multiOptions' => $backendTypes
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/forms/DatabaseCreationPage.php b/modules/setup/application/forms/DatabaseCreationPage.php
new file mode 100644
index 0000000..f7092a1
--- /dev/null
+++ b/modules/setup/application/forms/DatabaseCreationPage.php
@@ -0,0 +1,209 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use PDOException;
+use Icinga\Web\Form;
+use Icinga\Module\Setup\Utils\DbTool;
+
+/**
+ * Wizard page to define a database user that is able to create databases and tables
+ */
+class DatabaseCreationPage extends Form
+{
+ /**
+ * The resource configuration to use
+ *
+ * @var array
+ */
+ protected $config;
+
+ /**
+ * The required privileges to setup the database
+ *
+ * @var array
+ */
+ protected $databaseSetupPrivileges;
+
+ /**
+ * The required privileges to operate the database
+ *
+ * @var array
+ */
+ protected $databaseUsagePrivileges;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setTitle($this->translate('Database Setup', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'It seems that either the database you defined earlier does not yet exist and cannot be created'
+ . ' using the provided access credentials, the database does not have the required schema to be'
+ . ' operated by Icinga Web 2 or the provided access credentials do not have the sufficient '
+ . 'permissions to access the database. Please provide appropriate access credentials to solve this.'
+ ));
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ /**
+ * Set the required privileges to setup the database
+ *
+ * @param array $privileges The privileges
+ *
+ * @return $this
+ */
+ public function setDatabaseSetupPrivileges(array $privileges)
+ {
+ $this->databaseSetupPrivileges = $privileges;
+ return $this;
+ }
+
+ /**
+ * Set the required privileges to operate the database
+ *
+ * @param array $privileges The privileges
+ *
+ * @return $this
+ */
+ public function setDatabaseUsagePrivileges(array $privileges)
+ {
+ $this->databaseUsagePrivileges = $privileges;
+ return $this;
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $skipValidation = isset($formData['skip_validation']) && $formData['skip_validation'];
+ $this->addElement(
+ 'text',
+ 'username',
+ array(
+ 'required' => false === $skipValidation,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'A user which is able to create databases and/or touch the database schema'
+ )
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'renderPassword' => true,
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password for the database user defined above'),
+ 'autocomplete' => 'new-password'
+ )
+ );
+
+ if ($skipValidation) {
+ $this->addSkipValidationCheckbox();
+ } else {
+ $this->addElement(
+ 'hidden',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'value' => 0
+ )
+ );
+ }
+ }
+
+ /**
+ * Validate the given form data and check whether the defined user has sufficient access rights
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+
+ if (isset($data['skip_validation']) && $data['skip_validation']) {
+ return true;
+ }
+
+ $config = $this->config;
+ $config['username'] = $this->getValue('username');
+ $config['password'] = $this->getValue('password');
+ $db = new DbTool($config);
+
+ try {
+ $db->connectToDb(); // Are we able to login on the database?
+ } catch (PDOException $_) {
+ try {
+ $db->connectToHost(); // Are we able to login on the server?
+ } catch (PDOException $e) {
+ // We are NOT able to login on the server..
+ $this->error($e->getMessage());
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+ }
+
+ // In case we are connected the credentials filled into this
+ // form need to be granted to create databases, users...
+ if (false === $db->checkPrivileges($this->databaseSetupPrivileges)) {
+ $this->error(
+ $this->translate('The provided credentials cannot be used to create the database and/or the user.')
+ );
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+
+ // ...and to grant all required usage privileges to others
+ if (false === $db->isGrantable($this->databaseUsagePrivileges)) {
+ $this->error(sprintf(
+ $this->translate(
+ 'The provided credentials cannot be used to grant all required privileges to the login "%s".'
+ ),
+ $this->config['username']
+ ));
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the login and privilege validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'required' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this to not to validate the ability to login and required privileges'
+ )
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/forms/DbResourcePage.php b/modules/setup/application/forms/DbResourcePage.php
new file mode 100644
index 0000000..a417710
--- /dev/null
+++ b/modules/setup/application/forms/DbResourcePage.php
@@ -0,0 +1,183 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Exception;
+use Icinga\Web\Form;
+use Icinga\Forms\Config\Resource\DbResourceForm;
+use Icinga\Module\Setup\Utils\DbTool;
+
+/**
+ * Wizard page to define connection details for a database resource
+ */
+class DbResourcePage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setTitle($this->translate('Database Resource', 'setup.page.title'));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'hidden',
+ 'type',
+ array(
+ 'required' => true,
+ 'value' => 'db'
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ $this->addSkipValidationCheckbox();
+ } else {
+ $this->addElement(
+ 'hidden',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'value' => 0
+ )
+ );
+ }
+
+ $resourceForm = new DbResourceForm();
+ $this->addElements($resourceForm->createElements($formData)->getElements());
+ $this->getElement('name')->setValue('icingaweb_db');
+ }
+
+ /**
+ * Validate the given form data and check whether it's possible to connect to the database server
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+
+ if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) {
+ if (! $this->validateConfiguration()) {
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check whether it's possible to connect to the database server
+ *
+ * This will only run the check if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ if (! $this->validateConfiguration()) {
+ return false;
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ } elseif (! isset($formData['backend_validation'])) {
+ // This is usually done by isValid(Partial), but as we're not calling any of these...
+ $this->populate($formData);
+ }
+
+ return true;
+ }
+
+ /**
+ * Return whether the configuration is valid
+ *
+ * @return bool
+ */
+ protected function validateConfiguration()
+ {
+ try {
+ $db = new DbTool($this->getValues());
+ $db->checkConnectivity();
+ } catch (Exception $e) {
+ $this->error(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $e->getMessage()
+ ));
+ return false;
+ }
+
+ $state = true;
+ $connectionError = null;
+
+ try {
+ $db->connectToDb();
+ } catch (Exception $e) {
+ $connectionError = $e;
+ }
+
+ if ($connectionError === null && array_search('icinga_instances', $db->listTables(), true) !== false) {
+ $this->warning($this->translate(
+ 'The database you\'ve configured to use for Icinga Web 2 seems to be the one of Icinga. Please be aware'
+ . ' that this database configuration is supposed to be used for Icinga Web 2\'s configuration and that'
+ . ' it is highly recommended to not mix different schemas in the same database. If this is intentional,'
+ . ' you can skip the validation and ignore this warning. If not, please provide a different database.'
+ ));
+ $state = false;
+ }
+
+ if ($this->getValue('db') === 'pgsql') {
+ if ($connectionError !== null) {
+// $this->warning(sprintf(
+// $this->translate('Unable to check the server\'s version. This is usually not a critical error'
+// . ' as there is probably only access to the database permitted which does not exist yet. If you are'
+// . ' absolutely sure you are running PostgreSQL in a version equal to or newer than 9.1,'
+// . ' you can skip the validation and safely proceed to the next step. The error was: %s'),
+// $connectionError->getMessage()
+// ));
+// $state = false;
+ } else {
+ $version = $db->getServerVersion();
+ if (version_compare($version, '9.1', '<')) {
+ $this->error(sprintf(
+ $this->translate('The server\'s version %s is too old. The minimum required version is %s.'),
+ $version,
+ '9.1'
+ ));
+ $state = false;
+ }
+ }
+ }
+
+ return $state;
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the configuration validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate('Check this to not to validate the configuration')
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/forms/GeneralConfigPage.php b/modules/setup/application/forms/GeneralConfigPage.php
new file mode 100644
index 0000000..5b9f011
--- /dev/null
+++ b/modules/setup/application/forms/GeneralConfigPage.php
@@ -0,0 +1,41 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Forms\Config\General\ApplicationConfigForm;
+use Icinga\Forms\Config\General\LoggingConfigForm;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define the application and logging configuration
+ */
+class GeneralConfigPage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_general_config');
+ $this->setTitle($this->translate('Application Configuration', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Now please adjust all application and logging related configuration options to fit your needs.'
+ ));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $appConfigForm = new ApplicationConfigForm();
+ $appConfigForm->createElements($formData);
+ $appConfigForm->removeElement('global_module_path');
+ $appConfigForm->removeElement('global_config_resource');
+ $this->addElements($appConfigForm->getElements());
+
+ $loggingConfigForm = new LoggingConfigForm();
+ $this->addElements($loggingConfigForm->createElements($formData)->getElements());
+ }
+}
diff --git a/modules/setup/application/forms/LdapDiscoveryConfirmPage.php b/modules/setup/application/forms/LdapDiscoveryConfirmPage.php
new file mode 100644
index 0000000..33bc907
--- /dev/null
+++ b/modules/setup/application/forms/LdapDiscoveryConfirmPage.php
@@ -0,0 +1,133 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define the connection details for a LDAP resource
+ */
+class LdapDiscoveryConfirmPage extends Form
+{
+ const TYPE_AD = 'MS ActiveDirectory';
+ const TYPE_MISC = 'LDAP';
+
+ private $infoTemplate = <<< 'EOT'
+<table><tbody>
+ <tr><td><strong>Type:</strong></td><td>{type}</td></tr>
+ <tr><td><strong>Port:</strong></td><td>{port}</td></tr>
+ <tr><td><strong>Root DN:</strong></td><td>{root_dn}</td></tr>
+ <tr><td><strong>User Object Class:</strong></td><td>{user_class}</td></tr>
+ <tr><td><strong>User Name Attribute:</strong></td><td>{user_attribute}</td></tr>
+</tbody></table>
+EOT;
+
+ /**
+ * The previous configuration
+ *
+ * @var array
+ */
+ private $config;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_ldap_discovery_confirm');
+ $this->setTitle($this->translate('LDAP Discovery Results', 'setup.page.title'));
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ /**
+ * Return the resource configuration as Config object
+ *
+ * @return ConfigObject
+ */
+ public function getResourceConfig()
+ {
+ return new ConfigObject($this->config);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $resource = $this->config['resource'];
+ $backend = $this->config['backend'];
+ $html = $this->infoTemplate;
+ $html = str_replace('{type}', $this->config['type'], $html);
+ $html = str_replace('{hostname}', $resource['hostname'], $html);
+ $html = str_replace('{port}', $resource['port'], $html);
+ $html = str_replace('{root_dn}', $resource['root_dn'], $html);
+ $html = str_replace('{user_attribute}', $backend['user_name_attribute'], $html);
+ $html = str_replace('{user_class}', $backend['user_class'], $html);
+
+ $this->addDescription(sprintf(
+ $this->translate('The following directory service has been found on domain "%s".'),
+ $this->config['domain']
+ ));
+
+ $this->addElement(
+ 'note',
+ 'suggestion',
+ array(
+ 'value' => $html,
+ 'decorators' => array(
+ 'ViewHelper',
+ array(
+ 'HtmlTag', array('tag' => 'div')
+ )
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'confirm',
+ array(
+ 'value' => '1',
+ 'label' => $this->translate('Use this configuration?')
+ )
+ );
+ }
+
+ /**
+ * Validate the given form data and check whether a BIND-request is successful
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+ return true;
+ }
+
+ public function getValues($suppressArrayNotation = false)
+ {
+ if ($this->getValue('confirm') === '1') {
+ // use configuration
+ return $this->config;
+ }
+ return null;
+ }
+}
diff --git a/modules/setup/application/forms/LdapDiscoveryPage.php b/modules/setup/application/forms/LdapDiscoveryPage.php
new file mode 100644
index 0000000..7b5de17
--- /dev/null
+++ b/modules/setup/application/forms/LdapDiscoveryPage.php
@@ -0,0 +1,115 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Exception;
+use Zend_Validate_NotEmpty;
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Form;
+use Icinga\Web\Form\ErrorLabeller;
+use Icinga\Forms\LdapDiscoveryForm;
+use Icinga\Protocol\Ldap\Discovery;
+use Icinga\Module\Setup\Forms\LdapDiscoveryConfirmPage;
+
+/**
+ * Wizard page to define the connection details for a LDAP resource
+ */
+class LdapDiscoveryPage extends Form
+{
+ /**
+ * @var Discovery
+ */
+ private $discovery;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_ldap_discovery');
+ $this->setTitle($this->translate('LDAP Discovery', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'You can use this page to discover LDAP or ActiveDirectory servers ' .
+ ' for authentication. If you don\'t want to execute a discovery, just skip this step.'
+ ));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $discoveryForm = new LdapDiscoveryForm();
+ $this->addElements($discoveryForm->createElements($formData)->getElements());
+
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'label' => $this->translate('Skip'),
+ 'description' => $this->translate('Do not discover LDAP servers and enter all settings manually.')
+ )
+ );
+ }
+
+ /**
+ * Validate the given form data and check whether a BIND-request is successful
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+ if (isset($data['skip_validation']) && $data['skip_validation']) {
+ return true;
+ }
+
+ if (isset($data['domain']) && $data['domain']) {
+ try {
+ $this->discovery = Discovery::discoverDomain($data['domain']);
+ if ($this->discovery->isSuccess()) {
+ return true;
+ } else {
+ $this->error($this->discovery->getError()->getMessage());
+ }
+ } catch (Exception $e) {
+ $this->error(sprintf(
+ $this->translate('Could not find any LDAP servers on the domain "%s". An error occurred: %s'),
+ $data['domain'],
+ IcingaException::describe($e)
+ ));
+ }
+ } else {
+ $labeller = new ErrorLabeller(array('element' => $this->getElement('domain')));
+ $this->getElement('domain')->addError($labeller->translate(Zend_Validate_NotEmpty::IS_EMPTY));
+ }
+
+ return false;
+ }
+
+ /**
+ * Suggest settings based on the underlying discovery
+ *
+ * @param bool $suppressArrayNotation
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ if (! isset($this->discovery) || ! $this->discovery->isSuccess()) {
+ return [];
+ }
+ $disc = $this->discovery;
+ return array(
+ 'domain' => $this->getValue('domain'),
+ 'type' => $disc->isAd() ? LdapDiscoveryConfirmPage::TYPE_AD : LdapDiscoveryConfirmPage::TYPE_MISC,
+ 'resource' => $disc->suggestResourceSettings(),
+ 'backend' => $disc->suggestBackendSettings()
+ );
+ }
+}
diff --git a/modules/setup/application/forms/LdapResourcePage.php b/modules/setup/application/forms/LdapResourcePage.php
new file mode 100644
index 0000000..7786407
--- /dev/null
+++ b/modules/setup/application/forms/LdapResourcePage.php
@@ -0,0 +1,152 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Web\Form;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Forms\Config\Resource\LdapResourceForm;
+
+/**
+ * Wizard page to define the connection details for a LDAP resource
+ */
+class LdapResourcePage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_ldap_resource');
+ $this->setTitle($this->translate('LDAP Resource', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Now please configure your AD/LDAP resource. This will later '
+ . 'be used to authenticate users logging in to Icinga Web 2.'
+ ));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'hidden',
+ 'type',
+ array(
+ 'required' => true,
+ 'value' => 'ldap'
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ $this->addSkipValidationCheckbox();
+ } else {
+ $this->addElement(
+ 'hidden',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'value' => 0
+ )
+ );
+ }
+
+ $resourceForm = new LdapResourceForm();
+ $this->addElements($resourceForm->createElements($formData)->getElements());
+ $this->getElement('name')->setValue('icingaweb_ldap');
+ }
+
+ /**
+ * Validate the given form data and check whether a BIND-request is successful
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (! parent::isValid($data)) {
+ return false;
+ }
+
+ if (! isset($data['skip_validation']) || $data['skip_validation'] == 0) {
+ $inspection = ResourceConfigForm::inspectResource($this);
+ if ($inspection !== null && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ $inspection = ResourceConfigForm::inspectResource($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_string($e) ? $e : join("\n", array_map($join, $e));
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ } elseif (! isset($formData['backend_validation'])) {
+ // This is usually done by isValid(Partial), but as we're not calling any of these...
+ $this->populate($formData);
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the connection validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this to not to validate connectivity with the given directory service'
+ )
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/forms/ModulePage.php b/modules/setup/application/forms/ModulePage.php
new file mode 100644
index 0000000..d62b5a9
--- /dev/null
+++ b/modules/setup/application/forms/ModulePage.php
@@ -0,0 +1,108 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Web\Form;
+
+class ModulePage extends Form
+{
+ protected $modules;
+
+ protected $modulePaths;
+
+ protected $foundIcingaDB = false;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_modules');
+ $this->setViewScript('form/setup-modules.phtml');
+
+ $this->modulePaths = array();
+ if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) {
+ $this->modulePaths[] = $appModulePath;
+ }
+ }
+
+ public function createElements(array $formData)
+ {
+ foreach ($this->getModules() as $module) {
+ $checked = false;
+ if ($module->getName() === 'monitoring') {
+ $checked = ! $this->foundIcingaDB;
+ } elseif ($this->foundIcingaDB && $module->getName() === 'icingadb') {
+ $checked = true;
+ }
+
+ $this->addElement(
+ 'checkbox',
+ $module->getName(),
+ array(
+ 'description' => $module->getDescription(),
+ 'label' => ucfirst($module->getName()),
+ 'value' => (int) $checked,
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ }
+ }
+
+ /**
+ * @return Module[]
+ */
+ protected function getModules()
+ {
+ if ($this->modules !== null) {
+ return $this->modules;
+ } else {
+ $this->modules = array();
+ }
+
+ $moduleManager = Icinga::app()->getModuleManager();
+ $moduleManager->detectInstalledModules($this->modulePaths);
+ foreach ($moduleManager->listInstalledModules() as $moduleName) {
+ if ($moduleName !== 'setup') {
+ $this->modules[$moduleName] = $moduleManager->loadModule($moduleName)->getModule($moduleName);
+ }
+
+ if ($moduleName === 'icingadb') {
+ $this->foundIcingaDB = true;
+ }
+ }
+
+ return $this->modules;
+ }
+
+ public function getCheckedModules()
+ {
+ $modules = $this->getModules();
+
+ $checked = array();
+ foreach ($this->getElements() as $name => $element) {
+ if (array_key_exists($name, $modules) && $element->isChecked()) {
+ $checked[$name] = $modules[$name];
+ }
+ }
+
+ return $checked;
+ }
+
+ public function getModuleWizards()
+ {
+ $checked = $this->getCheckedModules();
+
+ $wizards = array();
+ foreach ($checked as $name => $module) {
+ if ($module->providesSetupWizard()) {
+ $wizards[$name] = $module->getSetupWizard();
+ }
+ }
+
+ return $wizards;
+ }
+}
diff --git a/modules/setup/application/forms/RequirementsPage.php b/modules/setup/application/forms/RequirementsPage.php
new file mode 100644
index 0000000..d1fb70e
--- /dev/null
+++ b/modules/setup/application/forms/RequirementsPage.php
@@ -0,0 +1,68 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Web\Form;
+use Icinga\Module\Setup\SetupWizard;
+
+/**
+ * Wizard page to list setup requirements
+ */
+class RequirementsPage extends Form
+{
+ /**
+ * The wizard
+ *
+ * @var SetupWizard
+ */
+ protected $wizard;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_requirements');
+ $this->setViewScript('form/setup-requirements.phtml');
+ }
+
+ /**
+ * Set the wizard
+ *
+ * @param SetupWizard $wizard
+ *
+ * @return $this
+ */
+ public function setWizard(SetupWizard $wizard)
+ {
+ $this->wizard = $wizard;
+ return $this;
+ }
+
+ /**
+ * Return the wizard
+ *
+ * @return SetupWizard
+ */
+ public function getWizard()
+ {
+ return $this->wizard;
+ }
+
+ /**
+ * Validate the given form data and check whether the wizard's requirements are fulfilled
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+
+ return $this->wizard->getRequirements()->fulfilled();
+ }
+}
diff --git a/modules/setup/application/forms/SummaryPage.php b/modules/setup/application/forms/SummaryPage.php
new file mode 100644
index 0000000..ab62d55
--- /dev/null
+++ b/modules/setup/application/forms/SummaryPage.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use LogicException;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page that displays a summary of what is going to be "done"
+ */
+class SummaryPage extends Form
+{
+ /**
+ * The title of what is being set up
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * The summary to show
+ *
+ * @var array
+ */
+ protected $summary;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ if ($this->getName() === $this->filterName(get_class($this))) {
+ throw new LogicException(
+ 'When utilizing ' . get_class($this) . ' it is required to set a unique name by using the form options'
+ );
+ }
+
+ $this->setViewScript('form/setup-summary.phtml');
+ }
+
+ /**
+ * Set the title of what is being set up
+ *
+ * @param string $title
+ */
+ public function setSubjectTitle($title)
+ {
+ $this->title = $title;
+ }
+
+ /**
+ * Return the title of what is being set up
+ *
+ * @return string
+ */
+ public function getSubjectTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Set the summary to show
+ *
+ * @param array $summary
+ *
+ * @return $this
+ */
+ public function setSummary(array $summary)
+ {
+ $this->summary = $summary;
+ return $this;
+ }
+
+ /**
+ * Return the summary to show
+ *
+ * @return array
+ */
+ public function getSummary()
+ {
+ return $this->summary;
+ }
+}
diff --git a/modules/setup/application/forms/UserGroupBackendPage.php b/modules/setup/application/forms/UserGroupBackendPage.php
new file mode 100644
index 0000000..751270f
--- /dev/null
+++ b/modules/setup/application/forms/UserGroupBackendPage.php
@@ -0,0 +1,147 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\Config\UserGroup\LdapUserGroupBackendForm;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define user group backend specific details
+ */
+class UserGroupBackendPage extends Form
+{
+ /**
+ * The resource configuration to use
+ *
+ * @var array
+ */
+ protected $resourceConfig;
+
+ /**
+ * The user backend configuration to use
+ *
+ * @var array
+ */
+ protected $backendConfig;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_usergroup_backend');
+ $this->setTitle($this->translate('User Group Backend', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'To allow Icinga Web 2 to associate users and groups, you\'ll need to provide some further information'
+ . ' about the LDAP Connection that is already going to be used to locate account details.'
+ ));
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $this->resourceConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Set the user backend configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setBackendConfig(array $config)
+ {
+ $this->backendConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the resource configuration as Config object
+ *
+ * @return Config
+ */
+ protected function createResourceConfiguration()
+ {
+ $config = new Config();
+ $config->setSection($this->resourceConfig['name'], $this->resourceConfig);
+ return $config;
+ }
+
+ /**
+ * Return the user backend configuration as Config object
+ *
+ * @return Config
+ */
+ protected function createBackendConfiguration()
+ {
+ $config = new Config();
+ $backendConfig = $this->backendConfig;
+ $backendConfig['resource'] = $this->resourceConfig['name'];
+ $config->setSection($this->backendConfig['name'], $backendConfig);
+ return $config;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ // LdapUserGroupBackendForm requires these factories to provide valid configurations
+ ResourceFactory::setConfig($this->createResourceConfiguration());
+ UserBackend::setConfig($this->createBackendConfiguration());
+
+ $backendForm = new LdapUserGroupBackendForm();
+ $formData['type'] = 'ldap';
+ $backendForm->create($formData);
+ $backendForm->getElement('name')->setValue('icingaweb2');
+ $this->addSubForm($backendForm, 'backend_form');
+
+ $backendForm->addElement(
+ 'hidden',
+ 'resource',
+ array(
+ 'required' => true,
+ 'value' => $this->resourceConfig['name'],
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $backendForm->addElement(
+ 'hidden',
+ 'user_backend',
+ array(
+ 'required' => true,
+ 'value' => $this->backendConfig['name'],
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * @param bool $suppressArrayNotation Ignored
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues();
+ $values = array_merge($values, $values['backend_form']);
+ unset($values['backend_form']);
+ return $values;
+ }
+}
diff --git a/modules/setup/application/forms/WelcomePage.php b/modules/setup/application/forms/WelcomePage.php
new file mode 100644
index 0000000..124a31f
--- /dev/null
+++ b/modules/setup/application/forms/WelcomePage.php
@@ -0,0 +1,45 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Form;
+use Icinga\Module\Setup\Web\Form\Validator\TokenValidator;
+
+/**
+ * Wizard page to authenticate and welcome the user
+ */
+class WelcomePage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setRequiredCue(null);
+ $this->setName('setup_welcome');
+ $this->setViewScript('form/setup-welcome.phtml');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'token',
+ array(
+ 'class' => 'autofocus',
+ 'required' => true,
+ 'label' => $this->translate('Setup Token'),
+ 'description' => $this->translate(
+ 'For security reasons we need to ensure that you are permitted to run this wizard.'
+ . ' Please provide a token by following the instructions below.'
+ ),
+ 'validators' => array(new TokenValidator(Icinga::app()->getConfigDir() . '/setup.token'))
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/views/scripts/form/setup-modules.phtml b/modules/setup/application/views/scripts/form/setup-modules.phtml
new file mode 100644
index 0000000..e57c7dc
--- /dev/null
+++ b/modules/setup/application/views/scripts/form/setup-modules.phtml
@@ -0,0 +1,33 @@
+<?php
+
+use Icinga\Web\Wizard;
+
+?>
+<form
+ id="<?= $this->escape($form->getName()); ?>"
+ name="<?= $this->escape($form->getName()); ?>"
+ enctype="<?= $this->escape($form->getEncType()); ?>"
+ method="<?= $this->escape($form->getMethod()); ?>"
+ action="<?= $this->escape($form->getAction()); ?>"
+ class="icinga-controls"
+ data-progress-element="<?= Wizard::PROGRESS_ELEMENT; ?>"
+>
+<h2><?= $this->translate('Modules', 'setup.page.title'); ?></h2>
+<p><?= $this->translate('The following modules were found in your Icinga Web 2 installation. To enable and configure a module, just tick it and click "Next".'); ?></p>
+<?php foreach ($form->getElements() as $element): ?>
+ <?php if (! in_array($element->getName(), array(Wizard::BTN_PREV, Wizard::BTN_NEXT, Wizard::PROGRESS_ELEMENT, $form->getTokenElementName(), $form->getUidElementName()))): ?>
+ <div class="module">
+ <div class="header">
+ <h3><label for="<?= $element->getId(); ?>"><strong><?= $element->getLabel(); ?></strong></label></h3>
+ <div class="element">
+ <?= $element; ?>
+ </div>
+ </div>
+ <label class="description" for="<?= $element->getId(); ?>"><?= $element->getDescription(); ?></label>
+ </div>
+ <?php endif ?>
+<?php endforeach ?>
+ <?= $form->getElement($form->getTokenElementName()); ?>
+ <?= $form->getElement($form->getUidElementName()); ?>
+ <?= $form->getDisplayGroup('buttons'); ?>
+</form>
diff --git a/modules/setup/application/views/scripts/form/setup-requirements.phtml b/modules/setup/application/views/scripts/form/setup-requirements.phtml
new file mode 100644
index 0000000..544f284
--- /dev/null
+++ b/modules/setup/application/views/scripts/form/setup-requirements.phtml
@@ -0,0 +1,48 @@
+<?php
+
+use Icinga\Web\Wizard;
+
+if (! $form->getWizard()->getRequirements()->fulfilled()) {
+ $form->getElement(Wizard::BTN_NEXT)->setAttrib('disabled', 1);
+}
+
+?>
+<h1>Icinga Web 2</h1>
+<?= $form->getWizard()->getRequirements(true); ?>
+<?php foreach ($form->getWizard()->getPage('setup_modules')->getModuleWizards() as $moduleName => $wizard): ?>
+<h1><?= ucwords($moduleName) . ' ' . $this->translate('Module'); ?></h1>
+<?= $wizard->getRequirements(); ?>
+<?php endforeach ?>
+<form
+ id="<?= $this->escape($form->getName()); ?>"
+ name="<?= $this->escape($form->getName()); ?>"
+ enctype="<?= $this->escape($form->getEncType()); ?>"
+ method="<?= $this->escape($form->getMethod()); ?>"
+ action="<?= $this->escape($form->getAction()); ?>"
+ data-progress-element="<?= Wizard::PROGRESS_ELEMENT; ?>"
+>
+ <?= $form->getElement($form->getTokenElementName()); ?>
+ <?= $form->getElement($form->getUidElementName()); ?>
+ <div class="buttons">
+ <?php
+ $double = clone $form->getElement(Wizard::BTN_NEXT);
+ echo $double->setAttrib('class', 'double');
+ ?>
+ <?= $form->getElement(Wizard::BTN_PREV); ?>
+ <?= $form->getElement(Wizard::BTN_NEXT); ?>
+ <?= $form->getElement(Wizard::PROGRESS_ELEMENT); ?>
+ <div class="requirements-refresh">
+ <?php $title = $this->translate('You may also need to restart the web-server for the changes to take effect!'); ?>
+ <?= $this->qlink(
+ $this->translate('Refresh'),
+ null,
+ null,
+ array(
+ 'class' => 'button-link',
+ 'title' => $title,
+ 'aria-label' => sprintf($this->translate('Refresh the page; %s'), $title)
+ )
+ ); ?>
+ </div>
+ </div>
+</form> \ No newline at end of file
diff --git a/modules/setup/application/views/scripts/form/setup-summary.phtml b/modules/setup/application/views/scripts/form/setup-summary.phtml
new file mode 100644
index 0000000..3ad0265
--- /dev/null
+++ b/modules/setup/application/views/scripts/form/setup-summary.phtml
@@ -0,0 +1,40 @@
+<?php
+
+use Icinga\Web\Wizard;
+
+$form->getElement(Wizard::BTN_NEXT)->setAttrib(
+ 'class',
+ $form->getElement(Wizard::BTN_NEXT)->getAttrib('class') . ' finish'
+);
+
+?>
+<p><?= sprintf(
+ $this->translate(
+ 'You\'ve configured %1$s successfully. You can review the changes supposed to be made before setting it up.'
+ . ' Make sure that everything is correct (Feel free to navigate back to make any corrections!) so'
+ . ' that you can start using %1$s right after it has successfully been set up.'
+ ),
+ $form->getSubjectTitle()
+); ?></p>
+<div class="summary">
+<?php foreach ($form->getSummary() as $pageHtml): ?>
+ <?php if ($pageHtml): ?>
+ <div class="page">
+ <?= $pageHtml; ?>
+ </div>
+ <?php endif ?>
+<?php endforeach ?>
+</div>
+<form
+ id="<?= $this->escape($form->getName()); ?>"
+ name="<?= $this->escape($form->getName()); ?>"
+ enctype="<?= $this->escape($form->getEncType()); ?>"
+ method="<?= $this->escape($form->getMethod()); ?>"
+ action="<?= $this->escape($form->getAction()); ?>"
+ data-progress-element="<?= Wizard::PROGRESS_ELEMENT; ?>"
+ class="summary"
+>
+ <?= $form->getElement($form->getTokenElementName()); ?>
+ <?= $form->getElement($form->getUidElementName()); ?>
+ <?= $form->getDisplayGroup('buttons'); ?>
+</form> \ No newline at end of file
diff --git a/modules/setup/application/views/scripts/form/setup-welcome.phtml b/modules/setup/application/views/scripts/form/setup-welcome.phtml
new file mode 100644
index 0000000..1be68f3
--- /dev/null
+++ b/modules/setup/application/views/scripts/form/setup-welcome.phtml
@@ -0,0 +1,120 @@
+<?php
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Config;
+use Icinga\Application\Platform;
+use Icinga\Web\Wizard;
+
+$phpUser = Platform::getPhpUser();
+$configDir = Icinga::app()->getConfigDir();
+$setupTokenPath = rtrim($configDir, '/') . '/setup.token';
+$cliPath = realpath(Icinga::app()->getApplicationDir() . '/../bin/icingacli');
+
+$groupadd = null;
+$docker = getenv('ICINGAWEB_OFFICIAL_DOCKER_IMAGE');
+
+if (! (false === ($distro = Platform::getLinuxDistro(1)) || $distro === 'linux')) {
+ foreach (array(
+ 'groupadd -r icingaweb2' => array(
+ 'redhat', 'rhel', 'centos', 'fedora',
+ 'suse', 'sles', 'sled', 'opensuse'
+ ),
+ 'addgroup --system icingaweb2' => array('debian', 'ubuntu')
+ ) as $groupadd_ => $distros) {
+ if (in_array($distro, $distros)) {
+ $groupadd = $groupadd_;
+ break;
+ }
+ }
+
+ switch ($distro) {
+ case 'redhat':
+ case 'rhel':
+ case 'centos':
+ case 'fedora':
+ $usermod = 'usermod -a -G icingaweb2 %s';
+ $webSrvUser = 'apache';
+ break;
+ case 'suse':
+ case 'sles':
+ case 'sled':
+ case 'opensuse':
+ $usermod = 'usermod -A icingaweb2 %s';
+ $webSrvUser = 'wwwrun';
+ break;
+ case 'debian':
+ case 'ubuntu':
+ $usermod = 'usermod -a -G icingaweb2 %s';
+ $webSrvUser = 'www-data';
+ break;
+ default:
+ $usermod = $webSrvUser = null;
+ }
+}
+?>
+<div class="welcome-page">
+ <h2><?= $this->translate('Welcome to the configuration of Icinga Web 2!') ?></h2>
+ <?php if (false === file_exists($setupTokenPath) && file_exists(Config::resolvePath('config.ini'))): ?>
+ <p class="restart-warning"><?= $this->translate(
+ 'You\'ve already completed the configuration of Icinga Web 2. Note that most of your configuration'
+ . ' files will be overwritten in case you\'ll re-configure Icinga Web 2 using this wizard!'
+ ); ?></p>
+ <?php else: ?>
+ <p><?= $this->translate(
+ 'This wizard will guide you through the configuration of Icinga Web 2. Once completed and successfully'
+ . ' finished you are able to log in and to explore all the new and stunning features!'
+ ); ?></p>
+ <?php endif ?>
+ <form id="<?= $form->getName(); ?>" name="<?= $form->getName(); ?>" enctype="<?= $form->getEncType(); ?>" method="<?= $form->getMethod(); ?>" action="<?= $form->getAction(); ?>" class="icinga-controls">
+ <?= $form->getElement('token'); ?>
+ <?= $form->getElement($form->getTokenElementName()); ?>
+ <?= $form->getElement($form->getUidElementName()); ?>
+ <div class="buttons">
+ <?= $form->getElement(Wizard::BTN_NEXT); ?>
+ </div>
+ </form>
+ <div class="note">
+ <h3><?= $this->translate('Generating a New Setup Token'); ?></h3>
+ <div>
+ <p><?=
+ $this->translate(
+ 'To run this wizard a user needs to authenticate using a token which is usually'
+ . ' provided to him by an administrator who\'d followed the instructions below.'
+ ); ?></p>
+ <?php if (! $docker): ?>
+ <p><?= $this->translate('In any case, make sure that all of the following applies to your environment:'); ?></p>
+ <ul>
+ <li><?= $this->translate('A system group called "icingaweb2" exists'); ?></li>
+ <?php if ($phpUser): ?>
+ <li><?= sprintf($this->translate('The user "%s" is a member of the system group "icingaweb2"'), $phpUser); ?></li>
+ <?php else: ?>
+ <li><?= $this->translate('Your webserver\'s user is a member of the system group "icingaweb2"'); ?></li>
+ <?php endif ?>
+ </ul>
+ <?php if (! ($groupadd === null || $usermod === null)) { ?>
+ <div class="code">
+ <span><?= $this->escape($groupadd . ';') ?></span>
+ <span><?= $this->escape(sprintf($usermod, $phpUser ?: $webSrvUser) . ';') ?></span>
+ </div>
+ <?php } ?>
+ <p><?= $this->translate('If you\'ve got the IcingaCLI installed you can do the following:'); ?></p>
+ <?php endif; ?>
+ <div class="code">
+ <?php if (! $docker): ?>
+ <span><?= $cliPath ? $cliPath : 'icingacli'; ?> setup config directory --group icingaweb2<?= $configDir !== '/etc/icingaweb2' ? ' --config ' . $configDir : ''; ?>;</span>
+ <?php endif; ?>
+ <span><?= $cliPath ? $cliPath : 'icingacli'; ?> setup token create;</span>
+ </div>
+ <?php if (! $docker): ?>
+ <p><?= $this->translate('In case the IcingaCLI is missing you can create the token manually:'); ?></p>
+ <div class="code">
+ <span>su <?= $phpUser ?: $this->translate('<your-webserver-user>'); ?> -s /bin/sh -c "mkdir -m 2770 <?= dirname($setupTokenPath); ?>; chgrp icingaweb2 <?= dirname($setupTokenPath); ?>; head -c 12 /dev/urandom | base64 | tee <?= $setupTokenPath; ?>; chmod 0660 <?= $setupTokenPath; ?>;";</span>
+ </div>
+ <?php endif; ?>
+ <p><?= sprintf(
+ $this->translate('Please see the %s for an extensive description on how to access and use this wizard.'),
+ '<a href="http://docs.icinga.com/">' . $this->translate('Icinga Web 2 documentation') . '</a>' // TODO: Add link to iw2 docs which points to the installation topic
+ ); ?></p>
+ </div>
+ </div>
+</div>
diff --git a/modules/setup/application/views/scripts/index/index.phtml b/modules/setup/application/views/scripts/index/index.phtml
new file mode 100644
index 0000000..32952e7
--- /dev/null
+++ b/modules/setup/application/views/scripts/index/index.phtml
@@ -0,0 +1,224 @@
+<?php
+
+use Icinga\Util\Csp;
+use Icinga\Web\Notification;
+use ipl\Web\Style;
+
+$pages = $wizard->getPages();
+$finished = isset($success);
+$configPages = array_slice($pages, 3, count($pages) - 4, true);
+$currentPos = array_search($wizard->getCurrentPage(), $pages, true);
+list($configPagesLeft, $configPagesRight) = array_chunk($configPages, (int)(count($configPages) / 2), true);
+$setupStyle = (new Style())
+ ->setSelector('.setup-header > .progress-bar')
+ ->setNonce(Csp::getStyleNonce());
+
+$visitedPages = array_keys($wizard->getPageData());
+$maxProgress = max(array_merge([0], array_keys(array_filter(
+ $pages,
+ function ($page) use ($visitedPages) { return in_array($page->getName(), $visitedPages); }
+))));
+
+$setupStyle->add(
+ '.width-percent-10',
+ ['width' => '10%']
+)->add(
+ '.width-percent-60',
+ ['width' => '60%']
+);
+?>
+<div id="setup-content-wrapper" data-base-target="layout">
+ <div class="setup-header">
+ <?= $this->img('img/icinga-logo-big.png'); ?>
+ <div class="progress-bar">
+ <div class="step width-percent-10">
+ <h1><?= $this->translate('Welcome', 'setup.progress'); ?></h1>
+ <?php $stateClass = $finished || $currentPos > 0 ? 'complete' : (
+ $maxProgress > 0 ? 'visited' : 'active'
+ ); ?>
+ <table><tbody><tr>
+ <td class="left"></td>
+ <td class="middle"><div class="bubble <?= $stateClass; ?>"></div></td>
+ <td class="right"><div class="line right <?= $stateClass; ?>"></div></td>
+ </tr></tbody></table>
+ </div>
+ <div class="step width-percent-10">
+ <h1><?= $this->translate('Modules', 'setup.progress'); ?></h1>
+ <?php $stateClass = $finished || $currentPos > 1 ? ' complete' : (
+ $maxProgress > 1 ? ' visited' : (
+ $currentPos === 1 ? ' active' : ''
+ )
+ ); ?>
+ <table><tbody><tr>
+ <td class="left"><div class="line left<?= $stateClass; ?>"></div></td>
+ <td class="middle"><div class="bubble <?= $stateClass; ?>"></div></td>
+ <td class="right"><div class="line right <?= $stateClass; ?>"></div></td>
+ </tr></tbody></table>
+ <?php if (($maxProgress < $currentPos && $currentPos === 1) || ($maxProgress >= $currentPos && $maxProgress === 1)): ?>
+ <?= $this->restartForm ?>
+ <?php endif ?>
+ </div>
+ <div class="step width-percent-10">
+ <h1><?= $this->translate('Requirements', 'setup.progress'); ?></h1>
+ <?php $stateClass = $finished || $currentPos > 2 ? ' complete' : (
+ $maxProgress > 2 ? ' visited' : (
+ $currentPos === 2 ? ' active' : ''
+ )
+ ); ?>
+ <table><tbody><tr>
+ <td class="left"><div class="line left<?= $stateClass; ?>"></div></td>
+ <td class="middle"><div class="bubble<?= $stateClass; ?>"></div></td>
+ <td class="right"><div class="line right<?= $stateClass; ?>"></div></td>
+ </tr></tbody></table>
+ <?php if (($maxProgress < $currentPos && $currentPos === 2) || ($maxProgress >= $currentPos && $maxProgress === 2)): ?>
+ <?= $this->restartForm ?>
+ <?php endif ?>
+ </div>
+ <div class="step width-percent-60">
+ <h1><?= $this->translate('Configuration', 'setup.progress'); ?></h1>
+ <table><tbody><tr>
+ <td class="left">
+ <?php
+ $firstPage = current($configPagesLeft);
+ $lastPage = end($configPagesLeft);
+ $lineWidth = sprintf('%.2F', round(100 / count($configPagesLeft), 2, PHP_ROUND_HALF_DOWN));
+ ?>
+ <?php foreach ($configPagesLeft as $pos => $page): ?>
+ <?php $stateClass = $finished || $pos < $currentPos ? ' complete' : (
+ $pos < $maxProgress ? ' visited' : ($currentPos > 2 ? ' active' : '')
+ ); ?>
+ <?php if ($page === $firstPage): ?>
+ <?php
+ $setupStyle->add(
+ '.step .left-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => sprintf(
+ '%.2F%%',
+ 100 - (count($configPagesLeft) - 1) * $lineWidth
+ ),
+ 'margin-right' => 0
+ ]
+ );
+ ?>
+ <div class="line left<?= $stateClass; ?> left-line-<?= $pos; ?>"></div>
+ <?php elseif ($page === $lastPage): ?>
+ <?php
+ $setupStyle->add(
+ '.step .left-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => $lineWidth . '%',
+ 'margin-right' => '-0.1em'
+ ]
+ );
+ ?>
+ <div class="line<?= $stateClass; ?> left-line-<?= $pos; ?>"></div>
+ <?php else: ?>
+ <?php
+ $setupStyle->add(
+ '.step .left-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => $lineWidth . '%'
+ ]
+ );
+ ?>
+ <div class="line<?= $stateClass; ?> left-line-<?= $pos; ?>"></div>
+ <?php endif ?>
+ <?php endforeach ?>
+ </td>
+ <td class="middle">
+ <div class="bubble<?= array_key_exists($currentPos, $configPagesLeft) ? (
+ key($configPagesRight) <= $maxProgress ? ' visited' : ' active') : (
+ $finished || $currentPos > 2 ? ' complete' : (
+ key($configPagesRight) < $maxProgress ? ' visited' : ''
+ )
+ ); ?>"></div>
+ </td>
+ <td class="right">
+ <?php
+ $firstPage = current($configPagesRight);
+ $lastPage = end($configPagesRight);
+ $lineWidth = sprintf('%.2F', round(100 / count($configPagesRight), 2, PHP_ROUND_HALF_DOWN));
+ ?>
+ <?php foreach ($configPagesRight as $pos => $page): ?>
+ <?php $stateClass = $finished || $pos < $currentPos ? ' complete' : (
+ $pos < $maxProgress ? ' visited' : ($currentPos > 2 ? ' active' : '')
+ ); ?>
+ <?php if ($page === $firstPage): ?>
+ <?php
+ $setupStyle->add(
+ '.step .right-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => $lineWidth . '%',
+ 'margin-right' => '-0.1em'
+ ]
+ );
+ ?>
+ <div class="line<?= $stateClass; ?> right-line-<?= $pos; ?>"></div>
+ <?php elseif ($page === $lastPage): ?>
+ <?php
+ $setupStyle->add(
+ '.step .right-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => sprintf(
+ '%.2F%%',
+ 100 - (count($configPagesRight) - 1) * $lineWidth
+ ),
+ 'margin-right' => 0
+ ]
+ );
+ ?>
+ <div class="line right<?= $stateClass; ?> right-line-<?= $pos; ?>"></div>
+ <?php else: ?>
+ <?php
+ $setupStyle->add(
+ '.step .right-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => $lineWidth . '%'
+ ]
+ );
+ ?>
+ <div class="line<?= $stateClass; ?> right-line-<?= $pos; ?>"></div>
+ <?php endif ?>
+ <?php endforeach ?>
+ </td>
+ </tr></tbody></table>
+ <?php if ($maxProgress > 2 || $currentPos > 2): ?>
+ <?= $this->restartForm ?>
+ <?php endif ?>
+ </div>
+ <div class="step width-percent-10">
+ <h1><?= $this->translate('Finish', 'setup.progress'); ?></h1>
+ <?php $stateClass = $finished ? ' complete' : ($pages[$currentPos] === end($pages) ? ' active' : ''); ?>
+ <table><tbody><tr>
+ <td class="left"><div class="line left<?= $stateClass; ?>"></div></td>
+ <td class="middle"><div class="bubble<?= $stateClass; ?>"></div></td>
+ <td class="right"></td>
+ </tr></tbody></table>
+ </div>
+ </div>
+ </div>
+ <div class="setup-content">
+<?php if ($finished): ?>
+ <?= $this->render('index/parts/finish.phtml'); ?>
+<?php else: ?>
+ <?= $this->render('index/parts/wizard.phtml'); ?>
+<?php endif ?>
+ </div>
+</div>
+<div id="footer">
+ <ul role="alert" id="notifications"><?php
+ $notifications = Notification::getInstance();
+ if ($notifications->hasMessages()) {
+ foreach ($notifications->popMessages() as $m) {
+ echo '<li class="' . $m->type . '">' . $this->escape($m->message) . '</li>';
+ }
+ }
+ ?></ul>
+</div>
+<?= $setupStyle; ?>
diff --git a/modules/setup/application/views/scripts/index/parts/finish.phtml b/modules/setup/application/views/scripts/index/parts/finish.phtml
new file mode 100644
index 0000000..dcb34dc
--- /dev/null
+++ b/modules/setup/application/views/scripts/index/parts/finish.phtml
@@ -0,0 +1,34 @@
+<div id="setup-finish">
+ <?php if ($success): ?>
+ <h2 class="success"><?= $this->translate('Congratulations! Icinga Web 2 has been successfully set up.'); ?></h2>
+ <?php else: ?>
+ <h2 class="failure"><?= $this->translate('Sorry! Failed to set up Icinga Web 2 successfully.'); ?></h2>
+ <?php endif ?>
+ <div class="buttons pull-right">
+ <?php if ($success): ?>
+ <?= $this->qlink(
+ $this->translate('Login to Icinga Web 2'),
+ 'authentication/login',
+ null,
+ array(
+ 'class' => 'button-link login',
+ 'data-no-icinga-ajax' => true,
+ 'title' => $this->translate('Show the login page of Icinga Web 2')
+ )
+ ); ?>
+ <?php else: ?>
+ <?= $this->qlink(
+ $this->translate('Back'),
+ null,
+ null,
+ array(
+ 'class' => 'button-link',
+ 'title' => $this->translate('Show previous wizard-page')
+ )
+ ); ?>
+ <?php endif ?>
+ </div>
+ <pre class="log-output"><?= join("\n\n", array_map(function($a) {
+ return join("\n", $a);
+ }, $report ?? [])); ?></pre>
+</div>
diff --git a/modules/setup/application/views/scripts/index/parts/wizard.phtml b/modules/setup/application/views/scripts/index/parts/wizard.phtml
new file mode 100644
index 0000000..94891f9
--- /dev/null
+++ b/modules/setup/application/views/scripts/index/parts/wizard.phtml
@@ -0,0 +1 @@
+<?= $wizard->getForm()->render(); ?> \ No newline at end of file