summaryrefslogtreecommitdiffstats
path: root/modules/setup
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:39:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:39:39 +0000
commit8ca6cc32b2c789a3149861159ad258f2cb9491e3 (patch)
tree2492de6f1528dd44eaa169a5c1555026d9cb75ec /modules/setup
parentInitial commit. (diff)
downloadicingaweb2-8ca6cc32b2c789a3149861159ad258f2cb9491e3.tar.xz
icingaweb2-8ca6cc32b2c789a3149861159ad258f2cb9491e3.zip
Adding upstream version 2.11.4.upstream/2.11.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--modules/setup/application/clicommands/ConfigCommand.php185
-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.php423
-rw-r--r--modules/setup/application/forms/AuthBackendPage.php273
-rw-r--r--modules/setup/application/forms/AuthenticationPage.php69
-rw-r--r--modules/setup/application/forms/DatabaseCreationPage.php208
-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.phtml153
-rw-r--r--modules/setup/application/views/scripts/index/parts/finish.phtml34
-rw-r--r--modules/setup/application/views/scripts/index/parts/wizard.phtml1
-rw-r--r--modules/setup/library/Setup/Exception/SetupException.php22
-rw-r--r--modules/setup/library/Setup/Requirement.php343
-rw-r--r--modules/setup/library/Setup/Requirement/ClassRequirement.php48
-rw-r--r--modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php42
-rw-r--r--modules/setup/library/Setup/Requirement/OSRequirement.php27
-rw-r--r--modules/setup/library/Setup/Requirement/PhpConfigRequirement.php22
-rw-r--r--modules/setup/library/Setup/Requirement/PhpModuleRequirement.php42
-rw-r--r--modules/setup/library/Setup/Requirement/PhpVersionRequirement.php28
-rw-r--r--modules/setup/library/Setup/Requirement/SetRequirement.php34
-rw-r--r--modules/setup/library/Setup/Requirement/WebLibraryRequirement.php24
-rw-r--r--modules/setup/library/Setup/Requirement/WebModuleRequirement.php31
-rw-r--r--modules/setup/library/Setup/RequirementSet.php335
-rw-r--r--modules/setup/library/Setup/RequirementsRenderer.php64
-rw-r--r--modules/setup/library/Setup/Setup.php99
-rw-r--r--modules/setup/library/Setup/SetupWizard.php24
-rw-r--r--modules/setup/library/Setup/Step.php31
-rw-r--r--modules/setup/library/Setup/Steps/AuthenticationStep.php238
-rw-r--r--modules/setup/library/Setup/Steps/DatabaseStep.php266
-rw-r--r--modules/setup/library/Setup/Steps/GeneralConfigStep.php131
-rw-r--r--modules/setup/library/Setup/Steps/ResourceStep.php199
-rw-r--r--modules/setup/library/Setup/Steps/UserGroupStep.php213
-rw-r--r--modules/setup/library/Setup/Utils/DbTool.php943
-rw-r--r--modules/setup/library/Setup/Utils/EnableModuleStep.php77
-rw-r--r--modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php73
-rw-r--r--modules/setup/library/Setup/WebWizard.php752
-rw-r--r--modules/setup/library/Setup/Webserver.php233
-rw-r--r--modules/setup/library/Setup/Webserver/Apache.php142
-rw-r--r--modules/setup/library/Setup/Webserver/Nginx.php36
-rw-r--r--modules/setup/module.info6
53 files changed, 7368 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..130d797
--- /dev/null
+++ b/modules/setup/application/clicommands/ConfigCommand.php
@@ -0,0 +1,185 @@
+<?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\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.'));
+ }
+ 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..3252ec1
--- /dev/null
+++ b/modules/setup/application/forms/AdminAccountPage.php
@@ -0,0 +1,423 @@
+<?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\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();
+ 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';
+ }
+
+ 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.'
+ )
+ )
+ );
+ $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'))) {
+ $query->getQuery()->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'))) {
+ $query->getQuery()->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..4280c64
--- /dev/null
+++ b/modules/setup/application/forms/AuthBackendPage.php
@@ -0,0 +1,273 @@
+<?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();
+ }
+
+ 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..8660a21
--- /dev/null
+++ b/modules/setup/application/forms/DatabaseCreationPage.php
@@ -0,0 +1,208 @@
+<?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')
+ )
+ );
+
+ 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..b3f1784
--- /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..b2b3bda
--- /dev/null
+++ b/modules/setup/application/views/scripts/index/index.phtml
@@ -0,0 +1,153 @@
+<?php
+
+use Icinga\Web\Notification;
+
+$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);
+
+$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); }
+))));
+
+?>
+<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" style="width: 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" style="width: 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" style="width: 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" style="width: 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): ?>
+ <div class="line left<?= $stateClass; ?>" style="float: left; width: <?= sprintf(
+ '%.2F',
+ 100 - (count($configPagesLeft) - 1) * $lineWidth
+ ); ?>%; margin-right: 0"></div>
+ <?php elseif ($page === $lastPage): ?>
+ <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%; margin-right: -0.1em;"></div>
+ <?php else: ?>
+ <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%;"></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): ?>
+ <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%; margin-left: -0.1em;"></div>
+ <?php elseif ($page === $lastPage): ?>
+ <div class="line right<?= $stateClass; ?>" style="float: left; width: <?= sprintf(
+ '%.2F',
+ 100 - (count($configPagesRight) - 1) * $lineWidth
+ ); ?>%; margin-left: 0;"></div>
+ <?php else: ?>
+ <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%;"></div>
+ <?php endif ?>
+ <?php endforeach ?>
+ </td>
+ </tr></tbody></table>
+ <?php if ($maxProgress > 2 || $currentPos > 2): ?>
+ <?= $this->restartForm ?>
+ <?php endif ?>
+ </div>
+ <div class="step" style="width: 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>
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..dc5ba1c
--- /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
diff --git a/modules/setup/library/Setup/Exception/SetupException.php b/modules/setup/library/Setup/Exception/SetupException.php
new file mode 100644
index 0000000..c3ae591
--- /dev/null
+++ b/modules/setup/library/Setup/Exception/SetupException.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Exception;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Class SetupException
+ *
+ * Used to indicate that a setup should be aborted.
+ */
+class SetupException extends IcingaException
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct()
+ {
+ parent::__construct('Setup abortion');
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement.php b/modules/setup/library/Setup/Requirement.php
new file mode 100644
index 0000000..fd16405
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement.php
@@ -0,0 +1,343 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use LogicException;
+
+abstract class Requirement
+{
+ /**
+ * The state of this requirement
+ *
+ * @var bool
+ */
+ protected $state;
+
+ /**
+ * A descriptive text representing the current state of this requirement
+ *
+ * @var string
+ */
+ protected $stateText;
+
+ /**
+ * The descriptions of this requirement
+ *
+ * @var array
+ */
+ protected $descriptions;
+
+ /**
+ * The title of this requirement
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * The condition of this requirement
+ *
+ * @var mixed
+ */
+ protected $condition;
+
+ /**
+ * Whether this requirement is optional
+ *
+ * @var bool
+ */
+ protected $optional;
+
+ /**
+ * The alias to display the condition with in a human readable way
+ *
+ * @var string
+ */
+ protected $alias;
+
+ /**
+ * The text to display if the given requirement is fulfilled
+ *
+ * @var string
+ */
+ protected $textAvailable;
+
+ /**
+ * The text to display if the given requirement is not fulfilled
+ *
+ * @var string
+ */
+ protected $textMissing;
+
+ /**
+ * Create a new requirement
+ *
+ * @param array $options
+ *
+ * @throws LogicException In case there exists no setter for an option's key
+ */
+ public function __construct(array $options = array())
+ {
+ $this->optional = false;
+ $this->descriptions = array();
+
+ foreach ($options as $key => $value) {
+ $setMethod = 'set' . ucfirst($key);
+ $addMethod = 'add' . ucfirst($key);
+ if (method_exists($this, $setMethod)) {
+ $this->$setMethod($value);
+ } elseif (method_exists($this, $addMethod)) {
+ $this->$addMethod($value);
+ } else {
+ throw LogicException('No setter found for option key: ' . $key);
+ }
+ }
+ }
+
+ /**
+ * Set the state of this requirement
+ *
+ * @param bool $state
+ *
+ * @return Requirement
+ */
+ public function setState($state)
+ {
+ $this->state = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return the state of this requirement
+ *
+ * Evaluates the requirement in case there is no state set yet.
+ *
+ * @return int
+ */
+ public function getState()
+ {
+ if ($this->state === null) {
+ $this->state = $this->evaluate();
+ }
+
+ return $this->state;
+ }
+
+ /**
+ * Set a descriptive text for this requirement's current state
+ *
+ * @param string $text
+ *
+ * @return Requirement
+ */
+ public function setStateText($text)
+ {
+ $this->stateText = $text;
+ return $this;
+ }
+
+ /**
+ * Return a descriptive text for this requirement's current state
+ *
+ * @return string
+ */
+ public function getStateText()
+ {
+ $state = $this->getState();
+ if ($this->stateText === null) {
+ return $state ? $this->getTextAvailable() : $this->getTextMissing();
+ }
+ return $this->stateText;
+ }
+
+ /**
+ * Add a description for this requirement
+ *
+ * @param string $description
+ *
+ * @return Requirement
+ */
+ public function addDescription($description)
+ {
+ $this->descriptions[] = $description;
+ return $this;
+ }
+
+ /**
+ * Return the descriptions of this wizard
+ *
+ * @return array
+ */
+ public function getDescriptions()
+ {
+ return $this->descriptions;
+ }
+
+ /**
+ * Set the title for this requirement
+ *
+ * @param string $title
+ *
+ * @return Requirement
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * Return the title of this requirement
+ *
+ * In case there is no title set the alias is returned instead.
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ if ($this->title === null) {
+ return $this->getAlias();
+ }
+
+ return $this->title;
+ }
+
+ /**
+ * Set the condition for this requirement
+ *
+ * @param mixed $condition
+ *
+ * @return Requirement
+ */
+ public function setCondition($condition)
+ {
+ $this->condition = $condition;
+ return $this;
+ }
+
+ /**
+ * Return the condition of this requirement
+ *
+ * @return mixed
+ */
+ public function getCondition()
+ {
+ return $this->condition;
+ }
+
+ /**
+ * Set whether this requirement is optional
+ *
+ * @param bool $state
+ *
+ * @return Requirement
+ */
+ public function setOptional($state = true)
+ {
+ $this->optional = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this requirement is optional
+ *
+ * @return bool
+ */
+ public function isOptional()
+ {
+ return $this->optional;
+ }
+
+ /**
+ * Set the alias to display the condition with in a human readable way
+ *
+ * @param string $alias
+ *
+ * @return Requirement
+ */
+ public function setAlias($alias)
+ {
+ $this->alias = $alias;
+ return $this;
+ }
+
+ /**
+ * Return the alias to display the condition with in a human readable way
+ *
+ * @return string
+ */
+ public function getAlias()
+ {
+ return $this->alias;
+ }
+
+ /**
+ * Set the text to display if the given requirement is fulfilled
+ *
+ * @param string $textAvailable
+ *
+ * @return Requirement
+ */
+ public function setTextAvailable($textAvailable)
+ {
+ $this->textAvailable = $textAvailable;
+ return $this;
+ }
+
+ /**
+ * Get the text to display if the given requirement is fulfilled
+ *
+ * @return string
+ */
+ public function getTextAvailable()
+ {
+ return $this->textAvailable;
+ }
+
+ /**
+ * Set the text to display if the given requirement is not fulfilled
+ *
+ * @param string $textMissing
+ *
+ * @return Requirement
+ */
+ public function setTextMissing($textMissing)
+ {
+ $this->textMissing = $textMissing;
+ return $this;
+ }
+
+ /**
+ * Get the text to display if the given requirement is not fulfilled
+ *
+ * @return string
+ */
+ public function getTextMissing()
+ {
+ return $this->textMissing;
+ }
+
+ /**
+ * Evaluate this requirement and return whether it is fulfilled
+ *
+ * @return bool
+ */
+ abstract protected function evaluate();
+
+ /**
+ * Return whether the given requirement equals this one
+ *
+ * @param Requirement $requirement
+ *
+ * @return bool
+ */
+ public function equals(Requirement $requirement)
+ {
+ if ($requirement instanceof static) {
+ return $this->getCondition() === $requirement->getCondition();
+ }
+
+ return false;
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/ClassRequirement.php b/modules/setup/library/Setup/Requirement/ClassRequirement.php
new file mode 100644
index 0000000..d884c31
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/ClassRequirement.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class ClassRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ return Platform::classExists($this->getCondition());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStateText()
+ {
+ $stateText = parent::getStateText();
+ if ($stateText === null) {
+ $alias = $this->getAlias();
+ if ($this->getState()) {
+ $stateText = $alias === null
+ ? sprintf(
+ mt('setup', 'The %s class is available.', 'setup.requirement.class'),
+ $this->getCondition()
+ )
+ : sprintf(
+ mt('setup', 'The %s is available.', 'setup.requirement.class'),
+ $alias
+ );
+ } else {
+ $stateText = $alias === null
+ ? sprintf(
+ mt('setup', 'The %s class is missing.', 'setup.requirement.class'),
+ $this->getCondition()
+ )
+ : sprintf(
+ mt('setup', 'The %s is missing.', 'setup.requirement.class'),
+ $alias
+ );
+ }
+ }
+ return $stateText;
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php b/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php
new file mode 100644
index 0000000..7e9044c
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Module\Setup\Requirement;
+
+class ConfigDirectoryRequirement extends Requirement
+{
+ public function getTitle()
+ {
+ $title = parent::getTitle();
+ if ($title === null) {
+ return mt('setup', 'Read- and writable configuration directory');
+ }
+
+ return $title;
+ }
+
+ protected function evaluate()
+ {
+ $path = $this->getCondition();
+ if (file_exists($path)) {
+ $readable = is_readable($path);
+ if ($readable && is_writable($path)) {
+ $this->setStateText(sprintf(mt('setup', 'The directory %s is read- and writable.'), $path));
+ return true;
+ } else {
+ $this->setStateText(sprintf(
+ $readable
+ ? mt('setup', 'The directory %s is not writable.')
+ : mt('setup', 'The directory %s is not readable.'),
+ $path
+ ));
+ return false;
+ }
+ } else {
+ $this->setStateText(sprintf(mt('setup', 'The directory %s does not exist.'), $path));
+ return false;
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/OSRequirement.php b/modules/setup/library/Setup/Requirement/OSRequirement.php
new file mode 100644
index 0000000..760c97a
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/OSRequirement.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class OSRequirement extends Requirement
+{
+ public function getTitle()
+ {
+ $title = parent::getTitle();
+ if ($title === null) {
+ return sprintf(mt('setup', '%s Platform'), ucfirst($this->getCondition()));
+ }
+
+ return $title;
+ }
+
+ protected function evaluate()
+ {
+ $phpOS = Platform::getOperatingSystemName();
+ $this->setStateText(sprintf(mt('setup', 'You are running PHP on a %s system.'), ucfirst($phpOS)));
+ return strtolower($phpOS) === strtolower($this->getCondition());
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php b/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php
new file mode 100644
index 0000000..6c77af5
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class PhpConfigRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ list($configDirective, $value) = $this->getCondition();
+ $configValue = Platform::getPhpConfig($configDirective);
+ $this->setStateText(
+ $configValue
+ ? sprintf(mt('setup', 'The PHP config `%s\' is set to "%s".'), $configDirective, $configValue)
+ : sprintf(mt('setup', 'The PHP config `%s\' is not defined.'), $configDirective)
+ );
+ return is_bool($value) ? $configValue == $value : $configValue === $value;
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php b/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php
new file mode 100644
index 0000000..f8ab129
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class PhpModuleRequirement extends Requirement
+{
+ public function getTitle()
+ {
+ $title = parent::getTitle();
+ if ($title === $this->getAlias()) {
+ if ($title === null) {
+ $title = $this->getCondition();
+ }
+
+ return sprintf(mt('setup', 'PHP Module: %s'), $title);
+ }
+
+ return $title;
+ }
+
+ protected function evaluate()
+ {
+ $moduleName = $this->getCondition();
+ if (Platform::extensionLoaded($moduleName)) {
+ $this->setStateText(sprintf(
+ mt('setup', 'The PHP module %s is available.'),
+ $this->getAlias() ?: $moduleName
+ ));
+ return true;
+ } else {
+ $this->setStateText(sprintf(
+ mt('setup', 'The PHP module %s is missing.'),
+ $this->getAlias() ?: $moduleName
+ ));
+ return false;
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php b/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php
new file mode 100644
index 0000000..b811ca8
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class PhpVersionRequirement extends Requirement
+{
+ public function getTitle()
+ {
+ $title = parent::getTitle();
+ if ($title === null) {
+ return mt('setup', 'PHP Version');
+ }
+
+ return $title;
+ }
+
+ protected function evaluate()
+ {
+ $phpVersion = Platform::getPhpVersion();
+ $this->setStateText(sprintf(mt('setup', 'You are running PHP version %s.'), $phpVersion));
+ list($operator, $requiredVersion) = $this->getCondition();
+ return version_compare($phpVersion, $requiredVersion, $operator);
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/SetRequirement.php b/modules/setup/library/Setup/Requirement/SetRequirement.php
new file mode 100644
index 0000000..77cbaf0
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/SetRequirement.php
@@ -0,0 +1,34 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Module\Setup\Requirement;
+
+/**
+ * Add requirement field
+ *
+ * @package Icinga\Module\Setup\Requirement
+ */
+class SetRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ $condition = $this->getCondition();
+
+ if ($condition->getState()) {
+ $this->setStateText(sprintf(
+ mt('setup', '%s is available.'),
+ $this->getAlias() ?: $this->getTitle()
+ ));
+ return true;
+ }
+
+ $this->setStateText(sprintf(
+ mt('setup', '%s is missing.'),
+ $this->getAlias() ?: $this->getTitle()
+ ));
+
+ return false;
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php b/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php
new file mode 100644
index 0000000..bab587a
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php
@@ -0,0 +1,24 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Setup\Requirement;
+
+class WebLibraryRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ list($name, $op, $version) = $this->getCondition();
+
+ $libs = Icinga::app()->getLibraries();
+ if (! $libs->has($name)) {
+ $this->setStateText(sprintf(mt('setup', '%s is not installed'), $this->getAlias()));
+ return false;
+ }
+
+ $this->setStateText(sprintf(mt('setup', '%s version: %s'), $this->getAlias(), $libs->get($name)->getVersion()));
+ return $libs->has($name, $op . $version);
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/WebModuleRequirement.php b/modules/setup/library/Setup/Requirement/WebModuleRequirement.php
new file mode 100644
index 0000000..ad600e1
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/WebModuleRequirement.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Setup\Requirement;
+
+class WebModuleRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ list($name, $op, $version) = $this->getCondition();
+
+ $mm = Icinga::app()->getModuleManager();
+ if (! $mm->hasInstalled($name)) {
+ $this->setStateText(sprintf(mt('setup', '%s is not installed'), $this->getAlias()));
+ return false;
+ }
+
+ $module = $mm->getModule($name, false);
+
+ $moduleVersion = $module->getVersion();
+ if ($moduleVersion[0] === 'v') {
+ $moduleVersion = substr($moduleVersion, 1);
+ }
+
+ $this->setStateText(sprintf(mt('setup', '%s version: %s'), $this->getAlias(), $moduleVersion));
+ return version_compare($moduleVersion, $version, $op);
+ }
+}
diff --git a/modules/setup/library/Setup/RequirementSet.php b/modules/setup/library/Setup/RequirementSet.php
new file mode 100644
index 0000000..672fad4
--- /dev/null
+++ b/modules/setup/library/Setup/RequirementSet.php
@@ -0,0 +1,335 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use LogicException;
+use RecursiveIterator;
+use Traversable;
+
+/**
+ * Container to store and handle requirements
+ */
+class RequirementSet implements RecursiveIterator
+{
+ /**
+ * Mode AND (all requirements must be met)
+ */
+ const MODE_AND = 0;
+
+ /**
+ * Mode OR (at least one requirement must be met)
+ */
+ const MODE_OR = 1;
+
+ /**
+ * Whether all requirements meet their condition
+ *
+ * @var bool
+ */
+ protected $state;
+
+ /**
+ * Whether this set is optional
+ *
+ * @var bool
+ */
+ protected $optional;
+
+ /**
+ * The mode by which the requirements are evaluated
+ *
+ * @var string
+ */
+ protected $mode;
+
+ /**
+ * The registered requirements
+ *
+ * @var array
+ */
+ protected $requirements;
+
+ /**
+ * The raw state of this set's requirements
+ *
+ * @var bool
+ */
+ private $forcedState;
+
+ /**
+ * Initialize a new set of requirements
+ *
+ * @param bool $optional Whether this set is optional
+ * @param int $mode The mode by which to evaluate this set
+ */
+ public function __construct($optional = false, $mode = null)
+ {
+ $this->optional = $optional;
+ $this->requirements = array();
+ $this->setMode($mode ?: static::MODE_AND);
+ }
+
+ /**
+ * Set the state of this set
+ *
+ * @param bool $state
+ *
+ * @return RequirementSet
+ */
+ public function setState($state)
+ {
+ $this->state = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return the state of this set
+ *
+ * Alias for RequirementSet::fulfilled(true).
+ *
+ * @return bool
+ */
+ public function getState()
+ {
+ return $this->fulfilled(true);
+ }
+
+ /**
+ * Set whether this set of requirements should be optional
+ *
+ * @param bool $state
+ *
+ * @return RequirementSet
+ */
+ public function setOptional($state = true)
+ {
+ $this->optional = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this set of requirements is optional
+ *
+ * @return bool
+ */
+ public function isOptional()
+ {
+ return $this->optional;
+ }
+
+ /**
+ * Set the mode by which to evaluate the requirements
+ *
+ * @param int $mode
+ *
+ * @return RequirementSet
+ *
+ * @throws LogicException In case the given mode is invalid
+ */
+ public function setMode($mode)
+ {
+ if ($mode !== static::MODE_AND && $mode !== static::MODE_OR) {
+ throw new LogicException(sprintf('Invalid mode %u given.'), $mode);
+ }
+
+ $this->mode = $mode;
+ return $this;
+ }
+
+ /**
+ * Return the mode by which the requirements are evaluated
+ *
+ * @return int
+ */
+ public function getMode()
+ {
+ return $this->mode;
+ }
+
+ /**
+ * Register a requirement
+ *
+ * @param Requirement $requirement The requirement to add
+ *
+ * @return RequirementSet
+ */
+ public function add(Requirement $requirement)
+ {
+ $merged = false;
+ foreach ($this->requirements as $knownRequirement) {
+ if ($knownRequirement instanceof Requirement && $requirement->equals($knownRequirement)) {
+ $knownRequirement->setOptional($requirement->isOptional());
+ foreach ($requirement->getDescriptions() as $description) {
+ $knownRequirement->addDescription($description);
+ }
+
+ $merged = true;
+ break;
+ }
+ }
+
+ if (! $merged) {
+ $this->requirements[] = $requirement;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return all registered requirements
+ *
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->requirements;
+ }
+
+ /**
+ * Register the given set of requirements
+ *
+ * @param RequirementSet $set The set to register
+ *
+ * @return RequirementSet
+ */
+ public function merge(RequirementSet $set)
+ {
+ if ($this->getMode() === $set->getMode() && $this->isOptional() === $set->isOptional()) {
+ foreach ($set->getAll() as $requirement) {
+ if ($requirement instanceof static) {
+ $this->merge($requirement);
+ } else {
+ $this->add($requirement);
+ }
+ }
+ } else {
+ $this->requirements[] = $set;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether all requirements can successfully be evaluated based on the current mode
+ *
+ * In case this is a optional set of requirements (and $force is false), true is returned immediately.
+ *
+ * @param bool $force Whether to ignore the optionality of a set or single requirement
+ *
+ * @return bool
+ */
+ public function fulfilled($force = false)
+ {
+ $state = $this->isOptional();
+ if (! $force && $state) {
+ return true;
+ }
+
+ if (! $force && $this->state !== null) {
+ return $this->state;
+ } elseif ($force && $this->forcedState !== null) {
+ return $this->forcedState;
+ }
+
+ $self = $this->requirements;
+ foreach ($self as $requirement) {
+ if ($requirement->getState()) {
+ $state = true;
+ if ($this->getMode() === static::MODE_OR) {
+ break;
+ }
+ } elseif ($force || !$requirement->isOptional()) {
+ $state = false;
+ if ($this->getMode() === static::MODE_AND) {
+ break;
+ }
+ }
+ }
+
+ if ($force) {
+ return $this->forcedState = $state;
+ }
+
+ return $this->state = $state;
+ }
+
+ /**
+ * Return whether the current element represents a nested set of requirements
+ *
+ * @return bool
+ */
+ public function hasChildren(): bool
+ {
+ $current = $this->current();
+ return $current instanceof static;
+ }
+
+ /**
+ * Return a iterator for the current nested set of requirements
+ *
+ * @return ?RecursiveIterator
+ */
+ public function getChildren(): ?RecursiveIterator
+ {
+ return $this->current();
+ }
+
+ /**
+ * Rewind the iterator to its first element
+ */
+ public function rewind(): void
+ {
+ reset($this->requirements);
+ }
+
+ /**
+ * Return whether the current iterator position is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return key($this->requirements) !== null;
+ }
+
+ /**
+ * Return the current element in the iteration
+ *
+ * @return Requirement|RequirementSet
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return current($this->requirements);
+ }
+
+ /**
+ * Return the position of the current element in the iteration
+ *
+ * @return int
+ */
+ public function key(): int
+ {
+ return key($this->requirements);
+ }
+
+ /**
+ * Advance the iterator to the next element
+ */
+ public function next(): void
+ {
+ next($this->requirements);
+ }
+
+ /**
+ * Return this set of requirements rendered as HTML
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ $renderer = new RequirementsRenderer($this);
+ return (string) $renderer;
+ }
+}
diff --git a/modules/setup/library/Setup/RequirementsRenderer.php b/modules/setup/library/Setup/RequirementsRenderer.php
new file mode 100644
index 0000000..cc9392a
--- /dev/null
+++ b/modules/setup/library/Setup/RequirementsRenderer.php
@@ -0,0 +1,64 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use RecursiveIteratorIterator;
+
+class RequirementsRenderer extends RecursiveIteratorIterator
+{
+ public function beginIteration(): void
+ {
+ $this->tags[] = '<ul class="requirements">';
+ }
+
+ public function endIteration(): void
+ {
+ $this->tags[] = '</ul>';
+ }
+
+ public function beginChildren(): void
+ {
+ $this->tags[] = '<li>';
+ $currentSet = $this->getSubIterator();
+ $state = $currentSet->getState() ? 'fulfilled' : ($currentSet->isOptional() ? 'not-available' : 'missing');
+ $this->tags[] = '<ul class="set-state ' . $state . '">';
+ }
+
+ public function endChildren(): void
+ {
+ $this->tags[] = '</ul>';
+ $this->tags[] = '</li>';
+ }
+
+ public function render()
+ {
+ foreach ($this as $requirement) {
+ $this->tags[] = '<li class="clearfix">';
+ $this->tags[] = '<div class="title"><h2>' . $requirement->getTitle() . '</h2></div>';
+ $this->tags[] = '<div class="description">';
+ $descriptions = $requirement->getDescriptions();
+ if (count($descriptions) > 1) {
+ $this->tags[] = '<ul>';
+ foreach ($descriptions as $d) {
+ $this->tags[] = '<li>' . $d . '</li>';
+ }
+ $this->tags[] = '</ul>';
+ } elseif (! empty($descriptions)) {
+ $this->tags[] = $descriptions[0];
+ }
+ $this->tags[] = '</div>';
+ $this->tags[] = '<div class="state ' . ($requirement->getState() ? 'fulfilled' : (
+ $requirement->isOptional() ? 'not-available' : 'missing'
+ )) . '">' . $requirement->getStateText() . '</div>';
+ $this->tags[] = '</li>';
+ }
+
+ return implode("\n", $this->tags);
+ }
+
+ public function __toString()
+ {
+ return $this->render();
+ }
+}
diff --git a/modules/setup/library/Setup/Setup.php b/modules/setup/library/Setup/Setup.php
new file mode 100644
index 0000000..7b0baed
--- /dev/null
+++ b/modules/setup/library/Setup/Setup.php
@@ -0,0 +1,99 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use ArrayIterator;
+use IteratorAggregate;
+use Icinga\Module\Setup\Exception\SetupException;
+use Traversable;
+
+/**
+ * Container for multiple configuration steps
+ */
+class Setup implements IteratorAggregate
+{
+ protected $steps;
+
+ protected $state;
+
+ public function __construct()
+ {
+ $this->steps = array();
+ }
+
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->getSteps());
+ }
+
+ public function addStep(Step $step)
+ {
+ $this->steps[] = $step;
+ }
+
+ public function addSteps(array $steps)
+ {
+ foreach ($steps as $step) {
+ $this->addStep($step);
+ }
+ }
+
+ public function getSteps()
+ {
+ return $this->steps;
+ }
+
+ /**
+ * Run the configuration and return whether it succeeded
+ *
+ * @return bool
+ */
+ public function run()
+ {
+ $this->state = true;
+
+ try {
+ foreach ($this->steps as $step) {
+ $this->state &= $step->apply();
+ }
+ } catch (SetupException $_) {
+ $this->state = false;
+ }
+
+ return $this->state;
+ }
+
+ /**
+ * Return a summary of all actions designated to run
+ *
+ * @return array An array of HTML strings
+ */
+ public function getSummary()
+ {
+ $summaries = array();
+ foreach ($this->steps as $step) {
+ $summaries[] = $step->getSummary();
+ }
+
+ return $summaries;
+ }
+
+ /**
+ * Return a report of all actions that were run
+ *
+ * @return array An array of arrays of strings
+ */
+ public function getReport()
+ {
+ $reports = array();
+ foreach ($this->steps as $step) {
+ $report = $step->getReport();
+ if (! empty($report)) {
+ $reports[] = $report;
+ }
+ }
+
+ return $reports;
+ }
+}
diff --git a/modules/setup/library/Setup/SetupWizard.php b/modules/setup/library/Setup/SetupWizard.php
new file mode 100644
index 0000000..c7ad0c3
--- /dev/null
+++ b/modules/setup/library/Setup/SetupWizard.php
@@ -0,0 +1,24 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+/**
+ * Interface for wizards providing a setup and requirements
+ */
+interface SetupWizard
+{
+ /**
+ * Return the setup for this wizard
+ *
+ * @return Setup
+ */
+ public function getSetup();
+
+ /**
+ * Return the requirements of this wizard
+ *
+ * @return RequirementSet
+ */
+ public function getRequirements();
+}
diff --git a/modules/setup/library/Setup/Step.php b/modules/setup/library/Setup/Step.php
new file mode 100644
index 0000000..1d0797d
--- /dev/null
+++ b/modules/setup/library/Setup/Step.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+/**
+ * Class to implement functionality for a single setup step
+ */
+abstract class Step
+{
+ /**
+ * Apply this step's configuration changes
+ *
+ * @return bool
+ */
+ abstract public function apply();
+
+ /**
+ * Return a HTML representation of this step's configuration changes supposed to be made
+ *
+ * @return string
+ */
+ abstract public function getSummary();
+
+ /**
+ * Return a textual summary of all configuration changes made
+ *
+ * @return array
+ */
+ abstract public function getReport();
+}
diff --git a/modules/setup/library/Setup/Steps/AuthenticationStep.php b/modules/setup/library/Setup/Steps/AuthenticationStep.php
new file mode 100644
index 0000000..3c6c64a
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/AuthenticationStep.php
@@ -0,0 +1,238 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\IcingaException;
+use Icinga\Authentication\User\DbUserBackend;
+use Icinga\Module\Setup\Step;
+
+class AuthenticationStep extends Step
+{
+ protected $data;
+
+ protected $dbError;
+
+ protected $authIniError;
+
+ protected $permIniError;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $success = $this->createAuthenticationIni();
+ if (isset($this->data['adminAccountData']['resourceConfig'])) {
+ $success &= $this->createAccount();
+ }
+
+ $success &= $this->createRolesIni();
+ return $success;
+ }
+
+ protected function createAuthenticationIni()
+ {
+ $config = array();
+ $backendConfig = $this->data['backendConfig'];
+ $backendName = $backendConfig['name'];
+ unset($backendConfig['name']);
+ $config[$backendName] = $backendConfig;
+ if (isset($this->data['resourceName'])) {
+ $config[$backendName]['resource'] = $this->data['resourceName'];
+ }
+
+ try {
+ Config::fromArray($config)
+ ->setConfigFile(Config::resolvePath('authentication.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->authIniError = $e;
+ return false;
+ }
+
+ $this->authIniError = false;
+ return true;
+ }
+
+ protected function createRolesIni()
+ {
+ if (isset($this->data['adminAccountData']['username'])) {
+ $config = array(
+ 'users' => $this->data['adminAccountData']['username'],
+ 'permissions' => '*'
+ );
+
+ if ($this->data['backendConfig']['backend'] === 'db') {
+ $config['groups'] = mt('setup', 'Administrators', 'setup.role.name');
+ }
+ } else { // isset($this->data['adminAccountData']['groupname'])
+ $config = array(
+ 'groups' => $this->data['adminAccountData']['groupname'],
+ 'permissions' => '*'
+ );
+ }
+
+ try {
+ Config::fromArray(array(mt('setup', 'Administrators', 'setup.role.name') => $config))
+ ->setConfigFile(Config::resolvePath('roles.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->permIniError = $e;
+ return false;
+ }
+
+ $this->permIniError = false;
+ return true;
+ }
+
+ protected function createAccount()
+ {
+ try {
+ $backend = new DbUserBackend(
+ ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig']))
+ );
+
+ if ($backend->select()->where('user_name', $this->data['adminAccountData']['username'])->count() === 0) {
+ $backend->insert('user', array(
+ 'user_name' => $this->data['adminAccountData']['username'],
+ 'password' => $this->data['adminAccountData']['password'],
+ 'is_active' => true
+ ));
+ $this->dbError = false;
+ }
+ } catch (Exception $e) {
+ $this->dbError = $e;
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getSummary()
+ {
+ $pageTitle = '<h2>' . mt('setup', 'Authentication', 'setup.page.title') . '</h2>';
+ $backendTitle = '<h3>' . mt('setup', 'Authentication Backend', 'setup.page.title') . '</h3>';
+ $adminTitle = '<h3>' . mt('setup', 'Administration', 'setup.page.title') . '</h3>';
+
+ $authType = $this->data['backendConfig']['backend'];
+ $backendDesc = '<p>' . sprintf(
+ mt('setup', 'Users will authenticate using %s.', 'setup.summary.auth'),
+ $authType === 'db' ? mt('setup', 'a database', 'setup.summary.auth.type') : (
+ $authType === 'ldap' || $authType === 'msldap' ? 'LDAP' : (
+ mt('setup', 'webserver authentication', 'setup.summary.auth.type')
+ )
+ )
+ ) . '</p>';
+
+ $backendHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Backend Name') . '</strong></td>'
+ . '<td>' . $this->data['backendConfig']['name'] . '</td>'
+ . '</tr>'
+ . ($authType === 'ldap' || $authType === 'msldap' ? (
+ '<tr>'
+ . '<td><strong>' . mt('setup', 'User Object Class') . '</strong></td>'
+ . '<td>' . ($authType === 'msldap' ? 'user' : $this->data['backendConfig']['user_class']) . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Custom Filter') . '</strong></td>'
+ . '<td>' . (trim($this->data['backendConfig']['filter']) ?: t('None', 'auth.ldap.filter')) . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'User Name Attribute') . '</strong></td>'
+ . '<td>' . ($authType === 'msldap'
+ ? 'sAMAccountName'
+ : $this->data['backendConfig']['user_name_attribute']) . '</td>'
+ . '</tr>'
+ ) : ($authType === 'external' ? (
+ '<tr>'
+ . '<td><strong>' . t('Filter Pattern') . '</strong></td>'
+ . '<td>' . $this->data['backendConfig']['strip_username_regexp'] . '</td>'
+ . '</tr>'
+ ) : ''))
+ . '</tbody>'
+ . '</table>';
+
+ if (isset($this->data['adminAccountData']['username'])) {
+ $adminHtml = '<p>' . (isset($this->data['adminAccountData']['resourceConfig']) ? sprintf(
+ mt('setup', 'Administrative rights will initially be granted to a new account called "%s".'),
+ $this->data['adminAccountData']['username']
+ ) : sprintf(
+ mt('setup', 'Administrative rights will initially be granted to an existing account called "%s".'),
+ $this->data['adminAccountData']['username']
+ )) . '</p>';
+ } else { // isset($this->data['adminAccountData']['groupname'])
+ $adminHtml = '<p>' . sprintf(
+ mt('setup', 'Administrative rights will initially be granted to members of the user group "%s".'),
+ $this->data['adminAccountData']['groupname']
+ ) . '</p>';
+ }
+
+ return $pageTitle . '<div class="topic">' . $backendDesc . $backendTitle . $backendHtml . '</div>'
+ . '<div class="topic">' . $adminTitle . $adminHtml . '</div>';
+ }
+
+ public function getReport()
+ {
+ $report = array();
+
+ if ($this->authIniError === false) {
+ $report[] = sprintf(
+ mt('setup', 'Authentication configuration has been successfully written to: %s'),
+ Config::resolvePath('authentication.ini')
+ );
+ } elseif ($this->authIniError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'Authentication configuration could not be written to: %s. An error occured:'),
+ Config::resolvePath('authentication.ini')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->authIniError));
+ }
+
+ if ($this->dbError === false) {
+ $report[] = sprintf(
+ mt('setup', 'Account "%s" has been successfully created.'),
+ $this->data['adminAccountData']['username']
+ );
+ } elseif ($this->dbError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'Unable to create account "%s". An error occured:'),
+ $this->data['adminAccountData']['username']
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->dbError));
+ }
+
+ if ($this->permIniError === false) {
+ $report[] = isset($this->data['adminAccountData']['username']) ? sprintf(
+ mt('setup', 'Account "%s" has been successfully defined as initial administrator.'),
+ $this->data['adminAccountData']['username']
+ ) : sprintf(
+ mt('setup', 'The members of the user group "%s" were successfully defined as initial administrators.'),
+ $this->data['adminAccountData']['groupname']
+ );
+ } elseif ($this->permIniError !== null) {
+ $report[] = isset($this->data['adminAccountData']['username']) ? sprintf(
+ mt('setup', 'Unable to define account "%s" as initial administrator. An error occured:'),
+ $this->data['adminAccountData']['username']
+ ) : sprintf(
+ mt(
+ 'setup',
+ 'Unable to define the members of the user group "%s" as initial administrators. An error occured:'
+ ),
+ $this->data['adminAccountData']['groupname']
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->permIniError));
+ }
+
+ return $report;
+ }
+}
diff --git a/modules/setup/library/Setup/Steps/DatabaseStep.php b/modules/setup/library/Setup/Steps/DatabaseStep.php
new file mode 100644
index 0000000..32b2d15
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/DatabaseStep.php
@@ -0,0 +1,266 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use PDOException;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+use Icinga\Module\Setup\Utils\DbTool;
+use Icinga\Module\Setup\Exception\SetupException;
+
+class DatabaseStep extends Step
+{
+ protected $data;
+
+ protected $error;
+
+ protected $messages;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ $this->messages = array();
+ }
+
+ public function apply()
+ {
+ $resourceConfig = $this->data['resourceConfig'];
+ if (isset($this->data['adminName'])) {
+ $resourceConfig['username'] = $this->data['adminName'];
+ if (isset($this->data['adminPassword'])) {
+ $resourceConfig['password'] = $this->data['adminPassword'];
+ }
+ }
+
+ $db = new DbTool($resourceConfig);
+
+ try {
+ if ($resourceConfig['db'] === 'mysql') {
+ $this->setupMysqlDatabase($db);
+ } elseif ($resourceConfig['db'] === 'pgsql') {
+ $this->setupPgsqlDatabase($db);
+ }
+ } catch (Exception $e) {
+ $this->error = $e;
+ throw new SetupException();
+ }
+
+ $this->error = false;
+ return true;
+ }
+
+ protected function setupMysqlDatabase(DbTool $db)
+ {
+ try {
+ $db->connectToDb();
+ $this->log(
+ mt('setup', 'Successfully connected to existing database "%s"...'),
+ $this->data['resourceConfig']['dbname']
+ );
+ } catch (PDOException $_) {
+ $db->connectToHost();
+ $this->log(mt('setup', 'Creating new database "%s"...'), $this->data['resourceConfig']['dbname']);
+ $db->exec('CREATE DATABASE ' . $db->quoteIdentifier($this->data['resourceConfig']['dbname']));
+ $db->reconnect($this->data['resourceConfig']['dbname']);
+ }
+
+ if (array_search(reset($this->data['tables']), $db->listTables(), true) !== false) {
+ $this->log(mt('setup', 'Database schema already exists...'));
+ } else {
+ $this->log(mt('setup', 'Creating database schema...'));
+ $db->import($this->data['schemaPath'] . '/mysql.schema.sql');
+ }
+
+ if ($db->hasLogin($this->data['resourceConfig']['username'])) {
+ $this->log(mt('setup', 'Login "%s" already exists...'), $this->data['resourceConfig']['username']);
+ } else {
+ $this->log(mt('setup', 'Creating login "%s"...'), $this->data['resourceConfig']['username']);
+ $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']);
+ }
+
+ $username = $this->data['resourceConfig']['username'];
+ if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) {
+ $this->log(
+ mt('setup', 'Required privileges were already granted to login "%s".'),
+ $this->data['resourceConfig']['username']
+ );
+ } else {
+ $this->log(
+ mt('setup', 'Granting required privileges to login "%s"...'),
+ $this->data['resourceConfig']['username']
+ );
+ $db->grantPrivileges(
+ $this->data['privileges'],
+ $this->data['tables'],
+ $this->data['resourceConfig']['username']
+ );
+ }
+ }
+
+ protected function setupPgsqlDatabase(DbTool $db)
+ {
+ try {
+ $db->connectToDb();
+ $this->log(
+ mt('setup', 'Successfully connected to existing database "%s"...'),
+ $this->data['resourceConfig']['dbname']
+ );
+ } catch (PDOException $_) {
+ $db->connectToHost();
+ $this->log(mt('setup', 'Creating new database "%s"...'), $this->data['resourceConfig']['dbname']);
+ $db->exec(sprintf(
+ "CREATE DATABASE %s WITH ENCODING 'UTF-8'",
+ $db->quoteIdentifier($this->data['resourceConfig']['dbname'])
+ ));
+ $db->reconnect($this->data['resourceConfig']['dbname']);
+ }
+
+ if (array_search(reset($this->data['tables']), $db->listTables(), true) !== false) {
+ $this->log(mt('setup', 'Database schema already exists...'));
+ } else {
+ $this->log(mt('setup', 'Creating database schema...'));
+ $db->import($this->data['schemaPath'] . '/pgsql.schema.sql');
+ }
+
+ if ($db->hasLogin($this->data['resourceConfig']['username'])) {
+ $this->log(mt('setup', 'Login "%s" already exists...'), $this->data['resourceConfig']['username']);
+ } else {
+ $this->log(mt('setup', 'Creating login "%s"...'), $this->data['resourceConfig']['username']);
+ $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']);
+ }
+
+ $username = $this->data['resourceConfig']['username'];
+ if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) {
+ $this->log(
+ mt('setup', 'Required privileges were already granted to login "%s".'),
+ $this->data['resourceConfig']['username']
+ );
+ } else {
+ $this->log(
+ mt('setup', 'Granting required privileges to login "%s"...'),
+ $this->data['resourceConfig']['username']
+ );
+ $db->grantPrivileges(
+ $this->data['privileges'],
+ $this->data['tables'],
+ $this->data['resourceConfig']['username']
+ );
+ }
+ }
+
+ public function getSummary()
+ {
+ $resourceConfig = $this->data['resourceConfig'];
+ if (isset($this->data['adminName'])) {
+ $resourceConfig['username'] = $this->data['adminName'];
+ if (isset($this->data['adminPassword'])) {
+ $resourceConfig['password'] = $this->data['adminPassword'];
+ }
+ }
+
+ $db = new DbTool($resourceConfig);
+
+ try {
+ $db->connectToDb();
+ if (array_search(reset($this->data['tables']), $db->listTables(), true) === false) {
+ if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to setup the missing schema required by Icinga'
+ . ' Web 2 in database "%s" and to grant access to it to a new login called "%s".'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname'],
+ $this->data['resourceConfig']['username']
+ );
+ } else {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to setup the missing'
+ . ' schema required by Icinga Web 2 in database "%s".'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname']
+ );
+ }
+ } else {
+ $message = sprintf(
+ mt('setup', 'The database "%s" already seems to be fully set up. No action required.'),
+ $resourceConfig['dbname']
+ );
+ }
+ } catch (PDOException $_) {
+ try {
+ $db->connectToHost();
+ if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) {
+ if ($db->hasLogin($this->data['resourceConfig']['username'])) {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to create the missing database'
+ . ' "%s" with the schema required by Icinga Web 2 and to grant'
+ . ' access to it to an existing login called "%s".'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname'],
+ $this->data['resourceConfig']['username']
+ );
+ } else {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to create the missing database'
+ . ' "%s" with the schema required by Icinga Web 2 and to grant'
+ . ' access to it to a new login called "%s".'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname'],
+ $this->data['resourceConfig']['username']
+ );
+ }
+ } else {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to create the missing'
+ . ' database "%s" with the schema required by Icinga Web 2.'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname']
+ );
+ }
+ } catch (Exception $_) {
+ $message = mt(
+ 'setup',
+ 'No connection to database host possible. You\'ll need to setup the'
+ . ' database with the schema required by Icinga Web 2 manually.'
+ );
+ }
+ }
+
+ return '<h2>' . mt('setup', 'Database Setup', 'setup.page.title') . '</h2><p>' . $message . '</p>';
+ }
+
+ public function getReport()
+ {
+ if ($this->error === false) {
+ $report = $this->messages;
+ $report[] = mt('setup', 'The database has been fully set up!');
+ return $report;
+ } elseif ($this->error !== null) {
+ $report = $this->messages;
+ $report[] = mt('setup', 'Failed to fully setup the database. An error occured:');
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error));
+ return $report;
+ }
+ }
+
+ protected function log()
+ {
+ $this->messages[] = call_user_func_array('sprintf', func_get_args());
+ }
+}
diff --git a/modules/setup/library/Setup/Steps/GeneralConfigStep.php b/modules/setup/library/Setup/Steps/GeneralConfigStep.php
new file mode 100644
index 0000000..2c928f6
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/GeneralConfigStep.php
@@ -0,0 +1,131 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Application\Config;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+
+class GeneralConfigStep extends Step
+{
+ protected $data;
+
+ protected $error;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $config = array();
+ foreach ($this->data['generalConfig'] as $sectionAndPropertyName => $value) {
+ list($section, $property) = explode('_', $sectionAndPropertyName, 2);
+ $config[$section][$property] = $value;
+ }
+
+ $config['global']['config_resource'] = $this->data['resourceName'];
+
+ try {
+ Config::fromArray($config)
+ ->setConfigFile(Config::resolvePath('config.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->error = $e;
+ return false;
+ }
+
+ $this->error = false;
+ return true;
+ }
+
+ public function getSummary()
+ {
+ $pageTitle = '<h2>' . mt('setup', 'Application Configuration', 'setup.page.title') . '</h2>';
+ $generalTitle = '<h3>' . t('General', 'app.config') . '</h3>';
+ $loggingTitle = '<h3>' . t('Logging', 'app.config') . '</h3>';
+
+ $generalHtml = ''
+ . '<ul>'
+ . '<li>' . ($this->data['generalConfig']['global_show_stacktraces']
+ ? t('An exception\'s stacktrace is shown to every user by default.')
+ : t('An exception\'s stacktrace is hidden from every user by default.')
+ ) . '</li>'
+ . '<li>' . t('Preferences will be stored using a database.') . '</li>'
+ . '</ul>';
+
+ $type = $this->data['generalConfig']['logging_log'];
+ if ($type === 'none') {
+ $loggingHtml = '<p>' . mt('setup', 'Logging will be disabled.') . '</p>';
+ } else {
+ $level = $this->data['generalConfig']['logging_level'];
+
+ switch ($type) {
+ case 'php':
+ $typeDescription = t('Webserver Log', 'app.config.logging.type');
+ $typeSpecificHtml = '';
+ break;
+
+ case 'syslog':
+ $typeDescription = 'Syslog';
+ $typeSpecificHtml = '<td><strong>' . t('Application Prefix') . '</strong></td>'
+ . '<td>' . $this->data['generalConfig']['logging_application'] . '</td>';
+ break;
+
+ case 'file':
+ $typeDescription = t('File', 'app.config.logging.type');
+ $typeSpecificHtml = '<td><strong>' . t('Filepath') . '</strong></td>'
+ . '<td>' . $this->data['generalConfig']['logging_file'] . '</td>';
+ break;
+ }
+
+ $loggingHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Type', 'app.config.logging') . '</strong></td>'
+ . '<td>' . $typeDescription . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Level', 'app.config.logging') . '</strong></td>'
+ . '<td>' . ($level === Logger::$levels[Logger::ERROR] ? t('Error', 'app.config.logging.level') : (
+ $level === Logger::$levels[Logger::WARNING] ? t('Warning', 'app.config.logging.level') : (
+ $level === Logger::$levels[Logger::INFO] ? t('Information', 'app.config.logging.level') : (
+ t('Debug', 'app.config.logging.level')
+ )
+ )
+ )) . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . $typeSpecificHtml
+ . '</tr>'
+ . '</tbody>'
+ . '</table>';
+ }
+
+ return $pageTitle . '<div class="topic">' . $generalTitle . $generalHtml . '</div>'
+ . '<div class="topic">' . $loggingTitle . $loggingHtml . '</div>';
+ }
+
+ public function getReport()
+ {
+ if ($this->error === false) {
+ return array(sprintf(
+ mt('setup', 'General configuration has been successfully written to: %s'),
+ Config::resolvePath('config.ini')
+ ));
+ } elseif ($this->error !== null) {
+ return array(
+ sprintf(
+ mt('setup', 'General configuration could not be written to: %s. An error occured:'),
+ Config::resolvePath('config.ini')
+ ),
+ sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error))
+ );
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Steps/ResourceStep.php b/modules/setup/library/Setup/Steps/ResourceStep.php
new file mode 100644
index 0000000..d9daf3b
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/ResourceStep.php
@@ -0,0 +1,199 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+
+class ResourceStep extends Step
+{
+ protected $data;
+
+ protected $error;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $resourceConfig = array();
+ if (isset($this->data['dbResourceConfig'])) {
+ $dbConfig = $this->data['dbResourceConfig'];
+ $resourceName = $dbConfig['name'];
+ unset($dbConfig['name']);
+ $resourceConfig[$resourceName] = $dbConfig;
+ }
+
+ if (isset($this->data['ldapResourceConfig'])) {
+ $ldapConfig = $this->data['ldapResourceConfig'];
+ $resourceName = $ldapConfig['name'];
+ unset($ldapConfig['name']);
+ $resourceConfig[$resourceName] = $ldapConfig;
+ }
+
+ try {
+ Config::fromArray($resourceConfig)
+ ->setConfigFile(Config::resolvePath('resources.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->error = $e;
+ return false;
+ }
+
+ $this->error = false;
+ return true;
+ }
+
+ public function getSummary()
+ {
+ if (isset($this->data['dbResourceConfig']) && isset($this->data['ldapResourceConfig'])) {
+ $pageTitle = '<h2>' . mt('setup', 'Resources', 'setup.page.title') . '</h2>';
+ } else {
+ $pageTitle = '<h2>' . mt('setup', 'Resource', 'setup.page.title') . '</h2>';
+ }
+
+ if (isset($this->data['dbResourceConfig'])) {
+ $dbTitle = '<h3>' . mt('setup', 'Database', 'setup.page.title') . '</h3>';
+ $dbHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Resource Name') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['name'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Database Type') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['db'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Host') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['host'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Port') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['port'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Database Name') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['dbname'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Username') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['username'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Password') . '</strong></td>'
+ . '<td>' . str_repeat('*', strlen($this->data['dbResourceConfig']['password'])) . '</td>'
+ . '</tr>';
+
+ if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
+ && isset($this->data['resourceConfig']['ssl_do_not_verify_server_cert'])
+ && $this->data['resourceConfig']['ssl_do_not_verify_server_cert']
+ ) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('SSL Do Not Verify Server Certificate') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['ssl_do_not_verify_server_cert'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_key']) && $this->data['dbResourceConfig']['ssl_key']) {
+ $dbHtml .= ''
+ .'<tr>'
+ . '<td><strong>' . t('SSL Key') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_key'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_cert']) && $this->data['dbResourceConfig']['ssl_cert']) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('SSL Cert') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_cert'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_ca']) && $this->data['dbResourceConfig']['ssl_ca']) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('CA') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_ca'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_capath']) && $this->data['dbResourceConfig']['ssl_capath']) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('CA Path') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_capath'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_cipher']) && $this->data['dbResourceConfig']['ssl_cipher']) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('Cipher') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_cipher'] . '</td>'
+ . '</tr>';
+ }
+
+ $dbHtml .= ''
+ . '</tbody>'
+ . '</table>';
+ }
+
+ if (isset($this->data['ldapResourceConfig'])) {
+ $ldapTitle = '<h3>LDAP</h3>';
+ $ldapHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Resource Name') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['name'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Host') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['hostname'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Port') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['port'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Root DN') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['root_dn'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Bind DN') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['bind_dn'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Bind Password') . '</strong></td>'
+ . '<td>' . str_repeat('*', strlen($this->data['ldapResourceConfig']['bind_pw'])) . '</td>'
+ . '</tr>'
+ . '</tbody>'
+ . '</table>';
+ }
+
+ return $pageTitle . (isset($dbTitle) ? '<div class="topic">' . $dbTitle . $dbHtml . '</div>' : '')
+ . (isset($ldapTitle) ? '<div class="topic">' . $ldapTitle . $ldapHtml . '</div>' : '');
+ }
+
+ public function getReport()
+ {
+ if ($this->error === false) {
+ return array(sprintf(
+ mt('setup', 'Resource configuration has been successfully written to: %s'),
+ Config::resolvePath('resources.ini')
+ ));
+ } elseif ($this->error !== null) {
+ return array(
+ sprintf(
+ mt('setup', 'Resource configuration could not be written to: %s. An error occured:'),
+ Config::resolvePath('resources.ini')
+ ),
+ sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error))
+ );
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Steps/UserGroupStep.php b/modules/setup/library/Setup/Steps/UserGroupStep.php
new file mode 100644
index 0000000..4aab676
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/UserGroupStep.php
@@ -0,0 +1,213 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Authentication\UserGroup\DbUserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+
+class UserGroupStep extends Step
+{
+ protected $data;
+
+ protected $groupError;
+
+ protected $memberError;
+
+ protected $groupIniError;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $success = $this->createGroupsIni();
+ if (isset($this->data['resourceConfig'])) {
+ $success &= $this->createUserGroup();
+ if ($success) {
+ $success &= $this->createMembership();
+ }
+ }
+
+ return $success;
+ }
+
+ protected function createGroupsIni()
+ {
+ $config = array();
+ if (isset($this->data['groupConfig'])) {
+ $backendConfig = $this->data['groupConfig'];
+ $backendName = $backendConfig['name'];
+ unset($backendConfig['name']);
+ $config[$backendName] = $backendConfig;
+ } else {
+ $backendConfig = array(
+ 'backend' => $this->data['backendConfig']['backend'], // "db" or "msldap"
+ 'resource' => $this->data['resourceName']
+ );
+
+ if ($backendConfig['backend'] === 'msldap') {
+ $backendConfig['user_backend'] = $this->data['backendConfig']['name'];
+ }
+
+ $config[$this->data['backendConfig']['name']] = $backendConfig;
+ }
+
+ try {
+ Config::fromArray($config)
+ ->setConfigFile(Config::resolvePath('groups.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->groupIniError = $e;
+ return false;
+ }
+
+ $this->groupIniError = false;
+ return true;
+ }
+
+ protected function createUserGroup()
+ {
+ try {
+ $backend = new DbUserGroupBackend(
+ ResourceFactory::createResource(new ConfigObject($this->data['resourceConfig']))
+ );
+
+ $groupName = mt('setup', 'Administrators', 'setup.role.name');
+ if ($backend->select()->where('group_name', $groupName)->count() === 0) {
+ $backend->insert('group', array(
+ 'group_name' => $groupName
+ ));
+ $this->groupError = false;
+ }
+ } catch (Exception $e) {
+ $this->groupError = $e;
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function createMembership()
+ {
+ try {
+ $backend = new DbUserGroupBackend(
+ ResourceFactory::createResource(new ConfigObject($this->data['resourceConfig']))
+ );
+
+ $groupName = mt('setup', 'Administrators', 'setup.role.name');
+ $userName = $this->data['username'];
+ if ($backend
+ ->select()
+ ->from('group_membership')
+ ->where('group_name', $groupName)
+ ->where('user_name', $userName)
+ ->count() === 0
+ ) {
+ $backend->insert('group_membership', array(
+ 'group_name' => $groupName,
+ 'user_name' => $userName
+ ));
+ $this->memberError = false;
+ }
+ } catch (Exception $e) {
+ $this->memberError = $e;
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getSummary()
+ {
+ if (! isset($this->data['groupConfig'])) {
+ return; // It's not necessary to show the user something he didn't configure..
+ }
+
+ $pageTitle = '<h2>' . mt('setup', 'User Groups', 'setup.page.title') . '</h2>';
+ $backendTitle = '<h3>' . mt('setup', 'User Group Backend', 'setup.page.title') . '</h3>';
+
+ $backendHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Backend Name') . '</strong></td>'
+ . '<td>' . $this->data['groupConfig']['name'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Group Object Class') . '</strong></td>'
+ . '<td>' . $this->data['groupConfig']['group_class'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Custom Filter') . '</strong></td>'
+ . '<td>' . (trim($this->data['groupConfig']['group_filter']) ?: t('None', 'auth.ldap.filter')) . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Group Name Attribute') . '</strong></td>'
+ . '<td>' . $this->data['groupConfig']['group_name_attribute'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Group Member Attribute') . '</strong></td>'
+ . '<td>' . $this->data['groupConfig']['group_member_attribute'] . '</td>'
+ . '</tr>'
+ . '</tbody>'
+ . '</table>';
+
+ return $pageTitle . '<div class="topic">' . $backendTitle . $backendHtml . '</div>';
+ }
+
+ public function getReport()
+ {
+ $report = array();
+
+ if ($this->groupIniError === false) {
+ $report[] = sprintf(
+ mt('setup', 'User Group Backend configuration has been successfully written to: %s'),
+ Config::resolvePath('groups.ini')
+ );
+ } elseif ($this->groupIniError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'User Group Backend configuration could not be written to: %s. An error occured:'),
+ Config::resolvePath('groups.ini')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->groupIniError));
+ }
+
+ if ($this->groupError === false) {
+ $report[] = sprintf(
+ mt('setup', 'User Group "%s" has been successfully created.'),
+ mt('setup', 'Administrators', 'setup.role.name')
+ );
+ } elseif ($this->groupError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'Unable to create user group "%s". An error occured:'),
+ mt('setup', 'Administrators', 'setup.role.name')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->groupError));
+ }
+
+ if ($this->memberError === false) {
+ $report[] = sprintf(
+ mt('setup', 'Account "%s" has been successfully added as member to user group "%s".'),
+ $this->data['username'],
+ mt('setup', 'Administrators', 'setup.role.name')
+ );
+ } elseif ($this->memberError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'Unable to add account "%s" as member to user group "%s". An error occured:'),
+ $this->data['username'],
+ mt('setup', 'Administrators', 'setup.role.name')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->memberError));
+ }
+
+ return $report;
+ }
+}
diff --git a/modules/setup/library/Setup/Utils/DbTool.php b/modules/setup/library/Setup/Utils/DbTool.php
new file mode 100644
index 0000000..5cf203e
--- /dev/null
+++ b/modules/setup/library/Setup/Utils/DbTool.php
@@ -0,0 +1,943 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Utils;
+
+use PDO;
+use PDOException;
+use LogicException;
+use Zend_Db_Adapter_Pdo_Mysql;
+use Zend_Db_Adapter_Pdo_Pgsql;
+use Icinga\Util\File;
+use Icinga\Exception\ConfigurationError;
+
+/**
+ * Utility class to ease working with databases when setting up Icinga Web 2 or one of its modules
+ */
+class DbTool
+{
+ /**
+ * The PDO database connection
+ *
+ * @var PDO
+ */
+ protected $pdoConn;
+
+ /**
+ * The Zend database adapter
+ *
+ * @var Zend_Db_Adapter_Pdo_Abstract
+ */
+ protected $zendConn;
+
+ /**
+ * The resource configuration
+ *
+ * @var array
+ */
+ protected $config;
+
+ /**
+ * Whether we are connected to the database from the resource configuration
+ *
+ * @var bool
+ */
+ protected $dbFromConfig = false;
+
+ /**
+ * GRANT privilege level identifiers
+ */
+ const GLOBAL_LEVEL = 1;
+ const PROCEDURE_LEVEL = 2;
+ const DATABASE_LEVEL = 4;
+ const TABLE_LEVEL = 8;
+ const COLUMN_LEVEL = 16;
+ const FUNCTION_LEVEL = 32;
+
+ /**
+ * All MySQL GRANT privileges with their respective level identifiers
+ *
+ * @var array
+ */
+ protected $mysqlGrantContexts = array(
+ 'ALL' => 31,
+ 'ALL PRIVILEGES' => 31,
+ 'ALTER' => 13,
+ 'ALTER ROUTINE' => 7,
+ 'CREATE' => 13,
+ 'CREATE ROUTINE' => 5,
+ 'CREATE TEMPORARY TABLES' => 5,
+ 'CREATE USER' => 1,
+ 'CREATE VIEW' => 13,
+ 'DELETE' => 13,
+ 'DROP' => 13,
+ 'EXECUTE' => 5, // MySQL reference states this also supports database level, 5.1.73 not though
+ 'FILE' => 1,
+ 'GRANT OPTION' => 15,
+ 'INDEX' => 13,
+ 'INSERT' => 29,
+ 'LOCK TABLES' => 5,
+ 'PROCESS' => 1,
+ 'REFERENCES' => 12,
+ 'RELOAD' => 1,
+ 'REPLICATION CLIENT' => 1,
+ 'REPLICATION SLAVE' => 1,
+ 'SELECT' => 29,
+ 'SHOW DATABASES' => 1,
+ 'SHOW VIEW' => 13,
+ 'SHUTDOWN' => 1,
+ 'SUPER' => 1,
+ 'UPDATE' => 29
+ );
+
+ /**
+ * All PostgreSQL GRANT privileges with their respective level identifiers
+ *
+ * @var array
+ */
+ protected $pgsqlGrantContexts = array(
+ 'ALL' => 63,
+ 'ALL PRIVILEGES' => 63,
+ 'SELECT' => 24,
+ 'INSERT' => 24,
+ 'UPDATE' => 24,
+ 'DELETE' => 8,
+ 'TRUNCATE' => 8,
+ 'REFERENCES' => 24,
+ 'TRIGGER' => 8,
+ 'CREATE' => 12,
+ 'CONNECT' => 4,
+ 'TEMPORARY' => 4,
+ 'TEMP' => 4,
+ 'EXECUTE' => 32,
+ 'USAGE' => 33,
+ 'CREATEROLE' => 1
+ );
+
+ /**
+ * Create a new DbTool
+ *
+ * @param array $config The resource configuration to use
+ */
+ public function __construct(array $config)
+ {
+ if (! isset($config['port'])) {
+ // TODO: This is not quite correct, but works as it previously did. Previously empty values were not
+ // transformed no NULL (now they are) so if the port is now null, it's been the empty string.
+ $config['port'] = '';
+ }
+
+ $this->config = $config;
+ }
+
+ /**
+ * Connect to the server
+ *
+ * @return $this
+ */
+ public function connectToHost()
+ {
+ $this->assertHostAccess();
+
+ if ($this->config['db'] == 'pgsql') {
+ // PostgreSQL requires us to specify a database on each connection and will use
+ // the current user name as default database in cases none is provided. If
+ // that database doesn't exist (which might be the case here) it will error.
+ // Therefore, we specify the maintenance database 'postgres' as database, which
+ // is most probably present and public. (http://stackoverflow.com/q/4483139)
+ $this->connect('postgres');
+ } else {
+ $this->connect();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Connect to the database
+ *
+ * @return $this
+ */
+ public function connectToDb()
+ {
+ $this->assertHostAccess();
+ $this->assertDatabaseAccess();
+ $this->connect($this->config['dbname']);
+ return $this;
+ }
+
+ /**
+ * Assert that all configuration values exist that are required to connect to a server
+ *
+ * @throws ConfigurationError
+ */
+ protected function assertHostAccess()
+ {
+ if (! isset($this->config['db'])) {
+ throw new ConfigurationError('Can\'t connect to database server of unknown type');
+ } elseif (! isset($this->config['host'])) {
+ throw new ConfigurationError('Can\'t connect to database server without a hostname or address');
+ } elseif (! isset($this->config['port'])) {
+ throw new ConfigurationError('Can\'t connect to database server without a port');
+ } elseif (! isset($this->config['username'])) {
+ throw new ConfigurationError('Can\'t connect to database server without a username');
+ } elseif (! isset($this->config['password'])) {
+ throw new ConfigurationError('Can\'t connect to database server without a password');
+ }
+ }
+
+ /**
+ * Assert that all configuration values exist that are required to connect to a database
+ *
+ * @throws ConfigurationError
+ */
+ protected function assertDatabaseAccess()
+ {
+ if (! isset($this->config['dbname'])) {
+ throw new ConfigurationError('Can\'t connect to database without a valid database name');
+ }
+ }
+
+ /**
+ * Assert that a connection with a database has been established
+ *
+ * @throws LogicException
+ */
+ protected function assertConnectedToDb()
+ {
+ if ($this->zendConn === null) {
+ throw new LogicException('Not connected to database');
+ }
+ }
+
+ /**
+ * Return whether a connection with the server has been established
+ *
+ * @return bool
+ */
+ public function isConnected()
+ {
+ return $this->pdoConn !== null;
+ }
+
+ /**
+ * Establish a connection with the database or just the server by omitting the database name
+ *
+ * @param string $dbname The name of the database to connect to
+ */
+ public function connect($dbname = null)
+ {
+ $this->pdoConnect($dbname);
+ if ($dbname !== null) {
+ $this->zendConnect($dbname);
+ $this->dbFromConfig = $dbname === $this->config['dbname'];
+ }
+ }
+
+ /**
+ * Reestablish a connection with the database or just the server by omitting the database name
+ *
+ * @param string $dbname The name of the database to connect to
+ */
+ public function reconnect($dbname = null)
+ {
+ $this->pdoConn = null;
+ $this->zendConn = null;
+ $this->connect($dbname);
+ }
+
+ /**
+ * Initialize Zend database adapter
+ *
+ * @param string $dbname The name of the database to connect with
+ *
+ * @throws ConfigurationError In case the resource type is not a supported PDO driver name
+ */
+ private function zendConnect($dbname)
+ {
+ if ($this->zendConn !== null) {
+ return;
+ }
+
+ $config = array(
+ 'dbname' => $dbname,
+ 'host' => $this->config['host'],
+ 'port' => $this->config['port'],
+ 'username' => $this->config['username'],
+ 'password' => $this->config['password']
+ );
+
+ if ($this->config['db'] === 'mysql') {
+ if (isset($this->config['use_ssl']) && $this->config['use_ssl']) {
+ $this->config['driver_options'] = array();
+ # The presence of these keys as empty strings or null cause non-ssl connections to fail
+ if ($this->config['ssl_key']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key'];
+ }
+ if ($this->config['ssl_cert']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert'];
+ }
+ if ($this->config['ssl_ca']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca'];
+ }
+ if ($this->config['ssl_capath']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath'];
+ }
+ if ($this->config['ssl_cipher']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher'];
+ }
+ if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
+ && $this->config['ssl_do_not_verify_server_cert']
+ ) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
+ }
+ }
+ $this->zendConn = new Zend_Db_Adapter_Pdo_Mysql($config);
+ } elseif ($this->config['db'] === 'pgsql') {
+ $this->zendConn = new Zend_Db_Adapter_Pdo_Pgsql($config);
+ } else {
+ throw new ConfigurationError(
+ 'Failed to connect to database. Unsupported PDO driver "%s"',
+ $this->config['db']
+ );
+ }
+
+ $this->zendConn->getConnection(); // Force connection attempt
+ }
+
+ /**
+ * Initialize PDO connection
+ *
+ * @param string $dbname The name of the database to connect with
+ */
+ private function pdoConnect($dbname)
+ {
+ if ($this->pdoConn !== null) {
+ return;
+ }
+
+ $driverOptions = array(
+ PDO::ATTR_TIMEOUT => 1,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+ );
+
+ if ($this->config['db'] === 'mysql'
+ && isset($this->config['use_ssl'])
+ && $this->config['use_ssl']
+ ) {
+ # The presence of these keys as empty strings or null cause non-ssl connections to fail
+ if ($this->config['ssl_key']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key'];
+ }
+ if ($this->config['ssl_cert']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert'];
+ }
+ if ($this->config['ssl_ca']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca'];
+ }
+ if ($this->config['ssl_capath']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath'];
+ }
+ if ($this->config['ssl_cipher']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher'];
+ }
+ if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
+ && $this->config['ssl_do_not_verify_server_cert']
+ ) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
+ }
+ }
+
+ $this->pdoConn = new PDO(
+ $this->buildDsn($this->config['db'], $dbname),
+ $this->config['username'],
+ $this->config['password'],
+ $driverOptions
+ );
+ }
+
+ /**
+ * Return a datasource name for the given database type and name
+ *
+ * @param string $dbtype
+ * @param string $dbname
+ *
+ * @return string
+ *
+ * @throws ConfigurationError In case the passed database type is not supported
+ */
+ protected function buildDsn($dbtype, $dbname = null)
+ {
+ if ($dbtype === 'mysql') {
+ return 'mysql:host=' . $this->config['host'] . ';port=' . $this->config['port']
+ . ($dbname !== null ? ';dbname=' . $dbname : '');
+ } elseif ($dbtype === 'pgsql') {
+ return 'pgsql:host=' . $this->config['host'] . ';port=' . $this->config['port']
+ . ($dbname !== null ? ';dbname=' . $dbname : '');
+ } else {
+ throw new ConfigurationError(
+ 'Failed to build data source name. Unsupported PDO driver "%s"',
+ $dbtype
+ );
+ }
+ }
+
+ /**
+ * Try to connect to the server and throw an exception if this fails
+ *
+ * @throws PDOException In case an error occurs that does not indicate that authentication failed
+ */
+ public function checkConnectivity()
+ {
+ try {
+ $this->connectToHost();
+ } catch (PDOException $e) {
+ if ($this->config['db'] === 'mysql') {
+ $code = $e->getCode();
+ /*
+ * 1040 .. Too many connections
+ * 1045 .. Access denied for user '%s'@'%s' (using password: %s)
+ * 1698 .. Access denied for user '%s'@'%s'
+ */
+ if ($code !== 1040 && $code !== 1045 && $code !== 1698) {
+ throw $e;
+ }
+ } elseif ($this->config['db'] === 'pgsql') {
+ if (strpos($e->getMessage(), $this->config['username']) === false) {
+ throw $e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return the given identifier escaped with backticks
+ *
+ * @param string $identifier The identifier to escape
+ *
+ * @return string
+ *
+ * @throws LogicException In case there is no behaviour implemented for the current PDO driver
+ */
+ public function quoteIdentifier($identifier)
+ {
+ if ($this->config['db'] === 'mysql') {
+ return '`' . str_replace('`', '``', $identifier) . '`';
+ } elseif ($this->config['db'] === 'pgsql') {
+ return '"' . str_replace('"', '""', $identifier) . '"';
+ } else {
+ throw new LogicException('Unable to quote identifier.');
+ }
+ }
+
+ /**
+ * Return the given table name with all wildcards being escaped
+ *
+ * @param string $tableName
+ *
+ * @return string
+ *
+ * @throws LogicException In case there is no behaviour implemented for the current PDO driver
+ */
+ public function escapeTableWildcards($tableName)
+ {
+ if ($this->config['db'] === 'mysql') {
+ return str_replace(array('_', '%'), array('\_', '\%'), $tableName);
+ }
+
+ throw new LogicException('Unable to escape table wildcards.');
+ }
+
+ /**
+ * Return the given value escaped as string
+ *
+ * @param mixed $value The value to escape
+ *
+ * @return string
+ *
+ * @throws LogicException In case there is no behaviour implemented for the current PDO driver
+ */
+ public function quote($value)
+ {
+ $quoted = $this->pdoConn->quote($value);
+ if ($quoted === false) {
+ throw new LogicException(sprintf('Unable to quote value: %s', $value));
+ }
+
+ return $quoted;
+ }
+
+ /**
+ * Execute a SQL statement and return the affected row count
+ *
+ * Use $params to use a prepared statement.
+ *
+ * @param string $statement The statement to execute
+ * @param array $params The params to bind
+ *
+ * @return int
+ */
+ public function exec($statement, $params = array())
+ {
+ if (empty($params)) {
+ return $this->pdoConn->exec($statement);
+ }
+
+ $stmt = $this->pdoConn->prepare($statement);
+ $stmt->execute($params);
+ return $stmt->rowCount();
+ }
+
+ /**
+ * Execute a SQL statement and return the result
+ *
+ * Use $params to use a prepared statement.
+ *
+ * @param string $statement The statement to execute
+ * @param array $params The params to bind
+ *
+ * @return mixed
+ */
+ public function query($statement, $params = array())
+ {
+ if ($this->zendConn !== null) {
+ return $this->zendConn->query($statement, $params);
+ }
+
+ if (empty($params)) {
+ return $this->pdoConn->query($statement);
+ }
+
+ $stmt = $this->pdoConn->prepare($statement);
+ $stmt->execute($params);
+ return $stmt;
+ }
+
+ /**
+ * Return the version of the server currently connected to
+ *
+ * @return string|null
+ */
+ public function getServerVersion()
+ {
+ if ($this->config['db'] === 'mysql') {
+ return $this->query('show variables like "version"')->fetchColumn(1) ?: null;
+ } elseif ($this->config['db'] === 'pgsql') {
+ return $this->query('show server_version')->fetchColumn() ?: null;
+ } else {
+ throw new LogicException(
+ sprintf('Unable to fetch the server\'s version. Unsupported PDO driver "%s"', $this->config['db'])
+ );
+ }
+ }
+
+ /**
+ * Import the given SQL file
+ *
+ * @param string $filepath The file to import
+ */
+ public function import($filepath)
+ {
+ $file = new File($filepath);
+ $content = join(PHP_EOL, iterator_to_array($file)); // There is no fread() before PHP 5.5 :(
+
+ foreach (preg_split('@;(?! \\\\)@', $content) as $statement) {
+ if (($statement = trim($statement)) !== '') {
+ $this->exec($statement);
+ }
+ }
+ }
+
+ /**
+ * Return whether the given privileges were granted
+ *
+ * @param array $privileges An array of strings with the required privilege names
+ * @param array $context An array describing the context for which the given privileges need to apply.
+ * Only one or more table names are currently supported
+ * @param string $username The login name for which to check the privileges,
+ * if NULL the current login is used
+ *
+ * @return bool
+ */
+ public function checkPrivileges(array $privileges, array $context = null, $username = null)
+ {
+ if ($this->config['db'] === 'mysql') {
+ return $this->checkMysqlPrivileges($privileges, false, $context, $username);
+ } elseif ($this->config['db'] === 'pgsql') {
+ return $this->checkPgsqlPrivileges($privileges, false, $context, $username);
+ }
+ }
+
+ /**
+ * Return whether the given privileges are grantable to other users
+ *
+ * @param array $privileges The privileges that should be grantable
+ *
+ * @return bool
+ */
+ public function isGrantable($privileges)
+ {
+ if ($this->config['db'] === 'mysql') {
+ return $this->checkMysqlPrivileges($privileges, true);
+ } elseif ($this->config['db'] === 'pgsql') {
+ return $this->checkPgsqlPrivileges($privileges, true);
+ }
+ }
+
+ /**
+ * Grant all given privileges to the given user
+ *
+ * @param array $privileges The privilege names to grant
+ * @param array $context An array describing the context for which the given privileges need to apply.
+ * Only one or more table names are currently supported
+ * @param string $username The username to grant the privileges to
+ */
+ public function grantPrivileges(array $privileges, array $context, $username)
+ {
+ if ($this->config['db'] === 'mysql') {
+ list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn());
+ $quotedDbName = $this->quoteIdentifier($this->config['dbname']);
+
+ $grant = 'GRANT %s';
+ $on = ' ON %s.%s';
+ $to = sprintf(
+ ' TO %s@%s',
+ $this->quoteIdentifier($username),
+ $this->quoteIdentifier($host)
+ );
+
+ $dbPrivileges = array();
+ $tablePrivileges = array();
+ foreach (array_intersect($privileges, array_keys($this->mysqlGrantContexts)) as $privilege) {
+ if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) {
+ $tablePrivileges[] = $privilege;
+ } elseif ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+ }
+
+ if (! empty($tablePrivileges)) {
+ $tableGrant = sprintf($grant, join(',', $tablePrivileges));
+ foreach ($context as $table) {
+ $this->exec($tableGrant . sprintf($on, $quotedDbName, $this->quoteIdentifier($table)) . $to);
+ }
+ }
+
+ if (! empty($dbPrivileges)) {
+ $this->exec(
+ sprintf($grant, join(',', $dbPrivileges))
+ . sprintf($on, $this->escapeTableWildcards($quotedDbName), '*')
+ . $to
+ );
+ }
+ } elseif ($this->config['db'] === 'pgsql') {
+ $dbPrivileges = array();
+ $tablePrivileges = array();
+ foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) {
+ if (! empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) {
+ $tablePrivileges[] = $privilege;
+ } elseif ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+ }
+
+ if (! empty($dbPrivileges)) {
+ $this->exec(sprintf(
+ 'GRANT %s ON DATABASE %s TO %s',
+ join(',', $dbPrivileges),
+ $this->config['dbname'],
+ $username
+ ));
+ }
+
+ if (! empty($tablePrivileges)) {
+ foreach ($context as $table) {
+ $this->exec(sprintf(
+ 'GRANT %s ON TABLE %s TO %s',
+ join(',', $tablePrivileges),
+ $table,
+ $username
+ ));
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a list of all existing database tables
+ *
+ * @return array
+ */
+ public function listTables()
+ {
+ $this->assertConnectedToDb();
+ return $this->zendConn->listTables();
+ }
+
+ /**
+ * Return whether the given database login exists
+ *
+ * @param string $username The username to search
+ *
+ * @return bool
+ */
+ public function hasLogin($username)
+ {
+ if ($this->config['db'] === 'mysql') {
+ $queryString = <<<EOD
+SELECT 1
+ FROM information_schema.user_privileges
+ WHERE grantee = REPLACE(CONCAT("'", REPLACE(CURRENT_USER(), '@', "'@'"), "'"), :current, :wanted)
+EOD;
+
+ $query = $this->query(
+ $queryString,
+ array(
+ ':current' => $this->config['username'],
+ ':wanted' => $username
+ )
+ );
+ return count($query->fetchAll()) > 0;
+ } elseif ($this->config['db'] === 'pgsql') {
+ $query = $this->query(
+ 'SELECT 1 FROM pg_catalog.pg_user WHERE usename = :ident LIMIT 1',
+ array(':ident' => $username)
+ );
+ return count($query->fetchAll()) === 1;
+ }
+ }
+
+ /**
+ * Add a new database login
+ *
+ * @param string $username The username of the new login
+ * @param string $password The password of the new login
+ */
+ public function addLogin($username, $password)
+ {
+ if ($this->config['db'] === 'mysql') {
+ list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn());
+ $this->exec(
+ 'CREATE USER :user@:host IDENTIFIED BY :passw',
+ array(':user' => $username, ':host' => $host, ':passw' => $password)
+ );
+ } elseif ($this->config['db'] === 'pgsql') {
+ $this->exec(sprintf(
+ 'CREATE USER %s WITH PASSWORD %s',
+ $this->quoteIdentifier($username),
+ $this->quote($password)
+ ));
+ }
+ }
+
+ /**
+ * Check whether the current user has the given privileges
+ *
+ * @param array $privileges The privilege names
+ * @param bool $requireGrants Only return true when all privileges can be granted to others
+ * @param array $context An array describing the context for which the given privileges need to apply.
+ * Only one or more table names are currently supported
+ * @param string $username The login name to which the passed privileges need to be granted
+ *
+ * @return bool
+ */
+ protected function checkMysqlPrivileges(
+ array $privileges,
+ $requireGrants = false,
+ array $context = null,
+ $username = null
+ ) {
+ $mysqlPrivileges = array_intersect($privileges, array_keys($this->mysqlGrantContexts));
+ list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn());
+ $grantee = "'" . ($username === null ? $this->config['username'] : $username) . "'@'" . $host . "'";
+
+ if (isset($this->config['dbname'])) {
+ $dbPrivileges = array();
+ $tablePrivileges = array();
+ foreach ($mysqlPrivileges as $privilege) {
+ if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) {
+ $tablePrivileges[] = $privilege;
+ }
+ if ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+ }
+
+ $dbPrivilegesGranted = true;
+ $tablePrivilegesGranted = true;
+
+ if (! empty($dbPrivileges)) {
+ $queryString = 'SELECT COUNT(*) as matches'
+ . ' FROM information_schema.schema_privileges'
+ . ' WHERE grantee = :grantee'
+ . ' AND table_schema = :dbname'
+ . ' AND privilege_type IN (%s)'
+ . ($requireGrants ? " AND is_grantable = 'YES'" : '');
+
+ $dbAndTableQuery = $this->query(
+ sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbPrivileges))),
+ array(':grantee' => $grantee, ':dbname' => $this->escapeTableWildcards($this->config['dbname']))
+ );
+ $grantedDbAndTablePrivileges = (int) $dbAndTableQuery->fetchObject()->matches;
+ if ($grantedDbAndTablePrivileges === count($dbPrivileges)) {
+ $tableExclusivePrivileges = array_diff($tablePrivileges, $dbPrivileges);
+ if (! empty($tableExclusivePrivileges)) {
+ $tablePrivileges = $tableExclusivePrivileges;
+ $tablePrivilegesGranted = false;
+ }
+ } else {
+ $tablePrivilegesGranted = false;
+ $dbExclusivePrivileges = array_diff($dbPrivileges, $tablePrivileges);
+ if (! empty($dbExclusivePrivileges)) {
+ $dbExclusiveQuery = $this->query(
+ sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbExclusivePrivileges))),
+ array(
+ ':grantee' => $grantee,
+ ':dbname' => $this->escapeTableWildcards($this->config['dbname'])
+ )
+ );
+ $dbPrivilegesGranted = (int) $dbExclusiveQuery->fetchObject()->matches === count(
+ $dbExclusivePrivileges
+ );
+ }
+ }
+ }
+
+ if (! $tablePrivilegesGranted && !empty($tablePrivileges)) {
+ $query = $this->query(
+ 'SELECT COUNT(*) as matches'
+ . ' FROM information_schema.table_privileges'
+ . ' WHERE grantee = :grantee'
+ . ' AND table_schema = :dbname'
+ . ' AND table_name IN (' . join(',', array_map(array($this, 'quote'), $context)) . ')'
+ . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $tablePrivileges)) . ')'
+ . ($requireGrants ? " AND is_grantable = 'YES'" : ''),
+ array(':grantee' => $grantee, ':dbname' => $this->config['dbname'])
+ );
+ $expectedAmountOfMatches = count($context) * count($tablePrivileges);
+ $tablePrivilegesGranted = (int) $query->fetchObject()->matches === $expectedAmountOfMatches;
+ }
+
+ if ($dbPrivilegesGranted && $tablePrivilegesGranted) {
+ return true;
+ }
+ }
+
+ $query = $this->query(
+ 'SELECT COUNT(*) as matches FROM information_schema.user_privileges WHERE grantee = :grantee'
+ . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $mysqlPrivileges)) . ')'
+ . ($requireGrants ? " AND is_grantable = 'YES'" : ''),
+ array(':grantee' => $grantee)
+ );
+ return (int) $query->fetchObject()->matches === count($mysqlPrivileges);
+ }
+
+ /**
+ * Check whether the current user has the given privileges
+ *
+ * Note that database and table specific privileges (i.e. not SUPER, CREATE and CREATEROLE) are ignored
+ * in case no connection to the database defined in the resource configuration has been established
+ *
+ * @param array $privileges The privilege names
+ * @param bool $requireGrants Only return true when all privileges can be granted to others
+ * @param array $context An array describing the context for which the given privileges need to apply.
+ * Only one or more table names are currently supported
+ * @param string $username The login name to which the passed privileges need to be granted
+ *
+ * @return bool
+ */
+ public function checkPgsqlPrivileges(
+ array $privileges,
+ $requireGrants = false,
+ array $context = null,
+ $username = null
+ ) {
+ $privilegesGranted = true;
+ if ($this->dbFromConfig) {
+ $dbPrivileges = array();
+ $tablePrivileges = array();
+ foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) {
+ if (! empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) {
+ $tablePrivileges[] = $privilege;
+ }
+ if ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+ }
+
+ if (! empty($dbPrivileges)) {
+ $dbExclusivesGranted = true;
+ foreach ($dbPrivileges as $dbPrivilege) {
+ $query = $this->query(
+ 'SELECT has_database_privilege(:user, :dbname, :privilege) AS db_privilege_granted',
+ array(
+ ':user' => $username !== null ? $username : $this->config['username'],
+ ':dbname' => $this->config['dbname'],
+ ':privilege' => $dbPrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '')
+ )
+ );
+ if (! $query->fetchObject()->db_privilege_granted) {
+ $privilegesGranted = false;
+ if (! in_array($dbPrivilege, $tablePrivileges)) {
+ $dbExclusivesGranted = false;
+ }
+ }
+ }
+
+ if ($privilegesGranted) {
+ // Do not check privileges twice if they are already granted at database level
+ $tablePrivileges = array_diff($tablePrivileges, $dbPrivileges);
+ } elseif ($dbExclusivesGranted) {
+ $privilegesGranted = true;
+ }
+ }
+
+ if ($privilegesGranted && !empty($tablePrivileges)) {
+ foreach (array_intersect($context, $this->listTables()) as $table) {
+ foreach ($tablePrivileges as $tablePrivilege) {
+ $query = $this->query(
+ 'SELECT has_table_privilege(:user, :table, :privilege) AS table_privilege_granted',
+ array(
+ ':user' => $username !== null ? $username : $this->config['username'],
+ ':table' => $table,
+ ':privilege' => $tablePrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '')
+ )
+ );
+ $privilegesGranted &= $query->fetchObject()->table_privilege_granted;
+ }
+ }
+ }
+ } else {
+ // In case we cannot check whether the user got the required db-/table-privileges due to not being
+ // connected to the database defined in the resource configuration it is safe to just ignore them
+ // as the chances are very high that the database is created later causing the current user being
+ // the owner with ALL privileges. (Which in turn can be granted to others.)
+
+ if (array_search('CREATE', $privileges, true) !== false) {
+ $query = $this->query(
+ 'select rolcreatedb from pg_roles where rolname = :user',
+ array(':user' => $username !== null ? $username : $this->config['username'])
+ );
+ $privilegesGranted &= $query->fetchColumn() !== false;
+ }
+ }
+
+ if (array_search('CREATEROLE', $privileges, true) !== false) {
+ $query = $this->query(
+ 'select rolcreaterole from pg_roles where rolname = :user',
+ array(':user' => $username !== null ? $username : $this->config['username'])
+ );
+ $privilegesGranted &= $query->fetchColumn() !== false;
+ }
+
+ if (array_search('SUPER', $privileges, true) !== false) {
+ $query = $this->query(
+ 'select rolsuper from pg_roles where rolname = :user',
+ array(':user' => $username !== null ? $username : $this->config['username'])
+ );
+ $privilegesGranted &= $query->fetchColumn() !== false;
+ }
+
+ return (bool) $privilegesGranted;
+ }
+}
diff --git a/modules/setup/library/Setup/Utils/EnableModuleStep.php b/modules/setup/library/Setup/Utils/EnableModuleStep.php
new file mode 100644
index 0000000..92af5b7
--- /dev/null
+++ b/modules/setup/library/Setup/Utils/EnableModuleStep.php
@@ -0,0 +1,77 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Utils;
+
+use Exception;
+use Icinga\Application\Icinga;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+
+class EnableModuleStep extends Step
+{
+ protected $modulePaths;
+
+ protected $moduleNames;
+
+ protected $errors;
+
+ protected $warnings;
+
+ public function __construct(array $moduleNames)
+ {
+ $this->moduleNames = $moduleNames;
+
+ $this->modulePaths = array();
+ if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) {
+ $this->modulePaths[] = $appModulePath;
+ }
+ }
+
+ public function apply()
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ $moduleManager->detectInstalledModules($this->modulePaths);
+
+ $success = true;
+ foreach ($this->moduleNames as $moduleName) {
+ try {
+ $moduleManager->enableModule($moduleName);
+ } catch (ConfigurationError $e) {
+ $this->warnings[$moduleName] = $e;
+ } catch (Exception $e) {
+ $this->errors[$moduleName] = $e;
+ $success = false;
+ }
+ }
+
+ return $success;
+ }
+
+ public function getSummary()
+ {
+ // Enabling a module is like a implicit action, which does not need to be shown to the user...
+ }
+
+ public function getReport()
+ {
+ $okMessage = mt('setup', 'Module "%s" has been successfully enabled.');
+ $failMessage = mt('setup', 'Module "%s" could not be enabled. An error occured:');
+
+ $report = array();
+ foreach ($this->moduleNames as $moduleName) {
+ if (isset($this->errors[$moduleName])) {
+ $report[] = sprintf($failMessage, $moduleName);
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->errors[$moduleName]));
+ } elseif (isset($this->warnings[$moduleName])) {
+ $report[] = sprintf($failMessage, $moduleName);
+ $report[] = sprintf(mt('setup', 'WARNING: %s'), $this->warnings[$moduleName]->getMessage());
+ } else {
+ $report[] = sprintf($okMessage, $moduleName);
+ }
+ }
+
+ return $report;
+ }
+}
diff --git a/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php b/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php
new file mode 100644
index 0000000..a3f218b
--- /dev/null
+++ b/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php
@@ -0,0 +1,73 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Web\Form\Validator;
+
+use Exception;
+use Zend_Validate_Abstract;
+use Icinga\Util\File;
+
+/**
+ * Validator that checks if a token matches with the contents of a corresponding token-file
+ */
+class TokenValidator extends Zend_Validate_Abstract
+{
+ /**
+ * The path to the token file
+ *
+ * @var string
+ */
+ protected $tokenPath;
+
+ /**
+ * Create a new TokenValidator
+ *
+ * @param string $tokenPath The path to the token-file
+ */
+ public function __construct($tokenPath)
+ {
+ $this->tokenPath = $tokenPath;
+ $this->_messageTemplates = array(
+ 'TOKEN_FILE_ERROR' => sprintf(
+ mt('setup', 'Cannot validate token: %s (%s)'),
+ $tokenPath,
+ '%value%'
+ ),
+ 'TOKEN_FILE_EMPTY' => sprintf(
+ mt('setup', 'Cannot validate token, file "%s" is empty. Please define a token.'),
+ $tokenPath
+ ),
+ 'TOKEN_INVALID' => mt('setup', 'Invalid token supplied.')
+ );
+ }
+
+ /**
+ * Validate the given token with the one in the token-file
+ *
+ * @param string $value The token to validate
+ * @param null $context The form context (ignored)
+ *
+ * @return bool
+ */
+ public function isValid($value, $context = null)
+ {
+ try {
+ $file = new File($this->tokenPath);
+ $expectedToken = trim($file->fgets());
+ } catch (Exception $e) {
+ $msg = $e->getMessage();
+ $this->_error('TOKEN_FILE_ERROR', substr($msg, strpos($msg, ']: ') + 3));
+ return false;
+ }
+
+ if (empty($expectedToken)) {
+ $this->_error('TOKEN_FILE_EMPTY');
+ return false;
+ } elseif ($value !== $expectedToken) {
+ $this->_error('TOKEN_INVALID');
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/modules/setup/library/Setup/WebWizard.php b/modules/setup/library/Setup/WebWizard.php
new file mode 100644
index 0000000..f25be55
--- /dev/null
+++ b/modules/setup/library/Setup/WebWizard.php
@@ -0,0 +1,752 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement\SetRequirement;
+use Icinga\Module\Setup\Requirement\WebLibraryRequirement;
+use PDOException;
+use Icinga\Web\Form;
+use Icinga\Web\Wizard;
+use Icinga\Web\Request;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Module\Setup\Forms\ModulePage;
+use Icinga\Module\Setup\Forms\WelcomePage;
+use Icinga\Module\Setup\Forms\SummaryPage;
+use Icinga\Module\Setup\Forms\DbResourcePage;
+use Icinga\Module\Setup\Forms\AuthBackendPage;
+use Icinga\Module\Setup\Forms\AdminAccountPage;
+use Icinga\Module\Setup\Forms\LdapDiscoveryPage;
+//use Icinga\Module\Setup\Forms\LdapDiscoveryConfirmPage;
+use Icinga\Module\Setup\Forms\LdapResourcePage;
+use Icinga\Module\Setup\Forms\RequirementsPage;
+use Icinga\Module\Setup\Forms\GeneralConfigPage;
+use Icinga\Module\Setup\Forms\AuthenticationPage;
+use Icinga\Module\Setup\Forms\DatabaseCreationPage;
+use Icinga\Module\Setup\Forms\UserGroupBackendPage;
+use Icinga\Module\Setup\Steps\DatabaseStep;
+use Icinga\Module\Setup\Steps\GeneralConfigStep;
+use Icinga\Module\Setup\Steps\ResourceStep;
+use Icinga\Module\Setup\Steps\AuthenticationStep;
+use Icinga\Module\Setup\Steps\UserGroupStep;
+use Icinga\Module\Setup\Utils\EnableModuleStep;
+use Icinga\Module\Setup\Utils\DbTool;
+use Icinga\Module\Setup\Requirement\OSRequirement;
+use Icinga\Module\Setup\Requirement\PhpModuleRequirement;
+use Icinga\Module\Setup\Requirement\PhpVersionRequirement;
+use Icinga\Module\Setup\Requirement\ConfigDirectoryRequirement;
+use Icinga\Module\Monitoring\Forms\Config\Transport\ApiTransportForm;
+
+/**
+ * Icinga Web 2 Setup Wizard
+ */
+class WebWizard extends Wizard implements SetupWizard
+{
+ /**
+ * The privileges required by Icinga Web 2 to create the database and a login
+ *
+ * @var array
+ */
+ protected $databaseCreationPrivileges = array(
+ 'CREATE',
+ 'CREATE USER', // MySQL
+ 'CREATEROLE' // PostgreSQL
+ );
+
+ /**
+ * The privileges required by Icinga Web 2 to setup the database
+ *
+ * @var array
+ */
+ protected $databaseSetupPrivileges = array(
+ 'CREATE',
+ 'ALTER', // MySQL only
+ 'REFERENCES'
+ );
+
+ /**
+ * The privileges required by Icinga Web 2 to operate the database
+ *
+ * @var array
+ */
+ protected $databaseUsagePrivileges = array(
+ 'SELECT',
+ 'INSERT',
+ 'UPDATE',
+ 'DELETE',
+ 'EXECUTE',
+ 'TEMPORARY', // PostgreSql
+ 'CREATE TEMPORARY TABLES' // MySQL
+ );
+
+ /**
+ * The database tables operated by Icinga Web 2
+ *
+ * @var array
+ */
+ protected $databaseTables = array(
+ 'icingaweb_group',
+ 'icingaweb_group_membership',
+ 'icingaweb_user',
+ 'icingaweb_user_preference',
+ 'icingaweb_rememberme'
+ );
+
+ /**
+ * Register all pages and module wizards for this wizard
+ */
+ protected function init()
+ {
+ $this->addPage(new WelcomePage());
+ $this->addPage(new ModulePage());
+ $this->addPage(new RequirementsPage());
+ $this->addPage(new AuthenticationPage());
+ $this->addPage(new DbResourcePage(array('name' => 'setup_auth_db_resource')));
+ $this->addPage(new DatabaseCreationPage(array('name' => 'setup_auth_db_creation')));
+ $this->addPage(new LdapDiscoveryPage());
+ //$this->addPage(new LdapDiscoveryConfirmPage());
+ $this->addPage(new LdapResourcePage());
+ $this->addPage(new AuthBackendPage());
+ $this->addPage(new UserGroupBackendPage());
+ $this->addPage(new AdminAccountPage());
+ $this->addPage(new GeneralConfigPage());
+ $this->addPage(new DbResourcePage(array('name' => 'setup_config_db_resource')));
+ $this->addPage(new DatabaseCreationPage(array('name' => 'setup_config_db_creation')));
+ $this->addPage(new SummaryPage(array('name' => 'setup_summary')));
+
+ if (($modulePageData = $this->getPageData('setup_modules')) !== null) {
+ $modulePage = $this->getPage('setup_modules')->populate($modulePageData);
+ foreach ($modulePage->getModuleWizards() as $moduleWizard) {
+ $this->addPage($moduleWizard);
+ }
+ }
+ }
+
+ /**
+ * Setup the given page that is either going to be displayed or validated
+ *
+ * @param Form $page The page to setup
+ * @param Request $request The current request
+ */
+ public function setupPage(Form $page, Request $request)
+ {
+ if ($page->getName() === 'setup_requirements') {
+ $page->setWizard($this);
+ } elseif ($page->getName() === 'setup_authentication_backend') {
+ /** @var AuthBackendPage $page */
+
+ $authData = $this->getPageData('setup_authentication_type');
+ if ($authData['type'] === 'db') {
+ $page->setResourceConfig($this->getPageData('setup_auth_db_resource'));
+ } elseif ($authData['type'] === 'ldap') {
+ $page->setResourceConfig($this->getPageData('setup_ldap_resource'));
+
+ $suggestions = $this->getPageData('setup_ldap_discovery');
+ if (isset($suggestions['backend'])) {
+ $page->setSuggestions($suggestions['backend']);
+ }
+
+ if ($this->getDirection() === static::FORWARD) {
+ $backendConfig = $this->getPageData('setup_authentication_backend');
+ if ($backendConfig !== null && $request->getPost('name') !== $backendConfig['name']) {
+ $pageData = & $this->getPageData();
+ unset($pageData['setup_usergroup_backend']);
+ }
+ }
+ }
+
+ if ($this->getDirection() === static::FORWARD) {
+ $backendConfig = $this->getPageData('setup_authentication_backend');
+ if ($backendConfig !== null && $request->getPost('backend') !== $backendConfig['backend']) {
+ $pageData = & $this->getPageData();
+ unset($pageData['setup_usergroup_backend']);
+ }
+ }
+ /*} elseif ($page->getName() === 'setup_ldap_discovery_confirm') {
+ $page->setResourceConfig($this->getPageData('setup_ldap_discovery'));*/
+ } elseif ($page->getName() === 'setup_auth_db_resource') {
+ $page->addDescription(mt(
+ 'setup',
+ 'Now please configure the database resource where to store users and user groups.'
+ ));
+ $page->addDescription(mt(
+ 'setup',
+ 'Note that the database itself does not need to exist at this time as'
+ . ' it is going to be created once the wizard is about to be finished.'
+ ));
+ } elseif ($page->getName() === 'setup_usergroup_backend') {
+ $page->setResourceConfig($this->getPageData('setup_ldap_resource'));
+ $page->setBackendConfig($this->getPageData('setup_authentication_backend'));
+ } elseif ($page->getName() === 'setup_admin_account') {
+ $page->setBackendConfig($this->getPageData('setup_authentication_backend'));
+ $page->setGroupConfig($this->getPageData('setup_usergroup_backend'));
+ $authData = $this->getPageData('setup_authentication_type');
+ if ($authData['type'] === 'db') {
+ $page->setResourceConfig($this->getPageData('setup_auth_db_resource'));
+ } elseif ($authData['type'] === 'ldap') {
+ $page->setResourceConfig($this->getPageData('setup_ldap_resource'));
+ }
+ } elseif ($page->getName() === 'setup_auth_db_creation' || $page->getName() === 'setup_config_db_creation') {
+ $page->setDatabaseSetupPrivileges(
+ array_unique(array_merge($this->databaseCreationPrivileges, $this->databaseSetupPrivileges))
+ );
+ $page->setDatabaseUsagePrivileges($this->databaseUsagePrivileges);
+ $page->setResourceConfig(
+ $this->getPageData('setup_auth_db_resource') ?: $this->getPageData('setup_config_db_resource')
+ );
+ } elseif ($page->getName() === 'setup_summary') {
+ $page->setSubjectTitle('Icinga Web 2');
+ $page->setSummary($this->getSetup()->getSummary());
+ } elseif ($page->getName() === 'setup_config_db_resource') {
+ $page->addDescription(mt(
+ 'setup',
+ 'Now please configure the database resource where to store user preferences.'
+ ));
+ $page->addDescription(mt(
+ 'setup',
+ 'Note that the database itself does not need to exist at this time as'
+ . ' it is going to be created once the wizard is about to be finished.'
+ ));
+
+ $ldapData = $this->getPageData('setup_ldap_resource');
+ if ($ldapData !== null && $request->getPost('name') === $ldapData['name']) {
+ $page->error(
+ mt('setup', 'The given resource name must be unique and is already in use by the LDAP resource')
+ );
+ }
+ } elseif ($page->getName() === 'setup_ldap_resource') {
+ $suggestion = $this->getPageData('setup_ldap_discovery');
+ if (isset($suggestion['resource'])) {
+ $page->populate($suggestion['resource']);
+ }
+
+ if ($this->getDirection() === static::FORWARD) {
+ $resourceConfig = $this->getPageData('setup_ldap_resource');
+ if ($resourceConfig !== null && $request->getPost('name') !== $resourceConfig['name']) {
+ $pageData = & $this->getPageData();
+ unset($pageData['setup_usergroup_backend']);
+ }
+ }
+ } elseif ($page->getName() === 'setup_authentication_type') {
+ $authData = $this->getPageData($page->getName());
+ $pageData = & $this->getPageData();
+
+ if ($authData !== null && $request->getPost('type') !== $authData['type']) {
+ // Drop any existing page data in case the authentication type has changed,
+ // otherwise it will conflict with other forms that depend on this one
+ unset($pageData['setup_admin_account']);
+ unset($pageData['setup_authentication_backend']);
+
+ if ($authData['type'] === 'db') {
+ unset($pageData['setup_auth_db_resource']);
+ unset($pageData['setup_auth_db_creation']);
+ } elseif ($request->getPost('type') === 'db') {
+ unset($pageData['setup_config_db_resource']);
+ unset($pageData['setup_config_db_creation']);
+ }
+ } elseif (isset($authData['type']) && $authData['type'] == 'external') {
+ // If you choose the authentication type external and validate the database and then come
+ // back to change the authentication type but do not change it, you will get an database configuration
+ // related error message on the next page. To avoid this error, the 'setup_config_db_resource'
+ // page must be unset.
+
+ unset($pageData['setup_config_db_resource']);
+ }
+ }
+ }
+
+ /**
+ * Return the new page to set as current page
+ *
+ * {@inheritdoc} Runs additional checks related to some registered pages.
+ *
+ * @param string $requestedPage The name of the requested page
+ * @param Form $originPage The origin page
+ *
+ * @return Form The new page
+ *
+ * @throws InvalidArgumentException In case the requested page does not exist or is not permitted yet
+ */
+ protected function getNewPage($requestedPage, Form $originPage)
+ {
+ $skip = false;
+ $newPage = parent::getNewPage($requestedPage, $originPage);
+ if ($newPage->getName() === 'setup_auth_db_resource') {
+ $authData = $this->getPageData('setup_authentication_type');
+ $skip = $authData['type'] !== 'db';
+ } elseif ($newPage->getname() === 'setup_ldap_discovery') {
+ $authData = $this->getPageData('setup_authentication_type');
+ $skip = $authData['type'] !== 'ldap';
+ /*} elseif ($newPage->getName() === 'setup_ldap_discovery_confirm') {
+ $skip = false === $this->hasPageData('setup_ldap_discovery');*/
+ } elseif ($newPage->getName() === 'setup_ldap_resource') {
+ $authData = $this->getPageData('setup_authentication_type');
+ $skip = $authData['type'] !== 'ldap';
+ } elseif ($newPage->getName() === 'setup_usergroup_backend') {
+ $backendConfig = $this->getPageData('setup_authentication_backend');
+ $skip = $backendConfig['backend'] !== 'ldap';
+ } elseif ($newPage->getName() === 'setup_config_db_resource') {
+ $authData = $this->getPageData('setup_authentication_type');
+ $skip = $authData['type'] === 'db';
+ } elseif (in_array($newPage->getName(), array('setup_auth_db_creation', 'setup_config_db_creation'))) {
+ if (($newPage->getName() === 'setup_auth_db_creation' || $this->hasPageData('setup_config_db_resource'))
+ && (($config = $this->getPageData('setup_auth_db_resource')) !== null
+ || ($config = $this->getPageData('setup_config_db_resource')) !== null)
+ && !$config['skip_validation'] && $this->getDirection() == static::FORWARD
+ ) {
+ // Execute this code only if the direction is forward.
+ // Otherwise, an error will be output when you go back.
+ $db = new DbTool($config);
+
+ try {
+ $db->connectToDb(); // Are we able to login on the database?
+
+ if (array_search(reset($this->databaseTables), $db->listTables(), true) === false) {
+ // In case the database schema does not yet exist the
+ // user needs the privileges to setup the database
+ $skip = $db->checkPrivileges($this->databaseSetupPrivileges, $this->databaseTables);
+ } else {
+ // In case the database schema exists the user needs the required privileges
+ // to operate the database, if those are missing we ask for another user
+ $skip = $db->checkPrivileges($this->databaseUsagePrivileges, $this->databaseTables);
+ }
+ } catch (PDOException $_) {
+ try {
+ $db->connectToHost(); // Are we able to login on the server?
+ // It is not possible to reliably determine whether a database exists or not if a user can't
+ // log in to the database, so we just require the user to be able to create the database
+ $skip = $db->checkPrivileges(
+ array_unique(
+ array_merge($this->databaseCreationPrivileges, $this->databaseSetupPrivileges)
+ ),
+ $this->databaseTables
+ );
+ } catch (PDOException $_) {
+ // We are NOT able to login on the server..
+ }
+ }
+ } else {
+ $skip = true;
+ }
+ }
+
+ return $skip ? $this->skipPage($newPage) : $newPage;
+ }
+
+ /**
+ * Add buttons to the given page based on its position in the page-chain
+ *
+ * @param Form $page The page to add the buttons to
+ */
+ protected function addButtons(Form $page)
+ {
+ parent::addButtons($page);
+
+ $pages = $this->getPages();
+ $index = array_search($page, $pages, true);
+ if ($index === 0) {
+ $page->getElement(static::BTN_NEXT)->setLabel(
+ mt('setup', 'Start', 'setup.welcome.btn.next')
+ );
+ } elseif ($index === count($pages) - 1) {
+ $page->getElement(static::BTN_NEXT)->setLabel(
+ mt('setup', 'Setup Icinga Web 2', 'setup.summary.btn.finish')
+ );
+ }
+
+ $authData = $this->getPageData('setup_authentication_type');
+ $veto = $page->getName() === 'setup_authentication_backend' && $authData['type'] === 'db';
+ if (! $veto && in_array($page->getName(), array(
+ 'setup_authentication_backend',
+ 'setup_auth_db_resource',
+ 'setup_config_db_resource',
+ 'setup_ldap_resource',
+ 'setup_monitoring_ido',
+ 'setup_icingadb_resource',
+ 'setup_icingadb_redis',
+ 'setup_icingadb_api_transport'
+ ))) {
+ $page->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => t('Validate Configuration'),
+ 'data-progress-label' => t('Validation In Progress'),
+ 'decorators' => array('ViewHelper'),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ $page->getDisplayGroup('buttons')->addElement($page->getElement('backend_validation'));
+ }
+
+ if ($page->getName() === 'setup_command_transport') {
+ if ($page->getSubForm('transport_form')->getSubForm('transport_form') instanceof ApiTransportForm) {
+ $page->addElement(
+ 'submit',
+ 'transport_validation',
+ array(
+ 'ignore' => true,
+ 'label' => t('Validate Configuration'),
+ 'data-progress-label' => t('Validation In Progress'),
+ 'decorators' => array('ViewHelper'),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ $page->getDisplayGroup('buttons')->addElement($page->getElement('transport_validation'));
+ }
+ }
+ }
+
+ /**
+ * Clear the session being used by this wizard
+ *
+ * @param bool $removeToken If true, the setup token will be removed
+ */
+ public function clearSession($removeToken = true)
+ {
+ parent::clearSession();
+
+ if ($removeToken) {
+ $tokenPath = Config::resolvePath('setup.token');
+ if (file_exists($tokenPath)) {
+ @unlink($tokenPath);
+ }
+ }
+ }
+
+ /**
+ * Return the setup for this wizard
+ *
+ * @return Setup
+ */
+ public function getSetup()
+ {
+ $pageData = $this->getPageData();
+ $setup = new Setup();
+
+ if (isset($pageData['setup_auth_db_resource'])
+ && !$pageData['setup_auth_db_resource']['skip_validation']
+ && (! isset($pageData['setup_auth_db_creation'])
+ || !$pageData['setup_auth_db_creation']['skip_validation']
+ )
+ ) {
+ $setup->addStep(
+ new DatabaseStep(array(
+ 'tables' => $this->databaseTables,
+ 'privileges' => $this->databaseUsagePrivileges,
+ 'resourceConfig' => $pageData['setup_auth_db_resource'],
+ 'adminName' => isset($pageData['setup_auth_db_creation']['username'])
+ ? $pageData['setup_auth_db_creation']['username']
+ : null,
+ 'adminPassword' => isset($pageData['setup_auth_db_creation']['password'])
+ ? $pageData['setup_auth_db_creation']['password']
+ : null,
+ 'schemaPath' => Config::module('setup')
+ ->get('schema', 'path', Icinga::app()->getBaseDir('schema'))
+ ))
+ );
+ } elseif (isset($pageData['setup_config_db_resource'])
+ && !$pageData['setup_config_db_resource']['skip_validation']
+ && (! isset($pageData['setup_config_db_creation'])
+ || !$pageData['setup_config_db_creation']['skip_validation']
+ )
+ ) {
+ $setup->addStep(
+ new DatabaseStep(array(
+ 'tables' => $this->databaseTables,
+ 'privileges' => $this->databaseUsagePrivileges,
+ 'resourceConfig' => $pageData['setup_config_db_resource'],
+ 'adminName' => isset($pageData['setup_config_db_creation']['username'])
+ ? $pageData['setup_config_db_creation']['username']
+ : null,
+ 'adminPassword' => isset($pageData['setup_config_db_creation']['password'])
+ ? $pageData['setup_config_db_creation']['password']
+ : null,
+ 'schemaPath' => Config::module('setup')
+ ->get('schema', 'path', Icinga::app()->getBaseDir('schema'))
+ ))
+ );
+ }
+
+ $setup->addStep(
+ new GeneralConfigStep(array(
+ 'generalConfig' => $pageData['setup_general_config'],
+ 'resourceName' => isset($pageData['setup_auth_db_resource']['name'])
+ ? $pageData['setup_auth_db_resource']['name']
+ : (isset($pageData['setup_config_db_resource']['name'])
+ ? $pageData['setup_config_db_resource']['name']
+ : null
+ )
+ ))
+ );
+
+ $adminAccountType = $pageData['setup_admin_account']['user_type'];
+ if ($adminAccountType === 'user_group') {
+ $adminAccountData = array('groupname' => $pageData['setup_admin_account'][$adminAccountType]);
+ } else {
+ $adminAccountData = array('username' => $pageData['setup_admin_account'][$adminAccountType]);
+ if ($adminAccountType === 'new_user' && !$pageData['setup_auth_db_resource']['skip_validation']
+ && (! isset($pageData['setup_auth_db_creation'])
+ || !$pageData['setup_auth_db_creation']['skip_validation']
+ )
+ ) {
+ $adminAccountData['resourceConfig'] = $pageData['setup_auth_db_resource'];
+ $adminAccountData['password'] = $pageData['setup_admin_account']['new_user_password'];
+ }
+ }
+ $authType = $pageData['setup_authentication_type']['type'];
+ $setup->addStep(
+ new AuthenticationStep(array(
+ 'adminAccountData' => $adminAccountData,
+ 'backendConfig' => $pageData['setup_authentication_backend'],
+ 'resourceName' => $authType === 'db' ? $pageData['setup_auth_db_resource']['name'] : (
+ $authType === 'ldap' ? $pageData['setup_ldap_resource']['name'] : null
+ )
+ ))
+ );
+
+ if ($authType !== 'external') {
+ $setup->addStep(
+ new UserGroupStep(array(
+ 'backendConfig' => $pageData['setup_authentication_backend'],
+ 'groupConfig' => isset($pageData['setup_usergroup_backend'])
+ ? $pageData['setup_usergroup_backend']
+ : null,
+ 'resourceName' => $authType === 'db'
+ ? $pageData['setup_auth_db_resource']['name']
+ : $pageData['setup_ldap_resource']['name'],
+ 'resourceConfig' => $authType === 'db'
+ ? $pageData['setup_auth_db_resource']
+ : null,
+ 'username' => $authType === 'db'
+ ? $pageData['setup_admin_account'][$adminAccountType]
+ : null
+ ))
+ );
+ }
+
+ if (isset($pageData['setup_auth_db_resource'])
+ || isset($pageData['setup_config_db_resource'])
+ || isset($pageData['setup_ldap_resource'])
+ ) {
+ $setup->addStep(
+ new ResourceStep(array(
+ 'dbResourceConfig' => isset($pageData['setup_auth_db_resource'])
+ ? array_diff_key($pageData['setup_auth_db_resource'], array('skip_validation' => null))
+ : (isset($pageData['setup_config_db_resource'])
+ ? array_diff_key($pageData['setup_config_db_resource'], array('skip_validation' => null))
+ : null
+ ),
+ 'ldapResourceConfig' => isset($pageData['setup_ldap_resource'])
+ ? array_diff_key($pageData['setup_ldap_resource'], array('skip_validation' => null))
+ : null
+ ))
+ );
+ }
+
+ foreach ($this->getWizards() as $wizard) {
+ if ($wizard->isComplete()) {
+ $setup->addSteps($wizard->getSetup()->getSteps());
+ }
+ }
+
+ $setup->addStep(new EnableModuleStep(array_keys($this->getPage('setup_modules')->getCheckedModules())));
+
+ return $setup;
+ }
+
+ /**
+ * Return the requirements of this wizard
+ *
+ * @return RequirementSet
+ */
+ public function getRequirements($skipModules = false)
+ {
+ $set = new RequirementSet();
+
+ $set->add(new PhpVersionRequirement(array(
+ 'condition' => array('>=', '7.2'),
+ 'description' => sprintf(mt(
+ 'setup',
+ 'Running Icinga Web 2 requires PHP version %s.'
+ ), '7.2')
+ )));
+
+ $set->add(new OSRequirement(array(
+ 'optional' => true,
+ 'condition' => 'linux',
+ 'description' => mt(
+ 'setup',
+ 'Icinga Web 2 is developed for and tested on Linux. While we cannot'
+ . ' guarantee they will, other platforms may also perform as well.'
+ )
+ )));
+
+ $set->add(new WebLibraryRequirement(array(
+ 'condition' => ['icinga-php-library', '>=', '0.9.0'],
+ 'alias' => 'Icinga PHP library',
+ 'description' => mt(
+ 'setup',
+ 'The Icinga PHP library (IPL) is required for Icinga Web 2 and modules'
+ )
+ )));
+
+ $set->add(new WebLibraryRequirement(array(
+ 'condition' => ['icinga-php-thirdparty', '>=', '0.11.0'],
+ 'alias' => 'Icinga PHP Thirdparty',
+ 'description' => mt(
+ 'setup',
+ 'The Icinga PHP Thirdparty library is required for Icinga Web 2 and modules'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'OpenSSL',
+ 'description' => mt(
+ 'setup',
+ 'The PHP module for OpenSSL is required to generate cryptographically safe password salts.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'XML',
+ 'description' => mt(
+ 'setup',
+ 'The XML module for PHP is required for Markdown and custom HTML annotations.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'JSON',
+ 'description' => mt(
+ 'setup',
+ 'The JSON module for PHP is required for various export functionalities as well as APIs.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'gettext',
+ 'description' => mt(
+ 'setup',
+ 'For message localization, the gettext module for PHP is required.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'INTL',
+ 'description' => mt(
+ 'setup',
+ 'For language, timezone and date/time format negotiation, the INTL module for PHP is required.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'DOM',
+ 'description' => mt(
+ 'setup',
+ 'For charts and exports of views and reports to PDF, the DOM module for PHP is required.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'LDAP',
+ 'description' => mt(
+ 'setup',
+ 'If you\'d like to authenticate users using LDAP the corresponding PHP module is required.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'mbstring',
+ 'description' => mt(
+ 'setup',
+ 'In case you want views being exported to PDF, you\'ll need the mbstring extension for PHP.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'GD',
+ 'description' => mt(
+ 'setup',
+ 'In case you want views being exported to PDF, you\'ll need the GD extension for PHP.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'Imagick',
+ 'description' => mt(
+ 'setup',
+ 'In case you want graphs being exported to PDF as well, you\'ll need the ImageMagick extension for PHP.'
+ )
+ )));
+
+ $dbSet = new RequirementSet(false, RequirementSet::MODE_OR);
+ $dbSet->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'pdo_mysql',
+ 'alias' => 'PDO-MySQL',
+ 'description' => mt(
+ 'setup',
+ 'To store users or preferences in a MySQL database the PDO-MySQL module for PHP is required.'
+ )
+ )));
+ $dbSet->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'pdo_pgsql',
+ 'alias' => 'PDO-PostgreSQL',
+ 'description' => mt(
+ 'setup',
+ 'To store users or preferences in a PostgreSQL database the PDO-PostgreSQL module for PHP is required.'
+ )
+ )));
+ $set->merge($dbSet);
+
+ $dbRequire = (new SetRequirement(array(
+ 'optional' => false,
+ 'condition' => $dbSet,
+ 'title' =>'Database',
+ 'alias' => 'PDO-MySQL OR PDO-PostgreSQL',
+ 'description' => mt(
+ 'setup',
+ 'A database is mandatory, therefore at least one module '
+ . 'PDO-MySQL or PDO-PostgreSQL for PHP is required.'
+ )
+ )));
+
+ $set->add($dbRequire);
+
+ $set->add(new ConfigDirectoryRequirement(array(
+ 'condition' => Icinga::app()->getStorageDir(),
+ 'title' => mt('setup', 'Read- and writable storage directory'),
+ 'description' => mt(
+ 'setup',
+ 'The Icinga Web 2 storage directory defaults to "/var/lib/icingaweb2", if' .
+ ' not explicitly set in the environment variable "ICINGAWEB_STORAGEDIR".'
+ )
+ )));
+
+ $set->add(new ConfigDirectoryRequirement(array(
+ 'condition' => Icinga::app()->getConfigDir(),
+ 'description' => mt(
+ 'setup',
+ 'The Icinga Web 2 configuration directory defaults to "/etc/icingaweb2", if' .
+ ' not explicitly set in the environment variable "ICINGAWEB_CONFIGDIR".'
+ )
+ )));
+
+ if (! $skipModules) {
+ foreach ($this->getWizards() as $wizard) {
+ $set->merge($wizard->getRequirements());
+ }
+ }
+
+ return $set;
+ }
+}
diff --git a/modules/setup/library/Setup/Webserver.php b/modules/setup/library/Setup/Webserver.php
new file mode 100644
index 0000000..2251ba3
--- /dev/null
+++ b/modules/setup/library/Setup/Webserver.php
@@ -0,0 +1,233 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Base class for generating webserver configuration
+ */
+abstract class Webserver
+{
+ /**
+ * Document root
+ *
+ * @var string
+ */
+ protected $documentRoot;
+
+ /**
+ * URL path of Icinga Web 2
+ *
+ * @var string
+ */
+ protected $urlPath = '/icingaweb2';
+
+ /**
+ * Path to Icinga Web 2's configuration files
+ *
+ * @var string
+ */
+ protected $configDir;
+
+ /**
+ * Address or path where to pass requests to FPM
+ *
+ * @var string
+ */
+ protected $fpmUri;
+
+ /**
+ * Enable to pass requests to FPM
+ *
+ * @var bool
+ */
+ protected $enableFpm = false;
+
+ /**
+ * Create instance by type name
+ *
+ * @param string $type
+ *
+ * @return WebServer
+ *
+ * @throws ProgrammingError
+ */
+ public static function createInstance($type)
+ {
+ $class = __NAMESPACE__ . '\\Webserver\\' . ucfirst($type);
+ if (class_exists($class)) {
+ return new $class();
+ }
+ throw new ProgrammingError('Class "%s" does not exist', $class);
+ }
+
+ /**
+ * Generate configuration
+ *
+ * @return string
+ */
+ public function generate()
+ {
+ $template = $this->getTemplate();
+
+ $searchTokens = array(
+ '{urlPath}',
+ '{documentRoot}',
+ '{aliasDocumentRoot}',
+ '{configDir}',
+ '{fpmUri}'
+ );
+ $replaceTokens = array(
+ $this->getUrlPath(),
+ $this->getDocumentRoot(),
+ preg_match('~/$~', $this->getUrlPath()) ? $this->getDocumentRoot() . '/' : $this->getDocumentRoot(),
+ $this->getConfigDir(),
+ $this->getFpmUri()
+ );
+ $template = str_replace($searchTokens, $replaceTokens, $template);
+ return $template;
+ }
+
+ /**
+ * Specific template
+ *
+ * @return string
+ */
+ abstract protected function getTemplate();
+
+ /**
+ * Set the URL path of Icinga Web 2
+ *
+ * @param string $urlPath
+ *
+ * @return $this
+ */
+ public function setUrlPath($urlPath)
+ {
+ $this->urlPath = '/' . ltrim(trim((string) $urlPath), '/');
+ return $this;
+ }
+
+ /**
+ * Get the URL path of Icinga Web 2
+ *
+ * @return string
+ */
+ public function getUrlPath()
+ {
+ return $this->urlPath;
+ }
+
+ /**
+ * Set the document root
+ *
+ * @param string $documentRoot
+ *
+ * @return $this
+ */
+ public function setDocumentRoot($documentRoot)
+ {
+ $this->documentRoot = trim((string) $documentRoot);
+ return $this;
+ }
+
+ /**
+ * Detect the document root
+ *
+ * @return string
+ */
+ public function detectDocumentRoot()
+ {
+ return Icinga::app()->getBaseDir('public');
+ }
+
+ /**
+ * Get the document root
+ *
+ * @return string
+ */
+ public function getDocumentRoot()
+ {
+ if ($this->documentRoot === null) {
+ $this->documentRoot = $this->detectDocumentRoot();
+ }
+ return $this->documentRoot;
+ }
+
+ /**
+ * Set the configuration directory
+ *
+ * @param string $configDir
+ *
+ * @return $this
+ */
+ public function setConfigDir($configDir)
+ {
+ $this->configDir = (string) $configDir;
+ return $this;
+ }
+
+ /**
+ * Get the configuration directory
+ *
+ * @return string
+ */
+ public function getConfigDir()
+ {
+ if ($this->configDir === null) {
+ return Icinga::app()->getConfigDir();
+ }
+ return $this->configDir;
+ }
+
+ /**
+ * Get whether FPM is enabled
+ *
+ * @return bool
+ */
+ public function getEnableFpm()
+ {
+ return $this->enableFpm;
+ }
+
+ /**
+ * Set FPM enabled
+ *
+ * @param bool $flag
+ *
+ * @return $this
+ */
+ public function setEnableFpm($flag)
+ {
+ $this->enableFpm = (bool) $flag;
+
+ return $this;
+ }
+
+ /**
+ * Get the address or path where to pass requests to FPM
+ *
+ * @return string
+ */
+ public function getFpmUri()
+ {
+ return $this->fpmUri;
+ }
+
+ /**
+ * Set the address or path where to pass requests to FPM
+ *
+ * @param string $uri
+ *
+ * @return $this
+ */
+ public function setFpmUri($uri)
+ {
+ $this->fpmUri = (string) $uri;
+
+ return $this;
+ }
+}
diff --git a/modules/setup/library/Setup/Webserver/Apache.php b/modules/setup/library/Setup/Webserver/Apache.php
new file mode 100644
index 0000000..fdb367f
--- /dev/null
+++ b/modules/setup/library/Setup/Webserver/Apache.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Webserver;
+
+use Icinga\Module\Setup\Webserver;
+
+/**
+ * Generate Apache 2.x configuration
+ */
+class Apache extends Webserver
+{
+ protected $fpmUri = '127.0.0.1:9000';
+
+ protected function getTemplate()
+ {
+ if (! $this->enableFpm) {
+ return <<<'EOD'
+Alias {urlPath} "{aliasDocumentRoot}"
+
+# Remove comments if you want to use PHP FPM and your Apache version is older than 2.4
+#<IfVersion < 2.4>
+# # Forward PHP requests to FPM
+# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+# <LocationMatch "^{urlPath}/(.*\.php)$">
+# ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
+# </LocationMatch>
+#</IfVersion>
+
+<Directory "{documentRoot}">
+ Options SymLinksIfOwnerMatch
+ AllowOverride None
+
+ DirectoryIndex index.php
+
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ <RequireAll>
+ Require all granted
+ </RequireAll>
+ </IfModule>
+
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order allow,deny
+ Allow from all
+ </IfModule>
+
+ SetEnv ICINGAWEB_CONFIGDIR "{configDir}"
+
+ EnableSendfile Off
+
+ <IfModule mod_rewrite.c>
+ RewriteEngine on
+ RewriteBase {urlPath}/
+ RewriteCond %{REQUEST_FILENAME} -s [OR]
+ RewriteCond %{REQUEST_FILENAME} -l [OR]
+ RewriteCond %{REQUEST_FILENAME} -d
+ RewriteRule ^.*$ - [NC,L]
+ RewriteRule ^.*$ index.php [NC,L]
+ </IfModule>
+
+ <IfModule !mod_rewrite.c>
+ DirectoryIndex error_norewrite.html
+ ErrorDocument 404 {urlPath}/error_norewrite.html
+ </IfModule>
+
+# Remove comments if you want to use PHP FPM and your Apache version
+# is greater than or equal to 2.4
+# <IfVersion >= 2.4>
+# # Forward PHP requests to FPM
+# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+# <FilesMatch "\.php$">
+# SetHandler "proxy:fcgi://{fpmUri}"
+# ErrorDocument 503 {urlPath}/error_unavailable.html
+# </FilesMatch>
+# </IfVersion>
+</Directory>
+EOD;
+ } else {
+ return <<<'EOD'
+Alias {urlPath} "{aliasDocumentRoot}"
+
+<IfVersion < 2.4>
+ # Forward PHP requests to FPM
+ SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+ <LocationMatch "^{urlPath}/(.*\.php)$">
+ ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
+ </LocationMatch>
+</IfVersion>
+
+<Directory "{documentRoot}">
+ Options SymLinksIfOwnerMatch
+ AllowOverride None
+
+ DirectoryIndex index.php
+
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ <RequireAll>
+ Require all granted
+ </RequireAll>
+ </IfModule>
+
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order allow,deny
+ Allow from all
+ </IfModule>
+
+ SetEnv ICINGAWEB_CONFIGDIR "{configDir}"
+
+ EnableSendfile Off
+
+ <IfModule mod_rewrite.c>
+ RewriteEngine on
+ RewriteBase {urlPath}/
+ RewriteCond %{REQUEST_FILENAME} -s [OR]
+ RewriteCond %{REQUEST_FILENAME} -l [OR]
+ RewriteCond %{REQUEST_FILENAME} -d
+ RewriteRule ^.*$ - [NC,L]
+ RewriteRule ^.*$ index.php [NC,L]
+ </IfModule>
+
+ <IfModule !mod_rewrite.c>
+ DirectoryIndex error_norewrite.html
+ ErrorDocument 404 {urlPath}/error_norewrite.html
+ </IfModule>
+
+ <IfVersion >= 2.4>
+ # Forward PHP requests to FPM
+ SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+ <FilesMatch "\.php$">
+ SetHandler "proxy:fcgi://{fpmUri}"
+ ErrorDocument 503 {urlPath}/error_unavailable.html
+ </FilesMatch>
+ </IfVersion>
+</Directory>
+EOD;
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Webserver/Nginx.php b/modules/setup/library/Setup/Webserver/Nginx.php
new file mode 100644
index 0000000..c7ae716
--- /dev/null
+++ b/modules/setup/library/Setup/Webserver/Nginx.php
@@ -0,0 +1,36 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Webserver;
+
+use Icinga\Module\Setup\Webserver;
+
+/**
+ * Generate nginx configuration
+ */
+class Nginx extends Webserver
+{
+ protected $fpmUri = '127.0.0.1:9000';
+
+ protected $enableFpm = true;
+
+ protected function getTemplate()
+ {
+ return <<<'EOD'
+location ~ ^{urlPath}/index\.php(.*)$ {
+ fastcgi_pass {fpmUri};
+ fastcgi_index index.php;
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME {documentRoot}/index.php;
+ fastcgi_param ICINGAWEB_CONFIGDIR {configDir};
+ fastcgi_param REMOTE_USER $remote_user;
+}
+
+location ~ ^{urlPath}(.+)? {
+ alias {documentRoot};
+ index index.php;
+ try_files $1 $uri $uri/ {urlPath}/index.php$is_args$args;
+}
+EOD;
+ }
+}
diff --git a/modules/setup/module.info b/modules/setup/module.info
new file mode 100644
index 0000000..e3570bd
--- /dev/null
+++ b/modules/setup/module.info
@@ -0,0 +1,6 @@
+Module: setup
+Version: 2.11.4
+Description: Setup module
+ Web based wizard for setting up Icinga Web 2 and its modules.
+ This includes the data backends (e.g. relational database, LDAP),
+ the authentication method, where to store the user preferences and much more.