From 3e02d5aff85babc3ffbfcf52313f2108e313aa23 Mon Sep 17 00:00:00 2001
From: Daniel Baumann
Date: Sat, 13 Apr 2024 13:46:43 +0200
Subject: Adding upstream version 2.12.1.
Signed-off-by: Daniel Baumann
---
application/VERSION | 1 +
application/clicommands/AutocompleteCommand.php | 120 +++
application/clicommands/HelpCommand.php | 43 +
application/clicommands/ModuleCommand.php | 228 ++++++
application/clicommands/VersionCommand.php | 55 ++
application/clicommands/WebCommand.php | 101 +++
application/controllers/AboutController.php | 27 +
application/controllers/AccountController.php | 83 ++
.../controllers/AnnouncementsController.php | 123 +++
.../controllers/ApplicationStateController.php | 95 +++
.../controllers/AuthenticationController.php | 127 +++
application/controllers/ConfigController.php | 518 ++++++++++++
application/controllers/DashboardController.php | 346 ++++++++
application/controllers/ErrorController.php | 176 +++++
application/controllers/GroupController.php | 418 ++++++++++
application/controllers/HealthController.php | 65 ++
application/controllers/IframeController.php | 20 +
application/controllers/IndexController.php | 36 +
application/controllers/LayoutController.php | 28 +
application/controllers/ListController.php | 59 ++
.../controllers/ManageUserDevicesController.php | 84 ++
application/controllers/MigrationsController.php | 249 ++++++
application/controllers/MyDevicesController.php | 74 ++
application/controllers/NavigationController.php | 447 +++++++++++
application/controllers/RoleController.php | 392 +++++++++
application/controllers/SearchController.php | 28 +
application/controllers/StaticController.php | 78 ++
application/controllers/UserController.php | 374 +++++++++
.../controllers/UsergroupbackendController.php | 133 ++++
application/fonts/fontello-ifont/LICENSE.txt | 57 ++
application/fonts/fontello-ifont/README.txt | 75 ++
application/fonts/fontello-ifont/config.json | 874 +++++++++++++++++++++
application/fonts/fontello-ifont/css/animation.css | 85 ++
.../fonts/fontello-ifont/css/ifont-codes.css | 145 ++++
.../fonts/fontello-ifont/css/ifont-embedded.css | 198 +++++
.../fonts/fontello-ifont/css/ifont-ie7-codes.css | 145 ++++
application/fonts/fontello-ifont/css/ifont-ie7.css | 156 ++++
application/fonts/fontello-ifont/css/ifont.css | 201 +++++
application/fonts/fontello-ifont/demo.html | 519 ++++++++++++
application/fonts/fontello-ifont/font/ifont.eot | Bin 0 -> 46504 bytes
application/fonts/fontello-ifont/font/ifont.svg | 298 +++++++
application/fonts/fontello-ifont/font/ifont.ttf | Bin 0 -> 46348 bytes
application/fonts/fontello-ifont/font/ifont.woff | Bin 0 -> 27688 bytes
application/fonts/fontello-ifont/font/ifont.woff2 | Bin 0 -> 22984 bytes
application/fonts/icingaweb.md | 9 +
application/forms/Account/ChangePasswordForm.php | 123 +++
.../AcknowledgeApplicationStateMessageForm.php | 75 ++
application/forms/ActionForm.php | 78 ++
.../Announcement/AcknowledgeAnnouncementForm.php | 92 +++
.../forms/Announcement/AnnouncementForm.php | 135 ++++
application/forms/Authentication/LoginForm.php | 214 +++++
application/forms/AutoRefreshForm.php | 83 ++
.../forms/Config/General/ApplicationConfigForm.php | 105 +++
.../DefaultAuthenticationDomainConfigForm.php | 46 ++
.../forms/Config/General/LoggingConfigForm.php | 142 ++++
.../forms/Config/General/ThemingConfigForm.php | 78 ++
application/forms/Config/GeneralConfigForm.php | 40 +
.../forms/Config/Resource/DbResourceForm.php | 239 ++++++
.../forms/Config/Resource/FileResourceForm.php | 67 ++
.../forms/Config/Resource/LdapResourceForm.php | 129 +++
.../forms/Config/Resource/SshResourceForm.php | 148 ++++
application/forms/Config/ResourceConfigForm.php | 442 +++++++++++
.../forms/Config/User/CreateMembershipForm.php | 192 +++++
application/forms/Config/User/UserForm.php | 210 +++++
.../forms/Config/UserBackend/DbBackendForm.php | 82 ++
.../Config/UserBackend/ExternalBackendForm.php | 83 ++
.../forms/Config/UserBackend/LdapBackendForm.php | 414 ++++++++++
application/forms/Config/UserBackendConfigForm.php | 482 ++++++++++++
.../forms/Config/UserBackendReorderForm.php | 86 ++
.../forms/Config/UserGroup/AddMemberForm.php | 183 +++++
.../Config/UserGroup/DbUserGroupBackendForm.php | 79 ++
.../Config/UserGroup/LdapUserGroupBackendForm.php | 370 +++++++++
.../Config/UserGroup/UserGroupBackendForm.php | 314 ++++++++
.../forms/Config/UserGroup/UserGroupForm.php | 158 ++++
application/forms/ConfigForm.php | 192 +++++
application/forms/ConfirmRemovalForm.php | 38 +
application/forms/Control/LimiterControlForm.php | 134 ++++
application/forms/Dashboard/DashletForm.php | 171 ++++
application/forms/LdapDiscoveryForm.php | 34 +
application/forms/MigrationForm.php | 143 ++++
application/forms/Navigation/DashletForm.php | 35 +
application/forms/Navigation/MenuItemForm.php | 31 +
.../forms/Navigation/NavigationConfigForm.php | 853 ++++++++++++++++++++
.../forms/Navigation/NavigationItemForm.php | 114 +++
application/forms/PreferenceForm.php | 485 ++++++++++++
application/forms/RepositoryForm.php | 453 +++++++++++
application/forms/Security/RoleForm.php | 632 +++++++++++++++
application/layouts/scripts/body.phtml | 98 +++
application/layouts/scripts/external-logout.phtml | 34 +
application/layouts/scripts/guest-error.phtml | 10 +
application/layouts/scripts/inline.phtml | 2 +
application/layouts/scripts/layout.phtml | 83 ++
application/layouts/scripts/parts/navigation.phtml | 35 +
application/layouts/scripts/pdf.phtml | 44 ++
application/views/helpers/CreateTicketLinks.php | 23 +
application/views/helpers/FormDate.php | 46 ++
application/views/helpers/FormDateTime.php | 63 ++
application/views/helpers/FormNumber.php | 77 ++
application/views/helpers/FormTime.php | 46 ++
application/views/helpers/ProtectId.php | 13 +
application/views/helpers/Util.php | 68 ++
application/views/scripts/about/index.phtml | 199 +++++
application/views/scripts/account/index.phtml | 11 +
.../views/scripts/announcements/index.phtml | 71 ++
.../views/scripts/authentication/login.phtml | 74 ++
.../views/scripts/authentication/logout.phtml | 64 ++
application/views/scripts/config/devtools.phtml | 6 +
application/views/scripts/config/general.phtml | 6 +
.../config/module-configuration-error.phtml | 28 +
application/views/scripts/config/module.phtml | 136 ++++
application/views/scripts/config/modules.phtml | 42 +
application/views/scripts/config/resource.phtml | 73 ++
.../views/scripts/config/resource/create.phtml | 6 +
.../views/scripts/config/resource/modify.phtml | 6 +
.../views/scripts/config/resource/remove.phtml | 6 +
.../views/scripts/config/userbackend/reorder.phtml | 75 ++
application/views/scripts/dashboard/error.phtml | 13 +
application/views/scripts/dashboard/index.phtml | 26 +
.../views/scripts/dashboard/new-dashlet.phtml | 6 +
.../views/scripts/dashboard/remove-dashlet.phtml | 6 +
.../views/scripts/dashboard/remove-pane.phtml | 6 +
.../views/scripts/dashboard/rename-pane.phtml | 6 +
application/views/scripts/dashboard/settings.phtml | 91 +++
.../views/scripts/dashboard/update-dashlet.phtml | 6 +
application/views/scripts/error/error.phtml | 106 +++
application/views/scripts/filter/index.phtml | 11 +
.../views/scripts/form/reorder-authbackend.phtml | 83 ++
application/views/scripts/group/form.phtml | 6 +
application/views/scripts/group/list.phtml | 96 +++
application/views/scripts/group/show.phtml | 108 +++
application/views/scripts/iframe/index.phtml | 8 +
application/views/scripts/index/welcome.phtml | 2 +
application/views/scripts/inline.phtml | 2 +
application/views/scripts/joystickPagination.phtml | 162 ++++
.../views/scripts/layout/announcements.phtml | 1 +
application/views/scripts/layout/menu.phtml | 20 +
.../views/scripts/list/applicationlog.phtml | 29 +
application/views/scripts/mixedPagination.phtml | 79 ++
.../views/scripts/navigation/dashboard.phtml | 27 +
application/views/scripts/navigation/index.phtml | 78 ++
application/views/scripts/navigation/shared.phtml | 68 ++
.../views/scripts/pivottablePagination.phtml | 48 ++
application/views/scripts/role/list.phtml | 65 ++
application/views/scripts/search/hint.phtml | 8 +
application/views/scripts/search/index.phtml | 7 +
application/views/scripts/showConfiguration.phtml | 27 +
application/views/scripts/simple-form.phtml | 6 +
application/views/scripts/user/form.phtml | 6 +
application/views/scripts/user/list.phtml | 90 +++
application/views/scripts/user/show.phtml | 138 ++++
150 files changed, 18380 insertions(+)
create mode 100644 application/VERSION
create mode 100644 application/clicommands/AutocompleteCommand.php
create mode 100644 application/clicommands/HelpCommand.php
create mode 100644 application/clicommands/ModuleCommand.php
create mode 100644 application/clicommands/VersionCommand.php
create mode 100644 application/clicommands/WebCommand.php
create mode 100644 application/controllers/AboutController.php
create mode 100644 application/controllers/AccountController.php
create mode 100644 application/controllers/AnnouncementsController.php
create mode 100644 application/controllers/ApplicationStateController.php
create mode 100644 application/controllers/AuthenticationController.php
create mode 100644 application/controllers/ConfigController.php
create mode 100644 application/controllers/DashboardController.php
create mode 100644 application/controllers/ErrorController.php
create mode 100644 application/controllers/GroupController.php
create mode 100644 application/controllers/HealthController.php
create mode 100644 application/controllers/IframeController.php
create mode 100644 application/controllers/IndexController.php
create mode 100644 application/controllers/LayoutController.php
create mode 100644 application/controllers/ListController.php
create mode 100644 application/controllers/ManageUserDevicesController.php
create mode 100644 application/controllers/MigrationsController.php
create mode 100644 application/controllers/MyDevicesController.php
create mode 100644 application/controllers/NavigationController.php
create mode 100644 application/controllers/RoleController.php
create mode 100644 application/controllers/SearchController.php
create mode 100644 application/controllers/StaticController.php
create mode 100644 application/controllers/UserController.php
create mode 100644 application/controllers/UsergroupbackendController.php
create mode 100644 application/fonts/fontello-ifont/LICENSE.txt
create mode 100644 application/fonts/fontello-ifont/README.txt
create mode 100644 application/fonts/fontello-ifont/config.json
create mode 100644 application/fonts/fontello-ifont/css/animation.css
create mode 100644 application/fonts/fontello-ifont/css/ifont-codes.css
create mode 100644 application/fonts/fontello-ifont/css/ifont-embedded.css
create mode 100644 application/fonts/fontello-ifont/css/ifont-ie7-codes.css
create mode 100644 application/fonts/fontello-ifont/css/ifont-ie7.css
create mode 100644 application/fonts/fontello-ifont/css/ifont.css
create mode 100644 application/fonts/fontello-ifont/demo.html
create mode 100644 application/fonts/fontello-ifont/font/ifont.eot
create mode 100644 application/fonts/fontello-ifont/font/ifont.svg
create mode 100644 application/fonts/fontello-ifont/font/ifont.ttf
create mode 100644 application/fonts/fontello-ifont/font/ifont.woff
create mode 100644 application/fonts/fontello-ifont/font/ifont.woff2
create mode 100644 application/fonts/icingaweb.md
create mode 100644 application/forms/Account/ChangePasswordForm.php
create mode 100644 application/forms/AcknowledgeApplicationStateMessageForm.php
create mode 100644 application/forms/ActionForm.php
create mode 100644 application/forms/Announcement/AcknowledgeAnnouncementForm.php
create mode 100644 application/forms/Announcement/AnnouncementForm.php
create mode 100644 application/forms/Authentication/LoginForm.php
create mode 100644 application/forms/AutoRefreshForm.php
create mode 100644 application/forms/Config/General/ApplicationConfigForm.php
create mode 100644 application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
create mode 100644 application/forms/Config/General/LoggingConfigForm.php
create mode 100644 application/forms/Config/General/ThemingConfigForm.php
create mode 100644 application/forms/Config/GeneralConfigForm.php
create mode 100644 application/forms/Config/Resource/DbResourceForm.php
create mode 100644 application/forms/Config/Resource/FileResourceForm.php
create mode 100644 application/forms/Config/Resource/LdapResourceForm.php
create mode 100644 application/forms/Config/Resource/SshResourceForm.php
create mode 100644 application/forms/Config/ResourceConfigForm.php
create mode 100644 application/forms/Config/User/CreateMembershipForm.php
create mode 100644 application/forms/Config/User/UserForm.php
create mode 100644 application/forms/Config/UserBackend/DbBackendForm.php
create mode 100644 application/forms/Config/UserBackend/ExternalBackendForm.php
create mode 100644 application/forms/Config/UserBackend/LdapBackendForm.php
create mode 100644 application/forms/Config/UserBackendConfigForm.php
create mode 100644 application/forms/Config/UserBackendReorderForm.php
create mode 100644 application/forms/Config/UserGroup/AddMemberForm.php
create mode 100644 application/forms/Config/UserGroup/DbUserGroupBackendForm.php
create mode 100644 application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
create mode 100644 application/forms/Config/UserGroup/UserGroupBackendForm.php
create mode 100644 application/forms/Config/UserGroup/UserGroupForm.php
create mode 100644 application/forms/ConfigForm.php
create mode 100644 application/forms/ConfirmRemovalForm.php
create mode 100644 application/forms/Control/LimiterControlForm.php
create mode 100644 application/forms/Dashboard/DashletForm.php
create mode 100644 application/forms/LdapDiscoveryForm.php
create mode 100644 application/forms/MigrationForm.php
create mode 100644 application/forms/Navigation/DashletForm.php
create mode 100644 application/forms/Navigation/MenuItemForm.php
create mode 100644 application/forms/Navigation/NavigationConfigForm.php
create mode 100644 application/forms/Navigation/NavigationItemForm.php
create mode 100644 application/forms/PreferenceForm.php
create mode 100644 application/forms/RepositoryForm.php
create mode 100644 application/forms/Security/RoleForm.php
create mode 100644 application/layouts/scripts/body.phtml
create mode 100644 application/layouts/scripts/external-logout.phtml
create mode 100644 application/layouts/scripts/guest-error.phtml
create mode 100644 application/layouts/scripts/inline.phtml
create mode 100644 application/layouts/scripts/layout.phtml
create mode 100644 application/layouts/scripts/parts/navigation.phtml
create mode 100644 application/layouts/scripts/pdf.phtml
create mode 100644 application/views/helpers/CreateTicketLinks.php
create mode 100644 application/views/helpers/FormDate.php
create mode 100644 application/views/helpers/FormDateTime.php
create mode 100644 application/views/helpers/FormNumber.php
create mode 100644 application/views/helpers/FormTime.php
create mode 100644 application/views/helpers/ProtectId.php
create mode 100644 application/views/helpers/Util.php
create mode 100644 application/views/scripts/about/index.phtml
create mode 100644 application/views/scripts/account/index.phtml
create mode 100644 application/views/scripts/announcements/index.phtml
create mode 100644 application/views/scripts/authentication/login.phtml
create mode 100644 application/views/scripts/authentication/logout.phtml
create mode 100644 application/views/scripts/config/devtools.phtml
create mode 100644 application/views/scripts/config/general.phtml
create mode 100644 application/views/scripts/config/module-configuration-error.phtml
create mode 100644 application/views/scripts/config/module.phtml
create mode 100644 application/views/scripts/config/modules.phtml
create mode 100644 application/views/scripts/config/resource.phtml
create mode 100644 application/views/scripts/config/resource/create.phtml
create mode 100644 application/views/scripts/config/resource/modify.phtml
create mode 100644 application/views/scripts/config/resource/remove.phtml
create mode 100644 application/views/scripts/config/userbackend/reorder.phtml
create mode 100644 application/views/scripts/dashboard/error.phtml
create mode 100644 application/views/scripts/dashboard/index.phtml
create mode 100644 application/views/scripts/dashboard/new-dashlet.phtml
create mode 100644 application/views/scripts/dashboard/remove-dashlet.phtml
create mode 100644 application/views/scripts/dashboard/remove-pane.phtml
create mode 100644 application/views/scripts/dashboard/rename-pane.phtml
create mode 100644 application/views/scripts/dashboard/settings.phtml
create mode 100644 application/views/scripts/dashboard/update-dashlet.phtml
create mode 100644 application/views/scripts/error/error.phtml
create mode 100644 application/views/scripts/filter/index.phtml
create mode 100644 application/views/scripts/form/reorder-authbackend.phtml
create mode 100644 application/views/scripts/group/form.phtml
create mode 100644 application/views/scripts/group/list.phtml
create mode 100644 application/views/scripts/group/show.phtml
create mode 100644 application/views/scripts/iframe/index.phtml
create mode 100644 application/views/scripts/index/welcome.phtml
create mode 100644 application/views/scripts/inline.phtml
create mode 100644 application/views/scripts/joystickPagination.phtml
create mode 100644 application/views/scripts/layout/announcements.phtml
create mode 100644 application/views/scripts/layout/menu.phtml
create mode 100644 application/views/scripts/list/applicationlog.phtml
create mode 100644 application/views/scripts/mixedPagination.phtml
create mode 100644 application/views/scripts/navigation/dashboard.phtml
create mode 100644 application/views/scripts/navigation/index.phtml
create mode 100644 application/views/scripts/navigation/shared.phtml
create mode 100644 application/views/scripts/pivottablePagination.phtml
create mode 100644 application/views/scripts/role/list.phtml
create mode 100644 application/views/scripts/search/hint.phtml
create mode 100644 application/views/scripts/search/index.phtml
create mode 100644 application/views/scripts/showConfiguration.phtml
create mode 100644 application/views/scripts/simple-form.phtml
create mode 100644 application/views/scripts/user/form.phtml
create mode 100644 application/views/scripts/user/list.phtml
create mode 100644 application/views/scripts/user/show.phtml
(limited to 'application')
diff --git a/application/VERSION b/application/VERSION
new file mode 100644
index 0000000..d963627
--- /dev/null
+++ b/application/VERSION
@@ -0,0 +1 @@
+cd2daeb2cb8537c633d343a29eb76c54cd2ebbf2 2023-11-15 12:50:13 +0100
diff --git a/application/clicommands/AutocompleteCommand.php b/application/clicommands/AutocompleteCommand.php
new file mode 100644
index 0000000..34e4005
--- /dev/null
+++ b/application/clicommands/AutocompleteCommand.php
@@ -0,0 +1,120 @@
+] [ []]
+ */
+class AutocompleteCommand extends Command
+{
+ protected $defaultActionName = 'complete';
+
+ protected function suggest($suggestions)
+ {
+ if ($suggestions) {
+ $key = array_search('autocomplete', $suggestions);
+ if ($key !== false) {
+ unset($suggestions[$key]);
+ }
+ echo implode("\n", $suggestions)
+ //. serialize($GLOBALS['argv'])
+ . "\n";
+ }
+ }
+
+ /**
+ * Show help for modules, commands and actions [default]
+ *
+ * The help command shows help for a given command, module and also for a
+ * given module's command or a specific command's action.
+ *
+ * Usage: icingacli autocomplete [] [ []]
+ */
+ public function completeAction()
+ {
+ $module = null;
+ $command = null;
+ $action = null;
+
+ $loader = new Loader($this->app);
+ $params = $this->params;
+ $bare_params = $GLOBALS['argv'];
+ $cword = (int) $params->shift('autoindex');
+
+ $search_word = $bare_params[$cword];
+ if ($search_word === '--') {
+ // TODO: Unfinished, completion missing
+ return $this->suggest(array('--verbose', '--help', '--debug'));
+ }
+
+ $search = $params->shift();
+ if (!$search) {
+ return $this->suggest(
+ array_merge($loader->listCommands(), $loader->listModules())
+ );
+ }
+ $found = $loader->resolveName($search);
+ if ($found) {
+ // Do not return suggestions if we are already on the next word:
+ if ($bare_params[$cword] === $search) {
+ return $this->suggest(array($found));
+ }
+ } else {
+ return $this->suggest($loader->getLastSuggestions());
+ }
+
+ $obj = null;
+ if ($loader->hasCommand($found)) {
+ $command = $found;
+ $obj = $loader->getCommandInstance($command);
+ } elseif ($loader->hasModule($found)) {
+ $module = $found;
+ $search = $params->shift();
+ if (! $search) {
+ return $this->suggest(
+ $loader->listModuleCommands($module)
+ );
+ }
+ $command = $loader->resolveModuleCommandName($found, $search);
+ if ($command) {
+ // Do not return suggestions if we are already on the next word:
+ if ($bare_params[$cword] === $search) {
+ return $this->suggest(array($command));
+ }
+ $obj = $loader->getModuleCommandInstance(
+ $module,
+ $command
+ );
+ } else {
+ return $this->suggest($loader->getLastSuggestions());
+ }
+ }
+
+ if ($obj !== null) {
+ $search = $params->shift();
+ if (! $search) {
+ return $this->suggest($obj->listActions());
+ }
+ $action = $loader->resolveObjectActionName(
+ $obj,
+ $search
+ );
+ if ($action) {
+ if ($bare_params[$cword] === $search) {
+ return $this->suggest(array($action));
+ }
+ } else {
+ return $this->suggest($loader->getLastSuggestions());
+ }
+ }
+ }
+}
diff --git a/application/clicommands/HelpCommand.php b/application/clicommands/HelpCommand.php
new file mode 100644
index 0000000..a863eb4
--- /dev/null
+++ b/application/clicommands/HelpCommand.php
@@ -0,0 +1,43 @@
+] [ []]
+ */
+class HelpCommand extends Command
+{
+ protected $defaultActionName = 'show';
+
+ /**
+ * Show help for modules, commands and actions [default]
+ *
+ * The help command shows help for a given command, module and also for a
+ * given module's command or a specific command's action.
+ *
+ * Usage: icingacli help [] [ []]
+ */
+ public function showAction()
+ {
+ $module = null;
+ $command = null;
+ $action = null;
+ $loader = new Loader($this->app);
+ $loader->parseParams();
+ echo $this->docs()->usage(
+ $loader->getModuleName(),
+ $loader->getCommandName(),
+ $loader->getActionName()
+ );
+ }
+}
diff --git a/application/clicommands/ModuleCommand.php b/application/clicommands/ModuleCommand.php
new file mode 100644
index 0000000..fc42167
--- /dev/null
+++ b/application/clicommands/ModuleCommand.php
@@ -0,0 +1,228 @@
+] []
+ */
+class ModuleCommand extends Command
+{
+ /**
+ * @var Manager
+ */
+ protected $modules;
+
+ public function init()
+ {
+ $this->modules = $this->app->getModuleManager();
+ }
+
+ /**
+ * List all enabled modules
+ *
+ * If you are interested in all installed modules pass 'installed' (or
+ * even --installed) as a command parameter. If you enable --verbose even
+ * more details will be shown
+ *
+ * Usage: icingacli module list [installed] [--verbose]
+ */
+ public function listAction()
+ {
+ if ($type = $this->params->shift()) {
+ if (! in_array($type, array('enabled', 'installed'))) {
+ return $this->showUsage();
+ }
+ } else {
+ $type = 'enabled';
+ $this->params->shift('enabled');
+ if ($this->params->shift('installed')) {
+ $type = 'installed';
+ }
+ }
+
+ if ($this->hasRemainingParams()) {
+ return $this->showUsage();
+ }
+
+ if ($type === 'enabled') {
+ $modules = $this->modules->listEnabledModules();
+ } else {
+ $modules = $this->modules->listInstalledModules();
+ }
+ if (empty($modules)) {
+ echo "There are no $type modules\n";
+ return;
+ }
+ if ($this->isVerbose) {
+ printf("%-14s %-9s %-9s DIRECTORY\n", 'MODULE', 'VERSION', 'STATE');
+ } else {
+ printf("%-14s %-9s %-9s %s\n", 'MODULE', 'VERSION', 'STATE', 'DESCRIPTION');
+ }
+ foreach ($modules as $module) {
+ $mod = $this->modules->loadModule($module)->getModule($module);
+ if ($this->isVerbose) {
+ $dir = ' ' . $this->modules->getModuleDir($module);
+ } else {
+ $dir = $mod->getTitle();
+ }
+ printf(
+ "%-14s %-9s %-9s %s\n",
+ $module,
+ $mod->getVersion(),
+ ($type === 'enabled' || $this->modules->hasEnabled($module))
+ ? $this->modules->hasInstalled($module) ? 'enabled' : 'dangling'
+ : 'disabled',
+ $dir
+ );
+ }
+ echo "\n";
+ }
+
+ /**
+ * Enable a given module
+ *
+ * Usage: icingacli module enable
+ */
+ public function enableAction()
+ {
+ if (! $module = $this->params->shift()) {
+ $module = $this->params->shift('module');
+ }
+
+ if (! $module || $this->hasRemainingParams()) {
+ return $this->showUsage();
+ }
+
+ $this->modules->enableModule($module, true);
+ }
+
+ /**
+ * Disable a given module
+ *
+ * Usage: icingacli module disable
+ */
+ public function disableAction()
+ {
+ if (! $module = $this->params->shift()) {
+ $module = $this->params->shift('module');
+ }
+ if (! $module || $this->hasRemainingParams()) {
+ return $this->showUsage();
+ }
+
+ if ($this->modules->hasEnabled($module)) {
+ $this->modules->disableModule($module);
+ } else {
+ Logger::info('Module "%s" is already disabled', $module);
+ }
+ }
+
+ /**
+ * Show all restrictions provided by your modules
+ *
+ * Asks each enabled module for all available restriction names and
+ * descriptions and shows a quick overview
+ *
+ * Usage: icingacli module restrictions
+ */
+ public function restrictionsAction()
+ {
+ printf("%-14s %-16s %s\n", 'MODULE', 'RESTRICTION', 'DESCRIPTION');
+ foreach ($this->modules->listEnabledModules() as $moduleName) {
+ $module = $this->modules->loadModule($moduleName)->getModule($moduleName);
+ foreach ($module->getProvidedRestrictions() as $restriction) {
+ printf(
+ "%-14s %-16s %s\n",
+ $moduleName,
+ $restriction->name,
+ $restriction->description
+ );
+ }
+ }
+ }
+
+ /**
+ * Show all permissions provided by your modules
+ *
+ * Asks each enabled module for it's available permission names and
+ * descriptions and shows a quick overview
+ *
+ * Usage: icingacli module permissions
+ */
+ public function permissionsAction()
+ {
+ printf("%-14s %-24s %s\n", 'MODULE', 'PERMISSION', 'DESCRIPTION');
+ foreach ($this->modules->listEnabledModules() as $moduleName) {
+ $module = $this->modules->loadModule($moduleName)->getModule($moduleName);
+ foreach ($module->getProvidedPermissions() as $restriction) {
+ printf(
+ "%-14s %-24s %s\n",
+ $moduleName,
+ $restriction->name,
+ $restriction->description
+ );
+ }
+ }
+ }
+
+ /**
+ * Search for a given module
+ *
+ * Does a lookup against your configured IcingaWeb app stores and tries to
+ * find modules matching your search string
+ *
+ * Usage: icingacli module search
+ */
+ public function searchAction()
+ {
+ $this->fail("Not implemented yet");
+ }
+
+ /**
+ * Install a given module
+ *
+ * Downloads a given module or installes a module from a given archive
+ *
+ * Usage: icingacli module install
+ * icingacli module install
+ */
+ public function installAction()
+ {
+ $this->fail("Not implemented yet");
+ }
+
+ /**
+ * Remove a given module
+ *
+ * Removes the given module from your disk. Module configuration will be
+ * preserved
+ *
+ * Usage: icingacli module remove
+ */
+ public function removeAction()
+ {
+ $this->fail("Not implemented yet");
+ }
+
+ /**
+ * Purge a given module
+ *
+ * Removes the given module from your disk. Also wipes configuration files
+ * and other data stored and/or generated by this module
+ *
+ * Usage: icingacli module remove
+ */
+ public function purgeAction()
+ {
+ $this->fail("Not implemented yet");
+ }
+}
diff --git a/application/clicommands/VersionCommand.php b/application/clicommands/VersionCommand.php
new file mode 100644
index 0000000..9bdd443
--- /dev/null
+++ b/application/clicommands/VersionCommand.php
@@ -0,0 +1,55 @@
+getModuleManager()->loadEnabledModules()->getLoadedModules();
+
+ $maxLength = 0;
+ foreach ($modules as $module) {
+ $length = strlen($module->getName());
+ if ($length > $maxLength) {
+ $maxLength = $length;
+ }
+ }
+
+ printf("%-{$maxLength}s %-9s \n", 'MODULE', 'VERSION');
+ foreach ($modules as $module) {
+ printf("%-{$maxLength}s %-9s \n", $module->getName(), $module->getVersion());
+ }
+ }
+}
diff --git a/application/clicommands/WebCommand.php b/application/clicommands/WebCommand.php
new file mode 100644
index 0000000..67d50a3
--- /dev/null
+++ b/application/clicommands/WebCommand.php
@@ -0,0 +1,101 @@
+]
+ *
+ * OPTIONS
+ *
+ * --daemonize Run in background
+ * --port= The port to listen on
+ * --listen= The address to listen on
+ * The document root directory of Icinga Web 2 (e.g. ./public)
+ *
+ * EXAMPLES
+ *
+ * icingacli web serve --port=8080
+ * icingacli web serve --listen=127.0.0.1:8080 ./public
+ */
+ public function serveAction()
+ {
+ $fork = $this->params->get('daemonize');
+ $listen = $this->params->get('listen');
+ $port = $this->params->get('port');
+ $documentRoot = $this->params->shift();
+ if ($listen === null) {
+ $socket = $port === null ? $this->params->shift() : '0.0.0.0:' . $port;
+ } else {
+ $socket = $listen;
+ }
+
+ if ($socket === null) {
+ $socket = $this->Config()->get('standalone', 'listen', '0.0.0.0:80');
+ }
+ if ($documentRoot === null) {
+ $documentRoot = Icinga::app()->getBaseDir('public');
+ if (! file_exists($documentRoot) || ! is_dir($documentRoot)) {
+ throw new IcingaException('Document root directory is required');
+ }
+ }
+ $documentRoot = realpath($documentRoot);
+
+ if ($fork) {
+ $this->forkAndExit();
+ }
+ echo "Serving Icinga Web 2 from directory $documentRoot and listening on $socket\n";
+
+ // TODO: Store webserver log, switch uid, log index.php includes, pid file
+ pcntl_exec(
+ readlink('/proc/self/exe'),
+ ['-S', $socket, '-t', $documentRoot, Icinga::app()->getLibraryDir('/Icinga/Application/webrouter.php')]
+ );
+ }
+
+ public function stopAction()
+ {
+ // TODO: No, that's NOT what we want
+ $prog = readlink('/proc/self/exe');
+ `killall $prog`;
+ }
+
+ protected function forkAndExit()
+ {
+ $pid = pcntl_fork();
+ if ($pid == -1) {
+ throw new IcingaException('Could not fork');
+ } elseif ($pid) {
+ echo $this->screen->colorize('[OK]')
+ . " Icinga Web server forked successfully\n";
+ fclose(STDIN);
+ fclose(STDOUT);
+ fclose(STDERR);
+ exit;
+ // pcntl_wait($status);
+ } else {
+ // child
+
+ // Replace console with /dev/null by first freeing the (lowest possible) FDs 0, 1 and 2
+ // and then opening /dev/null once for every one of them (open(2) chooses the lowest free FD).
+
+ fclose(STDIN);
+ fclose(STDOUT);
+ fclose(STDERR);
+
+ fopen('/dev/null', 'rb');
+ fopen('/dev/null', 'wb');
+ fopen('/dev/null', 'wb');
+ }
+ }
+}
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 @@
+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 @@
+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 @@
+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 @@
+_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 @@
+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 @@
+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 @@
+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 @@
+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 @@
+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 @@
+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 @@
+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 @@
+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 @@
+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 @@
+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' => '/(?[0-9]{4}(?:-[0-9]{2}){2}' // date
+ . 'T[0-9]{2}(?::[0-9]{2}){2}(?:[\+\-][0-9]{2}:[0-9]{2})?)' // time
+ . ' - (?[A-Za-z]+) - (?.*)(?!.)/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 @@
+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 @@
+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 $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 $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 @@
+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 @@
+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 @@
+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(' '),
+ 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(' '),
+ 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 @@
+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 @@
+_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 @@
+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 @@
+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'));
+ }
+}
diff --git a/application/fonts/fontello-ifont/LICENSE.txt b/application/fonts/fontello-ifont/LICENSE.txt
new file mode 100644
index 0000000..b4cfe3f
--- /dev/null
+++ b/application/fonts/fontello-ifont/LICENSE.txt
@@ -0,0 +1,57 @@
+Font license info
+
+
+## Font Awesome
+
+ Copyright (C) 2016 by Dave Gandy
+
+ Author: Dave Gandy
+ License: SIL ()
+ Homepage: http://fortawesome.github.com/Font-Awesome/
+
+
+## Iconic
+
+ Copyright (C) 2012 by P.J. Onori
+
+ Author: P.J. Onori
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://somerandomdude.com/work/iconic/
+
+
+## Entypo
+
+ Copyright (C) 2012 by Daniel Bruce
+
+ Author: Daniel Bruce
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://www.entypo.com
+
+
+## Fontelico
+
+ Copyright (C) 2012 by Fontello project
+
+ Author: Crowdsourced, for Fontello project
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://fontello.com
+
+
+## Typicons
+
+ (c) Stephen Hutchings 2012
+
+ Author: Stephen Hutchings
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://typicons.com/
+
+
+## MFG Labs
+
+ Copyright (C) 2012 by Daniel Bruce
+
+ Author: MFG Labs
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://www.mfglabs.com/
+
+
diff --git a/application/fonts/fontello-ifont/README.txt b/application/fonts/fontello-ifont/README.txt
new file mode 100644
index 0000000..beaab33
--- /dev/null
+++ b/application/fonts/fontello-ifont/README.txt
@@ -0,0 +1,75 @@
+This webfont is generated by http://fontello.com open source project.
+
+
+================================================================================
+Please, note, that you should obey original font licenses, used to make this
+webfont pack. Details available in LICENSE.txt file.
+
+- Usually, it's enough to publish content of LICENSE.txt file somewhere on your
+ site in "About" section.
+
+- If your project is open-source, usually, it will be ok to make LICENSE.txt
+ file publicly available in your repository.
+
+- Fonts, used in Fontello, don't require a clickable link on your site.
+ But any kind of additional authors crediting is welcome.
+================================================================================
+
+
+Comments on archive content
+---------------------------
+
+- /font/* - fonts in different formats
+
+- /css/* - different kinds of css, for all situations. Should be ok with
+ twitter bootstrap. Also, you can skip style and assign icon classes
+ directly to text elements, if you don't mind about IE7.
+
+- demo.html - demo file, to show your webfont content
+
+- LICENSE.txt - license info about source fonts, used to build your one.
+
+- config.json - keeps your settings. You can import it back into fontello
+ anytime, to continue your work
+
+
+Why so many CSS files ?
+-----------------------
+
+Because we like to fit all your needs :)
+
+- basic file, .css - is usually enough, it contains @font-face
+ and character code definitions
+
+- *-ie7.css - if you need IE7 support, but still don't wish to put char codes
+ directly into html
+
+- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face
+ rules, but still wish to benefit from css generation. That can be very
+ convenient for automated asset build systems. When you need to update font -
+ no need to manually edit files, just override old version with archive
+ content. See fontello source code for examples.
+
+- *-embedded.css - basic css file, but with embedded WOFF font, to avoid
+ CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain.
+ We strongly recommend to resolve this issue by `Access-Control-Allow-Origin`
+ server headers. But if you ok with dirty hack - this file is for you. Note,
+ that data url moved to separate @font-face to avoid problems with
+
+
+
+
+
+
+
+
+
+
+
icon-dashboard 0xe800
+
icon-user 0xe801
+
icon-users 0xe802
+
icon-ok 0xe803
+
+
+
icon-cancel 0xe804
+
icon-plus 0xe805
+
icon-minus 0xe806
+
icon-folder-empty 0xe807
+
+
+
icon-download 0xe808
+
icon-upload 0xe809
+
icon-git 0xe80a
+
icon-cubes 0xe80b
+
+
+
icon-database 0xe80c
+
icon-gauge 0xe80d
+
icon-sitemap 0xe80e
+
icon-sort-name-up 0xe80f
+
+
+
icon-sort-name-down 0xe810
+
icon-megaphone 0xe811
+
icon-bug 0xe812
+
icon-tasks 0xe813
+
+
+
icon-filter 0xe814
+
icon-off 0xe815
+
icon-book 0xe816
+
icon-paste 0xe817
+
+
+
icon-scissors 0xe818
+
icon-globe 0xe819
+
icon-cloud 0xe81a
+
icon-flash 0xe81b
+
+
+
icon-barchart 0xe81c
+
icon-down-dir 0xe81d
+
icon-up-dir 0xe81e
+
icon-left-dir 0xe81f
+
+
+
icon-right-dir 0xe820
+
icon-down-open 0xe821
+
icon-right-open 0xe822
+
icon-up-open 0xe823
+
+
+
icon-left-open 0xe824
+
icon-up-big 0xe825
+
icon-right-big 0xe826
+
icon-left-big 0xe827
+
+
+
icon-down-big 0xe828
+
icon-resize-full-alt 0xe829
+
icon-resize-full 0xe82a
+
icon-resize-small 0xe82b
+
+
+
icon-move 0xe82c
+
icon-resize-horizontal 0xe82d
+
icon-resize-vertical 0xe82e
+
icon-zoom-in 0xe82f
+
+
+
icon-block 0xe830
+
icon-zoom-out 0xe831
+
icon-lightbulb 0xe832
+
icon-clock 0xe833
+
+
+
icon-volume-up 0xe834
+
icon-volume-down 0xe835
+
icon-volume-off 0xe836
+
icon-mute 0xe837
+
+
+
icon-mic 0xe838
+
icon-endtime 0xe839
+
icon-starttime 0xe83a
+
icon-calendar-empty 0xe83b
+
+
+
icon-calendar 0xe83c
+
icon-wrench 0xe83d
+
icon-sliders 0xe83e
+
icon-services 0xe83f
+
+
+
icon-service 0xe840
+
icon-phone 0xe841
+
icon-file-pdf 0xe842
+
icon-file-word 0xe843
+
+
+
icon-file-excel 0xe844
+
icon-doc-text 0xe845
+
icon-trash 0xe846
+
icon-comment-empty 0xe847
+
+
+
icon-comment 0xe848
+
icon-chat 0xe849
+
icon-chat-empty 0xe84a
+
icon-bell 0xe84b
+
+
+
icon-bell-alt 0xe84c
+
icon-attention-alt 0xe84d
+
icon-print 0xe84e
+
icon-edit 0xe84f
+
+
+
icon-forward 0xe850
+
icon-reply 0xe851
+
icon-reply-all 0xe852
+
icon-eye 0xe853
+
+
+
icon-tag 0xe854
+
icon-tags 0xe855
+
icon-lock-open-alt 0xe856
+
icon-lock-open 0xe857
+
+
+
icon-lock 0xe858
+
icon-home 0xe859
+
icon-info 0xe85a
+
icon-help 0xe85b
+
+
+
icon-search 0xe85c
+
icon-flapping 0xe85d
+
icon-rewind 0xe85e
+
icon-chart-line 0xe85f
+
+
+
icon-bell-off 0xe860
+
icon-bell-off-empty 0xe861
+
icon-plug 0xe862
+
icon-eye-off 0xe863
+
+
+
icon-arrows-cw 0xe864
+
icon-cw 0xe865
+
icon-host 0xe866
+
icon-thumbs-up 0xe867
+
+
+
icon-thumbs-down 0xe868
+
icon-spinner 0xe869
+
icon-attach 0xe86a
+
icon-keyboard 0xe86b
+
+
+
icon-menu 0xe86c
+
icon-wifi 0xe86d
+
icon-moon 0xe86e
+
icon-chart-pie 0xe86f
+
+
+
icon-chart-area 0xe870
+
icon-chart-bar 0xe871
+
icon-beaker 0xe872
+
icon-magic 0xe873
+
+
+
icon-spin6 0xe874
+
icon-down-small 0xe875
+
icon-left-small 0xe876
+
icon-right-small 0xe877
+
+
+
icon-up-small 0xe878
+
icon-pin 0xe879
+
icon-angle-double-left 0xe87a
+
icon-angle-double-right 0xe87b
+
+
+
icon-circle 0xe87c
+
icon-info-circled 0xe87d
+
icon-twitter 0xe87e
+
icon-facebook-squared 0xe87f
+
+
+
icon-gplus-squared 0xe880
+
icon-attention-circled 0xe881
+
icon-check 0xe883
+
icon-reschedule 0xe884
+
+
+
icon-warning-empty 0xe885
+
icon-th-list 0xf009
+
icon-th-thumb-empty 0xf00b
+
icon-github-circled 0xf09b
+
+
+
icon-angle-double-up 0xf102
+
icon-angle-double-down 0xf103
+
icon-angle-left 0xf104
+
icon-angle-right 0xf105
+
+
+
icon-angle-up 0xf106
+
icon-angle-down 0xf107
+
icon-history 0xf1da
+
icon-binoculars 0xf1e5
+
+
+
+
+
\ No newline at end of file
diff --git a/application/fonts/fontello-ifont/font/ifont.eot b/application/fonts/fontello-ifont/font/ifont.eot
new file mode 100644
index 0000000..091db2f
Binary files /dev/null and b/application/fonts/fontello-ifont/font/ifont.eot differ
diff --git a/application/fonts/fontello-ifont/font/ifont.svg b/application/fonts/fontello-ifont/font/ifont.svg
new file mode 100644
index 0000000..9257938
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.svg
@@ -0,0 +1,298 @@
+
+
+
+Copyright (C) 2020 by original authors @ fontello.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/fonts/fontello-ifont/font/ifont.ttf b/application/fonts/fontello-ifont/font/ifont.ttf
new file mode 100644
index 0000000..2853b70
Binary files /dev/null and b/application/fonts/fontello-ifont/font/ifont.ttf differ
diff --git a/application/fonts/fontello-ifont/font/ifont.woff b/application/fonts/fontello-ifont/font/ifont.woff
new file mode 100644
index 0000000..d6d485d
Binary files /dev/null and b/application/fonts/fontello-ifont/font/ifont.woff differ
diff --git a/application/fonts/fontello-ifont/font/ifont.woff2 b/application/fonts/fontello-ifont/font/ifont.woff2
new file mode 100644
index 0000000..948e103
Binary files /dev/null and b/application/fonts/fontello-ifont/font/ifont.woff2 differ
diff --git a/application/fonts/icingaweb.md b/application/fonts/icingaweb.md
new file mode 100644
index 0000000..5699f07
--- /dev/null
+++ b/application/fonts/icingaweb.md
@@ -0,0 +1,9 @@
+# fontello-ifont font files moved
+
+New target is: public/font
+
+The font directory has been moved to the public structure because of
+Internet Explorer version 8 compatibility. The common way for browsers is to
+include the binary embeded font type in the javascript. IE8 falls back and
+include one of the provided font sources. Therefore it is important to have
+the font files available public and exported by the HTTP server.
diff --git a/application/forms/Account/ChangePasswordForm.php b/application/forms/Account/ChangePasswordForm.php
new file mode 100644
index 0000000..5bca11c
--- /dev/null
+++ b/application/forms/Account/ChangePasswordForm.php
@@ -0,0 +1,123 @@
+setSubmitLabel($this->translate('Update Account'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'password',
+ 'old_password',
+ array(
+ 'label' => $this->translate('Old Password'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_password',
+ array(
+ 'label' => $this->translate('New Password'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_password_confirmation',
+ array(
+ 'label' => $this->translate('Confirm New Password'),
+ 'required' => true,
+ 'validators' => array(
+ array('identical', false, array('new_password'))
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $backend = $this->getBackend();
+ $backend->update(
+ $backend->getBaseTable(),
+ array('password' => $this->getElement('new_password')->getValue()),
+ Filter::where('user_name', $this->Auth()->getUser()->getUsername())
+ );
+ Notification::success($this->translate('Account updated'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+ if (! $valid) {
+ return false;
+ }
+
+ $oldPasswordEl = $this->getElement('old_password');
+
+ if (! $this->backend->authenticate($this->Auth()->getUser(), $oldPasswordEl->getValue())) {
+ $oldPasswordEl->addError($this->translate('Old password is invalid'));
+ $this->markAsError();
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the user backend
+ *
+ * @return DbUserBackend
+ */
+ public function getBackend()
+ {
+ return $this->backend;
+ }
+
+ /**
+ * Set the user backend
+ *
+ * @param DbUserBackend $backend
+ *
+ * @return $this
+ */
+ public function setBackend(DbUserBackend $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+}
diff --git a/application/forms/AcknowledgeApplicationStateMessageForm.php b/application/forms/AcknowledgeApplicationStateMessageForm.php
new file mode 100644
index 0000000..61f5824
--- /dev/null
+++ b/application/forms/AcknowledgeApplicationStateMessageForm.php
@@ -0,0 +1,75 @@
+setAction(Url::fromPath('application-state/acknowledge-message'));
+ $this->setAttrib('class', 'application-state-acknowledge-message-control');
+ $this->setRedirectUrl('application-state/summary');
+ }
+
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ [
+ 'class' => 'link-button spinner',
+ 'decorators' => [
+ 'ViewHelper',
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']]
+ ],
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Acknowledge message'),
+ 'type' => 'submit'
+ ]
+ );
+ return $this;
+ }
+
+ public function createElements(array $formData = [])
+ {
+ $this->addElements(
+ [
+ [
+ 'hidden',
+ 'id',
+ [
+ 'required' => true,
+ 'validators' => ['NotEmpty'],
+ 'decorators' => ['ViewHelper']
+ ]
+ ]
+ ]
+ );
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $cookie = new ApplicationStateCookie();
+
+ $ack = $cookie->getAcknowledgedMessages();
+ $ack[] = $this->getValue('id');
+
+ $active = ApplicationStateHook::getAllMessages();
+
+ $cookie->setAcknowledgedMessages(array_keys(array_intersect_key($active, array_flip($ack))));
+
+ $this->getResponse()->setCookie($cookie);
+
+ return true;
+ }
+}
diff --git a/application/forms/ActionForm.php b/application/forms/ActionForm.php
new file mode 100644
index 0000000..5b5b6ed
--- /dev/null
+++ b/application/forms/ActionForm.php
@@ -0,0 +1,78 @@
+icon = (string) $name;
+ return $this;
+ }
+
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ $this->setUidDisabled(true);
+ $this->setDecorators(['FormElements', 'Form']);
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'hidden',
+ 'identifier',
+ [
+ 'required' => true,
+ 'decorators' => ['ViewHelper']
+ ]
+ );
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ [
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => ['ViewHelper'],
+ 'label' => $this->getView()->icon($this->icon),
+ 'title' => $this->getDescription()
+ ]
+ );
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid) {
+ $valid = ConfigFormEventsHook::runIsValid($this);
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ ConfigFormEventsHook::runOnSuccess($this);
+ }
+}
diff --git a/application/forms/Announcement/AcknowledgeAnnouncementForm.php b/application/forms/Announcement/AcknowledgeAnnouncementForm.php
new file mode 100644
index 0000000..85fecdc
--- /dev/null
+++ b/application/forms/Announcement/AcknowledgeAnnouncementForm.php
@@ -0,0 +1,92 @@
+setAction(Url::fromPath('announcements/acknowledge'));
+ $this->setAttrib('class', 'acknowledge-announcement-control');
+ $this->setRedirectUrl('layout/announcements');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'class' => 'link-button spinner',
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Acknowledge this announcement'),
+ 'type' => 'submit'
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElements(
+ array(
+ array(
+ 'hidden',
+ 'hash',
+ array(
+ 'required' => true,
+ 'validators' => array('NotEmpty'),
+ 'decorators' => array('ViewHelper')
+ )
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $cookie = new AnnouncementCookie();
+ $repo = new AnnouncementIniRepository();
+ $query = $repo->findActive();
+ $filter = array();
+ foreach ($cookie->getAcknowledged() as $hash) {
+ $filter[] = Filter::expression('hash', '=', $hash);
+ }
+ $query->addFilter(Filter::matchAny($filter));
+ $acknowledged = array();
+ foreach ($query as $row) {
+ $acknowledged[] = $row->hash;
+ }
+ $acknowledged[] = $this->getElement('hash')->getValue();
+ $cookie->setAcknowledged($acknowledged);
+ $this->getResponse()->setCookie($cookie);
+ return true;
+ }
+}
diff --git a/application/forms/Announcement/AnnouncementForm.php b/application/forms/Announcement/AnnouncementForm.php
new file mode 100644
index 0000000..4da47e2
--- /dev/null
+++ b/application/forms/Announcement/AnnouncementForm.php
@@ -0,0 +1,135 @@
+start !== null) {
+ $entry->start = (new DateTime())->setTimestamp($entry->start);
+ }
+ if ($entry->end !== null) {
+ $entry->end = (new DateTime())->setTimestamp($entry->end);
+ }
+ }
+
+ return $entry;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'author',
+ array(
+ 'disabled' => ! $this->getRequest()->isApiRequest(),
+ 'required' => true,
+ 'value' => Auth::getInstance()->getUser()->getUsername()
+ )
+ );
+ $this->addElement(
+ 'textarea',
+ 'message',
+ array(
+ 'description' => $this->translate('The message to display to users'),
+ 'label' => $this->translate('Message'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'dateTimePicker',
+ 'start',
+ array(
+ 'description' => $this->translate('The time to display the announcement from'),
+ 'label' => $this->translate('Start'),
+ 'placeholder' => new DateTime('tomorrow'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'dateTimePicker',
+ 'end',
+ array(
+ 'description' => $this->translate('The time to display the announcement until'),
+ 'label' => $this->translate('End'),
+ 'placeholder' => new DateTime('tomorrow +1day'),
+ 'required' => true
+ )
+ );
+
+ $this->setTitle($this->translate('Create a new announcement'));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+ /**
+ * {@inheritDoc}
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+ $this->setTitle(sprintf($this->translate('Edit announcement %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove announcement %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-controls');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createFilter()
+ {
+ return Filter::where('id', $this->getIdentifier());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getInsertMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement created')
+ : $this->translate('Failed to create announcement');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getUpdateMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement updated')
+ : $this->translate('Failed to update announcement');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDeleteMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement removed')
+ : $this->translate('Failed to remove announcement');
+ }
+}
diff --git a/application/forms/Authentication/LoginForm.php b/application/forms/Authentication/LoginForm.php
new file mode 100644
index 0000000..87b32ab
--- /dev/null
+++ b/application/forms/Authentication/LoginForm.php
@@ -0,0 +1,214 @@
+ '']],
+ ['Help', []],
+ ['Errors', ['separator' => '']],
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group']]
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setRequiredCue(null);
+ $this->setName('form_login');
+ $this->setSubmitLabel($this->translate('Login'));
+ $this->setProgressLabel($this->translate('Logging in'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'username',
+ array(
+ 'autocapitalize' => 'off',
+ 'autocomplete' => 'username',
+ 'class' => false === isset($formData['username']) ? 'autofocus' : '',
+ 'placeholder' => $this->translate('Username'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'autocomplete' => 'current-password',
+ 'placeholder' => $this->translate('Password'),
+ 'class' => isset($formData['username']) ? 'autofocus' : ''
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'rememberme',
+ [
+ 'label' => $this->translate('Stay logged in'),
+ 'decorators' => [
+ ['ViewHelper', ['separator' => '']],
+ ['Label', [
+ 'tag' => 'span',
+ 'separator' => '',
+ 'class' => 'control-label',
+ 'placement' => 'APPEND'
+ ]],
+ ['Help', []],
+ ['Errors', ['separator' => '']],
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group remember-me-box']]
+ ]
+ ]
+ );
+ if (! RememberMe::isSupported()) {
+ $this->getElement('rememberme')
+ ->setAttrib('disabled', true)
+ ->setDescription($this->translate(
+ 'Staying logged in requires a database configuration backend'
+ . ' and an appropriate OpenSSL encryption method'
+ ));
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'redirect',
+ array(
+ 'value' => Url::fromRequest()->getParam('redirect')
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrl()
+ {
+ $redirect = null;
+ if ($this->created) {
+ $redirect = $this->getElement('redirect')->getValue();
+ }
+
+ if (empty($redirect) || strpos($redirect, 'authentication/logout') !== false) {
+ $redirect = static::REDIRECT_URL;
+ }
+
+ $redirectUrl = Url::fromPath($redirect);
+ if ($redirectUrl->isExternal()) {
+ throw new HttpBadRequestException('nope');
+ }
+
+ return $redirectUrl;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $auth = Auth::getInstance();
+ $authChain = $auth->getAuthChain();
+ $authChain->setSkipExternalBackends(true);
+ $user = new User($this->getElement('username')->getValue());
+ if (! $user->hasDomain()) {
+ $user->setDomain(Config::app()->get('authentication', 'default_domain'));
+ }
+ $password = $this->getElement('password')->getValue();
+ $authenticated = $authChain->authenticate($user, $password);
+ if ($authenticated) {
+ $auth->setAuthenticated($user);
+ if ($this->getElement('rememberme')->isChecked()) {
+ try {
+ $rememberMe = RememberMe::fromCredentials($user->getUsername(), $password);
+ $this->getResponse()->setCookie($rememberMe->getCookie());
+ $rememberMe->persist();
+ } catch (Exception $e) {
+ Logger::error('Failed to let user "%s" stay logged in: %s', $user->getUsername(), $e);
+ }
+ }
+
+ // Call provided AuthenticationHook(s) after successful login
+ AuthenticationHook::triggerLogin($user);
+ $this->getResponse()->setRerenderLayout(true);
+ return true;
+ }
+ switch ($authChain->getError()) {
+ case $authChain::EEMPTY:
+ $this->addError($this->translate(
+ 'No authentication methods available.'
+ . ' Did you create authentication.ini when setting up Icinga Web 2?'
+ ));
+ break;
+ case $authChain::EFAIL:
+ $this->addError($this->translate(
+ 'All configured authentication methods failed.'
+ . ' Please check the system log or Icinga Web 2 log for more information.'
+ ));
+ break;
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case $authChain::ENOTALL:
+ $this->addError($this->translate(
+ 'Please note that not all authentication methods were available.'
+ . ' Check the system log or Icinga Web 2 log for more information.'
+ ));
+ // Move to default
+ default:
+ $this->getElement('password')->addError($this->translate('Incorrect username or password'));
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onRequest()
+ {
+ $auth = Auth::getInstance();
+ $onlyExternal = true;
+ // TODO(el): This may be set on the auth chain once iterated. See Auth::authExternal().
+ foreach ($auth->getAuthChain() as $backend) {
+ if (! $backend instanceof ExternalBackend) {
+ $onlyExternal = false;
+ }
+ }
+ if ($onlyExternal) {
+ $this->addError($this->translate(
+ 'You\'re currently not authenticated using any of the web server\'s authentication mechanisms.'
+ . ' Make sure you\'ll configure such, otherwise you\'ll not be able to login.'
+ ));
+ }
+ }
+}
diff --git a/application/forms/AutoRefreshForm.php b/application/forms/AutoRefreshForm.php
new file mode 100644
index 0000000..122f635
--- /dev/null
+++ b/application/forms/AutoRefreshForm.php
@@ -0,0 +1,83 @@
+setName('form_auto_refresh');
+ // Post against the current location
+ $this->setAction('');
+ }
+
+ /**
+ * Adjust preferences and persist them
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ /** @var Preferences $preferences */
+ $preferences = $this->getRequest()->getUser()->getPreferences();
+ $icingaweb = $preferences->get('icingaweb');
+
+ if ((bool) $preferences->getValue('icingaweb', 'auto_refresh', true) === false) {
+ $icingaweb['auto_refresh'] = '1';
+ $notification = $this->translate('Auto refresh successfully enabled');
+ } else {
+ $icingaweb['auto_refresh'] = '0';
+ $notification = $this->translate('Auto refresh successfully disabled');
+ }
+ $preferences->icingaweb = $icingaweb;
+
+ Session::getSession()->user->setPreferences($preferences);
+ Notification::success($notification);
+
+ $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes');
+ $this->setRedirectUrl(Url::fromRequest()->without('renderLayout'));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $preferences = $this->getRequest()->getUser()->getPreferences();
+
+ if ((bool) $preferences->getValue('icingaweb', 'auto_refresh', true) === false) {
+ $value = $this->translate('Enable auto refresh');
+ } else {
+ $value = $this->translate('Disable auto refresh');
+ }
+
+ $this->addElements(array(
+ array(
+ 'button',
+ 'btn_submit',
+ array(
+ 'ignore' => true,
+ 'type' => 'submit',
+ 'value' => $value,
+ 'decorators' => array('ViewHelper'),
+ 'escape' => false,
+ 'class' => 'link-like'
+ )
+ )
+ ));
+ }
+}
diff --git a/application/forms/Config/General/ApplicationConfigForm.php b/application/forms/Config/General/ApplicationConfigForm.php
new file mode 100644
index 0000000..21f76a1
--- /dev/null
+++ b/application/forms/Config/General/ApplicationConfigForm.php
@@ -0,0 +1,105 @@
+setName('form_config_general_application');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'global_show_stacktraces',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Stacktraces'),
+ 'description' => $this->translate(
+ 'Set whether to show an exception\'s stacktrace by default. This can also'
+ . ' be set in a user\'s preferences with the appropriate permission.'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'global_show_application_state_messages',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Application State Messages'),
+ 'description' => $this->translate(
+ "Set whether to show application state messages."
+ . " This can also be set in a user's preferences."
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'security_use_strict_csp',
+ [
+ 'label' => $this->translate('Enable strict content security policy'),
+ 'description' => $this->translate(
+ 'Set whether to to use strict content security policy (CSP).'
+ . ' This setting helps to protect from cross-site scripting (XSS).'
+ )
+ ]
+ );
+
+ $this->addElement(
+ 'text',
+ 'global_module_path',
+ array(
+ 'label' => $this->translate('Module Path'),
+ 'required' => true,
+ 'value' => implode(':', Icinga::app()->getModuleManager()->getModuleDirs()),
+ 'description' => $this->translate(
+ 'Contains the directories that will be searched for available modules, separated by '
+ . 'colons. Modules that don\'t exist in these directories can still be symlinked in '
+ . 'the module folder, but won\'t show up in the list of disabled modules.'
+ )
+ )
+ );
+
+ $backends = array_keys(ResourceFactory::getResourceConfigs()->toArray());
+ $backends = array_combine($backends, $backends);
+
+ $this->addElement(
+ 'select',
+ 'global_config_resource',
+ array(
+ 'required' => true,
+ 'multiOptions' => array_merge(
+ ['' => sprintf(' - %s - ', $this->translate('Please choose'))],
+ $backends
+ ),
+ 'disable' => [''],
+ 'value' => '',
+ 'label' => $this->translate('Configuration Database')
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
new file mode 100644
index 0000000..0ff6c32
--- /dev/null
+++ b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
@@ -0,0 +1,46 @@
+setName('form_config_general_authentication');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'authentication_default_domain',
+ array(
+ 'label' => $this->translate('Default Login Domain'),
+ 'description' => $this->translate(
+ 'If a user logs in without specifying any domain (e.g. "jdoe" instead of "jdoe@example.com"),'
+ . ' this default domain will be assumed for the user. Note that if none your LDAP authentication'
+ . ' backends are configured to be responsible for this domain or if none of your authentication'
+ . ' backends holds usernames with the domain part, users will not be able to login.'
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/LoggingConfigForm.php b/application/forms/Config/General/LoggingConfigForm.php
new file mode 100644
index 0000000..bbc7723
--- /dev/null
+++ b/application/forms/Config/General/LoggingConfigForm.php
@@ -0,0 +1,142 @@
+setName('form_config_general_logging');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $defaultType = getenv('ICINGAWEB_OFFICIAL_DOCKER_IMAGE') ? 'php' : 'syslog';
+
+ $this->addElement(
+ 'select',
+ 'logging_log',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Logging Type'),
+ 'description' => $this->translate('The type of logging to utilize.'),
+ 'value' => $defaultType,
+ 'multiOptions' => array(
+ 'syslog' => 'Syslog',
+ 'php' => $this->translate('Webserver Log', 'app.config.logging.type'),
+ 'file' => $this->translate('File', 'app.config.logging.type'),
+ 'none' => $this->translate('None', 'app.config.logging.type')
+ )
+ )
+ );
+
+ if (! isset($formData['logging_log']) || $formData['logging_log'] !== 'none') {
+ $this->addElement(
+ 'select',
+ 'logging_level',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Logging Level'),
+ 'description' => $this->translate('The maximum logging level to emit.'),
+ 'multiOptions' => array(
+ Logger::$levels[Logger::ERROR] => $this->translate('Error', 'app.config.logging.level'),
+ Logger::$levels[Logger::WARNING] => $this->translate('Warning', 'app.config.logging.level'),
+ Logger::$levels[Logger::INFO] => $this->translate('Information', 'app.config.logging.level'),
+ Logger::$levels[Logger::DEBUG] => $this->translate('Debug', 'app.config.logging.level')
+ )
+ )
+ );
+ }
+
+ if (! isset($formData['logging_log']) || in_array($formData['logging_log'], array('syslog', 'php'))) {
+ $this->addElement(
+ 'text',
+ 'logging_application',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Application Prefix'),
+ 'description' => $this->translate(
+ 'The name of the application by which to prefix log messages.'
+ ),
+ 'requirement' => $this->translate('The application prefix must not contain whitespace.'),
+ 'value' => 'icingaweb2',
+ 'validators' => array(
+ array(
+ 'Regex',
+ false,
+ array(
+ 'pattern' => '/^\S+$/',
+ 'messages' => array(
+ 'regexNotMatch' => $this->translate(
+ 'The application prefix must not contain whitespace.'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ if ((isset($formData['logging_log']) ? $formData['logging_log'] : $defaultType) === 'syslog') {
+ if (Platform::isWindows()) {
+ /* @see https://secure.php.net/manual/en/function.openlog.php */
+ $this->addElement(
+ 'hidden',
+ 'logging_facility',
+ array(
+ 'value' => 'user',
+ 'disabled' => true
+ )
+ );
+ } else {
+ $facilities = array_keys(SyslogWriter::$facilities);
+ $this->addElement(
+ 'select',
+ 'logging_facility',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Facility'),
+ 'description' => $this->translate('The syslog facility to utilize.'),
+ 'value' => 'user',
+ 'multiOptions' => array_combine($facilities, $facilities)
+ )
+ );
+ }
+ }
+ } elseif (isset($formData['logging_log']) && $formData['logging_log'] === 'file') {
+ $this->addElement(
+ 'text',
+ 'logging_file',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('File path'),
+ 'description' => $this->translate('The full path to the log file to write messages to.'),
+ 'value' => '/var/log/icingaweb2/icingaweb2.log',
+ 'validators' => array('WritablePathValidator')
+ )
+ );
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/ThemingConfigForm.php b/application/forms/Config/General/ThemingConfigForm.php
new file mode 100644
index 0000000..54ef2b1
--- /dev/null
+++ b/application/forms/Config/General/ThemingConfigForm.php
@@ -0,0 +1,78 @@
+setName('form_config_general_theming');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $themes = Icinga::app()->getThemes();
+ $themes[StyleSheet::DEFAULT_THEME] .= ' (' . $this->translate('default') . ')';
+
+ $this->addElement(
+ 'select',
+ 'themes_default',
+ array(
+ 'description' => $this->translate('The default theme', 'Form element description'),
+ 'disabled' => count($themes) < 2 ? 'disabled' : null,
+ 'label' => $this->translate('Default Theme', 'Form element label'),
+ 'multiOptions' => $themes,
+ 'value' => StyleSheet::DEFAULT_THEME
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'themes_disabled',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for disallowing users to change the theme. If a default theme is set, it will be'
+ . ' used nonetheless',
+ 'Form element description'
+ ),
+ 'label' => $this->translate('Users Can\'t Change Theme', 'Form element label')
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ if ($values['themes_default'] === '' || $values['themes_default'] === StyleSheet::DEFAULT_THEME) {
+ $values['themes_default'] = null;
+ }
+ if (! $values['themes_disabled']) {
+ $values['themes_disabled'] = null;
+ }
+ return $values;
+ }
+}
diff --git a/application/forms/Config/GeneralConfigForm.php b/application/forms/Config/GeneralConfigForm.php
new file mode 100644
index 0000000..5f15512
--- /dev/null
+++ b/application/forms/Config/GeneralConfigForm.php
@@ -0,0 +1,40 @@
+setName('form_config_general');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $appConfigForm = new ApplicationConfigForm();
+ $loggingConfigForm = new LoggingConfigForm();
+ $themingConfigForm = new ThemingConfigForm();
+ $domainConfigForm = new DefaultAuthenticationDomainConfigForm();
+ $this->addSubForm($appConfigForm->create($formData));
+ $this->addSubForm($loggingConfigForm->create($formData));
+ $this->addSubForm($themingConfigForm->create($formData));
+ $this->addSubForm($domainConfigForm->create($formData));
+ }
+}
diff --git a/application/forms/Config/Resource/DbResourceForm.php b/application/forms/Config/Resource/DbResourceForm.php
new file mode 100644
index 0000000..c9d7601
--- /dev/null
+++ b/application/forms/Config/Resource/DbResourceForm.php
@@ -0,0 +1,239 @@
+setName('form_config_resource_db');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $dbChoices = array();
+ if (Platform::hasMysqlSupport()) {
+ $dbChoices['mysql'] = 'MySQL';
+ }
+ if (Platform::hasPostgresqlSupport()) {
+ $dbChoices['pgsql'] = 'PostgreSQL';
+ }
+ if (Platform::hasMssqlSupport()) {
+ $dbChoices['mssql'] = 'MSSQL';
+ }
+ if (Platform::hasIbmSupport()) {
+ $dbChoices['ibm'] = 'IBM (DB2)';
+ }
+ if (Platform::hasOracleSupport()) {
+ $dbChoices['oracle'] = 'Oracle';
+ }
+ if (Platform::hasOciSupport()) {
+ $dbChoices['oci'] = 'Oracle (OCI8)';
+ }
+ if (Platform::hasSqliteSupport()) {
+ $dbChoices['sqlite'] = 'SQLite';
+ }
+
+ $offerPostgres = false;
+ $offerMysql = false;
+ $dbChoice = isset($formData['db']) ? $formData['db'] : key($dbChoices);
+ if ($dbChoice === 'pgsql') {
+ $offerPostgres = true;
+ } elseif ($dbChoice === 'mysql') {
+ $offerMysql = true;
+ }
+
+ if ($dbChoice === 'oracle' || $dbChoice === 'oci') {
+ $hostIsRequired = false;
+ } else {
+ $hostIsRequired = true;
+ }
+
+ $socketInfo = '';
+ if ($offerPostgres) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify the path to the unix domain socket directory'
+ );
+ } elseif ($offerMysql) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify localhost'
+ );
+ }
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'db',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Database Type'),
+ 'description' => $this->translate('The type of SQL database'),
+ 'multiOptions' => $dbChoices
+ )
+ );
+ if ($dbChoice === 'sqlite') {
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ } else {
+ $this->addElement(
+ 'text',
+ 'host',
+ array (
+ 'required' => $hostIsRequired,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate('The hostname of the database')
+ . ($socketInfo ? '. ' . $socketInfo : ''),
+ 'value' => $hostIsRequired ? 'localhost' : ''
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'description' => $this->translate('The port to use'),
+ 'label' => $this->translate('Port'),
+ 'preserveDefault' => true,
+ 'required' => $offerPostgres,
+ 'value' => $offerPostgres ? 5432 : null
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'username',
+ array (
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate('The user name to use for authentication')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'renderPassword' => true,
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password to use for authentication'),
+ 'autocomplete' => 'new-password'
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'charset',
+ array (
+ 'description' => $this->translate('The character set for the database'),
+ 'label' => $this->translate('Character Set')
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'use_ssl',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('Use SSL'),
+ 'description' => $this->translate(
+ 'Whether to encrypt the connection or to authenticate using certificates'
+ )
+ )
+ );
+ if (isset($formData['use_ssl']) && $formData['use_ssl']) {
+ if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
+ $this->addElement(
+ 'checkbox',
+ 'ssl_do_not_verify_server_cert',
+ array(
+ 'label' => $this->translate('SSL Do Not Verify Server Certificate'),
+ 'description' => $this->translate(
+ 'Whether to disable verification of the server certificate'
+ )
+ )
+ );
+ }
+ $this->addElement(
+ 'text',
+ 'ssl_key',
+ array(
+ 'label' => $this->translate('SSL Key'),
+ 'description' => $this->translate('The client key file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cert',
+ array(
+ 'label' => $this->translate('SSL Certificate'),
+ 'description' => $this->translate('The certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_ca',
+ array(
+ 'label' => $this->translate('SSL CA'),
+ 'description' => $this->translate('The CA certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_capath',
+ array(
+ 'label' => $this->translate('SSL CA Path'),
+ 'description' => $this->translate(
+ 'The trusted CA certificates in PEM format directory path'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cipher',
+ array(
+ 'label' => $this->translate('SSL Cipher'),
+ 'description' => $this->translate('The list of permissible ciphers')
+ )
+ );
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/FileResourceForm.php b/application/forms/Config/Resource/FileResourceForm.php
new file mode 100644
index 0000000..b98f1b4
--- /dev/null
+++ b/application/forms/Config/Resource/FileResourceForm.php
@@ -0,0 +1,67 @@
+setName('form_config_resource_file');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filename',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Filepath'),
+ 'description' => $this->translate('The filename to fetch information from'),
+ 'validators' => array('ReadablePathValidator')
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'fields',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Pattern'),
+ 'description' => $this->translate('The pattern by which to identify columns.'),
+ 'requirement' => $this->translate('The column pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/LdapResourceForm.php b/application/forms/Config/Resource/LdapResourceForm.php
new file mode 100644
index 0000000..7ffccdc
--- /dev/null
+++ b/application/forms/Config/Resource/LdapResourceForm.php
@@ -0,0 +1,129 @@
+setName('form_config_resource_ldap');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $defaultPort = ! array_key_exists('encryption', $formData) || $formData['encryption'] !== LdapConnection::LDAPS
+ ? 389
+ : 636;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'hostname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate(
+ 'The hostname or address of the LDAP server to use for authentication.'
+ . ' You can also provide multiple hosts separated by a space'
+ ),
+ 'value' => 'localhost'
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'required' => true,
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Port'),
+ 'description' => $this->translate('The port of the LDAP server to use for authentication'),
+ 'value' => $defaultPort
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'encryption',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Encryption'),
+ 'description' => $this->translate(
+ 'Whether to encrypt communication. Choose STARTTLS or LDAPS for encrypted communication or'
+ . ' none for unencrypted communication'
+ ),
+ 'multiOptions' => array(
+ 'none' => $this->translate('None', 'resource.ldap.encryption'),
+ LdapConnection::STARTTLS => 'STARTTLS',
+ LdapConnection::LDAPS => 'LDAPS'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'root_dn',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Root DN'),
+ 'description' => $this->translate(
+ 'Only the root and its child nodes will be accessible on this resource.'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'bind_dn',
+ array(
+ 'label' => $this->translate('Bind DN'),
+ 'description' => $this->translate(
+ 'The user dn to use for querying the ldap server. Leave the dn and password empty for attempting'
+ . ' an anonymous bind'
+ )
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'bind_pw',
+ array(
+ 'renderPassword' => true,
+ 'label' => $this->translate('Bind Password'),
+ 'description' => $this->translate('The password to use for querying the ldap server')
+ )
+ );
+
+ $this->addElement(
+ 'number',
+ 'timeout',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Timeout'),
+ 'description' => $this->translate('Connection timeout for every LDAP connection'),
+ 'value' => 5 // see LdapConnection::__construct()
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/SshResourceForm.php b/application/forms/Config/Resource/SshResourceForm.php
new file mode 100644
index 0000000..a15dc8c
--- /dev/null
+++ b/application/forms/Config/Resource/SshResourceForm.php
@@ -0,0 +1,148 @@
+setName('form_config_resource_ssh');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('User'),
+ 'description' => $this->translate(
+ 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be'
+ . ' possible for this user'
+ )
+ )
+ );
+
+ if ($this->getRequest()->getActionName() != 'editresource') {
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ if (substr(ltrim($value), 0, 7) === 'file://'
+ || openssl_pkey_get_private($value) === false
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('The given SSH key is invalid'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'private_key',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Private Key'),
+ 'description' => $this->translate('The private key which will be used for the SSH connections'),
+ 'class' => 'resource ssh-identity',
+ 'validators' => array($callbackValidator)
+ )
+ );
+ } else {
+ $resourceName = $formData['name'];
+ $this->addElement(
+ 'note',
+ 'private_key_note',
+ array(
+ 'escape' => false,
+ 'label' => $this->translate('Private Key'),
+ 'value' => sprintf(
+ '%3$s ',
+ $this->getView()->url('config/removeresource', array('resource' => $resourceName)),
+ $this->getView()->escape(sprintf($this->translate(
+ 'Remove the %s resource'
+ ), $resourceName)),
+ $this->translate('To modify the private key you must recreate this resource.')
+ )
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove the assigned key to the resource
+ *
+ * @param ConfigObject $config
+ *
+ * @return bool
+ */
+ public static function beforeRemove(ConfigObject $config)
+ {
+ $file = $config->private_key;
+
+ if (file_exists($file)) {
+ unlink($file);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Creates the assigned key to the resource
+ *
+ * @param ResourceConfigForm $form
+ *
+ * @return bool
+ */
+ public static function beforeAdd(ResourceConfigForm $form)
+ {
+ $configDir = Icinga::app()->getConfigDir();
+ $user = $form->getElement('user')->getValue();
+
+ $filePath = join(DIRECTORY_SEPARATOR, [$configDir, 'ssh', sha1($user)]);
+ if (! file_exists($filePath)) {
+ $file = File::create($filePath, 0600);
+ } else {
+ $form->error(
+ sprintf($form->translate('The private key for the user "%s" already exists.'), $user)
+ );
+ return false;
+ }
+
+ $file->fwrite($form->getElement('private_key')->getValue());
+
+ $form->getElement('private_key')->setValue($filePath);
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/ResourceConfigForm.php b/application/forms/Config/ResourceConfigForm.php
new file mode 100644
index 0000000..c2d0d18
--- /dev/null
+++ b/application/forms/Config/ResourceConfigForm.php
@@ -0,0 +1,442 @@
+setName('form_config_resource');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * Return a form object for the given resource type
+ *
+ * @param string $type The resource type for which to return a form
+ *
+ * @return Form
+ */
+ public function getResourceForm($type)
+ {
+ if ($type === 'db') {
+ return new DbResourceForm();
+ } elseif ($type === 'ldap') {
+ return new LdapResourceForm();
+ } elseif ($type === 'file') {
+ return new FileResourceForm();
+ } elseif ($type === 'ssh') {
+ return new SshResourceForm();
+ } else {
+ throw new InvalidArgumentException(sprintf($this->translate('Invalid resource type "%s" provided'), $type));
+ }
+ }
+
+ /**
+ * Add a particular resource
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $values The values to extend the configuration with
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case the resource does already exist
+ */
+ public function add(array $values)
+ {
+ $name = isset($values['name']) ? $values['name'] : '';
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif ($this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Resource already exists'));
+ }
+
+ unset($values['name']);
+ $this->config->setSection($name, $values);
+ return $this;
+ }
+
+ /**
+ * Edit a particular resource
+ *
+ * @param string $name The name of the resource to edit
+ * @param array $values The values to edit the configuration with
+ *
+ * @return ConfigObject The edited configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function edit($name, array $values)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Old resource name missing'));
+ } elseif (! ($newName = isset($values['name']) ? $values['name'] : '')) {
+ throw new InvalidArgumentException($this->translate('New resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $this->config->removeSection($name);
+ unset($values['name']);
+ $this->config->setSection($newName, $resourceConfig->merge($values));
+
+ if ($newName !== $name) {
+ $appConfig = Config::app();
+ $section = $appConfig->getSection('global');
+ if ($section->config_resource === $name) {
+ $section->config_resource = $newName;
+ $this->updatedAppConfig = $appConfig->setSection('global', $section);
+ }
+ }
+
+ return $resourceConfig;
+ }
+
+ /**
+ * Remove a particular resource
+ *
+ * @param string $name The name of the resource to remove
+ *
+ * @return ConfigObject The removed resource configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function remove($name)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $resourceForm = $this->getResourceForm($resourceConfig->type);
+ if (method_exists($resourceForm, 'beforeRemove')) {
+ $resourceForm::beforeRemove($resourceConfig);
+ }
+
+ $this->config->removeSection($name);
+ return $resourceConfig;
+ }
+
+ /**
+ * Add or edit a resource and save the configuration
+ *
+ * Performs a connectivity validation using the submitted values. A checkbox is
+ * added to the form to skip the check if it fails and redirection is aborted.
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $resourceForm = $this->getResourceForm($this->getElement('type')->getValue());
+
+ if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ $this->addElement($this->getForceCreationCheckbox());
+ return false;
+ }
+ }
+
+ $resource = $this->request->getQuery('resource');
+ try {
+ if ($resource === null) { // create new resource
+ if (method_exists($resourceForm, 'beforeAdd')) {
+ if (! $resourceForm::beforeAdd($this)) {
+ return false;
+ }
+ }
+ $this->add(static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully created');
+ } else { // edit existing resource
+ $this->edit($resource, static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully changed');
+ }
+ } catch (InvalidArgumentException $e) {
+ Notification::error($e->getMessage());
+ return false;
+ }
+
+ if ($this->save()) {
+ Notification::success(sprintf($message, $this->getElement('name')->getValue()));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Populate the form in case a resource is being edited
+ *
+ * @see Form::onRequest()
+ *
+ * @throws ConfigurationError In case the backend name is missing in the request or is invalid
+ */
+ public function onRequest()
+ {
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null) {
+ if ($resource === '') {
+ throw new ConfigurationError($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($resource)) {
+ throw new ConfigurationError($this->translate('Unknown resource provided'));
+ }
+ $configValues = $this->config->getSection($resource)->toArray();
+ $configValues['name'] = $resource;
+ $this->populate($configValues);
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password' && strlen($element->getValue())) {
+ $element->setValue(static::$dummyPassword);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a checkbox to be displayed at the beginning of the form
+ * which allows the user to skip the connection validation
+ *
+ * @return Zend_Form_Element
+ */
+ protected function getForceCreationCheckbox()
+ {
+ return $this->createElement(
+ 'checkbox',
+ 'force_creation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Force Changes'),
+ 'description' => $this->translate('Check this box to enforce changes without connectivity validation')
+ )
+ );
+ }
+
+ /**
+ * @see Form::createElemeents()
+ */
+ public function createElements(array $formData)
+ {
+ $resourceType = isset($formData['type']) ? $formData['type'] : 'db';
+
+ $resourceTypes = array(
+ 'file' => $this->translate('File'),
+ 'ssh' => $this->translate('SSH Identity'),
+ );
+ if ($resourceType === 'ldap' || Platform::hasLdapSupport()) {
+ $resourceTypes['ldap'] = 'LDAP';
+ }
+ if ($resourceType === 'db' || Platform::hasDatabaseSupport()) {
+ $resourceTypes['db'] = $this->translate('SQL Database');
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Resource Type'),
+ 'description' => $this->translate('The type of resource'),
+ 'multiOptions' => $resourceTypes,
+ 'value' => $resourceType
+ )
+ );
+
+ if (isset($formData['force_creation']) && $formData['force_creation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addElement($this->getForceCreationCheckbox());
+ }
+
+ $this->addElements($this->getResourceForm($resourceType)->createElements($formData)->getElements());
+ }
+
+ /**
+ * Create a resource by using the given form's values and return its inspection results
+ *
+ * @param Form $form
+ *
+ * @return ?Inspection
+ */
+ public static function inspectResource(Form $form)
+ {
+ if ($form->getValue('type') !== 'ssh') {
+ $resource = ResourceFactory::createResource(new ConfigObject($form->getValues()));
+ if ($resource instanceof Inspectable) {
+ return $resource->inspect();
+ }
+ }
+ }
+
+ /**
+ * Run the configured resource's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'resource_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if ($this->getElement('resource_validation')->isChecked() && parent::isValid($formData)) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '' . $this->translate('Validation Log') . " \n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'resource_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'resource-progress');
+ $this->addElement(
+ 'note',
+ 'resource-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'resource-progress'))
+ )
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('btn_submit', 'resource_validation', 'resource-progress'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null && $this->config->hasSection($resource)) {
+ $resourceConfig = $this->config->getSection($resource)->toArray();
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password') {
+ $name = $element->getName();
+ if (isset($values[$name]) && $values[$name] === static::$dummyPassword) {
+ if (isset($resourceConfig[$name])) {
+ $values[$name] = $resourceConfig[$name];
+ } else {
+ unset($values[$name]);
+ }
+ }
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function writeConfig(Config $config)
+ {
+ parent::writeConfig($config);
+ if ($this->updatedAppConfig !== null) {
+ $this->updatedAppConfig->saveIni();
+ }
+ }
+}
diff --git a/application/forms/Config/User/CreateMembershipForm.php b/application/forms/Config/User/CreateMembershipForm.php
new file mode 100644
index 0000000..6c74c8c
--- /dev/null
+++ b/application/forms/Config/User/CreateMembershipForm.php
@@ -0,0 +1,192 @@
+backends = $backends;
+ return $this;
+ }
+
+ /**
+ * Set the username to create memberships for
+ *
+ * @param string $userName
+ *
+ * @return $this
+ */
+ public function setUsername($userName)
+ {
+ $this->userName = $userName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $query = $this->createDataSource()->select()->from('group', array('group_name', 'backend_name'));
+
+ $options = array();
+ foreach ($query as $row) {
+ $options[$row->backend_name . ';' . $row->group_name] = $row->group_name . ' (' . $row->backend_name . ')';
+ }
+
+ $this->addElement(
+ 'multiselect',
+ 'groups',
+ array(
+ 'required' => true,
+ 'multiOptions' => $options,
+ 'label' => $this->translate('Groups'),
+ 'description' => sprintf(
+ $this->translate('Select one or more groups where to add %s as member'),
+ $this->userName
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Create memberships for %s'), $this->userName));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+
+ /**
+ * Instantly redirect back in case the user is already a member of all groups
+ */
+ public function onRequest()
+ {
+ if ($this->createDataSource()->select()->from('group')->count() === 0) {
+ Notification::info(sprintf($this->translate('User %s is already a member of all groups'), $this->userName));
+ $this->getResponse()->redirectAndExit($this->getRedirectUrl());
+ }
+ }
+
+ /**
+ * Create the memberships for the user
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $backendMap = array();
+ foreach ($this->backends as $backend) {
+ $backendMap[$backend->getName()] = $backend;
+ }
+
+ $single = null;
+ $groupName = null;
+ foreach ($this->getValue('groups') as $backendAndGroup) {
+ list($backendName, $groupName) = explode(';', $backendAndGroup, 2);
+ try {
+ $backendMap[$backendName]->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $groupName,
+ 'user_name' => $this->userName
+ )
+ );
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $this->userName,
+ $groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(
+ sprintf($this->translate('Membership for group %s created successfully'), $groupName)
+ );
+ } else {
+ Notification::success($this->translate('Memberships created successfully'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Create and return a data source to fetch all groups from all backends where the user is not already a member of
+ *
+ * @return ArrayDatasource
+ */
+ protected function createDataSource()
+ {
+ $groups = $failures = array();
+ foreach ($this->backends as $backend) {
+ try {
+ $memberships = $backend
+ ->select()
+ ->from('group_membership', array('group_name'))
+ ->where('user_name', $this->userName)
+ ->fetchColumn();
+ foreach ($backend->select(array('group_name')) as $row) {
+ if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter
+ $row->backend_name = $backend->getName();
+ $groups[] = $row;
+ }
+ }
+ } catch (Exception $e) {
+ $failures[] = array($backend->getName(), $e);
+ }
+ }
+
+ if (empty($groups) && !empty($failures)) {
+ // In case there are only failures, throw the very first exception again
+ throw $failures[0][1];
+ } elseif (! empty($failures)) {
+ foreach ($failures as $failure) {
+ Logger::error($failure[1]);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any groups from backend %s. Please check your log'),
+ $failure[0]
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+}
diff --git a/application/forms/Config/User/UserForm.php b/application/forms/Config/User/UserForm.php
new file mode 100644
index 0000000..fb2ef4d
--- /dev/null
+++ b/application/forms/Config/User/UserForm.php
@@ -0,0 +1,210 @@
+addElement(
+ 'checkbox',
+ 'is_active',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Active'),
+ 'description' => $this->translate('Prevents the user from logging in if unchecked')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Password')
+ )
+ );
+
+ $this->setTitle($this->translate('Add a new user'));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Create and add elements to this form to update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'description' => $this->translate('Leave empty for not updating the user\'s password'),
+ 'label' => $this->translate('Password'),
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Edit user %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * Update a user
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('user_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('user', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * Strips off the password if null or the empty string.
+ *
+ * @param bool $suppressArrayNotation
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ // before checking if password values is empty
+ // we have to check that the password field is set
+ // otherwise an error is thrown
+ if (isset($values['password']) && ! $values['password']) {
+ unset($values['password']);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Create and add elements to this form to delete a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove user %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a user
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('user_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('User added successfully');
+ } else {
+ return $this->translate('Failed to add user');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}
diff --git a/application/forms/Config/UserBackend/DbBackendForm.php b/application/forms/Config/UserBackend/DbBackendForm.php
new file mode 100644
index 0000000..693ea14
--- /dev/null
+++ b/application/forms/Config/UserBackend/DbBackendForm.php
@@ -0,0 +1,82 @@
+setName('form_config_authbackend_db');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate(
+ 'The database connection to use for authenticating with this provider'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array()
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'db'
+ )
+ );
+ }
+}
diff --git a/application/forms/Config/UserBackend/ExternalBackendForm.php b/application/forms/Config/UserBackend/ExternalBackendForm.php
new file mode 100644
index 0000000..f4a4639
--- /dev/null
+++ b/application/forms/Config/UserBackend/ExternalBackendForm.php
@@ -0,0 +1,83 @@
+setName('form_config_authbackend_external');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'strip_username_regexp',
+ array(
+ 'label' => $this->translate('Filter Pattern'),
+ 'description' => $this->translate(
+ 'The filter to use to strip specific parts off from usernames.'
+ . ' Leave empty if you do not want to strip off anything.'
+ ),
+ 'requirement' => $this->translate('The filter pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'external'
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * Validate the configuration by creating a backend and requesting the user count
+ *
+ * Returns always true as backends of type "external" are just "passive" backends.
+ *
+ * @param Form $form The form to fetch the configuration values from
+ *
+ * @return bool Whether validation succeeded or not
+ */
+ public static function isValidUserBackend(Form $form)
+ {
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserBackend/LdapBackendForm.php b/application/forms/Config/UserBackend/LdapBackendForm.php
new file mode 100644
index 0000000..e7804cc
--- /dev/null
+++ b/application/forms/Config/UserBackend/LdapBackendForm.php
@@ -0,0 +1,414 @@
+setName('form_config_authbackend_ldap');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $isAd = isset($formData['type']) ? $formData['type'] === 'msldap' : false;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others.'
+ ),
+ 'value' => $this->getSuggestion('name')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate(
+ 'The LDAP connection to use for authenticating with this provider.'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array(),
+ 'value' => $this->getSuggestion('resource')
+ )
+ );
+
+ if (! $isAd && !empty($this->resources)) {
+ $this->addElement(
+ 'button',
+ 'discovery_btn',
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => 'discovery_btn',
+ 'label' => $this->translate('Discover', 'A button to discover LDAP capabilities'),
+ 'title' => $this->translate(
+ 'Push to fill in the chosen connection\'s default settings.'
+ ),
+ 'decorators' => array(
+ array('ViewHelper', array('separator' => '')),
+ array('Spinner'),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ }
+
+ if ($isAd) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $this->getSuggestion('user_class', $userClass)
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'value' => $this->getSuggestion('filter', $filter),
+ 'label' => $this->translate('LDAP Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression.'
+ . ' (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ // This is not meant to be a full syntax check. It will just
+ // ensure that we can safely strip unnecessary parentheses.
+ $v = trim($v);
+ return ! $v || $v[0] !== '(' || (
+ strpos($v, ')(') !== false ? substr($v, -2) === '))' : substr($v, -1) === ')'
+ );
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter is invalid. Please check your syntax.')
+ )
+ )
+ )
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing the user name on the LDAP server.'
+ ),
+ 'value' => $this->getSuggestion('user_name_attribute', $userNameAttribute)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => $this->getSuggestion('backend', $isAd ? 'msldap' : 'ldap')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'required' => false,
+ 'label' => $this->translate('LDAP Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $this->getSuggestion('base_dn')
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Domain'),
+ 'description' => $this->translate(
+ 'The domain the LDAP server is responsible for upon authentication.'
+ . ' Note that if you specify a domain here,'
+ . ' the LDAP backend only authenticates users who specify a domain upon login.'
+ . ' If the domain of the user matches the domain configured here, this backend is responsible for'
+ . ' authenticating the user based on the username without the domain part.'
+ . ' If your LDAP backend holds usernames with a domain part or if it is not necessary in your setup'
+ . ' to authenticate users based on their domains, leave this field empty.'
+ ),
+ 'preserveDefault' => true,
+ 'value' => $this->getSuggestion('domain')
+ )
+ );
+
+ $this->addElement(
+ 'button',
+ 'btn_discover_domain',
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => 'discovery_btn',
+ 'label' => $this->translate('Discover the domain'),
+ 'title' => $this->translate(
+ 'Push to disover and fill in the domain of the LDAP server.'
+ ),
+ 'decorators' => array(
+ array('ViewHelper', array('separator' => '')),
+ array('Spinner'),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ }
+
+ public function isValidPartial(array $formData)
+ {
+ $isAd = isset($formData['type']) && $formData['type'] === 'msldap';
+ $baseDn = null;
+ $hasAdOid = false;
+ $discoverySuccessful = false;
+
+ if (! $isAd && ! empty($this->resources) && isset($formData['discovery_btn'])
+ && $formData['discovery_btn'] === 'discovery_btn') {
+ $discoverySuccessful = true;
+ try {
+ $capabilities = $this->getLdapCapabilities($formData);
+ $baseDn = $capabilities->getDefaultNamingContext();
+ $hasAdOid = $capabilities->isActiveDirectory();
+ } catch (Exception $e) {
+ $this->warning(sprintf(
+ $this->translate('Failed to discover the chosen LDAP connection: %s'),
+ $e->getMessage()
+ ));
+ $discoverySuccessful = false;
+ }
+ }
+
+ if ($discoverySuccessful) {
+ if ($isAd || $hasAdOid) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $formData['user_class'] = $userClass;
+
+ if (! isset($formData['filter']) || $formData['filter'] === '') {
+ $formData['filter'] = $filter;
+ }
+
+ $formData['user_name_attribute'] = $userNameAttribute;
+
+ if ($baseDn !== null && (! isset($formData['base_dn']) || $formData['base_dn'] === '')) {
+ $formData['base_dn'] = $baseDn;
+ }
+ }
+
+ if (isset($formData['btn_discover_domain']) && $formData['btn_discover_domain'] === 'discovery_btn') {
+ try {
+ $formData['domain'] = $this->discoverDomain($formData);
+ } catch (LdapException $e) {
+ $this->error($e->getMessage());
+ }
+ }
+
+ return parent::isValidPartial($formData);
+ }
+
+ /**
+ * Get the LDAP capabilities of either the resource specified by the user or the default one
+ *
+ * @param string[] $formData
+ *
+ * @return LdapCapabilities
+ */
+ protected function getLdapCapabilities(array $formData)
+ {
+ if ($this->ldapCapabilities === null) {
+ $this->ldapCapabilities = ResourceFactory::create(
+ isset($formData['resource']) ? $formData['resource'] : reset($this->resources)
+ )->bind()->getCapabilities();
+ }
+
+ return $this->ldapCapabilities;
+ }
+
+ /**
+ * Discover the domain the LDAP server is responsible for
+ *
+ * @param string[] $formData
+ *
+ * @return string
+ */
+ protected function discoverDomain(array $formData)
+ {
+ $cap = $this->getLdapCapabilities($formData);
+
+ if ($cap->isActiveDirectory()) {
+ $netBiosName = $cap->getNetBiosName();
+ if ($netBiosName !== null) {
+ return $netBiosName;
+ }
+ }
+
+ return $this->defaultNamingContextToFQDN($cap);
+ }
+
+ /**
+ * Get the default naming context as FQDN
+ *
+ * @param LdapCapabilities $cap
+ *
+ * @return string|null
+ */
+ protected function defaultNamingContextToFQDN(LdapCapabilities $cap)
+ {
+ $defaultNamingContext = $cap->getDefaultNamingContext();
+ if ($defaultNamingContext !== null) {
+ $validationMatches = array();
+ if (preg_match('/\bdc=[^,]+(?:,dc=[^,]+)*$/', strtolower($defaultNamingContext), $validationMatches)) {
+ $splitMatches = array();
+ preg_match_all('/dc=([^,]+)/', $validationMatches[0], $splitMatches);
+ return implode('.', $splitMatches[1]);
+ }
+ }
+ }
+
+ /**
+ * Get the default values for the form elements
+ *
+ * @return string[]
+ */
+ public function getSuggestions()
+ {
+ return $this->suggestions;
+ }
+
+ /**
+ * Get the default value for the given form element or the given default
+ *
+ * @param string $element
+ * @param string $default
+ *
+ * @return string
+ */
+ public function getSuggestion($element, $default = null)
+ {
+ return isset($this->suggestions[$element]) ? $this->suggestions[$element] : $default;
+ }
+
+ /**
+ * Set the default values for the form elements
+ *
+ * @param string[] $suggestions
+ *
+ * @return $this
+ */
+ public function setSuggestions(array $suggestions)
+ {
+ $this->suggestions = $suggestions;
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php
new file mode 100644
index 0000000..fdca657
--- /dev/null
+++ b/application/forms/Config/UserBackendConfigForm.php
@@ -0,0 +1,482 @@
+setName('form_config_authbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ $this->customBackends = UserBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param Config $resourceConfig The resource configuration
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError In case there are no valid resources for authentication available
+ */
+ public function setResourceConfig(Config $resourceConfig)
+ {
+ $resources = array();
+ foreach ($resourceConfig as $name => $resource) {
+ if (in_array($resource->type, array('db', 'ldap'))) {
+ $resources[$resource->type][] = $name;
+ }
+ }
+
+ if (empty($resources)) {
+ $externalBackends = $this->config->toArray();
+ array_walk(
+ $externalBackends,
+ function (&$authBackendCfg) {
+ if (! isset($authBackendCfg['backend']) || $authBackendCfg['backend'] !== 'external') {
+ $authBackendCfg = null;
+ }
+ }
+ );
+ if (count(array_filter($externalBackends)) > 0 && (
+ $this->backendToLoad === null || !isset($externalBackends[$this->backendToLoad])
+ )) {
+ throw new ConfigurationError($this->translate(
+ 'Could not find any valid user backend resources.'
+ . ' Please configure a resource for authentication first.'
+ ));
+ }
+ }
+
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ $form = new DbBackendForm();
+ $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array());
+ break;
+ case 'ldap':
+ case 'msldap':
+ $form = new LdapBackendForm();
+ $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array());
+ break;
+ case 'external':
+ $form = new ExternalBackendForm();
+ break;
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+
+ return $form;
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException(
+ $this->translate('A user backend with the name "%s" does already exist'),
+ $backendName
+ );
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $backendConfig->merge($data);
+ $this->config->setSection($name, $backendConfig);
+ return $this;
+ }
+
+ /**
+ * Remove a user backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Move the given user backend up or down in order
+ *
+ * @param string $name The name of the backend to be moved
+ * @param int $position The new (absolute) position of the backend
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function move($name, $position)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendOrder = $this->config->keys();
+ array_splice($backendOrder, array_search($name, $backendOrder), 1);
+ array_splice($backendOrder, $position, 0, $name);
+
+ $newConfig = array();
+ foreach ($backendOrder as $backendName) {
+ $newConfig[$backendName] = $this->config->getSection($backendName);
+ }
+
+ $config = Config::fromArray($newConfig);
+ $this->config = $config->setConfigFile($this->config->getConfigFile());
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array();
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+
+ if (isset($this->resources['db'])) {
+ $backendTypes['db'] = $this->translate('Database');
+ }
+ if (isset($this->resources['ldap'])) {
+ $backendTypes['ldap'] = 'LDAP';
+ $backendTypes['msldap'] = 'ActiveDirectory';
+ }
+
+ $externalBackends = array_filter(
+ $this->config->toArray(),
+ function ($authBackendCfg) {
+ return isset($authBackendCfg['backend']) && $authBackendCfg['backend'] === 'external';
+ }
+ );
+ if ($backendType === 'external' || empty($externalBackends)) {
+ $backendTypes['external'] = $this->translate('External');
+ }
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate(
+ 'The type of the resource to use for this authenticaton provider'
+ ),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addSkipValidationCheckbox();
+ }
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['name'] = $this->backendToLoad;
+ $data['type'] = $data['backend'];
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($el = $this->getElement('skip_validation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ if ($el === null) {
+ $this->addSkipValidationCheckbox();
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a user backend by using the given form's values and return its inspection results
+ *
+ * Returns null for non-inspectable backends.
+ *
+ * @param Form $form
+ *
+ * @return Inspection|null
+ */
+ public static function inspectUserBackend(Form $form)
+ {
+ $backend = UserBackend::create(null, new ConfigObject($form->getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the connection validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this box to enforce changes without validating that authentication is possible.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (! parent::isValidPartial($formData)) {
+ return false;
+ }
+
+ if ($this->getElement('backend_validation')->isChecked() && parent::isValid($formData)) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '' . $this->translate('Validation Log') . " \n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendReorderForm.php b/application/forms/Config/UserBackendReorderForm.php
new file mode 100644
index 0000000..019c032
--- /dev/null
+++ b/application/forms/Config/UserBackendReorderForm.php
@@ -0,0 +1,86 @@
+setName('form_reorder_authbackend');
+ $this->setViewScript('form/reorder-authbackend.phtml');
+ }
+
+ /**
+ * Return the ordered backend names
+ *
+ * @return array
+ */
+ public function getBackendOrder()
+ {
+ return $this->config->keys();
+ }
+
+ /**
+ * Return the ordered backend configuration
+ *
+ * @return Config
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ // This adds just a dummy element to be able to utilize Form::getValue as part of onSuccess()
+ $this->addElement('hidden', 'backend_newpos');
+ }
+
+ /**
+ * Update the user backend order and save the configuration
+ */
+ public function onSuccess()
+ {
+ $newPosData = $this->getValue('backend_newpos');
+ if ($newPosData) {
+ $configForm = $this->getConfigForm();
+ list($backendName, $position) = explode('|', $newPosData, 2);
+
+ try {
+ if ($configForm->move($backendName, $position)->save()) {
+ Notification::success($this->translate('Authentication order updated'));
+ } else {
+ return false;
+ }
+ } catch (NotFoundError $_) {
+ Notification::error(sprintf($this->translate('User backend "%s" not found'), $backendName));
+ }
+ }
+ }
+
+ /**
+ * Return the config form for user backends
+ *
+ * @return ConfigForm
+ */
+ protected function getConfigForm()
+ {
+ $form = new UserBackendConfigForm();
+ $form->setIniConfig($this->config);
+ return $form;
+ }
+}
diff --git a/application/forms/Config/UserGroup/AddMemberForm.php b/application/forms/Config/UserGroup/AddMemberForm.php
new file mode 100644
index 0000000..cda9d52
--- /dev/null
+++ b/application/forms/Config/UserGroup/AddMemberForm.php
@@ -0,0 +1,183 @@
+ds = $ds;
+ return $this;
+ }
+
+ /**
+ * Set the user group backend to use
+ *
+ * @param Extensible $backend
+ *
+ * @return $this
+ */
+ public function setBackend(Extensible $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ /**
+ * Set the group to add members for
+ *
+ * @param string $groupName
+ *
+ * @return $this
+ */
+ public function setGroupName($groupName)
+ {
+ $this->groupName = $groupName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ // TODO(jom): Fetching already existing members to prevent the user from mistakenly creating duplicate
+ // memberships (no matter whether the data source permits it or not, a member does never need to be
+ // added more than once) should be kept at backend level (GroupController::fetchUsers) but this does
+ // not work currently as our ldap protocol stuff is unable to handle our filter implementation..
+ $members = $this->backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $this->groupName)
+ ->fetchColumn();
+ $filter = empty($members) ? Filter::matchAll() : Filter::not(Filter::where('user_name', $members));
+
+ $users = $this->ds->select()->from('user', array('user_name'))->applyFilter($filter)->fetchColumn();
+ if (! empty($users)) {
+ $this->addElement(
+ 'multiselect',
+ 'user_name',
+ array(
+ 'multiOptions' => array_combine($users, $users),
+ 'label' => $this->translate('Backend Users'),
+ 'description' => $this->translate(
+ 'Select one or more users (fetched from your user backends) to add as group member'
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'required' => empty($users),
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Provide one or more usernames separated by comma to add as group member'
+ )
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Add members for group %s'), $this->groupName));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Insert the members for the group
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $userNames = $this->getValue('user_name') ?: array();
+ if (($users = $this->getValue('users'))) {
+ $userNames = array_merge($userNames, array_map('trim', explode(',', $users)));
+ }
+
+ if (empty($userNames)) {
+ $this->info($this->translate(
+ 'Please provide at least one username, either by choosing one '
+ . 'in the list or by manually typing one in the text box below'
+ ));
+ return false;
+ }
+
+ $single = null;
+ $userName = null;
+ foreach ($userNames as $userName) {
+ try {
+ $this->backend->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $this->groupName,
+ 'user_name' => $userName
+ )
+ );
+ } catch (NotFoundError $e) {
+ throw $e; // Trigger 404, the group name is initially accessed as GET parameter
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $userName,
+ $this->groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(sprintf($this->translate('Group member "%s" added successfully'), $userName));
+ } else {
+ Notification::success($this->translate('Group members added successfully'));
+ }
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserGroup/DbUserGroupBackendForm.php b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
new file mode 100644
index 0000000..daea8de
--- /dev/null
+++ b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
@@ -0,0 +1,79 @@
+setName('form_config_dbusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getDatabaseResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate('The database connection to use for this backend'),
+ 'multiOptions' => empty($resourceNames) ? array() : array_combine($resourceNames, $resourceNames)
+ )
+ );
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => 'db'
+ )
+ );
+ }
+
+ /**
+ * Return the names of all configured database resources
+ *
+ * @return array
+ */
+ protected function getDatabaseResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (strtolower($config->type) === 'db') {
+ $names[] = $name;
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
new file mode 100644
index 0000000..10c069a
--- /dev/null
+++ b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
@@ -0,0 +1,370 @@
+setName('form_config_ldapusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getLdapResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate('The LDAP connection to use for this backend.'),
+ 'multiOptions' => array_combine($resourceNames, $resourceNames)
+ )
+ );
+ $resource = ResourceFactory::create(
+ isset($formData['resource']) && in_array($formData['resource'], $resourceNames)
+ ? $formData['resource']
+ : $resourceNames[0]
+ );
+
+ $userBackendNames = $this->getLdapUserBackendNames($resource);
+ if (! empty($userBackendNames)) {
+ $userBackends = array_combine($userBackendNames, $userBackendNames);
+ $userBackends['none'] = $this->translate('None', 'usergroupbackend.ldap.user_backend');
+ } else {
+ $userBackends = array('none' => $this->translate('None', 'usergroupbackend.ldap.user_backend'));
+ }
+ $this->addElement(
+ 'select',
+ 'user_backend',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Backend'),
+ 'description' => $this->translate('The user backend to link with this user group backend.'),
+ 'multiOptions' => $userBackends
+ )
+ );
+
+ $groupBackend = new LdapUserGroupBackend($resource);
+ if ($formData['type'] === 'ldap') {
+ $defaults = $groupBackend->getOpenLdapDefaults();
+ $groupConfigDisabled = $userConfigDisabled = null; // MUST BE null, do NOT change this to false!
+ } else { // $formData['type'] === 'msldap'
+ $defaults = $groupBackend->getActiveDirectoryDefaults();
+ $groupConfigDisabled = $userConfigDisabled = true;
+ }
+
+ if ($formData['type'] === 'msldap') {
+ $this->addElement(
+ 'checkbox',
+ 'nested_group_search',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for nested group search in Active Directory based on the user'
+ ),
+ 'label' => $this->translate('Nested Group Search')
+ )
+ );
+ } else {
+ // This is required to purge already present options
+ $this->addElement('hidden', 'nested_group_search', array('disabled' => true));
+ }
+
+ $this->createGroupConfigElements($defaults, $groupConfigDisabled);
+ if (count($userBackends) === 1 || (isset($formData['user_backend']) && $formData['user_backend'] === 'none')) {
+ $this->createUserConfigElements($defaults, $userConfigDisabled);
+ } else {
+ $this->createHiddenUserConfigElements();
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => $formData['type']
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the group configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createGroupConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'group_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Object Class'),
+ 'description' => $this->translate('The object class used for storing groups on the LDAP server.'),
+ 'value' => $defaults->group_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP Group Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up groups using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->group_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a group\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->group_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_member_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Member Attribute'),
+ 'description' => $this->translate('The attribute name used for storing a group\'s members.'),
+ 'value' => $defaults->group_member_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP Group Base DN'),
+ 'description' => $this->translate(
+ 'The path where groups can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->base_dn
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the user configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createUserConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $defaults->user_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP User Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->user_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a user\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->user_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP User Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->user_base_dn
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Domain'),
+ 'description' => $this->translate(
+ 'The domain the LDAP server is responsible for.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements for the user configuration as hidden inputs
+ *
+ * This is required to purge already present options when unlinking a group backend with a user backend.
+ */
+ protected function createHiddenUserConfigElements()
+ {
+ $this->addElement('hidden', 'user_class', array('disabled' => true));
+ $this->addElement('hidden', 'user_filter', array('disabled' => true));
+ $this->addElement('hidden', 'user_name_attribute', array('disabled' => true));
+ $this->addElement('hidden', 'user_base_dn', array('disabled' => true));
+ $this->addElement('hidden', 'domain', array('disabled' => true));
+ }
+
+ /**
+ * Return the names of all configured LDAP resources
+ *
+ * @return array
+ */
+ protected function getLdapResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (in_array(strtolower($config->type), array('ldap', 'msldap'))) {
+ $names[] = $name;
+ }
+ }
+
+ if (empty($names)) {
+ Notification::error(
+ $this->translate('No LDAP resources available. Please configure an LDAP resource first.')
+ );
+ $this->getResponse()->redirectAndExit('config/createresource');
+ }
+
+ return $names;
+ }
+
+ /**
+ * Return the names of all configured LDAP user backends
+ *
+ * @param LdapConnection $resource
+ *
+ * @return array
+ */
+ protected function getLdapUserBackendNames(LdapConnection $resource)
+ {
+ $names = array();
+ foreach (UserBackend::getBackendConfigs() as $name => $config) {
+ if (in_array(strtolower($config->backend), array('ldap', 'msldap'))) {
+ $backendResource = ResourceFactory::create($config->resource);
+ if ($backendResource->getHostname() === $resource->getHostname()
+ && $backendResource->getPort() === $resource->getPort()
+ ) {
+ $names[] = $name;
+ }
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php
new file mode 100644
index 0000000..9ee4032
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php
@@ -0,0 +1,314 @@
+getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_usergroupbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->customBackends = UserGroupBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ return new DbUserGroupBackendForm();
+ case 'ldap':
+ case 'msldap':
+ return new LdapUserGroupBackendForm();
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user group backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException('A user group backend with the name "%s" does already exist', $backendName);
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user group backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $this->config->setSection($name, $backendConfig->merge($data));
+ return $this;
+ }
+
+ /**
+ * Remove a user group backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array(
+ 'db' => $this->translate('Database'),
+ 'ldap' => 'LDAP',
+ 'msldap' => 'ActiveDirectory'
+ );
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate('The type of this user group backend'),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['type'] = $data['backend'];
+ $data['name'] = $this->backendToLoad;
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '' . $this->translate('Validation Log') . " \n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupForm.php b/application/forms/Config/UserGroup/UserGroupForm.php
new file mode 100644
index 0000000..b944e97
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupForm.php
@@ -0,0 +1,158 @@
+addElement(
+ 'text',
+ 'group_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Group Name')
+ )
+ );
+
+ if ($this->shouldInsert()) {
+ $this->setTitle($this->translate('Add a new group'));
+ $this->setSubmitLabel($this->translate('Add'));
+ } else { // $this->shouldUpdate()
+ $this->setTitle(sprintf($this->translate('Edit group %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+ }
+
+ /**
+ * Update a group
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('group_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('group', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create and add elements to this form to delete a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove group %s?'), $this->getIdentifier()));
+ $this->addDescription($this->translate(
+ 'Note that all users that are currently a member of this group will'
+ . ' have their membership cleared automatically.'
+ ));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-form icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a group
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('group_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('Group added successfully');
+ } else {
+ return $this->translate('Failed to add group');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}
diff --git a/application/forms/ConfigForm.php b/application/forms/ConfigForm.php
new file mode 100644
index 0000000..8b0c5f9
--- /dev/null
+++ b/application/forms/ConfigForm.php
@@ -0,0 +1,192 @@
+_subForms) as $name) {
+ // Zend returns values from subforms grouped by their names, but we want them flat
+ $values = array_merge($values, $values[$name]);
+ unset($values[$name]);
+ }
+ return $values;
+ }
+
+ /**
+ * Set the configuration to use when populating the form or when saving the user's input
+ *
+ * @param Config $config The configuration to use
+ *
+ * @return $this
+ */
+ public function setIniConfig(Config $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ $sections = array();
+ foreach (static::transformEmptyValuesToNull($this->getValues()) as $sectionAndPropertyName => $value) {
+ list($section, $property) = explode('_', $sectionAndPropertyName, 2);
+ $sections[$section][$property] = $value;
+ }
+
+ foreach ($sections as $section => $config) {
+ if ($this->isEmptyConfig($config)) {
+ $this->config->removeSection($section);
+ } else {
+ $this->config->setSection($section, $config);
+ }
+ }
+
+ if ($this->save()) {
+ Notification::success($this->translate('New configuration has successfully been stored'));
+ } else {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+
+ public function onRequest()
+ {
+ $values = array();
+ foreach ($this->config as $section => $properties) {
+ foreach ($properties as $name => $value) {
+ $values[$section . '_' . $name] = $value;
+ }
+ }
+
+ $this->populate($values);
+ }
+
+ /**
+ * Persist the current configuration to disk
+ *
+ * If an error occurs the user is shown a view describing the issue and displaying the raw INI configuration.
+ *
+ * @return bool Whether the configuration could be persisted
+ */
+ public function save()
+ {
+ try {
+ $this->writeConfig($this->config);
+ } catch (ConfigurationError $e) {
+ $this->addError($e->getMessage());
+
+ return false;
+ } catch (Exception $e) {
+ $this->addDecorator('ViewScript', array(
+ 'viewModule' => 'default',
+ 'viewScript' => 'showConfiguration.phtml',
+ 'errorMessage' => $e->getMessage(),
+ 'configString' => $this->config,
+ 'filePath' => $this->config->getConfigFile(),
+ 'placement' => Zend_Form_Decorator_Abstract::PREPEND
+ ));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Write the configuration to disk
+ *
+ * @param Config $config
+ */
+ protected function writeConfig(Config $config)
+ {
+ $config->saveIni();
+ }
+
+ /**
+ * Get whether the given config is empty or has only empty values
+ *
+ * @param array|Config $config
+ *
+ * @return bool
+ */
+ protected function isEmptyConfig($config)
+ {
+ if ($config instanceof Config) {
+ $config = $config->toArray();
+ }
+
+ foreach ($config as $value) {
+ if ($value !== null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Transform all empty values of the given array to null
+ *
+ * @param array $values
+ *
+ * @return array
+ */
+ public static function transformEmptyValuesToNull(array $values)
+ {
+ array_walk($values, function (&$v) {
+ if ($v === '' || $v === false || $v === array()) {
+ $v = null;
+ }
+ });
+
+ return $values;
+ }
+}
diff --git a/application/forms/ConfirmRemovalForm.php b/application/forms/ConfirmRemovalForm.php
new file mode 100644
index 0000000..39fc661
--- /dev/null
+++ b/application/forms/ConfirmRemovalForm.php
@@ -0,0 +1,38 @@
+setName('form_confirm_removal');
+ $this->getSubmitLabel() ?: $this->setSubmitLabel($this->translate('Confirm Removal'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton();
+
+ if (($submit = $this->getElement('btn_submit')) !== null) {
+ $class = $submit->getAttrib('class');
+ $submit->setAttrib('class', empty($class) ? 'autofocus' : $class . ' autofocus');
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Control/LimiterControlForm.php b/application/forms/Control/LimiterControlForm.php
new file mode 100644
index 0000000..88adf4b
--- /dev/null
+++ b/application/forms/Control/LimiterControlForm.php
@@ -0,0 +1,134 @@
+ '10',
+ 25 => '25',
+ 50 => '50',
+ 100 => '100',
+ 500 => '500'
+ );
+
+ public static $defaultElementDecorators = [
+ ['Label', ['tag' => 'span', 'separator' => '']],
+ ['ViewHelper', ['separator' => '']],
+ ];
+
+ /**
+ * Default limit for this instance
+ *
+ * @var int|null
+ */
+ protected $defaultLimit;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', static::CSS_CLASS_LIMITER);
+ }
+
+ /**
+ * Get the default limit
+ *
+ * @return int
+ */
+ public function getDefaultLimit()
+ {
+ return $this->defaultLimit !== null ? $this->defaultLimit : static::DEFAULT_LIMIT;
+ }
+
+ /**
+ * Set the default limit
+ *
+ * @param int $defaultLimit
+ *
+ * @return $this
+ */
+ public function setDefaultLimit($defaultLimit)
+ {
+ $defaultLimit = (int) $defaultLimit;
+
+ if (! isset(static::$limits[$defaultLimit])) {
+ static::$limits[$defaultLimit] = $defaultLimit;
+ }
+
+ $this->defaultLimit = $defaultLimit;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrl()
+ {
+ return $this->getRequest()->getUrl()
+ ->setParam('limit', $this->getElement('limit')->getValue())
+ ->without('page');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $options = static::$limits;
+ $pageSize = (int) $this->getRequest()->getUrl()->getParam('limit', $this->getDefaultLimit());
+
+ if (! isset($options[$pageSize])) {
+ $options[$pageSize] = $pageSize;
+ }
+
+ $this->addElement(
+ 'select',
+ 'limit',
+ array(
+ 'autosubmit' => true,
+ 'escape' => false,
+ 'label' => '#',
+ 'multiOptions' => $options,
+ 'value' => $pageSize
+ )
+ );
+ }
+
+ /**
+ * Limiter control is always successful
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ return true;
+ }
+}
diff --git a/application/forms/Dashboard/DashletForm.php b/application/forms/Dashboard/DashletForm.php
new file mode 100644
index 0000000..1af65a9
--- /dev/null
+++ b/application/forms/Dashboard/DashletForm.php
@@ -0,0 +1,171 @@
+setName('form_dashboard_addurl');
+ if (! $this->getSubmitLabel()) {
+ $this->setSubmitLabel($this->translate('Add To Dashboard'));
+ }
+ $this->setAction(Url::fromRequest());
+ }
+
+ /**
+ * Build AddUrl form elements
+ *
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $groupElements = array();
+ $panes = array();
+
+ if ($this->dashboard) {
+ $panes = $this->dashboard->getPaneKeyTitleArray();
+ }
+
+ $sectionNameValidator = ['Callback', true, [
+ 'callback' => function ($value) {
+ if (strpos($value, '[') === false && strpos($value, ']') === false) {
+ return true;
+ }
+ },
+ 'messages' => [
+ 'callbackValue' => $this->translate('Brackets ([, ]) cannot be used here')
+ ]
+ ]];
+
+ $this->addElement(
+ 'hidden',
+ 'org_pane',
+ array(
+ 'required' => false
+ )
+ );
+
+ $this->addElement(
+ 'hidden',
+ 'org_dashlet',
+ array(
+ 'required' => false
+ )
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'url',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters.'
+ ),
+ 'validators' => array(new UrlValidator(), new InternalUrlValidator())
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'dashlet',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Dashlet Title'),
+ 'description' => $this->translate('Enter a title for the dashlet.'),
+ 'validators' => [$sectionNameValidator]
+ )
+ );
+ $this->addElement(
+ 'note',
+ 'note',
+ array(
+ 'decorators' => array(
+ array('HtmlTag', array('tag' => 'hr'))
+ )
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'create_new_pane',
+ array(
+ 'autosubmit' => true,
+ 'required' => false,
+ 'label' => $this->translate('New dashboard'),
+ 'description' => $this->translate('Check this box if you want to add the dashlet to a new dashboard')
+ )
+ );
+ if (empty($panes) || ((isset($formData['create_new_pane']) && $formData['create_new_pane'] != false))) {
+ $this->addElement(
+ 'text',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('New Dashboard Title'),
+ 'description' => $this->translate('Enter a title for the new dashboard'),
+ 'validators' => [$sectionNameValidator]
+ )
+ );
+ } else {
+ $this->addElement(
+ 'select',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Dashboard'),
+ 'multiOptions' => $panes,
+ 'description' => $this->translate('Select a dashboard you want to add the dashlet to')
+ )
+ );
+ }
+ }
+
+ /**
+ * @param \Icinga\Web\Widget\Dashboard $dashboard
+ */
+ public function setDashboard(Dashboard $dashboard)
+ {
+ $this->dashboard = $dashboard;
+ }
+
+ /**
+ * @return \Icinga\Web\Widget\Dashboard
+ */
+ public function getDashboard()
+ {
+ return $this->dashboard;
+ }
+
+ /**
+ * @param Dashlet $dashlet
+ */
+ public function load(Dashlet $dashlet)
+ {
+ $this->populate(array(
+ 'pane' => $dashlet->getPane()->getTitle(),
+ 'org_pane' => $dashlet->getPane()->getName(),
+ 'dashlet' => $dashlet->getTitle(),
+ 'org_dashlet' => $dashlet->getName(),
+ 'url' => $dashlet->getUrl()->getRelativeUrl()
+ ));
+ }
+}
diff --git a/application/forms/LdapDiscoveryForm.php b/application/forms/LdapDiscoveryForm.php
new file mode 100644
index 0000000..5c7fc87
--- /dev/null
+++ b/application/forms/LdapDiscoveryForm.php
@@ -0,0 +1,34 @@
+setName('form_ldap_discovery');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Search Domain'),
+ 'description' => $this->translate('Search this domain for records of available servers.'),
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/MigrationForm.php b/application/forms/MigrationForm.php
new file mode 100644
index 0000000..c5d517f
--- /dev/null
+++ b/application/forms/MigrationForm.php
@@ -0,0 +1,143 @@
+ ['icinga-form', 'migration-form', 'icinga-controls'],
+ 'name' => 'migration-form'
+ ];
+
+ /** @var bool Whether to allow changing the current database user and password */
+ protected $renderDatabaseUserChange = false;
+
+ public function hasBeenSubmitted(): bool
+ {
+ if (! $this->hasBeenSent()) {
+ return false;
+ }
+
+ $pressedButton = $this->getPressedSubmitElement();
+
+ return $pressedButton && strpos($pressedButton->getName(), 'migrate-') !== false;
+ }
+
+ public function setRenderDatabaseUserChange(bool $value = true): self
+ {
+ $this->renderDatabaseUserChange = $value;
+
+ return $this;
+ }
+
+ public function hasDefaultElementDecorator()
+ {
+ // The base implementation registers a decorator we don't want here
+ return false;
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml($this->createUidElement());
+
+ if ($this->renderDatabaseUserChange) {
+ $mm = MigrationManager::instance();
+ $newDbSetup = new FieldsetElement('database_setup', ['required' => true]);
+ $newDbSetup
+ ->setDefaultElementDecorator(new IcingaFormDecorator())
+ ->addElement('text', 'username', [
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'A user which is able to create and/or alter the database schema.'
+ )
+ ])
+ ->addElement('password', 'password', [
+ 'required' => true,
+ 'autocomplete' => 'new-password',
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password for the database user defined above.'),
+ 'validators' => [
+ new CallbackValidator(function ($_, CallbackValidator $validator) use ($mm, $newDbSetup): bool {
+ /** @var array $values */
+ $values = $this->getValue('database_setup');
+ /** @var CheckboxElement $checkBox */
+ $checkBox = $newDbSetup->getElement('grant_privileges');
+ $canIssueGrants = $checkBox->isChecked();
+ $elevationConfig = [
+ 'username' => $values['username'],
+ 'password' => $values['password']
+ ];
+
+ try {
+ if (! $mm->validateDatabasePrivileges($elevationConfig, $canIssueGrants)) {
+ $validator->addMessage(sprintf(
+ $this->translate(
+ 'The provided credentials cannot be used to execute "%s" SQL commands'
+ . ' and/or grant the missing privileges to other users.'
+ ),
+ implode(' ,', $mm->getRequiredDatabasePrivileges())
+ ));
+
+ return false;
+ }
+ } catch (PDOException $e) {
+ $validator->addMessage($e->getMessage());
+
+ return false;
+ }
+
+ return true;
+ })
+ ]
+ ])
+ ->addElement('checkbox', 'grant_privileges', [
+ 'required' => false,
+ 'label' => $this->translate('Grant Missing Privileges'),
+ 'description' => $this->translate(
+ 'Allows to automatically grant the required privileges to the database user specified'
+ . ' in the respective resource config. If you do not want to provide additional credentials'
+ . ' each time, you can enable this and Icinga Web will grant the active database user the'
+ . ' missing privileges.'
+ )
+ ]);
+
+ $this->addHtml(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'change-database-user-description']),
+ new HtmlElement('span', null, Text::create(sprintf(
+ $this->translate(
+ 'It seems that the currently used database user does not have the required privileges to'
+ . ' execute the %s SQL commands. Please provide an alternative user'
+ . ' that has the appropriate credentials to resolve this issue.'
+ ),
+ implode(', ', $mm->getRequiredDatabasePrivileges())
+ )))
+ )
+ );
+
+ $this->addElement($newDbSetup);
+ }
+ }
+}
diff --git a/application/forms/Navigation/DashletForm.php b/application/forms/Navigation/DashletForm.php
new file mode 100644
index 0000000..6575fd7
--- /dev/null
+++ b/application/forms/Navigation/DashletForm.php
@@ -0,0 +1,35 @@
+addElement(
+ 'text',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Pane'),
+ 'description' => $this->translate('The name of the dashboard pane in which to display this dashlet')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'url',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'The url to load in the dashlet. For external urls, make sure to prepend'
+ . ' an appropriate protocol identifier (e.g. http://example.tld)'
+ )
+ )
+ );
+ }
+}
diff --git a/application/forms/Navigation/MenuItemForm.php b/application/forms/Navigation/MenuItemForm.php
new file mode 100644
index 0000000..c9fa729
--- /dev/null
+++ b/application/forms/Navigation/MenuItemForm.php
@@ -0,0 +1,31 @@
+getElement('target')->removeMultiOption('_self');
+ $this->getElement('target')->removeMultiOption('_next');
+
+ $parentElement = $this->getParent()->getElement('parent');
+ if ($parentElement !== null) {
+ $parentElement->setDescription($this->translate(
+ 'The parent menu to assign this menu entry to. Select "None" to make this a main menu entry'
+ ));
+ }
+ }
+}
diff --git a/application/forms/Navigation/NavigationConfigForm.php b/application/forms/Navigation/NavigationConfigForm.php
new file mode 100644
index 0000000..0c4ae32
--- /dev/null
+++ b/application/forms/Navigation/NavigationConfigForm.php
@@ -0,0 +1,853 @@
+setName('form_config_navigation');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * Set the user for whom to manage navigation items
+ *
+ * @param User $user
+ *
+ * @return $this
+ */
+ public function setUser(User $user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * Return the user for whom to manage navigation items
+ *
+ * @return User
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the user's navigation configuration
+ *
+ * @param Config $config
+ *
+ * @return $this
+ */
+ public function setUserConfig(Config $config)
+ {
+ $config->getConfigObject()->setKeyColumn('name');
+ $this->userConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the user's navigation configuration
+ *
+ * @param string $type
+ *
+ * @return Config
+ */
+ public function getUserConfig($type = null)
+ {
+ if ($this->userConfig === null || $type !== null) {
+ if ($type === null) {
+ throw new ProgrammingError('You need to pass a type if no user configuration is set');
+ }
+
+ $this->setUserConfig(Config::navigation($type, $this->getUser()->getUsername()));
+ }
+
+ return $this->userConfig;
+ }
+
+ /**
+ * Set the shared navigation configuration
+ *
+ * @param Config $config
+ *
+ * @return $this
+ */
+ public function setShareConfig(Config $config)
+ {
+ $config->getConfigObject()->setKeyColumn('name');
+ $this->shareConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the shared navigation configuration
+ *
+ * @param string $type
+ *
+ * @return Config
+ */
+ public function getShareConfig($type = null)
+ {
+ if ($this->shareConfig === null) {
+ if ($type === null) {
+ throw new ProgrammingError('You need to pass a type if no share configuration is set');
+ }
+
+ $this->setShareConfig(Config::navigation($type));
+ }
+
+ return $this->shareConfig;
+ }
+
+ /**
+ * Set the available navigation item types
+ *
+ * @param array $itemTypes
+ *
+ * @return $this
+ */
+ public function setItemTypes(array $itemTypes)
+ {
+ $this->itemTypes = $itemTypes;
+ return $this;
+ }
+
+ /**
+ * Return the available navigation item types
+ *
+ * @return array
+ */
+ public function getItemTypes()
+ {
+ return $this->itemTypes ?: array();
+ }
+
+ /**
+ * Return a list of available parent items for the given type of navigation item
+ *
+ * @param string $type
+ * @param string $owner
+ *
+ * @return array
+ */
+ public function listAvailableParents($type, $owner = null)
+ {
+ $children = $this->itemToLoad ? $this->getFlattenedChildren($this->itemToLoad) : array();
+
+ $names = array();
+ foreach ($this->getShareConfig($type) as $sectionName => $sectionConfig) {
+ if ((string) $sectionName !== $this->itemToLoad
+ && $sectionConfig->owner === ($owner ?: $this->getUser()->getUsername())
+ && ! in_array($sectionName, $children, true)
+ ) {
+ $names[] = $sectionName;
+ }
+ }
+
+ foreach ($this->getUserConfig($type) as $sectionName => $sectionConfig) {
+ if ((string) $sectionName !== $this->itemToLoad
+ && ! in_array($sectionName, $children, true)
+ ) {
+ $names[] = $sectionName;
+ }
+ }
+
+ return $names;
+ }
+
+ /**
+ * Recursively return all children of the given navigation item
+ *
+ * @param string $name
+ *
+ * @return array
+ */
+ protected function getFlattenedChildren($name)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ return array();
+ }
+
+ $children = array();
+ foreach ($config->toArray() as $sectionName => $sectionConfig) {
+ if (isset($sectionConfig['parent']) && $sectionConfig['parent'] === $name) {
+ $children[] = $sectionName;
+ $children = array_merge($children, $this->getFlattenedChildren($sectionName));
+ }
+ }
+
+ return $children;
+ }
+
+ /**
+ * Populate the form with the given navigation item's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ */
+ public function load($name)
+ {
+ if ($this->getConfigForItem($name) === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $this->itemToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new navigation item
+ *
+ * The navigation item to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a navigation item name or type
+ * @throws IcingaException In case a navigation item with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ } elseif (! isset($data['type'])) {
+ throw new InvalidArgumentException('Key \'type\' missing');
+ }
+
+ $shared = false;
+ $config = $this->getUserConfig($data['type']);
+ if ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
+ if ($this->getUser()->can('user/share/navigation')) {
+ $data['owner'] = $this->getUser()->getUsername();
+ $config = $this->getShareConfig($data['type']);
+ $shared = true;
+ } else {
+ unset($data['users']);
+ unset($data['groups']);
+ }
+ } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'], $data['type'])) {
+ $data['owner'] = $this->getUser()->getUsername();
+ $config = $this->getShareConfig($data['type']);
+ $shared = true;
+ }
+
+ $itemName = $data['name'];
+ $exists = $config->hasSection($itemName);
+ if (! $exists) {
+ if ($shared) {
+ $exists = $this->getUserConfig($data['type'])->hasSection($itemName);
+ } else {
+ $exists = (bool) $this->getShareConfig($data['type'])
+ ->select()
+ ->where('name', $itemName)
+ ->where('owner', $this->getUser()->getUsername())
+ ->count();
+ }
+ }
+
+ if ($exists) {
+ throw new IcingaException(
+ $this->translate('A navigation item with the name "%s" does already exist'),
+ $itemName
+ );
+ }
+
+ unset($data['name']);
+ $config->setSection($itemName, $data);
+ $this->setIniConfig($config);
+ return $this;
+ }
+
+ /**
+ * Edit a navigation item
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case a navigation item with the same name already exists
+ */
+ public function edit($name, array $data)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ } else {
+ $itemConfig = $config->getSection($name);
+ }
+
+ $shared = false;
+ if ($this->hasBeenShared($name)) {
+ if (isset($data['parent']) && $data['parent']
+ ? ! $this->hasBeenShared($data['parent'])
+ : ((! isset($data['users']) || ! $data['users']) && (! isset($data['groups']) || ! $data['groups']))
+ ) {
+ // It is shared but shouldn't anymore
+ $config = $this->unshare($name, isset($data['parent']) ? $data['parent'] : null);
+ }
+ } elseif ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
+ if ($this->getUser()->can('user/share/navigation')) {
+ // It is not shared yet but should be
+ $this->secondaryConfig = $config;
+ $config = $this->getShareConfig();
+ $data['owner'] = $this->getUser()->getUsername();
+ $shared = true;
+ } else {
+ unset($data['users']);
+ unset($data['groups']);
+ }
+ } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'])) {
+ // Its parent is shared so should it itself
+ $this->secondaryConfig = $config;
+ $config = $this->getShareConfig();
+ $data['owner'] = $this->getUser()->getUsername();
+ $shared = true;
+ }
+
+ $oldName = null;
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $oldName = $name;
+ $name = $data['name'];
+
+ $exists = $config->hasSection($name);
+ if (! $exists) {
+ $ownerName = $itemConfig->owner ?: $this->getUser()->getUsername();
+ if ($shared || $this->hasBeenShared($oldName)) {
+ if ($ownerName === $this->getUser()->getUsername()) {
+ $exists = $this->getUserConfig()->hasSection($name);
+ } else {
+ $exists = Config::navigation($itemConfig->type, $ownerName)->hasSection($name);
+ }
+ } else {
+ $exists = (bool) $this->getShareConfig()
+ ->select()
+ ->where('name', $name)
+ ->where('owner', $ownerName)
+ ->count();
+ }
+ }
+
+ if ($exists) {
+ throw new IcingaException(
+ $this->translate('A navigation item with the name "%s" does already exist'),
+ $name
+ );
+ }
+ }
+
+ unset($data['name']);
+ }
+
+ $itemConfig->merge($data);
+
+ if ($shared) {
+ // Share all descendant children
+ foreach ($this->getFlattenedChildren($oldName ?: $name) as $child) {
+ $childConfig = $this->secondaryConfig->getSection($child);
+ $this->secondaryConfig->removeSection($child);
+ $childConfig->owner = $this->getUser()->getUsername();
+ $config->setSection($child, $childConfig);
+ }
+ }
+
+ if ($oldName) {
+ // Update the parent name on all direct children
+ foreach ($config as $sectionConfig) {
+ if ($sectionConfig->parent === $oldName) {
+ $sectionConfig->parent = $name;
+ }
+ }
+
+ $config->removeSection($oldName);
+ }
+
+ if ($this->secondaryConfig !== null) {
+ $this->secondaryConfig->removeSection($oldName ?: $name);
+ }
+
+ $config->setSection($name, $itemConfig);
+ $this->setIniConfig($config);
+ return $this;
+ }
+
+ /**
+ * Remove a navigation item
+ *
+ * @param string $name
+ *
+ * @return ConfigObject The navigation item's config
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case the navigation item has still children
+ */
+ public function delete($name)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $children = $this->getFlattenedChildren($name);
+ if (! empty($children)) {
+ throw new IcingaException(
+ $this->translate(
+ 'Unable to delete navigation item "%s". There'
+ . ' are other items dependent from it: %s'
+ ),
+ $name,
+ join(', ', $children)
+ );
+ }
+
+ $section = $config->getSection($name);
+ $config->removeSection($name);
+ $this->setIniConfig($config);
+ return $section;
+ }
+
+ /**
+ * Unshare the given navigation item
+ *
+ * @param string $name
+ * @param string $parent
+ *
+ * @return Config The new config of the given navigation item
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case the navigation item has a parent assigned to it
+ */
+ public function unshare($name, $parent = null)
+ {
+ $config = $this->getShareConfig();
+ if (! $config->hasSection($name)) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $itemConfig = $config->getSection($name);
+ if ($parent === null) {
+ $parent = $itemConfig->parent;
+ }
+
+ if ($parent && $this->hasBeenShared($parent)) {
+ throw new IcingaException(
+ $this->translate(
+ 'Unable to unshare navigation item "%s". It is dependent from item "%s".'
+ . ' Dependent items can only be unshared by unsharing their parent'
+ ),
+ $name,
+ $parent
+ );
+ }
+
+ $children = $this->getFlattenedChildren($name);
+ $config->removeSection($name);
+ $this->secondaryConfig = $config;
+
+ if (! $itemConfig->owner || $itemConfig->owner === $this->getUser()->getUsername()) {
+ $config = $this->getUserConfig();
+ } else {
+ $config = Config::navigation($itemConfig->type, $itemConfig->owner);
+ }
+
+ foreach ($children as $child) {
+ $childConfig = $this->secondaryConfig->getSection($child);
+ unset($childConfig->owner);
+ $this->secondaryConfig->removeSection($child);
+ $config->setSection($child, $childConfig);
+ }
+
+ unset($itemConfig->owner);
+ unset($itemConfig->users);
+ unset($itemConfig->groups);
+
+ $config->setSection($name, $itemConfig);
+ $this->setIniConfig($config);
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $shared = false;
+ $itemTypes = $this->getItemTypes();
+ $itemType = isset($formData['type']) ? $formData['type'] : key($itemTypes);
+ if ($itemType === null) {
+ throw new ProgrammingError(
+ 'This should actually not happen. Create a bug report at https://github.com/icinga/icingaweb2'
+ . ' or remove this assertion if you know what you\'re doing'
+ );
+ }
+
+ $itemForm = $this->getItemForm($itemType);
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Name'),
+ 'description' => $this->translate(
+ 'The name of this navigation item that is used to differentiate it from others'
+ )
+ )
+ );
+
+ if ((! $itemForm->requiresParentSelection() || ! isset($formData['parent']) || ! $formData['parent'])
+ && $this->getUser()->can('user/share/navigation')
+ ) {
+ $checked = isset($formData['shared']) ? null : (isset($formData['users']) || isset($formData['groups']));
+
+ $this->addElement(
+ 'checkbox',
+ 'shared',
+ array(
+ 'autosubmit' => true,
+ 'ignore' => true,
+ 'value' => $checked,
+ 'label' => $this->translate('Shared'),
+ 'description' => $this->translate('Tick this box to share this item with others')
+ )
+ );
+
+ if ($checked || (isset($formData['shared']) && $formData['shared'])) {
+ $shared = true;
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Comma separated list of usernames to share this item with'
+ )
+ )
+ );
+ $this->addElement(
+ 'textarea',
+ 'groups',
+ array(
+ 'label' => $this->translate('Groups'),
+ 'description' => $this->translate(
+ 'Comma separated list of group names to share this item with'
+ )
+ )
+ );
+ }
+ }
+
+ if (empty($itemTypes) || count($itemTypes) === 1) {
+ $this->addElement(
+ 'hidden',
+ 'type',
+ array(
+ 'value' => $itemType
+ )
+ );
+ } else {
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Type'),
+ 'description' => $this->translate('The type of this navigation item'),
+ 'multiOptions' => $itemTypes
+ )
+ );
+ }
+
+ if (! $shared && $itemForm->requiresParentSelection()) {
+ if ($this->itemToLoad && $this->hasBeenShared($this->itemToLoad)) {
+ $itemConfig = $this->getShareConfig()->getSection($this->itemToLoad);
+ $availableParents = $this->listAvailableParents($itemType, $itemConfig->owner);
+ } else {
+ $availableParents = $this->listAvailableParents($itemType);
+ }
+
+ $this->addElement(
+ 'select',
+ 'parent',
+ array(
+ 'allowEmpty' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Parent'),
+ 'description' => $this->translate(
+ 'The parent item to assign this navigation item to. '
+ . 'Select "None" to make this a main navigation item'
+ ),
+ 'multiOptions' => ['' => $this->translate('None', 'No parent for a navigation item')]
+ + (empty($availableParents) ? [] : array_combine($availableParents, $availableParents))
+ )
+ );
+ } else {
+ $this->addElement('hidden', 'parent', ['disabled' => true]);
+ }
+
+ $this->addSubForm($itemForm, 'item_form');
+ $itemForm->create($formData); // May require a parent which gets set by addSubForm()
+ }
+
+ /**
+ * DO NOT USE! This will be removed soon, very soon...
+ */
+ public function setDefaultUrl($url)
+ {
+ $this->defaultUrl = $url;
+ }
+
+ /**
+ * Populate the configuration of the navigation item to load
+ */
+ public function onRequest()
+ {
+ if ($this->itemToLoad) {
+ $data = $this->getConfigForItem($this->itemToLoad)->getSection($this->itemToLoad)->toArray();
+ $data['name'] = $this->itemToLoad;
+ $this->populate($data);
+ } elseif ($this->defaultUrl !== null) {
+ $this->populate(array('url' => $this->defaultUrl));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ $valid = true;
+ if (isset($formData['users']) && $formData['users']) {
+ $parsedUserRestrictions = array();
+ foreach (Auth::getInstance()->getRestrictions('application/share/users') as $userRestriction) {
+ $parsedUserRestrictions[] = array_map('trim', explode(',', $userRestriction));
+ }
+
+ if (! empty($parsedUserRestrictions)) {
+ $desiredUsers = array_map('trim', explode(',', $formData['users']));
+ array_unshift($parsedUserRestrictions, $desiredUsers);
+ $forbiddenUsers = call_user_func_array('array_diff', $parsedUserRestrictions);
+ if (! empty($forbiddenUsers)) {
+ $valid = false;
+ $this->getElement('users')->addError(
+ sprintf(
+ $this->translate(
+ 'You are not permitted to share this navigation item with the following users: %s'
+ ),
+ implode(', ', $forbiddenUsers)
+ )
+ );
+ }
+ }
+ }
+
+ if (isset($formData['groups']) && $formData['groups']) {
+ $parsedGroupRestrictions = array();
+ foreach (Auth::getInstance()->getRestrictions('application/share/groups') as $groupRestriction) {
+ $parsedGroupRestrictions[] = array_map('trim', explode(',', $groupRestriction));
+ }
+
+ if (! empty($parsedGroupRestrictions)) {
+ $desiredGroups = array_map('trim', explode(',', $formData['groups']));
+ array_unshift($parsedGroupRestrictions, $desiredGroups);
+ $forbiddenGroups = call_user_func_array('array_diff', $parsedGroupRestrictions);
+ if (! empty($forbiddenGroups)) {
+ $valid = false;
+ $this->getElement('groups')->addError(
+ sprintf(
+ $this->translate(
+ 'You are not permitted to share this navigation item with the following groups: %s'
+ ),
+ implode(', ', $forbiddenGroups)
+ )
+ );
+ }
+ }
+ }
+
+ return $valid;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function writeConfig(Config $config)
+ {
+ parent::writeConfig($config);
+
+ if ($this->secondaryConfig !== null) {
+ $this->config = $this->secondaryConfig; // Causes the config being displayed to the user in case of an error
+ parent::writeConfig($this->secondaryConfig);
+ }
+ }
+
+ /**
+ * Return the navigation configuration the given item is a part of
+ *
+ * @param string $name
+ *
+ * @return Config|null In case the item is not part of any configuration
+ */
+ protected function getConfigForItem($name)
+ {
+ if ($this->getUserConfig()->hasSection($name)) {
+ return $this->getUserConfig();
+ } elseif ($this->getShareConfig()->hasSection($name)) {
+ if ($this->getShareConfig()->get($name, 'owner') === $this->getUser()->getUsername()
+ || $this->getUser()->can('user/share/navigation')
+ ) {
+ return $this->getShareConfig();
+ }
+ }
+ }
+
+ /**
+ * Return whether the given navigation item has been shared
+ *
+ * @param string $name
+ * @param string $type
+ *
+ * @return bool
+ */
+ protected function hasBeenShared($name, $type = null)
+ {
+ return $this->getShareConfig($type) === $this->getConfigForItem($name);
+ }
+
+ /**
+ * Return the form for the given type of navigation item
+ *
+ * @param string $type
+ *
+ * @return Form
+ */
+ protected function getItemForm($type)
+ {
+ $className = StringHelper::cname($type, '-') . 'Form';
+
+ $form = null;
+ $classPath = null;
+ foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
+ $classPath = 'Icinga\\Module\\'
+ . ucfirst($module->getName())
+ . '\\'
+ . static::FORM_NS
+ . '\\'
+ . $className;
+ if (class_exists($classPath)) {
+ $form = new $classPath();
+ break;
+ }
+ }
+
+ if ($form === null) {
+ $classPath = 'Icinga\\' . static::FORM_NS . '\\' . $className;
+ if (class_exists($classPath)) {
+ $form = new $classPath();
+ }
+ }
+
+ if ($form === null) {
+ Logger::debug(
+ 'Failed to find custom navigation item form %s for item %s. Using form NavigationItemForm now',
+ $className,
+ $type
+ );
+
+ $form = new NavigationItemForm();
+ } elseif (! $form instanceof NavigationItemForm) {
+ throw new ProgrammingError('Class %s must inherit from NavigationItemForm', $classPath);
+ }
+
+ return $form;
+ }
+}
diff --git a/application/forms/Navigation/NavigationItemForm.php b/application/forms/Navigation/NavigationItemForm.php
new file mode 100644
index 0000000..6cf15e7
--- /dev/null
+++ b/application/forms/Navigation/NavigationItemForm.php
@@ -0,0 +1,114 @@
+requiresParentSelection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'select',
+ 'target',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Target'),
+ 'description' => $this->translate('The target where to open this navigation item\'s url'),
+ 'multiOptions' => array(
+ '_blank' => $this->translate('New Window'),
+ '_next' => $this->translate('New Column'),
+ '_main' => $this->translate('Single Column'),
+ '_self' => $this->translate('Current Column')
+ )
+ )
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'url',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'The url of this navigation item. Leave blank if only the name should be displayed.'
+ . ' For urls with username and password and for all external urls,'
+ . ' make sure to prepend an appropriate protocol identifier (e.g. http://example.tld)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($url) {
+ // Matches if the given url contains obviously
+ // a username but not any protocol identifier
+ return !preg_match('#^((?=[^/@]).)+@.*$#', $url);
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate(
+ 'Missing protocol identifier'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'icon',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Icon'),
+ 'description' => $this->translate(
+ 'The icon of this navigation item. Leave blank if you do not want a icon being displayed'
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ // The regex here specifically matches the port-macro as it's the only one preventing Url::fromPath() from
+ // successfully parsing the given url. Any other macro such as for the scheme or host simply gets identified
+ // as path which is just fine in this case.
+ if (isset($values['url']) && $values['url'] && !preg_match('~://.+:\d*?(\$.+\$)~', $values['url'])) {
+ $url = Url::fromPath($values['url']);
+ if ($url->getBasePath() === $this->getRequest()->getBasePath()) {
+ $values['url'] = $url->getRelativeUrl();
+ } else {
+ $values['url'] = $url->getAbsoluteUrl();
+ }
+ }
+
+ return $values;
+ }
+}
diff --git a/application/forms/PreferenceForm.php b/application/forms/PreferenceForm.php
new file mode 100644
index 0000000..3e431db
--- /dev/null
+++ b/application/forms/PreferenceForm.php
@@ -0,0 +1,485 @@
+setName('form_config_preferences');
+ $this->setSubmitLabel($this->translate('Save to the Preferences'));
+ }
+
+ /**
+ * Set preferences to work with
+ *
+ * @param Preferences $preferences The preferences to work with
+ *
+ * @return $this
+ */
+ public function setPreferences(Preferences $preferences)
+ {
+ $this->preferences = $preferences;
+ return $this;
+ }
+
+ /**
+ * Set the preference store to use
+ *
+ * @param PreferencesStore $store The preference store to use
+ *
+ * @return $this
+ */
+ public function setStore(PreferencesStore $store)
+ {
+ $this->store = $store;
+ return $this;
+ }
+
+ /**
+ * Persist preferences
+ *
+ * @return $this
+ */
+ public function save()
+ {
+ $this->store->save($this->preferences);
+ return $this;
+ }
+
+ /**
+ * Adjust preferences and persist them
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $currentPreferences = $this->Auth()->getUser()->getPreferences();
+ $oldTheme = $currentPreferences->getValue('icingaweb', 'theme');
+ $oldMode = $currentPreferences->getValue('icingaweb', 'theme_mode');
+ $oldLocale = $currentPreferences->getValue('icingaweb', 'language');
+ $defaultTheme = Config::app()->get('themes', 'default', StyleSheet::DEFAULT_THEME);
+
+ $this->preferences = new Preferences($this->store ? $this->store->load() : array());
+ $webPreferences = $this->preferences->get('icingaweb');
+ foreach ($this->getValues() as $key => $value) {
+ if ($value === ''
+ || $value === null
+ || $value === 'autodetect'
+ || ($key === 'theme' && $value === $defaultTheme)
+ ) {
+ if (isset($webPreferences[$key])) {
+ unset($webPreferences[$key]);
+ }
+ } else {
+ $webPreferences[$key] = $value;
+ }
+ }
+ $this->preferences->icingaweb = $webPreferences;
+ Session::getSession()->user->setPreferences($this->preferences);
+
+ if ((($theme = $this->getElement('theme')) !== null
+ && ($theme = $theme->getValue()) !== $oldTheme
+ && ($theme !== $defaultTheme || $oldTheme !== null))
+ || (($mode = $this->getElement('theme_mode')) !== null
+ && ($mode->getValue()) !== $oldMode)
+ ) {
+ $this->getResponse()->setReloadCss(true);
+ }
+
+ if (($locale = $this->getElement('language')) !== null
+ && $locale->getValue() !== 'autodetect'
+ && $locale->getValue() !== $oldLocale
+ ) {
+ $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes');
+ }
+
+ try {
+ if ($this->store && $this->getElement('btn_submit')->isChecked()) {
+ $this->save();
+ Notification::success($this->translate('Preferences successfully saved'));
+ } else {
+ Notification::success($this->translate('Preferences successfully saved for the current session'));
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::error($e->getMessage());
+ }
+ }
+
+ /**
+ * Populate preferences
+ *
+ * @see Form::onRequest()
+ */
+ public function onRequest()
+ {
+ $auth = Auth::getInstance();
+ $values = $auth->getUser()->getPreferences()->get('icingaweb');
+
+ if (! isset($values['language'])) {
+ $values['language'] = 'autodetect';
+ }
+
+ if (! isset($values['timezone'])) {
+ $values['timezone'] = 'autodetect';
+ }
+
+ if (! isset($values['auto_refresh'])) {
+ $values['auto_refresh'] = '1';
+ }
+
+ $this->populate($values);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ if (setlocale(LC_ALL, 0) === 'C') {
+ $this->warning(
+ $this->translate(
+ 'Your language setting is not applied because your platform is missing the corresponding locale.'
+ . ' Make sure to install the correct language pack and restart your web server afterwards.'
+ ),
+ false
+ );
+ }
+
+ $themeFile = StyleSheet::getThemeFile(Config::app()->get('themes', 'default'));
+ if (! (bool) Config::app()->get('themes', 'disabled', false)) {
+ $themes = Icinga::app()->getThemes();
+ if (count($themes) > 1) {
+ $defaultTheme = Config::app()->get('themes', 'default', StyleSheet::DEFAULT_THEME);
+ if (isset($themes[$defaultTheme])) {
+ $themes[$defaultTheme] .= ' (' . $this->translate('default') . ')';
+ }
+ $this->addElement(
+ 'select',
+ 'theme',
+ array(
+ 'label' => $this->translate('Theme', 'Form element label'),
+ 'multiOptions' => $themes,
+ 'autosubmit' => true,
+ 'value' => $this->preferences->getValue(
+ 'icingaweb',
+ 'theme',
+ $defaultTheme
+ )
+ )
+ );
+ }
+ }
+
+ if (isset($formData['theme'])) {
+ $themeFile = StyleSheet::getThemeFile($formData['theme']);
+ }
+
+ $disabled = [];
+ if ($themeFile !== null) {
+ $file = @file_get_contents($themeFile);
+ if ($file && strpos($file, StyleSheet::LIGHT_MODE_IDENTIFIER) === false) {
+ $disabled = ['', 'light', 'system'];
+ }
+ }
+
+ $this->addElement(
+ 'radio',
+ 'theme_mode',
+ [
+ 'class' => 'theme-mode-input',
+ 'label' => $this->translate('Theme Mode'),
+ 'multiOptions' => [
+ '' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-dark.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('Dark')),
+ 'light' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-light.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('Light')),
+ 'system' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-system.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('System'))
+ ],
+ 'disable' => $disabled,
+ 'escape' => false,
+ 'decorators' => array_merge(
+ array_slice(self::$defaultElementDecorators, 0, -1),
+ [['HtmlTag', ['tag' => 'div', 'class' => 'control-group theme-mode']]]
+ )
+ ]
+ );
+
+ /** @var GettextTranslator $translator */
+ $translator = StaticTranslator::$instance;
+
+ $languages = array();
+ $availableLocales = $translator->listLocales();
+
+ $locale = $this->getLocale($availableLocales);
+ if ($locale !== null) {
+ $languages['autodetect'] = sprintf($this->translate('Browser (%s)', 'preferences.form'), $locale);
+ }
+
+ $availableLocales[] = $translator->getDefaultLocale();
+ foreach ($availableLocales as $language) {
+ $languages[$language] = $language;
+ }
+
+ $tzList = array();
+ $tzList['autodetect'] = sprintf(
+ $this->translate('Browser (%s)', 'preferences.form'),
+ $this->getDefaultTimezone()
+ );
+ foreach (DateTimeZone::listIdentifiers() as $tz) {
+ $tzList[$tz] = $tz;
+ }
+
+ $this->addElement(
+ 'select',
+ 'language',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Your Current Language'),
+ 'description' => $this->translate('Use the following language to display texts and messages'),
+ 'multiOptions' => $languages,
+ 'value' => substr(setlocale(LC_ALL, 0), 0, 5)
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'timezone',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Your Current Timezone'),
+ 'description' => $this->translate('Use the following timezone for dates and times'),
+ 'multiOptions' => $tzList,
+ 'value' => $this->getDefaultTimezone()
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'show_application_state_messages',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Show application state messages'),
+ 'description' => $this->translate('Whether to show application state messages.'),
+ 'multiOptions' => [
+ 'system' => (bool) Config::app()->get('global', 'show_application_state_messages', true)
+ ? $this->translate('System (Yes)')
+ : $this->translate('System (No)'),
+ 1 => $this->translate('Yes'),
+ 0 => $this->translate('No')],
+ 'value' => 'system'
+ )
+ );
+
+ if (Auth::getInstance()->hasPermission('user/application/stacktraces')) {
+ $this->addElement(
+ 'checkbox',
+ 'show_stacktraces',
+ array(
+ 'value' => $this->getDefaultShowStacktraces(),
+ 'label' => $this->translate('Show Stacktraces'),
+ 'description' => $this->translate('Set whether to show an exception\'s stacktrace.')
+ )
+ );
+ }
+
+ $this->addElement(
+ 'checkbox',
+ 'show_benchmark',
+ array(
+ 'label' => $this->translate('Use benchmark')
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'auto_refresh',
+ array(
+ 'required' => false,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Enable auto refresh'),
+ 'description' => $this->translate(
+ 'This option allows you to enable or to disable the global page content auto refresh'
+ ),
+ 'value' => 1
+ )
+ );
+
+ if (isset($formData['auto_refresh']) && $formData['auto_refresh']) {
+ $this->addElement(
+ 'select',
+ 'auto_refresh_speed',
+ [
+ 'required' => false,
+ 'label' => $this->translate('Auto refresh speed'),
+ 'description' => $this->translate(
+ 'This option allows you to speed up or to slow down the global page content auto refresh'
+ ),
+ 'multiOptions' => [
+ '0.5' => $this->translate('Fast', 'refresh_speed'),
+ '' => $this->translate('Default', 'refresh_speed'),
+ '2' => $this->translate('Moderate', 'refresh_speed'),
+ '4' => $this->translate('Slow', 'refresh_speed')
+ ],
+ 'value' => ''
+ ]
+ );
+ }
+
+ $this->addElement(
+ 'number',
+ 'default_page_size',
+ array(
+ 'label' => $this->translate('Default page size'),
+ 'description' => $this->translate('Default number of items per page for list views'),
+ 'placeholder' => 25,
+ 'min' => 25,
+ 'step' => 1
+ )
+ );
+
+ if ($this->store) {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Save to the Preferences'),
+ 'decorators' => array('ViewHelper'),
+ 'class' => 'btn-primary'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'submit',
+ 'btn_submit_session',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Save for the current Session'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'preferences-progress');
+ $this->addElement(
+ 'note',
+ 'preferences-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'preferences-progress'))
+ )
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('btn_submit', 'btn_submit_session', 'preferences-progress'),
+ 'submit_buttons',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+ }
+
+ public function addSubmitButton()
+ {
+ return $this;
+ }
+
+ public function isSubmitted()
+ {
+ if (parent::isSubmitted()) {
+ return true;
+ }
+
+ return $this->getElement('btn_submit_session')->isChecked();
+ }
+
+ /**
+ * Return the current default timezone
+ *
+ * @return string
+ */
+ protected function getDefaultTimezone()
+ {
+ $detect = new TimezoneDetect();
+ if ($detect->success()) {
+ return $detect->getTimezoneName();
+ } else {
+ return @date_default_timezone_get();
+ }
+ }
+
+ /**
+ * Return the preferred locale based on the given HTTP header and the available translations
+ *
+ * @return string|null
+ */
+ protected function getLocale($availableLocales)
+ {
+ return isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])
+ ? (new Locale())->getPreferred($_SERVER['HTTP_ACCEPT_LANGUAGE'], $availableLocales)
+ : null;
+ }
+
+ /**
+ * Return the default global setting for show_stacktraces
+ *
+ * @return bool
+ */
+ protected function getDefaultShowStacktraces()
+ {
+ return Config::app()->get('global', 'show_stacktraces', true);
+ }
+}
diff --git a/application/forms/RepositoryForm.php b/application/forms/RepositoryForm.php
new file mode 100644
index 0000000..8e4665d
--- /dev/null
+++ b/application/forms/RepositoryForm.php
@@ -0,0 +1,453 @@
+repository = $repository;
+ return $this;
+ }
+
+ /**
+ * Return the target being worked with
+ *
+ * @return mixed
+ */
+ protected function getBaseTable()
+ {
+ if ($this->baseTable === null) {
+ return $this->repository->getBaseTable();
+ }
+
+ return $this->baseTable;
+ }
+
+ /**
+ * Return the name of the entry to handle
+ *
+ * @return string
+ */
+ protected function getIdentifier()
+ {
+ return $this->identifier;
+ }
+
+ /**
+ * Return the current data of the entry being handled
+ *
+ * @return array
+ */
+ protected function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Return whether an entry should be inserted
+ *
+ * @return bool
+ */
+ public function shouldInsert()
+ {
+ return $this->mode === self::MODE_INSERT;
+ }
+
+ /**
+ * Return whether an entry should be udpated
+ *
+ * @return bool
+ */
+ public function shouldUpdate()
+ {
+ return $this->mode === self::MODE_UPDATE;
+ }
+
+ /**
+ * Return whether an entry should be deleted
+ *
+ * @return bool
+ */
+ public function shouldDelete()
+ {
+ return $this->mode === self::MODE_DELETE;
+ }
+
+ /**
+ * Add a new entry
+ *
+ * @param array $data The defaults to use, if any
+ *
+ * @return $this
+ */
+ public function add(array $data = null)
+ {
+ $this->mode = static::MODE_INSERT;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Edit an entry
+ *
+ * @param string $name The entry's name
+ * @param array $data The entry's current data
+ *
+ * @return $this
+ */
+ public function edit($name, array $data = null)
+ {
+ $this->mode = static::MODE_UPDATE;
+ $this->identifier = $name;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Remove an entry
+ *
+ * @param string $name The entry's name
+ *
+ * @return $this
+ */
+ public function remove($name)
+ {
+ $this->mode = static::MODE_DELETE;
+ $this->identifier = $name;
+ return $this;
+ }
+
+ /**
+ * Fetch and return the entry to pre-populate the form with when in mode update
+ *
+ * @return false|object
+ */
+ protected function fetchEntry()
+ {
+ return $this->repository
+ ->select()
+ ->from($this->getBaseTable())
+ ->applyFilter($this->createFilter())
+ ->fetchRow();
+ }
+
+ /**
+ * Return whether the entry supposed to be removed exists
+ *
+ * @return bool
+ */
+ protected function entryExists()
+ {
+ $count = $this->repository
+ ->select()
+ ->from($this->getBaseTable())
+ ->addFilter($this->createFilter())
+ ->count();
+ return $count > 0;
+ }
+
+ /**
+ * Insert the new entry
+ */
+ protected function insertEntry()
+ {
+ $this->repository->insert($this->getBaseTable(), $this->getValues());
+ }
+
+ /**
+ * Update the entry
+ */
+ protected function updateEntry()
+ {
+ $this->repository->update($this->getBaseTable(), $this->getValues(), $this->createFilter());
+ }
+
+ /**
+ * Delete the entry
+ */
+ protected function deleteEntry()
+ {
+ $this->repository->delete($this->getBaseTable(), $this->createFilter());
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ if ($this->shouldInsert()) {
+ $this->createInsertElements($formData);
+ } elseif ($this->shouldUpdate()) {
+ $this->createUpdateElements($formData);
+ } elseif ($this->shouldDelete()) {
+ $this->createDeleteElements($formData);
+ }
+ }
+
+ /**
+ * Prepare the form for the requested mode
+ */
+ public function onRequest()
+ {
+ if ($this->shouldInsert()) {
+ $this->onInsertRequest();
+ } elseif ($this->shouldUpdate()) {
+ $this->onUpdateRequest();
+ } elseif ($this->shouldDelete()) {
+ $this->onDeleteRequest();
+ }
+ }
+
+ /**
+ * Prepare the form for mode insert
+ *
+ * Populates the form with the data passed to add().
+ */
+ protected function onInsertRequest()
+ {
+ $data = $this->getData();
+ if (! empty($data)) {
+ $this->setDefaults($data);
+ }
+ }
+
+ /**
+ * Prepare the form for mode update
+ *
+ * Populates the form with either the data passed to edit() or tries to fetch it from the repository.
+ *
+ * @throws NotFoundError In case the entry to update cannot be found
+ */
+ protected function onUpdateRequest()
+ {
+ $data = $this->getData();
+ if ($data === null) {
+ $row = $this->fetchEntry();
+ if ($row === false) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+
+ $data = get_object_vars($row);
+ }
+
+ $this->setDefaults($data);
+ }
+
+ /**
+ * Prepare the form for mode delete
+ *
+ * Verifies that the repository contains the entry to delete.
+ *
+ * @throws NotFoundError In case the entry to delete cannot be found
+ */
+ protected function onDeleteRequest()
+ {
+ if (! $this->entryExists()) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Apply the requested mode on the repository
+ *
+ * @return ?bool
+ */
+ public function onSuccess()
+ {
+ if ($this->shouldInsert()) {
+ return $this->onInsertSuccess();
+ } elseif ($this->shouldUpdate()) {
+ return $this->onUpdateSuccess();
+ } elseif ($this->shouldDelete()) {
+ return $this->onDeleteSuccess();
+ }
+ }
+
+ /**
+ * Apply mode insert on the repository
+ *
+ * @return bool
+ */
+ protected function onInsertSuccess()
+ {
+ try {
+ $this->insertEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getInsertMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getInsertMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode update on the repository
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ try {
+ $this->updateEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getUpdateMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getUpdateMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode delete on the repository
+ *
+ * @return bool
+ */
+ protected function onDeleteSuccess()
+ {
+ try {
+ $this->deleteEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getDeleteMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getDeleteMessage(true));
+ return true;
+ }
+
+ /**
+ * Create and add elements to this form to insert an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createInsertElements(array $formData);
+
+ /**
+ * Create and add elements to this form to update an entry
+ *
+ * Calls createInsertElements() by default. Overwrite this to add different elements when in mode update.
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+ }
+
+ /**
+ * Create and add elements to this form to delete an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createDeleteElements(array $formData);
+
+ /**
+ * Create and return a filter to use when selecting, updating or deleting an entry
+ *
+ * @return Filter
+ */
+ abstract protected function createFilter();
+
+ /**
+ * Return a notification message to use when inserting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getInsertMessage($success);
+
+ /**
+ * Return a notification message to use when updating an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getUpdateMessage($success);
+
+ /**
+ * Return a notification message to use when deleting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getDeleteMessage($success);
+}
diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php
new file mode 100644
index 0000000..58387f7
--- /dev/null
+++ b/application/forms/Security/RoleForm.php
@@ -0,0 +1,632 @@
+setAttrib('class', self::DEFAULT_CLASSES . ' role-form');
+
+ list($this->providedPermissions, $this->providedRestrictions) = static::collectProvidedPrivileges();
+ }
+
+ protected function createFilter()
+ {
+ return Filter::where('name', $this->getIdentifier());
+ }
+
+ public function filterName($value, $allowBrackets = false)
+ {
+ return parent::filterName($value, $allowBrackets) . '_element';
+ }
+
+ public function createInsertElements(array $formData = array())
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ [
+ 'required' => true,
+ 'label' => $this->translate('Role Name'),
+ 'description' => $this->translate('The name of the role')
+ ]
+ );
+ $this->addElement(
+ 'select',
+ 'parent',
+ [
+ 'label' => $this->translate('Inherit From'),
+ 'description' => $this->translate('Choose a role from which to inherit privileges'),
+ 'value' => '',
+ 'multiOptions' => array_merge(
+ ['' => $this->translate('None', 'parent role')],
+ $this->collectRoles()
+ )
+ ]
+ );
+ $this->addElement(
+ 'textarea',
+ 'users',
+ [
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate('Comma-separated list of users that are assigned to the role')
+ ]
+ );
+ $this->addElement(
+ 'textarea',
+ 'groups',
+ [
+ 'label' => $this->translate('Groups'),
+ 'description' => $this->translate('Comma-separated list of groups that are assigned to the role')
+ ]
+ );
+ $this->addElement(
+ 'checkbox',
+ self::WILDCARD_NAME,
+ [
+ 'autosubmit' => true,
+ 'label' => $this->translate('Administrative Access'),
+ 'description' => $this->translate('Everything is allowed')
+ ]
+ );
+ $this->addElement(
+ 'checkbox',
+ 'unrestricted',
+ [
+ 'autosubmit' => true,
+ 'uncheckedValue' => null,
+ 'label' => $this->translate('Unrestricted Access'),
+ 'description' => $this->translate('Access to any data is completely unrestricted')
+ ]
+ );
+
+ $hasAdminPerm = isset($formData[self::WILDCARD_NAME]) && $formData[self::WILDCARD_NAME];
+ $isUnrestricted = isset($formData['unrestricted']) && $formData['unrestricted'];
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $this->sortPermissions($permissionList);
+
+ $anythingGranted = false;
+ $anythingRefused = false;
+ $anythingRestricted = false;
+
+ $elements = [$moduleName . '_header'];
+ // The actual element is added last
+
+ $elements[] = 'permission_header';
+ $this->addElement('note', 'permission_header', [
+ 'decorators' => [['Callback', ['callback' => function () {
+ return '' . $this->translate('Permissions') . ' '
+ . $this->getView()->icon('ok', $this->translate(
+ 'Grant access by toggling a switch below'
+ ))
+ . $this->getView()->icon('cancel', $this->translate(
+ 'Deny access by toggling a switch below'
+ ));
+ }]], ['HtmlTag', ['tag' => 'div']]]
+ ]);
+
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+
+ if (isset($formData[$elementName]) && $formData[$elementName]) {
+ $anythingGranted = true;
+ }
+
+ if ($hasFullPerm || $hasAdminPerm) {
+ $elementName .= '_fake';
+ }
+
+ $denyCheckbox = null;
+ if (! isset($spec['isFullPerm'])
+ && substr($name, 0, strlen(self::DENY_PREFIX)) !== self::DENY_PREFIX
+ ) {
+ $denyCheckbox = $this->createElement('checkbox', $this->filterName(self::DENY_PREFIX . $name), [
+ 'decorators' => ['ViewHelper']
+ ]);
+ $this->addElement($denyCheckbox);
+ $this->removeFromIteration($denyCheckbox->getName());
+
+ if (isset($formData[$denyCheckbox->getName()]) && $formData[$denyCheckbox->getName()]) {
+ $anythingRefused = true;
+ }
+ }
+
+ $elements[] = $elementName;
+ $this->addElement(
+ 'checkbox',
+ $elementName,
+ [
+ 'ignore' => $hasFullPerm || $hasAdminPerm,
+ 'autosubmit' => isset($spec['isFullPerm']),
+ 'disabled' => $hasFullPerm || $hasAdminPerm ?: null,
+ 'value' => $hasFullPerm || $hasAdminPerm,
+ 'label' => isset($spec['label'])
+ ? $spec['label']
+ : join('', iterator_to_array(call_user_func(function ($segments) {
+ foreach ($segments as $segment) {
+ if ($segment[0] === '/') {
+ // Adds a zero-width char after each slash to help browsers break onto newlines
+ yield '/';
+ yield '' . substr($segment, 1) . ' ';
+ } else {
+ yield '' . $segment . ' ';
+ }
+ }
+ }, preg_split(
+ '~(/[^/]+)~',
+ $name,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ )))),
+ 'description' => isset($spec['description']) ? $spec['description'] : $name,
+ 'decorators' => array_merge(
+ array_slice(self::$defaultElementDecorators, 0, 3),
+ [['Callback', ['callback' => function () use ($denyCheckbox) {
+ return $denyCheckbox ? $denyCheckbox->render() : '';
+ }]]],
+ array_slice(self::$defaultElementDecorators, 3)
+ )
+ ]
+ )
+ ->getElement($elementName)
+ ->getDecorator('Label')
+ ->setOption('escape', false);
+
+ if ($hasFullPerm || $hasAdminPerm) {
+ // Add a hidden element to preserve the configured permission value
+ $this->addElement('hidden', $this->filterName($name));
+ }
+
+ if (isset($spec['isFullPerm'])) {
+ $filteredName = $this->filterName($name);
+ $hasFullPerm = isset($formData[$filteredName]) && $formData[$filteredName];
+ }
+ }
+
+ if (isset($this->providedRestrictions[$moduleName])) {
+ $elements[] = 'restriction_header';
+ $this->addElement('note', 'restriction_header', [
+ 'value' => '' . $this->translate('Restrictions') . ' ',
+ 'decorators' => ['ViewHelper']
+ ]);
+
+ foreach ($this->providedRestrictions[$moduleName] as $name => $spec) {
+ $elementName = $this->filterName($name);
+
+ if (isset($formData[$elementName]) && $formData[$elementName]) {
+ $anythingRestricted = true;
+ }
+
+ $elements[] = $elementName;
+ $this->addElement(
+ 'text',
+ $elementName,
+ [
+ 'label' => isset($spec['label'])
+ ? $spec['label']
+ : join('', iterator_to_array(call_user_func(function ($segments) {
+ foreach ($segments as $segment) {
+ if ($segment[0] === '/') {
+ // Add zero-width char after each slash to help browsers break onto newlines
+ yield '/';
+ yield '' . substr($segment, 1) . ' ';
+ } else {
+ yield '' . $segment . ' ';
+ }
+ }
+ }, preg_split(
+ '~(/[^/]+)~',
+ $name,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ )))),
+ 'description' => $spec['description'],
+ 'class' => $isUnrestricted ? 'unrestricted-role' : '',
+ 'readonly' => $isUnrestricted ?: null
+ ]
+ )
+ ->getElement($elementName)
+ ->getDecorator('Label')
+ ->setOption('escape', false);
+ }
+ }
+
+ $this->addElement(
+ 'note',
+ $moduleName . '_header',
+ [
+ 'decorators' => ['ViewHelper'],
+ 'value' => ''
+ . '' . ($moduleName !== 'application'
+ ? sprintf('%s %s ', $moduleName, $this->translate('Module'))
+ : 'Icinga Web 2'
+ ) . ' '
+ . ''
+ . ($hasAdminPerm || $anythingGranted ? new Icon('check-circle', ['class' => 'granted']) : '')
+ . ($anythingRefused ? new Icon('times-circle', ['class' => 'refused']) : '')
+ . (! $isUnrestricted && $anythingRestricted
+ ? new Icon('filter', ['class' => 'restricted'])
+ : ''
+ )
+ . ' '
+ . new Icon('angles-down', ['class' => 'collapse-icon'])
+ . new Icon('angles-left', ['class' => 'expand-icon'])
+ . ' '
+ ]
+ );
+
+ $this->addDisplayGroup($elements, $moduleName . '_elements', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', [
+ 'tag' => 'details',
+ 'class' => 'collapsible'
+ ]],
+ ['Fieldset']
+ ]
+ ]);
+ }
+ }
+
+ protected function createDeleteElements(array $formData)
+ {
+ }
+
+ public function fetchEntry()
+ {
+ $role = parent::fetchEntry();
+ if ($role === false) {
+ return false;
+ }
+
+ $values = [
+ 'parent' => $role->parent,
+ 'name' => $role->name,
+ 'users' => $role->users,
+ 'groups' => $role->groups,
+ 'unrestricted' => $role->unrestricted,
+ self::WILDCARD_NAME => $role->permissions && preg_match('~(?>^|,)\*(?>$|,)~', $role->permissions)
+ ];
+
+ if (! empty($role->permissions) || ! empty($role->refusals)) {
+ $permissions = StringHelper::trimSplit($role->permissions);
+ $refusals = StringHelper::trimSplit($role->refusals);
+
+ list($permissions, $newRefusals) = AdmissionLoader::migrateLegacyPermissions($permissions);
+ if (! empty($newRefusals)) {
+ array_push($refusals, ...$newRefusals);
+ }
+
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ if (in_array($name, $permissions, true)) {
+ $values[$this->filterName($name)] = 1;
+
+ if (isset($spec['isFullPerm'])) {
+ $hasFullPerm = true;
+ }
+ }
+
+ if (in_array($name, $refusals, true)) {
+ $values[$this->filterName(self::DENY_PREFIX . $name)] = 1;
+ }
+ }
+
+ if ($hasFullPerm) {
+ unset($values[$this->filterName(Manager::MODULE_PERMISSION_NS . $moduleName)]);
+ }
+ }
+ }
+
+ foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
+ foreach ($restrictionList as $name => $spec) {
+ if (isset($role->$name)) {
+ $values[$this->filterName($name)] = $role->$name;
+ }
+ }
+ }
+
+ return (object) $values;
+ }
+
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+
+ foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
+ foreach ($restrictionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+ if (isset($values[$elementName])) {
+ $values[$name] = $values[$elementName];
+ unset($values[$elementName]);
+ }
+ }
+ }
+
+ $permissions = [];
+ if (isset($values[self::WILDCARD_NAME]) && $values[self::WILDCARD_NAME]) {
+ $permissions[] = '*';
+ }
+
+ $refusals = [];
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+ if (isset($values[$elementName]) && $values[$elementName]) {
+ $permissions[] = $name;
+
+ if (isset($spec['isFullPerm'])) {
+ $hasFullPerm = true;
+ }
+ }
+
+ $denyName = $this->filterName(self::DENY_PREFIX . $name);
+ if (isset($values[$denyName]) && $values[$denyName]) {
+ $refusals[] = $name;
+ }
+
+ unset($values[$elementName], $values[$denyName]);
+ }
+
+ $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
+ if ($hasFullPerm && ! in_array($modulePermission, $permissions, true)) {
+ $permissions[] = $modulePermission;
+ }
+ }
+
+ unset($values[self::WILDCARD_NAME]);
+ $values['refusals'] = join(',', $refusals);
+ $values['permissions'] = join(',', $permissions);
+ return ConfigForm::transformEmptyValuesToNull($values);
+ }
+
+ protected function getInsertMessage($success)
+ {
+ return $success ? $this->translate('Role created') : $this->translate('Role creation failed');
+ }
+
+ protected function getUpdateMessage($success)
+ {
+ return $success ? $this->translate('Role updated') : $this->translate('Role update failed');
+ }
+
+ protected function getDeleteMessage($success)
+ {
+ return $success ? $this->translate('Role removed') : $this->translate('Role removal failed');
+ }
+
+ protected function sortPermissions(&$permissions)
+ {
+ return uksort($permissions, function ($a, $b) use ($permissions) {
+ if (isset($permissions[$a]['isUsagePerm'])) {
+ return isset($permissions[$b]['isFullPerm']) ? 1 : -1;
+ } elseif (isset($permissions[$b]['isUsagePerm'])) {
+ return isset($permissions[$a]['isFullPerm']) ? -1 : 1;
+ }
+
+ $aParts = explode('/', $a);
+ $bParts = explode('/', $b);
+
+ do {
+ $a = array_shift($aParts);
+ $b = array_shift($bParts);
+ } while ($a === $b);
+
+ return strnatcmp($a ?? '', $b ?? '');
+ });
+ }
+
+ protected function collectRoles()
+ {
+ // Function to get all connected children. Used to avoid reference loops
+ $getChildren = function ($name, $children = []) use (&$getChildren) {
+ foreach ($this->repository->select()->where('parent', $name) as $child) {
+ if (isset($children[$child->name])) {
+ // Don't follow already established loops here,
+ // the user should be able to solve such in the UI
+ continue;
+ }
+
+ $children[$child->name] = true;
+ $children = $getChildren($child->name, $children);
+ }
+
+ return $children;
+ };
+
+ $children = $this->getIdentifier() !== null ? $getChildren($this->getIdentifier()) : [];
+
+ $names = [];
+ foreach ($this->repository->select() as $role) {
+ if ($role->name !== $this->getIdentifier() && ! isset($children[$role->name])) {
+ $names[] = $role->name;
+ }
+ }
+
+ return array_combine($names, $names);
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if ($this->getIdentifier() && ($newName = $this->getValue('name')) !== $this->getIdentifier()) {
+ $this->repository->update(
+ $this->getBaseTable(),
+ ['parent' => $newName],
+ Filter::where('parent', $this->getIdentifier())
+ );
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+
+ /**
+ * Collect permissions and restrictions provided by Icinga Web 2 and modules
+ *
+ * @return array[$permissions, $restrictions]
+ */
+ public static function collectProvidedPrivileges()
+ {
+ $providedPermissions['application'] = [
+ 'application/announcements' => [
+ 'description' => t('Allow to manage announcements')
+ ],
+ 'application/log' => [
+ 'description' => t('Allow to view the application log')
+ ],
+ 'config/*' => [
+ 'description' => t('Allow full config access')
+ ],
+ 'config/general' => [
+ 'description' => t('Allow to adjust the general configuration')
+ ],
+ 'config/modules' => [
+ 'description' => t('Allow to enable/disable and configure modules')
+ ],
+ 'config/resources' => [
+ 'description' => t('Allow to manage resources')
+ ],
+ 'config/navigation' => [
+ 'description' => t('Allow to view and adjust shared navigation items')
+ ],
+ 'config/access-control/*' => [
+ 'description' => t('Allow to fully manage access-control')
+ ],
+ 'config/access-control/users' => [
+ 'description' => t('Allow to manage user accounts')
+ ],
+ 'config/access-control/groups' => [
+ 'description' => t('Allow to manage user groups')
+ ],
+ 'config/access-control/roles' => [
+ 'description' => t('Allow to manage roles')
+ ],
+ 'user/*' => [
+ 'description' => t('Allow all account related functionalities')
+ ],
+ 'user/password-change' => [
+ 'description' => t('Allow password changes in the account preferences')
+ ],
+ 'user/application/stacktraces' => [
+ 'description' => t('Allow to adjust in the preferences whether to show stacktraces')
+ ],
+ 'user/share/navigation' => [
+ 'description' => t('Allow to share navigation items')
+ ],
+ 'application/sessions' => [
+ 'description' => t('Allow to manage user sessions')
+ ],
+ 'application/migrations' => [
+ 'description' => t('Allow to apply pending application migrations')
+ ]
+ ];
+
+ $providedRestrictions['application'] = [
+ 'application/share/users' => [
+ 'description' => t('Restrict which users this role can share items and information with')
+ ],
+ 'application/share/groups' => [
+ 'description' => t('Restrict which groups this role can share items and information with')
+ ]
+ ];
+
+ $mm = Icinga::app()->getModuleManager();
+ foreach ($mm->listInstalledModules() as $moduleName) {
+ $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
+ $providedPermissions[$moduleName][$modulePermission] = [
+ 'isUsagePerm' => true,
+ 'label' => t('General Module Access'),
+ 'description' => sprintf(t('Allow access to module %s'), $moduleName)
+ ];
+
+ $module = $mm->getModule($moduleName, false);
+ $permissions = $module->getProvidedPermissions();
+
+ $providedPermissions[$moduleName][$moduleName . '/*'] = [
+ 'isFullPerm' => true,
+ 'label' => t('Full Module Access')
+ ];
+
+ foreach ($permissions as $permission) {
+ /** @var object $permission */
+ $providedPermissions[$moduleName][$permission->name] = [
+ 'description' => $permission->description
+ ];
+ }
+
+ foreach ($module->getProvidedRestrictions() as $restriction) {
+ $providedRestrictions[$moduleName][$restriction->name] = [
+ 'description' => $restriction->description
+ ];
+ }
+ }
+
+ return [$providedPermissions, $providedRestrictions];
+ }
+}
diff --git a/application/layouts/scripts/body.phtml b/application/layouts/scripts/body.phtml
new file mode 100644
index 0000000..87b570b
--- /dev/null
+++ b/application/layouts/scripts/body.phtml
@@ -0,0 +1,98 @@
+layout()->moduleName;
+if ($moduleName !== 'default') {
+ $moduleClass = ' icinga-module module-' . $moduleName;
+} else {
+ $moduleClass = '';
+}
+
+$refresh = '';
+if ($this->layout()->autorefreshInterval) {
+ $refresh = ' data-icinga-refresh="' . $this->layout()->autorefreshInterval . '"';
+}
+
+if ($this->layout()->inlineLayout) {
+ $inlineLayoutScript = $this->layout()->inlineLayout . '.phtml';
+} else {
+ $inlineLayoutScript = 'inline.phtml';
+}
+
+?>
+
+
+layout()->isIframe): ?>
+
+
+
+
+ data-icinga-module="= $moduleName ?>"
+
+ data-icinga-url="= $this->escape(Url::fromRequest()->without('renderLayout')->getAbsoluteUrl()); ?>"
+ = $refresh; ?>
+ >
+ = $this->render($inlineLayoutScript) ?>
+
+
+
+
+
+
diff --git a/application/layouts/scripts/external-logout.phtml b/application/layouts/scripts/external-logout.phtml
new file mode 100644
index 0000000..19b7e32
--- /dev/null
+++ b/application/layouts/scripts/external-logout.phtml
@@ -0,0 +1,34 @@
+parseLocale($translator->getLocale())->language;
+$showFullscreen = $this->layout()->showFullscreen;
+$innerLayoutScript = $this->layout()->innerLayout . '.phtml';
+
+?>
+
+
+
+
+
+ = $this->title ? $this->escape($this->title) : $this->defaultTitle ?>
+
+
+
+
+
+
+
+
+
+
+ = $this->render($innerLayoutScript); ?>
+
+
+
diff --git a/application/layouts/scripts/guest-error.phtml b/application/layouts/scripts/guest-error.phtml
new file mode 100644
index 0000000..49cdd68
--- /dev/null
+++ b/application/layouts/scripts/guest-error.phtml
@@ -0,0 +1,10 @@
+
+
+
+
+
+ = $this->render('inline.phtml') ?>
+
+
+
+
diff --git a/application/layouts/scripts/inline.phtml b/application/layouts/scripts/inline.phtml
new file mode 100644
index 0000000..47d5672
--- /dev/null
+++ b/application/layouts/scripts/inline.phtml
@@ -0,0 +1,2 @@
+= $this->layout()->content ?>
+= $this->layout()->benchmark ?>
diff --git a/application/layouts/scripts/layout.phtml b/application/layouts/scripts/layout.phtml
new file mode 100644
index 0000000..33ede0b
--- /dev/null
+++ b/application/layouts/scripts/layout.phtml
@@ -0,0 +1,83 @@
+parseLocale($translator->getLocale())->language;
+$timezone = date_default_timezone_get();
+$isIframe = $this->layout()->isIframe;
+$showFullscreen = $this->layout()->showFullscreen;
+$iframeClass = $isIframe ? ' iframe' : '';
+$innerLayoutScript = $this->layout()->innerLayout . '.phtml';
+
+?>
+
+ data-icinga-is-iframe
+
+>
+
+
+
+
+ = $this->title ? $this->escape($this->title) . ' :: ' : '' ?>= $this->defaultTitle ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+= $this->render($innerLayoutScript); ?>
+
+
+
+
+ = new Icon('angle-double-down', ['class' => 'expand-icon', 'title' => $this->translate('Expand')]) ?>
+ = new Icon('angle-double-up', ['class' => 'collapse-icon', 'title' => $this->translate('Collapse')]) ?>
+
+
+
+
+
+
+
diff --git a/application/layouts/scripts/parts/navigation.phtml b/application/layouts/scripts/parts/navigation.phtml
new file mode 100644
index 0000000..dd973f5
--- /dev/null
+++ b/application/layouts/scripts/parts/navigation.phtml
@@ -0,0 +1,35 @@
+auth()->isAuthenticated()) {
+ return;
+}
+
+?>
+
+
= t('Accessibility Skip Links') ?>
+
+
+
+
diff --git a/application/layouts/scripts/pdf.phtml b/application/layouts/scripts/pdf.phtml
new file mode 100644
index 0000000..87d07f8
--- /dev/null
+++ b/application/layouts/scripts/pdf.phtml
@@ -0,0 +1,44 @@
+layout()->moduleName;
+if ($moduleName !== 'default') {
+ $moduleClass = ' icinga-module module-' . $moduleName;
+} else {
+ $moduleClass = '';
+}
+
+$logoPath = Icinga::app()->getBootstrapDirectory() . '/img/icinga-logo-big-dark.png';
+$logo = base64_encode(file_get_contents($logoPath));
+
+
+?>
+
+
+
+
+
+
+
+
+
+ = $this->render('inline.phtml') ?>
+
+
+
diff --git a/application/views/helpers/CreateTicketLinks.php b/application/views/helpers/CreateTicketLinks.php
new file mode 100644
index 0000000..dda55a6
--- /dev/null
+++ b/application/views/helpers/CreateTicketLinks.php
@@ -0,0 +1,23 @@
+view->tickets;
+ /** @var \Icinga\Application\Hook\TicketHook|array|null $tickets */
+ return ! empty($tickets) ? $tickets->createLinks($text) : $text;
+ }
+}
diff --git a/application/views/helpers/FormDate.php b/application/views/helpers/FormDate.php
new file mode 100644
index 0000000..39e6d94
--- /dev/null
+++ b/application/views/helpers/FormDate.php
@@ -0,0 +1,46 @@
+_getInfo($name, $value, $attribs);
+
+ extract($info); // name, id, value, attribs, options, listsep, disable
+ /** @var string $id */
+ /** @var bool $disable */
+
+ $disabled = '';
+ if ($disable) {
+ $disabled = ' disabled="disabled"';
+ }
+
+ /** @var \Icinga\Web\View $view */
+ $view = $this->view;
+
+ $html5 = sprintf(
+ ' escape($name),
+ $view->escape($id),
+ $view->escape($value),
+ $disabled,
+ $this->_htmlAttribs($attribs),
+ $this->getClosingBracket()
+ );
+
+ return $html5;
+ }
+}
diff --git a/application/views/helpers/FormDateTime.php b/application/views/helpers/FormDateTime.php
new file mode 100644
index 0000000..de5eb4b
--- /dev/null
+++ b/application/views/helpers/FormDateTime.php
@@ -0,0 +1,63 @@
+format($format);
+ }
+
+ /**
+ * Render the date-and-time input control
+ *
+ * @param string $name The element name
+ * @param DateTime $value The default timestamp
+ * @param array $attribs Attributes for the element tag
+ *
+ * @return string The element XHTML
+ */
+ public function formDateTime($name, $value = null, $attribs = null)
+ {
+ $info = $this->_getInfo($name, $value, $attribs);
+ extract($info); // name, id, value, attribs, options, listsep, disable
+ /** @var string $id */
+ /** @var bool $disable */
+ $disabled = '';
+ if ($disable) {
+ $disabled = ' disabled="disabled"';
+ }
+ if ($value instanceof DateTime) {
+ // If value was valid, it's a DateTime object
+ $value = $this->formatDate($value, $attribs['local']);
+ }
+ if (isset($attribs['placeholder']) && $attribs['placeholder'] instanceof DateTime) {
+ $attribs['placeholder'] = $this->formatDate($attribs['placeholder'], $attribs['local']);
+ }
+ $type = $attribs['local'] === true ? 'datetime-local' : 'datetime';
+ unset($attribs['local']); // Unset local to not render it again in $this->_htmlAttribs($attribs)
+ $html5 = sprintf(
+ ' view->escape($name),
+ $this->view->escape($id),
+ $this->view->escape($value),
+ $disabled,
+ $this->_htmlAttribs($attribs),
+ $this->getClosingBracket()
+ );
+ return $html5;
+ }
+}
diff --git a/application/views/helpers/FormNumber.php b/application/views/helpers/FormNumber.php
new file mode 100644
index 0000000..f447180
--- /dev/null
+++ b/application/views/helpers/FormNumber.php
@@ -0,0 +1,77 @@
+view->escape(
+ sprintf(
+ ctype_digit((string) $number) ? '%d' : '%F',
+ $number
+ )
+ );
+ }
+
+ /**
+ * Render the number input control
+ *
+ * @param string $name
+ * @param int $value
+ * @param array $attribs
+ *
+ * @return string The rendered number input control
+ */
+ public function formNumber($name, $value = null, $attribs = null)
+ {
+ $info = $this->_getInfo($name, $value, $attribs);
+ extract($info); // name, id, value, attribs, options, listsep, disable
+ /** @var string $id */
+ /** @var bool $disable */
+ $disabled = '';
+ if ($disable) {
+ $disabled = ' disabled="disabled"';
+ }
+ $min = '';
+ if (isset($attribs['min'])) {
+ $min = sprintf(' min="%s"', $this->formatNumber($attribs['min']));
+ }
+ unset($attribs['min']); // Unset min to not render it again in $this->_htmlAttribs($attribs)
+ $max = '';
+ if (isset($attribs['max'])) {
+ $max = sprintf(' max="%s"', $this->formatNumber($attribs['max']));
+ }
+ unset($attribs['max']); // Unset max to not render it again in $this->_htmlAttribs($attribs)
+ $step = '';
+ if (isset($attribs['step'])) {
+ $step = sprintf(' step="%s"', $attribs['step'] === 'any' ? 'any' : $this->formatNumber($attribs['step']));
+ }
+ unset($attribs['step']); // Unset step to not render it again in $this->_htmlAttribs($attribs)
+ $html5 = sprintf(
+ ' view->escape($name),
+ $this->view->escape($id),
+ $this->view->escape($this->formatNumber($value)),
+ $min,
+ $max,
+ $step,
+ $disabled,
+ $this->_htmlAttribs($attribs),
+ $this->getClosingBracket()
+ );
+ return $html5;
+ }
+}
diff --git a/application/views/helpers/FormTime.php b/application/views/helpers/FormTime.php
new file mode 100644
index 0000000..39d1b83
--- /dev/null
+++ b/application/views/helpers/FormTime.php
@@ -0,0 +1,46 @@
+_getInfo($name, $value, $attribs);
+
+ extract($info); // name, id, value, attribs, options, listsep, disable
+ /** @var string $id */
+ /** @var bool $disable */
+
+ $disabled = '';
+ if ($disable) {
+ $disabled = ' disabled="disabled"';
+ }
+
+ /** @var \Icinga\Web\View $view */
+ $view = $this->view;
+
+ $html5 = sprintf(
+ ' escape($name),
+ $view->escape($id),
+ $view->escape($value),
+ $disabled,
+ $this->_htmlAttribs($attribs),
+ $this->getClosingBracket()
+ );
+
+ return $html5;
+ }
+}
diff --git a/application/views/helpers/ProtectId.php b/application/views/helpers/ProtectId.php
new file mode 100644
index 0000000..f6dc226
--- /dev/null
+++ b/application/views/helpers/ProtectId.php
@@ -0,0 +1,13 @@
+getRequest()->protectId($id);
+ }
+}
diff --git a/application/views/helpers/Util.php b/application/views/helpers/Util.php
new file mode 100644
index 0000000..7a3e410
--- /dev/null
+++ b/application/views/helpers/Util.php
@@ -0,0 +1,68 @@
+ 3600 * 24 * 3) {
+ if (date('Y') === date('Y', $timestamp)) {
+ return date('d.m.', $timestamp);
+ }
+ return date('m.Y', $timestamp);
+ }
+ return self::showHourMin($duration);
+ }
+
+ public static function showHourMin($sec)
+ {
+ $min = floor($sec / 60);
+ if ($min < 60) {
+ return $min . 'm ' . ($sec % 60) . 's';
+ }
+ $hour = floor($min / 60);
+ if ($hour < 24) {
+ return date('H:i', time() - $sec);
+ }
+ return floor($hour / 24) . 'd ' . ($hour % 24) . 'h';
+ }
+
+ public static function showSeconds($sec)
+ {
+ // Todo: localization
+ if ($sec < 1) {
+ return round($sec * 1000) . 'ms';
+ }
+ if ($sec < 60) {
+ return $sec . 's';
+ }
+ return floor($sec / 60) . 'm ' . ($sec % 60) . 's';
+ }
+
+ public static function showTime($timestamp)
+ {
+ // Todo: localization
+ if ($timestamp < 86400) {
+ return 'undef';
+ }
+ if (date('Ymd') === date('Ymd', $timestamp)) {
+ return date('H:i:s', $timestamp);
+ }
+ if (date('Y') === date('Y', $timestamp)) {
+ return date('H:i d.m.', $timestamp);
+ }
+ return date('H:i d.m.Y', $timestamp);
+ }
+}
diff --git a/application/views/scripts/about/index.phtml b/application/views/scripts/about/index.phtml
new file mode 100644
index 0000000..805e723
--- /dev/null
+++ b/application/views/scripts/about/index.phtml
@@ -0,0 +1,199 @@
+
+
+ = $tabs ?>
+
+
+
+ = $this->img('img/icinga-logo-big.svg', null, array('class' => 'icinga-logo', 'width' => 194)) ?>
+
+
+
+
+
+ = $this->translate('Icinga Web 2 Version') ?>
+ = $this->escape($version['appVersion']) ?>
+
+
+
+
+ = $this->translate('Git commit') ?>
+ = $this->escape($version['gitCommitID']) ?>
+
+
+
+ = $this->translate('PHP Version') ?>
+ = $this->escape(PHP_VERSION) ?>
+
+
+
+ = $this->translate('Git commit date') ?>
+ = $this->escape($version['gitCommitDate']) ?>
+
+
+
+
+
+
+ =
+ HtmlElement::create('a', [
+ 'href' => 'https://icinga.com/support/',
+ 'target' => '_blank',
+ 'title' => $this->translate('Get Icinga Support')
+ ], [
+ new Icon('life-ring'),
+ $this->translate('Get Icinga Support'),
+ ]
+ );
+ ?>
+
+
+ =
+ HtmlElement::create('a', [
+ 'href' => 'https://icinga.com/community/',
+ 'target' => '_blank',
+ 'title' => $this->translate('Icinga Community')
+ ], [
+ new Icon('globe-europe'),
+ $this->translate('Icinga Community'),
+ ]
+ );
+ ?>
+
+
+ =
+ HtmlElement::create('a', [
+ 'href' => 'https://github.com/icinga/icingaweb2/issues',
+ 'target' => '_blank',
+ 'title' => $this->translate('Icinga Community')
+ ], [
+ new Icon('bullhorn'),
+ $this->translate('Report a bug'),
+ ]
+ );
+ ?>
+
+
+ =
+ HtmlElement::create('a', [
+ 'href' => 'https://icinga.com/docs/icinga-web-2/'
+ . (isset($version['docVersion']) ? $version['docVersion'] : 'latest'),
+ 'target' => '_blank',
+ 'title' => $this->translate('Icinga Documentation')
+ ], [
+ new Icon('book'),
+ $this->translate('Icinga Documentation'),
+ ]
+ );
+ ?>
+
+
+
+ hasPendingMigrations()): ?>
+
+
= $this->translate('Pending Migrations') ?>
+
+ getPendingMigrations() as $migration): ?>
+
+ = $this->escape($migration->getName()) ?>
+ =
+ new StateBadge(
+ count($migration->getMigrations()),
+ BadgeNavigationItemRenderer::STATE_PENDING
+ );
+ ?>
+
+
+
+ = $this->qlink(
+ $this->translate('Show all'),
+ 'migrations',
+ null,
+ ['title' => $this->translate('Show all pending migrations')]
+ ) ?>
+
+
+
+ = $this->translate('Loaded Libraries') ?>
+
+
+
+
+ = $this->escape($library->getName()) ?>
+
+
+ = $this->escape($library->getVersion()) ?: '-' ?>
+
+
+
+
+
+ = $this->translate('Loaded Modules') ?>
+
+
+
+
+ = $this->escape($module->getName()) ?>
+
+
+
+ = $this->escape($module->getVersion()) ?>
+
+
+ hasPermission('config/modules')): ?>
+ = $this->qlink(
+ $this->translate('Configure'),
+ 'config/module/',
+ array('name' => $module->getName()),
+ array('title' => sprintf($this->translate('Show the overview of the %s module'), $module->getName()))
+ ) ?>
+
+
+
+
+
+
+
+
+
+ = $this->translate('Copyright') ?>
+ © 2013-= date('Y') ?>
+ = $this->qlink(
+ 'Icinga GmbH',
+ 'https://icinga.com',
+ null,
+ array(
+ 'target' => '_blank'
+ )
+ ) ?>
+
+
+ = $this->qlink(
+ null,
+ 'https://www.twitter.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'twitter',
+ 'title' => $this->translate('Icinga on Twitter')
+ )
+ ) ?> = $this->qlink(
+ null,
+ 'https://www.facebook.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'facebook-squared',
+ 'title' => $this->translate('Icinga on Facebook')
+ )
+ ) ?>
+
+
+
diff --git a/application/views/scripts/account/index.phtml b/application/views/scripts/account/index.phtml
new file mode 100644
index 0000000..efc2bcb
--- /dev/null
+++ b/application/views/scripts/account/index.phtml
@@ -0,0 +1,11 @@
+
+ = $tabs ?>
+
+
+
+
= $this->translate('Account') ?>
+ = $changePasswordForm ?>
+
+ = $this->translate('Preferences') ?>
+ = $form ?>
+
diff --git a/application/views/scripts/announcements/index.phtml b/application/views/scripts/announcements/index.phtml
new file mode 100644
index 0000000..ff87c66
--- /dev/null
+++ b/application/views/scripts/announcements/index.phtml
@@ -0,0 +1,71 @@
+compact): ?>
+
+ = $this->tabs ?>
+ = $this->paginator ?>
+
+ = $this->limiter ?>
+ = $this->sortBox ?>
+
+ = $this->filterEditor ?>
+
+
+
+hasPermission('application/announcements')) {
+ echo $this->qlink(
+ $this->translate('Create a New Announcement') ,
+ 'announcements/new',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new announcement')
+ )
+ );
+} ?>
+announcements)): ?>
+
= $this->translate('No announcements found.') ?>
+
+
+
+
+
+ = $this->translate('Author') ?>
+ = $this->translate('Message') ?>
+ = $this->translate('Start') ?>
+ = $this->translate('End') ?>
+
+
+
+
+ announcements as $announcement): /** @var object $announcement */ ?>
+
+ = $this->escape($announcement->author) ?>
+ hasPermission('application/announcements')): ?>
+
+
+ = $this->ellipsis($this->escape($announcement->message), 100) ?>
+
+
+
+ = $this->ellipsis($this->escape($announcement->message), 100) ?>
+
+ = $this->formatDateTime($announcement->start) ?>
+ = $this->formatDateTime($announcement->end) ?>
+ hasPermission('application/announcements')): ?>
+ = $this->qlink(
+ null,
+ 'announcements/remove',
+ array('id' => $announcement->id),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => $this->translate('Remove this announcement')
+ )
+ ) ?>
+
+
+
+
+
+
diff --git a/application/views/scripts/authentication/login.phtml b/application/views/scripts/authentication/login.phtml
new file mode 100644
index 0000000..167a468
--- /dev/null
+++ b/application/views/scripts/authentication/login.phtml
@@ -0,0 +1,74 @@
+
+
+
+
+ = $this->qlink(
+ null,
+ 'https://twitter.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'twitter',
+ 'title' => $this->translate('Icinga on Twitter')
+ )
+ ) ?>
+
+
+ = $this->qlink(
+ null,
+ 'https://www.facebook.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'facebook-squared',
+ 'title' => $this->translate('Icinga on Facebook')
+ )
+ ) ?>
+
+ = $this->qlink(
+ null,
+ 'https://github.com/Icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'github-circled',
+ 'title' => $this->translate('Icinga on GitHub')
+ )
+ ) ?>
+
+
+
+= $this->img('img/orb-analytics.png'); ?>
+= $this->img('img/orb-automation.png'); ?>
+= $this->img('img/orb-cloud.png'); ?>
+= $this->img('img/orb-icinga.png'); ?>
+= $this->img('img/orb-infrastructure.png'); ?>
+= $this->img('img/orb-metrics.png'); ?>
+= $this->img('img/orb-notifications.png'); ?>
diff --git a/application/views/scripts/authentication/logout.phtml b/application/views/scripts/authentication/logout.phtml
new file mode 100644
index 0000000..501ed20
--- /dev/null
+++ b/application/views/scripts/authentication/logout.phtml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
= $this->translate('Logging out...'); ?>
+
+ = $this->translate(
+ 'If this message does not disappear, it might be necessary to quit the'
+ . ' current session manually by clearing the cache, or by closing the current'
+ . ' browser session.'
+ ); ?>
+
+
+
= $this->translate('Logout successful'); ?>
+
+
+
+
+
diff --git a/application/views/scripts/config/devtools.phtml b/application/views/scripts/config/devtools.phtml
new file mode 100644
index 0000000..245a71a
--- /dev/null
+++ b/application/views/scripts/config/devtools.phtml
@@ -0,0 +1,6 @@
+
+= $this->tabs ?>
+
+
diff --git a/application/views/scripts/config/general.phtml b/application/views/scripts/config/general.phtml
new file mode 100644
index 0000000..13a8ed9
--- /dev/null
+++ b/application/views/scripts/config/general.phtml
@@ -0,0 +1,6 @@
+
+ = $tabs ?>
+
+
+ = $form ?>
+
diff --git a/application/views/scripts/config/module-configuration-error.phtml b/application/views/scripts/config/module-configuration-error.phtml
new file mode 100644
index 0000000..85fb128
--- /dev/null
+++ b/application/views/scripts/config/module-configuration-error.phtml
@@ -0,0 +1,28 @@
+action)) ? $this->action : 'do something with';
+ $moduleName = $this->moduleName;
+ $exceptionMessage = $this->exceptionMessage;
+?>
+= $this->tabs->render($this); ?>
+
+
+
Could not = $action; ?> module "= $moduleName; ?>"
+
+ While operation the following error occurred:
+
+ = $exceptionMessage; ?>
+
+
+
+
+ This could have one or more of the following reasons:
+
+ No file permissions to write into module directory
+ Errors on filesystems: Mount points, operational errors
+ General application error
+
+
+
+
+ Details can be seen in your application log (if you don't have access to this file, call your administrator in this case).
+
\ No newline at end of file
diff --git a/application/views/scripts/config/module.phtml b/application/views/scripts/config/module.phtml
new file mode 100644
index 0000000..6d41ab2
--- /dev/null
+++ b/application/views/scripts/config/module.phtml
@@ -0,0 +1,136 @@
+
+ = $this->tabs ?>
+
+
+
+ = $this->translate('There is no such module installed.') ?>
+
+ getRequiredModules();
+ $requiredLibs = $module->getRequiredLibraries();
+ $restrictions = $module->getProvidedRestrictions();
+ $permissions = $module->getProvidedPermissions();
+ $unmetDependencies = $moduleManager->hasUnmetDependencies($module->getName());
+ $isIcingadbSupported = isset($requiredMods['icingadb']);
+ $state = $moduleData->enabled ? ($moduleData->loaded ? 'enabled' : 'failed') : 'disabled';
+ ?>
+
+
+ = $this->escape($this->translate('Name')) ?>
+ = $this->escape($module->getName()) ?>
+
+
+ = $this->translate('State') ?>
+
+ = $state ?>
+ toggleForm)): ?>
+ enabled || ! $unmetDependencies): ?>
+ = $this->toggleForm ?>
+
+ = $this->icon('attention-alt', $this->translate('Module can\'t be enabled due to unmet dependencies')) ?>
+
+
+
+
+ = $this->escape($this->translate('Version')) ?>
+ = $this->escape($module->getVersion()) ?>
+
+
+
+ = $this->escape($this->translate('Git commit')) ?>
+ = $this->escape($moduleGitCommitId) ?>
+
+
+
+ = $this->escape($this->translate('Description')) ?>
+
+ = $this->escape($module->getTitle()) ?>
+ = nl2br($this->escape($module->getDescription())) ?>
+
+
+
+ = $this->escape($this->translate('Dependencies')) ?>
+
+
+ = $this->translate('This module has no dependencies') ?>
+
+
+
+ = $this->translate('Unmet dependencies found! Module can\'t be enabled unless all dependencies are met.') ?>
+
+
+
+
+ = $this->translate('Libraries') ?>
+ $versionString): ?>
+
+ = $this->escape($libraryName) ?>
+
+ has($libraryName, $versionString === true ? null : $versionString)): ?>
+ = $versionString === true ? '*' : $this->escape($versionString) ?>
+
+ = $versionString === true ? '*' : $this->escape($versionString) ?>
+ get($libraryName)) !== null): ?>
+ (= $library->getVersion() ?>)
+
+
+
+
+
+
+
+
+
+ = $this->translate('Modules') ?>
+ $versionString): ?>
+ has('icingadb', $requiredMods['icingadb'])) : ?>
+
+
+
+ = $this->escape($moduleName) ?>
+
+ has($moduleName, $versionString === true ? null : $versionString)): ?>
+ = $versionString === true ? '*' : $this->escape($versionString) ?>
+
+ has('monitoring', $requiredMods['monitoring'])) ? 'class="optional"' : 'class="missing"' ?>>
+ = $versionString === true ? '*' : $this->escape($versionString) ?>
+
+ hasInstalled($moduleName)): ?>
+ (= $this->translate('not installed') ?>)
+
+ (= $moduleManager->getModule($moduleName, false)->getVersion() ?>= $moduleManager->hasEnabled($moduleName) ? '' : ', ' . $this->translate('disabled') ?>)
+
+
+
+
+ = $this->translate('or') ?>
+
+
+
+
+
+
+
+
+
+
+ = $this->escape($this->translate('Permissions')) ?>
+
+
+ = $this->escape($permission->name) ?> : = $this->escape($permission->description) ?>
+
+
+
+
+
+
+ = $this->escape($this->translate('Restrictions')) ?>
+
+
+ = $this->escape($restriction->name) ?> : = $this->escape($restriction->description) ?>
+
+
+
+
+
+
diff --git a/application/views/scripts/config/modules.phtml b/application/views/scripts/config/modules.phtml
new file mode 100644
index 0000000..b13b378
--- /dev/null
+++ b/application/views/scripts/config/modules.phtml
@@ -0,0 +1,42 @@
+compact): ?>
+
+ = $this->tabs ?>
+ = $this->paginator ?>
+
+
+
+
+
+
+ = $this->translate('Module') ?>
+
+
+
+
+
+
+ installed) {
+ $this->icon('flash', sprintf($this->translate('Module %s is dangling'), $module->name));
+ } elseif ($module->enabled && $module->loaded) {
+ echo $this->icon('thumbs-up', sprintf($this->translate('Module %s is enabled'), $module->name));
+ } elseif (! $module->enabled) {
+ echo $this->icon('block', sprintf($this->translate('Module %s is disabled'), $module->name));
+ } else { // ! $module->loaded
+ echo $this->icon('block', sprintf($this->translate('Module %s has failed to load'), $module->name));
+ }
+
+ echo $this->qlink(
+ $module->name,
+ 'config/module',
+ array('name' => $module->name),
+ array(
+ 'class' => 'rowaction',
+ 'title' => sprintf($this->translate('Show the overview of the %s module'), $module->name)
+ )
+ ); ?>
+
+
+
+
+
+
diff --git a/application/views/scripts/config/resource.phtml b/application/views/scripts/config/resource.phtml
new file mode 100644
index 0000000..317c115
--- /dev/null
+++ b/application/views/scripts/config/resource.phtml
@@ -0,0 +1,73 @@
+
+ = $tabs ?>
+
+
+ = $this->qlink(
+ $this->translate('Create a New Resource') ,
+ 'config/createresource',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new resource')
+ )
+ ) ?>
+
+
+
+ = $this->translate('Resource') ?>
+
+
+
+
+resources as $name => $value): ?>
+
+
+ type) {
+ case 'db':
+ $icon = 'database';
+ break;
+ case 'ldap':
+ $icon = 'sitemap';
+ break;
+ case 'ssh':
+ $icon = 'user';
+ break;
+ case 'file':
+ case 'ini':
+ $icon = 'doc-text';
+ break;
+ default:
+ $icon = 'edit';
+ break;
+ }
+ ?>
+ = $this->qlink(
+ $name,
+ 'config/editresource',
+ array('resource' => $name),
+ array(
+ 'icon' => $icon,
+ 'title' => sprintf($this->translate('Edit resource %s'), $name)
+ )
+ ) ?>
+
+
+ = $this->qlink(
+ '',
+ 'config/removeresource',
+ array('resource' => $name),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove resource %s'), $name)
+ )
+ ) ?>
+
+
+
+
+
+
diff --git a/application/views/scripts/config/resource/create.phtml b/application/views/scripts/config/resource/create.phtml
new file mode 100644
index 0000000..13a8ed9
--- /dev/null
+++ b/application/views/scripts/config/resource/create.phtml
@@ -0,0 +1,6 @@
+
+ = $tabs ?>
+
+
+ = $form ?>
+
diff --git a/application/views/scripts/config/resource/modify.phtml b/application/views/scripts/config/resource/modify.phtml
new file mode 100644
index 0000000..13a8ed9
--- /dev/null
+++ b/application/views/scripts/config/resource/modify.phtml
@@ -0,0 +1,6 @@
+
+ = $tabs ?>
+
+
+ = $form ?>
+
diff --git a/application/views/scripts/config/resource/remove.phtml b/application/views/scripts/config/resource/remove.phtml
new file mode 100644
index 0000000..13a8ed9
--- /dev/null
+++ b/application/views/scripts/config/resource/remove.phtml
@@ -0,0 +1,6 @@
+
+ = $tabs ?>
+
+
+ = $form ?>
+
diff --git a/application/views/scripts/config/userbackend/reorder.phtml b/application/views/scripts/config/userbackend/reorder.phtml
new file mode 100644
index 0000000..c77fd2e
--- /dev/null
+++ b/application/views/scripts/config/userbackend/reorder.phtml
@@ -0,0 +1,75 @@
+
+ = $tabs ?>
+
+
+ auth()->hasPermission('config/access-control/users')): ?>
+
= $this->translate('User Backends') ?>
+ = $this->qlink(
+ $this->translate('Create a New User Backend') ,
+ 'config/createuserbackend',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new user backend')
+ )
+ ) ?>
+ = $form ?>
+
+
+ auth()->hasPermission('config/access-control/groups')): ?>
+
= $this->translate('User Group Backends') ?>
+ = $this->qlink(
+ $this->translate('Create a New User Group Backend') ,
+ 'usergroupbackend/create',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new user group backend')
+ )
+ ) ?>
+
+
+
+
+ = $this->translate('Backend') ?>
+
+
+
+
+ $config):
+ $type = $config->get('backend');
+?>
+
+
+ = $this->qlink(
+ $backendName,
+ 'usergroupbackend/edit',
+ array('backend' => $backendName),
+ array(
+ 'icon' => $type === 'external' ? 'magic' : ($type === 'ldap' || $type === 'msldap' ? 'sitemap' : 'database'),
+ 'title' => sprintf($this->translate('Edit user group backend %s'), $backendName)
+ )
+ ); ?>
+
+
+ = $this->qlink(
+ null,
+ 'usergroupbackend/remove',
+ array('backend' => $backendName),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove user group backend %s'), $backendName)
+ )
+ ) ?>
+
+
+
+
+
+
+
diff --git a/application/views/scripts/dashboard/error.phtml b/application/views/scripts/dashboard/error.phtml
new file mode 100644
index 0000000..9396b49
--- /dev/null
+++ b/application/views/scripts/dashboard/error.phtml
@@ -0,0 +1,13 @@
+
+
= $this->translate('Could not save dashboard'); ?>
+
+ = $this->translate('Please copy the following dashboard snippet to '); ?>
+ = $this->config->getConfigFile(); ?>; .
+
+ = $this->translate('Make sure that the webserver can write to this file.'); ?>
+
+
= $this->config; ?>
+
+
= $this->translate('Error details'); ?>
+
= $this->error->getMessage(); ?>
+
\ No newline at end of file
diff --git a/application/views/scripts/dashboard/index.phtml b/application/views/scripts/dashboard/index.phtml
new file mode 100644
index 0000000..1d56114
--- /dev/null
+++ b/application/views/scripts/dashboard/index.phtml
@@ -0,0 +1,26 @@
+
+compact): ?>
+= $this->tabs ?>
+
+
+dashboard): ?>
+
+ = $this->dashboard ?>
+
+
+
+
= $this->escape($this->translate('Welcome to Icinga Web!')) ?>
+
+ hasPermission('config/modules')) {
+ echo $this->escape($this->translate(
+ 'Currently there is no dashlet available. Please contact the administrator.'
+ ));
+ } else {
+ printf(
+ $this->escape($this->translate('Currently there is no dashlet available. This might change once you enabled some of the available %s.')),
+ $this->qlink($this->translate('modules'), 'config/modules')
+ );
+ } ?>
+
+
+
diff --git a/application/views/scripts/dashboard/new-dashlet.phtml b/application/views/scripts/dashboard/new-dashlet.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/new-dashlet.phtml
@@ -0,0 +1,6 @@
+
+ = $this->tabs ?>
+
+
+ = $this->form; ?>
+
\ No newline at end of file
diff --git a/application/views/scripts/dashboard/remove-dashlet.phtml b/application/views/scripts/dashboard/remove-dashlet.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/remove-dashlet.phtml
@@ -0,0 +1,6 @@
+
+ = $this->tabs ?>
+
+
+ = $this->form; ?>
+
\ No newline at end of file
diff --git a/application/views/scripts/dashboard/remove-pane.phtml b/application/views/scripts/dashboard/remove-pane.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/remove-pane.phtml
@@ -0,0 +1,6 @@
+
+ = $this->tabs ?>
+
+
+ = $this->form; ?>
+
\ No newline at end of file
diff --git a/application/views/scripts/dashboard/rename-pane.phtml b/application/views/scripts/dashboard/rename-pane.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/rename-pane.phtml
@@ -0,0 +1,6 @@
+
+ = $this->tabs ?>
+
+
+ = $this->form; ?>
+
\ No newline at end of file
diff --git a/application/views/scripts/dashboard/settings.phtml b/application/views/scripts/dashboard/settings.phtml
new file mode 100644
index 0000000..52d4f14
--- /dev/null
+++ b/application/views/scripts/dashboard/settings.phtml
@@ -0,0 +1,91 @@
+
+ = $this->tabs ?>
+
+
+
= t('Dashboard Settings'); ?>
+
+
+
+
+
+ = t('Dashlet Name') ?>
+
+
+ = t('Url') ?>
+
+
+
+
+
+ dashboard->getPanes() as $pane): ?>
+ getDisabled()) continue; ?>
+
+
+ isUserWidget()): ?>
+ = $this->qlink(
+ $pane->getName(),
+ 'dashboard/rename-pane',
+ array('pane' => $pane->getName()),
+ array('title' => sprintf($this->translate('Edit pane %s'), $pane->getName()))
+ ) ?>
+
+ = $this->escape($pane->getName()) ?>
+
+
+
+ = $this->qlink(
+ '',
+ 'dashboard/remove-pane',
+ array('pane' => $pane->getName()),
+ array(
+ 'icon' => 'trash',
+ 'title' => sprintf($this->translate('Remove pane %s'), $pane->getName())
+ )
+ ); ?>
+
+
+ getDashlets(); ?>
+
+
+
+ = $this->translate('No dashlets added to dashboard') ?>.
+
+
+
+
+ getDisabled()) continue; ?>
+
+
+ = $this->qlink(
+ $dashlet->getTitle(),
+ 'dashboard/update-dashlet',
+ array('pane' => $pane->getName(), 'dashlet' => $dashlet->getName()),
+ array('title' => sprintf($this->translate('Edit dashlet %s'), $dashlet->getTitle()))
+ ); ?>
+
+
+ = $this->qlink(
+ $dashlet->getUrl()->getRelativeUrl(),
+ $dashlet->getUrl()->getRelativeUrl(),
+ null,
+ array('title' => sprintf($this->translate('Show dashlet %s'), $dashlet->getTitle()))
+ ); ?>
+
+
+ = $this->qlink(
+ '',
+ 'dashboard/remove-dashlet',
+ array('pane' => $pane->getName(), 'dashlet' => $dashlet->getName()),
+ array(
+ 'icon' => 'trash',
+ 'title' => sprintf($this->translate('Remove dashlet %s from pane %s'), $dashlet->getTitle(), $pane->getTitle())
+ )
+ ); ?>
+
+
+
+
+
+
+
+
diff --git a/application/views/scripts/dashboard/update-dashlet.phtml b/application/views/scripts/dashboard/update-dashlet.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/update-dashlet.phtml
@@ -0,0 +1,6 @@
+
+ = $this->tabs ?>
+
+
+ = $this->form; ?>
+
\ No newline at end of file
diff --git a/application/views/scripts/error/error.phtml b/application/views/scripts/error/error.phtml
new file mode 100644
index 0000000..5f4579a
--- /dev/null
+++ b/application/views/scripts/error/error.phtml
@@ -0,0 +1,106 @@
+compact && ! $hideControls): ?>
+
+ = $tabs ?>
+
+
+
+ $message) {
+ echo '
' . nl2br($this->escape($message)) . '
'
+ . '
'
+ . '
' . $this->escape($stackTraces[$i]) . ' ';
+ }
+} else {
+ foreach ($messages as $message) {
+ echo '
' . nl2br($this->escape($message)) . '
';
+ }
+}
+
+$libraries = \Icinga\Application\Icinga::app()->getLibraries();
+$coreReason = [];
+$modReason = [];
+
+if (isset($requiredVendor, $requiredProject) && $requiredVendor && $requiredProject) {
+ // TODO: I don't like this, can we define requirements somewhere else?
+ $coreDeps = ['icinga-php-library' => '>= 0.13', 'icinga-php-thirdparty' => '>= 0.12'];
+
+ foreach ($coreDeps as $libraryName => $requiredVersion) {
+ if (! $libraries->has($libraryName)) {
+ $coreReason[] = sprintf($this->translate(
+ 'Library "%s" is required and missing. Please install a version of it matching the required one: %s'
+ ), $libraryName, $requiredVersion);
+ } elseif (! $libraries->has($libraryName, $requiredVersion) && $libraries->get($libraryName)->isRequired($requiredVendor, $requiredProject)) {
+ $coreReason[] = sprintf($this->translate(
+ 'Library "%s" is required and installed, but its version (%s) does not satisfy the required one: %s'
+ ), $libraryName, $libraries->get($libraryName)->getVersion() ?: '-', $requiredVersion);
+ }
+ }
+
+ if (! empty($coreReason)) {
+ array_unshift($coreReason, $this->translate('You have unmet dependencies. Please check Icinga Web 2\'s installation instructions.'));
+ }
+}
+
+if (isset($module)) {
+ $manager = \Icinga\Application\Icinga::app()->getModuleManager();
+ if ($manager->hasUnmetDependencies($module->getName())) {
+ if (isset($requiredModule) && $requiredModule && isset($module->getRequiredModules()[$requiredModule])) {
+ if (! $manager->hasInstalled($requiredModule)) {
+ $modReason[] = sprintf($this->translate(
+ 'Module "%s" is required and missing. Please install a version of it matching the required one: %s'
+ ), $requiredModule, $module->getRequiredModules()[$requiredModule]);
+ } elseif (! $manager->hasEnabled($requiredModule)) {
+ $modReason[] = sprintf($this->translate(
+ 'Module "%s" is required and installed, but not enabled. Please enable module "%1$s".'
+ ), $requiredModule);
+ } elseif (! $manager->has($requiredModule, $module->getRequiredModules()[$requiredModule])) {
+ $modReason[] = sprintf($this->translate(
+ 'Module "%s" is required and installed, but its version (%s) does not satisfy the required one: %s'
+ ), $requiredModule, $manager->getModule($requiredModule, false)->getVersion(), $module->getRequiredModules()[$requiredModule]);
+ }
+ } elseif (isset($requiredVendor, $requiredProject) && $requiredVendor && $requiredProject) {
+ foreach ($module->getRequiredLibraries() as $libraryName => $requiredVersion) {
+ if (! $libraries->has($libraryName)) {
+ $modReason[] = sprintf($this->translate(
+ 'Library "%s" is required and missing. Please install a version of it matching the required one: %s'
+ ), $libraryName, $requiredVersion);
+ } elseif (! $libraries->has($libraryName, $requiredVersion) && $libraries->get($libraryName)->isRequired($requiredVendor, $requiredProject)) {
+ $modReason[] = sprintf($this->translate(
+ 'Library "%s" is required and installed, but its version (%s) does not satisfy the required one: %s'
+ ), $libraryName, $libraries->get($libraryName)->getVersion() ?: '-', $requiredVersion);
+ }
+ }
+ }
+
+ if (! empty($modReason)) {
+ array_unshift($modReason, sprintf($this->translate(
+ 'This error might have occurred because module "%s" has unmet dependencies.'
+ . ' Please check it\'s installation instructions and install missing dependencies.'
+ ), $module->getName()));
+ }
+ }
+}
+
+// The following doesn't use ipl\Html because that's what the error possibly is about
+?>
+
+
+
+
+
+
+
+
diff --git a/application/views/scripts/filter/index.phtml b/application/views/scripts/filter/index.phtml
new file mode 100644
index 0000000..5e6a63d
--- /dev/null
+++ b/application/views/scripts/filter/index.phtml
@@ -0,0 +1,11 @@
+form;
+
+if ($this->tree) {
+ echo $this->tree->render($this);
+ echo '';
+ echo $this->sqlString;
+ echo '
';
+ print_r($this->params);
+}
\ No newline at end of file
diff --git a/application/views/scripts/form/reorder-authbackend.phtml b/application/views/scripts/form/reorder-authbackend.phtml
new file mode 100644
index 0000000..34b10b3
--- /dev/null
+++ b/application/views/scripts/form/reorder-authbackend.phtml
@@ -0,0 +1,83 @@
+
diff --git a/application/views/scripts/group/form.phtml b/application/views/scripts/group/form.phtml
new file mode 100644
index 0000000..cbf0659
--- /dev/null
+++ b/application/views/scripts/group/form.phtml
@@ -0,0 +1,6 @@
+
+ = $tabs->showOnlyCloseButton(); ?>
+
+
+ = $form; ?>
+
\ No newline at end of file
diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml
new file mode 100644
index 0000000..d362db4
--- /dev/null
+++ b/application/views/scripts/group/list.phtml
@@ -0,0 +1,96 @@
+compact): ?>
+
+ = $this->tabs ?>
+ = $this->paginator ?>
+
+ = $this->limiter ?>
+ = $this->sortBox ?>
+
+ = $this->backendSelection ?>
+ = $this->filterEditor ?>
+
+
+
+translate('No backend found which is able to list user groups') . '
';
+ return;
+} else {
+ $extensible = $this->hasPermission('config/access-control/groups') && $backend instanceof Extensible;
+ $reducible = $this->hasPermission('config/access-control/groups') && $backend instanceof Reducible;
+}
+?>
+
+
+ = $this->qlink(
+ $this->translate('Add a New User Group'),
+ 'group/add',
+ array('backend' => $backend->getName()),
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus'
+ )
+ ) ?>
+
+
+hasResult()): ?>
+ = $this->translate('No user groups found matching the filter'); ?>
+
+
+
+
+
+ = $this->translate('User Group'); ?>
+
+ = $this->translate('Remove'); ?>
+
+
+
+
+
+
+
+ = $this->qlink(
+ $group->group_name,
+ 'group/show',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'title' => sprintf(
+ $this->translate('Show detailed information for user group %s'),
+ $group->group_name
+ )
+ )
+ ); ?>
+
+
+
+ = $this->qlink(
+ null,
+ 'group/remove',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'class' => 'action-link',
+ 'title' => sprintf($this->translate('Remove user group %s'), $group->group_name),
+ 'icon' => 'cancel'
+ )
+ ); ?>
+
+
+
+
+
+
+
diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml
new file mode 100644
index 0000000..75f0b75
--- /dev/null
+++ b/application/views/scripts/group/show.phtml
@@ -0,0 +1,108 @@
+hasPermission('config/access-control/groups') && $backend instanceof Extensible;
+
+$editLink = null;
+if ($this->hasPermission('config/access-control/groups') && $backend instanceof Updatable) {
+ $editLink = $this->qlink(
+ null,
+ 'group/edit',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'title' => sprintf($this->translate('Edit group %s'), $group->group_name),
+ 'class' => 'group-edit',
+ 'icon' => 'edit'
+ )
+ );
+}
+
+?>
+
+compact): ?>
+ = $tabs; ?>
+
+
= $this->escape($group->group_name) ?>= $editLink ?>
+
+
+ = $this->translate('Created at'); ?>
+ = $group->created_at === null ? '-' : $this->formatDateTime($group->created_at); ?>
+
+
+ = $this->translate('Last modified'); ?>
+ = $group->last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?>
+
+
+compact): ?>
+
= $this->translate('Members'); ?>
+
+ = $this->limiter; ?>
+ = $this->paginator; ?>
+ = $this->sortBox; ?>
+
+ = $this->filterEditor; ?>
+
+
+
+
+ = $this->qlink(
+ $this->translate('Add New Member'),
+ 'group/addmember',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'icon' => 'plus',
+ 'class' => 'button-link'
+ )
+ ) ?>
+
+
+hasResult()): ?>
+
= $this->translate('No group member found matching the filter'); ?>
+
+
+
+
+
+
+ = $this->translate('Username'); ?>
+
+ = $this->translate('Remove'); ?>
+
+
+
+
+
+
+
+ hasPermission('config/access-control/users')
+ && ($userBackend = $backend->getUserBackendName($member->user_name)) !== null
+ ): ?>
+ = $this->qlink($member->user_name, 'user/show', array(
+ 'backend' => $userBackend,
+ 'user' => $member->user_name
+ ), array(
+ 'title' => sprintf($this->translate('Show detailed information about %s'), $member->user_name)
+ )); ?>
+
+ = $this->escape($member->user_name); ?>
+
+
+
+
+ getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>
+
+
+
+
+
+
+
diff --git a/application/views/scripts/iframe/index.phtml b/application/views/scripts/iframe/index.phtml
new file mode 100644
index 0000000..96e9de7
--- /dev/null
+++ b/application/views/scripts/iframe/index.phtml
@@ -0,0 +1,8 @@
+
+
+ = $tabs ?>
+
+
+
+
+
diff --git a/application/views/scripts/index/welcome.phtml b/application/views/scripts/index/welcome.phtml
new file mode 100644
index 0000000..496dec9
--- /dev/null
+++ b/application/views/scripts/index/welcome.phtml
@@ -0,0 +1,2 @@
+Welcome to Icinga!
+You should install/configure some modules now!
diff --git a/application/views/scripts/inline.phtml b/application/views/scripts/inline.phtml
new file mode 100644
index 0000000..2534d44
--- /dev/null
+++ b/application/views/scripts/inline.phtml
@@ -0,0 +1,2 @@
+= $this->layout()->content ?>
+
diff --git a/application/views/scripts/joystickPagination.phtml b/application/views/scripts/joystickPagination.phtml
new file mode 100644
index 0000000..a8c24c9
--- /dev/null
+++ b/application/views/scripts/joystickPagination.phtml
@@ -0,0 +1,162 @@
+translate('%s: Show %s %u to %u out of %u', 'pagination.joystick');
+$xAxisPages = $xAxisPaginator->getPages('all');
+$yAxisPages = $yAxisPaginator->getPages('all');
+
+$flipUrl = Url::fromRequest();
+if ($flipUrl->getParam('flipped')) {
+ $flipUrl->remove('flipped');
+} else {
+ $flipUrl->setParam('flipped');
+}
+if ($flipUrl->hasParam('page')) {
+ $flipUrl->setParam('page', implode(',', array_reverse(explode(',', $flipUrl->getParam('page')))));
+}
+if ($flipUrl->hasParam('limit')) {
+ $flipUrl->setParam('limit', implode(',', array_reverse(explode(',', $flipUrl->getParam('limit')))));
+}
+
+$totalYAxisPages = $yAxisPaginator->count();
+$currentYAxisPage = $yAxisPaginator->getCurrentPageNumber();
+$prevYAxisPage = $currentYAxisPage > 1 ? $currentYAxisPage - 1 : null;
+$nextYAxisPage = $currentYAxisPage < $totalYAxisPages ? $currentYAxisPage + 1 : null;
+
+$totalXAxisPages = $xAxisPaginator->count();
+$currentXAxisPage = $xAxisPaginator->getCurrentPageNumber();
+$prevXAxisPage = $currentXAxisPage > 1 ? $currentXAxisPage - 1 : null;
+$nextXAxisPage = $currentXAxisPage < $totalXAxisPages ? $currentXAxisPage + 1 : null;
+
+?>
+
+
diff --git a/application/views/scripts/layout/announcements.phtml b/application/views/scripts/layout/announcements.phtml
new file mode 100644
index 0000000..3be6b83
--- /dev/null
+++ b/application/views/scripts/layout/announcements.phtml
@@ -0,0 +1 @@
+= $this->widget('announcements') ?>
diff --git a/application/views/scripts/layout/menu.phtml b/application/views/scripts/layout/menu.phtml
new file mode 100644
index 0000000..dfb544d
--- /dev/null
+++ b/application/views/scripts/layout/menu.phtml
@@ -0,0 +1,20 @@
+setUser($this->Auth()->getUser());
+
+if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?>
+
+
+= $menuRenderer->setCssClass('primary-nav')->setElementTag('nav')->setHeading(t('Navigation')); ?>
+
diff --git a/application/views/scripts/list/applicationlog.phtml b/application/views/scripts/list/applicationlog.phtml
new file mode 100644
index 0000000..ca41c33
--- /dev/null
+++ b/application/views/scripts/list/applicationlog.phtml
@@ -0,0 +1,29 @@
+compact): ?>
+
+ = $this->tabs ?>
+ = $this->paginator ?>
+
+ = $this->limiter ?>
+
+
+
+
+logData !== null): ?>
+
+
+ logData as $value): ?>
+ datetime) ?>
+
+
+ = $this->escape($datetime->format('d.m. H:i')) ?>
+ = $this->escape($value->loglevel) ?>
+
+
+ = nl2br($this->escape($value->message), false) ?>
+
+
+
+
+
+
+
diff --git a/application/views/scripts/mixedPagination.phtml b/application/views/scripts/mixedPagination.phtml
new file mode 100644
index 0000000..e92a9c9
--- /dev/null
+++ b/application/views/scripts/mixedPagination.phtml
@@ -0,0 +1,79 @@
+pageCount <= 1) return; ?>
+
diff --git a/application/views/scripts/navigation/dashboard.phtml b/application/views/scripts/navigation/dashboard.phtml
new file mode 100644
index 0000000..f069882
--- /dev/null
+++ b/application/views/scripts/navigation/dashboard.phtml
@@ -0,0 +1,27 @@
+
+
+ = $tabs ?>
+
+
diff --git a/application/views/scripts/navigation/index.phtml b/application/views/scripts/navigation/index.phtml
new file mode 100644
index 0000000..bf08562
--- /dev/null
+++ b/application/views/scripts/navigation/index.phtml
@@ -0,0 +1,78 @@
+compact): ?>
+
+ = $this->tabs ?>
+
+ = $this->sortBox ?>
+
+ = $this->filterEditor ?>
+
+
+
+ = $this->qlink(
+ $this->translate('Create a New Navigation Item') ,
+ 'navigation/add',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new navigation item')
+ )
+ ) ?>
+
+
= $this->translate('You did not create any navigation item yet.') ?>
+
+
+
+
+
+ = $this->translate('Navigation') ?>
+ = $this->translate('Type') ?>
+ = $this->translate('Shared') ?>
+
+
+
+
+
+
+
+ = $this->qlink(
+ $item->name,
+ 'navigation/edit',
+ array(
+ 'name' => $item->name,
+ 'type' => $item->type
+ ),
+ array(
+ 'title' => sprintf($this->translate('Edit navigation item %s'), $item->name)
+ )
+ ) ?>
+
+
+ = $item->type && isset($types[$item->type])
+ ? $this->escape($types[$item->type])
+ : $this->escape($this->translate('Unknown')) ?>
+
+
+ = $item->owner ? $this->translate('Yes') : $this->translate('No') ?>
+
+
+ = $this->qlink(
+ '',
+ 'navigation/remove',
+ array(
+ 'name' => $item->name,
+ 'type' => $item->type
+ ),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove navigation item %s'), $item->name)
+ )
+ ) ?>
+
+
+
+
+
+
diff --git a/application/views/scripts/navigation/shared.phtml b/application/views/scripts/navigation/shared.phtml
new file mode 100644
index 0000000..e9e9164
--- /dev/null
+++ b/application/views/scripts/navigation/shared.phtml
@@ -0,0 +1,68 @@
+compact): ?>
+
+ = $this->tabs; ?>
+
+ = $this->sortBox ?>
+
+
+
+
+
+
= $this->translate('There are currently no navigation items being shared'); ?>
+
+
+
+ = $this->translate('Shared Navigation'); ?>
+ = $this->translate('Type'); ?>
+ = $this->translate('Owner'); ?>
+ = $this->translate('Unshare'); ?>
+
+
+
+
+ = $this->qlink(
+ $item->name,
+ 'navigation/edit',
+ array(
+ 'name' => $item->name,
+ 'type' => $item->type,
+ 'owner' => $item->owner,
+ 'referrer' => 'shared'
+ ),
+ array(
+ 'title' => sprintf($this->translate('Edit shared navigation item %s'), $item->name)
+ )
+ ); ?>
+ = $item->type && isset($types[$item->type])
+ ? $this->escape($types[$item->type])
+ : $this->escape($this->translate('Unknown')); ?>
+ = $this->escape($item->owner); ?>
+ parent): ?>
+ = $this->icon(
+ 'block',
+ sprintf(
+ $this->translate(
+ 'This is a child of the navigation item %1$s. You can'
+ . ' only unshare this item by unsharing %1$s'
+ ),
+ $item->parent
+ )
+ ); ?>
+
+ = $removeForm
+ ->setDefault('name', $item->name)
+ ->setAction(Url::fromPath(
+ 'navigation/unshare',
+ array('type' => $item->type, 'owner' => $item->owner)
+ )); ?>
+
+
+
+
+
+
+
diff --git a/application/views/scripts/pivottablePagination.phtml b/application/views/scripts/pivottablePagination.phtml
new file mode 100644
index 0000000..ce18014
--- /dev/null
+++ b/application/views/scripts/pivottablePagination.phtml
@@ -0,0 +1,48 @@
+count() <= 1 && $yAxisPaginator->count() <= 1) {
+ return; // Display this pagination only if there are multiple pages
+}
+
+$fromTo = t('%s: %d to %d of %d (on the %s-axis)');
+$xAxisPages = $xAxisPaginator->getPages('all');
+$yAxisPages = $yAxisPaginator->getPages('all');
+
+?>
+
+
diff --git a/application/views/scripts/role/list.phtml b/application/views/scripts/role/list.phtml
new file mode 100644
index 0000000..352e3e2
--- /dev/null
+++ b/application/views/scripts/role/list.phtml
@@ -0,0 +1,65 @@
+
+ = $tabs ?>
+ = $this->paginator ?>
+
+ = $this->limiter ?>
+ = $this->sortBox ?>
+
+ = $this->filterEditor ?>
+
+
+ = $this->qlink(
+ $this->translate('Create a New Role') ,
+ 'role/add',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new role')
+ )
+ ) ?>
+hasResult()): ?>
+
= $this->translate('No roles found.') ?>
+
+
+
+
+ = $this->translate('Name') ?>
+ = $this->translate('Users') ?>
+ = $this->translate('Groups') ?>
+ = $this->translate('Inherits From') ?>
+
+
+
+
+ $role): /** @var object $role */ ?>
+
+
+ = $this->qlink(
+ $name,
+ 'role/edit',
+ array('role' => $name),
+ array('title' => sprintf($this->translate('Edit role %s'), $name))
+ ) ?>
+
+ = $this->escape($role->users) ?>
+ = $this->escape($role->groups) ?>
+ = $this->escape($role->parent) ?>
+
+ = $this->qlink(
+ '',
+ 'role/remove',
+ array('role' => $name),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove role %s'), $name)
+ )
+ ) ?>
+
+
+
+
+
+
diff --git a/application/views/scripts/search/hint.phtml b/application/views/scripts/search/hint.phtml
new file mode 100644
index 0000000..d54c0b2
--- /dev/null
+++ b/application/views/scripts/search/hint.phtml
@@ -0,0 +1,8 @@
+
+
= $this->translate("I'm ready to search, waiting for your input") ?>
+
= $this->translate('Hint') ?>: = $this->translate(
+ 'Please use the asterisk (*) as a placeholder for wildcard searches.'
+ . " For convenience I'll always add a wildcard in front and after your"
+ . ' search string.'
+) ?>
+
diff --git a/application/views/scripts/search/index.phtml b/application/views/scripts/search/index.phtml
new file mode 100644
index 0000000..321597e
--- /dev/null
+++ b/application/views/scripts/search/index.phtml
@@ -0,0 +1,7 @@
+
+= $this->dashboard->getTabs() ?>
+
+
+
+= $this->dashboard ?>
+
diff --git a/application/views/scripts/showConfiguration.phtml b/application/views/scripts/showConfiguration.phtml
new file mode 100644
index 0000000..682b349
--- /dev/null
+++ b/application/views/scripts/showConfiguration.phtml
@@ -0,0 +1,27 @@
+
+
= $this->translate('Saving Configuration Failed'); ?>
+
+ = sprintf(
+ $this->translate('The file %s couldn\'t be stored. (Error: "%s")'),
+ $this->escape($filePath),
+ $this->escape($errorMessage)
+ ); ?>
+
+ = $this->translate('This could have one or more of the following reasons:'); ?>
+
+
+ = $this->translate('You don\'t have file-system permissions to write to the file'); ?>
+ = $this->translate('Something went wrong while writing the file'); ?>
+ = $this->translate('There\'s an application error preventing you from persisting the configuration'); ?>
+
+
+
+ = $this->translate('Details can be found in the application log. (If you don\'t have access to this log, call your administrator in this case)'); ?>
+
+ = $this->translate('In case you can access the file by yourself, you can open it and insert the config manually:'); ?>
+
+
+
+ = $this->escape($configString); ?>
+
+
diff --git a/application/views/scripts/simple-form.phtml b/application/views/scripts/simple-form.phtml
new file mode 100644
index 0000000..9bcba74
--- /dev/null
+++ b/application/views/scripts/simple-form.phtml
@@ -0,0 +1,6 @@
+
+ = $tabs ?>
+
+
+ = $form->create()->setTitle(null) // @TODO(el): create() has to be called because the UserForm is setting the title there ?>
+
diff --git a/application/views/scripts/user/form.phtml b/application/views/scripts/user/form.phtml
new file mode 100644
index 0000000..cbf0659
--- /dev/null
+++ b/application/views/scripts/user/form.phtml
@@ -0,0 +1,6 @@
+
+ = $tabs->showOnlyCloseButton(); ?>
+
+
+ = $form; ?>
+
\ No newline at end of file
diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml
new file mode 100644
index 0000000..bdb5f1a
--- /dev/null
+++ b/application/views/scripts/user/list.phtml
@@ -0,0 +1,90 @@
+compact): ?>
+
+ = $this->tabs ?>
+ = $this->paginator ?>
+
+ = $this->limiter ?>
+ = $this->sortBox ?>
+
+ = $this->backendSelection ?>
+ = $this->filterEditor ?>
+
+
+
+translate('No backend found which is able to list users') . '
';
+ return;
+} else {
+ $extensible = $this->hasPermission('config/access-control/users') && $backend instanceof Extensible;
+ $reducible = $this->hasPermission('config/access-control/users') && $backend instanceof Reducible;
+}
+?>
+
+
+ = $this->qlink(
+ $this->translate('Add a New User') ,
+ 'user/add',
+ array('backend' => $backend->getName()),
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus'
+ )
+ ) ?>
+
+
+hasResult()): ?>
+ = $this->translate('No users found matching the filter') ?>
+
+
+
+
+
+
+ = $this->translate('Username') ?>
+
+ = $this->translate('Remove') ?>
+
+
+
+
+
+
+ = $this->qlink(
+ $user->user_name,
+ 'user/show',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'title' => sprintf($this->translate('Show detailed information about %s'), $user->user_name)
+ )
+ ) ?>
+
+ = $this->qlink(
+ null,
+ 'user/remove',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove user %s'), $user->user_name)
+ )
+ ) ?>
+
+
+
+
+
+
diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml
new file mode 100644
index 0000000..b19c15a
--- /dev/null
+++ b/application/views/scripts/user/show.phtml
@@ -0,0 +1,138 @@
+
+
+compact): ?>
+ = $tabs; ?>
+
+
= $this->escape($user->user_name) ?>
+ hasPermission('config/access-control/users') && $backend instanceof Updatable) {
+ echo $this->qlink(
+ $this->translate('Edit User'),
+ 'user/edit',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'class' => 'button-link',
+ 'icon' => 'edit',
+ 'title' => sprintf($this->translate('Edit user %s'), $user->user_name)
+ )
+ );
+ }
+ ?>
+
+
+ = $this->translate('State'); ?>
+ = $user->is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?>
+
+
+ = $this->translate('Created at'); ?>
+ = $user->created_at === null ? '-' : $this->formatDateTime($user->created_at); ?>
+
+
+ = $this->translate('Last modified'); ?>
+ = $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?>
+
+
+ = $this->translate('Role Memberships'); ?>
+
+ getRoles(); ?>
+
+
+
+
+ allowedToEditRoles): ?>
+ = $this->qlink(
+ $role->getName(),
+ 'role/edit',
+ ['role' => $role->getName()],
+ ['title' => sprintf($this->translate('Edit role %s'), $role->getName())]
+ );
+ $role === end($roles) ? print '' : print ', '; ?>
+
+ = $role->getName() ?>
+
+
+
+
+
+ = $this->translate('No memberships found'); ?>
+
+
+
+
+compact): ?>
+
= $this->translate('Group Memberships'); ?>
+
+ = $this->limiter; ?>
+ = $this->paginator; ?>
+ = $this->sortBox; ?>
+
+ = $this->filterEditor; ?>
+
+
+
+
+ = $this->qlink(
+ $this->translate('Create New Membership'),
+ 'user/createmembership',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'icon' => 'plus',
+ 'class' => 'button-link'
+ )
+ ) ?>
+
+
+hasResult()): ?>
+
= $this->translate('No memberships found matching the filter'); ?>
+
+
+
+
+
+
+ = $this->translate('Group'); ?>
+ = $this->translate('Cancel', 'group.membership'); ?>
+
+
+
+
+
+
+ hasPermission('config/access-control/groups') && $membership->backend instanceof Selectable): ?>
+ = $this->qlink($membership->group_name, 'group/show', array(
+ 'backend' => $membership->backend->getName(),
+ 'group' => $membership->group_name
+ ), array(
+ 'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name)
+ )); ?>
+
+ = $this->escape($membership->group_name); ?>
+
+
+
+ backend instanceof Reducible): ?>
+ = $removeForm->setAction($this->url('group/removemember', array(
+ 'backend' => $membership->backend->getName(),
+ 'group' => $membership->group_name
+ ))); ?>
+
+ -
+
+
+
+
+
+
+
--
cgit v1.2.3