summaryrefslogtreecommitdiffstats
path: root/application/forms
diff options
context:
space:
mode:
Diffstat (limited to 'application/forms')
-rw-r--r--application/forms/Account/ChangePasswordForm.php123
-rw-r--r--application/forms/AcknowledgeApplicationStateMessageForm.php75
-rw-r--r--application/forms/ActionForm.php78
-rw-r--r--application/forms/Announcement/AcknowledgeAnnouncementForm.php92
-rw-r--r--application/forms/Announcement/AnnouncementForm.php135
-rw-r--r--application/forms/Authentication/LoginForm.php214
-rw-r--r--application/forms/AutoRefreshForm.php83
-rw-r--r--application/forms/Config/General/ApplicationConfigForm.php105
-rw-r--r--application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php46
-rw-r--r--application/forms/Config/General/LoggingConfigForm.php142
-rw-r--r--application/forms/Config/General/ThemingConfigForm.php78
-rw-r--r--application/forms/Config/GeneralConfigForm.php40
-rw-r--r--application/forms/Config/Resource/DbResourceForm.php239
-rw-r--r--application/forms/Config/Resource/FileResourceForm.php67
-rw-r--r--application/forms/Config/Resource/LdapResourceForm.php129
-rw-r--r--application/forms/Config/Resource/SshResourceForm.php148
-rw-r--r--application/forms/Config/ResourceConfigForm.php442
-rw-r--r--application/forms/Config/User/CreateMembershipForm.php192
-rw-r--r--application/forms/Config/User/UserForm.php210
-rw-r--r--application/forms/Config/UserBackend/DbBackendForm.php82
-rw-r--r--application/forms/Config/UserBackend/ExternalBackendForm.php83
-rw-r--r--application/forms/Config/UserBackend/LdapBackendForm.php414
-rw-r--r--application/forms/Config/UserBackendConfigForm.php482
-rw-r--r--application/forms/Config/UserBackendReorderForm.php86
-rw-r--r--application/forms/Config/UserGroup/AddMemberForm.php183
-rw-r--r--application/forms/Config/UserGroup/DbUserGroupBackendForm.php79
-rw-r--r--application/forms/Config/UserGroup/LdapUserGroupBackendForm.php370
-rw-r--r--application/forms/Config/UserGroup/UserGroupBackendForm.php314
-rw-r--r--application/forms/Config/UserGroup/UserGroupForm.php158
-rw-r--r--application/forms/ConfigForm.php192
-rw-r--r--application/forms/ConfirmRemovalForm.php38
-rw-r--r--application/forms/Control/LimiterControlForm.php134
-rw-r--r--application/forms/Dashboard/DashletForm.php171
-rw-r--r--application/forms/LdapDiscoveryForm.php34
-rw-r--r--application/forms/MigrationForm.php143
-rw-r--r--application/forms/Navigation/DashletForm.php35
-rw-r--r--application/forms/Navigation/MenuItemForm.php31
-rw-r--r--application/forms/Navigation/NavigationConfigForm.php853
-rw-r--r--application/forms/Navigation/NavigationItemForm.php114
-rw-r--r--application/forms/PreferenceForm.php485
-rw-r--r--application/forms/RepositoryForm.php453
-rw-r--r--application/forms/Security/RoleForm.php632
42 files changed, 8204 insertions, 0 deletions
diff --git a/application/forms/Account/ChangePasswordForm.php b/application/forms/Account/ChangePasswordForm.php
new file mode 100644
index 0000000..5bca11c
--- /dev/null
+++ b/application/forms/Account/ChangePasswordForm.php
@@ -0,0 +1,123 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Account;
+
+use Icinga\Authentication\User\DbUserBackend;
+use Icinga\Data\Filter\Filter;
+use Icinga\User;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for changing user passwords
+ */
+class ChangePasswordForm extends Form
+{
+ /**
+ * The user backend
+ *
+ * @var DbUserBackend
+ */
+ protected $backend;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setSubmitLabel($this->translate('Update Account'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'password',
+ 'old_password',
+ array(
+ 'label' => $this->translate('Old Password'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_password',
+ array(
+ 'label' => $this->translate('New Password'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_password_confirmation',
+ array(
+ 'label' => $this->translate('Confirm New Password'),
+ 'required' => true,
+ 'validators' => array(
+ array('identical', false, array('new_password'))
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $backend = $this->getBackend();
+ $backend->update(
+ $backend->getBaseTable(),
+ array('password' => $this->getElement('new_password')->getValue()),
+ Filter::where('user_name', $this->Auth()->getUser()->getUsername())
+ );
+ Notification::success($this->translate('Account updated'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+ if (! $valid) {
+ return false;
+ }
+
+ $oldPasswordEl = $this->getElement('old_password');
+
+ if (! $this->backend->authenticate($this->Auth()->getUser(), $oldPasswordEl->getValue())) {
+ $oldPasswordEl->addError($this->translate('Old password is invalid'));
+ $this->markAsError();
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the user backend
+ *
+ * @return DbUserBackend
+ */
+ public function getBackend()
+ {
+ return $this->backend;
+ }
+
+ /**
+ * Set the user backend
+ *
+ * @param DbUserBackend $backend
+ *
+ * @return $this
+ */
+ public function setBackend(DbUserBackend $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+}
diff --git a/application/forms/AcknowledgeApplicationStateMessageForm.php b/application/forms/AcknowledgeApplicationStateMessageForm.php
new file mode 100644
index 0000000..61f5824
--- /dev/null
+++ b/application/forms/AcknowledgeApplicationStateMessageForm.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\Hook\ApplicationStateHook;
+use Icinga\Web\ApplicationStateCookie;
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class AcknowledgeApplicationStateMessageForm extends Form
+{
+ public function init()
+ {
+ $this->setAction(Url::fromPath('application-state/acknowledge-message'));
+ $this->setAttrib('class', 'application-state-acknowledge-message-control');
+ $this->setRedirectUrl('application-state/summary');
+ }
+
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ [
+ 'class' => 'link-button spinner',
+ 'decorators' => [
+ 'ViewHelper',
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']]
+ ],
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Acknowledge message'),
+ 'type' => 'submit'
+ ]
+ );
+ return $this;
+ }
+
+ public function createElements(array $formData = [])
+ {
+ $this->addElements(
+ [
+ [
+ 'hidden',
+ 'id',
+ [
+ 'required' => true,
+ 'validators' => ['NotEmpty'],
+ 'decorators' => ['ViewHelper']
+ ]
+ ]
+ ]
+ );
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $cookie = new ApplicationStateCookie();
+
+ $ack = $cookie->getAcknowledgedMessages();
+ $ack[] = $this->getValue('id');
+
+ $active = ApplicationStateHook::getAllMessages();
+
+ $cookie->setAcknowledgedMessages(array_keys(array_intersect_key($active, array_flip($ack))));
+
+ $this->getResponse()->setCookie($cookie);
+
+ return true;
+ }
+}
diff --git a/application/forms/ActionForm.php b/application/forms/ActionForm.php
new file mode 100644
index 0000000..5b5b6ed
--- /dev/null
+++ b/application/forms/ActionForm.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Web\Form;
+
+class ActionForm extends Form
+{
+ /**
+ * The icon shown on the button
+ *
+ * @var string
+ */
+ protected $icon = 'arrows-cw';
+
+ /**
+ * Set the icon to show on the button
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setIcon($name)
+ {
+ $this->icon = (string) $name;
+ return $this;
+ }
+
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ $this->setUidDisabled(true);
+ $this->setDecorators(['FormElements', 'Form']);
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'hidden',
+ 'identifier',
+ [
+ 'required' => true,
+ 'decorators' => ['ViewHelper']
+ ]
+ );
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ [
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => ['ViewHelper'],
+ 'label' => $this->getView()->icon($this->icon),
+ 'title' => $this->getDescription()
+ ]
+ );
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid) {
+ $valid = ConfigFormEventsHook::runIsValid($this);
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ ConfigFormEventsHook::runOnSuccess($this);
+ }
+}
diff --git a/application/forms/Announcement/AcknowledgeAnnouncementForm.php b/application/forms/Announcement/AcknowledgeAnnouncementForm.php
new file mode 100644
index 0000000..85fecdc
--- /dev/null
+++ b/application/forms/Announcement/AcknowledgeAnnouncementForm.php
@@ -0,0 +1,92 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Announcement;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Web\Announcement\AnnouncementCookie;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class AcknowledgeAnnouncementForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAction(Url::fromPath('announcements/acknowledge'));
+ $this->setAttrib('class', 'acknowledge-announcement-control');
+ $this->setRedirectUrl('layout/announcements');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'class' => 'link-button spinner',
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Acknowledge this announcement'),
+ 'type' => 'submit'
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElements(
+ array(
+ array(
+ 'hidden',
+ 'hash',
+ array(
+ 'required' => true,
+ 'validators' => array('NotEmpty'),
+ 'decorators' => array('ViewHelper')
+ )
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $cookie = new AnnouncementCookie();
+ $repo = new AnnouncementIniRepository();
+ $query = $repo->findActive();
+ $filter = array();
+ foreach ($cookie->getAcknowledged() as $hash) {
+ $filter[] = Filter::expression('hash', '=', $hash);
+ }
+ $query->addFilter(Filter::matchAny($filter));
+ $acknowledged = array();
+ foreach ($query as $row) {
+ $acknowledged[] = $row->hash;
+ }
+ $acknowledged[] = $this->getElement('hash')->getValue();
+ $cookie->setAcknowledged($acknowledged);
+ $this->getResponse()->setCookie($cookie);
+ return true;
+ }
+}
diff --git a/application/forms/Announcement/AnnouncementForm.php b/application/forms/Announcement/AnnouncementForm.php
new file mode 100644
index 0000000..4da47e2
--- /dev/null
+++ b/application/forms/Announcement/AnnouncementForm.php
@@ -0,0 +1,135 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Announcement;
+
+use DateTime;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+
+/**
+ * Create, update and delete announcements
+ */
+class AnnouncementForm extends RepositoryForm
+{
+ protected function fetchEntry()
+ {
+ $entry = parent::fetchEntry();
+ if ($entry !== false) {
+ if ($entry->start !== null) {
+ $entry->start = (new DateTime())->setTimestamp($entry->start);
+ }
+ if ($entry->end !== null) {
+ $entry->end = (new DateTime())->setTimestamp($entry->end);
+ }
+ }
+
+ return $entry;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'author',
+ array(
+ 'disabled' => ! $this->getRequest()->isApiRequest(),
+ 'required' => true,
+ 'value' => Auth::getInstance()->getUser()->getUsername()
+ )
+ );
+ $this->addElement(
+ 'textarea',
+ 'message',
+ array(
+ 'description' => $this->translate('The message to display to users'),
+ 'label' => $this->translate('Message'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'dateTimePicker',
+ 'start',
+ array(
+ 'description' => $this->translate('The time to display the announcement from'),
+ 'label' => $this->translate('Start'),
+ 'placeholder' => new DateTime('tomorrow'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'dateTimePicker',
+ 'end',
+ array(
+ 'description' => $this->translate('The time to display the announcement until'),
+ 'label' => $this->translate('End'),
+ 'placeholder' => new DateTime('tomorrow +1day'),
+ 'required' => true
+ )
+ );
+
+ $this->setTitle($this->translate('Create a new announcement'));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+ /**
+ * {@inheritDoc}
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+ $this->setTitle(sprintf($this->translate('Edit announcement %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove announcement %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-controls');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createFilter()
+ {
+ return Filter::where('id', $this->getIdentifier());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getInsertMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement created')
+ : $this->translate('Failed to create announcement');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getUpdateMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement updated')
+ : $this->translate('Failed to update announcement');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDeleteMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement removed')
+ : $this->translate('Failed to remove announcement');
+ }
+}
diff --git a/application/forms/Authentication/LoginForm.php b/application/forms/Authentication/LoginForm.php
new file mode 100644
index 0000000..87b32ab
--- /dev/null
+++ b/application/forms/Authentication/LoginForm.php
@@ -0,0 +1,214 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Authentication;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Hook\AuthenticationHook;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Authentication\User\ExternalBackend;
+use Icinga\Common\Database;
+use Icinga\Exception\Http\HttpBadRequestException;
+use Icinga\User;
+use Icinga\Web\Form;
+use Icinga\Web\RememberMe;
+use Icinga\Web\Url;
+
+/**
+ * Form for user authentication
+ */
+class LoginForm extends Form
+{
+ use Database;
+
+ const DEFAULT_CLASSES = 'icinga-controls';
+
+ /**
+ * Redirect URL
+ */
+ const REDIRECT_URL = 'dashboard';
+
+ public static $defaultElementDecorators = [
+ ['ViewHelper', ['separator' => '']],
+ ['Help', []],
+ ['Errors', ['separator' => '']],
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group']]
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setRequiredCue(null);
+ $this->setName('form_login');
+ $this->setSubmitLabel($this->translate('Login'));
+ $this->setProgressLabel($this->translate('Logging in'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'username',
+ array(
+ 'autocapitalize' => 'off',
+ 'autocomplete' => 'username',
+ 'class' => false === isset($formData['username']) ? 'autofocus' : '',
+ 'placeholder' => $this->translate('Username'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'autocomplete' => 'current-password',
+ 'placeholder' => $this->translate('Password'),
+ 'class' => isset($formData['username']) ? 'autofocus' : ''
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'rememberme',
+ [
+ 'label' => $this->translate('Stay logged in'),
+ 'decorators' => [
+ ['ViewHelper', ['separator' => '']],
+ ['Label', [
+ 'tag' => 'span',
+ 'separator' => '',
+ 'class' => 'control-label',
+ 'placement' => 'APPEND'
+ ]],
+ ['Help', []],
+ ['Errors', ['separator' => '']],
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group remember-me-box']]
+ ]
+ ]
+ );
+ if (! RememberMe::isSupported()) {
+ $this->getElement('rememberme')
+ ->setAttrib('disabled', true)
+ ->setDescription($this->translate(
+ 'Staying logged in requires a database configuration backend'
+ . ' and an appropriate OpenSSL encryption method'
+ ));
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'redirect',
+ array(
+ 'value' => Url::fromRequest()->getParam('redirect')
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrl()
+ {
+ $redirect = null;
+ if ($this->created) {
+ $redirect = $this->getElement('redirect')->getValue();
+ }
+
+ if (empty($redirect) || strpos($redirect, 'authentication/logout') !== false) {
+ $redirect = static::REDIRECT_URL;
+ }
+
+ $redirectUrl = Url::fromPath($redirect);
+ if ($redirectUrl->isExternal()) {
+ throw new HttpBadRequestException('nope');
+ }
+
+ return $redirectUrl;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $auth = Auth::getInstance();
+ $authChain = $auth->getAuthChain();
+ $authChain->setSkipExternalBackends(true);
+ $user = new User($this->getElement('username')->getValue());
+ if (! $user->hasDomain()) {
+ $user->setDomain(Config::app()->get('authentication', 'default_domain'));
+ }
+ $password = $this->getElement('password')->getValue();
+ $authenticated = $authChain->authenticate($user, $password);
+ if ($authenticated) {
+ $auth->setAuthenticated($user);
+ if ($this->getElement('rememberme')->isChecked()) {
+ try {
+ $rememberMe = RememberMe::fromCredentials($user->getUsername(), $password);
+ $this->getResponse()->setCookie($rememberMe->getCookie());
+ $rememberMe->persist();
+ } catch (Exception $e) {
+ Logger::error('Failed to let user "%s" stay logged in: %s', $user->getUsername(), $e);
+ }
+ }
+
+ // Call provided AuthenticationHook(s) after successful login
+ AuthenticationHook::triggerLogin($user);
+ $this->getResponse()->setRerenderLayout(true);
+ return true;
+ }
+ switch ($authChain->getError()) {
+ case $authChain::EEMPTY:
+ $this->addError($this->translate(
+ 'No authentication methods available.'
+ . ' Did you create authentication.ini when setting up Icinga Web 2?'
+ ));
+ break;
+ case $authChain::EFAIL:
+ $this->addError($this->translate(
+ 'All configured authentication methods failed.'
+ . ' Please check the system log or Icinga Web 2 log for more information.'
+ ));
+ break;
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case $authChain::ENOTALL:
+ $this->addError($this->translate(
+ 'Please note that not all authentication methods were available.'
+ . ' Check the system log or Icinga Web 2 log for more information.'
+ ));
+ // Move to default
+ default:
+ $this->getElement('password')->addError($this->translate('Incorrect username or password'));
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onRequest()
+ {
+ $auth = Auth::getInstance();
+ $onlyExternal = true;
+ // TODO(el): This may be set on the auth chain once iterated. See Auth::authExternal().
+ foreach ($auth->getAuthChain() as $backend) {
+ if (! $backend instanceof ExternalBackend) {
+ $onlyExternal = false;
+ }
+ }
+ if ($onlyExternal) {
+ $this->addError($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 login.'
+ ));
+ }
+ }
+}
diff --git a/application/forms/AutoRefreshForm.php b/application/forms/AutoRefreshForm.php
new file mode 100644
index 0000000..122f635
--- /dev/null
+++ b/application/forms/AutoRefreshForm.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\Logger;
+use Icinga\User\Preferences;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Session;
+use Icinga\Web\Url;
+
+/**
+ * Form class to adjust user auto refresh preferences
+ */
+class AutoRefreshForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_auto_refresh');
+ // Post against the current location
+ $this->setAction('');
+ }
+
+ /**
+ * Adjust preferences and persist them
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ /** @var Preferences $preferences */
+ $preferences = $this->getRequest()->getUser()->getPreferences();
+ $icingaweb = $preferences->get('icingaweb');
+
+ if ((bool) $preferences->getValue('icingaweb', 'auto_refresh', true) === false) {
+ $icingaweb['auto_refresh'] = '1';
+ $notification = $this->translate('Auto refresh successfully enabled');
+ } else {
+ $icingaweb['auto_refresh'] = '0';
+ $notification = $this->translate('Auto refresh successfully disabled');
+ }
+ $preferences->icingaweb = $icingaweb;
+
+ Session::getSession()->user->setPreferences($preferences);
+ Notification::success($notification);
+
+ $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes');
+ $this->setRedirectUrl(Url::fromRequest()->without('renderLayout'));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $preferences = $this->getRequest()->getUser()->getPreferences();
+
+ if ((bool) $preferences->getValue('icingaweb', 'auto_refresh', true) === false) {
+ $value = $this->translate('Enable auto refresh');
+ } else {
+ $value = $this->translate('Disable auto refresh');
+ }
+
+ $this->addElements(array(
+ array(
+ 'button',
+ 'btn_submit',
+ array(
+ 'ignore' => true,
+ 'type' => 'submit',
+ 'value' => $value,
+ 'decorators' => array('ViewHelper'),
+ 'escape' => false,
+ 'class' => 'link-like'
+ )
+ )
+ ));
+ }
+}
diff --git a/application/forms/Config/General/ApplicationConfigForm.php b/application/forms/Config/General/ApplicationConfigForm.php
new file mode 100644
index 0000000..21f76a1
--- /dev/null
+++ b/application/forms/Config/General/ApplicationConfigForm.php
@@ -0,0 +1,105 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\ResourceFactory;
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for general application options
+ *
+ * This form is not used directly but as subform to the {@link GeneralConfigForm}.
+ */
+class ApplicationConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_application');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'global_show_stacktraces',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Stacktraces'),
+ 'description' => $this->translate(
+ 'Set whether to show an exception\'s stacktrace by default. This can also'
+ . ' be set in a user\'s preferences with the appropriate permission.'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'global_show_application_state_messages',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Application State Messages'),
+ 'description' => $this->translate(
+ "Set whether to show application state messages."
+ . " This can also be set in a user's preferences."
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'security_use_strict_csp',
+ [
+ 'label' => $this->translate('Enable strict content security policy'),
+ 'description' => $this->translate(
+ 'Set whether to to use strict content security policy (CSP).'
+ . ' This setting helps to protect from cross-site scripting (XSS).'
+ )
+ ]
+ );
+
+ $this->addElement(
+ 'text',
+ 'global_module_path',
+ array(
+ 'label' => $this->translate('Module Path'),
+ 'required' => true,
+ 'value' => implode(':', Icinga::app()->getModuleManager()->getModuleDirs()),
+ 'description' => $this->translate(
+ 'Contains the directories that will be searched for available modules, separated by '
+ . 'colons. Modules that don\'t exist in these directories can still be symlinked in '
+ . 'the module folder, but won\'t show up in the list of disabled modules.'
+ )
+ )
+ );
+
+ $backends = array_keys(ResourceFactory::getResourceConfigs()->toArray());
+ $backends = array_combine($backends, $backends);
+
+ $this->addElement(
+ 'select',
+ 'global_config_resource',
+ array(
+ 'required' => true,
+ 'multiOptions' => array_merge(
+ ['' => sprintf(' - %s - ', $this->translate('Please choose'))],
+ $backends
+ ),
+ 'disable' => [''],
+ 'value' => '',
+ 'label' => $this->translate('Configuration Database')
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
new file mode 100644
index 0000000..0ff6c32
--- /dev/null
+++ b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
@@ -0,0 +1,46 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for the default domain for authentication
+ *
+ * This form is not used directly but as subform to the {@link GeneralConfigForm}.
+ */
+class DefaultAuthenticationDomainConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_authentication');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'authentication_default_domain',
+ array(
+ 'label' => $this->translate('Default Login Domain'),
+ 'description' => $this->translate(
+ 'If a user logs in without specifying any domain (e.g. "jdoe" instead of "jdoe@example.com"),'
+ . ' this default domain will be assumed for the user. Note that if none your LDAP authentication'
+ . ' backends are configured to be responsible for this domain or if none of your authentication'
+ . ' backends holds usernames with the domain part, users will not be able to login.'
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/LoggingConfigForm.php b/application/forms/Config/General/LoggingConfigForm.php
new file mode 100644
index 0000000..bbc7723
--- /dev/null
+++ b/application/forms/Config/General/LoggingConfigForm.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Logger;
+use Icinga\Application\Logger\Writer\SyslogWriter;
+use Icinga\Application\Platform;
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for logging options
+ *
+ * This form is not used directly but as subform for the {@link GeneralConfigForm}.
+ */
+class LoggingConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_logging');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $defaultType = getenv('ICINGAWEB_OFFICIAL_DOCKER_IMAGE') ? 'php' : 'syslog';
+
+ $this->addElement(
+ 'select',
+ 'logging_log',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Logging Type'),
+ 'description' => $this->translate('The type of logging to utilize.'),
+ 'value' => $defaultType,
+ 'multiOptions' => array(
+ 'syslog' => 'Syslog',
+ 'php' => $this->translate('Webserver Log', 'app.config.logging.type'),
+ 'file' => $this->translate('File', 'app.config.logging.type'),
+ 'none' => $this->translate('None', 'app.config.logging.type')
+ )
+ )
+ );
+
+ if (! isset($formData['logging_log']) || $formData['logging_log'] !== 'none') {
+ $this->addElement(
+ 'select',
+ 'logging_level',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Logging Level'),
+ 'description' => $this->translate('The maximum logging level to emit.'),
+ 'multiOptions' => array(
+ Logger::$levels[Logger::ERROR] => $this->translate('Error', 'app.config.logging.level'),
+ Logger::$levels[Logger::WARNING] => $this->translate('Warning', 'app.config.logging.level'),
+ Logger::$levels[Logger::INFO] => $this->translate('Information', 'app.config.logging.level'),
+ Logger::$levels[Logger::DEBUG] => $this->translate('Debug', 'app.config.logging.level')
+ )
+ )
+ );
+ }
+
+ if (! isset($formData['logging_log']) || in_array($formData['logging_log'], array('syslog', 'php'))) {
+ $this->addElement(
+ 'text',
+ 'logging_application',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Application Prefix'),
+ 'description' => $this->translate(
+ 'The name of the application by which to prefix log messages.'
+ ),
+ 'requirement' => $this->translate('The application prefix must not contain whitespace.'),
+ 'value' => 'icingaweb2',
+ 'validators' => array(
+ array(
+ 'Regex',
+ false,
+ array(
+ 'pattern' => '/^\S+$/',
+ 'messages' => array(
+ 'regexNotMatch' => $this->translate(
+ 'The application prefix must not contain whitespace.'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ if ((isset($formData['logging_log']) ? $formData['logging_log'] : $defaultType) === 'syslog') {
+ if (Platform::isWindows()) {
+ /* @see https://secure.php.net/manual/en/function.openlog.php */
+ $this->addElement(
+ 'hidden',
+ 'logging_facility',
+ array(
+ 'value' => 'user',
+ 'disabled' => true
+ )
+ );
+ } else {
+ $facilities = array_keys(SyslogWriter::$facilities);
+ $this->addElement(
+ 'select',
+ 'logging_facility',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Facility'),
+ 'description' => $this->translate('The syslog facility to utilize.'),
+ 'value' => 'user',
+ 'multiOptions' => array_combine($facilities, $facilities)
+ )
+ );
+ }
+ }
+ } elseif (isset($formData['logging_log']) && $formData['logging_log'] === 'file') {
+ $this->addElement(
+ 'text',
+ 'logging_file',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('File path'),
+ 'description' => $this->translate('The full path to the log file to write messages to.'),
+ 'value' => '/var/log/icingaweb2/icingaweb2.log',
+ 'validators' => array('WritablePathValidator')
+ )
+ );
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/ThemingConfigForm.php b/application/forms/Config/General/ThemingConfigForm.php
new file mode 100644
index 0000000..54ef2b1
--- /dev/null
+++ b/application/forms/Config/General/ThemingConfigForm.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Web\Form;
+use Icinga\Web\StyleSheet;
+
+/**
+ * Configuration form for theming options
+ *
+ * This form is not used directly but as subform for the {@link GeneralConfigForm}.
+ */
+class ThemingConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_theming');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $themes = Icinga::app()->getThemes();
+ $themes[StyleSheet::DEFAULT_THEME] .= ' (' . $this->translate('default') . ')';
+
+ $this->addElement(
+ 'select',
+ 'themes_default',
+ array(
+ 'description' => $this->translate('The default theme', 'Form element description'),
+ 'disabled' => count($themes) < 2 ? 'disabled' : null,
+ 'label' => $this->translate('Default Theme', 'Form element label'),
+ 'multiOptions' => $themes,
+ 'value' => StyleSheet::DEFAULT_THEME
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'themes_disabled',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for disallowing users to change the theme. If a default theme is set, it will be'
+ . ' used nonetheless',
+ 'Form element description'
+ ),
+ 'label' => $this->translate('Users Can\'t Change Theme', 'Form element label')
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ if ($values['themes_default'] === '' || $values['themes_default'] === StyleSheet::DEFAULT_THEME) {
+ $values['themes_default'] = null;
+ }
+ if (! $values['themes_disabled']) {
+ $values['themes_disabled'] = null;
+ }
+ return $values;
+ }
+}
diff --git a/application/forms/Config/GeneralConfigForm.php b/application/forms/Config/GeneralConfigForm.php
new file mode 100644
index 0000000..5f15512
--- /dev/null
+++ b/application/forms/Config/GeneralConfigForm.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Forms\Config\General\ApplicationConfigForm;
+use Icinga\Forms\Config\General\DefaultAuthenticationDomainConfigForm;
+use Icinga\Forms\Config\General\LoggingConfigForm;
+use Icinga\Forms\Config\General\ThemingConfigForm;
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Configuration form for application-wide options
+ */
+class GeneralConfigForm extends ConfigForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $appConfigForm = new ApplicationConfigForm();
+ $loggingConfigForm = new LoggingConfigForm();
+ $themingConfigForm = new ThemingConfigForm();
+ $domainConfigForm = new DefaultAuthenticationDomainConfigForm();
+ $this->addSubForm($appConfigForm->create($formData));
+ $this->addSubForm($loggingConfigForm->create($formData));
+ $this->addSubForm($themingConfigForm->create($formData));
+ $this->addSubForm($domainConfigForm->create($formData));
+ }
+}
diff --git a/application/forms/Config/Resource/DbResourceForm.php b/application/forms/Config/Resource/DbResourceForm.php
new file mode 100644
index 0000000..c9d7601
--- /dev/null
+++ b/application/forms/Config/Resource/DbResourceForm.php
@@ -0,0 +1,239 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Application\Platform;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying database resources
+ */
+class DbResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_db');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $dbChoices = array();
+ if (Platform::hasMysqlSupport()) {
+ $dbChoices['mysql'] = 'MySQL';
+ }
+ if (Platform::hasPostgresqlSupport()) {
+ $dbChoices['pgsql'] = 'PostgreSQL';
+ }
+ if (Platform::hasMssqlSupport()) {
+ $dbChoices['mssql'] = 'MSSQL';
+ }
+ if (Platform::hasIbmSupport()) {
+ $dbChoices['ibm'] = 'IBM (DB2)';
+ }
+ if (Platform::hasOracleSupport()) {
+ $dbChoices['oracle'] = 'Oracle';
+ }
+ if (Platform::hasOciSupport()) {
+ $dbChoices['oci'] = 'Oracle (OCI8)';
+ }
+ if (Platform::hasSqliteSupport()) {
+ $dbChoices['sqlite'] = 'SQLite';
+ }
+
+ $offerPostgres = false;
+ $offerMysql = false;
+ $dbChoice = isset($formData['db']) ? $formData['db'] : key($dbChoices);
+ if ($dbChoice === 'pgsql') {
+ $offerPostgres = true;
+ } elseif ($dbChoice === 'mysql') {
+ $offerMysql = true;
+ }
+
+ if ($dbChoice === 'oracle' || $dbChoice === 'oci') {
+ $hostIsRequired = false;
+ } else {
+ $hostIsRequired = true;
+ }
+
+ $socketInfo = '';
+ if ($offerPostgres) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify the path to the unix domain socket directory'
+ );
+ } elseif ($offerMysql) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify localhost'
+ );
+ }
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'db',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Database Type'),
+ 'description' => $this->translate('The type of SQL database'),
+ 'multiOptions' => $dbChoices
+ )
+ );
+ if ($dbChoice === 'sqlite') {
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ } else {
+ $this->addElement(
+ 'text',
+ 'host',
+ array (
+ 'required' => $hostIsRequired,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate('The hostname of the database')
+ . ($socketInfo ? '. ' . $socketInfo : ''),
+ 'value' => $hostIsRequired ? 'localhost' : ''
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'description' => $this->translate('The port to use'),
+ 'label' => $this->translate('Port'),
+ 'preserveDefault' => true,
+ 'required' => $offerPostgres,
+ 'value' => $offerPostgres ? 5432 : null
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'username',
+ array (
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate('The user name to use for authentication')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'renderPassword' => true,
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password to use for authentication'),
+ 'autocomplete' => 'new-password'
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'charset',
+ array (
+ 'description' => $this->translate('The character set for the database'),
+ 'label' => $this->translate('Character Set')
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'use_ssl',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('Use SSL'),
+ 'description' => $this->translate(
+ 'Whether to encrypt the connection or to authenticate using certificates'
+ )
+ )
+ );
+ if (isset($formData['use_ssl']) && $formData['use_ssl']) {
+ if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
+ $this->addElement(
+ 'checkbox',
+ 'ssl_do_not_verify_server_cert',
+ array(
+ 'label' => $this->translate('SSL Do Not Verify Server Certificate'),
+ 'description' => $this->translate(
+ 'Whether to disable verification of the server certificate'
+ )
+ )
+ );
+ }
+ $this->addElement(
+ 'text',
+ 'ssl_key',
+ array(
+ 'label' => $this->translate('SSL Key'),
+ 'description' => $this->translate('The client key file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cert',
+ array(
+ 'label' => $this->translate('SSL Certificate'),
+ 'description' => $this->translate('The certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_ca',
+ array(
+ 'label' => $this->translate('SSL CA'),
+ 'description' => $this->translate('The CA certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_capath',
+ array(
+ 'label' => $this->translate('SSL CA Path'),
+ 'description' => $this->translate(
+ 'The trusted CA certificates in PEM format directory path'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cipher',
+ array(
+ 'label' => $this->translate('SSL Cipher'),
+ 'description' => $this->translate('The list of permissible ciphers')
+ )
+ );
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/FileResourceForm.php b/application/forms/Config/Resource/FileResourceForm.php
new file mode 100644
index 0000000..b98f1b4
--- /dev/null
+++ b/application/forms/Config/Resource/FileResourceForm.php
@@ -0,0 +1,67 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Zend_Validate_Callback;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying file resources
+ */
+class FileResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_file');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filename',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Filepath'),
+ 'description' => $this->translate('The filename to fetch information from'),
+ 'validators' => array('ReadablePathValidator')
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'fields',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Pattern'),
+ 'description' => $this->translate('The pattern by which to identify columns.'),
+ 'requirement' => $this->translate('The column pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/LdapResourceForm.php b/application/forms/Config/Resource/LdapResourceForm.php
new file mode 100644
index 0000000..7ffccdc
--- /dev/null
+++ b/application/forms/Config/Resource/LdapResourceForm.php
@@ -0,0 +1,129 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+use Icinga\Protocol\Ldap\LdapConnection;
+
+/**
+ * Form class for adding/modifying ldap resources
+ */
+class LdapResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_ldap');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $defaultPort = ! array_key_exists('encryption', $formData) || $formData['encryption'] !== LdapConnection::LDAPS
+ ? 389
+ : 636;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'hostname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate(
+ 'The hostname or address of the LDAP server to use for authentication.'
+ . ' You can also provide multiple hosts separated by a space'
+ ),
+ 'value' => 'localhost'
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'required' => true,
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Port'),
+ 'description' => $this->translate('The port of the LDAP server to use for authentication'),
+ 'value' => $defaultPort
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'encryption',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Encryption'),
+ 'description' => $this->translate(
+ 'Whether to encrypt communication. Choose STARTTLS or LDAPS for encrypted communication or'
+ . ' none for unencrypted communication'
+ ),
+ 'multiOptions' => array(
+ 'none' => $this->translate('None', 'resource.ldap.encryption'),
+ LdapConnection::STARTTLS => 'STARTTLS',
+ LdapConnection::LDAPS => 'LDAPS'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'root_dn',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Root DN'),
+ 'description' => $this->translate(
+ 'Only the root and its child nodes will be accessible on this resource.'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'bind_dn',
+ array(
+ 'label' => $this->translate('Bind DN'),
+ 'description' => $this->translate(
+ 'The user dn to use for querying the ldap server. Leave the dn and password empty for attempting'
+ . ' an anonymous bind'
+ )
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'bind_pw',
+ array(
+ 'renderPassword' => true,
+ 'label' => $this->translate('Bind Password'),
+ 'description' => $this->translate('The password to use for querying the ldap server')
+ )
+ );
+
+ $this->addElement(
+ 'number',
+ 'timeout',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Timeout'),
+ 'description' => $this->translate('Connection timeout for every LDAP connection'),
+ 'value' => 5 // see LdapConnection::__construct()
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/SshResourceForm.php b/application/forms/Config/Resource/SshResourceForm.php
new file mode 100644
index 0000000..a15dc8c
--- /dev/null
+++ b/application/forms/Config/Resource/SshResourceForm.php
@@ -0,0 +1,148 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\ConfigObject;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Web\Form;
+use Icinga\Util\File;
+use Zend_Validate_Callback;
+
+/**
+ * Form class for adding/modifying ssh identity resources
+ */
+class SshResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_ssh');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('User'),
+ 'description' => $this->translate(
+ 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be'
+ . ' possible for this user'
+ )
+ )
+ );
+
+ if ($this->getRequest()->getActionName() != 'editresource') {
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ if (substr(ltrim($value), 0, 7) === 'file://'
+ || openssl_pkey_get_private($value) === false
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('The given SSH key is invalid'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'private_key',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Private Key'),
+ 'description' => $this->translate('The private key which will be used for the SSH connections'),
+ 'class' => 'resource ssh-identity',
+ 'validators' => array($callbackValidator)
+ )
+ );
+ } else {
+ $resourceName = $formData['name'];
+ $this->addElement(
+ 'note',
+ 'private_key_note',
+ array(
+ 'escape' => false,
+ 'label' => $this->translate('Private Key'),
+ 'value' => sprintf(
+ '<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>',
+ $this->getView()->url('config/removeresource', array('resource' => $resourceName)),
+ $this->getView()->escape(sprintf($this->translate(
+ 'Remove the %s resource'
+ ), $resourceName)),
+ $this->translate('To modify the private key you must recreate this resource.')
+ )
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove the assigned key to the resource
+ *
+ * @param ConfigObject $config
+ *
+ * @return bool
+ */
+ public static function beforeRemove(ConfigObject $config)
+ {
+ $file = $config->private_key;
+
+ if (file_exists($file)) {
+ unlink($file);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Creates the assigned key to the resource
+ *
+ * @param ResourceConfigForm $form
+ *
+ * @return bool
+ */
+ public static function beforeAdd(ResourceConfigForm $form)
+ {
+ $configDir = Icinga::app()->getConfigDir();
+ $user = $form->getElement('user')->getValue();
+
+ $filePath = join(DIRECTORY_SEPARATOR, [$configDir, 'ssh', sha1($user)]);
+ if (! file_exists($filePath)) {
+ $file = File::create($filePath, 0600);
+ } else {
+ $form->error(
+ sprintf($form->translate('The private key for the user "%s" already exists.'), $user)
+ );
+ return false;
+ }
+
+ $file->fwrite($form->getElement('private_key')->getValue());
+
+ $form->getElement('private_key')->setValue($filePath);
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/ResourceConfigForm.php b/application/forms/Config/ResourceConfigForm.php
new file mode 100644
index 0000000..c2d0d18
--- /dev/null
+++ b/application/forms/Config/ResourceConfigForm.php
@@ -0,0 +1,442 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Application\Config;
+use InvalidArgumentException;
+use Icinga\Application\Platform;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\Config\Resource\DbResourceForm;
+use Icinga\Forms\Config\Resource\FileResourceForm;
+use Icinga\Forms\Config\Resource\LdapResourceForm;
+use Icinga\Forms\Config\Resource\SshResourceForm;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Zend_Form_Element;
+
+class ResourceConfigForm extends ConfigForm
+{
+ /**
+ * Bogus password when inspecting password elements
+ *
+ * @var string
+ */
+ protected static $dummyPassword = '_web_form_5847ed1b5b8ca';
+
+ /**
+ * If the global config must be updated because a resource has been changed, this is the updated global config
+ *
+ * @var Config|null
+ */
+ protected $updatedAppConfig = null;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * Return a form object for the given resource type
+ *
+ * @param string $type The resource type for which to return a form
+ *
+ * @return Form
+ */
+ public function getResourceForm($type)
+ {
+ if ($type === 'db') {
+ return new DbResourceForm();
+ } elseif ($type === 'ldap') {
+ return new LdapResourceForm();
+ } elseif ($type === 'file') {
+ return new FileResourceForm();
+ } elseif ($type === 'ssh') {
+ return new SshResourceForm();
+ } else {
+ throw new InvalidArgumentException(sprintf($this->translate('Invalid resource type "%s" provided'), $type));
+ }
+ }
+
+ /**
+ * Add a particular resource
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $values The values to extend the configuration with
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case the resource does already exist
+ */
+ public function add(array $values)
+ {
+ $name = isset($values['name']) ? $values['name'] : '';
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif ($this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Resource already exists'));
+ }
+
+ unset($values['name']);
+ $this->config->setSection($name, $values);
+ return $this;
+ }
+
+ /**
+ * Edit a particular resource
+ *
+ * @param string $name The name of the resource to edit
+ * @param array $values The values to edit the configuration with
+ *
+ * @return ConfigObject The edited configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function edit($name, array $values)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Old resource name missing'));
+ } elseif (! ($newName = isset($values['name']) ? $values['name'] : '')) {
+ throw new InvalidArgumentException($this->translate('New resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $this->config->removeSection($name);
+ unset($values['name']);
+ $this->config->setSection($newName, $resourceConfig->merge($values));
+
+ if ($newName !== $name) {
+ $appConfig = Config::app();
+ $section = $appConfig->getSection('global');
+ if ($section->config_resource === $name) {
+ $section->config_resource = $newName;
+ $this->updatedAppConfig = $appConfig->setSection('global', $section);
+ }
+ }
+
+ return $resourceConfig;
+ }
+
+ /**
+ * Remove a particular resource
+ *
+ * @param string $name The name of the resource to remove
+ *
+ * @return ConfigObject The removed resource configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function remove($name)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $resourceForm = $this->getResourceForm($resourceConfig->type);
+ if (method_exists($resourceForm, 'beforeRemove')) {
+ $resourceForm::beforeRemove($resourceConfig);
+ }
+
+ $this->config->removeSection($name);
+ return $resourceConfig;
+ }
+
+ /**
+ * Add or edit a resource and save the configuration
+ *
+ * Performs a connectivity validation using the submitted values. A checkbox is
+ * added to the form to skip the check if it fails and redirection is aborted.
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $resourceForm = $this->getResourceForm($this->getElement('type')->getValue());
+
+ if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ $this->addElement($this->getForceCreationCheckbox());
+ return false;
+ }
+ }
+
+ $resource = $this->request->getQuery('resource');
+ try {
+ if ($resource === null) { // create new resource
+ if (method_exists($resourceForm, 'beforeAdd')) {
+ if (! $resourceForm::beforeAdd($this)) {
+ return false;
+ }
+ }
+ $this->add(static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully created');
+ } else { // edit existing resource
+ $this->edit($resource, static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully changed');
+ }
+ } catch (InvalidArgumentException $e) {
+ Notification::error($e->getMessage());
+ return false;
+ }
+
+ if ($this->save()) {
+ Notification::success(sprintf($message, $this->getElement('name')->getValue()));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Populate the form in case a resource is being edited
+ *
+ * @see Form::onRequest()
+ *
+ * @throws ConfigurationError In case the backend name is missing in the request or is invalid
+ */
+ public function onRequest()
+ {
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null) {
+ if ($resource === '') {
+ throw new ConfigurationError($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($resource)) {
+ throw new ConfigurationError($this->translate('Unknown resource provided'));
+ }
+ $configValues = $this->config->getSection($resource)->toArray();
+ $configValues['name'] = $resource;
+ $this->populate($configValues);
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password' && strlen($element->getValue())) {
+ $element->setValue(static::$dummyPassword);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a checkbox to be displayed at the beginning of the form
+ * which allows the user to skip the connection validation
+ *
+ * @return Zend_Form_Element
+ */
+ protected function getForceCreationCheckbox()
+ {
+ return $this->createElement(
+ 'checkbox',
+ 'force_creation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Force Changes'),
+ 'description' => $this->translate('Check this box to enforce changes without connectivity validation')
+ )
+ );
+ }
+
+ /**
+ * @see Form::createElemeents()
+ */
+ public function createElements(array $formData)
+ {
+ $resourceType = isset($formData['type']) ? $formData['type'] : 'db';
+
+ $resourceTypes = array(
+ 'file' => $this->translate('File'),
+ 'ssh' => $this->translate('SSH Identity'),
+ );
+ if ($resourceType === 'ldap' || Platform::hasLdapSupport()) {
+ $resourceTypes['ldap'] = 'LDAP';
+ }
+ if ($resourceType === 'db' || Platform::hasDatabaseSupport()) {
+ $resourceTypes['db'] = $this->translate('SQL Database');
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Resource Type'),
+ 'description' => $this->translate('The type of resource'),
+ 'multiOptions' => $resourceTypes,
+ 'value' => $resourceType
+ )
+ );
+
+ if (isset($formData['force_creation']) && $formData['force_creation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addElement($this->getForceCreationCheckbox());
+ }
+
+ $this->addElements($this->getResourceForm($resourceType)->createElements($formData)->getElements());
+ }
+
+ /**
+ * Create a resource by using the given form's values and return its inspection results
+ *
+ * @param Form $form
+ *
+ * @return ?Inspection
+ */
+ public static function inspectResource(Form $form)
+ {
+ if ($form->getValue('type') !== 'ssh') {
+ $resource = ResourceFactory::createResource(new ConfigObject($form->getValues()));
+ if ($resource instanceof Inspectable) {
+ return $resource->inspect();
+ }
+ }
+ }
+
+ /**
+ * Run the configured resource's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'resource_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if ($this->getElement('resource_validation')->isChecked() && parent::isValid($formData)) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $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.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'resource_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'resource-progress');
+ $this->addElement(
+ 'note',
+ 'resource-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'resource-progress'))
+ )
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('btn_submit', 'resource_validation', 'resource-progress'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null && $this->config->hasSection($resource)) {
+ $resourceConfig = $this->config->getSection($resource)->toArray();
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password') {
+ $name = $element->getName();
+ if (isset($values[$name]) && $values[$name] === static::$dummyPassword) {
+ if (isset($resourceConfig[$name])) {
+ $values[$name] = $resourceConfig[$name];
+ } else {
+ unset($values[$name]);
+ }
+ }
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function writeConfig(Config $config)
+ {
+ parent::writeConfig($config);
+ if ($this->updatedAppConfig !== null) {
+ $this->updatedAppConfig->saveIni();
+ }
+ }
+}
diff --git a/application/forms/Config/User/CreateMembershipForm.php b/application/forms/Config/User/CreateMembershipForm.php
new file mode 100644
index 0000000..6c74c8c
--- /dev/null
+++ b/application/forms/Config/User/CreateMembershipForm.php
@@ -0,0 +1,192 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\User;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for creating one or more group memberships
+ */
+class CreateMembershipForm extends Form
+{
+ /**
+ * The user group backends to fetch groups from
+ *
+ * Each backend must implement the Icinga\Data\Extensible and Icinga\Data\Selectable interface.
+ *
+ * @var array
+ */
+ protected $backends;
+
+ /**
+ * The username to create memberships for
+ *
+ * @var string
+ */
+ protected $userName;
+
+ /**
+ * Set the user group backends to fetch groups from
+ *
+ * @param array $backends
+ *
+ * @return $this
+ */
+ public function setBackends($backends)
+ {
+ $this->backends = $backends;
+ return $this;
+ }
+
+ /**
+ * Set the username to create memberships for
+ *
+ * @param string $userName
+ *
+ * @return $this
+ */
+ public function setUsername($userName)
+ {
+ $this->userName = $userName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $query = $this->createDataSource()->select()->from('group', array('group_name', 'backend_name'));
+
+ $options = array();
+ foreach ($query as $row) {
+ $options[$row->backend_name . ';' . $row->group_name] = $row->group_name . ' (' . $row->backend_name . ')';
+ }
+
+ $this->addElement(
+ 'multiselect',
+ 'groups',
+ array(
+ 'required' => true,
+ 'multiOptions' => $options,
+ 'label' => $this->translate('Groups'),
+ 'description' => sprintf(
+ $this->translate('Select one or more groups where to add %s as member'),
+ $this->userName
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Create memberships for %s'), $this->userName));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+
+ /**
+ * Instantly redirect back in case the user is already a member of all groups
+ */
+ public function onRequest()
+ {
+ if ($this->createDataSource()->select()->from('group')->count() === 0) {
+ Notification::info(sprintf($this->translate('User %s is already a member of all groups'), $this->userName));
+ $this->getResponse()->redirectAndExit($this->getRedirectUrl());
+ }
+ }
+
+ /**
+ * Create the memberships for the user
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $backendMap = array();
+ foreach ($this->backends as $backend) {
+ $backendMap[$backend->getName()] = $backend;
+ }
+
+ $single = null;
+ $groupName = null;
+ foreach ($this->getValue('groups') as $backendAndGroup) {
+ list($backendName, $groupName) = explode(';', $backendAndGroup, 2);
+ try {
+ $backendMap[$backendName]->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $groupName,
+ 'user_name' => $this->userName
+ )
+ );
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $this->userName,
+ $groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(
+ sprintf($this->translate('Membership for group %s created successfully'), $groupName)
+ );
+ } else {
+ Notification::success($this->translate('Memberships created successfully'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Create and return a data source to fetch all groups from all backends where the user is not already a member of
+ *
+ * @return ArrayDatasource
+ */
+ protected function createDataSource()
+ {
+ $groups = $failures = array();
+ foreach ($this->backends as $backend) {
+ try {
+ $memberships = $backend
+ ->select()
+ ->from('group_membership', array('group_name'))
+ ->where('user_name', $this->userName)
+ ->fetchColumn();
+ foreach ($backend->select(array('group_name')) as $row) {
+ if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter
+ $row->backend_name = $backend->getName();
+ $groups[] = $row;
+ }
+ }
+ } catch (Exception $e) {
+ $failures[] = array($backend->getName(), $e);
+ }
+ }
+
+ if (empty($groups) && !empty($failures)) {
+ // In case there are only failures, throw the very first exception again
+ throw $failures[0][1];
+ } elseif (! empty($failures)) {
+ foreach ($failures as $failure) {
+ Logger::error($failure[1]);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any groups from backend %s. Please check your log'),
+ $failure[0]
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+}
diff --git a/application/forms/Config/User/UserForm.php b/application/forms/Config/User/UserForm.php
new file mode 100644
index 0000000..fb2ef4d
--- /dev/null
+++ b/application/forms/Config/User/UserForm.php
@@ -0,0 +1,210 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\User;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Web\Notification;
+
+class UserForm extends RepositoryForm
+{
+ /**
+ * Create and add elements to this form to insert or update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'is_active',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Active'),
+ 'description' => $this->translate('Prevents the user from logging in if unchecked')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Password')
+ )
+ );
+
+ $this->setTitle($this->translate('Add a new user'));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Create and add elements to this form to update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'description' => $this->translate('Leave empty for not updating the user\'s password'),
+ 'label' => $this->translate('Password'),
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Edit user %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * Update a user
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('user_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('user', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * Strips off the password if null or the empty string.
+ *
+ * @param bool $suppressArrayNotation
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ // before checking if password values is empty
+ // we have to check that the password field is set
+ // otherwise an error is thrown
+ if (isset($values['password']) && ! $values['password']) {
+ unset($values['password']);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Create and add elements to this form to delete a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove user %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a user
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('user_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('User added successfully');
+ } else {
+ return $this->translate('Failed to add user');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}
diff --git a/application/forms/Config/UserBackend/DbBackendForm.php b/application/forms/Config/UserBackend/DbBackendForm.php
new file mode 100644
index 0000000..693ea14
--- /dev/null
+++ b/application/forms/Config/UserBackend/DbBackendForm.php
@@ -0,0 +1,82 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying database user backends
+ */
+class DbBackendForm extends Form
+{
+ /**
+ * The database resource names the user can choose from
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_db');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate(
+ 'The database connection to use for authenticating with this provider'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array()
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'db'
+ )
+ );
+ }
+}
diff --git a/application/forms/Config/UserBackend/ExternalBackendForm.php b/application/forms/Config/UserBackend/ExternalBackendForm.php
new file mode 100644
index 0000000..f4a4639
--- /dev/null
+++ b/application/forms/Config/UserBackend/ExternalBackendForm.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Zend_Validate_Callback;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying user backends of type "external"
+ */
+class ExternalBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_external');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'strip_username_regexp',
+ array(
+ 'label' => $this->translate('Filter Pattern'),
+ 'description' => $this->translate(
+ 'The filter to use to strip specific parts off from usernames.'
+ . ' Leave empty if you do not want to strip off anything.'
+ ),
+ 'requirement' => $this->translate('The filter pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'external'
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * Validate the configuration by creating a backend and requesting the user count
+ *
+ * Returns always true as backends of type "external" are just "passive" backends.
+ *
+ * @param Form $form The form to fetch the configuration values from
+ *
+ * @return bool Whether validation succeeded or not
+ */
+ public static function isValidUserBackend(Form $form)
+ {
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserBackend/LdapBackendForm.php b/application/forms/Config/UserBackend/LdapBackendForm.php
new file mode 100644
index 0000000..e7804cc
--- /dev/null
+++ b/application/forms/Config/UserBackend/LdapBackendForm.php
@@ -0,0 +1,414 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Exception;
+use Icinga\Data\ResourceFactory;
+use Icinga\Protocol\Ldap\LdapCapabilities;
+use Icinga\Protocol\Ldap\LdapConnection;
+use Icinga\Protocol\Ldap\LdapException;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying LDAP user backends
+ */
+class LdapBackendForm extends Form
+{
+ /**
+ * The ldap resource names the user can choose from
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * Default values for the form elements
+ *
+ * @var string[]
+ */
+ protected $suggestions = array();
+
+ /**
+ * Cache for {@link getLdapCapabilities()}
+ *
+ * @var LdapCapabilities
+ */
+ protected $ldapCapabilities;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_ldap');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $isAd = isset($formData['type']) ? $formData['type'] === 'msldap' : false;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others.'
+ ),
+ 'value' => $this->getSuggestion('name')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate(
+ 'The LDAP connection to use for authenticating with this provider.'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array(),
+ 'value' => $this->getSuggestion('resource')
+ )
+ );
+
+ if (! $isAd && !empty($this->resources)) {
+ $this->addElement(
+ 'button',
+ 'discovery_btn',
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => 'discovery_btn',
+ 'label' => $this->translate('Discover', 'A button to discover LDAP capabilities'),
+ 'title' => $this->translate(
+ 'Push to fill in the chosen connection\'s default settings.'
+ ),
+ 'decorators' => array(
+ array('ViewHelper', array('separator' => '')),
+ array('Spinner'),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ }
+
+ if ($isAd) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $this->getSuggestion('user_class', $userClass)
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'value' => $this->getSuggestion('filter', $filter),
+ 'label' => $this->translate('LDAP Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression.'
+ . ' (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ // This is not meant to be a full syntax check. It will just
+ // ensure that we can safely strip unnecessary parentheses.
+ $v = trim($v);
+ return ! $v || $v[0] !== '(' || (
+ strpos($v, ')(') !== false ? substr($v, -2) === '))' : substr($v, -1) === ')'
+ );
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter is invalid. Please check your syntax.')
+ )
+ )
+ )
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing the user name on the LDAP server.'
+ ),
+ 'value' => $this->getSuggestion('user_name_attribute', $userNameAttribute)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => $this->getSuggestion('backend', $isAd ? 'msldap' : 'ldap')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'required' => false,
+ 'label' => $this->translate('LDAP Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $this->getSuggestion('base_dn')
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Domain'),
+ 'description' => $this->translate(
+ 'The domain the LDAP server is responsible for upon authentication.'
+ . ' Note that if you specify a domain here,'
+ . ' the LDAP backend only authenticates users who specify a domain upon login.'
+ . ' If the domain of the user matches the domain configured here, this backend is responsible for'
+ . ' authenticating the user based on the username without the domain part.'
+ . ' If your LDAP backend holds usernames with a domain part or if it is not necessary in your setup'
+ . ' to authenticate users based on their domains, leave this field empty.'
+ ),
+ 'preserveDefault' => true,
+ 'value' => $this->getSuggestion('domain')
+ )
+ );
+
+ $this->addElement(
+ 'button',
+ 'btn_discover_domain',
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => 'discovery_btn',
+ 'label' => $this->translate('Discover the domain'),
+ 'title' => $this->translate(
+ 'Push to disover and fill in the domain of the LDAP server.'
+ ),
+ 'decorators' => array(
+ array('ViewHelper', array('separator' => '')),
+ array('Spinner'),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ }
+
+ public function isValidPartial(array $formData)
+ {
+ $isAd = isset($formData['type']) && $formData['type'] === 'msldap';
+ $baseDn = null;
+ $hasAdOid = false;
+ $discoverySuccessful = false;
+
+ if (! $isAd && ! empty($this->resources) && isset($formData['discovery_btn'])
+ && $formData['discovery_btn'] === 'discovery_btn') {
+ $discoverySuccessful = true;
+ try {
+ $capabilities = $this->getLdapCapabilities($formData);
+ $baseDn = $capabilities->getDefaultNamingContext();
+ $hasAdOid = $capabilities->isActiveDirectory();
+ } catch (Exception $e) {
+ $this->warning(sprintf(
+ $this->translate('Failed to discover the chosen LDAP connection: %s'),
+ $e->getMessage()
+ ));
+ $discoverySuccessful = false;
+ }
+ }
+
+ if ($discoverySuccessful) {
+ if ($isAd || $hasAdOid) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $formData['user_class'] = $userClass;
+
+ if (! isset($formData['filter']) || $formData['filter'] === '') {
+ $formData['filter'] = $filter;
+ }
+
+ $formData['user_name_attribute'] = $userNameAttribute;
+
+ if ($baseDn !== null && (! isset($formData['base_dn']) || $formData['base_dn'] === '')) {
+ $formData['base_dn'] = $baseDn;
+ }
+ }
+
+ if (isset($formData['btn_discover_domain']) && $formData['btn_discover_domain'] === 'discovery_btn') {
+ try {
+ $formData['domain'] = $this->discoverDomain($formData);
+ } catch (LdapException $e) {
+ $this->error($e->getMessage());
+ }
+ }
+
+ return parent::isValidPartial($formData);
+ }
+
+ /**
+ * Get the LDAP capabilities of either the resource specified by the user or the default one
+ *
+ * @param string[] $formData
+ *
+ * @return LdapCapabilities
+ */
+ protected function getLdapCapabilities(array $formData)
+ {
+ if ($this->ldapCapabilities === null) {
+ $this->ldapCapabilities = ResourceFactory::create(
+ isset($formData['resource']) ? $formData['resource'] : reset($this->resources)
+ )->bind()->getCapabilities();
+ }
+
+ return $this->ldapCapabilities;
+ }
+
+ /**
+ * Discover the domain the LDAP server is responsible for
+ *
+ * @param string[] $formData
+ *
+ * @return string
+ */
+ protected function discoverDomain(array $formData)
+ {
+ $cap = $this->getLdapCapabilities($formData);
+
+ if ($cap->isActiveDirectory()) {
+ $netBiosName = $cap->getNetBiosName();
+ if ($netBiosName !== null) {
+ return $netBiosName;
+ }
+ }
+
+ return $this->defaultNamingContextToFQDN($cap);
+ }
+
+ /**
+ * Get the default naming context as FQDN
+ *
+ * @param LdapCapabilities $cap
+ *
+ * @return string|null
+ */
+ protected function defaultNamingContextToFQDN(LdapCapabilities $cap)
+ {
+ $defaultNamingContext = $cap->getDefaultNamingContext();
+ if ($defaultNamingContext !== null) {
+ $validationMatches = array();
+ if (preg_match('/\bdc=[^,]+(?:,dc=[^,]+)*$/', strtolower($defaultNamingContext), $validationMatches)) {
+ $splitMatches = array();
+ preg_match_all('/dc=([^,]+)/', $validationMatches[0], $splitMatches);
+ return implode('.', $splitMatches[1]);
+ }
+ }
+ }
+
+ /**
+ * Get the default values for the form elements
+ *
+ * @return string[]
+ */
+ public function getSuggestions()
+ {
+ return $this->suggestions;
+ }
+
+ /**
+ * Get the default value for the given form element or the given default
+ *
+ * @param string $element
+ * @param string $default
+ *
+ * @return string
+ */
+ public function getSuggestion($element, $default = null)
+ {
+ return isset($this->suggestions[$element]) ? $this->suggestions[$element] : $default;
+ }
+
+ /**
+ * Set the default values for the form elements
+ *
+ * @param string[] $suggestions
+ *
+ * @return $this
+ */
+ public function setSuggestions(array $suggestions)
+ {
+ $this->suggestions = $suggestions;
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php
new file mode 100644
index 0000000..fdca657
--- /dev/null
+++ b/application/forms/Config/UserBackendConfigForm.php
@@ -0,0 +1,482 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
+use Icinga\Forms\Config\UserBackend\DbBackendForm;
+use Icinga\Forms\Config\UserBackend\LdapBackendForm;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing user backends
+ */
+class UserBackendConfigForm extends ConfigForm
+{
+ /**
+ * The available user backend resources split by type
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
+ /**
+ * The loaded custom backends list
+ *
+ * @var array
+ */
+ protected $customBackends;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ $this->customBackends = UserBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param Config $resourceConfig The resource configuration
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError In case there are no valid resources for authentication available
+ */
+ public function setResourceConfig(Config $resourceConfig)
+ {
+ $resources = array();
+ foreach ($resourceConfig as $name => $resource) {
+ if (in_array($resource->type, array('db', 'ldap'))) {
+ $resources[$resource->type][] = $name;
+ }
+ }
+
+ if (empty($resources)) {
+ $externalBackends = $this->config->toArray();
+ array_walk(
+ $externalBackends,
+ function (&$authBackendCfg) {
+ if (! isset($authBackendCfg['backend']) || $authBackendCfg['backend'] !== 'external') {
+ $authBackendCfg = null;
+ }
+ }
+ );
+ if (count(array_filter($externalBackends)) > 0 && (
+ $this->backendToLoad === null || !isset($externalBackends[$this->backendToLoad])
+ )) {
+ throw new ConfigurationError($this->translate(
+ 'Could not find any valid user backend resources.'
+ . ' Please configure a resource for authentication first.'
+ ));
+ }
+ }
+
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ $form = new DbBackendForm();
+ $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array());
+ break;
+ case 'ldap':
+ case 'msldap':
+ $form = new LdapBackendForm();
+ $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array());
+ break;
+ case 'external':
+ $form = new ExternalBackendForm();
+ break;
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+
+ return $form;
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException(
+ $this->translate('A user backend with the name "%s" does already exist'),
+ $backendName
+ );
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $backendConfig->merge($data);
+ $this->config->setSection($name, $backendConfig);
+ return $this;
+ }
+
+ /**
+ * Remove a user backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Move the given user backend up or down in order
+ *
+ * @param string $name The name of the backend to be moved
+ * @param int $position The new (absolute) position of the backend
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function move($name, $position)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendOrder = $this->config->keys();
+ array_splice($backendOrder, array_search($name, $backendOrder), 1);
+ array_splice($backendOrder, $position, 0, $name);
+
+ $newConfig = array();
+ foreach ($backendOrder as $backendName) {
+ $newConfig[$backendName] = $this->config->getSection($backendName);
+ }
+
+ $config = Config::fromArray($newConfig);
+ $this->config = $config->setConfigFile($this->config->getConfigFile());
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array();
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+
+ if (isset($this->resources['db'])) {
+ $backendTypes['db'] = $this->translate('Database');
+ }
+ if (isset($this->resources['ldap'])) {
+ $backendTypes['ldap'] = 'LDAP';
+ $backendTypes['msldap'] = 'ActiveDirectory';
+ }
+
+ $externalBackends = array_filter(
+ $this->config->toArray(),
+ function ($authBackendCfg) {
+ return isset($authBackendCfg['backend']) && $authBackendCfg['backend'] === 'external';
+ }
+ );
+ if ($backendType === 'external' || empty($externalBackends)) {
+ $backendTypes['external'] = $this->translate('External');
+ }
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate(
+ 'The type of the resource to use for this authenticaton provider'
+ ),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addSkipValidationCheckbox();
+ }
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['name'] = $this->backendToLoad;
+ $data['type'] = $data['backend'];
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($el = $this->getElement('skip_validation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ if ($el === null) {
+ $this->addSkipValidationCheckbox();
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a user backend by using the given form's values and return its inspection results
+ *
+ * Returns null for non-inspectable backends.
+ *
+ * @param Form $form
+ *
+ * @return Inspection|null
+ */
+ public static function inspectUserBackend(Form $form)
+ {
+ $backend = UserBackend::create(null, new ConfigObject($form->getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the connection validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this box to enforce changes without validating that authentication is possible.'
+ )
+ )
+ );
+ }
+
+ /**
+ * 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 (! parent::isValidPartial($formData)) {
+ return false;
+ }
+
+ if ($this->getElement('backend_validation')->isChecked() && parent::isValid($formData)) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $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.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendReorderForm.php b/application/forms/Config/UserBackendReorderForm.php
new file mode 100644
index 0000000..019c032
--- /dev/null
+++ b/application/forms/Config/UserBackendReorderForm.php
@@ -0,0 +1,86 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Application\Config;
+use Icinga\Forms\ConfigForm;
+use Icinga\Exception\NotFoundError;
+use Icinga\Web\Notification;
+
+class UserBackendReorderForm extends ConfigForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_reorder_authbackend');
+ $this->setViewScript('form/reorder-authbackend.phtml');
+ }
+
+ /**
+ * Return the ordered backend names
+ *
+ * @return array
+ */
+ public function getBackendOrder()
+ {
+ return $this->config->keys();
+ }
+
+ /**
+ * Return the ordered backend configuration
+ *
+ * @return Config
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ // This adds just a dummy element to be able to utilize Form::getValue as part of onSuccess()
+ $this->addElement('hidden', 'backend_newpos');
+ }
+
+ /**
+ * Update the user backend order and save the configuration
+ */
+ public function onSuccess()
+ {
+ $newPosData = $this->getValue('backend_newpos');
+ if ($newPosData) {
+ $configForm = $this->getConfigForm();
+ list($backendName, $position) = explode('|', $newPosData, 2);
+
+ try {
+ if ($configForm->move($backendName, $position)->save()) {
+ Notification::success($this->translate('Authentication order updated'));
+ } else {
+ return false;
+ }
+ } catch (NotFoundError $_) {
+ Notification::error(sprintf($this->translate('User backend "%s" not found'), $backendName));
+ }
+ }
+ }
+
+ /**
+ * Return the config form for user backends
+ *
+ * @return ConfigForm
+ */
+ protected function getConfigForm()
+ {
+ $form = new UserBackendConfigForm();
+ $form->setIniConfig($this->config);
+ return $form;
+ }
+}
diff --git a/application/forms/Config/UserGroup/AddMemberForm.php b/application/forms/Config/UserGroup/AddMemberForm.php
new file mode 100644
index 0000000..cda9d52
--- /dev/null
+++ b/application/forms/Config/UserGroup/AddMemberForm.php
@@ -0,0 +1,183 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Exception;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Selectable;
+use Icinga\Exception\NotFoundError;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for adding one or more group members
+ */
+class AddMemberForm extends Form
+{
+ /**
+ * The data source to fetch users from
+ *
+ * @var Selectable
+ */
+ protected $ds;
+
+ /**
+ * The user group backend to use
+ *
+ * @var Extensible
+ */
+ protected $backend;
+
+ /**
+ * The group to add members for
+ *
+ * @var string
+ */
+ protected $groupName;
+
+ /**
+ * Set the data source to fetch users from
+ *
+ * @param Selectable $ds
+ *
+ * @return $this
+ */
+ public function setDataSource(Selectable $ds)
+ {
+ $this->ds = $ds;
+ return $this;
+ }
+
+ /**
+ * Set the user group backend to use
+ *
+ * @param Extensible $backend
+ *
+ * @return $this
+ */
+ public function setBackend(Extensible $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ /**
+ * Set the group to add members for
+ *
+ * @param string $groupName
+ *
+ * @return $this
+ */
+ public function setGroupName($groupName)
+ {
+ $this->groupName = $groupName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ // TODO(jom): Fetching already existing members to prevent the user from mistakenly creating duplicate
+ // memberships (no matter whether the data source permits it or not, a member does never need to be
+ // added more than once) should be kept at backend level (GroupController::fetchUsers) but this does
+ // not work currently as our ldap protocol stuff is unable to handle our filter implementation..
+ $members = $this->backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $this->groupName)
+ ->fetchColumn();
+ $filter = empty($members) ? Filter::matchAll() : Filter::not(Filter::where('user_name', $members));
+
+ $users = $this->ds->select()->from('user', array('user_name'))->applyFilter($filter)->fetchColumn();
+ if (! empty($users)) {
+ $this->addElement(
+ 'multiselect',
+ 'user_name',
+ array(
+ 'multiOptions' => array_combine($users, $users),
+ 'label' => $this->translate('Backend Users'),
+ 'description' => $this->translate(
+ 'Select one or more users (fetched from your user backends) to add as group member'
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'required' => empty($users),
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Provide one or more usernames separated by comma to add as group member'
+ )
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Add members for group %s'), $this->groupName));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Insert the members for the group
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $userNames = $this->getValue('user_name') ?: array();
+ if (($users = $this->getValue('users'))) {
+ $userNames = array_merge($userNames, array_map('trim', explode(',', $users)));
+ }
+
+ if (empty($userNames)) {
+ $this->info($this->translate(
+ 'Please provide at least one username, either by choosing one '
+ . 'in the list or by manually typing one in the text box below'
+ ));
+ return false;
+ }
+
+ $single = null;
+ $userName = null;
+ foreach ($userNames as $userName) {
+ try {
+ $this->backend->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $this->groupName,
+ 'user_name' => $userName
+ )
+ );
+ } catch (NotFoundError $e) {
+ throw $e; // Trigger 404, the group name is initially accessed as GET parameter
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $userName,
+ $this->groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(sprintf($this->translate('Group member "%s" added successfully'), $userName));
+ } else {
+ Notification::success($this->translate('Group members added successfully'));
+ }
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserGroup/DbUserGroupBackendForm.php b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
new file mode 100644
index 0000000..daea8de
--- /dev/null
+++ b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
@@ -0,0 +1,79 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Data\ResourceFactory;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing database user group backends
+ */
+class DbUserGroupBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_dbusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getDatabaseResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate('The database connection to use for this backend'),
+ 'multiOptions' => empty($resourceNames) ? array() : array_combine($resourceNames, $resourceNames)
+ )
+ );
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => 'db'
+ )
+ );
+ }
+
+ /**
+ * Return the names of all configured database resources
+ *
+ * @return array
+ */
+ protected function getDatabaseResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (strtolower($config->type) === 'db') {
+ $names[] = $name;
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
new file mode 100644
index 0000000..10c069a
--- /dev/null
+++ b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
@@ -0,0 +1,370 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\UserGroup\LdapUserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Protocol\Ldap\LdapConnection;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for managing LDAP user group backends
+ */
+class LdapUserGroupBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_ldapusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getLdapResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate('The LDAP connection to use for this backend.'),
+ 'multiOptions' => array_combine($resourceNames, $resourceNames)
+ )
+ );
+ $resource = ResourceFactory::create(
+ isset($formData['resource']) && in_array($formData['resource'], $resourceNames)
+ ? $formData['resource']
+ : $resourceNames[0]
+ );
+
+ $userBackendNames = $this->getLdapUserBackendNames($resource);
+ if (! empty($userBackendNames)) {
+ $userBackends = array_combine($userBackendNames, $userBackendNames);
+ $userBackends['none'] = $this->translate('None', 'usergroupbackend.ldap.user_backend');
+ } else {
+ $userBackends = array('none' => $this->translate('None', 'usergroupbackend.ldap.user_backend'));
+ }
+ $this->addElement(
+ 'select',
+ 'user_backend',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Backend'),
+ 'description' => $this->translate('The user backend to link with this user group backend.'),
+ 'multiOptions' => $userBackends
+ )
+ );
+
+ $groupBackend = new LdapUserGroupBackend($resource);
+ if ($formData['type'] === 'ldap') {
+ $defaults = $groupBackend->getOpenLdapDefaults();
+ $groupConfigDisabled = $userConfigDisabled = null; // MUST BE null, do NOT change this to false!
+ } else { // $formData['type'] === 'msldap'
+ $defaults = $groupBackend->getActiveDirectoryDefaults();
+ $groupConfigDisabled = $userConfigDisabled = true;
+ }
+
+ if ($formData['type'] === 'msldap') {
+ $this->addElement(
+ 'checkbox',
+ 'nested_group_search',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for nested group search in Active Directory based on the user'
+ ),
+ 'label' => $this->translate('Nested Group Search')
+ )
+ );
+ } else {
+ // This is required to purge already present options
+ $this->addElement('hidden', 'nested_group_search', array('disabled' => true));
+ }
+
+ $this->createGroupConfigElements($defaults, $groupConfigDisabled);
+ if (count($userBackends) === 1 || (isset($formData['user_backend']) && $formData['user_backend'] === 'none')) {
+ $this->createUserConfigElements($defaults, $userConfigDisabled);
+ } else {
+ $this->createHiddenUserConfigElements();
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => $formData['type']
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the group configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createGroupConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'group_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Object Class'),
+ 'description' => $this->translate('The object class used for storing groups on the LDAP server.'),
+ 'value' => $defaults->group_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP Group Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up groups using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->group_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a group\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->group_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_member_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Member Attribute'),
+ 'description' => $this->translate('The attribute name used for storing a group\'s members.'),
+ 'value' => $defaults->group_member_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP Group Base DN'),
+ 'description' => $this->translate(
+ 'The path where groups can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->base_dn
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the user configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createUserConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $defaults->user_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP User Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->user_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a user\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->user_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP User Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->user_base_dn
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Domain'),
+ 'description' => $this->translate(
+ 'The domain the LDAP server is responsible for.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements for the user configuration as hidden inputs
+ *
+ * This is required to purge already present options when unlinking a group backend with a user backend.
+ */
+ protected function createHiddenUserConfigElements()
+ {
+ $this->addElement('hidden', 'user_class', array('disabled' => true));
+ $this->addElement('hidden', 'user_filter', array('disabled' => true));
+ $this->addElement('hidden', 'user_name_attribute', array('disabled' => true));
+ $this->addElement('hidden', 'user_base_dn', array('disabled' => true));
+ $this->addElement('hidden', 'domain', array('disabled' => true));
+ }
+
+ /**
+ * Return the names of all configured LDAP resources
+ *
+ * @return array
+ */
+ protected function getLdapResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (in_array(strtolower($config->type), array('ldap', 'msldap'))) {
+ $names[] = $name;
+ }
+ }
+
+ if (empty($names)) {
+ Notification::error(
+ $this->translate('No LDAP resources available. Please configure an LDAP resource first.')
+ );
+ $this->getResponse()->redirectAndExit('config/createresource');
+ }
+
+ return $names;
+ }
+
+ /**
+ * Return the names of all configured LDAP user backends
+ *
+ * @param LdapConnection $resource
+ *
+ * @return array
+ */
+ protected function getLdapUserBackendNames(LdapConnection $resource)
+ {
+ $names = array();
+ foreach (UserBackend::getBackendConfigs() as $name => $config) {
+ if (in_array(strtolower($config->backend), array('ldap', 'msldap'))) {
+ $backendResource = ResourceFactory::create($config->resource);
+ if ($backendResource->getHostname() === $resource->getHostname()
+ && $backendResource->getPort() === $resource->getPort()
+ ) {
+ $names[] = $name;
+ }
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php
new file mode 100644
index 0000000..9ee4032
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php
@@ -0,0 +1,314 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Authentication\UserGroup\UserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Web\Form;
+use InvalidArgumentException;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Form for managing user group backends
+ */
+class UserGroupBackendForm extends ConfigForm
+{
+ protected $validatePartial = true;
+
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
+ /**
+ * Known custom backends
+ *
+ * @var array
+ */
+ protected $customBackends;
+
+ /**
+ * Create a user group backend by using the given form's values and return its inspection results
+ *
+ * Returns null for non-inspectable backends.
+ *
+ * @param Form $form
+ *
+ * @return Inspection|null
+ */
+ public static function inspectUserBackend(Form $form)
+ {
+ $backend = UserGroupBackend::create(null, new ConfigObject($form->getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_usergroupbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->customBackends = UserGroupBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ return new DbUserGroupBackendForm();
+ case 'ldap':
+ case 'msldap':
+ return new LdapUserGroupBackendForm();
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user group backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException('A user group backend with the name "%s" does already exist', $backendName);
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user group backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $this->config->setSection($name, $backendConfig->merge($data));
+ return $this;
+ }
+
+ /**
+ * Remove a user group backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array(
+ 'db' => $this->translate('Database'),
+ 'ldap' => 'LDAP',
+ 'msldap' => 'ActiveDirectory'
+ );
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate('The type of this user group backend'),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['type'] = $data['backend'];
+ $data['name'] = $this->backendToLoad;
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * 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 = static::inspectUserBackend($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $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.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupForm.php b/application/forms/Config/UserGroup/UserGroupForm.php
new file mode 100644
index 0000000..b944e97
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupForm.php
@@ -0,0 +1,158 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Web\Notification;
+
+class UserGroupForm extends RepositoryForm
+{
+ /**
+ * Create and add elements to this form to insert or update a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'group_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Group Name')
+ )
+ );
+
+ if ($this->shouldInsert()) {
+ $this->setTitle($this->translate('Add a new group'));
+ $this->setSubmitLabel($this->translate('Add'));
+ } else { // $this->shouldUpdate()
+ $this->setTitle(sprintf($this->translate('Edit group %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+ }
+
+ /**
+ * Update a group
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('group_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('group', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create and add elements to this form to delete a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove group %s?'), $this->getIdentifier()));
+ $this->addDescription($this->translate(
+ 'Note that all users that are currently a member of this group will'
+ . ' have their membership cleared automatically.'
+ ));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-form icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a group
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('group_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('Group added successfully');
+ } else {
+ return $this->translate('Failed to add group');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}
diff --git a/application/forms/ConfigForm.php b/application/forms/ConfigForm.php
new file mode 100644
index 0000000..8b0c5f9
--- /dev/null
+++ b/application/forms/ConfigForm.php
@@ -0,0 +1,192 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Exception;
+use Zend_Form_Decorator_Abstract;
+use Icinga\Application\Config;
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form base-class providing standard functionality for configuration forms
+ */
+class ConfigForm extends Form
+{
+ /**
+ * The configuration to work with
+ *
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * {@inheritdoc}
+ *
+ * Values from subforms are directly added to the returned values array instead of being grouped by the subforms'
+ * names.
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ foreach (array_keys($this->_subForms) as $name) {
+ // Zend returns values from subforms grouped by their names, but we want them flat
+ $values = array_merge($values, $values[$name]);
+ unset($values[$name]);
+ }
+ return $values;
+ }
+
+ /**
+ * Set the configuration to use when populating the form or when saving the user's input
+ *
+ * @param Config $config The configuration to use
+ *
+ * @return $this
+ */
+ public function setIniConfig(Config $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ $sections = array();
+ foreach (static::transformEmptyValuesToNull($this->getValues()) as $sectionAndPropertyName => $value) {
+ list($section, $property) = explode('_', $sectionAndPropertyName, 2);
+ $sections[$section][$property] = $value;
+ }
+
+ foreach ($sections as $section => $config) {
+ if ($this->isEmptyConfig($config)) {
+ $this->config->removeSection($section);
+ } else {
+ $this->config->setSection($section, $config);
+ }
+ }
+
+ if ($this->save()) {
+ Notification::success($this->translate('New configuration has successfully been stored'));
+ } else {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+
+ public function onRequest()
+ {
+ $values = array();
+ foreach ($this->config as $section => $properties) {
+ foreach ($properties as $name => $value) {
+ $values[$section . '_' . $name] = $value;
+ }
+ }
+
+ $this->populate($values);
+ }
+
+ /**
+ * Persist the current configuration to disk
+ *
+ * If an error occurs the user is shown a view describing the issue and displaying the raw INI configuration.
+ *
+ * @return bool Whether the configuration could be persisted
+ */
+ public function save()
+ {
+ try {
+ $this->writeConfig($this->config);
+ } catch (ConfigurationError $e) {
+ $this->addError($e->getMessage());
+
+ return false;
+ } catch (Exception $e) {
+ $this->addDecorator('ViewScript', array(
+ 'viewModule' => 'default',
+ 'viewScript' => 'showConfiguration.phtml',
+ 'errorMessage' => $e->getMessage(),
+ 'configString' => $this->config,
+ 'filePath' => $this->config->getConfigFile(),
+ 'placement' => Zend_Form_Decorator_Abstract::PREPEND
+ ));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Write the configuration to disk
+ *
+ * @param Config $config
+ */
+ protected function writeConfig(Config $config)
+ {
+ $config->saveIni();
+ }
+
+ /**
+ * Get whether the given config is empty or has only empty values
+ *
+ * @param array|Config $config
+ *
+ * @return bool
+ */
+ protected function isEmptyConfig($config)
+ {
+ if ($config instanceof Config) {
+ $config = $config->toArray();
+ }
+
+ foreach ($config as $value) {
+ if ($value !== null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Transform all empty values of the given array to null
+ *
+ * @param array $values
+ *
+ * @return array
+ */
+ public static function transformEmptyValuesToNull(array $values)
+ {
+ array_walk($values, function (&$v) {
+ if ($v === '' || $v === false || $v === array()) {
+ $v = null;
+ }
+ });
+
+ return $values;
+ }
+}
diff --git a/application/forms/ConfirmRemovalForm.php b/application/forms/ConfirmRemovalForm.php
new file mode 100644
index 0000000..39fc661
--- /dev/null
+++ b/application/forms/ConfirmRemovalForm.php
@@ -0,0 +1,38 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Web\Form;
+
+/**
+ * Form for confirming removal of an object
+ */
+class ConfirmRemovalForm extends Form
+{
+ const DEFAULT_CLASSES = 'icinga-controls';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_confirm_removal');
+ $this->getSubmitLabel() ?: $this->setSubmitLabel($this->translate('Confirm Removal'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton();
+
+ if (($submit = $this->getElement('btn_submit')) !== null) {
+ $class = $submit->getAttrib('class');
+ $submit->setAttrib('class', empty($class) ? 'autofocus' : $class . ' autofocus');
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Control/LimiterControlForm.php b/application/forms/Control/LimiterControlForm.php
new file mode 100644
index 0000000..88adf4b
--- /dev/null
+++ b/application/forms/Control/LimiterControlForm.php
@@ -0,0 +1,134 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Control;
+
+use Icinga\Web\Form;
+
+/**
+ * Limiter control form
+ */
+class LimiterControlForm extends Form
+{
+ /**
+ * CSS class for the limiter control
+ *
+ * @var string
+ */
+ const CSS_CLASS_LIMITER = 'limiter-control icinga-controls inline';
+
+ /**
+ * Default limit
+ *
+ * @var int
+ */
+ const DEFAULT_LIMIT = 50;
+
+ /**
+ * Selectable default limits
+ *
+ * @var int[]
+ */
+ public static $limits = array(
+ 10 => '10',
+ 25 => '25',
+ 50 => '50',
+ 100 => '100',
+ 500 => '500'
+ );
+
+ public static $defaultElementDecorators = [
+ ['Label', ['tag' => 'span', 'separator' => '']],
+ ['ViewHelper', ['separator' => '']],
+ ];
+
+ /**
+ * Default limit for this instance
+ *
+ * @var int|null
+ */
+ protected $defaultLimit;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', static::CSS_CLASS_LIMITER);
+ }
+
+ /**
+ * Get the default limit
+ *
+ * @return int
+ */
+ public function getDefaultLimit()
+ {
+ return $this->defaultLimit !== null ? $this->defaultLimit : static::DEFAULT_LIMIT;
+ }
+
+ /**
+ * Set the default limit
+ *
+ * @param int $defaultLimit
+ *
+ * @return $this
+ */
+ public function setDefaultLimit($defaultLimit)
+ {
+ $defaultLimit = (int) $defaultLimit;
+
+ if (! isset(static::$limits[$defaultLimit])) {
+ static::$limits[$defaultLimit] = $defaultLimit;
+ }
+
+ $this->defaultLimit = $defaultLimit;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrl()
+ {
+ return $this->getRequest()->getUrl()
+ ->setParam('limit', $this->getElement('limit')->getValue())
+ ->without('page');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $options = static::$limits;
+ $pageSize = (int) $this->getRequest()->getUrl()->getParam('limit', $this->getDefaultLimit());
+
+ if (! isset($options[$pageSize])) {
+ $options[$pageSize] = $pageSize;
+ }
+
+ $this->addElement(
+ 'select',
+ 'limit',
+ array(
+ 'autosubmit' => true,
+ 'escape' => false,
+ 'label' => '#',
+ 'multiOptions' => $options,
+ 'value' => $pageSize
+ )
+ );
+ }
+
+ /**
+ * Limiter control is always successful
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ return true;
+ }
+}
diff --git a/application/forms/Dashboard/DashletForm.php b/application/forms/Dashboard/DashletForm.php
new file mode 100644
index 0000000..1af65a9
--- /dev/null
+++ b/application/forms/Dashboard/DashletForm.php
@@ -0,0 +1,171 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Dashboard;
+
+use Icinga\Web\Form;
+use Icinga\Web\Form\Validator\InternalUrlValidator;
+use Icinga\Web\Form\Validator\UrlValidator;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Dashboard;
+use Icinga\Web\Widget\Dashboard\Dashlet;
+
+/**
+ * Form to add an url a dashboard pane
+ */
+class DashletForm extends Form
+{
+ /**
+ * @var Dashboard
+ */
+ private $dashboard;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_dashboard_addurl');
+ if (! $this->getSubmitLabel()) {
+ $this->setSubmitLabel($this->translate('Add To Dashboard'));
+ }
+ $this->setAction(Url::fromRequest());
+ }
+
+ /**
+ * Build AddUrl form elements
+ *
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $groupElements = array();
+ $panes = array();
+
+ if ($this->dashboard) {
+ $panes = $this->dashboard->getPaneKeyTitleArray();
+ }
+
+ $sectionNameValidator = ['Callback', true, [
+ 'callback' => function ($value) {
+ if (strpos($value, '[') === false && strpos($value, ']') === false) {
+ return true;
+ }
+ },
+ 'messages' => [
+ 'callbackValue' => $this->translate('Brackets ([, ]) cannot be used here')
+ ]
+ ]];
+
+ $this->addElement(
+ 'hidden',
+ 'org_pane',
+ array(
+ 'required' => false
+ )
+ );
+
+ $this->addElement(
+ 'hidden',
+ 'org_dashlet',
+ array(
+ 'required' => false
+ )
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'url',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters.'
+ ),
+ 'validators' => array(new UrlValidator(), new InternalUrlValidator())
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'dashlet',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Dashlet Title'),
+ 'description' => $this->translate('Enter a title for the dashlet.'),
+ 'validators' => [$sectionNameValidator]
+ )
+ );
+ $this->addElement(
+ 'note',
+ 'note',
+ array(
+ 'decorators' => array(
+ array('HtmlTag', array('tag' => 'hr'))
+ )
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'create_new_pane',
+ array(
+ 'autosubmit' => true,
+ 'required' => false,
+ 'label' => $this->translate('New dashboard'),
+ 'description' => $this->translate('Check this box if you want to add the dashlet to a new dashboard')
+ )
+ );
+ if (empty($panes) || ((isset($formData['create_new_pane']) && $formData['create_new_pane'] != false))) {
+ $this->addElement(
+ 'text',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('New Dashboard Title'),
+ 'description' => $this->translate('Enter a title for the new dashboard'),
+ 'validators' => [$sectionNameValidator]
+ )
+ );
+ } else {
+ $this->addElement(
+ 'select',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Dashboard'),
+ 'multiOptions' => $panes,
+ 'description' => $this->translate('Select a dashboard you want to add the dashlet to')
+ )
+ );
+ }
+ }
+
+ /**
+ * @param \Icinga\Web\Widget\Dashboard $dashboard
+ */
+ public function setDashboard(Dashboard $dashboard)
+ {
+ $this->dashboard = $dashboard;
+ }
+
+ /**
+ * @return \Icinga\Web\Widget\Dashboard
+ */
+ public function getDashboard()
+ {
+ return $this->dashboard;
+ }
+
+ /**
+ * @param Dashlet $dashlet
+ */
+ public function load(Dashlet $dashlet)
+ {
+ $this->populate(array(
+ 'pane' => $dashlet->getPane()->getTitle(),
+ 'org_pane' => $dashlet->getPane()->getName(),
+ 'dashlet' => $dashlet->getTitle(),
+ 'org_dashlet' => $dashlet->getName(),
+ 'url' => $dashlet->getUrl()->getRelativeUrl()
+ ));
+ }
+}
diff --git a/application/forms/LdapDiscoveryForm.php b/application/forms/LdapDiscoveryForm.php
new file mode 100644
index 0000000..5c7fc87
--- /dev/null
+++ b/application/forms/LdapDiscoveryForm.php
@@ -0,0 +1,34 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Web\Form;
+
+class LdapDiscoveryForm extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('form_ldap_discovery');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Search Domain'),
+ 'description' => $this->translate('Search this domain for records of available servers.'),
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/MigrationForm.php b/application/forms/MigrationForm.php
new file mode 100644
index 0000000..c5d517f
--- /dev/null
+++ b/application/forms/MigrationForm.php
@@ -0,0 +1,143 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\MigrationManager;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\CheckboxElement;
+use ipl\Html\FormElement\FieldsetElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\I18n\Translation;
+use ipl\Validator\CallbackValidator;
+use ipl\Web\Common\CsrfCounterMeasure;
+use ipl\Web\Common\FormUid;
+use ipl\Web\Compat\CompatForm;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use PDOException;
+
+class MigrationForm extends CompatForm
+{
+ use CsrfCounterMeasure;
+ use FormUid;
+ use Translation;
+
+ protected $defaultAttributes = [
+ 'class' => ['icinga-form', 'migration-form', 'icinga-controls'],
+ 'name' => 'migration-form'
+ ];
+
+ /** @var bool Whether to allow changing the current database user and password */
+ protected $renderDatabaseUserChange = false;
+
+ public function hasBeenSubmitted(): bool
+ {
+ if (! $this->hasBeenSent()) {
+ return false;
+ }
+
+ $pressedButton = $this->getPressedSubmitElement();
+
+ return $pressedButton && strpos($pressedButton->getName(), 'migrate-') !== false;
+ }
+
+ public function setRenderDatabaseUserChange(bool $value = true): self
+ {
+ $this->renderDatabaseUserChange = $value;
+
+ return $this;
+ }
+
+ public function hasDefaultElementDecorator()
+ {
+ // The base implementation registers a decorator we don't want here
+ return false;
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml($this->createUidElement());
+
+ if ($this->renderDatabaseUserChange) {
+ $mm = MigrationManager::instance();
+ $newDbSetup = new FieldsetElement('database_setup', ['required' => true]);
+ $newDbSetup
+ ->setDefaultElementDecorator(new IcingaFormDecorator())
+ ->addElement('text', 'username', [
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'A user which is able to create and/or alter the database schema.'
+ )
+ ])
+ ->addElement('password', 'password', [
+ 'required' => true,
+ 'autocomplete' => 'new-password',
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password for the database user defined above.'),
+ 'validators' => [
+ new CallbackValidator(function ($_, CallbackValidator $validator) use ($mm, $newDbSetup): bool {
+ /** @var array<string, string> $values */
+ $values = $this->getValue('database_setup');
+ /** @var CheckboxElement $checkBox */
+ $checkBox = $newDbSetup->getElement('grant_privileges');
+ $canIssueGrants = $checkBox->isChecked();
+ $elevationConfig = [
+ 'username' => $values['username'],
+ 'password' => $values['password']
+ ];
+
+ try {
+ if (! $mm->validateDatabasePrivileges($elevationConfig, $canIssueGrants)) {
+ $validator->addMessage(sprintf(
+ $this->translate(
+ 'The provided credentials cannot be used to execute "%s" SQL commands'
+ . ' and/or grant the missing privileges to other users.'
+ ),
+ implode(' ,', $mm->getRequiredDatabasePrivileges())
+ ));
+
+ return false;
+ }
+ } catch (PDOException $e) {
+ $validator->addMessage($e->getMessage());
+
+ return false;
+ }
+
+ return true;
+ })
+ ]
+ ])
+ ->addElement('checkbox', 'grant_privileges', [
+ 'required' => false,
+ 'label' => $this->translate('Grant Missing Privileges'),
+ 'description' => $this->translate(
+ 'Allows to automatically grant the required privileges to the database user specified'
+ . ' in the respective resource config. If you do not want to provide additional credentials'
+ . ' each time, you can enable this and Icinga Web will grant the active database user the'
+ . ' missing privileges.'
+ )
+ ]);
+
+ $this->addHtml(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'change-database-user-description']),
+ new HtmlElement('span', null, Text::create(sprintf(
+ $this->translate(
+ 'It seems that the currently used database user does not have the required privileges to'
+ . ' execute the %s SQL commands. Please provide an alternative user'
+ . ' that has the appropriate credentials to resolve this issue.'
+ ),
+ implode(', ', $mm->getRequiredDatabasePrivileges())
+ )))
+ )
+ );
+
+ $this->addElement($newDbSetup);
+ }
+ }
+}
diff --git a/application/forms/Navigation/DashletForm.php b/application/forms/Navigation/DashletForm.php
new file mode 100644
index 0000000..6575fd7
--- /dev/null
+++ b/application/forms/Navigation/DashletForm.php
@@ -0,0 +1,35 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+class DashletForm extends NavigationItemForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Pane'),
+ 'description' => $this->translate('The name of the dashboard pane in which to display this dashlet')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'url',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'The url to load in the dashlet. For external urls, make sure to prepend'
+ . ' an appropriate protocol identifier (e.g. http://example.tld)'
+ )
+ )
+ );
+ }
+}
diff --git a/application/forms/Navigation/MenuItemForm.php b/application/forms/Navigation/MenuItemForm.php
new file mode 100644
index 0000000..c9fa729
--- /dev/null
+++ b/application/forms/Navigation/MenuItemForm.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+class MenuItemForm extends NavigationItemForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $requiresParentSelection = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ parent::createElements($formData);
+
+ // Remove _self and _next as for menu entries only _main is valid
+ $this->getElement('target')->removeMultiOption('_self');
+ $this->getElement('target')->removeMultiOption('_next');
+
+ $parentElement = $this->getParent()->getElement('parent');
+ if ($parentElement !== null) {
+ $parentElement->setDescription($this->translate(
+ 'The parent menu to assign this menu entry to. Select "None" to make this a main menu entry'
+ ));
+ }
+ }
+}
diff --git a/application/forms/Navigation/NavigationConfigForm.php b/application/forms/Navigation/NavigationConfigForm.php
new file mode 100644
index 0000000..0c4ae32
--- /dev/null
+++ b/application/forms/Navigation/NavigationConfigForm.php
@@ -0,0 +1,853 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Forms\ConfigForm;
+use Icinga\User;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing navigation items
+ */
+class NavigationConfigForm extends ConfigForm
+{
+ /**
+ * The class namespace where to locate navigation type forms
+ *
+ * @var string
+ */
+ const FORM_NS = 'Forms\\Navigation';
+
+ /**
+ * The secondary configuration to write
+ *
+ * This is always the reduced configuration and is only written to
+ * disk once the main configuration has been successfully written.
+ *
+ * @var Config
+ */
+ protected $secondaryConfig;
+
+ /**
+ * The navigation item to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $itemToLoad;
+
+ /**
+ * The user for whom to manage navigation items
+ *
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * The user's navigation configuration
+ *
+ * @var Config
+ */
+ protected $userConfig;
+
+ /**
+ * The shared navigation configuration
+ *
+ * @var Config
+ */
+ protected $shareConfig;
+
+ /**
+ * The available navigation item types
+ *
+ * @var array
+ */
+ protected $itemTypes;
+
+ private $defaultUrl;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_navigation');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * Set the user for whom to manage navigation items
+ *
+ * @param User $user
+ *
+ * @return $this
+ */
+ public function setUser(User $user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * Return the user for whom to manage navigation items
+ *
+ * @return User
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the user's navigation configuration
+ *
+ * @param Config $config
+ *
+ * @return $this
+ */
+ public function setUserConfig(Config $config)
+ {
+ $config->getConfigObject()->setKeyColumn('name');
+ $this->userConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the user's navigation configuration
+ *
+ * @param string $type
+ *
+ * @return Config
+ */
+ public function getUserConfig($type = null)
+ {
+ if ($this->userConfig === null || $type !== null) {
+ if ($type === null) {
+ throw new ProgrammingError('You need to pass a type if no user configuration is set');
+ }
+
+ $this->setUserConfig(Config::navigation($type, $this->getUser()->getUsername()));
+ }
+
+ return $this->userConfig;
+ }
+
+ /**
+ * Set the shared navigation configuration
+ *
+ * @param Config $config
+ *
+ * @return $this
+ */
+ public function setShareConfig(Config $config)
+ {
+ $config->getConfigObject()->setKeyColumn('name');
+ $this->shareConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the shared navigation configuration
+ *
+ * @param string $type
+ *
+ * @return Config
+ */
+ public function getShareConfig($type = null)
+ {
+ if ($this->shareConfig === null) {
+ if ($type === null) {
+ throw new ProgrammingError('You need to pass a type if no share configuration is set');
+ }
+
+ $this->setShareConfig(Config::navigation($type));
+ }
+
+ return $this->shareConfig;
+ }
+
+ /**
+ * Set the available navigation item types
+ *
+ * @param array $itemTypes
+ *
+ * @return $this
+ */
+ public function setItemTypes(array $itemTypes)
+ {
+ $this->itemTypes = $itemTypes;
+ return $this;
+ }
+
+ /**
+ * Return the available navigation item types
+ *
+ * @return array
+ */
+ public function getItemTypes()
+ {
+ return $this->itemTypes ?: array();
+ }
+
+ /**
+ * Return a list of available parent items for the given type of navigation item
+ *
+ * @param string $type
+ * @param string $owner
+ *
+ * @return array
+ */
+ public function listAvailableParents($type, $owner = null)
+ {
+ $children = $this->itemToLoad ? $this->getFlattenedChildren($this->itemToLoad) : array();
+
+ $names = array();
+ foreach ($this->getShareConfig($type) as $sectionName => $sectionConfig) {
+ if ((string) $sectionName !== $this->itemToLoad
+ && $sectionConfig->owner === ($owner ?: $this->getUser()->getUsername())
+ && ! in_array($sectionName, $children, true)
+ ) {
+ $names[] = $sectionName;
+ }
+ }
+
+ foreach ($this->getUserConfig($type) as $sectionName => $sectionConfig) {
+ if ((string) $sectionName !== $this->itemToLoad
+ && ! in_array($sectionName, $children, true)
+ ) {
+ $names[] = $sectionName;
+ }
+ }
+
+ return $names;
+ }
+
+ /**
+ * Recursively return all children of the given navigation item
+ *
+ * @param string $name
+ *
+ * @return array
+ */
+ protected function getFlattenedChildren($name)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ return array();
+ }
+
+ $children = array();
+ foreach ($config->toArray() as $sectionName => $sectionConfig) {
+ if (isset($sectionConfig['parent']) && $sectionConfig['parent'] === $name) {
+ $children[] = $sectionName;
+ $children = array_merge($children, $this->getFlattenedChildren($sectionName));
+ }
+ }
+
+ return $children;
+ }
+
+ /**
+ * Populate the form with the given navigation item's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ */
+ public function load($name)
+ {
+ if ($this->getConfigForItem($name) === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $this->itemToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new navigation item
+ *
+ * The navigation item to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a navigation item name or type
+ * @throws IcingaException In case a navigation item with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ } elseif (! isset($data['type'])) {
+ throw new InvalidArgumentException('Key \'type\' missing');
+ }
+
+ $shared = false;
+ $config = $this->getUserConfig($data['type']);
+ if ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
+ if ($this->getUser()->can('user/share/navigation')) {
+ $data['owner'] = $this->getUser()->getUsername();
+ $config = $this->getShareConfig($data['type']);
+ $shared = true;
+ } else {
+ unset($data['users']);
+ unset($data['groups']);
+ }
+ } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'], $data['type'])) {
+ $data['owner'] = $this->getUser()->getUsername();
+ $config = $this->getShareConfig($data['type']);
+ $shared = true;
+ }
+
+ $itemName = $data['name'];
+ $exists = $config->hasSection($itemName);
+ if (! $exists) {
+ if ($shared) {
+ $exists = $this->getUserConfig($data['type'])->hasSection($itemName);
+ } else {
+ $exists = (bool) $this->getShareConfig($data['type'])
+ ->select()
+ ->where('name', $itemName)
+ ->where('owner', $this->getUser()->getUsername())
+ ->count();
+ }
+ }
+
+ if ($exists) {
+ throw new IcingaException(
+ $this->translate('A navigation item with the name "%s" does already exist'),
+ $itemName
+ );
+ }
+
+ unset($data['name']);
+ $config->setSection($itemName, $data);
+ $this->setIniConfig($config);
+ return $this;
+ }
+
+ /**
+ * Edit a navigation item
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case a navigation item with the same name already exists
+ */
+ public function edit($name, array $data)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ } else {
+ $itemConfig = $config->getSection($name);
+ }
+
+ $shared = false;
+ if ($this->hasBeenShared($name)) {
+ if (isset($data['parent']) && $data['parent']
+ ? ! $this->hasBeenShared($data['parent'])
+ : ((! isset($data['users']) || ! $data['users']) && (! isset($data['groups']) || ! $data['groups']))
+ ) {
+ // It is shared but shouldn't anymore
+ $config = $this->unshare($name, isset($data['parent']) ? $data['parent'] : null);
+ }
+ } elseif ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
+ if ($this->getUser()->can('user/share/navigation')) {
+ // It is not shared yet but should be
+ $this->secondaryConfig = $config;
+ $config = $this->getShareConfig();
+ $data['owner'] = $this->getUser()->getUsername();
+ $shared = true;
+ } else {
+ unset($data['users']);
+ unset($data['groups']);
+ }
+ } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'])) {
+ // Its parent is shared so should it itself
+ $this->secondaryConfig = $config;
+ $config = $this->getShareConfig();
+ $data['owner'] = $this->getUser()->getUsername();
+ $shared = true;
+ }
+
+ $oldName = null;
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $oldName = $name;
+ $name = $data['name'];
+
+ $exists = $config->hasSection($name);
+ if (! $exists) {
+ $ownerName = $itemConfig->owner ?: $this->getUser()->getUsername();
+ if ($shared || $this->hasBeenShared($oldName)) {
+ if ($ownerName === $this->getUser()->getUsername()) {
+ $exists = $this->getUserConfig()->hasSection($name);
+ } else {
+ $exists = Config::navigation($itemConfig->type, $ownerName)->hasSection($name);
+ }
+ } else {
+ $exists = (bool) $this->getShareConfig()
+ ->select()
+ ->where('name', $name)
+ ->where('owner', $ownerName)
+ ->count();
+ }
+ }
+
+ if ($exists) {
+ throw new IcingaException(
+ $this->translate('A navigation item with the name "%s" does already exist'),
+ $name
+ );
+ }
+ }
+
+ unset($data['name']);
+ }
+
+ $itemConfig->merge($data);
+
+ if ($shared) {
+ // Share all descendant children
+ foreach ($this->getFlattenedChildren($oldName ?: $name) as $child) {
+ $childConfig = $this->secondaryConfig->getSection($child);
+ $this->secondaryConfig->removeSection($child);
+ $childConfig->owner = $this->getUser()->getUsername();
+ $config->setSection($child, $childConfig);
+ }
+ }
+
+ if ($oldName) {
+ // Update the parent name on all direct children
+ foreach ($config as $sectionConfig) {
+ if ($sectionConfig->parent === $oldName) {
+ $sectionConfig->parent = $name;
+ }
+ }
+
+ $config->removeSection($oldName);
+ }
+
+ if ($this->secondaryConfig !== null) {
+ $this->secondaryConfig->removeSection($oldName ?: $name);
+ }
+
+ $config->setSection($name, $itemConfig);
+ $this->setIniConfig($config);
+ return $this;
+ }
+
+ /**
+ * Remove a navigation item
+ *
+ * @param string $name
+ *
+ * @return ConfigObject The navigation item's config
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case the navigation item has still children
+ */
+ public function delete($name)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $children = $this->getFlattenedChildren($name);
+ if (! empty($children)) {
+ throw new IcingaException(
+ $this->translate(
+ 'Unable to delete navigation item "%s". There'
+ . ' are other items dependent from it: %s'
+ ),
+ $name,
+ join(', ', $children)
+ );
+ }
+
+ $section = $config->getSection($name);
+ $config->removeSection($name);
+ $this->setIniConfig($config);
+ return $section;
+ }
+
+ /**
+ * Unshare the given navigation item
+ *
+ * @param string $name
+ * @param string $parent
+ *
+ * @return Config The new config of the given navigation item
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case the navigation item has a parent assigned to it
+ */
+ public function unshare($name, $parent = null)
+ {
+ $config = $this->getShareConfig();
+ if (! $config->hasSection($name)) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $itemConfig = $config->getSection($name);
+ if ($parent === null) {
+ $parent = $itemConfig->parent;
+ }
+
+ if ($parent && $this->hasBeenShared($parent)) {
+ throw new IcingaException(
+ $this->translate(
+ 'Unable to unshare navigation item "%s". It is dependent from item "%s".'
+ . ' Dependent items can only be unshared by unsharing their parent'
+ ),
+ $name,
+ $parent
+ );
+ }
+
+ $children = $this->getFlattenedChildren($name);
+ $config->removeSection($name);
+ $this->secondaryConfig = $config;
+
+ if (! $itemConfig->owner || $itemConfig->owner === $this->getUser()->getUsername()) {
+ $config = $this->getUserConfig();
+ } else {
+ $config = Config::navigation($itemConfig->type, $itemConfig->owner);
+ }
+
+ foreach ($children as $child) {
+ $childConfig = $this->secondaryConfig->getSection($child);
+ unset($childConfig->owner);
+ $this->secondaryConfig->removeSection($child);
+ $config->setSection($child, $childConfig);
+ }
+
+ unset($itemConfig->owner);
+ unset($itemConfig->users);
+ unset($itemConfig->groups);
+
+ $config->setSection($name, $itemConfig);
+ $this->setIniConfig($config);
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $shared = false;
+ $itemTypes = $this->getItemTypes();
+ $itemType = isset($formData['type']) ? $formData['type'] : key($itemTypes);
+ if ($itemType === null) {
+ throw new ProgrammingError(
+ 'This should actually not happen. Create a bug report at https://github.com/icinga/icingaweb2'
+ . ' or remove this assertion if you know what you\'re doing'
+ );
+ }
+
+ $itemForm = $this->getItemForm($itemType);
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Name'),
+ 'description' => $this->translate(
+ 'The name of this navigation item that is used to differentiate it from others'
+ )
+ )
+ );
+
+ if ((! $itemForm->requiresParentSelection() || ! isset($formData['parent']) || ! $formData['parent'])
+ && $this->getUser()->can('user/share/navigation')
+ ) {
+ $checked = isset($formData['shared']) ? null : (isset($formData['users']) || isset($formData['groups']));
+
+ $this->addElement(
+ 'checkbox',
+ 'shared',
+ array(
+ 'autosubmit' => true,
+ 'ignore' => true,
+ 'value' => $checked,
+ 'label' => $this->translate('Shared'),
+ 'description' => $this->translate('Tick this box to share this item with others')
+ )
+ );
+
+ if ($checked || (isset($formData['shared']) && $formData['shared'])) {
+ $shared = true;
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Comma separated list of usernames to share this item with'
+ )
+ )
+ );
+ $this->addElement(
+ 'textarea',
+ 'groups',
+ array(
+ 'label' => $this->translate('Groups'),
+ 'description' => $this->translate(
+ 'Comma separated list of group names to share this item with'
+ )
+ )
+ );
+ }
+ }
+
+ if (empty($itemTypes) || count($itemTypes) === 1) {
+ $this->addElement(
+ 'hidden',
+ 'type',
+ array(
+ 'value' => $itemType
+ )
+ );
+ } else {
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Type'),
+ 'description' => $this->translate('The type of this navigation item'),
+ 'multiOptions' => $itemTypes
+ )
+ );
+ }
+
+ if (! $shared && $itemForm->requiresParentSelection()) {
+ if ($this->itemToLoad && $this->hasBeenShared($this->itemToLoad)) {
+ $itemConfig = $this->getShareConfig()->getSection($this->itemToLoad);
+ $availableParents = $this->listAvailableParents($itemType, $itemConfig->owner);
+ } else {
+ $availableParents = $this->listAvailableParents($itemType);
+ }
+
+ $this->addElement(
+ 'select',
+ 'parent',
+ array(
+ 'allowEmpty' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Parent'),
+ 'description' => $this->translate(
+ 'The parent item to assign this navigation item to. '
+ . 'Select "None" to make this a main navigation item'
+ ),
+ 'multiOptions' => ['' => $this->translate('None', 'No parent for a navigation item')]
+ + (empty($availableParents) ? [] : array_combine($availableParents, $availableParents))
+ )
+ );
+ } else {
+ $this->addElement('hidden', 'parent', ['disabled' => true]);
+ }
+
+ $this->addSubForm($itemForm, 'item_form');
+ $itemForm->create($formData); // May require a parent which gets set by addSubForm()
+ }
+
+ /**
+ * DO NOT USE! This will be removed soon, very soon...
+ */
+ public function setDefaultUrl($url)
+ {
+ $this->defaultUrl = $url;
+ }
+
+ /**
+ * Populate the configuration of the navigation item to load
+ */
+ public function onRequest()
+ {
+ if ($this->itemToLoad) {
+ $data = $this->getConfigForItem($this->itemToLoad)->getSection($this->itemToLoad)->toArray();
+ $data['name'] = $this->itemToLoad;
+ $this->populate($data);
+ } elseif ($this->defaultUrl !== null) {
+ $this->populate(array('url' => $this->defaultUrl));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ $valid = true;
+ if (isset($formData['users']) && $formData['users']) {
+ $parsedUserRestrictions = array();
+ foreach (Auth::getInstance()->getRestrictions('application/share/users') as $userRestriction) {
+ $parsedUserRestrictions[] = array_map('trim', explode(',', $userRestriction));
+ }
+
+ if (! empty($parsedUserRestrictions)) {
+ $desiredUsers = array_map('trim', explode(',', $formData['users']));
+ array_unshift($parsedUserRestrictions, $desiredUsers);
+ $forbiddenUsers = call_user_func_array('array_diff', $parsedUserRestrictions);
+ if (! empty($forbiddenUsers)) {
+ $valid = false;
+ $this->getElement('users')->addError(
+ sprintf(
+ $this->translate(
+ 'You are not permitted to share this navigation item with the following users: %s'
+ ),
+ implode(', ', $forbiddenUsers)
+ )
+ );
+ }
+ }
+ }
+
+ if (isset($formData['groups']) && $formData['groups']) {
+ $parsedGroupRestrictions = array();
+ foreach (Auth::getInstance()->getRestrictions('application/share/groups') as $groupRestriction) {
+ $parsedGroupRestrictions[] = array_map('trim', explode(',', $groupRestriction));
+ }
+
+ if (! empty($parsedGroupRestrictions)) {
+ $desiredGroups = array_map('trim', explode(',', $formData['groups']));
+ array_unshift($parsedGroupRestrictions, $desiredGroups);
+ $forbiddenGroups = call_user_func_array('array_diff', $parsedGroupRestrictions);
+ if (! empty($forbiddenGroups)) {
+ $valid = false;
+ $this->getElement('groups')->addError(
+ sprintf(
+ $this->translate(
+ 'You are not permitted to share this navigation item with the following groups: %s'
+ ),
+ implode(', ', $forbiddenGroups)
+ )
+ );
+ }
+ }
+ }
+
+ return $valid;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function writeConfig(Config $config)
+ {
+ parent::writeConfig($config);
+
+ if ($this->secondaryConfig !== null) {
+ $this->config = $this->secondaryConfig; // Causes the config being displayed to the user in case of an error
+ parent::writeConfig($this->secondaryConfig);
+ }
+ }
+
+ /**
+ * Return the navigation configuration the given item is a part of
+ *
+ * @param string $name
+ *
+ * @return Config|null In case the item is not part of any configuration
+ */
+ protected function getConfigForItem($name)
+ {
+ if ($this->getUserConfig()->hasSection($name)) {
+ return $this->getUserConfig();
+ } elseif ($this->getShareConfig()->hasSection($name)) {
+ if ($this->getShareConfig()->get($name, 'owner') === $this->getUser()->getUsername()
+ || $this->getUser()->can('user/share/navigation')
+ ) {
+ return $this->getShareConfig();
+ }
+ }
+ }
+
+ /**
+ * Return whether the given navigation item has been shared
+ *
+ * @param string $name
+ * @param string $type
+ *
+ * @return bool
+ */
+ protected function hasBeenShared($name, $type = null)
+ {
+ return $this->getShareConfig($type) === $this->getConfigForItem($name);
+ }
+
+ /**
+ * Return the form for the given type of navigation item
+ *
+ * @param string $type
+ *
+ * @return Form
+ */
+ protected function getItemForm($type)
+ {
+ $className = StringHelper::cname($type, '-') . 'Form';
+
+ $form = null;
+ $classPath = null;
+ foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
+ $classPath = 'Icinga\\Module\\'
+ . ucfirst($module->getName())
+ . '\\'
+ . static::FORM_NS
+ . '\\'
+ . $className;
+ if (class_exists($classPath)) {
+ $form = new $classPath();
+ break;
+ }
+ }
+
+ if ($form === null) {
+ $classPath = 'Icinga\\' . static::FORM_NS . '\\' . $className;
+ if (class_exists($classPath)) {
+ $form = new $classPath();
+ }
+ }
+
+ if ($form === null) {
+ Logger::debug(
+ 'Failed to find custom navigation item form %s for item %s. Using form NavigationItemForm now',
+ $className,
+ $type
+ );
+
+ $form = new NavigationItemForm();
+ } elseif (! $form instanceof NavigationItemForm) {
+ throw new ProgrammingError('Class %s must inherit from NavigationItemForm', $classPath);
+ }
+
+ return $form;
+ }
+}
diff --git a/application/forms/Navigation/NavigationItemForm.php b/application/forms/Navigation/NavigationItemForm.php
new file mode 100644
index 0000000..6cf15e7
--- /dev/null
+++ b/application/forms/Navigation/NavigationItemForm.php
@@ -0,0 +1,114 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class NavigationItemForm extends Form
+{
+ /**
+ * Whether to create a select input to choose a parent for a navigation item of a particular type
+ *
+ * @var bool
+ */
+ protected $requiresParentSelection = false;
+
+ /**
+ * Return whether to create a select input to choose a parent for a navigation item of a particular type
+ *
+ * @return bool
+ */
+ public function requiresParentSelection()
+ {
+ return $this->requiresParentSelection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'select',
+ 'target',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Target'),
+ 'description' => $this->translate('The target where to open this navigation item\'s url'),
+ 'multiOptions' => array(
+ '_blank' => $this->translate('New Window'),
+ '_next' => $this->translate('New Column'),
+ '_main' => $this->translate('Single Column'),
+ '_self' => $this->translate('Current Column')
+ )
+ )
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'url',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'The url of this navigation item. Leave blank if only the name should be displayed.'
+ . ' For urls with username and password and for all external urls,'
+ . ' make sure to prepend an appropriate protocol identifier (e.g. http://example.tld)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($url) {
+ // Matches if the given url contains obviously
+ // a username but not any protocol identifier
+ return !preg_match('#^((?=[^/@]).)+@.*$#', $url);
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate(
+ 'Missing protocol identifier'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'icon',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Icon'),
+ 'description' => $this->translate(
+ 'The icon of this navigation item. Leave blank if you do not want a icon being displayed'
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ // The regex here specifically matches the port-macro as it's the only one preventing Url::fromPath() from
+ // successfully parsing the given url. Any other macro such as for the scheme or host simply gets identified
+ // as path which is just fine in this case.
+ if (isset($values['url']) && $values['url'] && !preg_match('~://.+:\d*?(\$.+\$)~', $values['url'])) {
+ $url = Url::fromPath($values['url']);
+ if ($url->getBasePath() === $this->getRequest()->getBasePath()) {
+ $values['url'] = $url->getRelativeUrl();
+ } else {
+ $values['url'] = $url->getAbsoluteUrl();
+ }
+ }
+
+ return $values;
+ }
+}
diff --git a/application/forms/PreferenceForm.php b/application/forms/PreferenceForm.php
new file mode 100644
index 0000000..3e431db
--- /dev/null
+++ b/application/forms/PreferenceForm.php
@@ -0,0 +1,485 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use DateTimeZone;
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\User\Preferences;
+use Icinga\User\Preferences\PreferencesStore;
+use Icinga\Util\TimezoneDetect;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Session;
+use Icinga\Web\StyleSheet;
+use ipl\Html\HtmlElement;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\Locale;
+use ipl\I18n\StaticTranslator;
+
+/**
+ * Form class to adjust user preferences
+ */
+class PreferenceForm extends Form
+{
+ /**
+ * The preferences to work with
+ *
+ * @var Preferences
+ */
+ protected $preferences;
+
+ /**
+ * The preference store to use
+ *
+ * @var PreferencesStore
+ */
+ protected $store;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_preferences');
+ $this->setSubmitLabel($this->translate('Save to the Preferences'));
+ }
+
+ /**
+ * Set preferences to work with
+ *
+ * @param Preferences $preferences The preferences to work with
+ *
+ * @return $this
+ */
+ public function setPreferences(Preferences $preferences)
+ {
+ $this->preferences = $preferences;
+ return $this;
+ }
+
+ /**
+ * Set the preference store to use
+ *
+ * @param PreferencesStore $store The preference store to use
+ *
+ * @return $this
+ */
+ public function setStore(PreferencesStore $store)
+ {
+ $this->store = $store;
+ return $this;
+ }
+
+ /**
+ * Persist preferences
+ *
+ * @return $this
+ */
+ public function save()
+ {
+ $this->store->save($this->preferences);
+ return $this;
+ }
+
+ /**
+ * Adjust preferences and persist them
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $currentPreferences = $this->Auth()->getUser()->getPreferences();
+ $oldTheme = $currentPreferences->getValue('icingaweb', 'theme');
+ $oldMode = $currentPreferences->getValue('icingaweb', 'theme_mode');
+ $oldLocale = $currentPreferences->getValue('icingaweb', 'language');
+ $defaultTheme = Config::app()->get('themes', 'default', StyleSheet::DEFAULT_THEME);
+
+ $this->preferences = new Preferences($this->store ? $this->store->load() : array());
+ $webPreferences = $this->preferences->get('icingaweb');
+ foreach ($this->getValues() as $key => $value) {
+ if ($value === ''
+ || $value === null
+ || $value === 'autodetect'
+ || ($key === 'theme' && $value === $defaultTheme)
+ ) {
+ if (isset($webPreferences[$key])) {
+ unset($webPreferences[$key]);
+ }
+ } else {
+ $webPreferences[$key] = $value;
+ }
+ }
+ $this->preferences->icingaweb = $webPreferences;
+ Session::getSession()->user->setPreferences($this->preferences);
+
+ if ((($theme = $this->getElement('theme')) !== null
+ && ($theme = $theme->getValue()) !== $oldTheme
+ && ($theme !== $defaultTheme || $oldTheme !== null))
+ || (($mode = $this->getElement('theme_mode')) !== null
+ && ($mode->getValue()) !== $oldMode)
+ ) {
+ $this->getResponse()->setReloadCss(true);
+ }
+
+ if (($locale = $this->getElement('language')) !== null
+ && $locale->getValue() !== 'autodetect'
+ && $locale->getValue() !== $oldLocale
+ ) {
+ $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes');
+ }
+
+ try {
+ if ($this->store && $this->getElement('btn_submit')->isChecked()) {
+ $this->save();
+ Notification::success($this->translate('Preferences successfully saved'));
+ } else {
+ Notification::success($this->translate('Preferences successfully saved for the current session'));
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::error($e->getMessage());
+ }
+ }
+
+ /**
+ * Populate preferences
+ *
+ * @see Form::onRequest()
+ */
+ public function onRequest()
+ {
+ $auth = Auth::getInstance();
+ $values = $auth->getUser()->getPreferences()->get('icingaweb');
+
+ if (! isset($values['language'])) {
+ $values['language'] = 'autodetect';
+ }
+
+ if (! isset($values['timezone'])) {
+ $values['timezone'] = 'autodetect';
+ }
+
+ if (! isset($values['auto_refresh'])) {
+ $values['auto_refresh'] = '1';
+ }
+
+ $this->populate($values);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ if (setlocale(LC_ALL, 0) === 'C') {
+ $this->warning(
+ $this->translate(
+ 'Your language setting is not applied because your platform is missing the corresponding locale.'
+ . ' Make sure to install the correct language pack and restart your web server afterwards.'
+ ),
+ false
+ );
+ }
+
+ $themeFile = StyleSheet::getThemeFile(Config::app()->get('themes', 'default'));
+ if (! (bool) Config::app()->get('themes', 'disabled', false)) {
+ $themes = Icinga::app()->getThemes();
+ if (count($themes) > 1) {
+ $defaultTheme = Config::app()->get('themes', 'default', StyleSheet::DEFAULT_THEME);
+ if (isset($themes[$defaultTheme])) {
+ $themes[$defaultTheme] .= ' (' . $this->translate('default') . ')';
+ }
+ $this->addElement(
+ 'select',
+ 'theme',
+ array(
+ 'label' => $this->translate('Theme', 'Form element label'),
+ 'multiOptions' => $themes,
+ 'autosubmit' => true,
+ 'value' => $this->preferences->getValue(
+ 'icingaweb',
+ 'theme',
+ $defaultTheme
+ )
+ )
+ );
+ }
+ }
+
+ if (isset($formData['theme'])) {
+ $themeFile = StyleSheet::getThemeFile($formData['theme']);
+ }
+
+ $disabled = [];
+ if ($themeFile !== null) {
+ $file = @file_get_contents($themeFile);
+ if ($file && strpos($file, StyleSheet::LIGHT_MODE_IDENTIFIER) === false) {
+ $disabled = ['', 'light', 'system'];
+ }
+ }
+
+ $this->addElement(
+ 'radio',
+ 'theme_mode',
+ [
+ 'class' => 'theme-mode-input',
+ 'label' => $this->translate('Theme Mode'),
+ 'multiOptions' => [
+ '' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-dark.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('Dark')),
+ 'light' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-light.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('Light')),
+ 'system' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-system.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('System'))
+ ],
+ 'disable' => $disabled,
+ 'escape' => false,
+ 'decorators' => array_merge(
+ array_slice(self::$defaultElementDecorators, 0, -1),
+ [['HtmlTag', ['tag' => 'div', 'class' => 'control-group theme-mode']]]
+ )
+ ]
+ );
+
+ /** @var GettextTranslator $translator */
+ $translator = StaticTranslator::$instance;
+
+ $languages = array();
+ $availableLocales = $translator->listLocales();
+
+ $locale = $this->getLocale($availableLocales);
+ if ($locale !== null) {
+ $languages['autodetect'] = sprintf($this->translate('Browser (%s)', 'preferences.form'), $locale);
+ }
+
+ $availableLocales[] = $translator->getDefaultLocale();
+ foreach ($availableLocales as $language) {
+ $languages[$language] = $language;
+ }
+
+ $tzList = array();
+ $tzList['autodetect'] = sprintf(
+ $this->translate('Browser (%s)', 'preferences.form'),
+ $this->getDefaultTimezone()
+ );
+ foreach (DateTimeZone::listIdentifiers() as $tz) {
+ $tzList[$tz] = $tz;
+ }
+
+ $this->addElement(
+ 'select',
+ 'language',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Your Current Language'),
+ 'description' => $this->translate('Use the following language to display texts and messages'),
+ 'multiOptions' => $languages,
+ 'value' => substr(setlocale(LC_ALL, 0), 0, 5)
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'timezone',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Your Current Timezone'),
+ 'description' => $this->translate('Use the following timezone for dates and times'),
+ 'multiOptions' => $tzList,
+ 'value' => $this->getDefaultTimezone()
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'show_application_state_messages',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Show application state messages'),
+ 'description' => $this->translate('Whether to show application state messages.'),
+ 'multiOptions' => [
+ 'system' => (bool) Config::app()->get('global', 'show_application_state_messages', true)
+ ? $this->translate('System (Yes)')
+ : $this->translate('System (No)'),
+ 1 => $this->translate('Yes'),
+ 0 => $this->translate('No')],
+ 'value' => 'system'
+ )
+ );
+
+ if (Auth::getInstance()->hasPermission('user/application/stacktraces')) {
+ $this->addElement(
+ 'checkbox',
+ 'show_stacktraces',
+ array(
+ 'value' => $this->getDefaultShowStacktraces(),
+ 'label' => $this->translate('Show Stacktraces'),
+ 'description' => $this->translate('Set whether to show an exception\'s stacktrace.')
+ )
+ );
+ }
+
+ $this->addElement(
+ 'checkbox',
+ 'show_benchmark',
+ array(
+ 'label' => $this->translate('Use benchmark')
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'auto_refresh',
+ array(
+ 'required' => false,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Enable auto refresh'),
+ 'description' => $this->translate(
+ 'This option allows you to enable or to disable the global page content auto refresh'
+ ),
+ 'value' => 1
+ )
+ );
+
+ if (isset($formData['auto_refresh']) && $formData['auto_refresh']) {
+ $this->addElement(
+ 'select',
+ 'auto_refresh_speed',
+ [
+ 'required' => false,
+ 'label' => $this->translate('Auto refresh speed'),
+ 'description' => $this->translate(
+ 'This option allows you to speed up or to slow down the global page content auto refresh'
+ ),
+ 'multiOptions' => [
+ '0.5' => $this->translate('Fast', 'refresh_speed'),
+ '' => $this->translate('Default', 'refresh_speed'),
+ '2' => $this->translate('Moderate', 'refresh_speed'),
+ '4' => $this->translate('Slow', 'refresh_speed')
+ ],
+ 'value' => ''
+ ]
+ );
+ }
+
+ $this->addElement(
+ 'number',
+ 'default_page_size',
+ array(
+ 'label' => $this->translate('Default page size'),
+ 'description' => $this->translate('Default number of items per page for list views'),
+ 'placeholder' => 25,
+ 'min' => 25,
+ 'step' => 1
+ )
+ );
+
+ if ($this->store) {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Save to the Preferences'),
+ 'decorators' => array('ViewHelper'),
+ 'class' => 'btn-primary'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'submit',
+ 'btn_submit_session',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Save for the current Session'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'preferences-progress');
+ $this->addElement(
+ 'note',
+ 'preferences-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'preferences-progress'))
+ )
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('btn_submit', 'btn_submit_session', 'preferences-progress'),
+ 'submit_buttons',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+ }
+
+ public function addSubmitButton()
+ {
+ return $this;
+ }
+
+ public function isSubmitted()
+ {
+ if (parent::isSubmitted()) {
+ return true;
+ }
+
+ return $this->getElement('btn_submit_session')->isChecked();
+ }
+
+ /**
+ * Return the current default timezone
+ *
+ * @return string
+ */
+ protected function getDefaultTimezone()
+ {
+ $detect = new TimezoneDetect();
+ if ($detect->success()) {
+ return $detect->getTimezoneName();
+ } else {
+ return @date_default_timezone_get();
+ }
+ }
+
+ /**
+ * Return the preferred locale based on the given HTTP header and the available translations
+ *
+ * @return string|null
+ */
+ protected function getLocale($availableLocales)
+ {
+ return isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])
+ ? (new Locale())->getPreferred($_SERVER['HTTP_ACCEPT_LANGUAGE'], $availableLocales)
+ : null;
+ }
+
+ /**
+ * Return the default global setting for show_stacktraces
+ *
+ * @return bool
+ */
+ protected function getDefaultShowStacktraces()
+ {
+ return Config::app()->get('global', 'show_stacktraces', true);
+ }
+}
diff --git a/application/forms/RepositoryForm.php b/application/forms/RepositoryForm.php
new file mode 100644
index 0000000..8e4665d
--- /dev/null
+++ b/application/forms/RepositoryForm.php
@@ -0,0 +1,453 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\NotFoundError;
+use Icinga\Repository\Repository;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form base-class providing standard functionality for extensible, updatable and reducible repositories
+ */
+abstract class RepositoryForm extends Form
+{
+ /**
+ * Insert mode
+ */
+ const MODE_INSERT = 0;
+
+ /**
+ * Update mode
+ */
+ const MODE_UPDATE = 1;
+
+ /**
+ * Delete mode
+ */
+ const MODE_DELETE = 2;
+
+ /**
+ * The repository being worked with
+ *
+ * @var Repository
+ */
+ protected $repository;
+
+ /**
+ * The target being worked with
+ *
+ * @var mixed
+ */
+ protected $baseTable;
+
+ /**
+ * How to interact with the repository
+ *
+ * @var int
+ */
+ protected $mode;
+
+ /**
+ * The name of the entry being handled when in mode update or delete
+ *
+ * @var string
+ */
+ protected $identifier;
+
+ /**
+ * The data of the entry to pre-populate the form with when in mode insert or update
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * Set the repository to work with
+ *
+ * @param Repository $repository
+ *
+ * @return $this
+ */
+ public function setRepository(Repository $repository)
+ {
+ $this->repository = $repository;
+ return $this;
+ }
+
+ /**
+ * Return the target being worked with
+ *
+ * @return mixed
+ */
+ protected function getBaseTable()
+ {
+ if ($this->baseTable === null) {
+ return $this->repository->getBaseTable();
+ }
+
+ return $this->baseTable;
+ }
+
+ /**
+ * Return the name of the entry to handle
+ *
+ * @return string
+ */
+ protected function getIdentifier()
+ {
+ return $this->identifier;
+ }
+
+ /**
+ * Return the current data of the entry being handled
+ *
+ * @return array
+ */
+ protected function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Return whether an entry should be inserted
+ *
+ * @return bool
+ */
+ public function shouldInsert()
+ {
+ return $this->mode === self::MODE_INSERT;
+ }
+
+ /**
+ * Return whether an entry should be udpated
+ *
+ * @return bool
+ */
+ public function shouldUpdate()
+ {
+ return $this->mode === self::MODE_UPDATE;
+ }
+
+ /**
+ * Return whether an entry should be deleted
+ *
+ * @return bool
+ */
+ public function shouldDelete()
+ {
+ return $this->mode === self::MODE_DELETE;
+ }
+
+ /**
+ * Add a new entry
+ *
+ * @param array $data The defaults to use, if any
+ *
+ * @return $this
+ */
+ public function add(array $data = null)
+ {
+ $this->mode = static::MODE_INSERT;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Edit an entry
+ *
+ * @param string $name The entry's name
+ * @param array $data The entry's current data
+ *
+ * @return $this
+ */
+ public function edit($name, array $data = null)
+ {
+ $this->mode = static::MODE_UPDATE;
+ $this->identifier = $name;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Remove an entry
+ *
+ * @param string $name The entry's name
+ *
+ * @return $this
+ */
+ public function remove($name)
+ {
+ $this->mode = static::MODE_DELETE;
+ $this->identifier = $name;
+ return $this;
+ }
+
+ /**
+ * Fetch and return the entry to pre-populate the form with when in mode update
+ *
+ * @return false|object
+ */
+ protected function fetchEntry()
+ {
+ return $this->repository
+ ->select()
+ ->from($this->getBaseTable())
+ ->applyFilter($this->createFilter())
+ ->fetchRow();
+ }
+
+ /**
+ * Return whether the entry supposed to be removed exists
+ *
+ * @return bool
+ */
+ protected function entryExists()
+ {
+ $count = $this->repository
+ ->select()
+ ->from($this->getBaseTable())
+ ->addFilter($this->createFilter())
+ ->count();
+ return $count > 0;
+ }
+
+ /**
+ * Insert the new entry
+ */
+ protected function insertEntry()
+ {
+ $this->repository->insert($this->getBaseTable(), $this->getValues());
+ }
+
+ /**
+ * Update the entry
+ */
+ protected function updateEntry()
+ {
+ $this->repository->update($this->getBaseTable(), $this->getValues(), $this->createFilter());
+ }
+
+ /**
+ * Delete the entry
+ */
+ protected function deleteEntry()
+ {
+ $this->repository->delete($this->getBaseTable(), $this->createFilter());
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ if ($this->shouldInsert()) {
+ $this->createInsertElements($formData);
+ } elseif ($this->shouldUpdate()) {
+ $this->createUpdateElements($formData);
+ } elseif ($this->shouldDelete()) {
+ $this->createDeleteElements($formData);
+ }
+ }
+
+ /**
+ * Prepare the form for the requested mode
+ */
+ public function onRequest()
+ {
+ if ($this->shouldInsert()) {
+ $this->onInsertRequest();
+ } elseif ($this->shouldUpdate()) {
+ $this->onUpdateRequest();
+ } elseif ($this->shouldDelete()) {
+ $this->onDeleteRequest();
+ }
+ }
+
+ /**
+ * Prepare the form for mode insert
+ *
+ * Populates the form with the data passed to add().
+ */
+ protected function onInsertRequest()
+ {
+ $data = $this->getData();
+ if (! empty($data)) {
+ $this->setDefaults($data);
+ }
+ }
+
+ /**
+ * Prepare the form for mode update
+ *
+ * Populates the form with either the data passed to edit() or tries to fetch it from the repository.
+ *
+ * @throws NotFoundError In case the entry to update cannot be found
+ */
+ protected function onUpdateRequest()
+ {
+ $data = $this->getData();
+ if ($data === null) {
+ $row = $this->fetchEntry();
+ if ($row === false) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+
+ $data = get_object_vars($row);
+ }
+
+ $this->setDefaults($data);
+ }
+
+ /**
+ * Prepare the form for mode delete
+ *
+ * Verifies that the repository contains the entry to delete.
+ *
+ * @throws NotFoundError In case the entry to delete cannot be found
+ */
+ protected function onDeleteRequest()
+ {
+ if (! $this->entryExists()) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Apply the requested mode on the repository
+ *
+ * @return ?bool
+ */
+ public function onSuccess()
+ {
+ if ($this->shouldInsert()) {
+ return $this->onInsertSuccess();
+ } elseif ($this->shouldUpdate()) {
+ return $this->onUpdateSuccess();
+ } elseif ($this->shouldDelete()) {
+ return $this->onDeleteSuccess();
+ }
+ }
+
+ /**
+ * Apply mode insert on the repository
+ *
+ * @return bool
+ */
+ protected function onInsertSuccess()
+ {
+ try {
+ $this->insertEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getInsertMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getInsertMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode update on the repository
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ try {
+ $this->updateEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getUpdateMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getUpdateMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode delete on the repository
+ *
+ * @return bool
+ */
+ protected function onDeleteSuccess()
+ {
+ try {
+ $this->deleteEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getDeleteMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getDeleteMessage(true));
+ return true;
+ }
+
+ /**
+ * Create and add elements to this form to insert an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createInsertElements(array $formData);
+
+ /**
+ * Create and add elements to this form to update an entry
+ *
+ * Calls createInsertElements() by default. Overwrite this to add different elements when in mode update.
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+ }
+
+ /**
+ * Create and add elements to this form to delete an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createDeleteElements(array $formData);
+
+ /**
+ * Create and return a filter to use when selecting, updating or deleting an entry
+ *
+ * @return Filter
+ */
+ abstract protected function createFilter();
+
+ /**
+ * Return a notification message to use when inserting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getInsertMessage($success);
+
+ /**
+ * Return a notification message to use when updating an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getUpdateMessage($success);
+
+ /**
+ * Return a notification message to use when deleting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getDeleteMessage($success);
+}
diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php
new file mode 100644
index 0000000..58387f7
--- /dev/null
+++ b/application/forms/Security/RoleForm.php
@@ -0,0 +1,632 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Security;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Manager;
+use Icinga\Authentication\AdmissionLoader;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Notification;
+use ipl\Web\Widget\Icon;
+
+/**
+ * Form for managing roles
+ */
+class RoleForm extends RepositoryForm
+{
+ /**
+ * The name to use instead of `*`
+ */
+ const WILDCARD_NAME = 'allAndEverything';
+
+ /**
+ * The prefix used to deny a permission
+ */
+ const DENY_PREFIX = 'no-';
+
+ /**
+ * Provided permissions by currently installed modules
+ *
+ * @var array
+ */
+ protected $providedPermissions;
+
+ /**
+ * Provided restrictions by currently installed modules
+ *
+ * @var array
+ */
+ protected $providedRestrictions;
+
+ public function init()
+ {
+ $this->setAttrib('class', self::DEFAULT_CLASSES . ' role-form');
+
+ list($this->providedPermissions, $this->providedRestrictions) = static::collectProvidedPrivileges();
+ }
+
+ protected function createFilter()
+ {
+ return Filter::where('name', $this->getIdentifier());
+ }
+
+ public function filterName($value, $allowBrackets = false)
+ {
+ return parent::filterName($value, $allowBrackets) . '_element';
+ }
+
+ public function createInsertElements(array $formData = array())
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ [
+ 'required' => true,
+ 'label' => $this->translate('Role Name'),
+ 'description' => $this->translate('The name of the role')
+ ]
+ );
+ $this->addElement(
+ 'select',
+ 'parent',
+ [
+ 'label' => $this->translate('Inherit From'),
+ 'description' => $this->translate('Choose a role from which to inherit privileges'),
+ 'value' => '',
+ 'multiOptions' => array_merge(
+ ['' => $this->translate('None', 'parent role')],
+ $this->collectRoles()
+ )
+ ]
+ );
+ $this->addElement(
+ 'textarea',
+ 'users',
+ [
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate('Comma-separated list of users that are assigned to the role')
+ ]
+ );
+ $this->addElement(
+ 'textarea',
+ 'groups',
+ [
+ 'label' => $this->translate('Groups'),
+ 'description' => $this->translate('Comma-separated list of groups that are assigned to the role')
+ ]
+ );
+ $this->addElement(
+ 'checkbox',
+ self::WILDCARD_NAME,
+ [
+ 'autosubmit' => true,
+ 'label' => $this->translate('Administrative Access'),
+ 'description' => $this->translate('Everything is allowed')
+ ]
+ );
+ $this->addElement(
+ 'checkbox',
+ 'unrestricted',
+ [
+ 'autosubmit' => true,
+ 'uncheckedValue' => null,
+ 'label' => $this->translate('Unrestricted Access'),
+ 'description' => $this->translate('Access to any data is completely unrestricted')
+ ]
+ );
+
+ $hasAdminPerm = isset($formData[self::WILDCARD_NAME]) && $formData[self::WILDCARD_NAME];
+ $isUnrestricted = isset($formData['unrestricted']) && $formData['unrestricted'];
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $this->sortPermissions($permissionList);
+
+ $anythingGranted = false;
+ $anythingRefused = false;
+ $anythingRestricted = false;
+
+ $elements = [$moduleName . '_header'];
+ // The actual element is added last
+
+ $elements[] = 'permission_header';
+ $this->addElement('note', 'permission_header', [
+ 'decorators' => [['Callback', ['callback' => function () {
+ return '<h4>' . $this->translate('Permissions') . '</h4>'
+ . $this->getView()->icon('ok', $this->translate(
+ 'Grant access by toggling a switch below'
+ ))
+ . $this->getView()->icon('cancel', $this->translate(
+ 'Deny access by toggling a switch below'
+ ));
+ }]], ['HtmlTag', ['tag' => 'div']]]
+ ]);
+
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+
+ if (isset($formData[$elementName]) && $formData[$elementName]) {
+ $anythingGranted = true;
+ }
+
+ if ($hasFullPerm || $hasAdminPerm) {
+ $elementName .= '_fake';
+ }
+
+ $denyCheckbox = null;
+ if (! isset($spec['isFullPerm'])
+ && substr($name, 0, strlen(self::DENY_PREFIX)) !== self::DENY_PREFIX
+ ) {
+ $denyCheckbox = $this->createElement('checkbox', $this->filterName(self::DENY_PREFIX . $name), [
+ 'decorators' => ['ViewHelper']
+ ]);
+ $this->addElement($denyCheckbox);
+ $this->removeFromIteration($denyCheckbox->getName());
+
+ if (isset($formData[$denyCheckbox->getName()]) && $formData[$denyCheckbox->getName()]) {
+ $anythingRefused = true;
+ }
+ }
+
+ $elements[] = $elementName;
+ $this->addElement(
+ 'checkbox',
+ $elementName,
+ [
+ 'ignore' => $hasFullPerm || $hasAdminPerm,
+ 'autosubmit' => isset($spec['isFullPerm']),
+ 'disabled' => $hasFullPerm || $hasAdminPerm ?: null,
+ 'value' => $hasFullPerm || $hasAdminPerm,
+ 'label' => isset($spec['label'])
+ ? $spec['label']
+ : join('', iterator_to_array(call_user_func(function ($segments) {
+ foreach ($segments as $segment) {
+ if ($segment[0] === '/') {
+ // Adds a zero-width char after each slash to help browsers break onto newlines
+ yield '/&#8203;';
+ yield '<span class="no-wrap">' . substr($segment, 1) . '</span>';
+ } else {
+ yield '<em>' . $segment . '</em>';
+ }
+ }
+ }, preg_split(
+ '~(/[^/]+)~',
+ $name,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ )))),
+ 'description' => isset($spec['description']) ? $spec['description'] : $name,
+ 'decorators' => array_merge(
+ array_slice(self::$defaultElementDecorators, 0, 3),
+ [['Callback', ['callback' => function () use ($denyCheckbox) {
+ return $denyCheckbox ? $denyCheckbox->render() : '';
+ }]]],
+ array_slice(self::$defaultElementDecorators, 3)
+ )
+ ]
+ )
+ ->getElement($elementName)
+ ->getDecorator('Label')
+ ->setOption('escape', false);
+
+ if ($hasFullPerm || $hasAdminPerm) {
+ // Add a hidden element to preserve the configured permission value
+ $this->addElement('hidden', $this->filterName($name));
+ }
+
+ if (isset($spec['isFullPerm'])) {
+ $filteredName = $this->filterName($name);
+ $hasFullPerm = isset($formData[$filteredName]) && $formData[$filteredName];
+ }
+ }
+
+ if (isset($this->providedRestrictions[$moduleName])) {
+ $elements[] = 'restriction_header';
+ $this->addElement('note', 'restriction_header', [
+ 'value' => '<h4>' . $this->translate('Restrictions') . '</h4>',
+ 'decorators' => ['ViewHelper']
+ ]);
+
+ foreach ($this->providedRestrictions[$moduleName] as $name => $spec) {
+ $elementName = $this->filterName($name);
+
+ if (isset($formData[$elementName]) && $formData[$elementName]) {
+ $anythingRestricted = true;
+ }
+
+ $elements[] = $elementName;
+ $this->addElement(
+ 'text',
+ $elementName,
+ [
+ 'label' => isset($spec['label'])
+ ? $spec['label']
+ : join('', iterator_to_array(call_user_func(function ($segments) {
+ foreach ($segments as $segment) {
+ if ($segment[0] === '/') {
+ // Add zero-width char after each slash to help browsers break onto newlines
+ yield '/&#8203;';
+ yield '<span class="no-wrap">' . substr($segment, 1) . '</span>';
+ } else {
+ yield '<em>' . $segment . '</em>';
+ }
+ }
+ }, preg_split(
+ '~(/[^/]+)~',
+ $name,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ )))),
+ 'description' => $spec['description'],
+ 'class' => $isUnrestricted ? 'unrestricted-role' : '',
+ 'readonly' => $isUnrestricted ?: null
+ ]
+ )
+ ->getElement($elementName)
+ ->getDecorator('Label')
+ ->setOption('escape', false);
+ }
+ }
+
+ $this->addElement(
+ 'note',
+ $moduleName . '_header',
+ [
+ 'decorators' => ['ViewHelper'],
+ 'value' => '<summary class="collapsible-control">'
+ . '<span>' . ($moduleName !== 'application'
+ ? sprintf('%s <em>%s</em>', $moduleName, $this->translate('Module'))
+ : 'Icinga Web 2'
+ ) . '</span>'
+ . '<span class="privilege-preview">'
+ . ($hasAdminPerm || $anythingGranted ? new Icon('check-circle', ['class' => 'granted']) : '')
+ . ($anythingRefused ? new Icon('times-circle', ['class' => 'refused']) : '')
+ . (! $isUnrestricted && $anythingRestricted
+ ? new Icon('filter', ['class' => 'restricted'])
+ : ''
+ )
+ . '</span>'
+ . new Icon('angles-down', ['class' => 'collapse-icon'])
+ . new Icon('angles-left', ['class' => 'expand-icon'])
+ . '</summary>'
+ ]
+ );
+
+ $this->addDisplayGroup($elements, $moduleName . '_elements', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', [
+ 'tag' => 'details',
+ 'class' => 'collapsible'
+ ]],
+ ['Fieldset']
+ ]
+ ]);
+ }
+ }
+
+ protected function createDeleteElements(array $formData)
+ {
+ }
+
+ public function fetchEntry()
+ {
+ $role = parent::fetchEntry();
+ if ($role === false) {
+ return false;
+ }
+
+ $values = [
+ 'parent' => $role->parent,
+ 'name' => $role->name,
+ 'users' => $role->users,
+ 'groups' => $role->groups,
+ 'unrestricted' => $role->unrestricted,
+ self::WILDCARD_NAME => $role->permissions && preg_match('~(?>^|,)\*(?>$|,)~', $role->permissions)
+ ];
+
+ if (! empty($role->permissions) || ! empty($role->refusals)) {
+ $permissions = StringHelper::trimSplit($role->permissions);
+ $refusals = StringHelper::trimSplit($role->refusals);
+
+ list($permissions, $newRefusals) = AdmissionLoader::migrateLegacyPermissions($permissions);
+ if (! empty($newRefusals)) {
+ array_push($refusals, ...$newRefusals);
+ }
+
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ if (in_array($name, $permissions, true)) {
+ $values[$this->filterName($name)] = 1;
+
+ if (isset($spec['isFullPerm'])) {
+ $hasFullPerm = true;
+ }
+ }
+
+ if (in_array($name, $refusals, true)) {
+ $values[$this->filterName(self::DENY_PREFIX . $name)] = 1;
+ }
+ }
+
+ if ($hasFullPerm) {
+ unset($values[$this->filterName(Manager::MODULE_PERMISSION_NS . $moduleName)]);
+ }
+ }
+ }
+
+ foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
+ foreach ($restrictionList as $name => $spec) {
+ if (isset($role->$name)) {
+ $values[$this->filterName($name)] = $role->$name;
+ }
+ }
+ }
+
+ return (object) $values;
+ }
+
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+
+ foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
+ foreach ($restrictionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+ if (isset($values[$elementName])) {
+ $values[$name] = $values[$elementName];
+ unset($values[$elementName]);
+ }
+ }
+ }
+
+ $permissions = [];
+ if (isset($values[self::WILDCARD_NAME]) && $values[self::WILDCARD_NAME]) {
+ $permissions[] = '*';
+ }
+
+ $refusals = [];
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+ if (isset($values[$elementName]) && $values[$elementName]) {
+ $permissions[] = $name;
+
+ if (isset($spec['isFullPerm'])) {
+ $hasFullPerm = true;
+ }
+ }
+
+ $denyName = $this->filterName(self::DENY_PREFIX . $name);
+ if (isset($values[$denyName]) && $values[$denyName]) {
+ $refusals[] = $name;
+ }
+
+ unset($values[$elementName], $values[$denyName]);
+ }
+
+ $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
+ if ($hasFullPerm && ! in_array($modulePermission, $permissions, true)) {
+ $permissions[] = $modulePermission;
+ }
+ }
+
+ unset($values[self::WILDCARD_NAME]);
+ $values['refusals'] = join(',', $refusals);
+ $values['permissions'] = join(',', $permissions);
+ return ConfigForm::transformEmptyValuesToNull($values);
+ }
+
+ protected function getInsertMessage($success)
+ {
+ return $success ? $this->translate('Role created') : $this->translate('Role creation failed');
+ }
+
+ protected function getUpdateMessage($success)
+ {
+ return $success ? $this->translate('Role updated') : $this->translate('Role update failed');
+ }
+
+ protected function getDeleteMessage($success)
+ {
+ return $success ? $this->translate('Role removed') : $this->translate('Role removal failed');
+ }
+
+ protected function sortPermissions(&$permissions)
+ {
+ return uksort($permissions, function ($a, $b) use ($permissions) {
+ if (isset($permissions[$a]['isUsagePerm'])) {
+ return isset($permissions[$b]['isFullPerm']) ? 1 : -1;
+ } elseif (isset($permissions[$b]['isUsagePerm'])) {
+ return isset($permissions[$a]['isFullPerm']) ? -1 : 1;
+ }
+
+ $aParts = explode('/', $a);
+ $bParts = explode('/', $b);
+
+ do {
+ $a = array_shift($aParts);
+ $b = array_shift($bParts);
+ } while ($a === $b);
+
+ return strnatcmp($a ?? '', $b ?? '');
+ });
+ }
+
+ protected function collectRoles()
+ {
+ // Function to get all connected children. Used to avoid reference loops
+ $getChildren = function ($name, $children = []) use (&$getChildren) {
+ foreach ($this->repository->select()->where('parent', $name) as $child) {
+ if (isset($children[$child->name])) {
+ // Don't follow already established loops here,
+ // the user should be able to solve such in the UI
+ continue;
+ }
+
+ $children[$child->name] = true;
+ $children = $getChildren($child->name, $children);
+ }
+
+ return $children;
+ };
+
+ $children = $this->getIdentifier() !== null ? $getChildren($this->getIdentifier()) : [];
+
+ $names = [];
+ foreach ($this->repository->select() as $role) {
+ if ($role->name !== $this->getIdentifier() && ! isset($children[$role->name])) {
+ $names[] = $role->name;
+ }
+ }
+
+ return array_combine($names, $names);
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if ($this->getIdentifier() && ($newName = $this->getValue('name')) !== $this->getIdentifier()) {
+ $this->repository->update(
+ $this->getBaseTable(),
+ ['parent' => $newName],
+ Filter::where('parent', $this->getIdentifier())
+ );
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+
+ /**
+ * Collect permissions and restrictions provided by Icinga Web 2 and modules
+ *
+ * @return array[$permissions, $restrictions]
+ */
+ public static function collectProvidedPrivileges()
+ {
+ $providedPermissions['application'] = [
+ 'application/announcements' => [
+ 'description' => t('Allow to manage announcements')
+ ],
+ 'application/log' => [
+ 'description' => t('Allow to view the application log')
+ ],
+ 'config/*' => [
+ 'description' => t('Allow full config access')
+ ],
+ 'config/general' => [
+ 'description' => t('Allow to adjust the general configuration')
+ ],
+ 'config/modules' => [
+ 'description' => t('Allow to enable/disable and configure modules')
+ ],
+ 'config/resources' => [
+ 'description' => t('Allow to manage resources')
+ ],
+ 'config/navigation' => [
+ 'description' => t('Allow to view and adjust shared navigation items')
+ ],
+ 'config/access-control/*' => [
+ 'description' => t('Allow to fully manage access-control')
+ ],
+ 'config/access-control/users' => [
+ 'description' => t('Allow to manage user accounts')
+ ],
+ 'config/access-control/groups' => [
+ 'description' => t('Allow to manage user groups')
+ ],
+ 'config/access-control/roles' => [
+ 'description' => t('Allow to manage roles')
+ ],
+ 'user/*' => [
+ 'description' => t('Allow all account related functionalities')
+ ],
+ 'user/password-change' => [
+ 'description' => t('Allow password changes in the account preferences')
+ ],
+ 'user/application/stacktraces' => [
+ 'description' => t('Allow to adjust in the preferences whether to show stacktraces')
+ ],
+ 'user/share/navigation' => [
+ 'description' => t('Allow to share navigation items')
+ ],
+ 'application/sessions' => [
+ 'description' => t('Allow to manage user sessions')
+ ],
+ 'application/migrations' => [
+ 'description' => t('Allow to apply pending application migrations')
+ ]
+ ];
+
+ $providedRestrictions['application'] = [
+ 'application/share/users' => [
+ 'description' => t('Restrict which users this role can share items and information with')
+ ],
+ 'application/share/groups' => [
+ 'description' => t('Restrict which groups this role can share items and information with')
+ ]
+ ];
+
+ $mm = Icinga::app()->getModuleManager();
+ foreach ($mm->listInstalledModules() as $moduleName) {
+ $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
+ $providedPermissions[$moduleName][$modulePermission] = [
+ 'isUsagePerm' => true,
+ 'label' => t('General Module Access'),
+ 'description' => sprintf(t('Allow access to module %s'), $moduleName)
+ ];
+
+ $module = $mm->getModule($moduleName, false);
+ $permissions = $module->getProvidedPermissions();
+
+ $providedPermissions[$moduleName][$moduleName . '/*'] = [
+ 'isFullPerm' => true,
+ 'label' => t('Full Module Access')
+ ];
+
+ foreach ($permissions as $permission) {
+ /** @var object $permission */
+ $providedPermissions[$moduleName][$permission->name] = [
+ 'description' => $permission->description
+ ];
+ }
+
+ foreach ($module->getProvidedRestrictions() as $restriction) {
+ $providedRestrictions[$moduleName][$restriction->name] = [
+ 'description' => $restriction->description
+ ];
+ }
+ }
+
+ return [$providedPermissions, $providedRestrictions];
+ }
+}