summaryrefslogtreecommitdiffstats
path: root/application/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'application/controllers')
-rw-r--r--application/controllers/AboutController.php27
-rw-r--r--application/controllers/AccountController.php83
-rw-r--r--application/controllers/AnnouncementsController.php123
-rw-r--r--application/controllers/ApplicationStateController.php95
-rw-r--r--application/controllers/AuthenticationController.php127
-rw-r--r--application/controllers/ConfigController.php518
-rw-r--r--application/controllers/DashboardController.php346
-rw-r--r--application/controllers/ErrorController.php176
-rw-r--r--application/controllers/GroupController.php418
-rw-r--r--application/controllers/HealthController.php65
-rw-r--r--application/controllers/IframeController.php20
-rw-r--r--application/controllers/IndexController.php36
-rw-r--r--application/controllers/LayoutController.php28
-rw-r--r--application/controllers/ListController.php59
-rw-r--r--application/controllers/ManageUserDevicesController.php84
-rw-r--r--application/controllers/MigrationsController.php249
-rw-r--r--application/controllers/MyDevicesController.php74
-rw-r--r--application/controllers/NavigationController.php447
-rw-r--r--application/controllers/RoleController.php392
-rw-r--r--application/controllers/SearchController.php28
-rw-r--r--application/controllers/StaticController.php78
-rw-r--r--application/controllers/UserController.php374
-rw-r--r--application/controllers/UsergroupbackendController.php133
23 files changed, 3980 insertions, 0 deletions
diff --git a/application/controllers/AboutController.php b/application/controllers/AboutController.php
new file mode 100644
index 0000000..59e3c20
--- /dev/null
+++ b/application/controllers/AboutController.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Version;
+use Icinga\Web\Controller;
+
+class AboutController extends Controller
+{
+ public function indexAction()
+ {
+ $this->view->version = Version::get();
+ $this->view->libraries = Icinga::app()->getLibraries();
+ $this->view->modules = Icinga::app()->getModuleManager()->getLoadedModules();
+ $this->view->title = $this->translate('About');
+ $this->view->tabs = $this->getTabs()->add(
+ 'about',
+ array(
+ 'label' => $this->translate('About'),
+ 'title' => $this->translate('About Icinga Web 2'),
+ 'url' => 'about'
+ )
+ )->activate('about');
+ }
+}
diff --git a/application/controllers/AccountController.php b/application/controllers/AccountController.php
new file mode 100644
index 0000000..f172cfe
--- /dev/null
+++ b/application/controllers/AccountController.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Forms\Account\ChangePasswordForm;
+use Icinga\Forms\PreferenceForm;
+use Icinga\User\Preferences\PreferencesStore;
+use Icinga\Web\Controller;
+
+/**
+ * My Account
+ */
+class AccountController extends Controller
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->getTabs()
+ ->add('account', array(
+ 'title' => $this->translate('Update your account'),
+ 'label' => $this->translate('My Account'),
+ 'url' => 'account'
+ ))
+ ->add('navigation', array(
+ 'title' => $this->translate('List and configure your own navigation items'),
+ 'label' => $this->translate('Navigation'),
+ 'url' => 'navigation'
+ ))
+ ->add(
+ 'devices',
+ array(
+ 'title' => $this->translate('List of devices you are logged in'),
+ 'label' => $this->translate('My Devices'),
+ 'url' => 'my-devices'
+ )
+ );
+ }
+
+ /**
+ * My account
+ */
+ public function indexAction()
+ {
+ $config = Config::app()->getSection('global');
+ $user = $this->Auth()->getUser();
+ if ($user->getAdditional('backend_type') === 'db') {
+ if ($user->can('user/password-change')) {
+ try {
+ $userBackend = UserBackend::create($user->getAdditional('backend_name'));
+ } catch (ConfigurationError $e) {
+ $userBackend = null;
+ }
+ if ($userBackend !== null) {
+ $changePasswordForm = new ChangePasswordForm();
+ $changePasswordForm
+ ->setBackend($userBackend)
+ ->handleRequest();
+ $this->view->changePasswordForm = $changePasswordForm;
+ }
+ }
+ }
+
+ $form = new PreferenceForm();
+ $form->setPreferences($user->getPreferences());
+ if (isset($config->config_resource)) {
+ $form->setStore(PreferencesStore::create(new ConfigObject(array(
+ 'resource' => $config->config_resource
+ )), $user));
+ }
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('My Account');
+ $this->getTabs()->activate('account');
+ }
+}
diff --git a/application/controllers/AnnouncementsController.php b/application/controllers/AnnouncementsController.php
new file mode 100644
index 0000000..ee7fd4c
--- /dev/null
+++ b/application/controllers/AnnouncementsController.php
@@ -0,0 +1,123 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Announcement\AcknowledgeAnnouncementForm;
+use Icinga\Forms\Announcement\AnnouncementForm;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Controller;
+use Icinga\Web\Url;
+
+class AnnouncementsController extends Controller
+{
+ public function init()
+ {
+ $this->view->title = $this->translate('Announcements');
+
+ parent::init();
+ }
+
+ /**
+ * List all announcements
+ */
+ public function indexAction()
+ {
+ $this->getTabs()->add(
+ 'announcements',
+ array(
+ 'active' => true,
+ 'label' => $this->translate('Announcements'),
+ 'title' => $this->translate('List All Announcements'),
+ 'url' => Url::fromPath('announcements')
+ )
+ );
+
+ $announcements = (new AnnouncementIniRepository())
+ ->select([
+ 'id',
+ 'author',
+ 'message',
+ 'start',
+ 'end'
+ ]);
+
+ $sortAndFilterColumns = [
+ 'author' => $this->translate('Author'),
+ 'message' => $this->translate('Message'),
+ 'start' => $this->translate('Start'),
+ 'end' => $this->translate('End')
+ ];
+
+ $this->setupSortControl($sortAndFilterColumns, $announcements, ['start' => 'desc']);
+ $this->setupFilterControl($announcements, $sortAndFilterColumns, ['message']);
+
+ $this->view->announcements = $announcements->fetchAll();
+ }
+
+ /**
+ * Create an announcement
+ */
+ public function newAction()
+ {
+ $this->assertPermission('application/announcements');
+
+ $form = $this->prepareForm()->add();
+ $form->handleRequest();
+ $this->renderForm($form, $this->translate('New Announcement'));
+ }
+
+ /**
+ * Update an announcement
+ */
+ public function updateAction()
+ {
+ $this->assertPermission('application/announcements');
+
+ $form = $this->prepareForm()->edit($this->params->getRequired('id'));
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound($this->translate('Announcement not found'));
+ }
+ $this->renderForm($form, $this->translate('Update Announcement'));
+ }
+
+ /**
+ * Remove an announcement
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('application/announcements');
+
+ $form = $this->prepareForm()->remove($this->params->getRequired('id'));
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound($this->translate('Announcement not found'));
+ }
+ $this->renderForm($form, $this->translate('Remove Announcement'));
+ }
+
+ public function acknowledgeAction()
+ {
+ $this->assertHttpMethod('POST');
+ $this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true);
+ $form = new AcknowledgeAnnouncementForm();
+ $form->handleRequest();
+ }
+
+ /**
+ * Assert permission admin and return a prepared RepositoryForm
+ *
+ * @return AnnouncementForm
+ */
+ protected function prepareForm()
+ {
+ $form = new AnnouncementForm();
+ return $form
+ ->setRepository(new AnnouncementIniRepository())
+ ->setRedirectUrl(Url::fromPath('announcements'));
+ }
+}
diff --git a/application/controllers/ApplicationStateController.php b/application/controllers/ApplicationStateController.php
new file mode 100644
index 0000000..b828ca2
--- /dev/null
+++ b/application/controllers/ApplicationStateController.php
@@ -0,0 +1,95 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Forms\AcknowledgeApplicationStateMessageForm;
+use Icinga\Web\Announcement\AnnouncementCookie;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Controller;
+use Icinga\Web\RememberMe;
+use Icinga\Web\Session;
+use Icinga\Web\Widget;
+
+/**
+ * @TODO(el): https://dev.icinga.com/issues/10646
+ */
+class ApplicationStateController extends Controller
+{
+ protected $requiresAuthentication = false;
+
+ protected $autorefreshInterval = 60;
+
+ public function init()
+ {
+ $this->_helper->layout->disableLayout();
+ $this->_helper->viewRenderer->setNoRender(true);
+ }
+
+ public function indexAction()
+ {
+ if ($this->Auth()->isAuthenticated()) {
+ if (isset($_COOKIE['icingaweb2-session'])) {
+ $last = (int) $_COOKIE['icingaweb2-session'];
+ } else {
+ $last = 0;
+ }
+ $now = time();
+ if ($last + 600 < $now) {
+ Session::getSession()->write();
+ $params = session_get_cookie_params();
+ setcookie(
+ 'icingaweb2-session',
+ $now,
+ 0,
+ $params['path'],
+ $params['domain'],
+ $params['secure'],
+ $params['httponly']
+ );
+ $_COOKIE['icingaweb2-session'] = $now;
+ }
+ $announcementCookie = new AnnouncementCookie();
+ $announcementRepo = new AnnouncementIniRepository();
+ if ($announcementCookie->getEtag() !== $announcementRepo->getEtag()) {
+ $announcementCookie
+ ->setEtag($announcementRepo->getEtag())
+ ->setNextActive($announcementRepo->findNextActive());
+ $this->getResponse()->setCookie($announcementCookie);
+ $this->getResponse()->setHeader('X-Icinga-Announcements', 'refresh', true);
+ } else {
+ $nextActive = $announcementCookie->getNextActive();
+ if ($nextActive && $nextActive <= $now) {
+ $announcementCookie->setNextActive($announcementRepo->findNextActive());
+ $this->getResponse()->setCookie($announcementCookie);
+ $this->getResponse()->setHeader('X-Icinga-Announcements', 'refresh', true);
+ }
+ }
+ }
+
+ RememberMe::removeExpired();
+ }
+
+ public function summaryAction()
+ {
+ if ($this->Auth()->isAuthenticated()) {
+ $this->getResponse()->setBody((string) Widget::create('ApplicationStateMessages'));
+ }
+ }
+
+ public function acknowledgeMessageAction()
+ {
+ if (! $this->Auth()->isAuthenticated()) {
+ $this->getResponse()
+ ->setHttpResponseCode(401)
+ ->sendHeaders();
+ exit;
+ }
+
+ $this->assertHttpMethod('POST');
+
+ $this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true);
+
+ (new AcknowledgeApplicationStateMessageForm())->handleRequest();
+ }
+}
diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php
new file mode 100644
index 0000000..752f845
--- /dev/null
+++ b/application/controllers/AuthenticationController.php
@@ -0,0 +1,127 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\AuthenticationHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Common\Database;
+use Icinga\Exception\AuthenticationException;
+use Icinga\Forms\Authentication\LoginForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Helper\CookieHelper;
+use Icinga\Web\RememberMe;
+use Icinga\Web\Url;
+use RuntimeException;
+
+/**
+ * Application wide controller for authentication
+ */
+class AuthenticationController extends Controller
+{
+ use Database;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $innerLayout = 'inline';
+
+ /**
+ * Log into the application
+ */
+ public function loginAction()
+ {
+ $icinga = Icinga::app();
+ if (($requiresSetup = $icinga->requiresSetup()) && $icinga->setupTokenExists()) {
+ $this->redirectNow(Url::fromPath('setup'));
+ }
+ $form = new LoginForm();
+
+ if (RememberMe::hasCookie() && $this->hasDb()) {
+ $authenticated = false;
+ try {
+ $rememberMeOld = RememberMe::fromCookie();
+ $authenticated = $rememberMeOld->authenticate();
+ if ($authenticated) {
+ $rememberMe = $rememberMeOld->renew();
+ $this->getResponse()->setCookie($rememberMe->getCookie());
+ $rememberMe->persist($rememberMeOld->getAesCrypt()->getIV());
+ }
+ } catch (RuntimeException $e) {
+ Logger::error("Can't authenticate user via remember me cookie: %s", $e->getMessage());
+ } catch (AuthenticationException $e) {
+ Logger::error($e);
+ }
+
+ if (! $authenticated) {
+ $this->getResponse()->setCookie(RememberMe::forget());
+ }
+ }
+
+ if ($this->Auth()->isAuthenticated()) {
+ // Call provided AuthenticationHook(s) when login action is called
+ // but icinga web user is already authenticated
+ AuthenticationHook::triggerLogin($this->Auth()->getUser());
+
+ $redirect = $this->params->get('redirect');
+ if ($redirect) {
+ $redirectUrl = Url::fromPath($redirect, [], $this->getRequest());
+ if ($redirectUrl->isExternal()) {
+ $this->httpBadRequest('nope');
+ }
+ } else {
+ $redirectUrl = $form->getRedirectUrl();
+ }
+
+ $this->redirectNow($redirectUrl);
+ }
+ if (! $requiresSetup) {
+ $cookies = new CookieHelper($this->getRequest());
+ if (! $cookies->isSupported()) {
+ $this
+ ->getResponse()
+ ->setBody("Cookies must be enabled to run this application.\n")
+ ->setHttpResponseCode(403)
+ ->sendResponse();
+ exit;
+ }
+ $form->handleRequest();
+ }
+ $this->view->form = $form;
+ $this->view->defaultTitle = $this->translate('Icinga Web 2 Login');
+ $this->view->requiresSetup = $requiresSetup;
+ }
+
+ /**
+ * Log out the current user
+ */
+ public function logoutAction()
+ {
+ $auth = $this->Auth();
+ if (! $auth->isAuthenticated()) {
+ $this->redirectToLogin();
+ }
+ // Get info whether the user is externally authenticated before removing authorization which destroys the
+ // session and the user object
+ $isExternalUser = $auth->getUser()->isExternalUser();
+ // Call provided AuthenticationHook(s) when logout action is called
+ AuthenticationHook::triggerLogout($auth->getUser());
+ $auth->removeAuthorization();
+ if ($isExternalUser) {
+ $this->view->layout()->setLayout('external-logout');
+ $this->getResponse()->setHttpResponseCode(401);
+ } else {
+ if (RememberMe::hasCookie() && $this->hasDb()) {
+ $this->getResponse()->setCookie(RememberMe::forget());
+ }
+
+ $this->redirectToLogin();
+ }
+ }
+}
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
new file mode 100644
index 0000000..671e1a7
--- /dev/null
+++ b/application/controllers/ConfigController.php
@@ -0,0 +1,518 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Version;
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ActionForm;
+use Icinga\Forms\Config\GeneralConfigForm;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Forms\Config\UserBackendConfigForm;
+use Icinga\Forms\Config\UserBackendReorderForm;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Controller;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget;
+
+/**
+ * Application and module configuration
+ */
+class ConfigController extends Controller
+{
+ /**
+ * Create and return the tabs to display when showing application configuration
+ */
+ public function createApplicationTabs()
+ {
+ $tabs = $this->getTabs();
+ if ($this->hasPermission('config/general')) {
+ $tabs->add('general', array(
+ 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
+ 'label' => $this->translate('General'),
+ 'url' => 'config/general',
+ 'baseTarget' => '_main'
+ ));
+ }
+ if ($this->hasPermission('config/resources')) {
+ $tabs->add('resource', array(
+ 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'),
+ 'label' => $this->translate('Resources'),
+ 'url' => 'config/resource',
+ 'baseTarget' => '_main'
+ ));
+ }
+ if ($this->hasPermission('config/access-control/users')
+ || $this->hasPermission('config/access-control/groups')
+ ) {
+ $tabs->add('authentication', array(
+ 'title' => $this->translate('Configure the user and group backends'),
+ 'label' => $this->translate('Access Control Backends'),
+ 'url' => 'config/userbackend',
+ 'baseTarget' => '_main'
+ ));
+ }
+
+ return $tabs;
+ }
+
+ public function devtoolsAction()
+ {
+ $this->view->tabs = null;
+ }
+
+ /**
+ * Redirect to the general configuration
+ */
+ public function indexAction()
+ {
+ if ($this->hasPermission('config/general')) {
+ $this->redirectNow('config/general');
+ } elseif ($this->hasPermission('config/resources')) {
+ $this->redirectNow('config/resource');
+ } elseif ($this->hasPermission('config/access-control/*')) {
+ $this->redirectNow('config/userbackend');
+ } else {
+ throw new SecurityException('No permission to configure Icinga Web 2');
+ }
+ }
+
+ /**
+ * General configuration
+ *
+ * @throws SecurityException If the user lacks the permission for configuring the general configuration
+ */
+ public function generalAction()
+ {
+ $this->assertPermission('config/general');
+ $form = new GeneralConfigForm();
+ $form->setIniConfig(Config::app());
+ $form->setOnSuccess(function (GeneralConfigForm $form) {
+ $config = Config::app();
+ $useStrictCsp = (bool) $config->get('security', 'use_strict_csp', false);
+ if ($form->onSuccess() === false) {
+ return false;
+ }
+
+ $appConfigForm = $form->getSubForm('form_config_general_application');
+ if ($appConfigForm && (bool) $appConfigForm->getValue('security_use_strict_csp') !== $useStrictCsp) {
+ $this->getResponse()->setReloadWindow(true);
+ }
+ })->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('General');
+ $this->createApplicationTabs()->activate('general');
+ }
+
+ /**
+ * Display the list of all modules
+ */
+ public function modulesAction()
+ {
+ $this->assertPermission('config/modules');
+ // Overwrite tabs created in init
+ // @TODO(el): This seems not natural to me. Module configuration should have its own controller.
+ $this->view->tabs = Widget::create('tabs')
+ ->add('modules', array(
+ 'label' => $this->translate('Modules'),
+ 'title' => $this->translate('List intalled modules'),
+ 'url' => 'config/modules'
+ ))
+ ->activate('modules');
+ $this->view->modules = Icinga::app()->getModuleManager()->select()
+ ->from('modules')
+ ->order('enabled', 'desc')
+ ->order('installed', 'asc')
+ ->order('name');
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->modules);
+ $this->view->title = $this->translate('Modules');
+ }
+
+ public function moduleAction()
+ {
+ $this->assertPermission('config/modules');
+ $app = Icinga::app();
+ $manager = $app->getModuleManager();
+ $name = $this->getParam('name');
+ if ($manager->hasInstalled($name) || $manager->hasEnabled($name)) {
+ $this->view->moduleData = $manager->select()->from('modules')->where('name', $name)->fetchRow();
+ if ($manager->hasLoaded($name)) {
+ $module = $manager->getModule($name);
+ } else {
+ $module = new Module($app, $name, $manager->getModuleDir($name));
+ }
+
+ $toggleForm = new ActionForm();
+ $toggleForm->setDefaults(['identifier' => $name]);
+ if (! $this->view->moduleData->enabled) {
+ $toggleForm->setAction(Url::fromPath('config/moduleenable'));
+ $toggleForm->setDescription(sprintf($this->translate('Enable the %s module'), $name));
+ } elseif ($this->view->moduleData->loaded) {
+ $toggleForm->setAction(Url::fromPath('config/moduledisable'));
+ $toggleForm->setDescription(sprintf($this->translate('Disable the %s module'), $name));
+ } else {
+ $toggleForm = null;
+ }
+
+ $this->view->module = $module;
+ $this->view->libraries = $app->getLibraries();
+ $this->view->moduleManager = $manager;
+ $this->view->toggleForm = $toggleForm;
+ $this->view->title = $module->getName();
+ $this->view->tabs = $module->getConfigTabs()->activate('info');
+ $this->view->moduleGitCommitId = Version::getGitHead($module->getBaseDir());
+ } else {
+ $this->view->module = false;
+ $this->view->tabs = null;
+ }
+ }
+
+ /**
+ * Enable a specific module provided by the 'name' param
+ */
+ public function moduleenableAction()
+ {
+ $this->assertPermission('config/modules');
+
+ $form = new ActionForm();
+ $form->setOnSuccess(function (ActionForm $form) {
+ $moduleName = $form->getValue('identifier');
+ $module = Icinga::app()->getModuleManager()
+ ->enableModule($moduleName)
+ ->getModule($moduleName);
+ Notification::success(sprintf($this->translate('Module "%s" enabled'), $moduleName));
+ $form->onSuccess();
+
+ if ($module->hasJs()) {
+ $this->getResponse()
+ ->setReloadWindow(true)
+ ->sendResponse();
+ } else {
+ if ($module->hasCss()) {
+ $this->reloadCss();
+ }
+
+ $this->rerenderLayout()->redirectNow('config/modules');
+ }
+ });
+
+ try {
+ $form->handleRequest();
+ } catch (Exception $e) {
+ $this->view->exceptionMessage = $e->getMessage();
+ $this->view->moduleName = $form->getValue('identifier');
+ $this->view->action = 'enable';
+ $this->render('module-configuration-error');
+ }
+ }
+
+ /**
+ * Disable a module specific module provided by the 'name' param
+ */
+ public function moduledisableAction()
+ {
+ $this->assertPermission('config/modules');
+
+ $form = new ActionForm();
+ $form->setOnSuccess(function (ActionForm $form) {
+ $mm = Icinga::app()->getModuleManager();
+ $moduleName = $form->getValue('identifier');
+ $module = $mm->getModule($moduleName);
+ $mm->disableModule($moduleName);
+ Notification::success(sprintf($this->translate('Module "%s" disabled'), $moduleName));
+ $form->onSuccess();
+
+ if ($module->hasJs()) {
+ $this->getResponse()
+ ->setReloadWindow(true)
+ ->sendResponse();
+ } else {
+ if ($module->hasCss()) {
+ $this->reloadCss();
+ }
+
+ $this->rerenderLayout()->redirectNow('config/modules');
+ }
+ });
+
+ try {
+ $form->handleRequest();
+ } catch (Exception $e) {
+ $this->view->exceptionMessage = $e->getMessage();
+ $this->view->moduleName = $form->getValue('identifier');
+ $this->view->action = 'disable';
+ $this->render('module-configuration-error');
+ }
+ }
+
+ /**
+ * Action for listing user and group backends
+ */
+ public function userbackendAction()
+ {
+ if ($this->hasPermission('config/access-control/users')) {
+ $form = new UserBackendReorderForm();
+ $form->setIniConfig(Config::app('authentication'));
+ $form->handleRequest();
+ $this->view->form = $form;
+ }
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $this->view->backendNames = Config::app('groups');
+ }
+
+ $this->createApplicationTabs()->activate('authentication');
+ $this->view->title = $this->translate('Authentication');
+ $this->render('userbackend/reorder');
+ }
+
+ /**
+ * Create a new user backend
+ */
+ public function createuserbackendAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $form = new UserBackendConfigForm();
+ $form
+ ->setRedirectUrl('config/userbackend')
+ ->addDescription($this->translate(
+ 'Create a new backend for authenticating your users. This backend'
+ . ' will be added at the end of your authentication order.'
+ ))
+ ->setIniConfig(Config::app('authentication'));
+
+ try {
+ $form->setResourceConfig(ResourceFactory::getResourceConfigs());
+ } catch (ConfigurationError $e) {
+ if ($this->hasPermission('config/resources')) {
+ Notification::error($e->getMessage());
+ $this->redirectNow('config/createresource');
+ }
+
+ throw $e; // No permission for resource configuration, show the error
+ }
+
+ $form->setOnSuccess(function (UserBackendConfigForm $form) {
+ try {
+ $form->add($form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(t('User backend successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('New User Backend'));
+ }
+
+ /**
+ * Edit a user backend
+ */
+ public function edituserbackendAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $backendName = $this->params->getRequired('backend');
+
+ $form = new UserBackendConfigForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setIniConfig(Config::app('authentication'));
+ $form->setOnSuccess(function (UserBackendConfigForm $form) use ($backendName) {
+ try {
+ $form->edit($backendName, $form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(sprintf(t('User backend "%s" successfully updated'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($backendName);
+ $form->setResourceConfig(ResourceFactory::getResourceConfigs());
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User backend "%s" not found'), $backendName));
+ }
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Update User Backend'));
+ }
+
+ /**
+ * Display a confirmation form to remove the backend identified by the 'backend' parameter
+ */
+ public function removeuserbackendAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $backendName = $this->params->getRequired('backend');
+
+ $backendForm = new UserBackendConfigForm();
+ $backendForm->setIniConfig(Config::app('authentication'));
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) {
+ try {
+ $backendForm->delete($backendName);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($backendForm->save()) {
+ Notification::success(sprintf(t('User backend "%s" successfully removed'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Remove User Backend'));
+ }
+
+ /**
+ * Display all available resources and a link to create a new one and to remove existing ones
+ */
+ public function resourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->view->resources = Config::app('resources', true)->getConfigObject()
+ ->setKeyColumn('name')
+ ->select()
+ ->order('name');
+ $this->view->title = $this->translate('Resources');
+ $this->createApplicationTabs()->activate('resource');
+ }
+
+ /**
+ * Display a form to create a new resource
+ */
+ public function createresourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->getTabs()->add('resources/new', array(
+ 'label' => $this->translate('New Resource'),
+ 'url' => Url::fromRequest()
+ ))->activate('resources/new');
+ $form = new ResourceConfigForm();
+ $form->addDescription($this->translate('Resources are entities that provide data to Icinga Web 2.'));
+ $form->setIniConfig(Config::app('resources'));
+ $form->setRedirectUrl('config/resource');
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Resources');
+ $this->render('resource/create');
+ }
+
+ /**
+ * Display a form to edit a existing resource
+ */
+ public function editresourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->getTabs()->add('resources/update', array(
+ 'label' => $this->translate('Update Resource'),
+ 'url' => Url::fromRequest()
+ ))->activate('resources/update');
+ $form = new ResourceConfigForm();
+ $form->setIniConfig(Config::app('resources'));
+ $form->setRedirectUrl('config/resource');
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Resources');
+ $this->render('resource/modify');
+ }
+
+ /**
+ * Display a confirmation form to remove a resource
+ */
+ public function removeresourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->getTabs()->add('resources/remove', array(
+ 'label' => $this->translate('Remove Resource'),
+ 'url' => Url::fromRequest()
+ ))->activate('resources/remove');
+ $form = new ConfirmRemovalForm(array(
+ 'onSuccess' => function ($form) {
+ $configForm = new ResourceConfigForm();
+ $configForm->setIniConfig(Config::app('resources'));
+ $resource = $form->getRequest()->getQuery('resource');
+
+ try {
+ $configForm->remove($resource);
+ } catch (InvalidArgumentException $e) {
+ Notification::error($e->getMessage());
+ return false;
+ }
+
+ if ($configForm->save()) {
+ Notification::success(sprintf(t('Resource "%s" has been successfully removed'), $resource));
+ } else {
+ return false;
+ }
+ }
+ ));
+ $form->setRedirectUrl('config/resource');
+ $form->handleRequest();
+
+ // Check if selected resource is currently used for authentication
+ $resource = $this->getRequest()->getQuery('resource');
+ $authConfig = Config::app('authentication');
+ foreach ($authConfig as $backendName => $config) {
+ if ($config->get('resource') === $resource) {
+ $form->warning(sprintf(
+ $this->translate(
+ 'The resource "%s" is currently utilized for authentication by user backend "%s".'
+ . ' Removing the resource can result in noone being able to log in any longer.'
+ ),
+ $resource,
+ $backendName
+ ));
+ }
+ }
+
+ // Check if selected resource is currently used as user preferences backend
+ if (Config::app()->get('global', 'config_resource') === $resource) {
+ $form->warning(sprintf(
+ $this->translate(
+ 'The resource "%s" is currently utilized to store user preferences. Removing the'
+ . ' resource causes all current user preferences not being available any longer.'
+ ),
+ $resource
+ ));
+ }
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Resources');
+ $this->render('resource/remove');
+ }
+}
diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php
new file mode 100644
index 0000000..ff2580c
--- /dev/null
+++ b/application/controllers/DashboardController.php
@@ -0,0 +1,346 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Zend_Controller_Action_Exception;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\Http\HttpNotFoundException;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Forms\Dashboard\DashletForm;
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Dashboard;
+use Icinga\Web\Widget\Tabextension\DashboardSettings;
+
+/**
+ * Handle creation, removal and displaying of dashboards, panes and dashlets
+ *
+ * @see Icinga\Web\Widget\Dashboard for more information about dashboards
+ */
+class DashboardController extends ActionController
+{
+ /**
+ * @var Dashboard;
+ */
+ private $dashboard;
+
+ public function init()
+ {
+ $this->dashboard = new Dashboard();
+ $this->dashboard->setUser($this->Auth()->getUser());
+ $this->dashboard->load();
+ }
+
+ public function newDashletAction()
+ {
+ $form = new DashletForm();
+ $this->getTabs()->add('new-dashlet', array(
+ 'active' => true,
+ 'label' => $this->translate('New Dashlet'),
+ 'url' => Url::fromRequest()
+ ));
+ $dashboard = $this->dashboard;
+ $form->setDashboard($dashboard);
+ if ($this->_request->getParam('url')) {
+ $params = $this->_request->getParams();
+ $params['url'] = rawurldecode($this->_request->getParam('url'));
+ $form->populate($params);
+ }
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $action) {
+ try {
+ $pane = $dashboard->getPane($form->getValue('pane'));
+ } catch (ProgrammingError $e) {
+ $pane = new Dashboard\Pane($form->getValue('pane'));
+ $pane->setUserWidget();
+ $dashboard->addPane($pane);
+ }
+ $dashlet = new Dashboard\Dashlet($form->getValue('dashlet'), $form->getValue('url'), $pane);
+ $dashlet->setUserWidget();
+ $pane->addDashlet($dashlet);
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ Notification::success(t('Dashlet created'));
+ return true;
+ });
+ $form->setTitle($this->translate('Add Dashlet To Dashboard'));
+ $form->setRedirectUrl('dashboard');
+ $form->handleRequest();
+ $this->view->form = $form;
+ }
+
+ public function updateDashletAction()
+ {
+ $this->getTabs()->add('update-dashlet', array(
+ 'active' => true,
+ 'label' => $this->translate('Update Dashlet'),
+ 'url' => Url::fromRequest()
+ ));
+ $dashboard = $this->dashboard;
+ $form = new DashletForm();
+ $form->setDashboard($dashboard);
+ $form->setSubmitLabel($this->translate('Update Dashlet'));
+ if (! $this->_request->getParam('pane')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "pane"',
+ 400
+ );
+ }
+ if (! $this->_request->getParam('dashlet')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "dashlet"',
+ 400
+ );
+ }
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $action) {
+ try {
+ $pane = $dashboard->getPane($form->getValue('org_pane'));
+ $pane->setTitle($form->getValue('pane'));
+ } catch (ProgrammingError $e) {
+ $pane = new Dashboard\Pane($form->getValue('pane'));
+ $pane->setUserWidget();
+ $dashboard->addPane($pane);
+ }
+ try {
+ $dashlet = $pane->getDashlet($form->getValue('org_dashlet'));
+ $dashlet->setTitle($form->getValue('dashlet'));
+ $dashlet->setUrl($form->getValue('url'));
+ } catch (ProgrammingError $e) {
+ $dashlet = new Dashboard\Dashlet($form->getValue('dashlet'), $form->getValue('url'), $pane);
+ $pane->addDashlet($dashlet);
+ }
+ $dashlet->setUserWidget();
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ Notification::success(t('Dashlet updated'));
+ return true;
+ });
+ $form->setTitle($this->translate('Edit Dashlet'));
+ $form->setRedirectUrl('dashboard/settings');
+ $form->handleRequest();
+ $pane = $dashboard->getPane($this->getParam('pane'));
+ $dashlet = $pane->getDashlet($this->getParam('dashlet'));
+ $form->load($dashlet);
+
+ $this->view->form = $form;
+ }
+
+ public function removeDashletAction()
+ {
+ $form = new ConfirmRemovalForm();
+ $this->getTabs()->add('remove-dashlet', array(
+ 'active' => true,
+ 'label' => $this->translate('Remove Dashlet'),
+ 'url' => Url::fromRequest()
+ ));
+ $dashboard = $this->dashboard;
+ if (! $this->_request->getParam('pane')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "pane"',
+ 400
+ );
+ }
+ if (! $this->_request->getParam('dashlet')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "dashlet"',
+ 400
+ );
+ }
+ $pane = $this->_request->getParam('pane');
+ $dashlet = $this->_request->getParam('dashlet');
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $dashlet, $pane, $action) {
+ $pane = $dashboard->getPane($pane);
+ $pane->removeDashlet($dashlet);
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ Notification::success(t('Dashlet has been removed from') . ' ' . $pane->getTitle());
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ return true;
+ });
+ $form->setTitle($this->translate('Remove Dashlet From Dashboard'));
+ $form->setRedirectUrl('dashboard/settings');
+ $form->handleRequest();
+ $this->view->pane = $pane;
+ $this->view->dashlet = $dashlet;
+ $this->view->form = $form;
+ }
+
+ public function renamePaneAction()
+ {
+ $paneName = $this->params->getRequired('pane');
+ if (! $this->dashboard->hasPane($paneName)) {
+ throw new HttpNotFoundException('Pane not found');
+ }
+
+ $form = new Form();
+ $form->setRedirectUrl('dashboard/settings');
+ $form->setSubmitLabel($this->translate('Update Pane'));
+ $form->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Name')
+ )
+ );
+ $form->addElement(
+ 'text',
+ 'title',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Title')
+ )
+ );
+ $form->setDefaults(array(
+ 'name' => $paneName,
+ 'title' => $this->dashboard->getPane($paneName)->getTitle()
+ ));
+ $form->setOnSuccess(function ($form) use ($paneName) {
+ $newName = $form->getValue('name');
+ $newTitle = $form->getValue('title');
+
+ $pane = $this->dashboard->getPane($paneName);
+ $pane->setName($newName);
+ $pane->setTitle($newTitle);
+ $this->dashboard->getConfig()->saveIni();
+
+ Notification::success(
+ sprintf($this->translate('Pane "%s" successfully renamed to "%s"'), $paneName, $newName)
+ );
+ });
+
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->getTabs()->add(
+ 'update-pane',
+ array(
+ 'title' => $this->translate('Update Pane'),
+ 'url' => $this->getRequest()->getUrl()
+ )
+ )->activate('update-pane');
+ }
+
+ public function removePaneAction()
+ {
+ $form = new ConfirmRemovalForm();
+ $this->createTabs();
+ $dashboard = $this->dashboard;
+ if (! $this->_request->getParam('pane')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "pane"',
+ 400
+ );
+ }
+ $pane = $this->_request->getParam('pane');
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $pane, $action) {
+ $pane = $dashboard->getPane($pane);
+ $dashboard->removePane($pane->getName());
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ Notification::success(t('Dashboard has been removed') . ': ' . $pane->getTitle());
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ return true;
+ });
+ $form->setTitle($this->translate('Remove Dashboard'));
+ $form->setRedirectUrl('dashboard/settings');
+ $form->handleRequest();
+ $this->view->pane = $pane;
+ $this->view->form = $form;
+ }
+
+ /**
+ * Display the dashboard with the pane set in the 'pane' request parameter
+ *
+ * If no pane is submitted or the submitted one doesn't exist, the default pane is
+ * displayed (normally the first one)
+ */
+ public function indexAction()
+ {
+ $this->createTabs();
+ if (! $this->dashboard->hasPanes()) {
+ $this->view->title = 'Dashboard';
+ } else {
+ $panes = array_filter(
+ $this->dashboard->getPanes(),
+ function ($pane) {
+ return ! $pane->getDisabled();
+ }
+ );
+ if (empty($panes)) {
+ $this->view->title = 'Dashboard';
+ $this->getTabs()->add('dashboard', array(
+ 'active' => true,
+ 'title' => $this->translate('Dashboard'),
+ 'url' => Url::fromRequest()
+ ));
+ } else {
+ if ($this->_getParam('pane')) {
+ $pane = $this->_getParam('pane');
+ $this->dashboard->activate($pane);
+ }
+ if ($this->dashboard === null) {
+ $this->view->title = 'Dashboard';
+ } else {
+ $this->view->title = $this->dashboard->getActivePane()->getTitle() . ' :: Dashboard';
+ if ($this->hasParam('remove')) {
+ $this->dashboard->getActivePane()->removeDashlet($this->getParam('remove'));
+ $this->dashboard->getConfig()->saveIni();
+ $this->redirectNow(URL::fromRequest()->remove('remove'));
+ }
+ $this->view->dashboard = $this->dashboard;
+ }
+ }
+ }
+ }
+
+ /**
+ * Setting dialog
+ */
+ public function settingsAction()
+ {
+ $this->createTabs();
+ $this->view->dashboard = $this->dashboard;
+ }
+
+ /**
+ * Create tab aggregation
+ */
+ private function createTabs()
+ {
+ $this->view->tabs = $this->dashboard->getTabs()->extend(new DashboardSettings());
+ }
+}
diff --git a/application/controllers/ErrorController.php b/application/controllers/ErrorController.php
new file mode 100644
index 0000000..476b71f
--- /dev/null
+++ b/application/controllers/ErrorController.php
@@ -0,0 +1,176 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Application\MigrationManager;
+use Icinga\Exception\IcingaException;
+use Zend_Controller_Plugin_ErrorHandler;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Exception\Http\HttpExceptionInterface;
+use Icinga\Exception\MissingParameterException;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Url;
+
+/**
+ * Application wide controller for displaying exceptions
+ */
+class ErrorController extends ActionController
+{
+ /**
+ * Regular expression to match exceptions resulting from missing functions/classes
+ */
+ const MISSING_DEP_ERROR =
+ "/Uncaught Error:.*(?:undefined function (\S+)|Class ['\"]([^']+)['\"] not found).* in ([^:]+)/";
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->rerenderLayout = $this->params->has('renderLayout');
+ }
+
+ /**
+ * Display exception
+ */
+ public function errorAction()
+ {
+ $error = $this->_getParam('error_handler');
+ $exception = $error->exception;
+ /** @var \Exception $exception */
+
+ if (! ($isAuthenticated = $this->Auth()->isAuthenticated())) {
+ $this->innerLayout = 'guest-error';
+ }
+
+ $modules = Icinga::app()->getModuleManager();
+ $sourcePath = ltrim($this->_request->get('PATH_INFO'), '/');
+ $pathParts = preg_split('~/~', $sourcePath);
+ $moduleName = array_shift($pathParts);
+
+ $module = null;
+ switch ($error->type) {
+ case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
+ case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
+ case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
+ $this->getResponse()->setHttpResponseCode(404);
+ $this->view->messages = array($this->translate('Page not found.'));
+ if ($isAuthenticated) {
+ if ($modules->hasInstalled($moduleName) && ! $modules->hasEnabled($moduleName)) {
+ $this->view->messages[0] .= ' ' . sprintf(
+ $this->translate('Enabling the "%s" module might help!'),
+ $moduleName
+ );
+ }
+ }
+
+ break;
+ default:
+ switch (true) {
+ case $exception instanceof HttpExceptionInterface:
+ $this->getResponse()->setHttpResponseCode($exception->getStatusCode());
+ foreach ($exception->getHeaders() as $name => $value) {
+ $this->getResponse()->setHeader($name, $value, true);
+ }
+ break;
+ case $exception instanceof MissingParameterException:
+ $this->getResponse()->setHttpResponseCode(400);
+ $this->getResponse()->setHeader(
+ 'X-Status-Reason',
+ 'Missing parameter ' . $exception->getParameter()
+ );
+ break;
+ case $exception instanceof SecurityException:
+ $this->getResponse()->setHttpResponseCode(403);
+ break;
+ default:
+ $mm = MigrationManager::instance();
+ $action = $this->getRequest()->getActionName();
+ $controller = $this->getRequest()->getControllerName();
+ if ($action !== 'hint' && $controller !== 'migrations' && $mm->hasMigrations($moduleName)) {
+ // The view renderer from IPL web doesn't render the HTML content set in the respective
+ // controller if the error_handler request param is set, as it doesn't support error
+ // rendering. Since this error handler isn't caused by the migrations controller, we can
+ // safely unset this.
+ $this->setParam('error_handler', null);
+ $this->forward('hint', 'migrations', 'default', [
+ DbMigrationHook::MIGRATION_PARAM => $moduleName
+ ]);
+
+ return;
+ }
+
+ $this->getResponse()->setHttpResponseCode(500);
+ $module = $modules->hasLoaded($moduleName) ? $modules->getModule($moduleName) : null;
+ Logger::error("%s\n%s", $exception, IcingaException::getConfidentialTraceAsString($exception));
+ break;
+ }
+
+ // Try to narrow down why the request has failed
+ if (preg_match(self::MISSING_DEP_ERROR, $exception->getMessage(), $match)) {
+ $sourcePath = $match[3];
+ foreach ($modules->listLoadedModules() as $name) {
+ $candidate = $modules->getModule($name);
+ $modulePath = $candidate->getBaseDir();
+ if (substr($sourcePath, 0, strlen($modulePath)) === $modulePath) {
+ $module = $candidate;
+ break;
+ }
+ }
+
+ if (preg_match('/^(?:Icinga\\\Module\\\(\w+)|(\w+)\\\(\w+))/', $match[1] ?: $match[2], $natch)) {
+ $this->view->requiredModule = isset($natch[1]) ? strtolower($natch[1]) : null;
+ $this->view->requiredVendor = isset($natch[2]) ? $natch[2] : null;
+ $this->view->requiredProject = isset($natch[3]) ? $natch[3] : null;
+ }
+ }
+
+ $this->view->messages = array();
+
+ if ($this->getInvokeArg('displayExceptions')) {
+ $this->view->stackTraces = array();
+
+ do {
+ $this->view->messages[] = $exception->getMessage();
+ $this->view->stackTraces[] = IcingaException::getConfidentialTraceAsString($exception);
+ $exception = $exception->getPrevious();
+ } while ($exception !== null);
+ } else {
+ do {
+ $this->view->messages[] = $exception->getMessage();
+ $exception = $exception->getPrevious();
+ } while ($exception !== null);
+ }
+
+ break;
+ }
+
+ if ($this->getRequest()->isApiRequest()) {
+ $this->getResponse()->json()
+ ->setErrorMessage($this->view->messages[0])
+ ->sendResponse();
+ }
+
+ $this->view->module = $module;
+ $this->view->request = $error->request;
+ if (! $isAuthenticated) {
+ $this->view->hideControls = true;
+ } else {
+ $this->view->hideControls = false;
+ $this->getTabs()->add('error', array(
+ 'active' => true,
+ 'label' => $this->translate('Error'),
+ 'url' => Url::fromRequest()
+ ));
+ }
+ }
+}
diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php
new file mode 100644
index 0000000..d18397c
--- /dev/null
+++ b/application/controllers/GroupController.php
@@ -0,0 +1,418 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Authentication\User\DomainAwareInterface;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Reducible;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Config\UserGroup\AddMemberForm;
+use Icinga\Forms\Config\UserGroup\UserGroupForm;
+use Icinga\User;
+use Icinga\Web\Controller\AuthBackendController;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget;
+
+class GroupController extends AuthBackendController
+{
+ public function init()
+ {
+ $this->view->title = $this->translate('User Groups');
+
+ parent::init();
+ }
+
+ /**
+ * List all user groups of a single backend
+ */
+ public function listAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $this->createListTabs()->activate('group/list');
+ $backendNames = array_map(
+ function ($b) {
+ return $b->getName();
+ },
+ $this->loadUserGroupBackends('Icinga\Data\Selectable')
+ );
+ if (empty($backendNames)) {
+ return;
+ }
+
+ $this->view->backendSelection = new Form();
+ $this->view->backendSelection->setAttrib('class', 'backend-selection icinga-controls');
+ $this->view->backendSelection->setUidDisabled();
+ $this->view->backendSelection->setMethod('GET');
+ $this->view->backendSelection->setTokenDisabled();
+ $this->view->backendSelection->addElement(
+ 'select',
+ 'backend',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Group Backend'),
+ 'multiOptions' => array_combine($backendNames, $backendNames),
+ 'value' => $this->params->get('backend')
+ )
+ );
+
+ $backend = $this->getUserGroupBackend($this->params->get('backend'));
+ if ($backend === null) {
+ $this->view->backend = null;
+ return;
+ }
+
+ $query = $backend->select(array('group_name'));
+
+ $this->view->groups = $query;
+ $this->view->backend = $backend;
+
+ $this->setupPaginationControl($query);
+ $this->setupFilterControl($query);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'group_name' => $this->translate('User Group'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Show a group
+ */
+ public function showAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'));
+
+ $group = $backend->select(array(
+ 'group_name',
+ 'created_at',
+ 'last_modified'
+ ))->where('group_name', $groupName)->fetchRow();
+ if ($group === false) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $members = $backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $groupName);
+
+ $this->setupFilterControl($members, null, array('user'), array('group'));
+ $this->setupPaginationControl($members);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'user_name' => $this->translate('Username'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $members
+ );
+
+ $this->view->group = $group;
+ $this->view->backend = $backend;
+ $this->view->members = $members;
+ $this->createShowTabs($backend->getName(), $groupName)->activate('group/show');
+
+ if ($this->hasPermission('config/access-control/groups') && $backend instanceof Reducible) {
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAttrib('class', 'inline');
+ $removeForm->setAction(
+ Url::fromPath('group/removemember', array('backend' => $backend->getName(), 'group' => $groupName))
+ );
+ $removeForm->addElement('hidden', 'user_name', array(
+ 'isArray' => true,
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('group/show', array(
+ 'backend' => $backend->getName(),
+ 'group' => $groupName
+ )),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Remove this member')
+ ));
+ $this->view->removeForm = $removeForm;
+ }
+ }
+
+ /**
+ * Add a group
+ */
+ public function addAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+ $form->add()->handleRequest();
+
+ $this->renderForm($form, $this->translate('New User Group'));
+ }
+
+ /**
+ * Edit a group
+ */
+ public function editAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
+
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(
+ Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
+ );
+ $form->setRepository($backend);
+
+ try {
+ $form->edit($groupName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->renderForm($form, $this->translate('Update User Group'));
+ }
+
+ /**
+ * Remove a group
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+
+ try {
+ $form->remove($groupName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->renderForm($form, $this->translate('Remove User Group'));
+ }
+
+ /**
+ * Add a group member
+ */
+ public function addmemberAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+
+ $form = new AddMemberForm();
+ $form->setDataSource($this->fetchUsers())
+ ->setBackend($backend)
+ ->setGroupName($groupName)
+ ->setRedirectUrl(
+ Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
+ )
+ ->setUidDisabled();
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->renderForm($form, $this->translate('New User Group Member'));
+ }
+
+ /**
+ * Remove a group member
+ */
+ public function removememberAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $this->assertHttpMethod('POST');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new Form(array(
+ 'onSuccess' => function ($form) use ($groupName, $backend) {
+ foreach ($form->getValue('user_name') as $userName) {
+ try {
+ $backend->delete(
+ 'group_membership',
+ Filter::matchAll(
+ Filter::where('group_name', $groupName),
+ Filter::where('user_name', $userName)
+ )
+ );
+ Notification::success(sprintf(
+ t('User "%s" has been removed from group "%s"'),
+ $userName,
+ $groupName
+ ));
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ Notification::error($e->getMessage());
+ }
+ }
+
+ $redirect = $form->getValue('redirect');
+ if (! empty($redirect)) {
+ $form->setRedirectUrl(htmlspecialchars_decode($redirect));
+ }
+
+ return true;
+ }
+ ));
+ $form->setUidDisabled();
+ $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called
+ $form->addElement('hidden', 'user_name', array('required' => true, 'isArray' => true));
+ $form->addElement('hidden', 'redirect');
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+ }
+
+ /**
+ * Fetch and return all users from all user backends
+ *
+ * @return ArrayDatasource
+ */
+ protected function fetchUsers()
+ {
+ $users = array();
+ foreach ($this->loadUserBackends('Icinga\Data\Selectable') as $backend) {
+ try {
+ if ($backend instanceof DomainAwareInterface) {
+ $domain = $backend->getDomain();
+ } else {
+ $domain = null;
+ }
+ foreach ($backend->select(array('user_name')) as $user) {
+ $userObj = new User($user->user_name);
+ if ($domain !== null) {
+ if ($userObj->hasDomain() && $userObj->getDomain() !== $domain) {
+ // Users listed in a user backend which is configured to be responsible for a domain should
+ // not have a domain in their username. Ultimately, if the username has a domain, it must
+ // not differ from the backend's domain. We could log here - but hey, who cares :)
+ continue;
+ } else {
+ $userObj->setDomain($domain);
+ }
+ }
+
+ $user->user_name = $userObj->getUsername();
+
+ $users[] = $user;
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any users from backend %s. Please check your log'),
+ $backend->getName()
+ ));
+ }
+ }
+
+ return new ArrayDatasource($users);
+ }
+
+ /**
+ * Create the tabs to display when showing a group
+ *
+ * @param string $backendName
+ * @param string $groupName
+ */
+ protected function createShowTabs($backendName, $groupName)
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'group/show',
+ array(
+ 'title' => sprintf($this->translate('Show group %s'), $groupName),
+ 'label' => $this->translate('Group'),
+ 'url' => Url::fromPath('group/show', array('backend' => $backendName, 'group' => $groupName))
+ )
+ );
+
+ return $tabs;
+ }
+
+ /**
+ * Create the tabs to display when listing groups
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+
+ if ($this->hasPermission('config/access-control/roles')) {
+ $tabs->add(
+ 'role/list',
+ array(
+ 'baseTarget' => '_main',
+ 'label' => $this->translate('Roles'),
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'url' => 'role/list'
+ )
+ );
+
+ $tabs->add(
+ 'role/audit',
+ [
+ 'title' => $this->translate('Audit a user\'s or group\'s privileges'),
+ 'label' => $this->translate('Audit'),
+ 'url' => 'role/audit',
+ 'baseTarget' => '_main',
+ ]
+ );
+ }
+
+ if ($this->hasPermission('config/access-control/users')) {
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'user/list'
+ )
+ );
+ }
+
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'group/list'
+ )
+ );
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/HealthController.php b/application/controllers/HealthController.php
new file mode 100644
index 0000000..f176e69
--- /dev/null
+++ b/application/controllers/HealthController.php
@@ -0,0 +1,65 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\HealthHook;
+use Icinga\Web\View\AppHealth;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use ipl\Web\Compat\CompatController;
+
+class HealthController extends CompatController
+{
+ public function indexAction()
+ {
+ $query = HealthHook::collectHealthData()
+ ->select();
+
+ $this->setupSortControl(
+ [
+ 'module' => $this->translate('Module'),
+ 'name' => $this->translate('Name'),
+ 'state' => $this->translate('State')
+ ],
+ $query,
+ ['state' => 'desc']
+ );
+ $this->setupLimitControl();
+ $this->setupPaginationControl($query);
+ $this->setupFilterControl($query, [
+ 'module' => $this->translate('Module'),
+ 'name' => $this->translate('Name'),
+ 'state' => $this->translate('State'),
+ 'message' => $this->translate('Message')
+ ], ['name'], ['format']);
+
+ $this->getTabs()->extend(new OutputFormat(['csv']));
+ $this->handleFormatRequest($query);
+
+ $this->addControl(HtmlString::create((string) $this->view->paginator));
+ $this->addControl(Html::tag('div', ['class' => 'sort-controls-container'], [
+ HtmlString::create((string) $this->view->limiter),
+ HtmlString::create((string) $this->view->sortBox)
+ ]));
+ $this->addControl(HtmlString::create((string) $this->view->filterEditor));
+
+ $this->addTitleTab(t('Health'));
+ $this->setAutorefreshInterval(10);
+ $this->addContent(new AppHealth($query));
+ }
+
+ protected function handleFormatRequest($query)
+ {
+ $formatJson = $this->params->get('format') === 'json';
+ if (! $formatJson && ! $this->getRequest()->isApiRequest()) {
+ return;
+ }
+
+ $this->getResponse()
+ ->json()
+ ->setSuccessData($query->fetchAll())
+ ->sendResponse();
+ }
+}
diff --git a/application/controllers/IframeController.php b/application/controllers/IframeController.php
new file mode 100644
index 0000000..8aebba4
--- /dev/null
+++ b/application/controllers/IframeController.php
@@ -0,0 +1,20 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller;
+
+/**
+ * Display external or internal links within an iframe
+ */
+class IframeController extends Controller
+{
+ /**
+ * Display iframe w/ the given URL
+ */
+ public function indexAction()
+ {
+ $this->view->url = $this->params->getRequired('url');
+ }
+}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
new file mode 100644
index 0000000..539c16b
--- /dev/null
+++ b/application/controllers/IndexController.php
@@ -0,0 +1,36 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Url;
+
+/**
+ * Application wide index controller
+ */
+class IndexController extends ActionController
+{
+ /**
+ * Use a default redirection rule to welcome page
+ */
+ public function preDispatch()
+ {
+ if ($this->getRequest()->getActionName() !== 'welcome') {
+ $landingPage = getenv('ICINGAWEB_LANDING_PAGE');
+ if (! $landingPage) {
+ $landingPage = 'dashboard';
+ }
+
+ // @TODO(el): Avoid landing page redirects: https://dev.icinga.com/issues/9656
+ $this->redirectNow(Url::fromRequest()->setPath($landingPage));
+ }
+ }
+
+ /**
+ * Application's start page
+ */
+ public function welcomeAction()
+ {
+ }
+}
diff --git a/application/controllers/LayoutController.php b/application/controllers/LayoutController.php
new file mode 100644
index 0000000..237681c
--- /dev/null
+++ b/application/controllers/LayoutController.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Menu;
+
+/**
+ * Create complex layout parts
+ */
+class LayoutController extends ActionController
+{
+ /**
+ * Render the menu
+ */
+ public function menuAction()
+ {
+ $this->setAutorefreshInterval(15);
+ $this->_helper->layout()->disableLayout();
+ $this->view->menuRenderer = (new Menu())->getRenderer();
+ }
+
+ public function announcementsAction()
+ {
+ $this->_helper->layout()->disableLayout();
+ }
+}
diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php
new file mode 100644
index 0000000..2fbc5a9
--- /dev/null
+++ b/application/controllers/ListController.php
@@ -0,0 +1,59 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\Protocol\File\FileReader;
+use Icinga\Web\Controller;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+
+/**
+ * Application wide controller for various listing actions
+ */
+class ListController extends Controller
+{
+ /**
+ * Add title tab
+ *
+ * @param string $action
+ */
+ protected function addTitleTab($action)
+ {
+ $this->getTabs()->add($action, array(
+ 'label' => ucfirst($action),
+ 'url' => Url::fromPath('list/' . str_replace(' ', '', $action))
+ ))->extend(new OutputFormat())->extend(new DashboardAction())->extend(new MenuAction())->activate($action);
+ }
+
+ /**
+ * Display the application log
+ */
+ public function applicationlogAction()
+ {
+ $this->assertPermission('application/log');
+
+ if (! Logger::writesToFile()) {
+ $this->httpNotFound('Page not found');
+ }
+
+ $this->addTitleTab('application log');
+
+ $resource = new FileReader(new ConfigObject(array(
+ 'filename' => Config::app()->get('logging', 'file'),
+ 'fields' => '/(?<!.)(?<datetime>[0-9]{4}(?:-[0-9]{2}){2}' // date
+ . 'T[0-9]{2}(?::[0-9]{2}){2}(?:[\+\-][0-9]{2}:[0-9]{2})?)' // time
+ . ' - (?<loglevel>[A-Za-z]+) - (?<message>.*)(?!.)/msS' // loglevel, message
+ )));
+ $this->view->logData = $resource->select()->order('DESC');
+
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->logData);
+ $this->view->title = $this->translate('Application Log');
+ }
+}
diff --git a/application/controllers/ManageUserDevicesController.php b/application/controllers/ManageUserDevicesController.php
new file mode 100644
index 0000000..db054d1
--- /dev/null
+++ b/application/controllers/ManageUserDevicesController.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Common\Database;
+use Icinga\Web\Notification;
+use Icinga\Web\RememberMe;
+use Icinga\Web\RememberMeUserList;
+use Icinga\Web\RememberMeUserDevicesList;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Url;
+
+/**
+ * ManageUserDevicesController
+ *
+ * you need 'application/sessions' permission to use this controller
+ */
+class ManageUserDevicesController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ $this->assertPermission('application/sessions');
+ }
+
+ public function indexAction()
+ {
+ $this->getTabs()
+ ->add(
+ 'manage-user-devices',
+ array(
+ 'title' => $this->translate('List of users who stay logged in'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'manage-user-devices',
+ 'data-base-target' => '_self'
+ )
+ )->activate('manage-user-devices');
+
+ $usersList = (new RememberMeUserList())
+ ->setUsers(RememberMe::getAllUser())
+ ->setUrl('manage-user-devices/devices');
+
+ $this->addContent($usersList);
+
+ if (! $this->hasDb()) {
+ Notification::warning(
+ $this->translate("Users can't stay logged in without a database configuration backend")
+ );
+ }
+ }
+
+ public function devicesAction()
+ {
+ $this->getTabs()
+ ->add(
+ 'manage-devices',
+ array(
+ 'title' => $this->translate('List of devices'),
+ 'label' => $this->translate('Devices'),
+ 'url' => 'manage-user-devices/devices'
+ )
+ )->activate('manage-devices');
+
+ $name = $this->params->getRequired('name');
+ $data = (new RememberMeUserDevicesList())
+ ->setDevicesList(RememberMe::getAllByUsername($name))
+ ->setUsername($name)
+ ->setUrl('manage-user-devices/delete');
+
+ $this->addContent($data);
+ }
+
+ public function deleteAction()
+ {
+ (new RememberMe())->remove($this->params->getRequired('fingerprint'));
+
+ $this->redirectNow(
+ Url::fromPath('manage-user-devices/devices')
+ ->addParams(['name' => $this->params->getRequired('name')])
+ );
+ }
+}
diff --git a/application/controllers/MigrationsController.php b/application/controllers/MigrationsController.php
new file mode 100644
index 0000000..5229f06
--- /dev/null
+++ b/application/controllers/MigrationsController.php
@@ -0,0 +1,249 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\MigrationManager;
+use Icinga\Common\Database;
+use Icinga\Exception\MissingParameterException;
+use Icinga\Forms\MigrationForm;
+use Icinga\Web\Notification;
+use Icinga\Web\Widget\ItemList\MigrationList;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\SubmitButtonElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Widget\ActionLink;
+
+class MigrationsController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ Icinga::app()->getModuleManager()->loadModule('setup');
+ }
+
+ public function indexAction(): void
+ {
+ $mm = MigrationManager::instance();
+
+ $this->getTabs()->extend(new OutputFormat(['csv']));
+ $this->addTitleTab($this->translate('Migrations'));
+
+ $canApply = $this->hasPermission('application/migrations');
+ if (! $canApply) {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-state-banner']),
+ new HtmlElement(
+ 'span',
+ null,
+ Text::create(
+ $this->translate('You do not have the required permission to apply pending migrations.')
+ )
+ )
+ )
+ );
+ }
+
+ $migrateListForm = new MigrationForm();
+ $migrateListForm->setAttribute('id', $this->getRequest()->protectId('migration-form'));
+ $migrateListForm->setRenderDatabaseUserChange(! $mm->validateDatabasePrivileges());
+
+ if ($canApply && $mm->hasPendingMigrations()) {
+ $migrateAllButton = new SubmitButtonElement(sprintf('migrate-%s', DbMigrationHook::ALL_MIGRATIONS), [
+ 'form' => $migrateListForm->getAttribute('id')->getValue(),
+ 'label' => $this->translate('Migrate All'),
+ 'title' => $this->translate('Migrate all pending migrations')
+ ]);
+
+ // Is the first button, so will be cloned and that the visible
+ // button is outside the form doesn't matter for Web's JS
+ $migrateListForm->registerElement($migrateAllButton);
+
+ // Make sure it looks familiar, even if not inside a form
+ $migrateAllButton->setWrapper(new HtmlElement('div', Attributes::create(['class' => 'icinga-controls'])));
+
+ $this->controls->getAttributes()->add('class', 'default-layout');
+ $this->addControl($migrateAllButton);
+ }
+
+ $this->handleFormatRequest($mm->toArray());
+
+ $frameworkList = new MigrationList($mm->yieldMigrations(), $migrateListForm);
+ $frameworkListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
+ $frameworkListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('System'))));
+ $frameworkListControl->addHtml($frameworkList);
+
+ $moduleList = new MigrationList($mm->yieldMigrations(true), $migrateListForm);
+ $moduleListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
+ $moduleListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('Modules'))));
+ $moduleListControl->addHtml($moduleList);
+
+ $migrateListForm->addHtml($frameworkListControl, $moduleListControl);
+ if ($canApply && $mm->hasPendingMigrations()) {
+ $frameworkList->ensureAssembled();
+ $moduleList->ensureAssembled();
+
+ $this->handleMigrateRequest($migrateListForm);
+ }
+
+ $migrations = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
+ $migrations->addHtml($migrateListForm);
+
+ $this->addContent($migrations);
+ }
+
+ public function hintAction(): void
+ {
+ // The forwarded request doesn't modify the original server query string, but adds the migration param to the
+ // request param instead. So, there is no way to access the migration param other than via the request instance.
+ /** @var ?string $module */
+ $module = $this->getRequest()->getParam(DbMigrationHook::MIGRATION_PARAM);
+ if ($module === null) {
+ throw new MissingParameterException(
+ $this->translate('Required parameter \'%s\' missing'),
+ DbMigrationHook::MIGRATION_PARAM
+ );
+ }
+
+ $mm = MigrationManager::instance();
+ if (! $mm->hasMigrations($module)) {
+ $this->httpNotFound(sprintf('There are no pending migrations matching the given name: %s', $module));
+ }
+
+ $migration = $mm->getMigration($module);
+ $this->addTitleTab($this->translate('Error'));
+ $this->addContent(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'pending-migrations-hint']),
+ new HtmlElement('h2', null, Text::create($this->translate('Error!'))),
+ new HtmlElement(
+ 'p',
+ null,
+ Text::create(sprintf($this->translate('%s has pending migrations.'), $migration->getName()))
+ ),
+ new HtmlElement('p', null, Text::create($this->translate('Please apply the migrations first.'))),
+ new ActionLink($this->translate('View pending Migrations'), 'migrations')
+ )
+ );
+ }
+
+ public function migrationAction(): void
+ {
+ /** @var string $name */
+ $name = $this->params->getRequired(DbMigrationHook::MIGRATION_PARAM);
+
+ $this->addTitleTab($this->translate('Migration'));
+ $this->getTabs()->disableLegacyExtensions();
+ $this->controls->getAttributes()->add('class', 'default-layout');
+
+ $mm = MigrationManager::instance();
+ if (! $mm->hasMigrations($name)) {
+ $migrations = [];
+ } else {
+ $hook = $mm->getMigration($name);
+ $migrations = array_reverse($hook->getMigrations());
+ if (! $this->hasPermission('application/migrations')) {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-state-banner']),
+ new HtmlElement(
+ 'span',
+ null,
+ Text::create(
+ $this->translate('You do not have the required permission to apply pending migrations.')
+ )
+ )
+ )
+ );
+ } else {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-controls']),
+ new HtmlElement('span', null, Text::create($hook->getName()))
+ )
+ );
+ }
+ }
+
+ $migrationWidget = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
+ $migrationWidget->addHtml((new MigrationList($migrations))->setMinimal(false));
+ $this->addContent($migrationWidget);
+ }
+
+ public function handleMigrateRequest(MigrationForm $form): void
+ {
+ $this->assertPermission('application/migrations');
+
+ $form->on(MigrationForm::ON_SUCCESS, function (MigrationForm $form) {
+ $mm = MigrationManager::instance();
+
+ /** @var array<string, string> $elevatedPrivileges */
+ $elevatedPrivileges = $form->getValue('database_setup');
+ if ($elevatedPrivileges !== null && $elevatedPrivileges['grant_privileges'] === 'y') {
+ $mm->fixIcingaWebMysqlGrants($this->getDb(), $elevatedPrivileges);
+ }
+
+ $pressedButton = $form->getPressedSubmitElement();
+ if ($pressedButton) {
+ $name = substr($pressedButton->getName(), 8);
+ switch ($name) {
+ case DbMigrationHook::ALL_MIGRATIONS:
+ if ($mm->applyAll($elevatedPrivileges)) {
+ Notification::success($this->translate('Applied all migrations successfully'));
+ } else {
+ Notification::error(
+ $this->translate(
+ 'Applied migrations successfully. Though, one or more migration hooks'
+ . ' failed to run. See logs for details'
+ )
+ );
+ }
+ break;
+ default:
+ $migration = $mm->getMigration($name);
+ if ($mm->apply($migration, $elevatedPrivileges)) {
+ Notification::success($this->translate('Applied pending migrations successfully'));
+ } else {
+ Notification::error(
+ $this->translate('Failed to apply pending migration(s). See logs for details')
+ );
+ }
+ }
+ }
+
+ $this->sendExtraUpdates(['#col2' => '__CLOSE__']);
+
+ $this->redirectNow('migrations');
+ })->handleRequest($this->getServerRequest());
+ }
+
+ /**
+ * Handle exports
+ *
+ * @param array<string, mixed> $data
+ */
+ protected function handleFormatRequest(array $data): void
+ {
+ $formatJson = $this->params->get('format') === 'json';
+ if (! $formatJson && ! $this->getRequest()->isApiRequest()) {
+ return;
+ }
+
+ $this->getResponse()
+ ->json()
+ ->setSuccessData($data)
+ ->sendResponse();
+ }
+}
diff --git a/application/controllers/MyDevicesController.php b/application/controllers/MyDevicesController.php
new file mode 100644
index 0000000..e0fb98a
--- /dev/null
+++ b/application/controllers/MyDevicesController.php
@@ -0,0 +1,74 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Common\Database;
+use Icinga\Web\Notification;
+use Icinga\Web\RememberMe;
+use Icinga\Web\RememberMeUserDevicesList;
+use ipl\Web\Compat\CompatController;
+
+/**
+ * MyDevicesController
+ *
+ * this controller shows you all the devices you are logged in
+ */
+class MyDevicesController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ $this->getTabs()
+ ->add(
+ 'account',
+ array(
+ 'title' => $this->translate('Update your account'),
+ 'label' => $this->translate('My Account'),
+ 'url' => 'account'
+ )
+ )
+ ->add(
+ 'navigation',
+ array(
+ 'title' => $this->translate('List and configure your own navigation items'),
+ 'label' => $this->translate('Navigation'),
+ 'url' => 'navigation'
+ )
+ )
+ ->add(
+ 'devices',
+ array(
+ 'title' => $this->translate('List of devices you are logged in'),
+ 'label' => $this->translate('My Devices'),
+ 'url' => 'my-devices'
+ )
+ )->activate('devices');
+ }
+
+ public function indexAction()
+ {
+ $name = $this->auth->getUser()->getUsername();
+
+ $data = (new RememberMeUserDevicesList())
+ ->setDevicesList(RememberMe::getAllByUsername($name))
+ ->setUsername($name)
+ ->setUrl('my-devices/delete');
+
+ $this->addContent($data);
+
+ if (! $this->hasDb()) {
+ Notification::warning(
+ $this->translate("Users can't stay logged in without a database configuration backend")
+ );
+ }
+ }
+
+ public function deleteAction()
+ {
+ (new RememberMe())->remove($this->params->getRequired('fingerprint'));
+
+ $this->redirectNow('my-devices');
+ }
+}
diff --git a/application/controllers/NavigationController.php b/application/controllers/NavigationController.php
new file mode 100644
index 0000000..b0babc3
--- /dev/null
+++ b/application/controllers/NavigationController.php
@@ -0,0 +1,447 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\FilterMatchCaseInsensitive;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Forms\Navigation\NavigationConfigForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Form;
+use Icinga\Web\Menu;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+
+/**
+ * Navigation configuration
+ */
+class NavigationController extends Controller
+{
+ /**
+ * The global navigation item type configuration
+ *
+ * @var array
+ */
+ protected $itemTypeConfig;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ parent::init();
+ $this->itemTypeConfig = Navigation::getItemTypeConfiguration();
+ }
+
+ /**
+ * Return the label for the given navigation item type
+ *
+ * @param string $type
+ *
+ * @return string $type if no label can be found
+ */
+ protected function getItemLabel($type)
+ {
+ return isset($this->itemTypeConfig[$type]['label']) ? $this->itemTypeConfig[$type]['label'] : $type;
+ }
+
+ /**
+ * Return a list of available navigation item types
+ *
+ * @return array
+ */
+ protected function listItemTypes()
+ {
+ $types = array();
+ foreach ($this->itemTypeConfig as $type => $options) {
+ $types[$type] = isset($options['label']) ? $options['label'] : $type;
+ }
+
+ return $types;
+ }
+
+ /**
+ * Return all shared navigation item configurations
+ *
+ * @param string $owner A username if only items shared by a specific user are desired
+ *
+ * @return array
+ */
+ protected function fetchSharedNavigationItemConfigs($owner = null)
+ {
+ $configs = array();
+ foreach ($this->itemTypeConfig as $type => $_) {
+ $config = Config::navigation($type);
+ $config->getConfigObject()->setKeyColumn('name');
+ $query = $config->select();
+ if ($owner !== null) {
+ $query->applyFilter(new FilterMatchCaseInsensitive('owner', '=', $owner));
+ }
+
+ foreach ($query as $itemConfig) {
+ $configs[] = $itemConfig;
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Return all user navigation item configurations
+ *
+ * @param string $username
+ *
+ * @return array
+ */
+ protected function fetchUserNavigationItemConfigs($username)
+ {
+ $configs = array();
+ foreach ($this->itemTypeConfig as $type => $_) {
+ $config = Config::navigation($type, $username);
+ $config->getConfigObject()->setKeyColumn('name');
+ foreach ($config->select() as $itemConfig) {
+ $configs[] = $itemConfig;
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Show the current user a list of his/her navigation items
+ */
+ public function indexAction()
+ {
+ $user = $this->Auth()->getUser();
+ $ds = new ArrayDatasource(array_merge(
+ $this->fetchSharedNavigationItemConfigs($user->getUsername()),
+ $this->fetchUserNavigationItemConfigs($user->getUsername())
+ ));
+ $query = $ds->select();
+
+ $this->view->types = $this->listItemTypes();
+ $this->view->items = $query;
+
+ $this->view->title = $this->translate('Navigation');
+ $this->getTabs()
+ ->add(
+ 'account',
+ array(
+ 'title' => $this->translate('Update your account'),
+ 'label' => $this->translate('My Account'),
+ 'url' => 'account'
+ )
+ )
+ ->add(
+ 'navigation',
+ array(
+ 'active' => true,
+ 'title' => $this->translate('List and configure your own navigation items'),
+ 'label' => $this->translate('Navigation'),
+ 'url' => 'navigation'
+ )
+ )
+ ->add(
+ 'devices',
+ array(
+ 'title' => $this->translate('List of devices you are logged in'),
+ 'label' => $this->translate('My Devices'),
+ 'url' => 'my-devices'
+ )
+ );
+ $this->setupSortControl(
+ array(
+ 'type' => $this->translate('Type'),
+ 'owner' => $this->translate('Shared'),
+ 'name' => $this->translate('Navigation')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * List all shared navigation items
+ */
+ public function sharedAction()
+ {
+ $this->assertPermission('config/navigation');
+ $ds = new ArrayDatasource($this->fetchSharedNavigationItemConfigs());
+ $query = $ds->select();
+
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAttrib('class', 'inline');
+ $removeForm->addElement('hidden', 'name', array(
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('navigation/shared'),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Unshare this navigation item')
+ ));
+
+ $this->view->removeForm = $removeForm;
+ $this->view->types = $this->listItemTypes();
+ $this->view->items = $query;
+
+ $this->view->title = $this->translate('Shared Navigation');
+ $this->getTabs()->add(
+ 'navigation/shared',
+ array(
+ 'title' => $this->translate('List and configure shared navigation items'),
+ 'label' => $this->translate('Shared Navigation'),
+ 'url' => 'navigation/shared'
+ )
+ )->activate('navigation/shared');
+ $this->setupSortControl(
+ array(
+ 'type' => $this->translate('Type'),
+ 'owner' => $this->translate('Owner'),
+ 'name' => $this->translate('Shared Navigation')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Add a navigation item
+ */
+ public function addAction()
+ {
+ $form = new NavigationConfigForm();
+ $form->setRedirectUrl('navigation');
+ $form->setUser($this->Auth()->getUser());
+ $form->setItemTypes($this->listItemTypes());
+ $form->addDescription($this->translate('Create a new navigation item, such as a menu entry or dashlet.'));
+
+ // TODO: Fetch all "safe" parameters from the url and populate them
+ $form->setDefaultUrl(rawurldecode($this->params->get('url', '')));
+
+ $form->setOnSuccess(function (NavigationConfigForm $form) {
+ $data = $form::transformEmptyValuesToNull($form->getValues());
+
+ try {
+ $form->add($data);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ if ($data['type'] === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(t('Navigation item successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Navigation');
+ $this->renderForm($form, $this->translate('New Navigation Item'));
+ }
+
+ /**
+ * Edit a navigation item
+ */
+ public function editAction()
+ {
+ $itemName = $this->params->getRequired('name');
+ $itemType = $this->params->getRequired('type');
+ $referrer = $this->params->get('referrer', 'index');
+
+ $user = $this->Auth()->getUser();
+ if ($user->can('config/navigation')) {
+ $itemOwner = $this->params->get('owner', $user->getUsername());
+ } else {
+ $itemOwner = $user->getUsername();
+ }
+
+ $form = new NavigationConfigForm();
+ $form->setUser($user);
+ $form->setShareConfig(Config::navigation($itemType));
+ $form->setUserConfig(Config::navigation($itemType, $itemOwner));
+ $form->setRedirectUrl($referrer === 'shared' ? 'navigation/shared' : 'navigation');
+ $form->setOnSuccess(function (NavigationConfigForm $form) use ($itemName) {
+ $data = $form::transformEmptyValuesToNull($form->getValues());
+
+ try {
+ $form->edit($itemName, $data);
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ if (isset($data['type']) && $data['type'] === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(sprintf(t('Navigation item "%s" successfully updated'), $itemName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($itemName);
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $itemName));
+ }
+
+ $this->view->title = $this->translate('Navigation');
+ $this->renderForm($form, $this->translate('Update Navigation Item'));
+ }
+
+ /**
+ * Remove a navigation item
+ */
+ public function removeAction()
+ {
+ $itemName = $this->params->getRequired('name');
+ $itemType = $this->params->getRequired('type');
+ $user = $this->Auth()->getUser();
+
+ $navigationConfigForm = new NavigationConfigForm();
+ $navigationConfigForm->setUser($user);
+ $navigationConfigForm->setShareConfig(Config::navigation($itemType));
+ $navigationConfigForm->setUserConfig(Config::navigation($itemType, $user->getUsername()));
+
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('navigation');
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($itemName, $navigationConfigForm) {
+ try {
+ $itemConfig = $navigationConfigForm->delete($itemName);
+ } catch (NotFoundError $e) {
+ Notification::success(sprintf(t('Navigation Item "%s" not found. No action required'), $itemName));
+ return true;
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($navigationConfigForm->save()) {
+ if ($itemConfig->type === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(sprintf(t('Navigation Item "%s" successfully removed'), $itemName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Navigation');
+ $this->renderForm($form, $this->translate('Remove Navigation Item'));
+ }
+
+ /**
+ * Unshare a navigation item
+ */
+ public function unshareAction()
+ {
+ $this->assertPermission('config/navigation');
+ $this->assertHttpMethod('POST');
+
+ // TODO: I'd like these being form fields
+ $itemType = $this->params->getRequired('type');
+ $itemOwner = $this->params->getRequired('owner');
+
+ $navigationConfigForm = new NavigationConfigForm();
+ $navigationConfigForm->setUser($this->Auth()->getUser());
+ $navigationConfigForm->setShareConfig(Config::navigation($itemType));
+ $navigationConfigForm->setUserConfig(Config::navigation($itemType, $itemOwner));
+
+ $form = new Form(array(
+ 'onSuccess' => function ($form) use ($navigationConfigForm) {
+ $itemName = $form->getValue('name');
+
+ try {
+ $newConfig = $navigationConfigForm->unshare($itemName);
+ if ($navigationConfigForm->save()) {
+ if ($newConfig->getSection($itemName)->type === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(sprintf(
+ t('Navigation item "%s" has been unshared'),
+ $form->getValue('name')
+ ));
+ } else {
+ // TODO: It failed obviously to write one of the configs, so we're leaving the user in
+ // a inconsistent state. Luckily, it's nothing lost but possibly duplicated...
+ Notification::error(sprintf(
+ t('Failed to unshare navigation item "%s"'),
+ $form->getValue('name')
+ ));
+ }
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ Notification::error($e->getMessage());
+ }
+
+ $redirect = $form->getValue('redirect');
+ if (! empty($redirect)) {
+ $form->setRedirectUrl(htmlspecialchars_decode($redirect));
+ }
+
+ return true;
+ }
+ ));
+ $form->setUidDisabled();
+ $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called
+ $form->addElement('hidden', 'name', array('required' => true));
+ $form->addElement('hidden', 'redirect');
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $form->getValue('name')));
+ }
+ }
+
+ public function dashboardAction()
+ {
+ $name = $this->params->getRequired('name');
+
+ $this->getTabs()->add('dashboard', array(
+ 'active' => true,
+ 'label' => ucwords($name),
+ 'url' => Url::fromRequest()
+ ));
+
+ $menu = new Menu();
+
+ $navigation = $menu->findItem($name);
+
+ if ($navigation === null) {
+ $this->httpNotFound($this->translate('Navigation not found'));
+ }
+
+ $this->view->navigation = $navigation;
+ $this->view->title = $navigation->getLabel();
+ }
+}
diff --git a/application/controllers/RoleController.php b/application/controllers/RoleController.php
new file mode 100644
index 0000000..4223d33
--- /dev/null
+++ b/application/controllers/RoleController.php
@@ -0,0 +1,392 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Authentication\AdmissionLoader;
+use Icinga\Authentication\Auth;
+use Icinga\Authentication\RolesConfig;
+use Icinga\Authentication\User\DomainAwareInterface;
+use Icinga\Data\Selectable;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Security\RoleForm;
+use Icinga\Repository\Repository;
+use Icinga\Security\SecurityException;
+use Icinga\User;
+use Icinga\Web\Controller\AuthBackendController;
+use Icinga\Web\View\PrivilegeAudit;
+use Icinga\Web\Widget\SingleValueSearchControl;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Link;
+
+/**
+ * Manage user permissions and restrictions based on roles
+ *
+ * @TODO(el): Rename to RolesController: https://dev.icinga.com/issues/10015
+ */
+class RoleController extends AuthBackendController
+{
+ public function init()
+ {
+ $this->assertPermission('config/access-control/roles');
+ $this->view->title = $this->translate('Roles');
+
+ parent::init();
+ }
+
+ public function indexAction()
+ {
+ if ($this->hasPermission('config/access-control/roles')) {
+ $this->redirectNow('role/list');
+ } elseif ($this->hasPermission('config/access-control/users')) {
+ $this->redirectNow('user/list');
+ } elseif ($this->hasPermission('config/access-control/groups')) {
+ $this->redirectNow('group/list');
+ } else {
+ throw new SecurityException('No permission to configure Icinga Web 2');
+ }
+ }
+
+ /**
+ * List roles
+ *
+ * @TODO(el): Rename to indexAction()
+ */
+ public function listAction()
+ {
+ $this->createListTabs()->activate('role/list');
+ $this->view->roles = (new RolesConfig())
+ ->select();
+
+ $sortAndFilterColumns = [
+ 'name' => $this->translate('Name'),
+ 'users' => $this->translate('Users'),
+ 'groups' => $this->translate('Groups'),
+ 'permissions' => $this->translate('Permissions')
+ ];
+
+ $this->setupFilterControl($this->view->roles, $sortAndFilterColumns, ['name']);
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->roles);
+ $this->setupSortControl($sortAndFilterColumns, $this->view->roles, ['name']);
+ }
+
+ /**
+ * Create a new role
+ *
+ * @TODO(el): Rename to newAction()
+ */
+ public function addAction()
+ {
+ $role = new RoleForm();
+ $role->setRedirectUrl('__CLOSE__');
+ $role->setRepository(new RolesConfig());
+ $role->setSubmitLabel($this->translate('Create Role'));
+ $role->add()->handleRequest();
+
+ $this->renderForm($role, $this->translate('New Role'));
+ }
+
+ /**
+ * Update a role
+ *
+ * @TODO(el): Rename to updateAction()
+ */
+ public function editAction()
+ {
+ $name = $this->params->getRequired('role');
+ $role = new RoleForm();
+ $role->setRedirectUrl('__CLOSE__');
+ $role->setRepository(new RolesConfig());
+ $role->setSubmitLabel($this->translate('Update Role'));
+ $role->edit($name);
+
+ try {
+ $role->handleRequest();
+ } catch (NotFoundError $e) {
+ $this->httpNotFound($this->translate('Role not found'));
+ }
+
+ $this->renderForm($role, $this->translate('Update Role'));
+ }
+
+ /**
+ * Remove a role
+ */
+ public function removeAction()
+ {
+ $name = $this->params->getRequired('role');
+ $role = new RoleForm();
+ $role->setRedirectUrl('__CLOSE__');
+ $role->setRepository(new RolesConfig());
+ $role->setSubmitLabel($this->translate('Remove Role'));
+ $role->remove($name);
+
+ try {
+ $role->handleRequest();
+ } catch (NotFoundError $e) {
+ $this->httpNotFound($this->translate('Role not found'));
+ }
+
+ $this->renderForm($role, $this->translate('Remove Role'));
+ }
+
+ public function auditAction()
+ {
+ $this->createListTabs()->activate('role/audit');
+ $this->view->title = t('Audit');
+
+ $roleName = $this->params->get('role');
+ $type = $this->params->has('group') ? 'group' : 'user';
+ $name = $this->params->get($type);
+
+ $backend = null;
+ if ($type === 'user') {
+ if ($name) {
+ $backend = $this->params->getRequired('backend');
+ } else {
+ $backends = $this->loadUserBackends();
+ if (! empty($backends)) {
+ $backend = array_shift($backends)->getName();
+ }
+ }
+ }
+
+ $form = new SingleValueSearchControl();
+ $form->setMetaDataNames('type', 'backend');
+ $form->populate(['q' => $name, 'q-type' => $type, 'q-backend' => $backend]);
+ $form->setInputLabel(t('Enter user or group name'));
+ $form->setSubmitLabel(t('Inspect'));
+ $form->setSuggestionUrl(Url::fromPath(
+ 'role/suggest-role-member',
+ ['_disableLayout' => true, 'showCompact' => true]
+ ));
+
+ $form->on(SingleValueSearchControl::ON_SUCCESS, function ($form) {
+ $type = $form->getValue('q-type') ?: 'user';
+ $params = [$type => $form->getValue('q')];
+
+ if ($type === 'user') {
+ $params['backend'] = $form->getValue('q-backend');
+ }
+
+ $this->redirectNow(Url::fromPath('role/audit', $params));
+ })->handleRequest(ServerRequest::fromGlobals());
+
+ $this->addControl($form);
+
+ if (! $name) {
+ $this->addContent(Html::wantHtml(t('No user or group selected.')));
+ return;
+ }
+
+ if ($type === 'user') {
+ $header = Html::tag('h2', sprintf(t('Privilege Audit for User "%s"'), $name));
+
+ $user = new User($name);
+ $user->setAdditional('backend_name', $backend);
+ Auth::getInstance()->setupUser($user);
+ } else {
+ $header = Html::tag('h2', sprintf(t('Privilege Audit for Group "%s"'), $name));
+
+ $user = new User((string) time());
+ $user->setGroups([$name]);
+ (new AdmissionLoader())->applyRoles($user);
+ }
+
+ $chosenRole = null;
+ $assignedRoles = array_filter($user->getRoles(), function ($role) use ($user, &$chosenRole, $roleName) {
+ if (! in_array($role->getName(), $user->getAdditional('assigned_roles'), true)) {
+ return false;
+ }
+
+ if ($role->getName() === $roleName) {
+ $chosenRole = $role;
+ }
+
+ return true;
+ });
+
+ $this->addControl(Html::tag(
+ 'ul',
+ ['class' => 'privilege-audit-role-control'],
+ [
+ Html::tag('li', $roleName ? null : ['class' => 'active'], new Link(
+ t('All roles'),
+ Url::fromRequest()->without('role'),
+ ['class' => 'button-link', 'title' => t('Show privileges of all roles')]
+ )),
+ array_map(function ($role) use ($roleName) {
+ return Html::tag(
+ 'li',
+ $role->getName() === $roleName ? ['class' => 'active'] : null,
+ new Link(
+ $role->getName(),
+ Url::fromRequest()->setParam('role', $role->getName()),
+ [
+ 'class' => 'button-link',
+ 'title' => sprintf(t('Only show privileges of role %s'), $role->getName())
+ ]
+ )
+ );
+ }, $assignedRoles)
+ ]
+ ));
+
+ $this->addControl($header);
+ $this->addContent(
+ (new PrivilegeAudit($chosenRole !== null ? [$chosenRole] : $assignedRoles))
+ ->addAttributes(['id' => 'role-audit'])
+ );
+ }
+
+ public function suggestRoleMemberAction()
+ {
+ $this->assertHttpMethod('POST');
+ $requestData = $this->getRequest()->getPost();
+ $limit = $this->params->get('limit', 50);
+
+ $searchTerm = $requestData['term']['label'];
+ $userBackends = $this->loadUserBackends(Selectable::class);
+
+ $suggestions = [];
+ while ($limit > 0 && ! empty($userBackends)) {
+ /** @var Repository $backend */
+ $backend = array_shift($userBackends);
+ $query = $backend->select()
+ ->from('user', ['user_name'])
+ ->where('user_name', $searchTerm)
+ ->limit($limit);
+
+ try {
+ $names = $query->fetchColumn();
+ } catch (Exception $e) {
+ continue;
+ }
+
+ $domain = '';
+ if ($backend instanceof DomainAwareInterface) {
+ $domain = '@' . $backend->getDomain();
+ }
+
+ $users = [];
+ foreach ($names as $name) {
+ $users[] = [$name . $domain, [
+ 'type' => 'user',
+ 'backend' => $backend->getName()
+ ]];
+ }
+
+ if (! empty($users)) {
+ $suggestions[] = [
+ [
+ t('Users'),
+ HtmlString::create('&nbsp;'),
+ Html::tag('span', ['class' => 'badge'], $backend->getName())
+ ],
+ $users
+ ];
+ }
+
+ $limit -= count($names);
+ }
+
+ $groupBackends = $this->loadUserGroupBackends(Selectable::class);
+
+ while ($limit > 0 && ! empty($groupBackends)) {
+ /** @var Repository $backend */
+ $backend = array_shift($groupBackends);
+ $query = $backend->select()
+ ->from('group', ['group_name'])
+ ->where('group_name', $searchTerm)
+ ->limit($limit);
+
+ try {
+ $names = $query->fetchColumn();
+ } catch (Exception $e) {
+ continue;
+ }
+
+ $groups = [];
+ foreach ($names as $name) {
+ $groups[] = [$name, ['type' => 'group']];
+ }
+
+ if (! empty($groups)) {
+ $suggestions[] = [
+ [
+ t('Groups'),
+ HtmlString::create('&nbsp;'),
+ Html::tag('span', ['class' => 'badge'], $backend->getName())
+ ],
+ $groups
+ ];
+ }
+
+ $limit -= count($names);
+ }
+
+ if (empty($suggestions)) {
+ $suggestions[] = [t('Your search does not match any user or group'), []];
+ }
+
+ $this->document->add(SingleValueSearchControl::createSuggestions($suggestions));
+ }
+
+ /**
+ * Create the tabs to display when listing roles
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'role/list',
+ array(
+ 'baseTarget' => '_main',
+ 'label' => $this->translate('Roles'),
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'url' => 'role/list'
+ )
+ );
+
+ $tabs->add(
+ 'role/audit',
+ [
+ 'title' => $this->translate('Audit a user\'s or group\'s privileges'),
+ 'label' => $this->translate('Audit'),
+ 'url' => 'role/audit',
+ 'baseTarget' => '_main'
+ ]
+ );
+
+ if ($this->hasPermission('config/access-control/users')) {
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'user/list'
+ )
+ );
+ }
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'group/list'
+ )
+ );
+ }
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/SearchController.php b/application/controllers/SearchController.php
new file mode 100644
index 0000000..92aeabe
--- /dev/null
+++ b/application/controllers/SearchController.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Widget;
+use Icinga\Web\Widget\SearchDashboard;
+
+/**
+ * Search controller
+ */
+class SearchController extends ActionController
+{
+ public function indexAction()
+ {
+ $searchDashboard = new SearchDashboard();
+ $searchDashboard->setUser($this->Auth()->getUser());
+ $this->view->dashboard = $searchDashboard->search($this->params->get('q'));
+
+ // NOTE: This renders the dashboard twice. Remove this once we can catch exceptions thrown in view scripts.
+ $this->view->dashboard->render();
+ }
+
+ public function hintAction()
+ {
+ }
+}
diff --git a/application/controllers/StaticController.php b/application/controllers/StaticController.php
new file mode 100644
index 0000000..44a807a
--- /dev/null
+++ b/application/controllers/StaticController.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Controller;
+use Icinga\Web\FileCache;
+
+/**
+ * Deliver static content to clients
+ */
+class StaticController extends Controller
+{
+ /**
+ * Static routes don't require authentication
+ *
+ * @var bool
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * Disable layout rendering as this controller doesn't provide any html layouts
+ */
+ public function init()
+ {
+ $this->_helper->viewRenderer->setNoRender(true);
+ $this->_helper->layout()->disableLayout();
+ }
+
+ /**
+ * Return an image from a module's public folder
+ */
+ public function imgAction()
+ {
+ $imgRoot = Icinga::app()
+ ->getModuleManager()
+ ->getModule($this->getParam('module_name'))
+ ->getBaseDir() . '/public/img/';
+
+ $file = $this->getParam('file');
+ $filePath = realpath($imgRoot . $file);
+
+ if ($filePath === false || substr($filePath, 0, strlen($imgRoot)) !== $imgRoot) {
+ $this->httpNotFound('%s does not exist', $file);
+ }
+
+ if (preg_match('/\.([a-z]+)$/i', $file, $m)) {
+ $extension = $m[1];
+ if ($extension === 'svg') {
+ $extension = 'svg+xml';
+ }
+ } else {
+ $extension = 'fixme';
+ }
+
+ $s = stat($filePath);
+ $eTag = sprintf('%x-%x-%x', $s['ino'], $s['size'], (float) str_pad((string) $s['mtime'], 16, '0'));
+
+ $this->getResponse()->setHeader(
+ 'Cache-Control',
+ 'public, max-age=1814400, stale-while-revalidate=604800',
+ true
+ );
+
+ if ($this->getRequest()->getServer('HTTP_IF_NONE_MATCH') === $eTag) {
+ $this->getResponse()
+ ->setHttpResponseCode(304);
+ } else {
+ $this->getResponse()
+ ->setHeader('ETag', $eTag)
+ ->setHeader('Content-Type', 'image/' . $extension, true)
+ ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $s['mtime']) . ' GMT');
+
+ readfile($filePath);
+ }
+ }
+}
diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
new file mode 100644
index 0000000..dac80d3
--- /dev/null
+++ b/application/controllers/UserController.php
@@ -0,0 +1,374 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Authentication\AdmissionLoader;
+use Icinga\Authentication\User\DomainAwareInterface;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Config\User\CreateMembershipForm;
+use Icinga\Forms\Config\User\UserForm;
+use Icinga\User;
+use Icinga\Web\Controller\AuthBackendController;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget;
+
+class UserController extends AuthBackendController
+{
+ public function init()
+ {
+ $this->view->title = $this->translate('Users');
+
+ parent::init();
+ }
+
+ /**
+ * List all users of a single backend
+ */
+ public function listAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $this->createListTabs()->activate('user/list');
+ $backendNames = array_map(
+ function ($b) {
+ return $b->getName();
+ },
+ $this->loadUserBackends('Icinga\Data\Selectable')
+ );
+ if (empty($backendNames)) {
+ return;
+ }
+
+ $this->view->backendSelection = new Form();
+ $this->view->backendSelection->setAttrib('class', 'backend-selection icinga-controls');
+ $this->view->backendSelection->setUidDisabled();
+ $this->view->backendSelection->setMethod('GET');
+ $this->view->backendSelection->setTokenDisabled();
+ $this->view->backendSelection->addElement(
+ 'select',
+ 'backend',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Backend'),
+ 'multiOptions' => array_combine($backendNames, $backendNames),
+ 'value' => $this->params->get('backend')
+ )
+ );
+
+ $backend = $this->getUserBackend($this->params->get('backend'));
+ if ($backend === null) {
+ $this->view->backend = null;
+ return;
+ }
+
+ $query = $backend->select(array('user_name'));
+
+ $this->view->users = $query;
+ $this->view->backend = $backend;
+
+ $this->setupPaginationControl($query);
+ $this->setupFilterControl($query);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'user_name' => $this->translate('Username'),
+ 'is_active' => $this->translate('Active'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Show a user
+ */
+ public function showAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'));
+
+ $user = $backend->select(array(
+ 'user_name',
+ 'is_active',
+ 'created_at',
+ 'last_modified'
+ ))->where('user_name', $userName)->fetchRow();
+ if ($user === false) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $userObj = new User($userName);
+ if ($backend instanceof DomainAwareInterface) {
+ $userObj->setDomain($backend->getDomain());
+ }
+
+ $memberships = $this->loadMemberships($userObj)->select();
+
+ $this->setupFilterControl(
+ $memberships,
+ array('group_name' => t('User Group')),
+ array('group'),
+ array('user')
+ );
+ $this->setupPaginationControl($memberships);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'group_name' => $this->translate('Group')
+ ),
+ $memberships
+ );
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
+ $this->view->showCreateMembershipLink = ! empty($extensibleBackends);
+ } else {
+ $this->view->showCreateMembershipLink = false;
+ }
+
+ $this->view->user = $user;
+ $this->view->backend = $backend;
+ $this->view->memberships = $memberships;
+ $this->createShowTabs($backend->getName(), $userName)->activate('user/show');
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAttrib('class', 'inline');
+ $removeForm->addElement('hidden', 'user_name', array(
+ 'isArray' => true,
+ 'value' => $userName,
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('user/show', array(
+ 'backend' => $backend->getName(),
+ 'user' => $userName
+ )),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Cancel this membership')
+ ));
+ $this->view->removeForm = $removeForm;
+ }
+
+ $admissionLoader = new AdmissionLoader();
+ $admissionLoader->applyRoles($userObj);
+ $this->view->userObj = $userObj;
+ $this->view->allowedToEditRoles = $this->hasPermission('config/access-control/groups');
+ }
+
+ /**
+ * Add a user
+ */
+ public function addAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+ $form->add()->handleRequest();
+
+ $this->renderForm($form, $this->translate('New User'));
+ }
+
+ /**
+ * Edit a user
+ */
+ public function editAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
+
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)));
+ $form->setRepository($backend);
+
+ try {
+ $form->edit($userName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $this->renderForm($form, $this->translate('Update User'));
+ }
+
+ /**
+ * Remove a user
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+
+ try {
+ $form->remove($userName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $this->renderForm($form, $this->translate('Remove User'));
+ }
+
+ /**
+ * Create a membership for a user
+ */
+ public function createmembershipAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'));
+
+ if ($backend->select()->where('user_name', $userName)->count() === 0) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $backends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
+ if (empty($backends)) {
+ throw new ConfigurationError($this->translate(
+ 'You\'ll need to configure at least one user group backend first that allows to create new memberships'
+ ));
+ }
+
+ $form = new CreateMembershipForm();
+ $form->setBackends($backends)
+ ->setUsername($userName)
+ ->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)))
+ ->handleRequest();
+
+ $this->renderForm($form, $this->translate('Create New Membership'));
+ }
+
+ /**
+ * Fetch and return the given user's groups from all user group backends
+ *
+ * @param User $user
+ *
+ * @return ArrayDatasource
+ */
+ protected function loadMemberships(User $user)
+ {
+ $groups = $alreadySeen = array();
+ foreach ($this->loadUserGroupBackends() as $backend) {
+ try {
+ foreach ($backend->getMemberships($user) as $groupName) {
+ if (array_key_exists($groupName, $alreadySeen)) {
+ continue; // Ignore duplicate memberships
+ }
+
+ $alreadySeen[$groupName] = null;
+ $groups[] = (object) array(
+ 'group_name' => $groupName,
+ 'group' => $groupName,
+ 'backend' => $backend
+ );
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch memberships from backend %s. Please check your log'),
+ $backend->getName()
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+
+ /**
+ * Create the tabs to display when showing a user
+ *
+ * @param string $backendName
+ * @param string $userName
+ */
+ protected function createShowTabs($backendName, $userName)
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'user/show',
+ array(
+ 'title' => sprintf($this->translate('Show user %s'), $userName),
+ 'label' => $this->translate('User'),
+ 'url' => Url::fromPath('user/show', array('backend' => $backendName, 'user' => $userName))
+ )
+ );
+
+ return $tabs;
+ }
+
+ /**
+ * Create the tabs to display when listing users
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+
+ if ($this->hasPermission('config/access-control/roles')) {
+ $tabs->add(
+ 'role/list',
+ array(
+ 'baseTarget' => '_main',
+ 'label' => $this->translate('Roles'),
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'url' => 'role/list'
+ )
+ );
+
+ $tabs->add(
+ 'role/audit',
+ [
+ 'title' => $this->translate('Audit a user\'s or group\'s privileges'),
+ 'label' => $this->translate('Audit'),
+ 'url' => 'role/audit',
+ 'baseTarget' => '_main'
+ ]
+ );
+ }
+
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'user/list'
+ )
+ );
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'group/list'
+ )
+ );
+ }
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php
new file mode 100644
index 0000000..a96ab75
--- /dev/null
+++ b/application/controllers/UsergroupbackendController.php
@@ -0,0 +1,133 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Config\UserGroup\UserGroupBackendForm;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Notification;
+
+/**
+ * Controller to configure user group backends
+ */
+class UsergroupbackendController extends Controller
+{
+ /**
+ * Initialize this controller
+ */
+ public function init()
+ {
+ $this->assertPermission('config/access-control/users');
+ }
+
+ /**
+ * Redirect to this controller's list action
+ */
+ public function indexAction()
+ {
+ $this->redirectNow('config/userbackend');
+ }
+
+ /**
+ * Create a new user group backend
+ */
+ public function createAction()
+ {
+ $form = new UserGroupBackendForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->addDescription($this->translate('Create a new backend to associate users and groups with.'));
+ $form->setIniConfig(Config::app('groups'));
+ $form->setOnSuccess(function (UserGroupBackendForm $form) {
+ try {
+ $form->add($form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(t('User group backend successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('New User Group Backend'));
+ }
+
+ /**
+ * Edit an user group backend
+ */
+ public function editAction()
+ {
+ $backendName = $this->params->getRequired('backend');
+
+ $form = new UserGroupBackendForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setIniConfig(Config::app('groups'));
+ $form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) {
+ try {
+ $form->edit($backendName, $form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(sprintf(t('User group backend "%s" successfully updated'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($backendName);
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $backendName));
+ }
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Update User Group Backend'));
+ }
+
+ /**
+ * Remove a user group backend
+ */
+ public function removeAction()
+ {
+ $backendName = $this->params->getRequired('backend');
+
+ $backendForm = new UserGroupBackendForm();
+ $backendForm->setIniConfig(Config::app('groups'));
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) {
+ try {
+ $backendForm->delete($backendName);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($backendForm->save()) {
+ Notification::success(sprintf(t('User group backend "%s" successfully removed'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Remove User Group Backend'));
+ }
+}