summaryrefslogtreecommitdiffstats
path: root/application
diff options
context:
space:
mode:
Diffstat (limited to 'application')
-rw-r--r--application/VERSION1
-rw-r--r--application/clicommands/AutocompleteCommand.php120
-rw-r--r--application/clicommands/HelpCommand.php43
-rw-r--r--application/clicommands/ModuleCommand.php228
-rw-r--r--application/clicommands/VersionCommand.php55
-rw-r--r--application/clicommands/WebCommand.php101
-rw-r--r--application/controllers/AboutController.php27
-rw-r--r--application/controllers/AccountController.php83
-rw-r--r--application/controllers/AnnouncementsController.php123
-rw-r--r--application/controllers/ApplicationStateController.php95
-rw-r--r--application/controllers/AuthenticationController.php127
-rw-r--r--application/controllers/ConfigController.php518
-rw-r--r--application/controllers/DashboardController.php346
-rw-r--r--application/controllers/ErrorController.php176
-rw-r--r--application/controllers/GroupController.php418
-rw-r--r--application/controllers/HealthController.php65
-rw-r--r--application/controllers/IframeController.php20
-rw-r--r--application/controllers/IndexController.php36
-rw-r--r--application/controllers/LayoutController.php28
-rw-r--r--application/controllers/ListController.php59
-rw-r--r--application/controllers/ManageUserDevicesController.php84
-rw-r--r--application/controllers/MigrationsController.php249
-rw-r--r--application/controllers/MyDevicesController.php74
-rw-r--r--application/controllers/NavigationController.php447
-rw-r--r--application/controllers/RoleController.php392
-rw-r--r--application/controllers/SearchController.php28
-rw-r--r--application/controllers/StaticController.php78
-rw-r--r--application/controllers/UserController.php374
-rw-r--r--application/controllers/UsergroupbackendController.php133
-rw-r--r--application/fonts/fontello-ifont/LICENSE.txt57
-rw-r--r--application/fonts/fontello-ifont/README.txt75
-rw-r--r--application/fonts/fontello-ifont/config.json874
-rw-r--r--application/fonts/fontello-ifont/css/animation.css85
-rw-r--r--application/fonts/fontello-ifont/css/ifont-codes.css145
-rw-r--r--application/fonts/fontello-ifont/css/ifont-embedded.css198
-rw-r--r--application/fonts/fontello-ifont/css/ifont-ie7-codes.css145
-rw-r--r--application/fonts/fontello-ifont/css/ifont-ie7.css156
-rw-r--r--application/fonts/fontello-ifont/css/ifont.css201
-rw-r--r--application/fonts/fontello-ifont/demo.html519
-rw-r--r--application/fonts/fontello-ifont/font/ifont.eotbin0 -> 46504 bytes
-rw-r--r--application/fonts/fontello-ifont/font/ifont.svg298
-rw-r--r--application/fonts/fontello-ifont/font/ifont.ttfbin0 -> 46348 bytes
-rw-r--r--application/fonts/fontello-ifont/font/ifont.woffbin0 -> 27688 bytes
-rw-r--r--application/fonts/fontello-ifont/font/ifont.woff2bin0 -> 22984 bytes
-rw-r--r--application/fonts/icingaweb.md9
-rw-r--r--application/forms/Account/ChangePasswordForm.php123
-rw-r--r--application/forms/AcknowledgeApplicationStateMessageForm.php75
-rw-r--r--application/forms/ActionForm.php78
-rw-r--r--application/forms/Announcement/AcknowledgeAnnouncementForm.php92
-rw-r--r--application/forms/Announcement/AnnouncementForm.php135
-rw-r--r--application/forms/Authentication/LoginForm.php214
-rw-r--r--application/forms/AutoRefreshForm.php83
-rw-r--r--application/forms/Config/General/ApplicationConfigForm.php105
-rw-r--r--application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php46
-rw-r--r--application/forms/Config/General/LoggingConfigForm.php142
-rw-r--r--application/forms/Config/General/ThemingConfigForm.php78
-rw-r--r--application/forms/Config/GeneralConfigForm.php40
-rw-r--r--application/forms/Config/Resource/DbResourceForm.php239
-rw-r--r--application/forms/Config/Resource/FileResourceForm.php67
-rw-r--r--application/forms/Config/Resource/LdapResourceForm.php129
-rw-r--r--application/forms/Config/Resource/SshResourceForm.php148
-rw-r--r--application/forms/Config/ResourceConfigForm.php442
-rw-r--r--application/forms/Config/User/CreateMembershipForm.php192
-rw-r--r--application/forms/Config/User/UserForm.php210
-rw-r--r--application/forms/Config/UserBackend/DbBackendForm.php82
-rw-r--r--application/forms/Config/UserBackend/ExternalBackendForm.php83
-rw-r--r--application/forms/Config/UserBackend/LdapBackendForm.php414
-rw-r--r--application/forms/Config/UserBackendConfigForm.php482
-rw-r--r--application/forms/Config/UserBackendReorderForm.php86
-rw-r--r--application/forms/Config/UserGroup/AddMemberForm.php183
-rw-r--r--application/forms/Config/UserGroup/DbUserGroupBackendForm.php79
-rw-r--r--application/forms/Config/UserGroup/LdapUserGroupBackendForm.php370
-rw-r--r--application/forms/Config/UserGroup/UserGroupBackendForm.php314
-rw-r--r--application/forms/Config/UserGroup/UserGroupForm.php158
-rw-r--r--application/forms/ConfigForm.php192
-rw-r--r--application/forms/ConfirmRemovalForm.php38
-rw-r--r--application/forms/Control/LimiterControlForm.php134
-rw-r--r--application/forms/Dashboard/DashletForm.php171
-rw-r--r--application/forms/LdapDiscoveryForm.php34
-rw-r--r--application/forms/MigrationForm.php143
-rw-r--r--application/forms/Navigation/DashletForm.php35
-rw-r--r--application/forms/Navigation/MenuItemForm.php31
-rw-r--r--application/forms/Navigation/NavigationConfigForm.php853
-rw-r--r--application/forms/Navigation/NavigationItemForm.php114
-rw-r--r--application/forms/PreferenceForm.php485
-rw-r--r--application/forms/RepositoryForm.php453
-rw-r--r--application/forms/Security/RoleForm.php632
-rw-r--r--application/layouts/scripts/body.phtml98
-rw-r--r--application/layouts/scripts/external-logout.phtml34
-rw-r--r--application/layouts/scripts/guest-error.phtml10
-rw-r--r--application/layouts/scripts/inline.phtml2
-rw-r--r--application/layouts/scripts/layout.phtml83
-rw-r--r--application/layouts/scripts/parts/navigation.phtml35
-rw-r--r--application/layouts/scripts/pdf.phtml44
-rw-r--r--application/views/helpers/CreateTicketLinks.php23
-rw-r--r--application/views/helpers/FormDate.php46
-rw-r--r--application/views/helpers/FormDateTime.php63
-rw-r--r--application/views/helpers/FormNumber.php77
-rw-r--r--application/views/helpers/FormTime.php46
-rw-r--r--application/views/helpers/ProtectId.php13
-rw-r--r--application/views/helpers/Util.php68
-rw-r--r--application/views/scripts/about/index.phtml199
-rw-r--r--application/views/scripts/account/index.phtml11
-rw-r--r--application/views/scripts/announcements/index.phtml71
-rw-r--r--application/views/scripts/authentication/login.phtml74
-rw-r--r--application/views/scripts/authentication/logout.phtml64
-rw-r--r--application/views/scripts/config/devtools.phtml6
-rw-r--r--application/views/scripts/config/general.phtml6
-rw-r--r--application/views/scripts/config/module-configuration-error.phtml28
-rw-r--r--application/views/scripts/config/module.phtml136
-rw-r--r--application/views/scripts/config/modules.phtml42
-rw-r--r--application/views/scripts/config/resource.phtml73
-rw-r--r--application/views/scripts/config/resource/create.phtml6
-rw-r--r--application/views/scripts/config/resource/modify.phtml6
-rw-r--r--application/views/scripts/config/resource/remove.phtml6
-rw-r--r--application/views/scripts/config/userbackend/reorder.phtml75
-rw-r--r--application/views/scripts/dashboard/error.phtml13
-rw-r--r--application/views/scripts/dashboard/index.phtml26
-rw-r--r--application/views/scripts/dashboard/new-dashlet.phtml6
-rw-r--r--application/views/scripts/dashboard/remove-dashlet.phtml6
-rw-r--r--application/views/scripts/dashboard/remove-pane.phtml6
-rw-r--r--application/views/scripts/dashboard/rename-pane.phtml6
-rw-r--r--application/views/scripts/dashboard/settings.phtml91
-rw-r--r--application/views/scripts/dashboard/update-dashlet.phtml6
-rw-r--r--application/views/scripts/error/error.phtml106
-rw-r--r--application/views/scripts/filter/index.phtml11
-rw-r--r--application/views/scripts/form/reorder-authbackend.phtml83
-rw-r--r--application/views/scripts/group/form.phtml6
-rw-r--r--application/views/scripts/group/list.phtml96
-rw-r--r--application/views/scripts/group/show.phtml108
-rw-r--r--application/views/scripts/iframe/index.phtml8
-rw-r--r--application/views/scripts/index/welcome.phtml2
-rw-r--r--application/views/scripts/inline.phtml2
-rw-r--r--application/views/scripts/joystickPagination.phtml162
-rw-r--r--application/views/scripts/layout/announcements.phtml1
-rw-r--r--application/views/scripts/layout/menu.phtml20
-rw-r--r--application/views/scripts/list/applicationlog.phtml29
-rw-r--r--application/views/scripts/mixedPagination.phtml79
-rw-r--r--application/views/scripts/navigation/dashboard.phtml27
-rw-r--r--application/views/scripts/navigation/index.phtml78
-rw-r--r--application/views/scripts/navigation/shared.phtml68
-rw-r--r--application/views/scripts/pivottablePagination.phtml48
-rw-r--r--application/views/scripts/role/list.phtml65
-rw-r--r--application/views/scripts/search/hint.phtml8
-rw-r--r--application/views/scripts/search/index.phtml7
-rw-r--r--application/views/scripts/showConfiguration.phtml27
-rw-r--r--application/views/scripts/simple-form.phtml6
-rw-r--r--application/views/scripts/user/form.phtml6
-rw-r--r--application/views/scripts/user/list.phtml90
-rw-r--r--application/views/scripts/user/show.phtml138
150 files changed, 18380 insertions, 0 deletions
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 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Cli\Command;
+use Icinga\Cli\Loader;
+
+/**
+ * Autocomplete for modules, commands and actions
+ *
+ * The autocomplete 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 [<module>] [<command> [<action>]]
+ */
+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 [<module>] [<command> [<action>]]
+ */
+ 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 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Cli\Command;
+use Icinga\Cli\Loader;
+use Icinga\Cli\Documentation;
+
+/**
+ * Help for modules, commands and actions
+ *
+ * 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 [<module>] [<command> [<action>]]
+ */
+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 [<module>] [<command> [<action>]]
+ */
+ 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 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Application\Logger;
+use Icinga\Application\Modules\Manager;
+use Icinga\Cli\Command;
+
+/**
+ * List and handle modules
+ *
+ * The module command allows you to handle your IcingaWeb modules
+ *
+ * Usage: icingacli module [<action>] [<modulename>]
+ */
+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 <module-name>
+ */
+ 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 <module-name>
+ */
+ 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 <search-string>
+ */
+ 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 <module-name>
+ * icingacli module install </path/to/archive.tar.gz>
+ */
+ 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 <module-name>
+ */
+ 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 <module-name>
+ */
+ 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 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Application\Version;
+use Icinga\Application\Icinga;
+use Icinga\Cli\Loader;
+use Icinga\Cli\Command;
+
+/**
+ * Shows version of Icinga Web 2, loaded modules and PHP
+ *
+ * The version command shows version numbers for Icinga Web 2, loaded modules and PHP.
+ *
+ * Usage: icingacli --version
+ */
+class VersionCommand extends Command
+{
+ protected $defaultActionName = 'show';
+
+ /**
+ * Shows version of Icinga Web 2, loaded modules and PHP
+ *
+ * The version command shows version numbers for Icinga Web 2, loaded modules and PHP.
+ *
+ * Usage: icingacli --version
+ */
+ public function showAction()
+ {
+ $getVersion = Version::get();
+ printf("%-12s %-9s \n", 'Icinga Web 2', $getVersion['appVersion']);
+
+ if (isset($getVersion['gitCommitID'])) {
+ printf("%-12s %-9s \n", 'Git Commit', $getVersion['gitCommitID']);
+ }
+
+ printf("%-12s %-9s \n", 'PHP Version', PHP_VERSION);
+
+ $modules = Icinga::app()->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 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Application\Icinga;
+use Icinga\Cli\Command;
+use Icinga\Exception\IcingaException;
+
+class WebCommand extends Command
+{
+ /**
+ * Serve Icinga Web 2 with PHP's built-in web server
+ *
+ * USAGE
+ *
+ * icingacli web serve [options] [<document-root>]
+ *
+ * OPTIONS
+ *
+ * --daemonize Run in background
+ * --port=<port> The port to listen on
+ * --listen=<host:port> The address to listen on
+ * <document-root> 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 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Version;
+use Icinga\Web\Controller;
+
+class AboutController extends Controller
+{
+ public function indexAction()
+ {
+ $this->view->version = Version::get();
+ $this->view->libraries = Icinga::app()->getLibraries();
+ $this->view->modules = Icinga::app()->getModuleManager()->getLoadedModules();
+ $this->view->title = $this->translate('About');
+ $this->view->tabs = $this->getTabs()->add(
+ 'about',
+ array(
+ 'label' => $this->translate('About'),
+ 'title' => $this->translate('About Icinga Web 2'),
+ 'url' => 'about'
+ )
+ )->activate('about');
+ }
+}
diff --git a/application/controllers/AccountController.php b/application/controllers/AccountController.php
new file mode 100644
index 0000000..f172cfe
--- /dev/null
+++ b/application/controllers/AccountController.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Forms\Account\ChangePasswordForm;
+use Icinga\Forms\PreferenceForm;
+use Icinga\User\Preferences\PreferencesStore;
+use Icinga\Web\Controller;
+
+/**
+ * My Account
+ */
+class AccountController extends Controller
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->getTabs()
+ ->add('account', array(
+ 'title' => $this->translate('Update your account'),
+ 'label' => $this->translate('My Account'),
+ 'url' => 'account'
+ ))
+ ->add('navigation', array(
+ 'title' => $this->translate('List and configure your own navigation items'),
+ 'label' => $this->translate('Navigation'),
+ 'url' => 'navigation'
+ ))
+ ->add(
+ 'devices',
+ array(
+ 'title' => $this->translate('List of devices you are logged in'),
+ 'label' => $this->translate('My Devices'),
+ 'url' => 'my-devices'
+ )
+ );
+ }
+
+ /**
+ * My account
+ */
+ public function indexAction()
+ {
+ $config = Config::app()->getSection('global');
+ $user = $this->Auth()->getUser();
+ if ($user->getAdditional('backend_type') === 'db') {
+ if ($user->can('user/password-change')) {
+ try {
+ $userBackend = UserBackend::create($user->getAdditional('backend_name'));
+ } catch (ConfigurationError $e) {
+ $userBackend = null;
+ }
+ if ($userBackend !== null) {
+ $changePasswordForm = new ChangePasswordForm();
+ $changePasswordForm
+ ->setBackend($userBackend)
+ ->handleRequest();
+ $this->view->changePasswordForm = $changePasswordForm;
+ }
+ }
+ }
+
+ $form = new PreferenceForm();
+ $form->setPreferences($user->getPreferences());
+ if (isset($config->config_resource)) {
+ $form->setStore(PreferencesStore::create(new ConfigObject(array(
+ 'resource' => $config->config_resource
+ )), $user));
+ }
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('My Account');
+ $this->getTabs()->activate('account');
+ }
+}
diff --git a/application/controllers/AnnouncementsController.php b/application/controllers/AnnouncementsController.php
new file mode 100644
index 0000000..ee7fd4c
--- /dev/null
+++ b/application/controllers/AnnouncementsController.php
@@ -0,0 +1,123 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Announcement\AcknowledgeAnnouncementForm;
+use Icinga\Forms\Announcement\AnnouncementForm;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Controller;
+use Icinga\Web\Url;
+
+class AnnouncementsController extends Controller
+{
+ public function init()
+ {
+ $this->view->title = $this->translate('Announcements');
+
+ parent::init();
+ }
+
+ /**
+ * List all announcements
+ */
+ public function indexAction()
+ {
+ $this->getTabs()->add(
+ 'announcements',
+ array(
+ 'active' => true,
+ 'label' => $this->translate('Announcements'),
+ 'title' => $this->translate('List All Announcements'),
+ 'url' => Url::fromPath('announcements')
+ )
+ );
+
+ $announcements = (new AnnouncementIniRepository())
+ ->select([
+ 'id',
+ 'author',
+ 'message',
+ 'start',
+ 'end'
+ ]);
+
+ $sortAndFilterColumns = [
+ 'author' => $this->translate('Author'),
+ 'message' => $this->translate('Message'),
+ 'start' => $this->translate('Start'),
+ 'end' => $this->translate('End')
+ ];
+
+ $this->setupSortControl($sortAndFilterColumns, $announcements, ['start' => 'desc']);
+ $this->setupFilterControl($announcements, $sortAndFilterColumns, ['message']);
+
+ $this->view->announcements = $announcements->fetchAll();
+ }
+
+ /**
+ * Create an announcement
+ */
+ public function newAction()
+ {
+ $this->assertPermission('application/announcements');
+
+ $form = $this->prepareForm()->add();
+ $form->handleRequest();
+ $this->renderForm($form, $this->translate('New Announcement'));
+ }
+
+ /**
+ * Update an announcement
+ */
+ public function updateAction()
+ {
+ $this->assertPermission('application/announcements');
+
+ $form = $this->prepareForm()->edit($this->params->getRequired('id'));
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound($this->translate('Announcement not found'));
+ }
+ $this->renderForm($form, $this->translate('Update Announcement'));
+ }
+
+ /**
+ * Remove an announcement
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('application/announcements');
+
+ $form = $this->prepareForm()->remove($this->params->getRequired('id'));
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound($this->translate('Announcement not found'));
+ }
+ $this->renderForm($form, $this->translate('Remove Announcement'));
+ }
+
+ public function acknowledgeAction()
+ {
+ $this->assertHttpMethod('POST');
+ $this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true);
+ $form = new AcknowledgeAnnouncementForm();
+ $form->handleRequest();
+ }
+
+ /**
+ * Assert permission admin and return a prepared RepositoryForm
+ *
+ * @return AnnouncementForm
+ */
+ protected function prepareForm()
+ {
+ $form = new AnnouncementForm();
+ return $form
+ ->setRepository(new AnnouncementIniRepository())
+ ->setRedirectUrl(Url::fromPath('announcements'));
+ }
+}
diff --git a/application/controllers/ApplicationStateController.php b/application/controllers/ApplicationStateController.php
new file mode 100644
index 0000000..b828ca2
--- /dev/null
+++ b/application/controllers/ApplicationStateController.php
@@ -0,0 +1,95 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Forms\AcknowledgeApplicationStateMessageForm;
+use Icinga\Web\Announcement\AnnouncementCookie;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Controller;
+use Icinga\Web\RememberMe;
+use Icinga\Web\Session;
+use Icinga\Web\Widget;
+
+/**
+ * @TODO(el): https://dev.icinga.com/issues/10646
+ */
+class ApplicationStateController extends Controller
+{
+ protected $requiresAuthentication = false;
+
+ protected $autorefreshInterval = 60;
+
+ public function init()
+ {
+ $this->_helper->layout->disableLayout();
+ $this->_helper->viewRenderer->setNoRender(true);
+ }
+
+ public function indexAction()
+ {
+ if ($this->Auth()->isAuthenticated()) {
+ if (isset($_COOKIE['icingaweb2-session'])) {
+ $last = (int) $_COOKIE['icingaweb2-session'];
+ } else {
+ $last = 0;
+ }
+ $now = time();
+ if ($last + 600 < $now) {
+ Session::getSession()->write();
+ $params = session_get_cookie_params();
+ setcookie(
+ 'icingaweb2-session',
+ $now,
+ 0,
+ $params['path'],
+ $params['domain'],
+ $params['secure'],
+ $params['httponly']
+ );
+ $_COOKIE['icingaweb2-session'] = $now;
+ }
+ $announcementCookie = new AnnouncementCookie();
+ $announcementRepo = new AnnouncementIniRepository();
+ if ($announcementCookie->getEtag() !== $announcementRepo->getEtag()) {
+ $announcementCookie
+ ->setEtag($announcementRepo->getEtag())
+ ->setNextActive($announcementRepo->findNextActive());
+ $this->getResponse()->setCookie($announcementCookie);
+ $this->getResponse()->setHeader('X-Icinga-Announcements', 'refresh', true);
+ } else {
+ $nextActive = $announcementCookie->getNextActive();
+ if ($nextActive && $nextActive <= $now) {
+ $announcementCookie->setNextActive($announcementRepo->findNextActive());
+ $this->getResponse()->setCookie($announcementCookie);
+ $this->getResponse()->setHeader('X-Icinga-Announcements', 'refresh', true);
+ }
+ }
+ }
+
+ RememberMe::removeExpired();
+ }
+
+ public function summaryAction()
+ {
+ if ($this->Auth()->isAuthenticated()) {
+ $this->getResponse()->setBody((string) Widget::create('ApplicationStateMessages'));
+ }
+ }
+
+ public function acknowledgeMessageAction()
+ {
+ if (! $this->Auth()->isAuthenticated()) {
+ $this->getResponse()
+ ->setHttpResponseCode(401)
+ ->sendHeaders();
+ exit;
+ }
+
+ $this->assertHttpMethod('POST');
+
+ $this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true);
+
+ (new AcknowledgeApplicationStateMessageForm())->handleRequest();
+ }
+}
diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php
new file mode 100644
index 0000000..752f845
--- /dev/null
+++ b/application/controllers/AuthenticationController.php
@@ -0,0 +1,127 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\AuthenticationHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Common\Database;
+use Icinga\Exception\AuthenticationException;
+use Icinga\Forms\Authentication\LoginForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Helper\CookieHelper;
+use Icinga\Web\RememberMe;
+use Icinga\Web\Url;
+use RuntimeException;
+
+/**
+ * Application wide controller for authentication
+ */
+class AuthenticationController extends Controller
+{
+ use Database;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $innerLayout = 'inline';
+
+ /**
+ * Log into the application
+ */
+ public function loginAction()
+ {
+ $icinga = Icinga::app();
+ if (($requiresSetup = $icinga->requiresSetup()) && $icinga->setupTokenExists()) {
+ $this->redirectNow(Url::fromPath('setup'));
+ }
+ $form = new LoginForm();
+
+ if (RememberMe::hasCookie() && $this->hasDb()) {
+ $authenticated = false;
+ try {
+ $rememberMeOld = RememberMe::fromCookie();
+ $authenticated = $rememberMeOld->authenticate();
+ if ($authenticated) {
+ $rememberMe = $rememberMeOld->renew();
+ $this->getResponse()->setCookie($rememberMe->getCookie());
+ $rememberMe->persist($rememberMeOld->getAesCrypt()->getIV());
+ }
+ } catch (RuntimeException $e) {
+ Logger::error("Can't authenticate user via remember me cookie: %s", $e->getMessage());
+ } catch (AuthenticationException $e) {
+ Logger::error($e);
+ }
+
+ if (! $authenticated) {
+ $this->getResponse()->setCookie(RememberMe::forget());
+ }
+ }
+
+ if ($this->Auth()->isAuthenticated()) {
+ // Call provided AuthenticationHook(s) when login action is called
+ // but icinga web user is already authenticated
+ AuthenticationHook::triggerLogin($this->Auth()->getUser());
+
+ $redirect = $this->params->get('redirect');
+ if ($redirect) {
+ $redirectUrl = Url::fromPath($redirect, [], $this->getRequest());
+ if ($redirectUrl->isExternal()) {
+ $this->httpBadRequest('nope');
+ }
+ } else {
+ $redirectUrl = $form->getRedirectUrl();
+ }
+
+ $this->redirectNow($redirectUrl);
+ }
+ if (! $requiresSetup) {
+ $cookies = new CookieHelper($this->getRequest());
+ if (! $cookies->isSupported()) {
+ $this
+ ->getResponse()
+ ->setBody("Cookies must be enabled to run this application.\n")
+ ->setHttpResponseCode(403)
+ ->sendResponse();
+ exit;
+ }
+ $form->handleRequest();
+ }
+ $this->view->form = $form;
+ $this->view->defaultTitle = $this->translate('Icinga Web 2 Login');
+ $this->view->requiresSetup = $requiresSetup;
+ }
+
+ /**
+ * Log out the current user
+ */
+ public function logoutAction()
+ {
+ $auth = $this->Auth();
+ if (! $auth->isAuthenticated()) {
+ $this->redirectToLogin();
+ }
+ // Get info whether the user is externally authenticated before removing authorization which destroys the
+ // session and the user object
+ $isExternalUser = $auth->getUser()->isExternalUser();
+ // Call provided AuthenticationHook(s) when logout action is called
+ AuthenticationHook::triggerLogout($auth->getUser());
+ $auth->removeAuthorization();
+ if ($isExternalUser) {
+ $this->view->layout()->setLayout('external-logout');
+ $this->getResponse()->setHttpResponseCode(401);
+ } else {
+ if (RememberMe::hasCookie() && $this->hasDb()) {
+ $this->getResponse()->setCookie(RememberMe::forget());
+ }
+
+ $this->redirectToLogin();
+ }
+ }
+}
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
new file mode 100644
index 0000000..671e1a7
--- /dev/null
+++ b/application/controllers/ConfigController.php
@@ -0,0 +1,518 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Version;
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ActionForm;
+use Icinga\Forms\Config\GeneralConfigForm;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Forms\Config\UserBackendConfigForm;
+use Icinga\Forms\Config\UserBackendReorderForm;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Controller;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget;
+
+/**
+ * Application and module configuration
+ */
+class ConfigController extends Controller
+{
+ /**
+ * Create and return the tabs to display when showing application configuration
+ */
+ public function createApplicationTabs()
+ {
+ $tabs = $this->getTabs();
+ if ($this->hasPermission('config/general')) {
+ $tabs->add('general', array(
+ 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
+ 'label' => $this->translate('General'),
+ 'url' => 'config/general',
+ 'baseTarget' => '_main'
+ ));
+ }
+ if ($this->hasPermission('config/resources')) {
+ $tabs->add('resource', array(
+ 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'),
+ 'label' => $this->translate('Resources'),
+ 'url' => 'config/resource',
+ 'baseTarget' => '_main'
+ ));
+ }
+ if ($this->hasPermission('config/access-control/users')
+ || $this->hasPermission('config/access-control/groups')
+ ) {
+ $tabs->add('authentication', array(
+ 'title' => $this->translate('Configure the user and group backends'),
+ 'label' => $this->translate('Access Control Backends'),
+ 'url' => 'config/userbackend',
+ 'baseTarget' => '_main'
+ ));
+ }
+
+ return $tabs;
+ }
+
+ public function devtoolsAction()
+ {
+ $this->view->tabs = null;
+ }
+
+ /**
+ * Redirect to the general configuration
+ */
+ public function indexAction()
+ {
+ if ($this->hasPermission('config/general')) {
+ $this->redirectNow('config/general');
+ } elseif ($this->hasPermission('config/resources')) {
+ $this->redirectNow('config/resource');
+ } elseif ($this->hasPermission('config/access-control/*')) {
+ $this->redirectNow('config/userbackend');
+ } else {
+ throw new SecurityException('No permission to configure Icinga Web 2');
+ }
+ }
+
+ /**
+ * General configuration
+ *
+ * @throws SecurityException If the user lacks the permission for configuring the general configuration
+ */
+ public function generalAction()
+ {
+ $this->assertPermission('config/general');
+ $form = new GeneralConfigForm();
+ $form->setIniConfig(Config::app());
+ $form->setOnSuccess(function (GeneralConfigForm $form) {
+ $config = Config::app();
+ $useStrictCsp = (bool) $config->get('security', 'use_strict_csp', false);
+ if ($form->onSuccess() === false) {
+ return false;
+ }
+
+ $appConfigForm = $form->getSubForm('form_config_general_application');
+ if ($appConfigForm && (bool) $appConfigForm->getValue('security_use_strict_csp') !== $useStrictCsp) {
+ $this->getResponse()->setReloadWindow(true);
+ }
+ })->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('General');
+ $this->createApplicationTabs()->activate('general');
+ }
+
+ /**
+ * Display the list of all modules
+ */
+ public function modulesAction()
+ {
+ $this->assertPermission('config/modules');
+ // Overwrite tabs created in init
+ // @TODO(el): This seems not natural to me. Module configuration should have its own controller.
+ $this->view->tabs = Widget::create('tabs')
+ ->add('modules', array(
+ 'label' => $this->translate('Modules'),
+ 'title' => $this->translate('List intalled modules'),
+ 'url' => 'config/modules'
+ ))
+ ->activate('modules');
+ $this->view->modules = Icinga::app()->getModuleManager()->select()
+ ->from('modules')
+ ->order('enabled', 'desc')
+ ->order('installed', 'asc')
+ ->order('name');
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->modules);
+ $this->view->title = $this->translate('Modules');
+ }
+
+ public function moduleAction()
+ {
+ $this->assertPermission('config/modules');
+ $app = Icinga::app();
+ $manager = $app->getModuleManager();
+ $name = $this->getParam('name');
+ if ($manager->hasInstalled($name) || $manager->hasEnabled($name)) {
+ $this->view->moduleData = $manager->select()->from('modules')->where('name', $name)->fetchRow();
+ if ($manager->hasLoaded($name)) {
+ $module = $manager->getModule($name);
+ } else {
+ $module = new Module($app, $name, $manager->getModuleDir($name));
+ }
+
+ $toggleForm = new ActionForm();
+ $toggleForm->setDefaults(['identifier' => $name]);
+ if (! $this->view->moduleData->enabled) {
+ $toggleForm->setAction(Url::fromPath('config/moduleenable'));
+ $toggleForm->setDescription(sprintf($this->translate('Enable the %s module'), $name));
+ } elseif ($this->view->moduleData->loaded) {
+ $toggleForm->setAction(Url::fromPath('config/moduledisable'));
+ $toggleForm->setDescription(sprintf($this->translate('Disable the %s module'), $name));
+ } else {
+ $toggleForm = null;
+ }
+
+ $this->view->module = $module;
+ $this->view->libraries = $app->getLibraries();
+ $this->view->moduleManager = $manager;
+ $this->view->toggleForm = $toggleForm;
+ $this->view->title = $module->getName();
+ $this->view->tabs = $module->getConfigTabs()->activate('info');
+ $this->view->moduleGitCommitId = Version::getGitHead($module->getBaseDir());
+ } else {
+ $this->view->module = false;
+ $this->view->tabs = null;
+ }
+ }
+
+ /**
+ * Enable a specific module provided by the 'name' param
+ */
+ public function moduleenableAction()
+ {
+ $this->assertPermission('config/modules');
+
+ $form = new ActionForm();
+ $form->setOnSuccess(function (ActionForm $form) {
+ $moduleName = $form->getValue('identifier');
+ $module = Icinga::app()->getModuleManager()
+ ->enableModule($moduleName)
+ ->getModule($moduleName);
+ Notification::success(sprintf($this->translate('Module "%s" enabled'), $moduleName));
+ $form->onSuccess();
+
+ if ($module->hasJs()) {
+ $this->getResponse()
+ ->setReloadWindow(true)
+ ->sendResponse();
+ } else {
+ if ($module->hasCss()) {
+ $this->reloadCss();
+ }
+
+ $this->rerenderLayout()->redirectNow('config/modules');
+ }
+ });
+
+ try {
+ $form->handleRequest();
+ } catch (Exception $e) {
+ $this->view->exceptionMessage = $e->getMessage();
+ $this->view->moduleName = $form->getValue('identifier');
+ $this->view->action = 'enable';
+ $this->render('module-configuration-error');
+ }
+ }
+
+ /**
+ * Disable a module specific module provided by the 'name' param
+ */
+ public function moduledisableAction()
+ {
+ $this->assertPermission('config/modules');
+
+ $form = new ActionForm();
+ $form->setOnSuccess(function (ActionForm $form) {
+ $mm = Icinga::app()->getModuleManager();
+ $moduleName = $form->getValue('identifier');
+ $module = $mm->getModule($moduleName);
+ $mm->disableModule($moduleName);
+ Notification::success(sprintf($this->translate('Module "%s" disabled'), $moduleName));
+ $form->onSuccess();
+
+ if ($module->hasJs()) {
+ $this->getResponse()
+ ->setReloadWindow(true)
+ ->sendResponse();
+ } else {
+ if ($module->hasCss()) {
+ $this->reloadCss();
+ }
+
+ $this->rerenderLayout()->redirectNow('config/modules');
+ }
+ });
+
+ try {
+ $form->handleRequest();
+ } catch (Exception $e) {
+ $this->view->exceptionMessage = $e->getMessage();
+ $this->view->moduleName = $form->getValue('identifier');
+ $this->view->action = 'disable';
+ $this->render('module-configuration-error');
+ }
+ }
+
+ /**
+ * Action for listing user and group backends
+ */
+ public function userbackendAction()
+ {
+ if ($this->hasPermission('config/access-control/users')) {
+ $form = new UserBackendReorderForm();
+ $form->setIniConfig(Config::app('authentication'));
+ $form->handleRequest();
+ $this->view->form = $form;
+ }
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $this->view->backendNames = Config::app('groups');
+ }
+
+ $this->createApplicationTabs()->activate('authentication');
+ $this->view->title = $this->translate('Authentication');
+ $this->render('userbackend/reorder');
+ }
+
+ /**
+ * Create a new user backend
+ */
+ public function createuserbackendAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $form = new UserBackendConfigForm();
+ $form
+ ->setRedirectUrl('config/userbackend')
+ ->addDescription($this->translate(
+ 'Create a new backend for authenticating your users. This backend'
+ . ' will be added at the end of your authentication order.'
+ ))
+ ->setIniConfig(Config::app('authentication'));
+
+ try {
+ $form->setResourceConfig(ResourceFactory::getResourceConfigs());
+ } catch (ConfigurationError $e) {
+ if ($this->hasPermission('config/resources')) {
+ Notification::error($e->getMessage());
+ $this->redirectNow('config/createresource');
+ }
+
+ throw $e; // No permission for resource configuration, show the error
+ }
+
+ $form->setOnSuccess(function (UserBackendConfigForm $form) {
+ try {
+ $form->add($form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(t('User backend successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('New User Backend'));
+ }
+
+ /**
+ * Edit a user backend
+ */
+ public function edituserbackendAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $backendName = $this->params->getRequired('backend');
+
+ $form = new UserBackendConfigForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setIniConfig(Config::app('authentication'));
+ $form->setOnSuccess(function (UserBackendConfigForm $form) use ($backendName) {
+ try {
+ $form->edit($backendName, $form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(sprintf(t('User backend "%s" successfully updated'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($backendName);
+ $form->setResourceConfig(ResourceFactory::getResourceConfigs());
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User backend "%s" not found'), $backendName));
+ }
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Update User Backend'));
+ }
+
+ /**
+ * Display a confirmation form to remove the backend identified by the 'backend' parameter
+ */
+ public function removeuserbackendAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $backendName = $this->params->getRequired('backend');
+
+ $backendForm = new UserBackendConfigForm();
+ $backendForm->setIniConfig(Config::app('authentication'));
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) {
+ try {
+ $backendForm->delete($backendName);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($backendForm->save()) {
+ Notification::success(sprintf(t('User backend "%s" successfully removed'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Remove User Backend'));
+ }
+
+ /**
+ * Display all available resources and a link to create a new one and to remove existing ones
+ */
+ public function resourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->view->resources = Config::app('resources', true)->getConfigObject()
+ ->setKeyColumn('name')
+ ->select()
+ ->order('name');
+ $this->view->title = $this->translate('Resources');
+ $this->createApplicationTabs()->activate('resource');
+ }
+
+ /**
+ * Display a form to create a new resource
+ */
+ public function createresourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->getTabs()->add('resources/new', array(
+ 'label' => $this->translate('New Resource'),
+ 'url' => Url::fromRequest()
+ ))->activate('resources/new');
+ $form = new ResourceConfigForm();
+ $form->addDescription($this->translate('Resources are entities that provide data to Icinga Web 2.'));
+ $form->setIniConfig(Config::app('resources'));
+ $form->setRedirectUrl('config/resource');
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Resources');
+ $this->render('resource/create');
+ }
+
+ /**
+ * Display a form to edit a existing resource
+ */
+ public function editresourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->getTabs()->add('resources/update', array(
+ 'label' => $this->translate('Update Resource'),
+ 'url' => Url::fromRequest()
+ ))->activate('resources/update');
+ $form = new ResourceConfigForm();
+ $form->setIniConfig(Config::app('resources'));
+ $form->setRedirectUrl('config/resource');
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Resources');
+ $this->render('resource/modify');
+ }
+
+ /**
+ * Display a confirmation form to remove a resource
+ */
+ public function removeresourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->getTabs()->add('resources/remove', array(
+ 'label' => $this->translate('Remove Resource'),
+ 'url' => Url::fromRequest()
+ ))->activate('resources/remove');
+ $form = new ConfirmRemovalForm(array(
+ 'onSuccess' => function ($form) {
+ $configForm = new ResourceConfigForm();
+ $configForm->setIniConfig(Config::app('resources'));
+ $resource = $form->getRequest()->getQuery('resource');
+
+ try {
+ $configForm->remove($resource);
+ } catch (InvalidArgumentException $e) {
+ Notification::error($e->getMessage());
+ return false;
+ }
+
+ if ($configForm->save()) {
+ Notification::success(sprintf(t('Resource "%s" has been successfully removed'), $resource));
+ } else {
+ return false;
+ }
+ }
+ ));
+ $form->setRedirectUrl('config/resource');
+ $form->handleRequest();
+
+ // Check if selected resource is currently used for authentication
+ $resource = $this->getRequest()->getQuery('resource');
+ $authConfig = Config::app('authentication');
+ foreach ($authConfig as $backendName => $config) {
+ if ($config->get('resource') === $resource) {
+ $form->warning(sprintf(
+ $this->translate(
+ 'The resource "%s" is currently utilized for authentication by user backend "%s".'
+ . ' Removing the resource can result in noone being able to log in any longer.'
+ ),
+ $resource,
+ $backendName
+ ));
+ }
+ }
+
+ // Check if selected resource is currently used as user preferences backend
+ if (Config::app()->get('global', 'config_resource') === $resource) {
+ $form->warning(sprintf(
+ $this->translate(
+ 'The resource "%s" is currently utilized to store user preferences. Removing the'
+ . ' resource causes all current user preferences not being available any longer.'
+ ),
+ $resource
+ ));
+ }
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Resources');
+ $this->render('resource/remove');
+ }
+}
diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php
new file mode 100644
index 0000000..ff2580c
--- /dev/null
+++ b/application/controllers/DashboardController.php
@@ -0,0 +1,346 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Zend_Controller_Action_Exception;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\Http\HttpNotFoundException;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Forms\Dashboard\DashletForm;
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Dashboard;
+use Icinga\Web\Widget\Tabextension\DashboardSettings;
+
+/**
+ * Handle creation, removal and displaying of dashboards, panes and dashlets
+ *
+ * @see Icinga\Web\Widget\Dashboard for more information about dashboards
+ */
+class DashboardController extends ActionController
+{
+ /**
+ * @var Dashboard;
+ */
+ private $dashboard;
+
+ public function init()
+ {
+ $this->dashboard = new Dashboard();
+ $this->dashboard->setUser($this->Auth()->getUser());
+ $this->dashboard->load();
+ }
+
+ public function newDashletAction()
+ {
+ $form = new DashletForm();
+ $this->getTabs()->add('new-dashlet', array(
+ 'active' => true,
+ 'label' => $this->translate('New Dashlet'),
+ 'url' => Url::fromRequest()
+ ));
+ $dashboard = $this->dashboard;
+ $form->setDashboard($dashboard);
+ if ($this->_request->getParam('url')) {
+ $params = $this->_request->getParams();
+ $params['url'] = rawurldecode($this->_request->getParam('url'));
+ $form->populate($params);
+ }
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $action) {
+ try {
+ $pane = $dashboard->getPane($form->getValue('pane'));
+ } catch (ProgrammingError $e) {
+ $pane = new Dashboard\Pane($form->getValue('pane'));
+ $pane->setUserWidget();
+ $dashboard->addPane($pane);
+ }
+ $dashlet = new Dashboard\Dashlet($form->getValue('dashlet'), $form->getValue('url'), $pane);
+ $dashlet->setUserWidget();
+ $pane->addDashlet($dashlet);
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ Notification::success(t('Dashlet created'));
+ return true;
+ });
+ $form->setTitle($this->translate('Add Dashlet To Dashboard'));
+ $form->setRedirectUrl('dashboard');
+ $form->handleRequest();
+ $this->view->form = $form;
+ }
+
+ public function updateDashletAction()
+ {
+ $this->getTabs()->add('update-dashlet', array(
+ 'active' => true,
+ 'label' => $this->translate('Update Dashlet'),
+ 'url' => Url::fromRequest()
+ ));
+ $dashboard = $this->dashboard;
+ $form = new DashletForm();
+ $form->setDashboard($dashboard);
+ $form->setSubmitLabel($this->translate('Update Dashlet'));
+ if (! $this->_request->getParam('pane')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "pane"',
+ 400
+ );
+ }
+ if (! $this->_request->getParam('dashlet')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "dashlet"',
+ 400
+ );
+ }
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $action) {
+ try {
+ $pane = $dashboard->getPane($form->getValue('org_pane'));
+ $pane->setTitle($form->getValue('pane'));
+ } catch (ProgrammingError $e) {
+ $pane = new Dashboard\Pane($form->getValue('pane'));
+ $pane->setUserWidget();
+ $dashboard->addPane($pane);
+ }
+ try {
+ $dashlet = $pane->getDashlet($form->getValue('org_dashlet'));
+ $dashlet->setTitle($form->getValue('dashlet'));
+ $dashlet->setUrl($form->getValue('url'));
+ } catch (ProgrammingError $e) {
+ $dashlet = new Dashboard\Dashlet($form->getValue('dashlet'), $form->getValue('url'), $pane);
+ $pane->addDashlet($dashlet);
+ }
+ $dashlet->setUserWidget();
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ Notification::success(t('Dashlet updated'));
+ return true;
+ });
+ $form->setTitle($this->translate('Edit Dashlet'));
+ $form->setRedirectUrl('dashboard/settings');
+ $form->handleRequest();
+ $pane = $dashboard->getPane($this->getParam('pane'));
+ $dashlet = $pane->getDashlet($this->getParam('dashlet'));
+ $form->load($dashlet);
+
+ $this->view->form = $form;
+ }
+
+ public function removeDashletAction()
+ {
+ $form = new ConfirmRemovalForm();
+ $this->getTabs()->add('remove-dashlet', array(
+ 'active' => true,
+ 'label' => $this->translate('Remove Dashlet'),
+ 'url' => Url::fromRequest()
+ ));
+ $dashboard = $this->dashboard;
+ if (! $this->_request->getParam('pane')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "pane"',
+ 400
+ );
+ }
+ if (! $this->_request->getParam('dashlet')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "dashlet"',
+ 400
+ );
+ }
+ $pane = $this->_request->getParam('pane');
+ $dashlet = $this->_request->getParam('dashlet');
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $dashlet, $pane, $action) {
+ $pane = $dashboard->getPane($pane);
+ $pane->removeDashlet($dashlet);
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ Notification::success(t('Dashlet has been removed from') . ' ' . $pane->getTitle());
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ return true;
+ });
+ $form->setTitle($this->translate('Remove Dashlet From Dashboard'));
+ $form->setRedirectUrl('dashboard/settings');
+ $form->handleRequest();
+ $this->view->pane = $pane;
+ $this->view->dashlet = $dashlet;
+ $this->view->form = $form;
+ }
+
+ public function renamePaneAction()
+ {
+ $paneName = $this->params->getRequired('pane');
+ if (! $this->dashboard->hasPane($paneName)) {
+ throw new HttpNotFoundException('Pane not found');
+ }
+
+ $form = new Form();
+ $form->setRedirectUrl('dashboard/settings');
+ $form->setSubmitLabel($this->translate('Update Pane'));
+ $form->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Name')
+ )
+ );
+ $form->addElement(
+ 'text',
+ 'title',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Title')
+ )
+ );
+ $form->setDefaults(array(
+ 'name' => $paneName,
+ 'title' => $this->dashboard->getPane($paneName)->getTitle()
+ ));
+ $form->setOnSuccess(function ($form) use ($paneName) {
+ $newName = $form->getValue('name');
+ $newTitle = $form->getValue('title');
+
+ $pane = $this->dashboard->getPane($paneName);
+ $pane->setName($newName);
+ $pane->setTitle($newTitle);
+ $this->dashboard->getConfig()->saveIni();
+
+ Notification::success(
+ sprintf($this->translate('Pane "%s" successfully renamed to "%s"'), $paneName, $newName)
+ );
+ });
+
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->getTabs()->add(
+ 'update-pane',
+ array(
+ 'title' => $this->translate('Update Pane'),
+ 'url' => $this->getRequest()->getUrl()
+ )
+ )->activate('update-pane');
+ }
+
+ public function removePaneAction()
+ {
+ $form = new ConfirmRemovalForm();
+ $this->createTabs();
+ $dashboard = $this->dashboard;
+ if (! $this->_request->getParam('pane')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "pane"',
+ 400
+ );
+ }
+ $pane = $this->_request->getParam('pane');
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $pane, $action) {
+ $pane = $dashboard->getPane($pane);
+ $dashboard->removePane($pane->getName());
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ Notification::success(t('Dashboard has been removed') . ': ' . $pane->getTitle());
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ return true;
+ });
+ $form->setTitle($this->translate('Remove Dashboard'));
+ $form->setRedirectUrl('dashboard/settings');
+ $form->handleRequest();
+ $this->view->pane = $pane;
+ $this->view->form = $form;
+ }
+
+ /**
+ * Display the dashboard with the pane set in the 'pane' request parameter
+ *
+ * If no pane is submitted or the submitted one doesn't exist, the default pane is
+ * displayed (normally the first one)
+ */
+ public function indexAction()
+ {
+ $this->createTabs();
+ if (! $this->dashboard->hasPanes()) {
+ $this->view->title = 'Dashboard';
+ } else {
+ $panes = array_filter(
+ $this->dashboard->getPanes(),
+ function ($pane) {
+ return ! $pane->getDisabled();
+ }
+ );
+ if (empty($panes)) {
+ $this->view->title = 'Dashboard';
+ $this->getTabs()->add('dashboard', array(
+ 'active' => true,
+ 'title' => $this->translate('Dashboard'),
+ 'url' => Url::fromRequest()
+ ));
+ } else {
+ if ($this->_getParam('pane')) {
+ $pane = $this->_getParam('pane');
+ $this->dashboard->activate($pane);
+ }
+ if ($this->dashboard === null) {
+ $this->view->title = 'Dashboard';
+ } else {
+ $this->view->title = $this->dashboard->getActivePane()->getTitle() . ' :: Dashboard';
+ if ($this->hasParam('remove')) {
+ $this->dashboard->getActivePane()->removeDashlet($this->getParam('remove'));
+ $this->dashboard->getConfig()->saveIni();
+ $this->redirectNow(URL::fromRequest()->remove('remove'));
+ }
+ $this->view->dashboard = $this->dashboard;
+ }
+ }
+ }
+ }
+
+ /**
+ * Setting dialog
+ */
+ public function settingsAction()
+ {
+ $this->createTabs();
+ $this->view->dashboard = $this->dashboard;
+ }
+
+ /**
+ * Create tab aggregation
+ */
+ private function createTabs()
+ {
+ $this->view->tabs = $this->dashboard->getTabs()->extend(new DashboardSettings());
+ }
+}
diff --git a/application/controllers/ErrorController.php b/application/controllers/ErrorController.php
new file mode 100644
index 0000000..476b71f
--- /dev/null
+++ b/application/controllers/ErrorController.php
@@ -0,0 +1,176 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Application\MigrationManager;
+use Icinga\Exception\IcingaException;
+use Zend_Controller_Plugin_ErrorHandler;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Exception\Http\HttpExceptionInterface;
+use Icinga\Exception\MissingParameterException;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Url;
+
+/**
+ * Application wide controller for displaying exceptions
+ */
+class ErrorController extends ActionController
+{
+ /**
+ * Regular expression to match exceptions resulting from missing functions/classes
+ */
+ const MISSING_DEP_ERROR =
+ "/Uncaught Error:.*(?:undefined function (\S+)|Class ['\"]([^']+)['\"] not found).* in ([^:]+)/";
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->rerenderLayout = $this->params->has('renderLayout');
+ }
+
+ /**
+ * Display exception
+ */
+ public function errorAction()
+ {
+ $error = $this->_getParam('error_handler');
+ $exception = $error->exception;
+ /** @var \Exception $exception */
+
+ if (! ($isAuthenticated = $this->Auth()->isAuthenticated())) {
+ $this->innerLayout = 'guest-error';
+ }
+
+ $modules = Icinga::app()->getModuleManager();
+ $sourcePath = ltrim($this->_request->get('PATH_INFO'), '/');
+ $pathParts = preg_split('~/~', $sourcePath);
+ $moduleName = array_shift($pathParts);
+
+ $module = null;
+ switch ($error->type) {
+ case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
+ case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
+ case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
+ $this->getResponse()->setHttpResponseCode(404);
+ $this->view->messages = array($this->translate('Page not found.'));
+ if ($isAuthenticated) {
+ if ($modules->hasInstalled($moduleName) && ! $modules->hasEnabled($moduleName)) {
+ $this->view->messages[0] .= ' ' . sprintf(
+ $this->translate('Enabling the "%s" module might help!'),
+ $moduleName
+ );
+ }
+ }
+
+ break;
+ default:
+ switch (true) {
+ case $exception instanceof HttpExceptionInterface:
+ $this->getResponse()->setHttpResponseCode($exception->getStatusCode());
+ foreach ($exception->getHeaders() as $name => $value) {
+ $this->getResponse()->setHeader($name, $value, true);
+ }
+ break;
+ case $exception instanceof MissingParameterException:
+ $this->getResponse()->setHttpResponseCode(400);
+ $this->getResponse()->setHeader(
+ 'X-Status-Reason',
+ 'Missing parameter ' . $exception->getParameter()
+ );
+ break;
+ case $exception instanceof SecurityException:
+ $this->getResponse()->setHttpResponseCode(403);
+ break;
+ default:
+ $mm = MigrationManager::instance();
+ $action = $this->getRequest()->getActionName();
+ $controller = $this->getRequest()->getControllerName();
+ if ($action !== 'hint' && $controller !== 'migrations' && $mm->hasMigrations($moduleName)) {
+ // The view renderer from IPL web doesn't render the HTML content set in the respective
+ // controller if the error_handler request param is set, as it doesn't support error
+ // rendering. Since this error handler isn't caused by the migrations controller, we can
+ // safely unset this.
+ $this->setParam('error_handler', null);
+ $this->forward('hint', 'migrations', 'default', [
+ DbMigrationHook::MIGRATION_PARAM => $moduleName
+ ]);
+
+ return;
+ }
+
+ $this->getResponse()->setHttpResponseCode(500);
+ $module = $modules->hasLoaded($moduleName) ? $modules->getModule($moduleName) : null;
+ Logger::error("%s\n%s", $exception, IcingaException::getConfidentialTraceAsString($exception));
+ break;
+ }
+
+ // Try to narrow down why the request has failed
+ if (preg_match(self::MISSING_DEP_ERROR, $exception->getMessage(), $match)) {
+ $sourcePath = $match[3];
+ foreach ($modules->listLoadedModules() as $name) {
+ $candidate = $modules->getModule($name);
+ $modulePath = $candidate->getBaseDir();
+ if (substr($sourcePath, 0, strlen($modulePath)) === $modulePath) {
+ $module = $candidate;
+ break;
+ }
+ }
+
+ if (preg_match('/^(?:Icinga\\\Module\\\(\w+)|(\w+)\\\(\w+))/', $match[1] ?: $match[2], $natch)) {
+ $this->view->requiredModule = isset($natch[1]) ? strtolower($natch[1]) : null;
+ $this->view->requiredVendor = isset($natch[2]) ? $natch[2] : null;
+ $this->view->requiredProject = isset($natch[3]) ? $natch[3] : null;
+ }
+ }
+
+ $this->view->messages = array();
+
+ if ($this->getInvokeArg('displayExceptions')) {
+ $this->view->stackTraces = array();
+
+ do {
+ $this->view->messages[] = $exception->getMessage();
+ $this->view->stackTraces[] = IcingaException::getConfidentialTraceAsString($exception);
+ $exception = $exception->getPrevious();
+ } while ($exception !== null);
+ } else {
+ do {
+ $this->view->messages[] = $exception->getMessage();
+ $exception = $exception->getPrevious();
+ } while ($exception !== null);
+ }
+
+ break;
+ }
+
+ if ($this->getRequest()->isApiRequest()) {
+ $this->getResponse()->json()
+ ->setErrorMessage($this->view->messages[0])
+ ->sendResponse();
+ }
+
+ $this->view->module = $module;
+ $this->view->request = $error->request;
+ if (! $isAuthenticated) {
+ $this->view->hideControls = true;
+ } else {
+ $this->view->hideControls = false;
+ $this->getTabs()->add('error', array(
+ 'active' => true,
+ 'label' => $this->translate('Error'),
+ 'url' => Url::fromRequest()
+ ));
+ }
+ }
+}
diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php
new file mode 100644
index 0000000..d18397c
--- /dev/null
+++ b/application/controllers/GroupController.php
@@ -0,0 +1,418 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Authentication\User\DomainAwareInterface;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Reducible;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Config\UserGroup\AddMemberForm;
+use Icinga\Forms\Config\UserGroup\UserGroupForm;
+use Icinga\User;
+use Icinga\Web\Controller\AuthBackendController;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget;
+
+class GroupController extends AuthBackendController
+{
+ public function init()
+ {
+ $this->view->title = $this->translate('User Groups');
+
+ parent::init();
+ }
+
+ /**
+ * List all user groups of a single backend
+ */
+ public function listAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $this->createListTabs()->activate('group/list');
+ $backendNames = array_map(
+ function ($b) {
+ return $b->getName();
+ },
+ $this->loadUserGroupBackends('Icinga\Data\Selectable')
+ );
+ if (empty($backendNames)) {
+ return;
+ }
+
+ $this->view->backendSelection = new Form();
+ $this->view->backendSelection->setAttrib('class', 'backend-selection icinga-controls');
+ $this->view->backendSelection->setUidDisabled();
+ $this->view->backendSelection->setMethod('GET');
+ $this->view->backendSelection->setTokenDisabled();
+ $this->view->backendSelection->addElement(
+ 'select',
+ 'backend',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Group Backend'),
+ 'multiOptions' => array_combine($backendNames, $backendNames),
+ 'value' => $this->params->get('backend')
+ )
+ );
+
+ $backend = $this->getUserGroupBackend($this->params->get('backend'));
+ if ($backend === null) {
+ $this->view->backend = null;
+ return;
+ }
+
+ $query = $backend->select(array('group_name'));
+
+ $this->view->groups = $query;
+ $this->view->backend = $backend;
+
+ $this->setupPaginationControl($query);
+ $this->setupFilterControl($query);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'group_name' => $this->translate('User Group'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Show a group
+ */
+ public function showAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'));
+
+ $group = $backend->select(array(
+ 'group_name',
+ 'created_at',
+ 'last_modified'
+ ))->where('group_name', $groupName)->fetchRow();
+ if ($group === false) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $members = $backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $groupName);
+
+ $this->setupFilterControl($members, null, array('user'), array('group'));
+ $this->setupPaginationControl($members);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'user_name' => $this->translate('Username'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $members
+ );
+
+ $this->view->group = $group;
+ $this->view->backend = $backend;
+ $this->view->members = $members;
+ $this->createShowTabs($backend->getName(), $groupName)->activate('group/show');
+
+ if ($this->hasPermission('config/access-control/groups') && $backend instanceof Reducible) {
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAttrib('class', 'inline');
+ $removeForm->setAction(
+ Url::fromPath('group/removemember', array('backend' => $backend->getName(), 'group' => $groupName))
+ );
+ $removeForm->addElement('hidden', 'user_name', array(
+ 'isArray' => true,
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('group/show', array(
+ 'backend' => $backend->getName(),
+ 'group' => $groupName
+ )),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Remove this member')
+ ));
+ $this->view->removeForm = $removeForm;
+ }
+ }
+
+ /**
+ * Add a group
+ */
+ public function addAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+ $form->add()->handleRequest();
+
+ $this->renderForm($form, $this->translate('New User Group'));
+ }
+
+ /**
+ * Edit a group
+ */
+ public function editAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
+
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(
+ Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
+ );
+ $form->setRepository($backend);
+
+ try {
+ $form->edit($groupName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->renderForm($form, $this->translate('Update User Group'));
+ }
+
+ /**
+ * Remove a group
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+
+ try {
+ $form->remove($groupName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->renderForm($form, $this->translate('Remove User Group'));
+ }
+
+ /**
+ * Add a group member
+ */
+ public function addmemberAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+
+ $form = new AddMemberForm();
+ $form->setDataSource($this->fetchUsers())
+ ->setBackend($backend)
+ ->setGroupName($groupName)
+ ->setRedirectUrl(
+ Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
+ )
+ ->setUidDisabled();
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->renderForm($form, $this->translate('New User Group Member'));
+ }
+
+ /**
+ * Remove a group member
+ */
+ public function removememberAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $this->assertHttpMethod('POST');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new Form(array(
+ 'onSuccess' => function ($form) use ($groupName, $backend) {
+ foreach ($form->getValue('user_name') as $userName) {
+ try {
+ $backend->delete(
+ 'group_membership',
+ Filter::matchAll(
+ Filter::where('group_name', $groupName),
+ Filter::where('user_name', $userName)
+ )
+ );
+ Notification::success(sprintf(
+ t('User "%s" has been removed from group "%s"'),
+ $userName,
+ $groupName
+ ));
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ Notification::error($e->getMessage());
+ }
+ }
+
+ $redirect = $form->getValue('redirect');
+ if (! empty($redirect)) {
+ $form->setRedirectUrl(htmlspecialchars_decode($redirect));
+ }
+
+ return true;
+ }
+ ));
+ $form->setUidDisabled();
+ $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called
+ $form->addElement('hidden', 'user_name', array('required' => true, 'isArray' => true));
+ $form->addElement('hidden', 'redirect');
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+ }
+
+ /**
+ * Fetch and return all users from all user backends
+ *
+ * @return ArrayDatasource
+ */
+ protected function fetchUsers()
+ {
+ $users = array();
+ foreach ($this->loadUserBackends('Icinga\Data\Selectable') as $backend) {
+ try {
+ if ($backend instanceof DomainAwareInterface) {
+ $domain = $backend->getDomain();
+ } else {
+ $domain = null;
+ }
+ foreach ($backend->select(array('user_name')) as $user) {
+ $userObj = new User($user->user_name);
+ if ($domain !== null) {
+ if ($userObj->hasDomain() && $userObj->getDomain() !== $domain) {
+ // Users listed in a user backend which is configured to be responsible for a domain should
+ // not have a domain in their username. Ultimately, if the username has a domain, it must
+ // not differ from the backend's domain. We could log here - but hey, who cares :)
+ continue;
+ } else {
+ $userObj->setDomain($domain);
+ }
+ }
+
+ $user->user_name = $userObj->getUsername();
+
+ $users[] = $user;
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any users from backend %s. Please check your log'),
+ $backend->getName()
+ ));
+ }
+ }
+
+ return new ArrayDatasource($users);
+ }
+
+ /**
+ * Create the tabs to display when showing a group
+ *
+ * @param string $backendName
+ * @param string $groupName
+ */
+ protected function createShowTabs($backendName, $groupName)
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'group/show',
+ array(
+ 'title' => sprintf($this->translate('Show group %s'), $groupName),
+ 'label' => $this->translate('Group'),
+ 'url' => Url::fromPath('group/show', array('backend' => $backendName, 'group' => $groupName))
+ )
+ );
+
+ return $tabs;
+ }
+
+ /**
+ * Create the tabs to display when listing groups
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+
+ if ($this->hasPermission('config/access-control/roles')) {
+ $tabs->add(
+ 'role/list',
+ array(
+ 'baseTarget' => '_main',
+ 'label' => $this->translate('Roles'),
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'url' => 'role/list'
+ )
+ );
+
+ $tabs->add(
+ 'role/audit',
+ [
+ 'title' => $this->translate('Audit a user\'s or group\'s privileges'),
+ 'label' => $this->translate('Audit'),
+ 'url' => 'role/audit',
+ 'baseTarget' => '_main',
+ ]
+ );
+ }
+
+ if ($this->hasPermission('config/access-control/users')) {
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'user/list'
+ )
+ );
+ }
+
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'group/list'
+ )
+ );
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/HealthController.php b/application/controllers/HealthController.php
new file mode 100644
index 0000000..f176e69
--- /dev/null
+++ b/application/controllers/HealthController.php
@@ -0,0 +1,65 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\HealthHook;
+use Icinga\Web\View\AppHealth;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use ipl\Web\Compat\CompatController;
+
+class HealthController extends CompatController
+{
+ public function indexAction()
+ {
+ $query = HealthHook::collectHealthData()
+ ->select();
+
+ $this->setupSortControl(
+ [
+ 'module' => $this->translate('Module'),
+ 'name' => $this->translate('Name'),
+ 'state' => $this->translate('State')
+ ],
+ $query,
+ ['state' => 'desc']
+ );
+ $this->setupLimitControl();
+ $this->setupPaginationControl($query);
+ $this->setupFilterControl($query, [
+ 'module' => $this->translate('Module'),
+ 'name' => $this->translate('Name'),
+ 'state' => $this->translate('State'),
+ 'message' => $this->translate('Message')
+ ], ['name'], ['format']);
+
+ $this->getTabs()->extend(new OutputFormat(['csv']));
+ $this->handleFormatRequest($query);
+
+ $this->addControl(HtmlString::create((string) $this->view->paginator));
+ $this->addControl(Html::tag('div', ['class' => 'sort-controls-container'], [
+ HtmlString::create((string) $this->view->limiter),
+ HtmlString::create((string) $this->view->sortBox)
+ ]));
+ $this->addControl(HtmlString::create((string) $this->view->filterEditor));
+
+ $this->addTitleTab(t('Health'));
+ $this->setAutorefreshInterval(10);
+ $this->addContent(new AppHealth($query));
+ }
+
+ protected function handleFormatRequest($query)
+ {
+ $formatJson = $this->params->get('format') === 'json';
+ if (! $formatJson && ! $this->getRequest()->isApiRequest()) {
+ return;
+ }
+
+ $this->getResponse()
+ ->json()
+ ->setSuccessData($query->fetchAll())
+ ->sendResponse();
+ }
+}
diff --git a/application/controllers/IframeController.php b/application/controllers/IframeController.php
new file mode 100644
index 0000000..8aebba4
--- /dev/null
+++ b/application/controllers/IframeController.php
@@ -0,0 +1,20 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller;
+
+/**
+ * Display external or internal links within an iframe
+ */
+class IframeController extends Controller
+{
+ /**
+ * Display iframe w/ the given URL
+ */
+ public function indexAction()
+ {
+ $this->view->url = $this->params->getRequired('url');
+ }
+}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
new file mode 100644
index 0000000..539c16b
--- /dev/null
+++ b/application/controllers/IndexController.php
@@ -0,0 +1,36 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Url;
+
+/**
+ * Application wide index controller
+ */
+class IndexController extends ActionController
+{
+ /**
+ * Use a default redirection rule to welcome page
+ */
+ public function preDispatch()
+ {
+ if ($this->getRequest()->getActionName() !== 'welcome') {
+ $landingPage = getenv('ICINGAWEB_LANDING_PAGE');
+ if (! $landingPage) {
+ $landingPage = 'dashboard';
+ }
+
+ // @TODO(el): Avoid landing page redirects: https://dev.icinga.com/issues/9656
+ $this->redirectNow(Url::fromRequest()->setPath($landingPage));
+ }
+ }
+
+ /**
+ * Application's start page
+ */
+ public function welcomeAction()
+ {
+ }
+}
diff --git a/application/controllers/LayoutController.php b/application/controllers/LayoutController.php
new file mode 100644
index 0000000..237681c
--- /dev/null
+++ b/application/controllers/LayoutController.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Menu;
+
+/**
+ * Create complex layout parts
+ */
+class LayoutController extends ActionController
+{
+ /**
+ * Render the menu
+ */
+ public function menuAction()
+ {
+ $this->setAutorefreshInterval(15);
+ $this->_helper->layout()->disableLayout();
+ $this->view->menuRenderer = (new Menu())->getRenderer();
+ }
+
+ public function announcementsAction()
+ {
+ $this->_helper->layout()->disableLayout();
+ }
+}
diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php
new file mode 100644
index 0000000..2fbc5a9
--- /dev/null
+++ b/application/controllers/ListController.php
@@ -0,0 +1,59 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\Protocol\File\FileReader;
+use Icinga\Web\Controller;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+
+/**
+ * Application wide controller for various listing actions
+ */
+class ListController extends Controller
+{
+ /**
+ * Add title tab
+ *
+ * @param string $action
+ */
+ protected function addTitleTab($action)
+ {
+ $this->getTabs()->add($action, array(
+ 'label' => ucfirst($action),
+ 'url' => Url::fromPath('list/' . str_replace(' ', '', $action))
+ ))->extend(new OutputFormat())->extend(new DashboardAction())->extend(new MenuAction())->activate($action);
+ }
+
+ /**
+ * Display the application log
+ */
+ public function applicationlogAction()
+ {
+ $this->assertPermission('application/log');
+
+ if (! Logger::writesToFile()) {
+ $this->httpNotFound('Page not found');
+ }
+
+ $this->addTitleTab('application log');
+
+ $resource = new FileReader(new ConfigObject(array(
+ 'filename' => Config::app()->get('logging', 'file'),
+ 'fields' => '/(?<!.)(?<datetime>[0-9]{4}(?:-[0-9]{2}){2}' // date
+ . 'T[0-9]{2}(?::[0-9]{2}){2}(?:[\+\-][0-9]{2}:[0-9]{2})?)' // time
+ . ' - (?<loglevel>[A-Za-z]+) - (?<message>.*)(?!.)/msS' // loglevel, message
+ )));
+ $this->view->logData = $resource->select()->order('DESC');
+
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->logData);
+ $this->view->title = $this->translate('Application Log');
+ }
+}
diff --git a/application/controllers/ManageUserDevicesController.php b/application/controllers/ManageUserDevicesController.php
new file mode 100644
index 0000000..db054d1
--- /dev/null
+++ b/application/controllers/ManageUserDevicesController.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Common\Database;
+use Icinga\Web\Notification;
+use Icinga\Web\RememberMe;
+use Icinga\Web\RememberMeUserList;
+use Icinga\Web\RememberMeUserDevicesList;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Url;
+
+/**
+ * ManageUserDevicesController
+ *
+ * you need 'application/sessions' permission to use this controller
+ */
+class ManageUserDevicesController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ $this->assertPermission('application/sessions');
+ }
+
+ public function indexAction()
+ {
+ $this->getTabs()
+ ->add(
+ 'manage-user-devices',
+ array(
+ 'title' => $this->translate('List of users who stay logged in'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'manage-user-devices',
+ 'data-base-target' => '_self'
+ )
+ )->activate('manage-user-devices');
+
+ $usersList = (new RememberMeUserList())
+ ->setUsers(RememberMe::getAllUser())
+ ->setUrl('manage-user-devices/devices');
+
+ $this->addContent($usersList);
+
+ if (! $this->hasDb()) {
+ Notification::warning(
+ $this->translate("Users can't stay logged in without a database configuration backend")
+ );
+ }
+ }
+
+ public function devicesAction()
+ {
+ $this->getTabs()
+ ->add(
+ 'manage-devices',
+ array(
+ 'title' => $this->translate('List of devices'),
+ 'label' => $this->translate('Devices'),
+ 'url' => 'manage-user-devices/devices'
+ )
+ )->activate('manage-devices');
+
+ $name = $this->params->getRequired('name');
+ $data = (new RememberMeUserDevicesList())
+ ->setDevicesList(RememberMe::getAllByUsername($name))
+ ->setUsername($name)
+ ->setUrl('manage-user-devices/delete');
+
+ $this->addContent($data);
+ }
+
+ public function deleteAction()
+ {
+ (new RememberMe())->remove($this->params->getRequired('fingerprint'));
+
+ $this->redirectNow(
+ Url::fromPath('manage-user-devices/devices')
+ ->addParams(['name' => $this->params->getRequired('name')])
+ );
+ }
+}
diff --git a/application/controllers/MigrationsController.php b/application/controllers/MigrationsController.php
new file mode 100644
index 0000000..5229f06
--- /dev/null
+++ b/application/controllers/MigrationsController.php
@@ -0,0 +1,249 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\MigrationManager;
+use Icinga\Common\Database;
+use Icinga\Exception\MissingParameterException;
+use Icinga\Forms\MigrationForm;
+use Icinga\Web\Notification;
+use Icinga\Web\Widget\ItemList\MigrationList;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\SubmitButtonElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Widget\ActionLink;
+
+class MigrationsController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ Icinga::app()->getModuleManager()->loadModule('setup');
+ }
+
+ public function indexAction(): void
+ {
+ $mm = MigrationManager::instance();
+
+ $this->getTabs()->extend(new OutputFormat(['csv']));
+ $this->addTitleTab($this->translate('Migrations'));
+
+ $canApply = $this->hasPermission('application/migrations');
+ if (! $canApply) {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-state-banner']),
+ new HtmlElement(
+ 'span',
+ null,
+ Text::create(
+ $this->translate('You do not have the required permission to apply pending migrations.')
+ )
+ )
+ )
+ );
+ }
+
+ $migrateListForm = new MigrationForm();
+ $migrateListForm->setAttribute('id', $this->getRequest()->protectId('migration-form'));
+ $migrateListForm->setRenderDatabaseUserChange(! $mm->validateDatabasePrivileges());
+
+ if ($canApply && $mm->hasPendingMigrations()) {
+ $migrateAllButton = new SubmitButtonElement(sprintf('migrate-%s', DbMigrationHook::ALL_MIGRATIONS), [
+ 'form' => $migrateListForm->getAttribute('id')->getValue(),
+ 'label' => $this->translate('Migrate All'),
+ 'title' => $this->translate('Migrate all pending migrations')
+ ]);
+
+ // Is the first button, so will be cloned and that the visible
+ // button is outside the form doesn't matter for Web's JS
+ $migrateListForm->registerElement($migrateAllButton);
+
+ // Make sure it looks familiar, even if not inside a form
+ $migrateAllButton->setWrapper(new HtmlElement('div', Attributes::create(['class' => 'icinga-controls'])));
+
+ $this->controls->getAttributes()->add('class', 'default-layout');
+ $this->addControl($migrateAllButton);
+ }
+
+ $this->handleFormatRequest($mm->toArray());
+
+ $frameworkList = new MigrationList($mm->yieldMigrations(), $migrateListForm);
+ $frameworkListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
+ $frameworkListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('System'))));
+ $frameworkListControl->addHtml($frameworkList);
+
+ $moduleList = new MigrationList($mm->yieldMigrations(true), $migrateListForm);
+ $moduleListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
+ $moduleListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('Modules'))));
+ $moduleListControl->addHtml($moduleList);
+
+ $migrateListForm->addHtml($frameworkListControl, $moduleListControl);
+ if ($canApply && $mm->hasPendingMigrations()) {
+ $frameworkList->ensureAssembled();
+ $moduleList->ensureAssembled();
+
+ $this->handleMigrateRequest($migrateListForm);
+ }
+
+ $migrations = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
+ $migrations->addHtml($migrateListForm);
+
+ $this->addContent($migrations);
+ }
+
+ public function hintAction(): void
+ {
+ // The forwarded request doesn't modify the original server query string, but adds the migration param to the
+ // request param instead. So, there is no way to access the migration param other than via the request instance.
+ /** @var ?string $module */
+ $module = $this->getRequest()->getParam(DbMigrationHook::MIGRATION_PARAM);
+ if ($module === null) {
+ throw new MissingParameterException(
+ $this->translate('Required parameter \'%s\' missing'),
+ DbMigrationHook::MIGRATION_PARAM
+ );
+ }
+
+ $mm = MigrationManager::instance();
+ if (! $mm->hasMigrations($module)) {
+ $this->httpNotFound(sprintf('There are no pending migrations matching the given name: %s', $module));
+ }
+
+ $migration = $mm->getMigration($module);
+ $this->addTitleTab($this->translate('Error'));
+ $this->addContent(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'pending-migrations-hint']),
+ new HtmlElement('h2', null, Text::create($this->translate('Error!'))),
+ new HtmlElement(
+ 'p',
+ null,
+ Text::create(sprintf($this->translate('%s has pending migrations.'), $migration->getName()))
+ ),
+ new HtmlElement('p', null, Text::create($this->translate('Please apply the migrations first.'))),
+ new ActionLink($this->translate('View pending Migrations'), 'migrations')
+ )
+ );
+ }
+
+ public function migrationAction(): void
+ {
+ /** @var string $name */
+ $name = $this->params->getRequired(DbMigrationHook::MIGRATION_PARAM);
+
+ $this->addTitleTab($this->translate('Migration'));
+ $this->getTabs()->disableLegacyExtensions();
+ $this->controls->getAttributes()->add('class', 'default-layout');
+
+ $mm = MigrationManager::instance();
+ if (! $mm->hasMigrations($name)) {
+ $migrations = [];
+ } else {
+ $hook = $mm->getMigration($name);
+ $migrations = array_reverse($hook->getMigrations());
+ if (! $this->hasPermission('application/migrations')) {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-state-banner']),
+ new HtmlElement(
+ 'span',
+ null,
+ Text::create(
+ $this->translate('You do not have the required permission to apply pending migrations.')
+ )
+ )
+ )
+ );
+ } else {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-controls']),
+ new HtmlElement('span', null, Text::create($hook->getName()))
+ )
+ );
+ }
+ }
+
+ $migrationWidget = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
+ $migrationWidget->addHtml((new MigrationList($migrations))->setMinimal(false));
+ $this->addContent($migrationWidget);
+ }
+
+ public function handleMigrateRequest(MigrationForm $form): void
+ {
+ $this->assertPermission('application/migrations');
+
+ $form->on(MigrationForm::ON_SUCCESS, function (MigrationForm $form) {
+ $mm = MigrationManager::instance();
+
+ /** @var array<string, string> $elevatedPrivileges */
+ $elevatedPrivileges = $form->getValue('database_setup');
+ if ($elevatedPrivileges !== null && $elevatedPrivileges['grant_privileges'] === 'y') {
+ $mm->fixIcingaWebMysqlGrants($this->getDb(), $elevatedPrivileges);
+ }
+
+ $pressedButton = $form->getPressedSubmitElement();
+ if ($pressedButton) {
+ $name = substr($pressedButton->getName(), 8);
+ switch ($name) {
+ case DbMigrationHook::ALL_MIGRATIONS:
+ if ($mm->applyAll($elevatedPrivileges)) {
+ Notification::success($this->translate('Applied all migrations successfully'));
+ } else {
+ Notification::error(
+ $this->translate(
+ 'Applied migrations successfully. Though, one or more migration hooks'
+ . ' failed to run. See logs for details'
+ )
+ );
+ }
+ break;
+ default:
+ $migration = $mm->getMigration($name);
+ if ($mm->apply($migration, $elevatedPrivileges)) {
+ Notification::success($this->translate('Applied pending migrations successfully'));
+ } else {
+ Notification::error(
+ $this->translate('Failed to apply pending migration(s). See logs for details')
+ );
+ }
+ }
+ }
+
+ $this->sendExtraUpdates(['#col2' => '__CLOSE__']);
+
+ $this->redirectNow('migrations');
+ })->handleRequest($this->getServerRequest());
+ }
+
+ /**
+ * Handle exports
+ *
+ * @param array<string, mixed> $data
+ */
+ protected function handleFormatRequest(array $data): void
+ {
+ $formatJson = $this->params->get('format') === 'json';
+ if (! $formatJson && ! $this->getRequest()->isApiRequest()) {
+ return;
+ }
+
+ $this->getResponse()
+ ->json()
+ ->setSuccessData($data)
+ ->sendResponse();
+ }
+}
diff --git a/application/controllers/MyDevicesController.php b/application/controllers/MyDevicesController.php
new file mode 100644
index 0000000..e0fb98a
--- /dev/null
+++ b/application/controllers/MyDevicesController.php
@@ -0,0 +1,74 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Common\Database;
+use Icinga\Web\Notification;
+use Icinga\Web\RememberMe;
+use Icinga\Web\RememberMeUserDevicesList;
+use ipl\Web\Compat\CompatController;
+
+/**
+ * MyDevicesController
+ *
+ * this controller shows you all the devices you are logged in
+ */
+class MyDevicesController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ $this->getTabs()
+ ->add(
+ 'account',
+ array(
+ 'title' => $this->translate('Update your account'),
+ 'label' => $this->translate('My Account'),
+ 'url' => 'account'
+ )
+ )
+ ->add(
+ 'navigation',
+ array(
+ 'title' => $this->translate('List and configure your own navigation items'),
+ 'label' => $this->translate('Navigation'),
+ 'url' => 'navigation'
+ )
+ )
+ ->add(
+ 'devices',
+ array(
+ 'title' => $this->translate('List of devices you are logged in'),
+ 'label' => $this->translate('My Devices'),
+ 'url' => 'my-devices'
+ )
+ )->activate('devices');
+ }
+
+ public function indexAction()
+ {
+ $name = $this->auth->getUser()->getUsername();
+
+ $data = (new RememberMeUserDevicesList())
+ ->setDevicesList(RememberMe::getAllByUsername($name))
+ ->setUsername($name)
+ ->setUrl('my-devices/delete');
+
+ $this->addContent($data);
+
+ if (! $this->hasDb()) {
+ Notification::warning(
+ $this->translate("Users can't stay logged in without a database configuration backend")
+ );
+ }
+ }
+
+ public function deleteAction()
+ {
+ (new RememberMe())->remove($this->params->getRequired('fingerprint'));
+
+ $this->redirectNow('my-devices');
+ }
+}
diff --git a/application/controllers/NavigationController.php b/application/controllers/NavigationController.php
new file mode 100644
index 0000000..b0babc3
--- /dev/null
+++ b/application/controllers/NavigationController.php
@@ -0,0 +1,447 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\FilterMatchCaseInsensitive;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Forms\Navigation\NavigationConfigForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Form;
+use Icinga\Web\Menu;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+
+/**
+ * Navigation configuration
+ */
+class NavigationController extends Controller
+{
+ /**
+ * The global navigation item type configuration
+ *
+ * @var array
+ */
+ protected $itemTypeConfig;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ parent::init();
+ $this->itemTypeConfig = Navigation::getItemTypeConfiguration();
+ }
+
+ /**
+ * Return the label for the given navigation item type
+ *
+ * @param string $type
+ *
+ * @return string $type if no label can be found
+ */
+ protected function getItemLabel($type)
+ {
+ return isset($this->itemTypeConfig[$type]['label']) ? $this->itemTypeConfig[$type]['label'] : $type;
+ }
+
+ /**
+ * Return a list of available navigation item types
+ *
+ * @return array
+ */
+ protected function listItemTypes()
+ {
+ $types = array();
+ foreach ($this->itemTypeConfig as $type => $options) {
+ $types[$type] = isset($options['label']) ? $options['label'] : $type;
+ }
+
+ return $types;
+ }
+
+ /**
+ * Return all shared navigation item configurations
+ *
+ * @param string $owner A username if only items shared by a specific user are desired
+ *
+ * @return array
+ */
+ protected function fetchSharedNavigationItemConfigs($owner = null)
+ {
+ $configs = array();
+ foreach ($this->itemTypeConfig as $type => $_) {
+ $config = Config::navigation($type);
+ $config->getConfigObject()->setKeyColumn('name');
+ $query = $config->select();
+ if ($owner !== null) {
+ $query->applyFilter(new FilterMatchCaseInsensitive('owner', '=', $owner));
+ }
+
+ foreach ($query as $itemConfig) {
+ $configs[] = $itemConfig;
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Return all user navigation item configurations
+ *
+ * @param string $username
+ *
+ * @return array
+ */
+ protected function fetchUserNavigationItemConfigs($username)
+ {
+ $configs = array();
+ foreach ($this->itemTypeConfig as $type => $_) {
+ $config = Config::navigation($type, $username);
+ $config->getConfigObject()->setKeyColumn('name');
+ foreach ($config->select() as $itemConfig) {
+ $configs[] = $itemConfig;
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Show the current user a list of his/her navigation items
+ */
+ public function indexAction()
+ {
+ $user = $this->Auth()->getUser();
+ $ds = new ArrayDatasource(array_merge(
+ $this->fetchSharedNavigationItemConfigs($user->getUsername()),
+ $this->fetchUserNavigationItemConfigs($user->getUsername())
+ ));
+ $query = $ds->select();
+
+ $this->view->types = $this->listItemTypes();
+ $this->view->items = $query;
+
+ $this->view->title = $this->translate('Navigation');
+ $this->getTabs()
+ ->add(
+ 'account',
+ array(
+ 'title' => $this->translate('Update your account'),
+ 'label' => $this->translate('My Account'),
+ 'url' => 'account'
+ )
+ )
+ ->add(
+ 'navigation',
+ array(
+ 'active' => true,
+ 'title' => $this->translate('List and configure your own navigation items'),
+ 'label' => $this->translate('Navigation'),
+ 'url' => 'navigation'
+ )
+ )
+ ->add(
+ 'devices',
+ array(
+ 'title' => $this->translate('List of devices you are logged in'),
+ 'label' => $this->translate('My Devices'),
+ 'url' => 'my-devices'
+ )
+ );
+ $this->setupSortControl(
+ array(
+ 'type' => $this->translate('Type'),
+ 'owner' => $this->translate('Shared'),
+ 'name' => $this->translate('Navigation')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * List all shared navigation items
+ */
+ public function sharedAction()
+ {
+ $this->assertPermission('config/navigation');
+ $ds = new ArrayDatasource($this->fetchSharedNavigationItemConfigs());
+ $query = $ds->select();
+
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAttrib('class', 'inline');
+ $removeForm->addElement('hidden', 'name', array(
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('navigation/shared'),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Unshare this navigation item')
+ ));
+
+ $this->view->removeForm = $removeForm;
+ $this->view->types = $this->listItemTypes();
+ $this->view->items = $query;
+
+ $this->view->title = $this->translate('Shared Navigation');
+ $this->getTabs()->add(
+ 'navigation/shared',
+ array(
+ 'title' => $this->translate('List and configure shared navigation items'),
+ 'label' => $this->translate('Shared Navigation'),
+ 'url' => 'navigation/shared'
+ )
+ )->activate('navigation/shared');
+ $this->setupSortControl(
+ array(
+ 'type' => $this->translate('Type'),
+ 'owner' => $this->translate('Owner'),
+ 'name' => $this->translate('Shared Navigation')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Add a navigation item
+ */
+ public function addAction()
+ {
+ $form = new NavigationConfigForm();
+ $form->setRedirectUrl('navigation');
+ $form->setUser($this->Auth()->getUser());
+ $form->setItemTypes($this->listItemTypes());
+ $form->addDescription($this->translate('Create a new navigation item, such as a menu entry or dashlet.'));
+
+ // TODO: Fetch all "safe" parameters from the url and populate them
+ $form->setDefaultUrl(rawurldecode($this->params->get('url', '')));
+
+ $form->setOnSuccess(function (NavigationConfigForm $form) {
+ $data = $form::transformEmptyValuesToNull($form->getValues());
+
+ try {
+ $form->add($data);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ if ($data['type'] === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(t('Navigation item successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Navigation');
+ $this->renderForm($form, $this->translate('New Navigation Item'));
+ }
+
+ /**
+ * Edit a navigation item
+ */
+ public function editAction()
+ {
+ $itemName = $this->params->getRequired('name');
+ $itemType = $this->params->getRequired('type');
+ $referrer = $this->params->get('referrer', 'index');
+
+ $user = $this->Auth()->getUser();
+ if ($user->can('config/navigation')) {
+ $itemOwner = $this->params->get('owner', $user->getUsername());
+ } else {
+ $itemOwner = $user->getUsername();
+ }
+
+ $form = new NavigationConfigForm();
+ $form->setUser($user);
+ $form->setShareConfig(Config::navigation($itemType));
+ $form->setUserConfig(Config::navigation($itemType, $itemOwner));
+ $form->setRedirectUrl($referrer === 'shared' ? 'navigation/shared' : 'navigation');
+ $form->setOnSuccess(function (NavigationConfigForm $form) use ($itemName) {
+ $data = $form::transformEmptyValuesToNull($form->getValues());
+
+ try {
+ $form->edit($itemName, $data);
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ if (isset($data['type']) && $data['type'] === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(sprintf(t('Navigation item "%s" successfully updated'), $itemName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($itemName);
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $itemName));
+ }
+
+ $this->view->title = $this->translate('Navigation');
+ $this->renderForm($form, $this->translate('Update Navigation Item'));
+ }
+
+ /**
+ * Remove a navigation item
+ */
+ public function removeAction()
+ {
+ $itemName = $this->params->getRequired('name');
+ $itemType = $this->params->getRequired('type');
+ $user = $this->Auth()->getUser();
+
+ $navigationConfigForm = new NavigationConfigForm();
+ $navigationConfigForm->setUser($user);
+ $navigationConfigForm->setShareConfig(Config::navigation($itemType));
+ $navigationConfigForm->setUserConfig(Config::navigation($itemType, $user->getUsername()));
+
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('navigation');
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($itemName, $navigationConfigForm) {
+ try {
+ $itemConfig = $navigationConfigForm->delete($itemName);
+ } catch (NotFoundError $e) {
+ Notification::success(sprintf(t('Navigation Item "%s" not found. No action required'), $itemName));
+ return true;
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($navigationConfigForm->save()) {
+ if ($itemConfig->type === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(sprintf(t('Navigation Item "%s" successfully removed'), $itemName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Navigation');
+ $this->renderForm($form, $this->translate('Remove Navigation Item'));
+ }
+
+ /**
+ * Unshare a navigation item
+ */
+ public function unshareAction()
+ {
+ $this->assertPermission('config/navigation');
+ $this->assertHttpMethod('POST');
+
+ // TODO: I'd like these being form fields
+ $itemType = $this->params->getRequired('type');
+ $itemOwner = $this->params->getRequired('owner');
+
+ $navigationConfigForm = new NavigationConfigForm();
+ $navigationConfigForm->setUser($this->Auth()->getUser());
+ $navigationConfigForm->setShareConfig(Config::navigation($itemType));
+ $navigationConfigForm->setUserConfig(Config::navigation($itemType, $itemOwner));
+
+ $form = new Form(array(
+ 'onSuccess' => function ($form) use ($navigationConfigForm) {
+ $itemName = $form->getValue('name');
+
+ try {
+ $newConfig = $navigationConfigForm->unshare($itemName);
+ if ($navigationConfigForm->save()) {
+ if ($newConfig->getSection($itemName)->type === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(sprintf(
+ t('Navigation item "%s" has been unshared'),
+ $form->getValue('name')
+ ));
+ } else {
+ // TODO: It failed obviously to write one of the configs, so we're leaving the user in
+ // a inconsistent state. Luckily, it's nothing lost but possibly duplicated...
+ Notification::error(sprintf(
+ t('Failed to unshare navigation item "%s"'),
+ $form->getValue('name')
+ ));
+ }
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ Notification::error($e->getMessage());
+ }
+
+ $redirect = $form->getValue('redirect');
+ if (! empty($redirect)) {
+ $form->setRedirectUrl(htmlspecialchars_decode($redirect));
+ }
+
+ return true;
+ }
+ ));
+ $form->setUidDisabled();
+ $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called
+ $form->addElement('hidden', 'name', array('required' => true));
+ $form->addElement('hidden', 'redirect');
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $form->getValue('name')));
+ }
+ }
+
+ public function dashboardAction()
+ {
+ $name = $this->params->getRequired('name');
+
+ $this->getTabs()->add('dashboard', array(
+ 'active' => true,
+ 'label' => ucwords($name),
+ 'url' => Url::fromRequest()
+ ));
+
+ $menu = new Menu();
+
+ $navigation = $menu->findItem($name);
+
+ if ($navigation === null) {
+ $this->httpNotFound($this->translate('Navigation not found'));
+ }
+
+ $this->view->navigation = $navigation;
+ $this->view->title = $navigation->getLabel();
+ }
+}
diff --git a/application/controllers/RoleController.php b/application/controllers/RoleController.php
new file mode 100644
index 0000000..4223d33
--- /dev/null
+++ b/application/controllers/RoleController.php
@@ -0,0 +1,392 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Authentication\AdmissionLoader;
+use Icinga\Authentication\Auth;
+use Icinga\Authentication\RolesConfig;
+use Icinga\Authentication\User\DomainAwareInterface;
+use Icinga\Data\Selectable;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Security\RoleForm;
+use Icinga\Repository\Repository;
+use Icinga\Security\SecurityException;
+use Icinga\User;
+use Icinga\Web\Controller\AuthBackendController;
+use Icinga\Web\View\PrivilegeAudit;
+use Icinga\Web\Widget\SingleValueSearchControl;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Link;
+
+/**
+ * Manage user permissions and restrictions based on roles
+ *
+ * @TODO(el): Rename to RolesController: https://dev.icinga.com/issues/10015
+ */
+class RoleController extends AuthBackendController
+{
+ public function init()
+ {
+ $this->assertPermission('config/access-control/roles');
+ $this->view->title = $this->translate('Roles');
+
+ parent::init();
+ }
+
+ public function indexAction()
+ {
+ if ($this->hasPermission('config/access-control/roles')) {
+ $this->redirectNow('role/list');
+ } elseif ($this->hasPermission('config/access-control/users')) {
+ $this->redirectNow('user/list');
+ } elseif ($this->hasPermission('config/access-control/groups')) {
+ $this->redirectNow('group/list');
+ } else {
+ throw new SecurityException('No permission to configure Icinga Web 2');
+ }
+ }
+
+ /**
+ * List roles
+ *
+ * @TODO(el): Rename to indexAction()
+ */
+ public function listAction()
+ {
+ $this->createListTabs()->activate('role/list');
+ $this->view->roles = (new RolesConfig())
+ ->select();
+
+ $sortAndFilterColumns = [
+ 'name' => $this->translate('Name'),
+ 'users' => $this->translate('Users'),
+ 'groups' => $this->translate('Groups'),
+ 'permissions' => $this->translate('Permissions')
+ ];
+
+ $this->setupFilterControl($this->view->roles, $sortAndFilterColumns, ['name']);
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->roles);
+ $this->setupSortControl($sortAndFilterColumns, $this->view->roles, ['name']);
+ }
+
+ /**
+ * Create a new role
+ *
+ * @TODO(el): Rename to newAction()
+ */
+ public function addAction()
+ {
+ $role = new RoleForm();
+ $role->setRedirectUrl('__CLOSE__');
+ $role->setRepository(new RolesConfig());
+ $role->setSubmitLabel($this->translate('Create Role'));
+ $role->add()->handleRequest();
+
+ $this->renderForm($role, $this->translate('New Role'));
+ }
+
+ /**
+ * Update a role
+ *
+ * @TODO(el): Rename to updateAction()
+ */
+ public function editAction()
+ {
+ $name = $this->params->getRequired('role');
+ $role = new RoleForm();
+ $role->setRedirectUrl('__CLOSE__');
+ $role->setRepository(new RolesConfig());
+ $role->setSubmitLabel($this->translate('Update Role'));
+ $role->edit($name);
+
+ try {
+ $role->handleRequest();
+ } catch (NotFoundError $e) {
+ $this->httpNotFound($this->translate('Role not found'));
+ }
+
+ $this->renderForm($role, $this->translate('Update Role'));
+ }
+
+ /**
+ * Remove a role
+ */
+ public function removeAction()
+ {
+ $name = $this->params->getRequired('role');
+ $role = new RoleForm();
+ $role->setRedirectUrl('__CLOSE__');
+ $role->setRepository(new RolesConfig());
+ $role->setSubmitLabel($this->translate('Remove Role'));
+ $role->remove($name);
+
+ try {
+ $role->handleRequest();
+ } catch (NotFoundError $e) {
+ $this->httpNotFound($this->translate('Role not found'));
+ }
+
+ $this->renderForm($role, $this->translate('Remove Role'));
+ }
+
+ public function auditAction()
+ {
+ $this->createListTabs()->activate('role/audit');
+ $this->view->title = t('Audit');
+
+ $roleName = $this->params->get('role');
+ $type = $this->params->has('group') ? 'group' : 'user';
+ $name = $this->params->get($type);
+
+ $backend = null;
+ if ($type === 'user') {
+ if ($name) {
+ $backend = $this->params->getRequired('backend');
+ } else {
+ $backends = $this->loadUserBackends();
+ if (! empty($backends)) {
+ $backend = array_shift($backends)->getName();
+ }
+ }
+ }
+
+ $form = new SingleValueSearchControl();
+ $form->setMetaDataNames('type', 'backend');
+ $form->populate(['q' => $name, 'q-type' => $type, 'q-backend' => $backend]);
+ $form->setInputLabel(t('Enter user or group name'));
+ $form->setSubmitLabel(t('Inspect'));
+ $form->setSuggestionUrl(Url::fromPath(
+ 'role/suggest-role-member',
+ ['_disableLayout' => true, 'showCompact' => true]
+ ));
+
+ $form->on(SingleValueSearchControl::ON_SUCCESS, function ($form) {
+ $type = $form->getValue('q-type') ?: 'user';
+ $params = [$type => $form->getValue('q')];
+
+ if ($type === 'user') {
+ $params['backend'] = $form->getValue('q-backend');
+ }
+
+ $this->redirectNow(Url::fromPath('role/audit', $params));
+ })->handleRequest(ServerRequest::fromGlobals());
+
+ $this->addControl($form);
+
+ if (! $name) {
+ $this->addContent(Html::wantHtml(t('No user or group selected.')));
+ return;
+ }
+
+ if ($type === 'user') {
+ $header = Html::tag('h2', sprintf(t('Privilege Audit for User "%s"'), $name));
+
+ $user = new User($name);
+ $user->setAdditional('backend_name', $backend);
+ Auth::getInstance()->setupUser($user);
+ } else {
+ $header = Html::tag('h2', sprintf(t('Privilege Audit for Group "%s"'), $name));
+
+ $user = new User((string) time());
+ $user->setGroups([$name]);
+ (new AdmissionLoader())->applyRoles($user);
+ }
+
+ $chosenRole = null;
+ $assignedRoles = array_filter($user->getRoles(), function ($role) use ($user, &$chosenRole, $roleName) {
+ if (! in_array($role->getName(), $user->getAdditional('assigned_roles'), true)) {
+ return false;
+ }
+
+ if ($role->getName() === $roleName) {
+ $chosenRole = $role;
+ }
+
+ return true;
+ });
+
+ $this->addControl(Html::tag(
+ 'ul',
+ ['class' => 'privilege-audit-role-control'],
+ [
+ Html::tag('li', $roleName ? null : ['class' => 'active'], new Link(
+ t('All roles'),
+ Url::fromRequest()->without('role'),
+ ['class' => 'button-link', 'title' => t('Show privileges of all roles')]
+ )),
+ array_map(function ($role) use ($roleName) {
+ return Html::tag(
+ 'li',
+ $role->getName() === $roleName ? ['class' => 'active'] : null,
+ new Link(
+ $role->getName(),
+ Url::fromRequest()->setParam('role', $role->getName()),
+ [
+ 'class' => 'button-link',
+ 'title' => sprintf(t('Only show privileges of role %s'), $role->getName())
+ ]
+ )
+ );
+ }, $assignedRoles)
+ ]
+ ));
+
+ $this->addControl($header);
+ $this->addContent(
+ (new PrivilegeAudit($chosenRole !== null ? [$chosenRole] : $assignedRoles))
+ ->addAttributes(['id' => 'role-audit'])
+ );
+ }
+
+ public function suggestRoleMemberAction()
+ {
+ $this->assertHttpMethod('POST');
+ $requestData = $this->getRequest()->getPost();
+ $limit = $this->params->get('limit', 50);
+
+ $searchTerm = $requestData['term']['label'];
+ $userBackends = $this->loadUserBackends(Selectable::class);
+
+ $suggestions = [];
+ while ($limit > 0 && ! empty($userBackends)) {
+ /** @var Repository $backend */
+ $backend = array_shift($userBackends);
+ $query = $backend->select()
+ ->from('user', ['user_name'])
+ ->where('user_name', $searchTerm)
+ ->limit($limit);
+
+ try {
+ $names = $query->fetchColumn();
+ } catch (Exception $e) {
+ continue;
+ }
+
+ $domain = '';
+ if ($backend instanceof DomainAwareInterface) {
+ $domain = '@' . $backend->getDomain();
+ }
+
+ $users = [];
+ foreach ($names as $name) {
+ $users[] = [$name . $domain, [
+ 'type' => 'user',
+ 'backend' => $backend->getName()
+ ]];
+ }
+
+ if (! empty($users)) {
+ $suggestions[] = [
+ [
+ t('Users'),
+ HtmlString::create('&nbsp;'),
+ Html::tag('span', ['class' => 'badge'], $backend->getName())
+ ],
+ $users
+ ];
+ }
+
+ $limit -= count($names);
+ }
+
+ $groupBackends = $this->loadUserGroupBackends(Selectable::class);
+
+ while ($limit > 0 && ! empty($groupBackends)) {
+ /** @var Repository $backend */
+ $backend = array_shift($groupBackends);
+ $query = $backend->select()
+ ->from('group', ['group_name'])
+ ->where('group_name', $searchTerm)
+ ->limit($limit);
+
+ try {
+ $names = $query->fetchColumn();
+ } catch (Exception $e) {
+ continue;
+ }
+
+ $groups = [];
+ foreach ($names as $name) {
+ $groups[] = [$name, ['type' => 'group']];
+ }
+
+ if (! empty($groups)) {
+ $suggestions[] = [
+ [
+ t('Groups'),
+ HtmlString::create('&nbsp;'),
+ Html::tag('span', ['class' => 'badge'], $backend->getName())
+ ],
+ $groups
+ ];
+ }
+
+ $limit -= count($names);
+ }
+
+ if (empty($suggestions)) {
+ $suggestions[] = [t('Your search does not match any user or group'), []];
+ }
+
+ $this->document->add(SingleValueSearchControl::createSuggestions($suggestions));
+ }
+
+ /**
+ * Create the tabs to display when listing roles
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'role/list',
+ array(
+ 'baseTarget' => '_main',
+ 'label' => $this->translate('Roles'),
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'url' => 'role/list'
+ )
+ );
+
+ $tabs->add(
+ 'role/audit',
+ [
+ 'title' => $this->translate('Audit a user\'s or group\'s privileges'),
+ 'label' => $this->translate('Audit'),
+ 'url' => 'role/audit',
+ 'baseTarget' => '_main'
+ ]
+ );
+
+ if ($this->hasPermission('config/access-control/users')) {
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'user/list'
+ )
+ );
+ }
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'group/list'
+ )
+ );
+ }
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/SearchController.php b/application/controllers/SearchController.php
new file mode 100644
index 0000000..92aeabe
--- /dev/null
+++ b/application/controllers/SearchController.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Widget;
+use Icinga\Web\Widget\SearchDashboard;
+
+/**
+ * Search controller
+ */
+class SearchController extends ActionController
+{
+ public function indexAction()
+ {
+ $searchDashboard = new SearchDashboard();
+ $searchDashboard->setUser($this->Auth()->getUser());
+ $this->view->dashboard = $searchDashboard->search($this->params->get('q'));
+
+ // NOTE: This renders the dashboard twice. Remove this once we can catch exceptions thrown in view scripts.
+ $this->view->dashboard->render();
+ }
+
+ public function hintAction()
+ {
+ }
+}
diff --git a/application/controllers/StaticController.php b/application/controllers/StaticController.php
new file mode 100644
index 0000000..44a807a
--- /dev/null
+++ b/application/controllers/StaticController.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Controller;
+use Icinga\Web\FileCache;
+
+/**
+ * Deliver static content to clients
+ */
+class StaticController extends Controller
+{
+ /**
+ * Static routes don't require authentication
+ *
+ * @var bool
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * Disable layout rendering as this controller doesn't provide any html layouts
+ */
+ public function init()
+ {
+ $this->_helper->viewRenderer->setNoRender(true);
+ $this->_helper->layout()->disableLayout();
+ }
+
+ /**
+ * Return an image from a module's public folder
+ */
+ public function imgAction()
+ {
+ $imgRoot = Icinga::app()
+ ->getModuleManager()
+ ->getModule($this->getParam('module_name'))
+ ->getBaseDir() . '/public/img/';
+
+ $file = $this->getParam('file');
+ $filePath = realpath($imgRoot . $file);
+
+ if ($filePath === false || substr($filePath, 0, strlen($imgRoot)) !== $imgRoot) {
+ $this->httpNotFound('%s does not exist', $file);
+ }
+
+ if (preg_match('/\.([a-z]+)$/i', $file, $m)) {
+ $extension = $m[1];
+ if ($extension === 'svg') {
+ $extension = 'svg+xml';
+ }
+ } else {
+ $extension = 'fixme';
+ }
+
+ $s = stat($filePath);
+ $eTag = sprintf('%x-%x-%x', $s['ino'], $s['size'], (float) str_pad((string) $s['mtime'], 16, '0'));
+
+ $this->getResponse()->setHeader(
+ 'Cache-Control',
+ 'public, max-age=1814400, stale-while-revalidate=604800',
+ true
+ );
+
+ if ($this->getRequest()->getServer('HTTP_IF_NONE_MATCH') === $eTag) {
+ $this->getResponse()
+ ->setHttpResponseCode(304);
+ } else {
+ $this->getResponse()
+ ->setHeader('ETag', $eTag)
+ ->setHeader('Content-Type', 'image/' . $extension, true)
+ ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $s['mtime']) . ' GMT');
+
+ readfile($filePath);
+ }
+ }
+}
diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
new file mode 100644
index 0000000..dac80d3
--- /dev/null
+++ b/application/controllers/UserController.php
@@ -0,0 +1,374 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Authentication\AdmissionLoader;
+use Icinga\Authentication\User\DomainAwareInterface;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Config\User\CreateMembershipForm;
+use Icinga\Forms\Config\User\UserForm;
+use Icinga\User;
+use Icinga\Web\Controller\AuthBackendController;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget;
+
+class UserController extends AuthBackendController
+{
+ public function init()
+ {
+ $this->view->title = $this->translate('Users');
+
+ parent::init();
+ }
+
+ /**
+ * List all users of a single backend
+ */
+ public function listAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $this->createListTabs()->activate('user/list');
+ $backendNames = array_map(
+ function ($b) {
+ return $b->getName();
+ },
+ $this->loadUserBackends('Icinga\Data\Selectable')
+ );
+ if (empty($backendNames)) {
+ return;
+ }
+
+ $this->view->backendSelection = new Form();
+ $this->view->backendSelection->setAttrib('class', 'backend-selection icinga-controls');
+ $this->view->backendSelection->setUidDisabled();
+ $this->view->backendSelection->setMethod('GET');
+ $this->view->backendSelection->setTokenDisabled();
+ $this->view->backendSelection->addElement(
+ 'select',
+ 'backend',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Backend'),
+ 'multiOptions' => array_combine($backendNames, $backendNames),
+ 'value' => $this->params->get('backend')
+ )
+ );
+
+ $backend = $this->getUserBackend($this->params->get('backend'));
+ if ($backend === null) {
+ $this->view->backend = null;
+ return;
+ }
+
+ $query = $backend->select(array('user_name'));
+
+ $this->view->users = $query;
+ $this->view->backend = $backend;
+
+ $this->setupPaginationControl($query);
+ $this->setupFilterControl($query);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'user_name' => $this->translate('Username'),
+ 'is_active' => $this->translate('Active'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Show a user
+ */
+ public function showAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'));
+
+ $user = $backend->select(array(
+ 'user_name',
+ 'is_active',
+ 'created_at',
+ 'last_modified'
+ ))->where('user_name', $userName)->fetchRow();
+ if ($user === false) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $userObj = new User($userName);
+ if ($backend instanceof DomainAwareInterface) {
+ $userObj->setDomain($backend->getDomain());
+ }
+
+ $memberships = $this->loadMemberships($userObj)->select();
+
+ $this->setupFilterControl(
+ $memberships,
+ array('group_name' => t('User Group')),
+ array('group'),
+ array('user')
+ );
+ $this->setupPaginationControl($memberships);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'group_name' => $this->translate('Group')
+ ),
+ $memberships
+ );
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
+ $this->view->showCreateMembershipLink = ! empty($extensibleBackends);
+ } else {
+ $this->view->showCreateMembershipLink = false;
+ }
+
+ $this->view->user = $user;
+ $this->view->backend = $backend;
+ $this->view->memberships = $memberships;
+ $this->createShowTabs($backend->getName(), $userName)->activate('user/show');
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAttrib('class', 'inline');
+ $removeForm->addElement('hidden', 'user_name', array(
+ 'isArray' => true,
+ 'value' => $userName,
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('user/show', array(
+ 'backend' => $backend->getName(),
+ 'user' => $userName
+ )),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Cancel this membership')
+ ));
+ $this->view->removeForm = $removeForm;
+ }
+
+ $admissionLoader = new AdmissionLoader();
+ $admissionLoader->applyRoles($userObj);
+ $this->view->userObj = $userObj;
+ $this->view->allowedToEditRoles = $this->hasPermission('config/access-control/groups');
+ }
+
+ /**
+ * Add a user
+ */
+ public function addAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+ $form->add()->handleRequest();
+
+ $this->renderForm($form, $this->translate('New User'));
+ }
+
+ /**
+ * Edit a user
+ */
+ public function editAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
+
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)));
+ $form->setRepository($backend);
+
+ try {
+ $form->edit($userName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $this->renderForm($form, $this->translate('Update User'));
+ }
+
+ /**
+ * Remove a user
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+
+ try {
+ $form->remove($userName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $this->renderForm($form, $this->translate('Remove User'));
+ }
+
+ /**
+ * Create a membership for a user
+ */
+ public function createmembershipAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'));
+
+ if ($backend->select()->where('user_name', $userName)->count() === 0) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $backends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
+ if (empty($backends)) {
+ throw new ConfigurationError($this->translate(
+ 'You\'ll need to configure at least one user group backend first that allows to create new memberships'
+ ));
+ }
+
+ $form = new CreateMembershipForm();
+ $form->setBackends($backends)
+ ->setUsername($userName)
+ ->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)))
+ ->handleRequest();
+
+ $this->renderForm($form, $this->translate('Create New Membership'));
+ }
+
+ /**
+ * Fetch and return the given user's groups from all user group backends
+ *
+ * @param User $user
+ *
+ * @return ArrayDatasource
+ */
+ protected function loadMemberships(User $user)
+ {
+ $groups = $alreadySeen = array();
+ foreach ($this->loadUserGroupBackends() as $backend) {
+ try {
+ foreach ($backend->getMemberships($user) as $groupName) {
+ if (array_key_exists($groupName, $alreadySeen)) {
+ continue; // Ignore duplicate memberships
+ }
+
+ $alreadySeen[$groupName] = null;
+ $groups[] = (object) array(
+ 'group_name' => $groupName,
+ 'group' => $groupName,
+ 'backend' => $backend
+ );
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch memberships from backend %s. Please check your log'),
+ $backend->getName()
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+
+ /**
+ * Create the tabs to display when showing a user
+ *
+ * @param string $backendName
+ * @param string $userName
+ */
+ protected function createShowTabs($backendName, $userName)
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'user/show',
+ array(
+ 'title' => sprintf($this->translate('Show user %s'), $userName),
+ 'label' => $this->translate('User'),
+ 'url' => Url::fromPath('user/show', array('backend' => $backendName, 'user' => $userName))
+ )
+ );
+
+ return $tabs;
+ }
+
+ /**
+ * Create the tabs to display when listing users
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+
+ if ($this->hasPermission('config/access-control/roles')) {
+ $tabs->add(
+ 'role/list',
+ array(
+ 'baseTarget' => '_main',
+ 'label' => $this->translate('Roles'),
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'url' => 'role/list'
+ )
+ );
+
+ $tabs->add(
+ 'role/audit',
+ [
+ 'title' => $this->translate('Audit a user\'s or group\'s privileges'),
+ 'label' => $this->translate('Audit'),
+ 'url' => 'role/audit',
+ 'baseTarget' => '_main'
+ ]
+ );
+ }
+
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'user/list'
+ )
+ );
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'group/list'
+ )
+ );
+ }
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php
new file mode 100644
index 0000000..a96ab75
--- /dev/null
+++ b/application/controllers/UsergroupbackendController.php
@@ -0,0 +1,133 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Config\UserGroup\UserGroupBackendForm;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Notification;
+
+/**
+ * Controller to configure user group backends
+ */
+class UsergroupbackendController extends Controller
+{
+ /**
+ * Initialize this controller
+ */
+ public function init()
+ {
+ $this->assertPermission('config/access-control/users');
+ }
+
+ /**
+ * Redirect to this controller's list action
+ */
+ public function indexAction()
+ {
+ $this->redirectNow('config/userbackend');
+ }
+
+ /**
+ * Create a new user group backend
+ */
+ public function createAction()
+ {
+ $form = new UserGroupBackendForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->addDescription($this->translate('Create a new backend to associate users and groups with.'));
+ $form->setIniConfig(Config::app('groups'));
+ $form->setOnSuccess(function (UserGroupBackendForm $form) {
+ try {
+ $form->add($form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(t('User group backend successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('New User Group Backend'));
+ }
+
+ /**
+ * Edit an user group backend
+ */
+ public function editAction()
+ {
+ $backendName = $this->params->getRequired('backend');
+
+ $form = new UserGroupBackendForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setIniConfig(Config::app('groups'));
+ $form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) {
+ try {
+ $form->edit($backendName, $form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(sprintf(t('User group backend "%s" successfully updated'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($backendName);
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $backendName));
+ }
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Update User Group Backend'));
+ }
+
+ /**
+ * Remove a user group backend
+ */
+ public function removeAction()
+ {
+ $backendName = $this->params->getRequired('backend');
+
+ $backendForm = new UserGroupBackendForm();
+ $backendForm->setIniConfig(Config::app('groups'));
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) {
+ try {
+ $backendForm->delete($backendName);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($backendForm->save()) {
+ Notification::success(sprintf(t('User group backend "%s" successfully removed'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Remove User Group Backend'));
+ }
+}
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 <i> 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, <your_font_name>.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 <IE9, when
+ string is too long.
+
+- animate.css - use it to get ideas about spinner rotation animation.
+
+
+Attention for server setup
+--------------------------
+
+You MUST setup server to reply with proper `mime-types` for font files -
+otherwise some browsers will fail to show fonts.
+
+Usually, `apache` already has necessary settings, but `nginx` and other
+webservers should be tuned. Here is list of mime types for our file extensions:
+
+- `application/vnd.ms-fontobject` - eot
+- `application/x-font-woff` - woff
+- `application/x-font-ttf` - ttf
+- `image/svg+xml` - svg
diff --git a/application/fonts/fontello-ifont/config.json b/application/fonts/fontello-ifont/config.json
new file mode 100644
index 0000000..a982335
--- /dev/null
+++ b/application/fonts/fontello-ifont/config.json
@@ -0,0 +1,874 @@
+{
+ "name": "ifont",
+ "css_prefix_text": "icon-",
+ "css_use_suffix": false,
+ "hinting": true,
+ "units_per_em": 1000,
+ "ascent": 850,
+ "glyphs": [
+ {
+ "uid": "9bc2902722abb366a213a052ade360bc",
+ "css": "spin6",
+ "code": 59508,
+ "src": "fontelico"
+ },
+ {
+ "uid": "9dd9e835aebe1060ba7190ad2b2ed951",
+ "css": "search",
+ "code": 59484,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "8b80d36d4ef43889db10bc1f0dc9a862",
+ "css": "user",
+ "code": 59393,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "31972e4e9d080eaa796290349ae6c1fd",
+ "css": "users",
+ "code": 59394,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b1887b423d2fd15c345e090320c91ca0",
+ "css": "dashboard",
+ "code": 59392,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ce3cf091d6ebd004dd0b52d24074e6e3",
+ "css": "help",
+ "code": 59483,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3d4ea8a78dc34efe891f3a0f3d961274",
+ "css": "info",
+ "code": 59482,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d7271d490b71df4311e32cdacae8b331",
+ "css": "home",
+ "code": 59481,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0d6ab6194c0eddda2b8c9cedf2ab248e",
+ "css": "attach",
+ "code": 59498,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "c1f1975c885aa9f3dad7810c53b82074",
+ "css": "lock",
+ "code": 59480,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "657ab647f6248a6b57a5b893beaf35a9",
+ "css": "lock-open",
+ "code": 59479,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "05376be04a27d5a46e855a233d6e8508",
+ "css": "lock-open-alt",
+ "code": 59478,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "c5fd349cbd3d23e4ade333789c29c729",
+ "css": "eye",
+ "code": 59475,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7fd683b2c518ceb9e5fa6757f2276faa",
+ "css": "eye-off",
+ "code": 59491,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3db5347bd219f3bce6025780f5d9ef45",
+ "css": "tag",
+ "code": 59476,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "a3f89e106175a5c5c4e9738870b12e55",
+ "css": "tags",
+ "code": 59477,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "acf41aa4018e58d49525665469e35665",
+ "css": "thumbs-up",
+ "code": 59495,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7533e68038fc6d520ede7a7ffa0a2f64",
+ "css": "thumbs-down",
+ "code": 59496,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9a76bc135eac17d2c8b8ad4a5774fc87",
+ "css": "download",
+ "code": 59400,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "eeec3208c90b7b48e804919d0d2d4a41",
+ "css": "upload",
+ "code": 59401,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "c6be5a58ee4e63a5ec399c2b0d15cf2c",
+ "css": "reply",
+ "code": 59473,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "1b5597a3bacaeca6600e88ae36d02e0a",
+ "css": "reply-all",
+ "code": 59474,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3d39c828009c04ddb6764c0b04cd2439",
+ "css": "forward",
+ "code": 59472,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "41087bc74d4b20b55059c60a33bf4008",
+ "css": "edit",
+ "code": 59471,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7277ded7695b2a307a5f9d50097bb64c",
+ "css": "print",
+ "code": 59470,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ecb97add13804c190456025e43ec003b",
+ "css": "keyboard",
+ "code": 59499,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "85528017f1e6053b2253785c31047f44",
+ "css": "comment",
+ "code": 59464,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "dcedf50ab1ede3283d7a6c70e2fe32f3",
+ "css": "chat",
+ "code": 59465,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9c1376672bb4f1ed616fdd78a23667e9",
+ "css": "comment-empty",
+ "code": 59463,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "31951fbb9820ed0690f675b3d495c8da",
+ "css": "chat-empty",
+ "code": 59466,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "cd21cbfb28ad4d903cede582157f65dc",
+ "css": "bell",
+ "code": 59467,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "671f29fa10dda08074a4c6a341bb4f39",
+ "css": "bell-alt",
+ "code": 59468,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "563683020e0bf9f22f3f055a69b5c57a",
+ "css": "bell-off",
+ "code": 59488,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "8a074400a056c59d389f2d0517281bd5",
+ "css": "bell-off-empty",
+ "code": 59489,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "00391fac5d419345ffcccd95b6f76263",
+ "css": "attention-alt",
+ "code": 59469,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "f48ae54adfb27d8ada53d0fd9e34ee10",
+ "css": "trash",
+ "code": 59462,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5408be43f7c42bccee419c6be53fdef5",
+ "css": "doc-text",
+ "code": 59461,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9daa1fdf0838118518a7e22715e83abc",
+ "css": "file-pdf",
+ "code": 59458,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "310ffd629da85142bc8669f010556f2d",
+ "css": "file-word",
+ "code": 59459,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "f761c3bbe16ba2d332914ecb28e7a042",
+ "css": "file-excel",
+ "code": 59460,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9f7e588c66cfd6891f6f507cf6f6596b",
+ "css": "phone",
+ "code": 59457,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "559647a6f430b3aeadbecd67194451dd",
+ "css": "menu",
+ "code": 59500,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e99461abfef3923546da8d745372c995",
+ "css": "service",
+ "code": 59456,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "98687378abd1faf8f6af97c254eb6cd6",
+ "css": "services",
+ "code": 59455,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5bb103cd29de77e0e06a52638527b575",
+ "css": "wrench",
+ "code": 59453,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "21b42d3c3e6be44c3cc3d73042faa216",
+ "css": "sliders",
+ "code": 59454,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "531bc468eecbb8867d822f1c11f1e039",
+ "css": "calendar",
+ "code": 59452,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ead4c82d04d7758db0f076584893a8c1",
+ "css": "calendar-empty",
+ "code": 59451,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3a00327e61b997b58518bd43ed83c3df",
+ "css": "endtime",
+ "code": 59449,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0d20938846444af8deb1920dc85a29fb",
+ "css": "starttime",
+ "code": 59450,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "19c50c52858a81de58f9db488aba77bc",
+ "css": "mic",
+ "code": 59448,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "43c629249e2cca7e73cd4ef410c9551f",
+ "css": "mute",
+ "code": 59447,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e44601720c64e6bb6a2d5cba6b0c588c",
+ "css": "volume-off",
+ "code": 59446,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "fee6e00f36e8ca8ef3e4a62caa213bf6",
+ "css": "volume-down",
+ "code": 59445,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "76857a03fbaa6857fe063b6c25aa98ed",
+ "css": "volume-up",
+ "code": 59444,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "598a5f2bcf3521d1615de8e1881ccd17",
+ "css": "clock",
+ "code": 59443,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5278ef7773e948d56c4d442c8c8c98cf",
+ "css": "lightbulb",
+ "code": 59442,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "98d9c83c1ee7c2c25af784b518c522c5",
+ "css": "block",
+ "code": 59440,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e594fc6e5870b4ab7e49f52571d52577",
+ "css": "resize-full",
+ "code": 59434,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b013f6403e5ab0326614e68d1850fd6b",
+ "css": "resize-full-alt",
+ "code": 59433,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3c24ee33c9487bbf18796ca6dffa1905",
+ "css": "resize-small",
+ "code": 59435,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d3b3f17bc3eb7cd809a07bbd4d178bee",
+ "css": "resize-vertical",
+ "code": 59438,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3c73d058e4589b65a8d959c0fc8f153d",
+ "css": "resize-horizontal",
+ "code": 59437,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "6605ee6441bf499ffa3c63d3c7409471",
+ "css": "move",
+ "code": 59436,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0b2b66e526028a6972d51a6f10281b4b",
+ "css": "zoom-in",
+ "code": 59439,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d25d10efa900f529ad1d275657cfd30e",
+ "css": "zoom-out",
+ "code": 59441,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "2d6150442079cbda7df64522dc24f482",
+ "css": "down-dir",
+ "code": 59421,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "80cd1022bd9ea151d554bec1fa05f2de",
+ "css": "up-dir",
+ "code": 59422,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9dc654095085167524602c9acc0c5570",
+ "css": "left-dir",
+ "code": 59423,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "fb1c799ffe5bf8fb7f8bcb647c8fe9e6",
+ "css": "right-dir",
+ "code": 59424,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ccddff8e8670dcd130e3cb55fdfc2fd0",
+ "css": "down-open",
+ "code": 59425,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d870630ff8f81e6de3958ecaeac532f2",
+ "css": "left-open",
+ "code": 59428,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "399ef63b1e23ab1b761dfbb5591fa4da",
+ "css": "right-open",
+ "code": 59426,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "fe6697b391355dec12f3d86d6d490397",
+ "css": "up-open",
+ "code": 59427,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "745f12abe1472d14f8f658de7e5aba66",
+ "css": "angle-double-left",
+ "code": 59514,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "fdfbd1fcbd4cb229716a810801a5f207",
+ "css": "angle-double-right",
+ "code": 59515,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "1c4068ed75209e21af36017df8871802",
+ "css": "down-big",
+ "code": 59432,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "555ef8c86832e686fef85f7af2eb7cde",
+ "css": "left-big",
+ "code": 59431,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ad6b3fbb5324abe71a9c0b6609cbb9f1",
+ "css": "right-big",
+ "code": 59430,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "95376bf082bfec6ce06ea1cda7bd7ead",
+ "css": "up-big",
+ "code": 59429,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d407a4707f719b042ed2ad28d2619d7e",
+ "css": "barchart",
+ "code": 59420,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "cd4bfdae4dc89b175ff49330ce29611a",
+ "css": "wifi",
+ "code": 59501,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "500fc1f109021e4b1de4deda2f7ed399",
+ "css": "host",
+ "code": 59494,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "197375a3cea8cb90b02d06e4ddf1433d",
+ "css": "globe",
+ "code": 59417,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "2c413e78faf1d6631fd7b094d14c2253",
+ "css": "cloud",
+ "code": 59418,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3212f42c65d41ed91cb435d0490e29ed",
+ "css": "flash",
+ "code": 59419,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "567e3e257f2cc8fba2c12bf691c9f2d8",
+ "css": "moon",
+ "code": 59502,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "8772331a9fec983cdb5d72902a6f9e0e",
+ "css": "scissors",
+ "code": 59416,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b429436ec5a518c78479d44ef18dbd60",
+ "css": "paste",
+ "code": 59415,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "8b9e6a8dd8f67f7c003ed8e7e5ee0857",
+ "css": "off",
+ "code": 59413,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9755f76110ae4d12ac5f9466c9152031",
+ "css": "book",
+ "code": 59414,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "130380e481a7defc690dfb24123a1f0c",
+ "css": "circle",
+ "code": 59516,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "266d5d9adf15a61800477a5acf9a4462",
+ "css": "chart-bar",
+ "code": 59505,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7d1ca956f4181a023de4b9efbed92524",
+ "css": "chart-area",
+ "code": 59504,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "554ee96588a6c9ee632624cd051fe6fc",
+ "css": "chart-pie",
+ "code": 59503,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ea2d9a8c51ca42b38ef0d2a07f16d9a7",
+ "css": "chart-line",
+ "code": 59487,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3e674995cacc2b09692c096ea7eb6165",
+ "css": "megaphone",
+ "code": 59409,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7432077e6a2d6aa19984ca821bb6bbda",
+ "css": "bug",
+ "code": 59410,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9396b2d8849e0213a0f11c5fd7fcc522",
+ "css": "tasks",
+ "code": 59411,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "4109c474ff99cad28fd5a2c38af2ec6f",
+ "css": "filter",
+ "code": 59412,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0f444c61b0d2c9966016d7ddb12f5837",
+ "css": "beaker",
+ "code": 59506,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ff70f7b3228702e0d590e60ed3b90bea",
+ "css": "magic",
+ "code": 59507,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3ed68ae14e9cde775121954242a412b2",
+ "css": "sort-name-up",
+ "code": 59407,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "6586267200a42008a9fc0a1bf7ac06c7",
+ "css": "sort-name-down",
+ "code": 59408,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0bda4bc779d4c32623dec2e43bd67ee8",
+ "css": "gauge",
+ "code": 59405,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "6fe95ffc3c807e62647d4f814a96e0d7",
+ "css": "sitemap",
+ "code": 59406,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "cda0cdcfd38f5f1d9255e722dad42012",
+ "css": "spinner",
+ "code": 59497,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "af95ef0ddda80a78828c62d386506433",
+ "css": "cubes",
+ "code": 59403,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "347c38a8b96a509270fdcabc951e7571",
+ "css": "database",
+ "code": 59404,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "a14be0c7e0689076e2bdde97f8e309f9",
+ "css": "plug",
+ "code": 59490,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "4743b088aa95d6f3b6b990e770d3b647",
+ "css": "facebook-squared",
+ "code": 59519,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e7cb72a17f3b21e3576f35c3f0a7639b",
+ "css": "git",
+ "code": 59402,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "f0cf7db1b03cb65adc450aa3bdaf8c4d",
+ "css": "gplus-squared",
+ "code": 59520,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "627abcdb627cb1789e009c08e2678ef9",
+ "css": "twitter",
+ "code": 59518,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7e4164950ffa4990961958b2d6318658",
+ "css": "info-circled",
+ "code": 59517,
+ "src": "entypo"
+ },
+ {
+ "uid": "465bb89b6f204234e5787c326b4ae54c",
+ "css": "rewind",
+ "code": 59486,
+ "src": "entypo"
+ },
+ {
+ "uid": "bb46b15cb78cc4cc05d3d715d522ac4d",
+ "css": "cw",
+ "code": 59493,
+ "src": "entypo"
+ },
+ {
+ "uid": "3bd18d47a12b8709e9f4fe9ead4f7518",
+ "css": "reschedule",
+ "code": 59524,
+ "src": "entypo"
+ },
+ {
+ "uid": "p57wgnf4glngbchbucdi029iptu8oxb8",
+ "css": "pin",
+ "code": 59513,
+ "src": "typicons"
+ },
+ {
+ "uid": "c16a63e911bc47b46dc2a7129d2f0c46",
+ "css": "down-small",
+ "code": 59509,
+ "src": "typicons"
+ },
+ {
+ "uid": "58b78b6ca784d5c3db5beefcd9e18061",
+ "css": "left-small",
+ "code": 59510,
+ "src": "typicons"
+ },
+ {
+ "uid": "877a233d7fdca8a1d82615b96ed0d7a2",
+ "css": "right-small",
+ "code": 59511,
+ "src": "typicons"
+ },
+ {
+ "uid": "62bc6fe2a82e4864e2b94d4c0985ee0c",
+ "css": "up-small",
+ "code": 59512,
+ "src": "typicons"
+ },
+ {
+ "uid": "11e664deed5b2587456a4f9c01d720b6",
+ "css": "cancel",
+ "code": 59396,
+ "src": "iconic"
+ },
+ {
+ "uid": "dbd39eb5a1d67beb54cfcb535e840e0f",
+ "css": "plus",
+ "code": 59397,
+ "src": "iconic"
+ },
+ {
+ "uid": "9559f17a471856ef50ed266e726cfa25",
+ "css": "minus",
+ "code": 59398,
+ "src": "iconic"
+ },
+ {
+ "uid": "13ea1e82d38c7ed614d9ee85e9c42053",
+ "css": "folder-empty",
+ "code": 59399,
+ "src": "iconic"
+ },
+ {
+ "uid": "8f28d948aa6379b1a69d2a090e7531d4",
+ "css": "warning-empty",
+ "code": 59525,
+ "src": "typicons"
+ },
+ {
+ "uid": "d4816c0845aa43767213d45574b3b145",
+ "css": "history",
+ "code": 61914,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b035c28eba2b35c6ffe92aee8b0df507",
+ "css": "attention-circled",
+ "code": 59521,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "73ffeb70554099177620847206c12457",
+ "css": "binoculars",
+ "code": 61925,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "a73c5deb486c8d66249811642e5d719a",
+ "css": "arrows-cw",
+ "code": 59492,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "dd6c6b221a1088ff8a9b9cd32d0b3dd5",
+ "css": "check",
+ "code": 59523,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b90d80c250a9bbdd6cd3fe00e6351710",
+ "css": "ok",
+ "code": 59395,
+ "src": "iconic"
+ },
+ {
+ "uid": "37c5ab63f10d7ad0b84d0978dcd0c7a8",
+ "css": "flapping",
+ "code": 59485,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0f6a2573a7b6df911ed199bb63717e27",
+ "css": "github-circled",
+ "code": 61595,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5eb43711f62fb4dcbef10d0224c28065",
+ "css": "th-thumb-empty",
+ "code": 61451,
+ "src": "mfglabs"
+ },
+ {
+ "uid": "1b58555745e7378f7634ee7c63eada46",
+ "css": "th-list",
+ "code": 61449,
+ "src": "mfglabs"
+ },
+ {
+ "uid": "63b3012c8cbe3654ba5bea598235aa3a",
+ "css": "angle-double-up",
+ "code": 61698,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "dfec4ffa849d8594c2e4b86f6320b8a6",
+ "css": "angle-double-down",
+ "code": 61699,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "f3f90c8c89795da30f7444634476ea4f",
+ "css": "angle-left",
+ "code": 61700,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7bf14281af5633a597f85b061ef1cfb9",
+ "css": "angle-right",
+ "code": 61701,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5de9370846a26947e03f63142a3f1c07",
+ "css": "angle-up",
+ "code": 61702,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e4dde1992f787163e2e2b534b8c8067d",
+ "css": "angle-down",
+ "code": 61703,
+ "src": "fontawesome"
+ }
+ ]
+} \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/animation.css b/application/fonts/fontello-ifont/css/animation.css
new file mode 100644
index 0000000..ac5a956
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/animation.css
@@ -0,0 +1,85 @@
+/*
+ Animation example, for spinners
+*/
+.animate-spin {
+ -moz-animation: spin 2s infinite linear;
+ -o-animation: spin 2s infinite linear;
+ -webkit-animation: spin 2s infinite linear;
+ animation: spin 2s infinite linear;
+ display: inline-block;
+}
+@-moz-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-webkit-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-o-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-ms-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
diff --git a/application/fonts/fontello-ifont/css/ifont-codes.css b/application/fonts/fontello-ifont/css/ifont-codes.css
new file mode 100644
index 0000000..74856f3
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont-codes.css
@@ -0,0 +1,145 @@
+
+.icon-dashboard:before { content: '\e800'; } /* '' */
+.icon-user:before { content: '\e801'; } /* '' */
+.icon-users:before { content: '\e802'; } /* '' */
+.icon-ok:before { content: '\e803'; } /* '' */
+.icon-cancel:before { content: '\e804'; } /* '' */
+.icon-plus:before { content: '\e805'; } /* '' */
+.icon-minus:before { content: '\e806'; } /* '' */
+.icon-folder-empty:before { content: '\e807'; } /* '' */
+.icon-download:before { content: '\e808'; } /* '' */
+.icon-upload:before { content: '\e809'; } /* '' */
+.icon-git:before { content: '\e80a'; } /* '' */
+.icon-cubes:before { content: '\e80b'; } /* '' */
+.icon-database:before { content: '\e80c'; } /* '' */
+.icon-gauge:before { content: '\e80d'; } /* '' */
+.icon-sitemap:before { content: '\e80e'; } /* '' */
+.icon-sort-name-up:before { content: '\e80f'; } /* '' */
+.icon-sort-name-down:before { content: '\e810'; } /* '' */
+.icon-megaphone:before { content: '\e811'; } /* '' */
+.icon-bug:before { content: '\e812'; } /* '' */
+.icon-tasks:before { content: '\e813'; } /* '' */
+.icon-filter:before { content: '\e814'; } /* '' */
+.icon-off:before { content: '\e815'; } /* '' */
+.icon-book:before { content: '\e816'; } /* '' */
+.icon-paste:before { content: '\e817'; } /* '' */
+.icon-scissors:before { content: '\e818'; } /* '' */
+.icon-globe:before { content: '\e819'; } /* '' */
+.icon-cloud:before { content: '\e81a'; } /* '' */
+.icon-flash:before { content: '\e81b'; } /* '' */
+.icon-barchart:before { content: '\e81c'; } /* '' */
+.icon-down-dir:before { content: '\e81d'; } /* '' */
+.icon-up-dir:before { content: '\e81e'; } /* '' */
+.icon-left-dir:before { content: '\e81f'; } /* '' */
+.icon-right-dir:before { content: '\e820'; } /* '' */
+.icon-down-open:before { content: '\e821'; } /* '' */
+.icon-right-open:before { content: '\e822'; } /* '' */
+.icon-up-open:before { content: '\e823'; } /* '' */
+.icon-left-open:before { content: '\e824'; } /* '' */
+.icon-up-big:before { content: '\e825'; } /* '' */
+.icon-right-big:before { content: '\e826'; } /* '' */
+.icon-left-big:before { content: '\e827'; } /* '' */
+.icon-down-big:before { content: '\e828'; } /* '' */
+.icon-resize-full-alt:before { content: '\e829'; } /* '' */
+.icon-resize-full:before { content: '\e82a'; } /* '' */
+.icon-resize-small:before { content: '\e82b'; } /* '' */
+.icon-move:before { content: '\e82c'; } /* '' */
+.icon-resize-horizontal:before { content: '\e82d'; } /* '' */
+.icon-resize-vertical:before { content: '\e82e'; } /* '' */
+.icon-zoom-in:before { content: '\e82f'; } /* '' */
+.icon-block:before { content: '\e830'; } /* '' */
+.icon-zoom-out:before { content: '\e831'; } /* '' */
+.icon-lightbulb:before { content: '\e832'; } /* '' */
+.icon-clock:before { content: '\e833'; } /* '' */
+.icon-volume-up:before { content: '\e834'; } /* '' */
+.icon-volume-down:before { content: '\e835'; } /* '' */
+.icon-volume-off:before { content: '\e836'; } /* '' */
+.icon-mute:before { content: '\e837'; } /* '' */
+.icon-mic:before { content: '\e838'; } /* '' */
+.icon-endtime:before { content: '\e839'; } /* '' */
+.icon-starttime:before { content: '\e83a'; } /* '' */
+.icon-calendar-empty:before { content: '\e83b'; } /* '' */
+.icon-calendar:before { content: '\e83c'; } /* '' */
+.icon-wrench:before { content: '\e83d'; } /* '' */
+.icon-sliders:before { content: '\e83e'; } /* '' */
+.icon-services:before { content: '\e83f'; } /* '' */
+.icon-service:before { content: '\e840'; } /* '' */
+.icon-phone:before { content: '\e841'; } /* '' */
+.icon-file-pdf:before { content: '\e842'; } /* '' */
+.icon-file-word:before { content: '\e843'; } /* '' */
+.icon-file-excel:before { content: '\e844'; } /* '' */
+.icon-doc-text:before { content: '\e845'; } /* '' */
+.icon-trash:before { content: '\e846'; } /* '' */
+.icon-comment-empty:before { content: '\e847'; } /* '' */
+.icon-comment:before { content: '\e848'; } /* '' */
+.icon-chat:before { content: '\e849'; } /* '' */
+.icon-chat-empty:before { content: '\e84a'; } /* '' */
+.icon-bell:before { content: '\e84b'; } /* '' */
+.icon-bell-alt:before { content: '\e84c'; } /* '' */
+.icon-attention-alt:before { content: '\e84d'; } /* '' */
+.icon-print:before { content: '\e84e'; } /* '' */
+.icon-edit:before { content: '\e84f'; } /* '' */
+.icon-forward:before { content: '\e850'; } /* '' */
+.icon-reply:before { content: '\e851'; } /* '' */
+.icon-reply-all:before { content: '\e852'; } /* '' */
+.icon-eye:before { content: '\e853'; } /* '' */
+.icon-tag:before { content: '\e854'; } /* '' */
+.icon-tags:before { content: '\e855'; } /* '' */
+.icon-lock-open-alt:before { content: '\e856'; } /* '' */
+.icon-lock-open:before { content: '\e857'; } /* '' */
+.icon-lock:before { content: '\e858'; } /* '' */
+.icon-home:before { content: '\e859'; } /* '' */
+.icon-info:before { content: '\e85a'; } /* '' */
+.icon-help:before { content: '\e85b'; } /* '' */
+.icon-search:before { content: '\e85c'; } /* '' */
+.icon-flapping:before { content: '\e85d'; } /* '' */
+.icon-rewind:before { content: '\e85e'; } /* '' */
+.icon-chart-line:before { content: '\e85f'; } /* '' */
+.icon-bell-off:before { content: '\e860'; } /* '' */
+.icon-bell-off-empty:before { content: '\e861'; } /* '' */
+.icon-plug:before { content: '\e862'; } /* '' */
+.icon-eye-off:before { content: '\e863'; } /* '' */
+.icon-arrows-cw:before { content: '\e864'; } /* '' */
+.icon-cw:before { content: '\e865'; } /* '' */
+.icon-host:before { content: '\e866'; } /* '' */
+.icon-thumbs-up:before { content: '\e867'; } /* '' */
+.icon-thumbs-down:before { content: '\e868'; } /* '' */
+.icon-spinner:before { content: '\e869'; } /* '' */
+.icon-attach:before { content: '\e86a'; } /* '' */
+.icon-keyboard:before { content: '\e86b'; } /* '' */
+.icon-menu:before { content: '\e86c'; } /* '' */
+.icon-wifi:before { content: '\e86d'; } /* '' */
+.icon-moon:before { content: '\e86e'; } /* '' */
+.icon-chart-pie:before { content: '\e86f'; } /* '' */
+.icon-chart-area:before { content: '\e870'; } /* '' */
+.icon-chart-bar:before { content: '\e871'; } /* '' */
+.icon-beaker:before { content: '\e872'; } /* '' */
+.icon-magic:before { content: '\e873'; } /* '' */
+.icon-spin6:before { content: '\e874'; } /* '' */
+.icon-down-small:before { content: '\e875'; } /* '' */
+.icon-left-small:before { content: '\e876'; } /* '' */
+.icon-right-small:before { content: '\e877'; } /* '' */
+.icon-up-small:before { content: '\e878'; } /* '' */
+.icon-pin:before { content: '\e879'; } /* '' */
+.icon-angle-double-left:before { content: '\e87a'; } /* '' */
+.icon-angle-double-right:before { content: '\e87b'; } /* '' */
+.icon-circle:before { content: '\e87c'; } /* '' */
+.icon-info-circled:before { content: '\e87d'; } /* '' */
+.icon-twitter:before { content: '\e87e'; } /* '' */
+.icon-facebook-squared:before { content: '\e87f'; } /* '' */
+.icon-gplus-squared:before { content: '\e880'; } /* '' */
+.icon-attention-circled:before { content: '\e881'; } /* '' */
+.icon-check:before { content: '\e883'; } /* '' */
+.icon-reschedule:before { content: '\e884'; } /* '' */
+.icon-warning-empty:before { content: '\e885'; } /* '' */
+.icon-th-list:before { content: '\f009'; } /* '' */
+.icon-th-thumb-empty:before { content: '\f00b'; } /* '' */
+.icon-github-circled:before { content: '\f09b'; } /* '' */
+.icon-angle-double-up:before { content: '\f102'; } /* '' */
+.icon-angle-double-down:before { content: '\f103'; } /* '' */
+.icon-angle-left:before { content: '\f104'; } /* '' */
+.icon-angle-right:before { content: '\f105'; } /* '' */
+.icon-angle-up:before { content: '\f106'; } /* '' */
+.icon-angle-down:before { content: '\f107'; } /* '' */
+.icon-history:before { content: '\f1da'; } /* '' */
+.icon-binoculars:before { content: '\f1e5'; } /* '' */ \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/ifont-embedded.css b/application/fonts/fontello-ifont/css/ifont-embedded.css
new file mode 100644
index 0000000..beb6f9a
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont-embedded.css
@@ -0,0 +1,198 @@
+@font-face {
+ font-family: 'ifont';
+ src: url('../font/ifont.eot?21447335');
+ src: url('../font/ifont.eot?21447335#iefix') format('embedded-opentype'),
+ url('../font/ifont.svg?21447335#ifont') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'ifont';
+ src: url('data:application/octet-stream;base64,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') format('woff'),
+ url('data:application/octet-stream;base64,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6DgAAAA5AADoOQAA6DkAAAA6AADoOgAA6DoAAAA7AADoOwAA6DsAAAA8AADoPAAA6DwAAAA9AADoPQAA6D0AAAA+AADoPgAA6D4AAAA/AADoPwAA6D8AAABAAADoQAAA6EAAAABBAADoQQAA6EEAAABCAADoQgAA6EIAAABDAADoQwAA6EMAAABEAADoRAAA6EQAAABFAADoRQAA6EUAAABGAADoRgAA6EYAAABHAADoRwAA6EcAAABIAADoSAAA6EgAAABJAADoSQAA6EkAAABKAADoSgAA6EoAAABLAADoSwAA6EsAAABMAADoTAAA6EwAAABNAADoTQAA6E0AAABOAADoTgAA6E4AAABPAADoTwAA6E8AAABQAADoUAAA6FAAAABRAADoUQAA6FEAAABSAADoUgAA6FIAAABTAADoUwAA6FMAAABUAADoVAAA6FQAAABVAADoVQAA6FUAAABWAADoVgAA6FYAAABXAADoVwAA6FcAAABYAADoWAAA6FgAAABZAADoWQAA6FkAAABaAADoWgAA6FoAAABbAADoWwAA6FsAAABcAADoXAAA6FwAAABdAADoXQAA6F0AAABeAADoXgAA6F4AAABfAADoXwAA6F8AAABgAADoYAAA6GAAAABhAADoYQAA6GEAAABiAADoYgAA6GIAAABjAADoYwAA6GMAAABkAADoZAAA6GQAAABlAADoZQAA6GUAAABmAADoZgAA6GYAAABnAADoZwAA6GcAAABoAADoaAAA6GgAAABpAADoaQAA6GkAAABqAADoagAA6GoAAABrAADoawAA6GsAAABsAADobAAA6GwAAABtAADobQAA6G0AAABuAADobgAA6G4AAABvAADobwAA6G8AAABwAADocAAA6HAAAABxAADocQAA6HEAAAByAADocgAA6HIAAABzAADocwAA6HMAAAB0AADodAAA6HQAAAB1AADodQAA6HUAAAB2AADodgAA6HYAAAB3AADodwAA6HcAAAB4AADoeAAA6HgAAAB5AADoeQAA6HkAAAB6AADoegAA6HoAAAB7AADoewAA6HsAAAB8AADofAAA6HwAAAB9AADofQAA6H0AAAB+AADofgAA6H4AAAB/AADofwAA6H8AAACAAADogAAA6IAAAACBAADogQAA6IEAAACCAADogwAA6IMAAACDAADohAAA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IABmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQBpAGYAbwBuAHQAUgBlAGcAdQBsAGEAcgBpAGYAbwBuAHQAaQBmAG8AbgB0AFYAZQByAHMAaQBvAG4AIAAxAC4AMABpAGYAbwBuAHQARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkQECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4BHwEgASEBIgEjASQBJQEmAScBKAEpASoBKwEsAS0BLgEvATABMQEyATMBNAE1ATYBNwE4ATkBOgE7ATwBPQE+AT8BQAFBAUIBQwFEAUUBRgFHAUgBSQFKAUsBTAFNAU4BTwFQAVEBUgFTAVQBVQFWAVcBWAFZAVoBWwFcAV0BXgFfAWABYQFiAWMBZAFlAWYBZwFoAWkBagFrAWwBbQFuAW8BcAFxAXIBcwF0AXUBdgF3AXgBeQF6AXsBfAF9AX4BfwGAAYEBggGDAYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAAlkYXNoYm9hcmQEdXNlcgV1c2VycwJvawZjYW5jZWwEcGx1cwVtaW51cwxmb2xkZXItZW1wdHkIZG93bmxvYWQGdXBsb2FkA2dpdAVjdWJlcwhkYXRhYmFzZQVnYXVnZQdzaXRlbWFwDHNvcnQtbmFtZS11cA5zb3J0LW5hbWUtZG93bgltZWdhcGhvbmUDYnVnBXRhc2tzBmZpbHRlcgNvZmYEYm9vawVwYXN0ZQhzY2lzc29ycwVnbG9iZQVjbG91ZAVmbGFzaAhiYXJjaGFydAhkb3duLWRpcgZ1cC1kaXIIbGVmdC1kaXIJcmlnaHQtZGlyCWRvd24tb3BlbgpyaWdodC1vcGVuB3VwLW9wZW4JbGVmdC1vcGVuBnVwLWJpZwlyaWdodC1iaWcIbGVmdC1iaWcIZG93bi1iaWcPcmVzaXplLWZ1bGwtYWx0C3Jlc2l6ZS1mdWxsDHJlc2l6ZS1zbWFsbARtb3ZlEXJlc2l6ZS1ob3Jpem9udGFsD3Jlc2l6ZS12ZXJ0aWNhbAd6b29tLWluBWJsb2NrCHpvb20tb3V0CWxpZ2h0YnVsYgVjbG9jawl2b2x1bWUtdXALdm9sdW1lLWRvd24Kdm9sdW1lLW9mZgRtdXRlA21pYwdlbmR0aW1lCXN0YXJ0dGltZQ5jYWxlbmRhci1lbXB0eQhjYWxlbmRhcgZ3cmVuY2gHc2xpZGVycwhzZXJ2aWNlcwdzZXJ2aWNlBXBob25lCGZpbGUtcGRmCWZpbGUtd29yZApmaWxlLWV4Y2VsCGRvYy10ZXh0BXRyYXNoDWNvbW1lbnQtZW1wdHkHY29tbWVudARjaGF0CmNoYXQtZW1wdHkEYmVsbAhiZWxsLWFsdA1hdHRlbnRpb24tYWx0BXByaW50BGVkaXQHZm9yd2FyZAVyZXBseQlyZXBseS1hbGwDZXllA3RhZwR0YWdzDWxvY2stb3Blbi1hbHQJbG9jay1vcGVuBGxvY2sEaG9tZQRpbmZvBGhlbHAGc2VhcmNoCGZsYXBwaW5nBnJld2luZApjaGFydC1saW5lCGJlbGwtb2ZmDmJlbGwtb2ZmLWVtcHR5BHBsdWcHZXllLW9mZglhcnJvd3MtY3cCY3cEaG9zdAl0aHVtYnMtdXALdGh1bWJzLWRvd24Hc3Bpbm5lcgZhdHRhY2gIa2V5Ym9hcmQEbWVudQR3aWZpBG1vb24JY2hhcnQtcGllCmNoYXJ0LWFyZWEJY2hhcnQtYmFyBmJlYWtlcgVtYWdpYwVzcGluNgpkb3duLXNtYWxsCmxlZnQtc21hbGwLcmlnaHQtc21hbGwIdXAtc21hbGwDcGluEWFuZ2xlLWRvdWJsZS1sZWZ0EmFuZ2xlLWRvdWJsZS1yaWdodAZjaXJjbGUMaW5mby1jaXJjbGVkB3R3aXR0ZXIQZmFjZWJvb2stc3F1YXJlZA1ncGx1cy1zcXVhcmVkEWF0dGVudGlvbi1jaXJjbGVkBWNoZWNrCnJlc2NoZWR1bGUNd2FybmluZy1lbXB0eQd0aC1saXN0DnRoLXRodW1iLWVtcHR5DmdpdGh1Yi1jaXJjbGVkD2FuZ2xlLWRvdWJsZS11cBFhbmdsZS1kb3VibGUtZG93bgphbmdsZS1sZWZ0C2FuZ2xlLXJp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') format('truetype');
+}
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/*
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'ifont';
+ src: url('../font/ifont.svg?21447335#ifont') format('svg');
+ }
+}
+*/
+
+ [class^="icon-"]:before, [class*=" icon-"]:before {
+ font-family: "ifont";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+}
+.icon-dashboard:before { content: '\e800'; } /* '' */
+.icon-user:before { content: '\e801'; } /* '' */
+.icon-users:before { content: '\e802'; } /* '' */
+.icon-ok:before { content: '\e803'; } /* '' */
+.icon-cancel:before { content: '\e804'; } /* '' */
+.icon-plus:before { content: '\e805'; } /* '' */
+.icon-minus:before { content: '\e806'; } /* '' */
+.icon-folder-empty:before { content: '\e807'; } /* '' */
+.icon-download:before { content: '\e808'; } /* '' */
+.icon-upload:before { content: '\e809'; } /* '' */
+.icon-git:before { content: '\e80a'; } /* '' */
+.icon-cubes:before { content: '\e80b'; } /* '' */
+.icon-database:before { content: '\e80c'; } /* '' */
+.icon-gauge:before { content: '\e80d'; } /* '' */
+.icon-sitemap:before { content: '\e80e'; } /* '' */
+.icon-sort-name-up:before { content: '\e80f'; } /* '' */
+.icon-sort-name-down:before { content: '\e810'; } /* '' */
+.icon-megaphone:before { content: '\e811'; } /* '' */
+.icon-bug:before { content: '\e812'; } /* '' */
+.icon-tasks:before { content: '\e813'; } /* '' */
+.icon-filter:before { content: '\e814'; } /* '' */
+.icon-off:before { content: '\e815'; } /* '' */
+.icon-book:before { content: '\e816'; } /* '' */
+.icon-paste:before { content: '\e817'; } /* '' */
+.icon-scissors:before { content: '\e818'; } /* '' */
+.icon-globe:before { content: '\e819'; } /* '' */
+.icon-cloud:before { content: '\e81a'; } /* '' */
+.icon-flash:before { content: '\e81b'; } /* '' */
+.icon-barchart:before { content: '\e81c'; } /* '' */
+.icon-down-dir:before { content: '\e81d'; } /* '' */
+.icon-up-dir:before { content: '\e81e'; } /* '' */
+.icon-left-dir:before { content: '\e81f'; } /* '' */
+.icon-right-dir:before { content: '\e820'; } /* '' */
+.icon-down-open:before { content: '\e821'; } /* '' */
+.icon-right-open:before { content: '\e822'; } /* '' */
+.icon-up-open:before { content: '\e823'; } /* '' */
+.icon-left-open:before { content: '\e824'; } /* '' */
+.icon-up-big:before { content: '\e825'; } /* '' */
+.icon-right-big:before { content: '\e826'; } /* '' */
+.icon-left-big:before { content: '\e827'; } /* '' */
+.icon-down-big:before { content: '\e828'; } /* '' */
+.icon-resize-full-alt:before { content: '\e829'; } /* '' */
+.icon-resize-full:before { content: '\e82a'; } /* '' */
+.icon-resize-small:before { content: '\e82b'; } /* '' */
+.icon-move:before { content: '\e82c'; } /* '' */
+.icon-resize-horizontal:before { content: '\e82d'; } /* '' */
+.icon-resize-vertical:before { content: '\e82e'; } /* '' */
+.icon-zoom-in:before { content: '\e82f'; } /* '' */
+.icon-block:before { content: '\e830'; } /* '' */
+.icon-zoom-out:before { content: '\e831'; } /* '' */
+.icon-lightbulb:before { content: '\e832'; } /* '' */
+.icon-clock:before { content: '\e833'; } /* '' */
+.icon-volume-up:before { content: '\e834'; } /* '' */
+.icon-volume-down:before { content: '\e835'; } /* '' */
+.icon-volume-off:before { content: '\e836'; } /* '' */
+.icon-mute:before { content: '\e837'; } /* '' */
+.icon-mic:before { content: '\e838'; } /* '' */
+.icon-endtime:before { content: '\e839'; } /* '' */
+.icon-starttime:before { content: '\e83a'; } /* '' */
+.icon-calendar-empty:before { content: '\e83b'; } /* '' */
+.icon-calendar:before { content: '\e83c'; } /* '' */
+.icon-wrench:before { content: '\e83d'; } /* '' */
+.icon-sliders:before { content: '\e83e'; } /* '' */
+.icon-services:before { content: '\e83f'; } /* '' */
+.icon-service:before { content: '\e840'; } /* '' */
+.icon-phone:before { content: '\e841'; } /* '' */
+.icon-file-pdf:before { content: '\e842'; } /* '' */
+.icon-file-word:before { content: '\e843'; } /* '' */
+.icon-file-excel:before { content: '\e844'; } /* '' */
+.icon-doc-text:before { content: '\e845'; } /* '' */
+.icon-trash:before { content: '\e846'; } /* '' */
+.icon-comment-empty:before { content: '\e847'; } /* '' */
+.icon-comment:before { content: '\e848'; } /* '' */
+.icon-chat:before { content: '\e849'; } /* '' */
+.icon-chat-empty:before { content: '\e84a'; } /* '' */
+.icon-bell:before { content: '\e84b'; } /* '' */
+.icon-bell-alt:before { content: '\e84c'; } /* '' */
+.icon-attention-alt:before { content: '\e84d'; } /* '' */
+.icon-print:before { content: '\e84e'; } /* '' */
+.icon-edit:before { content: '\e84f'; } /* '' */
+.icon-forward:before { content: '\e850'; } /* '' */
+.icon-reply:before { content: '\e851'; } /* '' */
+.icon-reply-all:before { content: '\e852'; } /* '' */
+.icon-eye:before { content: '\e853'; } /* '' */
+.icon-tag:before { content: '\e854'; } /* '' */
+.icon-tags:before { content: '\e855'; } /* '' */
+.icon-lock-open-alt:before { content: '\e856'; } /* '' */
+.icon-lock-open:before { content: '\e857'; } /* '' */
+.icon-lock:before { content: '\e858'; } /* '' */
+.icon-home:before { content: '\e859'; } /* '' */
+.icon-info:before { content: '\e85a'; } /* '' */
+.icon-help:before { content: '\e85b'; } /* '' */
+.icon-search:before { content: '\e85c'; } /* '' */
+.icon-flapping:before { content: '\e85d'; } /* '' */
+.icon-rewind:before { content: '\e85e'; } /* '' */
+.icon-chart-line:before { content: '\e85f'; } /* '' */
+.icon-bell-off:before { content: '\e860'; } /* '' */
+.icon-bell-off-empty:before { content: '\e861'; } /* '' */
+.icon-plug:before { content: '\e862'; } /* '' */
+.icon-eye-off:before { content: '\e863'; } /* '' */
+.icon-arrows-cw:before { content: '\e864'; } /* '' */
+.icon-cw:before { content: '\e865'; } /* '' */
+.icon-host:before { content: '\e866'; } /* '' */
+.icon-thumbs-up:before { content: '\e867'; } /* '' */
+.icon-thumbs-down:before { content: '\e868'; } /* '' */
+.icon-spinner:before { content: '\e869'; } /* '' */
+.icon-attach:before { content: '\e86a'; } /* '' */
+.icon-keyboard:before { content: '\e86b'; } /* '' */
+.icon-menu:before { content: '\e86c'; } /* '' */
+.icon-wifi:before { content: '\e86d'; } /* '' */
+.icon-moon:before { content: '\e86e'; } /* '' */
+.icon-chart-pie:before { content: '\e86f'; } /* '' */
+.icon-chart-area:before { content: '\e870'; } /* '' */
+.icon-chart-bar:before { content: '\e871'; } /* '' */
+.icon-beaker:before { content: '\e872'; } /* '' */
+.icon-magic:before { content: '\e873'; } /* '' */
+.icon-spin6:before { content: '\e874'; } /* '' */
+.icon-down-small:before { content: '\e875'; } /* '' */
+.icon-left-small:before { content: '\e876'; } /* '' */
+.icon-right-small:before { content: '\e877'; } /* '' */
+.icon-up-small:before { content: '\e878'; } /* '' */
+.icon-pin:before { content: '\e879'; } /* '' */
+.icon-angle-double-left:before { content: '\e87a'; } /* '' */
+.icon-angle-double-right:before { content: '\e87b'; } /* '' */
+.icon-circle:before { content: '\e87c'; } /* '' */
+.icon-info-circled:before { content: '\e87d'; } /* '' */
+.icon-twitter:before { content: '\e87e'; } /* '' */
+.icon-facebook-squared:before { content: '\e87f'; } /* '' */
+.icon-gplus-squared:before { content: '\e880'; } /* '' */
+.icon-attention-circled:before { content: '\e881'; } /* '' */
+.icon-check:before { content: '\e883'; } /* '' */
+.icon-reschedule:before { content: '\e884'; } /* '' */
+.icon-warning-empty:before { content: '\e885'; } /* '' */
+.icon-th-list:before { content: '\f009'; } /* '' */
+.icon-th-thumb-empty:before { content: '\f00b'; } /* '' */
+.icon-github-circled:before { content: '\f09b'; } /* '' */
+.icon-angle-double-up:before { content: '\f102'; } /* '' */
+.icon-angle-double-down:before { content: '\f103'; } /* '' */
+.icon-angle-left:before { content: '\f104'; } /* '' */
+.icon-angle-right:before { content: '\f105'; } /* '' */
+.icon-angle-up:before { content: '\f106'; } /* '' */
+.icon-angle-down:before { content: '\f107'; } /* '' */
+.icon-history:before { content: '\f1da'; } /* '' */
+.icon-binoculars:before { content: '\f1e5'; } /* '' */ \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/ifont-ie7-codes.css b/application/fonts/fontello-ifont/css/ifont-ie7-codes.css
new file mode 100644
index 0000000..df7974b
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont-ie7-codes.css
@@ -0,0 +1,145 @@
+
+.icon-dashboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
+.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
+.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
+.icon-ok { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
+.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
+.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
+.icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
+.icon-folder-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
+.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
+.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
+.icon-git { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
+.icon-cubes { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
+.icon-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
+.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
+.icon-sitemap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
+.icon-sort-name-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
+.icon-sort-name-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
+.icon-megaphone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
+.icon-bug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
+.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
+.icon-filter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
+.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
+.icon-book { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
+.icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
+.icon-scissors { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
+.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
+.icon-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
+.icon-flash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
+.icon-barchart { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81c;&nbsp;'); }
+.icon-down-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81d;&nbsp;'); }
+.icon-up-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81e;&nbsp;'); }
+.icon-left-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81f;&nbsp;'); }
+.icon-right-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe820;&nbsp;'); }
+.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe821;&nbsp;'); }
+.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe822;&nbsp;'); }
+.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe823;&nbsp;'); }
+.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe824;&nbsp;'); }
+.icon-up-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe825;&nbsp;'); }
+.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe826;&nbsp;'); }
+.icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe827;&nbsp;'); }
+.icon-down-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe828;&nbsp;'); }
+.icon-resize-full-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe829;&nbsp;'); }
+.icon-resize-full { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82a;&nbsp;'); }
+.icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82b;&nbsp;'); }
+.icon-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82c;&nbsp;'); }
+.icon-resize-horizontal { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82d;&nbsp;'); }
+.icon-resize-vertical { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82e;&nbsp;'); }
+.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82f;&nbsp;'); }
+.icon-block { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe830;&nbsp;'); }
+.icon-zoom-out { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe831;&nbsp;'); }
+.icon-lightbulb { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
+.icon-clock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe833;&nbsp;'); }
+.icon-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
+.icon-volume-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe835;&nbsp;'); }
+.icon-volume-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe836;&nbsp;'); }
+.icon-mute { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe837;&nbsp;'); }
+.icon-mic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe838;&nbsp;'); }
+.icon-endtime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe839;&nbsp;'); }
+.icon-starttime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83a;&nbsp;'); }
+.icon-calendar-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83b;&nbsp;'); }
+.icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83c;&nbsp;'); }
+.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83d;&nbsp;'); }
+.icon-sliders { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83e;&nbsp;'); }
+.icon-services { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83f;&nbsp;'); }
+.icon-service { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe840;&nbsp;'); }
+.icon-phone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe841;&nbsp;'); }
+.icon-file-pdf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe842;&nbsp;'); }
+.icon-file-word { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe843;&nbsp;'); }
+.icon-file-excel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe844;&nbsp;'); }
+.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe845;&nbsp;'); }
+.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe846;&nbsp;'); }
+.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe847;&nbsp;'); }
+.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe848;&nbsp;'); }
+.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe849;&nbsp;'); }
+.icon-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
+.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
+.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
+.icon-attention-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84d;&nbsp;'); }
+.icon-print { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84e;&nbsp;'); }
+.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84f;&nbsp;'); }
+.icon-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe850;&nbsp;'); }
+.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe851;&nbsp;'); }
+.icon-reply-all { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe852;&nbsp;'); }
+.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe853;&nbsp;'); }
+.icon-tag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe854;&nbsp;'); }
+.icon-tags { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe855;&nbsp;'); }
+.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe856;&nbsp;'); }
+.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe857;&nbsp;'); }
+.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe858;&nbsp;'); }
+.icon-home { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe859;&nbsp;'); }
+.icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85a;&nbsp;'); }
+.icon-help { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85b;&nbsp;'); }
+.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85c;&nbsp;'); }
+.icon-flapping { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85d;&nbsp;'); }
+.icon-rewind { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85e;&nbsp;'); }
+.icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85f;&nbsp;'); }
+.icon-bell-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe860;&nbsp;'); }
+.icon-bell-off-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe861;&nbsp;'); }
+.icon-plug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe862;&nbsp;'); }
+.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe863;&nbsp;'); }
+.icon-arrows-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe864;&nbsp;'); }
+.icon-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe865;&nbsp;'); }
+.icon-host { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe866;&nbsp;'); }
+.icon-thumbs-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe867;&nbsp;'); }
+.icon-thumbs-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe868;&nbsp;'); }
+.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe869;&nbsp;'); }
+.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86a;&nbsp;'); }
+.icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86b;&nbsp;'); }
+.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86c;&nbsp;'); }
+.icon-wifi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86d;&nbsp;'); }
+.icon-moon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86e;&nbsp;'); }
+.icon-chart-pie { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86f;&nbsp;'); }
+.icon-chart-area { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe870;&nbsp;'); }
+.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe871;&nbsp;'); }
+.icon-beaker { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe872;&nbsp;'); }
+.icon-magic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe873;&nbsp;'); }
+.icon-spin6 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe874;&nbsp;'); }
+.icon-down-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe875;&nbsp;'); }
+.icon-left-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe876;&nbsp;'); }
+.icon-right-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe877;&nbsp;'); }
+.icon-up-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe878;&nbsp;'); }
+.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe879;&nbsp;'); }
+.icon-angle-double-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87a;&nbsp;'); }
+.icon-angle-double-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87b;&nbsp;'); }
+.icon-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87c;&nbsp;'); }
+.icon-info-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87d;&nbsp;'); }
+.icon-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87e;&nbsp;'); }
+.icon-facebook-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87f;&nbsp;'); }
+.icon-gplus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe880;&nbsp;'); }
+.icon-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe881;&nbsp;'); }
+.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe883;&nbsp;'); }
+.icon-reschedule { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe884;&nbsp;'); }
+.icon-warning-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe885;&nbsp;'); }
+.icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf009;&nbsp;'); }
+.icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf00b;&nbsp;'); }
+.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); }
+.icon-angle-double-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf102;&nbsp;'); }
+.icon-angle-double-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf103;&nbsp;'); }
+.icon-angle-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf104;&nbsp;'); }
+.icon-angle-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf105;&nbsp;'); }
+.icon-angle-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf106;&nbsp;'); }
+.icon-angle-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf107;&nbsp;'); }
+.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1da;&nbsp;'); }
+.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); } \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/ifont-ie7.css b/application/fonts/fontello-ifont/css/ifont-ie7.css
new file mode 100644
index 0000000..084c292
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont-ie7.css
@@ -0,0 +1,156 @@
+[class^="icon-"], [class*=" icon-"] {
+ font-family: 'ifont';
+ font-style: normal;
+ font-weight: normal;
+
+ /* fix buttons height */
+ line-height: 1em;
+
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+}
+
+.icon-dashboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
+.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
+.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
+.icon-ok { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
+.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
+.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
+.icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
+.icon-folder-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
+.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
+.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
+.icon-git { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
+.icon-cubes { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
+.icon-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
+.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
+.icon-sitemap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
+.icon-sort-name-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
+.icon-sort-name-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
+.icon-megaphone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
+.icon-bug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
+.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
+.icon-filter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
+.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
+.icon-book { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
+.icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
+.icon-scissors { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
+.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
+.icon-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
+.icon-flash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
+.icon-barchart { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81c;&nbsp;'); }
+.icon-down-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81d;&nbsp;'); }
+.icon-up-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81e;&nbsp;'); }
+.icon-left-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81f;&nbsp;'); }
+.icon-right-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe820;&nbsp;'); }
+.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe821;&nbsp;'); }
+.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe822;&nbsp;'); }
+.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe823;&nbsp;'); }
+.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe824;&nbsp;'); }
+.icon-up-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe825;&nbsp;'); }
+.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe826;&nbsp;'); }
+.icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe827;&nbsp;'); }
+.icon-down-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe828;&nbsp;'); }
+.icon-resize-full-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe829;&nbsp;'); }
+.icon-resize-full { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82a;&nbsp;'); }
+.icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82b;&nbsp;'); }
+.icon-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82c;&nbsp;'); }
+.icon-resize-horizontal { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82d;&nbsp;'); }
+.icon-resize-vertical { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82e;&nbsp;'); }
+.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82f;&nbsp;'); }
+.icon-block { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe830;&nbsp;'); }
+.icon-zoom-out { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe831;&nbsp;'); }
+.icon-lightbulb { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
+.icon-clock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe833;&nbsp;'); }
+.icon-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
+.icon-volume-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe835;&nbsp;'); }
+.icon-volume-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe836;&nbsp;'); }
+.icon-mute { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe837;&nbsp;'); }
+.icon-mic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe838;&nbsp;'); }
+.icon-endtime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe839;&nbsp;'); }
+.icon-starttime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83a;&nbsp;'); }
+.icon-calendar-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83b;&nbsp;'); }
+.icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83c;&nbsp;'); }
+.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83d;&nbsp;'); }
+.icon-sliders { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83e;&nbsp;'); }
+.icon-services { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83f;&nbsp;'); }
+.icon-service { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe840;&nbsp;'); }
+.icon-phone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe841;&nbsp;'); }
+.icon-file-pdf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe842;&nbsp;'); }
+.icon-file-word { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe843;&nbsp;'); }
+.icon-file-excel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe844;&nbsp;'); }
+.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe845;&nbsp;'); }
+.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe846;&nbsp;'); }
+.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe847;&nbsp;'); }
+.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe848;&nbsp;'); }
+.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe849;&nbsp;'); }
+.icon-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
+.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
+.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
+.icon-attention-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84d;&nbsp;'); }
+.icon-print { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84e;&nbsp;'); }
+.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84f;&nbsp;'); }
+.icon-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe850;&nbsp;'); }
+.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe851;&nbsp;'); }
+.icon-reply-all { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe852;&nbsp;'); }
+.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe853;&nbsp;'); }
+.icon-tag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe854;&nbsp;'); }
+.icon-tags { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe855;&nbsp;'); }
+.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe856;&nbsp;'); }
+.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe857;&nbsp;'); }
+.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe858;&nbsp;'); }
+.icon-home { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe859;&nbsp;'); }
+.icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85a;&nbsp;'); }
+.icon-help { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85b;&nbsp;'); }
+.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85c;&nbsp;'); }
+.icon-flapping { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85d;&nbsp;'); }
+.icon-rewind { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85e;&nbsp;'); }
+.icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85f;&nbsp;'); }
+.icon-bell-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe860;&nbsp;'); }
+.icon-bell-off-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe861;&nbsp;'); }
+.icon-plug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe862;&nbsp;'); }
+.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe863;&nbsp;'); }
+.icon-arrows-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe864;&nbsp;'); }
+.icon-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe865;&nbsp;'); }
+.icon-host { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe866;&nbsp;'); }
+.icon-thumbs-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe867;&nbsp;'); }
+.icon-thumbs-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe868;&nbsp;'); }
+.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe869;&nbsp;'); }
+.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86a;&nbsp;'); }
+.icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86b;&nbsp;'); }
+.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86c;&nbsp;'); }
+.icon-wifi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86d;&nbsp;'); }
+.icon-moon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86e;&nbsp;'); }
+.icon-chart-pie { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86f;&nbsp;'); }
+.icon-chart-area { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe870;&nbsp;'); }
+.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe871;&nbsp;'); }
+.icon-beaker { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe872;&nbsp;'); }
+.icon-magic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe873;&nbsp;'); }
+.icon-spin6 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe874;&nbsp;'); }
+.icon-down-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe875;&nbsp;'); }
+.icon-left-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe876;&nbsp;'); }
+.icon-right-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe877;&nbsp;'); }
+.icon-up-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe878;&nbsp;'); }
+.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe879;&nbsp;'); }
+.icon-angle-double-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87a;&nbsp;'); }
+.icon-angle-double-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87b;&nbsp;'); }
+.icon-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87c;&nbsp;'); }
+.icon-info-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87d;&nbsp;'); }
+.icon-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87e;&nbsp;'); }
+.icon-facebook-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87f;&nbsp;'); }
+.icon-gplus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe880;&nbsp;'); }
+.icon-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe881;&nbsp;'); }
+.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe883;&nbsp;'); }
+.icon-reschedule { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe884;&nbsp;'); }
+.icon-warning-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe885;&nbsp;'); }
+.icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf009;&nbsp;'); }
+.icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf00b;&nbsp;'); }
+.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); }
+.icon-angle-double-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf102;&nbsp;'); }
+.icon-angle-double-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf103;&nbsp;'); }
+.icon-angle-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf104;&nbsp;'); }
+.icon-angle-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf105;&nbsp;'); }
+.icon-angle-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf106;&nbsp;'); }
+.icon-angle-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf107;&nbsp;'); }
+.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1da;&nbsp;'); }
+.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); } \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/ifont.css b/application/fonts/fontello-ifont/css/ifont.css
new file mode 100644
index 0000000..afabb48
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont.css
@@ -0,0 +1,201 @@
+@font-face {
+ font-family: 'ifont';
+ src: url('../font/ifont.eot?95568481');
+ src: url('../font/ifont.eot?95568481#iefix') format('embedded-opentype'),
+ url('../font/ifont.woff2?95568481') format('woff2'),
+ url('../font/ifont.woff?95568481') format('woff'),
+ url('../font/ifont.ttf?95568481') format('truetype'),
+ url('../font/ifont.svg?95568481#ifont') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/*
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'ifont';
+ src: url('../font/ifont.svg?95568481#ifont') format('svg');
+ }
+}
+*/
+
+ [class^="icon-"]:before, [class*=" icon-"]:before {
+ font-family: "ifont";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+
+ /* Font smoothing. That was taken from TWBS */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+}
+
+.icon-dashboard:before { content: '\e800'; } /* '' */
+.icon-user:before { content: '\e801'; } /* '' */
+.icon-users:before { content: '\e802'; } /* '' */
+.icon-ok:before { content: '\e803'; } /* '' */
+.icon-cancel:before { content: '\e804'; } /* '' */
+.icon-plus:before { content: '\e805'; } /* '' */
+.icon-minus:before { content: '\e806'; } /* '' */
+.icon-folder-empty:before { content: '\e807'; } /* '' */
+.icon-download:before { content: '\e808'; } /* '' */
+.icon-upload:before { content: '\e809'; } /* '' */
+.icon-git:before { content: '\e80a'; } /* '' */
+.icon-cubes:before { content: '\e80b'; } /* '' */
+.icon-database:before { content: '\e80c'; } /* '' */
+.icon-gauge:before { content: '\e80d'; } /* '' */
+.icon-sitemap:before { content: '\e80e'; } /* '' */
+.icon-sort-name-up:before { content: '\e80f'; } /* '' */
+.icon-sort-name-down:before { content: '\e810'; } /* '' */
+.icon-megaphone:before { content: '\e811'; } /* '' */
+.icon-bug:before { content: '\e812'; } /* '' */
+.icon-tasks:before { content: '\e813'; } /* '' */
+.icon-filter:before { content: '\e814'; } /* '' */
+.icon-off:before { content: '\e815'; } /* '' */
+.icon-book:before { content: '\e816'; } /* '' */
+.icon-paste:before { content: '\e817'; } /* '' */
+.icon-scissors:before { content: '\e818'; } /* '' */
+.icon-globe:before { content: '\e819'; } /* '' */
+.icon-cloud:before { content: '\e81a'; } /* '' */
+.icon-flash:before { content: '\e81b'; } /* '' */
+.icon-barchart:before { content: '\e81c'; } /* '' */
+.icon-down-dir:before { content: '\e81d'; } /* '' */
+.icon-up-dir:before { content: '\e81e'; } /* '' */
+.icon-left-dir:before { content: '\e81f'; } /* '' */
+.icon-right-dir:before { content: '\e820'; } /* '' */
+.icon-down-open:before { content: '\e821'; } /* '' */
+.icon-right-open:before { content: '\e822'; } /* '' */
+.icon-up-open:before { content: '\e823'; } /* '' */
+.icon-left-open:before { content: '\e824'; } /* '' */
+.icon-up-big:before { content: '\e825'; } /* '' */
+.icon-right-big:before { content: '\e826'; } /* '' */
+.icon-left-big:before { content: '\e827'; } /* '' */
+.icon-down-big:before { content: '\e828'; } /* '' */
+.icon-resize-full-alt:before { content: '\e829'; } /* '' */
+.icon-resize-full:before { content: '\e82a'; } /* '' */
+.icon-resize-small:before { content: '\e82b'; } /* '' */
+.icon-move:before { content: '\e82c'; } /* '' */
+.icon-resize-horizontal:before { content: '\e82d'; } /* '' */
+.icon-resize-vertical:before { content: '\e82e'; } /* '' */
+.icon-zoom-in:before { content: '\e82f'; } /* '' */
+.icon-block:before { content: '\e830'; } /* '' */
+.icon-zoom-out:before { content: '\e831'; } /* '' */
+.icon-lightbulb:before { content: '\e832'; } /* '' */
+.icon-clock:before { content: '\e833'; } /* '' */
+.icon-volume-up:before { content: '\e834'; } /* '' */
+.icon-volume-down:before { content: '\e835'; } /* '' */
+.icon-volume-off:before { content: '\e836'; } /* '' */
+.icon-mute:before { content: '\e837'; } /* '' */
+.icon-mic:before { content: '\e838'; } /* '' */
+.icon-endtime:before { content: '\e839'; } /* '' */
+.icon-starttime:before { content: '\e83a'; } /* '' */
+.icon-calendar-empty:before { content: '\e83b'; } /* '' */
+.icon-calendar:before { content: '\e83c'; } /* '' */
+.icon-wrench:before { content: '\e83d'; } /* '' */
+.icon-sliders:before { content: '\e83e'; } /* '' */
+.icon-services:before { content: '\e83f'; } /* '' */
+.icon-service:before { content: '\e840'; } /* '' */
+.icon-phone:before { content: '\e841'; } /* '' */
+.icon-file-pdf:before { content: '\e842'; } /* '' */
+.icon-file-word:before { content: '\e843'; } /* '' */
+.icon-file-excel:before { content: '\e844'; } /* '' */
+.icon-doc-text:before { content: '\e845'; } /* '' */
+.icon-trash:before { content: '\e846'; } /* '' */
+.icon-comment-empty:before { content: '\e847'; } /* '' */
+.icon-comment:before { content: '\e848'; } /* '' */
+.icon-chat:before { content: '\e849'; } /* '' */
+.icon-chat-empty:before { content: '\e84a'; } /* '' */
+.icon-bell:before { content: '\e84b'; } /* '' */
+.icon-bell-alt:before { content: '\e84c'; } /* '' */
+.icon-attention-alt:before { content: '\e84d'; } /* '' */
+.icon-print:before { content: '\e84e'; } /* '' */
+.icon-edit:before { content: '\e84f'; } /* '' */
+.icon-forward:before { content: '\e850'; } /* '' */
+.icon-reply:before { content: '\e851'; } /* '' */
+.icon-reply-all:before { content: '\e852'; } /* '' */
+.icon-eye:before { content: '\e853'; } /* '' */
+.icon-tag:before { content: '\e854'; } /* '' */
+.icon-tags:before { content: '\e855'; } /* '' */
+.icon-lock-open-alt:before { content: '\e856'; } /* '' */
+.icon-lock-open:before { content: '\e857'; } /* '' */
+.icon-lock:before { content: '\e858'; } /* '' */
+.icon-home:before { content: '\e859'; } /* '' */
+.icon-info:before { content: '\e85a'; } /* '' */
+.icon-help:before { content: '\e85b'; } /* '' */
+.icon-search:before { content: '\e85c'; } /* '' */
+.icon-flapping:before { content: '\e85d'; } /* '' */
+.icon-rewind:before { content: '\e85e'; } /* '' */
+.icon-chart-line:before { content: '\e85f'; } /* '' */
+.icon-bell-off:before { content: '\e860'; } /* '' */
+.icon-bell-off-empty:before { content: '\e861'; } /* '' */
+.icon-plug:before { content: '\e862'; } /* '' */
+.icon-eye-off:before { content: '\e863'; } /* '' */
+.icon-arrows-cw:before { content: '\e864'; } /* '' */
+.icon-cw:before { content: '\e865'; } /* '' */
+.icon-host:before { content: '\e866'; } /* '' */
+.icon-thumbs-up:before { content: '\e867'; } /* '' */
+.icon-thumbs-down:before { content: '\e868'; } /* '' */
+.icon-spinner:before { content: '\e869'; } /* '' */
+.icon-attach:before { content: '\e86a'; } /* '' */
+.icon-keyboard:before { content: '\e86b'; } /* '' */
+.icon-menu:before { content: '\e86c'; } /* '' */
+.icon-wifi:before { content: '\e86d'; } /* '' */
+.icon-moon:before { content: '\e86e'; } /* '' */
+.icon-chart-pie:before { content: '\e86f'; } /* '' */
+.icon-chart-area:before { content: '\e870'; } /* '' */
+.icon-chart-bar:before { content: '\e871'; } /* '' */
+.icon-beaker:before { content: '\e872'; } /* '' */
+.icon-magic:before { content: '\e873'; } /* '' */
+.icon-spin6:before { content: '\e874'; } /* '' */
+.icon-down-small:before { content: '\e875'; } /* '' */
+.icon-left-small:before { content: '\e876'; } /* '' */
+.icon-right-small:before { content: '\e877'; } /* '' */
+.icon-up-small:before { content: '\e878'; } /* '' */
+.icon-pin:before { content: '\e879'; } /* '' */
+.icon-angle-double-left:before { content: '\e87a'; } /* '' */
+.icon-angle-double-right:before { content: '\e87b'; } /* '' */
+.icon-circle:before { content: '\e87c'; } /* '' */
+.icon-info-circled:before { content: '\e87d'; } /* '' */
+.icon-twitter:before { content: '\e87e'; } /* '' */
+.icon-facebook-squared:before { content: '\e87f'; } /* '' */
+.icon-gplus-squared:before { content: '\e880'; } /* '' */
+.icon-attention-circled:before { content: '\e881'; } /* '' */
+.icon-check:before { content: '\e883'; } /* '' */
+.icon-reschedule:before { content: '\e884'; } /* '' */
+.icon-warning-empty:before { content: '\e885'; } /* '' */
+.icon-th-list:before { content: '\f009'; } /* '' */
+.icon-th-thumb-empty:before { content: '\f00b'; } /* '' */
+.icon-github-circled:before { content: '\f09b'; } /* '' */
+.icon-angle-double-up:before { content: '\f102'; } /* '' */
+.icon-angle-double-down:before { content: '\f103'; } /* '' */
+.icon-angle-left:before { content: '\f104'; } /* '' */
+.icon-angle-right:before { content: '\f105'; } /* '' */
+.icon-angle-up:before { content: '\f106'; } /* '' */
+.icon-angle-down:before { content: '\f107'; } /* '' */
+.icon-history:before { content: '\f1da'; } /* '' */
+.icon-binoculars:before { content: '\f1e5'; } /* '' */ \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/demo.html b/application/fonts/fontello-ifont/demo.html
new file mode 100644
index 0000000..c3a67d4
--- /dev/null
+++ b/application/fonts/fontello-ifont/demo.html
@@ -0,0 +1,519 @@
+<!DOCTYPE html>
+<html>
+ <head><!--[if lt IE 9]><script language="javascript" type="text/javascript" src="//html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
+ <meta charset="UTF-8"><style>/*
+ * Bootstrap v2.2.1
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ */
+.clearfix {
+ *zoom: 1;
+}
+.clearfix:before,
+.clearfix:after {
+ display: table;
+ content: "";
+ line-height: 0;
+}
+.clearfix:after {
+ clear: both;
+}
+html {
+ font-size: 100%;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+a:focus {
+ outline: thin dotted #333;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px;
+}
+a:hover,
+a:active {
+ outline: 0;
+}
+button,
+input,
+select,
+textarea {
+ margin: 0;
+ font-size: 100%;
+ vertical-align: middle;
+}
+button,
+input {
+ *overflow: visible;
+ line-height: normal;
+}
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+}
+body {
+ margin: 0;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 20px;
+ color: #333;
+ background-color: #fff;
+}
+a {
+ color: #08c;
+ text-decoration: none;
+}
+a:hover {
+ color: #005580;
+ text-decoration: underline;
+}
+.row {
+ margin-left: -20px;
+ *zoom: 1;
+}
+.row:before,
+.row:after {
+ display: table;
+ content: "";
+ line-height: 0;
+}
+.row:after {
+ clear: both;
+}
+[class*="span"] {
+ float: left;
+ min-height: 1px;
+ margin-left: 20px;
+}
+.container,
+.navbar-static-top .container,
+.navbar-fixed-top .container,
+.navbar-fixed-bottom .container {
+ width: 940px;
+}
+.span12 {
+ width: 940px;
+}
+.span11 {
+ width: 860px;
+}
+.span10 {
+ width: 780px;
+}
+.span9 {
+ width: 700px;
+}
+.span8 {
+ width: 620px;
+}
+.span7 {
+ width: 540px;
+}
+.span6 {
+ width: 460px;
+}
+.span5 {
+ width: 380px;
+}
+.span4 {
+ width: 300px;
+}
+.span3 {
+ width: 220px;
+}
+.span2 {
+ width: 140px;
+}
+.span1 {
+ width: 60px;
+}
+[class*="span"].pull-right,
+.row-fluid [class*="span"].pull-right {
+ float: right;
+}
+.container {
+ margin-right: auto;
+ margin-left: auto;
+ *zoom: 1;
+}
+.container:before,
+.container:after {
+ display: table;
+ content: "";
+ line-height: 0;
+}
+.container:after {
+ clear: both;
+}
+p {
+ margin: 0 0 10px;
+}
+.lead {
+ margin-bottom: 20px;
+ font-size: 21px;
+ font-weight: 200;
+ line-height: 30px;
+}
+small {
+ font-size: 85%;
+}
+h1 {
+ margin: 10px 0;
+ font-family: inherit;
+ font-weight: bold;
+ line-height: 20px;
+ color: inherit;
+ text-rendering: optimizelegibility;
+}
+h1 small {
+ font-weight: normal;
+ line-height: 1;
+ color: #999;
+}
+h1 {
+ line-height: 40px;
+}
+h1 {
+ font-size: 38.5px;
+}
+h1 small {
+ font-size: 24.5px;
+}
+body {
+ margin-top: 90px;
+}
+.header {
+ position: fixed;
+ top: 0;
+ left: 50%;
+ margin-left: -480px;
+ background-color: #fff;
+ border-bottom: 1px solid #ddd;
+ padding-top: 10px;
+ z-index: 10;
+}
+.footer {
+ color: #ddd;
+ font-size: 12px;
+ text-align: center;
+ margin-top: 20px;
+}
+.footer a {
+ color: #ccc;
+ text-decoration: underline;
+}
+.the-icons {
+ font-size: 14px;
+ line-height: 24px;
+}
+.switch {
+ position: absolute;
+ right: 0;
+ bottom: 10px;
+ color: #666;
+}
+.switch input {
+ margin-right: 0.3em;
+}
+.codesOn .i-name {
+ display: none;
+}
+.codesOn .i-code {
+ display: inline;
+}
+.i-code {
+ display: none;
+}
+@font-face {
+ font-family: 'ifont';
+ src: url('./font/ifont.eot?64148803');
+ src: url('./font/ifont.eot?64148803#iefix') format('embedded-opentype'),
+ url('./font/ifont.woff?64148803') format('woff'),
+ url('./font/ifont.ttf?64148803') format('truetype'),
+ url('./font/ifont.svg?64148803#ifont') format('svg');
+ font-weight: normal;
+ font-style: normal;
+ }
+
+
+ .demo-icon
+ {
+ font-family: "ifont";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+
+ /* You can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+
+ /* Font smoothing. That was taken from TWBS */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+ }
+ </style>
+ <link rel="stylesheet" href="css/animation.css"><!--[if IE 7]><link rel="stylesheet" href="css/" + font.fontname + "-ie7.css"><![endif]-->
+ <script>
+ function toggleCodes(on) {
+ var obj = document.getElementById('icons');
+
+ if (on) {
+ obj.className += ' codesOn';
+ } else {
+ obj.className = obj.className.replace(' codesOn', '');
+ }
+ }
+
+ </script>
+ </head>
+ <body>
+ <div class="container header">
+ <h1>ifont <small>font demo</small></h1>
+ <label class="switch">
+ <input type="checkbox" onclick="toggleCodes(this.checked)">show codes
+ </label>
+ </div>
+ <div class="container" id="icons">
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe800"><i class="demo-icon icon-dashboard">&#xe800;</i> <span class="i-name">icon-dashboard</span><span class="i-code">0xe800</span></div>
+ <div class="the-icons span3" title="Code: 0xe801"><i class="demo-icon icon-user">&#xe801;</i> <span class="i-name">icon-user</span><span class="i-code">0xe801</span></div>
+ <div class="the-icons span3" title="Code: 0xe802"><i class="demo-icon icon-users">&#xe802;</i> <span class="i-name">icon-users</span><span class="i-code">0xe802</span></div>
+ <div class="the-icons span3" title="Code: 0xe803"><i class="demo-icon icon-ok">&#xe803;</i> <span class="i-name">icon-ok</span><span class="i-code">0xe803</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe804"><i class="demo-icon icon-cancel">&#xe804;</i> <span class="i-name">icon-cancel</span><span class="i-code">0xe804</span></div>
+ <div class="the-icons span3" title="Code: 0xe805"><i class="demo-icon icon-plus">&#xe805;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe805</span></div>
+ <div class="the-icons span3" title="Code: 0xe806"><i class="demo-icon icon-minus">&#xe806;</i> <span class="i-name">icon-minus</span><span class="i-code">0xe806</span></div>
+ <div class="the-icons span3" title="Code: 0xe807"><i class="demo-icon icon-folder-empty">&#xe807;</i> <span class="i-name">icon-folder-empty</span><span class="i-code">0xe807</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe808"><i class="demo-icon icon-download">&#xe808;</i> <span class="i-name">icon-download</span><span class="i-code">0xe808</span></div>
+ <div class="the-icons span3" title="Code: 0xe809"><i class="demo-icon icon-upload">&#xe809;</i> <span class="i-name">icon-upload</span><span class="i-code">0xe809</span></div>
+ <div class="the-icons span3" title="Code: 0xe80a"><i class="demo-icon icon-git">&#xe80a;</i> <span class="i-name">icon-git</span><span class="i-code">0xe80a</span></div>
+ <div class="the-icons span3" title="Code: 0xe80b"><i class="demo-icon icon-cubes">&#xe80b;</i> <span class="i-name">icon-cubes</span><span class="i-code">0xe80b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe80c"><i class="demo-icon icon-database">&#xe80c;</i> <span class="i-name">icon-database</span><span class="i-code">0xe80c</span></div>
+ <div class="the-icons span3" title="Code: 0xe80d"><i class="demo-icon icon-gauge">&#xe80d;</i> <span class="i-name">icon-gauge</span><span class="i-code">0xe80d</span></div>
+ <div class="the-icons span3" title="Code: 0xe80e"><i class="demo-icon icon-sitemap">&#xe80e;</i> <span class="i-name">icon-sitemap</span><span class="i-code">0xe80e</span></div>
+ <div class="the-icons span3" title="Code: 0xe80f"><i class="demo-icon icon-sort-name-up">&#xe80f;</i> <span class="i-name">icon-sort-name-up</span><span class="i-code">0xe80f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-sort-name-down">&#xe810;</i> <span class="i-name">icon-sort-name-down</span><span class="i-code">0xe810</span></div>
+ <div class="the-icons span3" title="Code: 0xe811"><i class="demo-icon icon-megaphone">&#xe811;</i> <span class="i-name">icon-megaphone</span><span class="i-code">0xe811</span></div>
+ <div class="the-icons span3" title="Code: 0xe812"><i class="demo-icon icon-bug">&#xe812;</i> <span class="i-name">icon-bug</span><span class="i-code">0xe812</span></div>
+ <div class="the-icons span3" title="Code: 0xe813"><i class="demo-icon icon-tasks">&#xe813;</i> <span class="i-name">icon-tasks</span><span class="i-code">0xe813</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-filter">&#xe814;</i> <span class="i-name">icon-filter</span><span class="i-code">0xe814</span></div>
+ <div class="the-icons span3" title="Code: 0xe815"><i class="demo-icon icon-off">&#xe815;</i> <span class="i-name">icon-off</span><span class="i-code">0xe815</span></div>
+ <div class="the-icons span3" title="Code: 0xe816"><i class="demo-icon icon-book">&#xe816;</i> <span class="i-name">icon-book</span><span class="i-code">0xe816</span></div>
+ <div class="the-icons span3" title="Code: 0xe817"><i class="demo-icon icon-paste">&#xe817;</i> <span class="i-name">icon-paste</span><span class="i-code">0xe817</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-scissors">&#xe818;</i> <span class="i-name">icon-scissors</span><span class="i-code">0xe818</span></div>
+ <div class="the-icons span3" title="Code: 0xe819"><i class="demo-icon icon-globe">&#xe819;</i> <span class="i-name">icon-globe</span><span class="i-code">0xe819</span></div>
+ <div class="the-icons span3" title="Code: 0xe81a"><i class="demo-icon icon-cloud">&#xe81a;</i> <span class="i-name">icon-cloud</span><span class="i-code">0xe81a</span></div>
+ <div class="the-icons span3" title="Code: 0xe81b"><i class="demo-icon icon-flash">&#xe81b;</i> <span class="i-name">icon-flash</span><span class="i-code">0xe81b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe81c"><i class="demo-icon icon-barchart">&#xe81c;</i> <span class="i-name">icon-barchart</span><span class="i-code">0xe81c</span></div>
+ <div class="the-icons span3" title="Code: 0xe81d"><i class="demo-icon icon-down-dir">&#xe81d;</i> <span class="i-name">icon-down-dir</span><span class="i-code">0xe81d</span></div>
+ <div class="the-icons span3" title="Code: 0xe81e"><i class="demo-icon icon-up-dir">&#xe81e;</i> <span class="i-name">icon-up-dir</span><span class="i-code">0xe81e</span></div>
+ <div class="the-icons span3" title="Code: 0xe81f"><i class="demo-icon icon-left-dir">&#xe81f;</i> <span class="i-name">icon-left-dir</span><span class="i-code">0xe81f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe820"><i class="demo-icon icon-right-dir">&#xe820;</i> <span class="i-name">icon-right-dir</span><span class="i-code">0xe820</span></div>
+ <div class="the-icons span3" title="Code: 0xe821"><i class="demo-icon icon-down-open">&#xe821;</i> <span class="i-name">icon-down-open</span><span class="i-code">0xe821</span></div>
+ <div class="the-icons span3" title="Code: 0xe822"><i class="demo-icon icon-right-open">&#xe822;</i> <span class="i-name">icon-right-open</span><span class="i-code">0xe822</span></div>
+ <div class="the-icons span3" title="Code: 0xe823"><i class="demo-icon icon-up-open">&#xe823;</i> <span class="i-name">icon-up-open</span><span class="i-code">0xe823</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe824"><i class="demo-icon icon-left-open">&#xe824;</i> <span class="i-name">icon-left-open</span><span class="i-code">0xe824</span></div>
+ <div class="the-icons span3" title="Code: 0xe825"><i class="demo-icon icon-up-big">&#xe825;</i> <span class="i-name">icon-up-big</span><span class="i-code">0xe825</span></div>
+ <div class="the-icons span3" title="Code: 0xe826"><i class="demo-icon icon-right-big">&#xe826;</i> <span class="i-name">icon-right-big</span><span class="i-code">0xe826</span></div>
+ <div class="the-icons span3" title="Code: 0xe827"><i class="demo-icon icon-left-big">&#xe827;</i> <span class="i-name">icon-left-big</span><span class="i-code">0xe827</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe828"><i class="demo-icon icon-down-big">&#xe828;</i> <span class="i-name">icon-down-big</span><span class="i-code">0xe828</span></div>
+ <div class="the-icons span3" title="Code: 0xe829"><i class="demo-icon icon-resize-full-alt">&#xe829;</i> <span class="i-name">icon-resize-full-alt</span><span class="i-code">0xe829</span></div>
+ <div class="the-icons span3" title="Code: 0xe82a"><i class="demo-icon icon-resize-full">&#xe82a;</i> <span class="i-name">icon-resize-full</span><span class="i-code">0xe82a</span></div>
+ <div class="the-icons span3" title="Code: 0xe82b"><i class="demo-icon icon-resize-small">&#xe82b;</i> <span class="i-name">icon-resize-small</span><span class="i-code">0xe82b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe82c"><i class="demo-icon icon-move">&#xe82c;</i> <span class="i-name">icon-move</span><span class="i-code">0xe82c</span></div>
+ <div class="the-icons span3" title="Code: 0xe82d"><i class="demo-icon icon-resize-horizontal">&#xe82d;</i> <span class="i-name">icon-resize-horizontal</span><span class="i-code">0xe82d</span></div>
+ <div class="the-icons span3" title="Code: 0xe82e"><i class="demo-icon icon-resize-vertical">&#xe82e;</i> <span class="i-name">icon-resize-vertical</span><span class="i-code">0xe82e</span></div>
+ <div class="the-icons span3" title="Code: 0xe82f"><i class="demo-icon icon-zoom-in">&#xe82f;</i> <span class="i-name">icon-zoom-in</span><span class="i-code">0xe82f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe830"><i class="demo-icon icon-block">&#xe830;</i> <span class="i-name">icon-block</span><span class="i-code">0xe830</span></div>
+ <div class="the-icons span3" title="Code: 0xe831"><i class="demo-icon icon-zoom-out">&#xe831;</i> <span class="i-name">icon-zoom-out</span><span class="i-code">0xe831</span></div>
+ <div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-lightbulb">&#xe832;</i> <span class="i-name">icon-lightbulb</span><span class="i-code">0xe832</span></div>
+ <div class="the-icons span3" title="Code: 0xe833"><i class="demo-icon icon-clock">&#xe833;</i> <span class="i-name">icon-clock</span><span class="i-code">0xe833</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-volume-up">&#xe834;</i> <span class="i-name">icon-volume-up</span><span class="i-code">0xe834</span></div>
+ <div class="the-icons span3" title="Code: 0xe835"><i class="demo-icon icon-volume-down">&#xe835;</i> <span class="i-name">icon-volume-down</span><span class="i-code">0xe835</span></div>
+ <div class="the-icons span3" title="Code: 0xe836"><i class="demo-icon icon-volume-off">&#xe836;</i> <span class="i-name">icon-volume-off</span><span class="i-code">0xe836</span></div>
+ <div class="the-icons span3" title="Code: 0xe837"><i class="demo-icon icon-mute">&#xe837;</i> <span class="i-name">icon-mute</span><span class="i-code">0xe837</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe838"><i class="demo-icon icon-mic">&#xe838;</i> <span class="i-name">icon-mic</span><span class="i-code">0xe838</span></div>
+ <div class="the-icons span3" title="Code: 0xe839"><i class="demo-icon icon-endtime">&#xe839;</i> <span class="i-name">icon-endtime</span><span class="i-code">0xe839</span></div>
+ <div class="the-icons span3" title="Code: 0xe83a"><i class="demo-icon icon-starttime">&#xe83a;</i> <span class="i-name">icon-starttime</span><span class="i-code">0xe83a</span></div>
+ <div class="the-icons span3" title="Code: 0xe83b"><i class="demo-icon icon-calendar-empty">&#xe83b;</i> <span class="i-name">icon-calendar-empty</span><span class="i-code">0xe83b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe83c"><i class="demo-icon icon-calendar">&#xe83c;</i> <span class="i-name">icon-calendar</span><span class="i-code">0xe83c</span></div>
+ <div class="the-icons span3" title="Code: 0xe83d"><i class="demo-icon icon-wrench">&#xe83d;</i> <span class="i-name">icon-wrench</span><span class="i-code">0xe83d</span></div>
+ <div class="the-icons span3" title="Code: 0xe83e"><i class="demo-icon icon-sliders">&#xe83e;</i> <span class="i-name">icon-sliders</span><span class="i-code">0xe83e</span></div>
+ <div class="the-icons span3" title="Code: 0xe83f"><i class="demo-icon icon-services">&#xe83f;</i> <span class="i-name">icon-services</span><span class="i-code">0xe83f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe840"><i class="demo-icon icon-service">&#xe840;</i> <span class="i-name">icon-service</span><span class="i-code">0xe840</span></div>
+ <div class="the-icons span3" title="Code: 0xe841"><i class="demo-icon icon-phone">&#xe841;</i> <span class="i-name">icon-phone</span><span class="i-code">0xe841</span></div>
+ <div class="the-icons span3" title="Code: 0xe842"><i class="demo-icon icon-file-pdf">&#xe842;</i> <span class="i-name">icon-file-pdf</span><span class="i-code">0xe842</span></div>
+ <div class="the-icons span3" title="Code: 0xe843"><i class="demo-icon icon-file-word">&#xe843;</i> <span class="i-name">icon-file-word</span><span class="i-code">0xe843</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe844"><i class="demo-icon icon-file-excel">&#xe844;</i> <span class="i-name">icon-file-excel</span><span class="i-code">0xe844</span></div>
+ <div class="the-icons span3" title="Code: 0xe845"><i class="demo-icon icon-doc-text">&#xe845;</i> <span class="i-name">icon-doc-text</span><span class="i-code">0xe845</span></div>
+ <div class="the-icons span3" title="Code: 0xe846"><i class="demo-icon icon-trash">&#xe846;</i> <span class="i-name">icon-trash</span><span class="i-code">0xe846</span></div>
+ <div class="the-icons span3" title="Code: 0xe847"><i class="demo-icon icon-comment-empty">&#xe847;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xe847</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe848"><i class="demo-icon icon-comment">&#xe848;</i> <span class="i-name">icon-comment</span><span class="i-code">0xe848</span></div>
+ <div class="the-icons span3" title="Code: 0xe849"><i class="demo-icon icon-chat">&#xe849;</i> <span class="i-name">icon-chat</span><span class="i-code">0xe849</span></div>
+ <div class="the-icons span3" title="Code: 0xe84a"><i class="demo-icon icon-chat-empty">&#xe84a;</i> <span class="i-name">icon-chat-empty</span><span class="i-code">0xe84a</span></div>
+ <div class="the-icons span3" title="Code: 0xe84b"><i class="demo-icon icon-bell">&#xe84b;</i> <span class="i-name">icon-bell</span><span class="i-code">0xe84b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe84c"><i class="demo-icon icon-bell-alt">&#xe84c;</i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xe84c</span></div>
+ <div class="the-icons span3" title="Code: 0xe84d"><i class="demo-icon icon-attention-alt">&#xe84d;</i> <span class="i-name">icon-attention-alt</span><span class="i-code">0xe84d</span></div>
+ <div class="the-icons span3" title="Code: 0xe84e"><i class="demo-icon icon-print">&#xe84e;</i> <span class="i-name">icon-print</span><span class="i-code">0xe84e</span></div>
+ <div class="the-icons span3" title="Code: 0xe84f"><i class="demo-icon icon-edit">&#xe84f;</i> <span class="i-name">icon-edit</span><span class="i-code">0xe84f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe850"><i class="demo-icon icon-forward">&#xe850;</i> <span class="i-name">icon-forward</span><span class="i-code">0xe850</span></div>
+ <div class="the-icons span3" title="Code: 0xe851"><i class="demo-icon icon-reply">&#xe851;</i> <span class="i-name">icon-reply</span><span class="i-code">0xe851</span></div>
+ <div class="the-icons span3" title="Code: 0xe852"><i class="demo-icon icon-reply-all">&#xe852;</i> <span class="i-name">icon-reply-all</span><span class="i-code">0xe852</span></div>
+ <div class="the-icons span3" title="Code: 0xe853"><i class="demo-icon icon-eye">&#xe853;</i> <span class="i-name">icon-eye</span><span class="i-code">0xe853</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe854"><i class="demo-icon icon-tag">&#xe854;</i> <span class="i-name">icon-tag</span><span class="i-code">0xe854</span></div>
+ <div class="the-icons span3" title="Code: 0xe855"><i class="demo-icon icon-tags">&#xe855;</i> <span class="i-name">icon-tags</span><span class="i-code">0xe855</span></div>
+ <div class="the-icons span3" title="Code: 0xe856"><i class="demo-icon icon-lock-open-alt">&#xe856;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xe856</span></div>
+ <div class="the-icons span3" title="Code: 0xe857"><i class="demo-icon icon-lock-open">&#xe857;</i> <span class="i-name">icon-lock-open</span><span class="i-code">0xe857</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe858"><i class="demo-icon icon-lock">&#xe858;</i> <span class="i-name">icon-lock</span><span class="i-code">0xe858</span></div>
+ <div class="the-icons span3" title="Code: 0xe859"><i class="demo-icon icon-home">&#xe859;</i> <span class="i-name">icon-home</span><span class="i-code">0xe859</span></div>
+ <div class="the-icons span3" title="Code: 0xe85a"><i class="demo-icon icon-info">&#xe85a;</i> <span class="i-name">icon-info</span><span class="i-code">0xe85a</span></div>
+ <div class="the-icons span3" title="Code: 0xe85b"><i class="demo-icon icon-help">&#xe85b;</i> <span class="i-name">icon-help</span><span class="i-code">0xe85b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe85c"><i class="demo-icon icon-search">&#xe85c;</i> <span class="i-name">icon-search</span><span class="i-code">0xe85c</span></div>
+ <div class="the-icons span3" title="Code: 0xe85d"><i class="demo-icon icon-flapping">&#xe85d;</i> <span class="i-name">icon-flapping</span><span class="i-code">0xe85d</span></div>
+ <div class="the-icons span3" title="Code: 0xe85e"><i class="demo-icon icon-rewind">&#xe85e;</i> <span class="i-name">icon-rewind</span><span class="i-code">0xe85e</span></div>
+ <div class="the-icons span3" title="Code: 0xe85f"><i class="demo-icon icon-chart-line">&#xe85f;</i> <span class="i-name">icon-chart-line</span><span class="i-code">0xe85f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe860"><i class="demo-icon icon-bell-off">&#xe860;</i> <span class="i-name">icon-bell-off</span><span class="i-code">0xe860</span></div>
+ <div class="the-icons span3" title="Code: 0xe861"><i class="demo-icon icon-bell-off-empty">&#xe861;</i> <span class="i-name">icon-bell-off-empty</span><span class="i-code">0xe861</span></div>
+ <div class="the-icons span3" title="Code: 0xe862"><i class="demo-icon icon-plug">&#xe862;</i> <span class="i-name">icon-plug</span><span class="i-code">0xe862</span></div>
+ <div class="the-icons span3" title="Code: 0xe863"><i class="demo-icon icon-eye-off">&#xe863;</i> <span class="i-name">icon-eye-off</span><span class="i-code">0xe863</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe864"><i class="demo-icon icon-arrows-cw">&#xe864;</i> <span class="i-name">icon-arrows-cw</span><span class="i-code">0xe864</span></div>
+ <div class="the-icons span3" title="Code: 0xe865"><i class="demo-icon icon-cw">&#xe865;</i> <span class="i-name">icon-cw</span><span class="i-code">0xe865</span></div>
+ <div class="the-icons span3" title="Code: 0xe866"><i class="demo-icon icon-host">&#xe866;</i> <span class="i-name">icon-host</span><span class="i-code">0xe866</span></div>
+ <div class="the-icons span3" title="Code: 0xe867"><i class="demo-icon icon-thumbs-up">&#xe867;</i> <span class="i-name">icon-thumbs-up</span><span class="i-code">0xe867</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe868"><i class="demo-icon icon-thumbs-down">&#xe868;</i> <span class="i-name">icon-thumbs-down</span><span class="i-code">0xe868</span></div>
+ <div class="the-icons span3" title="Code: 0xe869"><i class="demo-icon icon-spinner">&#xe869;</i> <span class="i-name">icon-spinner</span><span class="i-code">0xe869</span></div>
+ <div class="the-icons span3" title="Code: 0xe86a"><i class="demo-icon icon-attach">&#xe86a;</i> <span class="i-name">icon-attach</span><span class="i-code">0xe86a</span></div>
+ <div class="the-icons span3" title="Code: 0xe86b"><i class="demo-icon icon-keyboard">&#xe86b;</i> <span class="i-name">icon-keyboard</span><span class="i-code">0xe86b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe86c"><i class="demo-icon icon-menu">&#xe86c;</i> <span class="i-name">icon-menu</span><span class="i-code">0xe86c</span></div>
+ <div class="the-icons span3" title="Code: 0xe86d"><i class="demo-icon icon-wifi">&#xe86d;</i> <span class="i-name">icon-wifi</span><span class="i-code">0xe86d</span></div>
+ <div class="the-icons span3" title="Code: 0xe86e"><i class="demo-icon icon-moon">&#xe86e;</i> <span class="i-name">icon-moon</span><span class="i-code">0xe86e</span></div>
+ <div class="the-icons span3" title="Code: 0xe86f"><i class="demo-icon icon-chart-pie">&#xe86f;</i> <span class="i-name">icon-chart-pie</span><span class="i-code">0xe86f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe870"><i class="demo-icon icon-chart-area">&#xe870;</i> <span class="i-name">icon-chart-area</span><span class="i-code">0xe870</span></div>
+ <div class="the-icons span3" title="Code: 0xe871"><i class="demo-icon icon-chart-bar">&#xe871;</i> <span class="i-name">icon-chart-bar</span><span class="i-code">0xe871</span></div>
+ <div class="the-icons span3" title="Code: 0xe872"><i class="demo-icon icon-beaker">&#xe872;</i> <span class="i-name">icon-beaker</span><span class="i-code">0xe872</span></div>
+ <div class="the-icons span3" title="Code: 0xe873"><i class="demo-icon icon-magic">&#xe873;</i> <span class="i-name">icon-magic</span><span class="i-code">0xe873</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe874"><i class="demo-icon icon-spin6 animate-spin">&#xe874;</i> <span class="i-name">icon-spin6</span><span class="i-code">0xe874</span></div>
+ <div class="the-icons span3" title="Code: 0xe875"><i class="demo-icon icon-down-small">&#xe875;</i> <span class="i-name">icon-down-small</span><span class="i-code">0xe875</span></div>
+ <div class="the-icons span3" title="Code: 0xe876"><i class="demo-icon icon-left-small">&#xe876;</i> <span class="i-name">icon-left-small</span><span class="i-code">0xe876</span></div>
+ <div class="the-icons span3" title="Code: 0xe877"><i class="demo-icon icon-right-small">&#xe877;</i> <span class="i-name">icon-right-small</span><span class="i-code">0xe877</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe878"><i class="demo-icon icon-up-small">&#xe878;</i> <span class="i-name">icon-up-small</span><span class="i-code">0xe878</span></div>
+ <div class="the-icons span3" title="Code: 0xe879"><i class="demo-icon icon-pin">&#xe879;</i> <span class="i-name">icon-pin</span><span class="i-code">0xe879</span></div>
+ <div class="the-icons span3" title="Code: 0xe87a"><i class="demo-icon icon-angle-double-left">&#xe87a;</i> <span class="i-name">icon-angle-double-left</span><span class="i-code">0xe87a</span></div>
+ <div class="the-icons span3" title="Code: 0xe87b"><i class="demo-icon icon-angle-double-right">&#xe87b;</i> <span class="i-name">icon-angle-double-right</span><span class="i-code">0xe87b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe87c"><i class="demo-icon icon-circle">&#xe87c;</i> <span class="i-name">icon-circle</span><span class="i-code">0xe87c</span></div>
+ <div class="the-icons span3" title="Code: 0xe87d"><i class="demo-icon icon-info-circled">&#xe87d;</i> <span class="i-name">icon-info-circled</span><span class="i-code">0xe87d</span></div>
+ <div class="the-icons span3" title="Code: 0xe87e"><i class="demo-icon icon-twitter">&#xe87e;</i> <span class="i-name">icon-twitter</span><span class="i-code">0xe87e</span></div>
+ <div class="the-icons span3" title="Code: 0xe87f"><i class="demo-icon icon-facebook-squared">&#xe87f;</i> <span class="i-name">icon-facebook-squared</span><span class="i-code">0xe87f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe880"><i class="demo-icon icon-gplus-squared">&#xe880;</i> <span class="i-name">icon-gplus-squared</span><span class="i-code">0xe880</span></div>
+ <div class="the-icons span3" title="Code: 0xe881"><i class="demo-icon icon-attention-circled">&#xe881;</i> <span class="i-name">icon-attention-circled</span><span class="i-code">0xe881</span></div>
+ <div class="the-icons span3" title="Code: 0xe883"><i class="demo-icon icon-check">&#xe883;</i> <span class="i-name">icon-check</span><span class="i-code">0xe883</span></div>
+ <div class="the-icons span3" title="Code: 0xe884"><i class="demo-icon icon-reschedule">&#xe884;</i> <span class="i-name">icon-reschedule</span><span class="i-code">0xe884</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe885"><i class="demo-icon icon-warning-empty">&#xe885;</i> <span class="i-name">icon-warning-empty</span><span class="i-code">0xe885</span></div>
+ <div class="the-icons span3" title="Code: 0xf009"><i class="demo-icon icon-th-list">&#xf009;</i> <span class="i-name">icon-th-list</span><span class="i-code">0xf009</span></div>
+ <div class="the-icons span3" title="Code: 0xf00b"><i class="demo-icon icon-th-thumb-empty">&#xf00b;</i> <span class="i-name">icon-th-thumb-empty</span><span class="i-code">0xf00b</span></div>
+ <div class="the-icons span3" title="Code: 0xf09b"><i class="demo-icon icon-github-circled">&#xf09b;</i> <span class="i-name">icon-github-circled</span><span class="i-code">0xf09b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xf102"><i class="demo-icon icon-angle-double-up">&#xf102;</i> <span class="i-name">icon-angle-double-up</span><span class="i-code">0xf102</span></div>
+ <div class="the-icons span3" title="Code: 0xf103"><i class="demo-icon icon-angle-double-down">&#xf103;</i> <span class="i-name">icon-angle-double-down</span><span class="i-code">0xf103</span></div>
+ <div class="the-icons span3" title="Code: 0xf104"><i class="demo-icon icon-angle-left">&#xf104;</i> <span class="i-name">icon-angle-left</span><span class="i-code">0xf104</span></div>
+ <div class="the-icons span3" title="Code: 0xf105"><i class="demo-icon icon-angle-right">&#xf105;</i> <span class="i-name">icon-angle-right</span><span class="i-code">0xf105</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xf106"><i class="demo-icon icon-angle-up">&#xf106;</i> <span class="i-name">icon-angle-up</span><span class="i-code">0xf106</span></div>
+ <div class="the-icons span3" title="Code: 0xf107"><i class="demo-icon icon-angle-down">&#xf107;</i> <span class="i-name">icon-angle-down</span><span class="i-code">0xf107</span></div>
+ <div class="the-icons span3" title="Code: 0xf1da"><i class="demo-icon icon-history">&#xf1da;</i> <span class="i-name">icon-history</span><span class="i-code">0xf1da</span></div>
+ <div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
+ </div>
+ </div>
+ <div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
+ </body>
+</html> \ 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
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.eot
Binary files 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 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Copyright (C) 2020 by original authors @ fontello.com</metadata>
+<defs>
+<font id="ifont" horiz-adv-x="1000" >
+<font-face font-family="ifont" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
+<missing-glyph horiz-adv-x="1000" />
+<glyph glyph-name="dashboard" unicode="&#xe800;" d="M286 154v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m0 285v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m357-285v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m-357 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m357-286v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m357-285v-108q0-22-16-37t-38-16h-178q-22 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m-357 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m357-286v-107q0-22-16-38t-38-15h-178q-22 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m0 286v-107q0-22-16-38t-38-16h-178q-22 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="user" unicode="&#xe801;" d="M714 69q0-60-35-104t-84-44h-476q-49 0-84 44t-35 104q0 48 5 90t17 85 33 73 52 50 76 19q73-72 174-72t175 72q42 0 75-19t52-50 33-73 18-85 4-90z m-143 495q0-88-62-151t-152-63-151 63-63 151 63 152 151 63 152-63 62-152z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="users" unicode="&#xe802;" d="M331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 24 12q34 0 62-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="ok" unicode="&#xe803;" d="M352-10l-334 333 158 160 176-174 400 401 159-160z" horiz-adv-x="928" />
+
+<glyph glyph-name="cancel" unicode="&#xe804;" d="M799 116l-156-157-234 235-235-235-156 157 234 234-234 234 156 157 235-235 234 235 156-157-234-234z" horiz-adv-x="817" />
+
+<glyph glyph-name="plus" unicode="&#xe805;" d="M911 462l0-223-335 0 0-336-223 0 0 336-335 0 0 223 335 0 0 335 223 0 0-335 335 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="minus" unicode="&#xe806;" d="M18 239l0 223 893 0 0-223-893 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="folder-empty" unicode="&#xe807;" d="M464 685l447 0 0-669q0-47-33-80t-79-33l-669 0q-46 0-79 33t-33 80l0 781 446 0 0-112z m-334 0l0-223 669 0 0 112-446 0 0 111-223 0z m669-669l0 335-669 0 0-335 669 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="download" unicode="&#xe808;" d="M714 100q0 15-10 25t-25 11-25-11-11-25 11-25 25-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-39l-250-250q-10-11-25-11t-25 11l-250 250q-17 16-8 39 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="upload" unicode="&#xe809;" d="M714 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="git" unicode="&#xe80a;" d="M332 5q0 56-92 56-88 0-88-58 0-57 96-57 84 0 84 59z m-33 422q0 34-17 56t-49 23q-69 0-69-81 0-75 69-75 66 0 66 77z m150 180v-112q-20-7-44-13 9-24 9-47 0-70-41-120t-110-63q-22-5-33-15t-11-33q0-17 13-28t32-18 44-12 48-15 44-21 32-35 13-55q0-170-203-170-38 0-72 7t-65 23-49 46-18 71q0 92 102 125v3q-38 22-38 70 0 61 35 76v3q-40 13-66 60t-27 93q0 77 53 129t131 51q54 0 100-26 54 0 121 26z m178-491h-124q2 25 2 74v340q0 53-2 72h124q-3-19-3-69v-343q0-49 3-74z m335 124v-110q-40-22-97-22-35 0-60 12t-39 27-22 44-10 51-2 58v196h1v2q-4 0-11 0t-10 1q-12 0-33-3v106h54v42q0 30-4 50h127q-3-23-3-92h95v-106q-8 0-24 1t-24 1h-47v-204q0-73 48-73 34 0 61 19z m-321 528q0-32-22-57t-54-24q-32 0-54 24t-23 57q0 33 22 57t55 25q33 0 54-25t22-57z" horiz-adv-x="1000" />
+
+<glyph glyph-name="cubes" unicode="&#xe80b;" d="M357-61l214 107v176l-214-92v-191z m-36 254l226 96-226 97-225-97z m608-254l214 107v176l-214-92v-191z m-36 254l225 96-225 97-226-97z m-250 163l214 92v149l-214-92v-149z m-36 212l246 105-246 106-246-106z m607-289v-233q0-20-10-37t-29-26l-250-125q-14-8-32-8t-32 8l-250 125q-2 1-4 2-1-1-4-2l-250-125q-14-8-32-8t-31 8l-250 125q-19 9-29 26t-11 37v233q0 21 12 39t32 26l242 104v223q0 22 12 40t31 26l250 107q13 6 28 6t28-6l250-107q20-9 32-26t12-40v-223l242-104q20-8 32-26t11-39z" horiz-adv-x="1285.7" />
+
+<glyph glyph-name="database" unicode="&#xe80c;" d="M429 421q132 0 247 24t181 71v-95q0-38-57-71t-157-52-214-19-215 19-156 52-58 71v95q66-47 181-71t248-24z m0-428q132 0 247 24t181 71v-95q0-39-57-72t-157-52-214-19-215 19-156 52-58 72v95q66-47 181-71t248-24z m0 214q132 0 247 24t181 71v-95q0-38-57-71t-157-52-214-20-215 20-156 52-58 71v95q66-47 181-71t248-24z m0 643q116 0 214-19t157-52 57-72v-71q0-39-57-72t-157-52-214-19-215 19-156 52-58 72v71q0 39 58 72t156 52 215 19z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="gauge" unicode="&#xe80d;" d="M214 207q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
+
+<glyph glyph-name="sitemap" unicode="&#xe80e;" d="M1000 154v-179q0-22-16-38t-38-16h-178q-22 0-38 16t-16 38v179q0 22 16 38t38 15h53v107h-285v-107h53q23 0 38-15t16-38v-179q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v179q0 22 16 38t38 15h53v107h-285v-107h53q23 0 38-15t16-38v-179q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v179q0 22 16 38t38 15h53v107q0 29 21 51t51 21h285v107h-53q-23 0-38 16t-16 37v179q0 22 16 38t38 16h178q23 0 38-16t16-38v-179q0-22-16-37t-38-16h-53v-107h285q29 0 51-21t21-51v-107h53q23 0 38-15t16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="sort-name-up" unicode="&#xe80f;" d="M665 622h98l-40 122-6 26q-2 9-2 11h-2l-1-11q0 0-2-10t-5-16z m-254-576q0-6-6-13l-178-178q-5-5-13-5-6 0-12 5l-179 179q-8 9-4 19 4 11 17 11h107v768q0 8 5 13t13 5h107q8 0 13-5t5-13v-768h107q8 0 13-5t5-13z m466-66v-130h-326v50l206 295q7 11 12 16l6 5v1q-1 0-3 0t-5 0q-6-2-16-2h-130v-64h-67v128h317v-50l-206-296q-4-4-12-14l-6-7v-1l8 1q5 2 16 2h139v66h67z m50 501v-60h-161v60h42l-26 80h-136l-26-80h42v-60h-160v60h39l128 369h91l128-369h39z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="sort-name-down" unicode="&#xe810;" d="M665 51h98l-40 122-6 26q-2 9-2 11h-2l-1-11q0-1-2-10t-5-16z m-254-5q0-6-6-13l-178-178q-5-5-13-5-6 0-12 5l-179 179q-8 9-4 19 4 11 17 11h107v768q0 8 5 13t13 5h107q8 0 13-5t5-13v-768h107q8 0 13-5t5-13z m516-137v-59h-161v59h42l-26 80h-136l-26-80h42v-59h-160v59h39l128 370h91l128-370h39z m-50 643v-131h-326v51l206 295q7 10 12 15l6 5v2q-1 0-3-1t-5 0q-6-2-16-2h-130v-64h-67v128h317v-50l-206-295q-4-5-12-15l-6-5v-2l8 2q5 0 16 0h139v67h67z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="megaphone" unicode="&#xe811;" d="M929 493q29 0 50-21t21-51-21-50-50-21v-214q0-29-22-50t-50-22q-233 194-453 212-32-10-51-36t-17-57 22-51q-11-19-13-37t4-32 19-31 26-28 35-28q-17-32-63-46t-94-7-73 31q-4 13-17 49t-18 53-12 50-9 56 2 55 12 62h-68q-36 0-63 26t-26 63v107q0 37 26 63t63 26h268q243 0 500 215 29 0 50-22t22-50v-214z m-72-337v532q-220-168-428-191v-151q210-23 428-190z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bug" unicode="&#xe812;" d="M911 314q0-14-11-25t-25-10h-125q0-96-37-162l116-117q10-11 10-25t-10-25q-10-11-25-11t-25 11l-111 110q-3-3-8-7t-24-16-36-21-46-16-54-7v500h-71v-500q-29 0-57 7t-49 19-36 22-25 18l-8 8-102-116q-11-12-27-12-13 0-24 9-11 10-11 25t8 26l113 127q-32 63-32 153h-125q-15 0-25 10t-11 25 11 25 25 11h125v164l-97 97q-11 10-11 25t11 25 25 10 25-10l97-97h471l96 97q11 10 25 10t26-10 10-25-10-25l-97-97v-164h125q15 0 25-11t11-25z m-268 322h-357q0 74 52 126t126 52 127-52 52-126z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="tasks" unicode="&#xe813;" d="M571 64h358v72h-358v-72z m-214 286h572v71h-572v-71z m357 286h215v71h-215v-71z m286-465v-142q0-15-11-25t-25-11h-928q-15 0-25 11t-11 25v142q0 15 11 26t25 10h928q15 0 25-10t11-26z m0 286v-143q0-14-11-25t-25-10h-928q-15 0-25 10t-11 25v143q0 15 11 25t25 11h928q15 0 25-11t11-25z m0 286v-143q0-14-11-25t-25-11h-928q-15 0-25 11t-11 25v143q0 14 11 25t25 11h928q15 0 25-11t11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="filter" unicode="&#xe814;" d="M783 685q9-22-8-39l-275-275v-414q0-23-22-33-7-3-14-3-15 0-25 11l-143 143q-10 11-10 25v271l-275 275q-18 17-8 39 9 22 33 22h714q23 0 33-22z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="off" unicode="&#xe815;" d="M857 350q0-87-34-166t-91-137-137-92-166-34-167 34-136 92-92 137-34 166q0 102 45 191t126 151q24 18 54 14t46-28q18-23 14-53t-28-47q-54-41-84-101t-30-127q0-58 23-111t61-91 91-61 111-23 110 23 92 61 61 91 22 111q0 68-30 127t-84 101q-23 18-28 47t14 53q17 24 47 28t53-14q81-61 126-151t45-191z m-357 429v-358q0-29-21-50t-50-21-51 21-21 50v358q0 29 21 50t51 21 50-21 21-50z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="book" unicode="&#xe816;" d="M915 583q22-31 10-72l-154-505q-10-36-42-60t-69-25h-515q-43 0-83 30t-55 74q-14 37-1 71 0 2 1 15t3 20q0 5-2 12t-2 11q1 6 5 12t9 13 9 13q13 21 25 51t17 51q2 6 0 17t0 16q2 6 9 15t10 13q12 20 23 51t14 51q1 5-1 17t0 16q2 7 12 17t13 13q10 14 23 47t16 54q0 4-2 14t-1 15q1 4 5 10t10 13 10 11q4 7 9 17t8 20 9 20 11 18 15 13 20 6 26-3l0-1q21 5 28 5h425q41 0 64-32t10-72l-153-506q-20-66-40-85t-72-20h-485q-15 0-21-8-6-9-1-24 14-39 81-39h515q16 0 31 9t20 23l167 550q4 13 3 32 21-8 33-24z m-594-1q-2-7 1-12t11-6h339q8 0 15 6t9 12l12 36q2 7-1 12t-12 6h-339q-7 0-14-6t-9-12z m-46-143q-3-7 1-12t11-6h339q7 0 14 6t10 12l11 36q3 7-1 13t-11 5h-339q-7 0-14-5t-10-13z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="paste" unicode="&#xe817;" d="M429-79h500v358h-233q-22 0-37 15t-16 38v232h-214v-643z m142 804v36q0 7-5 12t-12 6h-393q-7 0-13-6t-5-12v-36q0-7 5-13t13-5h393q7 0 12 5t5 13z m143-375h167l-167 167v-167z m286-71v-375q0-23-16-38t-38-16h-535q-23 0-38 16t-16 38v89h-303q-23 0-38 16t-16 37v750q0 23 16 38t38 16h607q22 0 38-16t15-38v-183q12-7 20-15l228-228q16-15 27-42t11-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="scissors" unicode="&#xe818;" d="M536 350q14 0 25-11t10-25-10-25-25-10-25 10-11 25 11 25 25 11z m167-36l283-222q16-11 14-31-3-20-19-28l-72-36q-7-4-16-4-10 0-17 4l-385 216-62-36q-4-3-7-3 8-28 6-54-4-43-31-83t-74-69q-74-47-154-47-76 0-124 44-51 47-44 116 4 42 31 82t73 69q74 47 155 47 46 0 84-18 5 8 13 13l68 40-68 41q-8 5-13 12-38-17-84-17-81 0-155 47-46 30-73 69t-31 82q-3 33 8 63t36 52q47 44 124 44 80 0 154-47 46-29 74-68t31-83q2-27-6-54 3-1 7-3l62-37 385 216q7 5 17 5 9 0 16-4l72-36q16-9 19-28 2-20-14-32z m-380 145q26 24 12 61t-59 65q-52 33-107 33-42 0-63-20-26-24-12-60t59-66q51-33 107-33 41 0 63 20z m-47-415q45 28 59 65t-12 60q-22 20-63 20-56 0-107-33-45-28-59-65t12-60q21-20 63-20 55 0 107 33z m99 342l54-33v7q0 20 18 31l8 4-44 26-15-14q-1-2-5-6t-7-7q-1-1-2-2t-2-1z m125-125l54-18 410 321-71 36-429-240v-64l-89-53 5-5q1-1 4-3 2-2 6-7t6-6l15-15z m393-232l71 35-290 228-99-77q-1-2-7-4z" horiz-adv-x="1000" />
+
+<glyph glyph-name="globe" unicode="&#xe819;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m153-291q-2-1-6-5t-7-6q1 0 2 3t3 6 2 4q3 4 12 8 8 4 29 7 19 5 29-6-1 1 5 7t8 7q2 1 8 3t9 4l1 12q-7-1-10 4t-3 12q0-2-4-5 0 4-2 5t-7-1-5-1q-5 2-8 5t-5 9-2 8q-1 3-5 6t-5 6q-1 1-2 3t-1 4-3 3-3 1-4-3-4-5-2-3q-2 1-4 1t-2-1-3-1-3-2q-1-2-4-2t-5-1q8 3-1 6-5 2-9 2 6 2 5 6t-5 8h3q-1 2-5 5t-10 5-7 3q-5 3-19 5t-18 1q-3-4-3-6t2-8 2-7q1-3-3-7t-3-7q0-4 7-9t6-12q-2-4-9-9t-9-6q-3-5-1-11t6-9q1-1 1-2t-2-3-3-2-4-2l-1-1q-7-3-12 3t-7 15q-4 14-9 17-13 4-16-1-3 7-23 15-14 5-33 2 4 0 0 8-4 9-10 7 1 3 2 10t0 7q2 8 7 13 1 1 4 5t5 7 1 4q19-3 28 6 2 3 6 9t6 10q5 3 8 3t8-3 8-3q8-1 8 6t-4 11q7 0 2 10-2 4-5 5-6 2-15-3-4-2 2-4-1 0-6-6t-9-10-9 3q0 0-3 7t-5 8q-5 0-9-9 1 5-6 9t-14 4q11 7-4 15-4 3-12 3t-11-2q-2-4-3-7t3-4 6-3 6-2 5-2q8-6 5-8-1 0-5-2t-6-2-4-2q-1-3 0-8t-1-8q-3 3-5 10t-4 9q4-5-14-3l-5 0q-3 0-9-1t-12-1-7 5q-3 4 0 11 0 2 2 1-2 2-6 5t-6 5q-25-8-52-23 3 0 6 1 3 1 8 4t5 3q19 7 24 4l3 2q7-9 11-14-4 3-17 1-11-3-12-7 4-6 2-10-2 2-6 6t-8 6-8 3q-9 0-13-1-81-45-131-124 4-4 7-4 2-1 3-5t1-6 6 1q5-4 2-10 1 0 25-15 10-10 11-12 2-6-5-10-1 1-5 5t-5 2q-2-3 0-10t6-7q-4 0-5-9t-2-20 0-13l1-1q-2-6 3-19t12-11q-7-1 11-24 3-4 4-5 2-1 7-4t9-6 5-5q2-3 6-13t8-13q-2-3 5-11t6-13q-1 0-2-1t-1 0q2-4 9-8t8-7q1-2 1-6t2-6 4-1q2 11-13 35-8 13-9 16-2 2-4 8t-2 8q1 0 3 0t5-2 4-3 1-1q-1-4 1-10t7-10 10-11 6-7q4-4 8-11t0-8q5 0 11-5t10-11q3-5 4-15t3-13q1-4 5-8t7-5l9-5t7-3q3-2 10-6t12-7q6-2 9-2t8 1 8 2q8 1 16-8t12-12q20-10 30-6-1 0 1-4t4-9 5-8 3-5q3-3 10-8t10-8q4 2 4 5-1-5 4-11t10-6q8 2 8 18-17-8-27 10 0 0-2 3t-2 5-1 4 0 5 2 1q5 0 6 2t-1 7-2 8q-1 4-6 11t-7 8q-3-5-9-4t-9 5q0-1-1-3t-1-4q-7 0-8 0 1 2 1 10t2 13q1 2 3 6t5 9 2 7-3 5-9 1q-11 0-15-11-1-2-2-6t-2-6-5-4q-4-2-14-1t-13 3q-8 4-13 16t-5 20q0 6 1 15t2 14-3 14q2 1 5 5t5 6q2 1 3 1t3 0 2 1 1 3q0 1-2 2-1 1-2 1 4-1 16 1t15-1q9-6 12 1 0 1-1 6t0 7q3-15 16-5 2-1 9-3t9-2q2-1 4-3t3-3 3 0 5 4q5-8 7-13 6-23 10-25 4-2 6-1t3 5 0 8-1 7l-1 5v10l0 4q-8 2-10 7t0 10 9 10q0 1 4 2t9 4 7 4q12 11 8 20 4 0 6 5 0 0-2 2t-5 2-2 2q5 2 1 8 3 2 4 7t4 5q5-6 12-1 5 5 1 9 2 4 11 6t10 5q4-1 5 1t0 7 2 7q2 2 9 5t7 2l9 7q2 2 0 2 10-1 18 6 5 6-4 11 2 4-1 5t-9 4q2 0 7 0t5 1q9 5-3 9-10 2-24-7z m-91-490q115 21 195 106-1 2-7 2t-7 2q-10 4-13 5 1 4-1 7t-5 5-7 5-6 4q-1 1-4 3t-4 3-4 2-5 2-5-1l-2-1q-2 0-3-1t-3-2-2-1 0-2q-12 10-20 13-3 0-6 3t-6 4-6 0-6-3q-3-3-4-9t-1-7q-4 3 0 10t1 10q-1 3-6 2t-6-2-7-5-5-3-4-3-5-5q-2-2-4-6t-2-6q-1 2-7 3t-5 3q1-5 2-19t3-22q4-17-7-26-15-14-16-23-2-12 7-14 0-4-5-12t-4-12q0-3 2-9z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="cloud" unicode="&#xe81a;" d="M1071 207q0-89-62-151t-152-63h-607q-103 0-177 73t-73 177q0 74 40 135t104 91q-1 16-1 24 0 118 84 202t202 84q88 0 159-49t105-129q39 35 93 35 59 0 101-42t42-101q0-42-23-77 72-17 119-75t46-134z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="flash" unicode="&#xe81b;" d="M494 534q10-11 4-24l-302-646q-7-14-23-14-2 0-8 1-9 3-14 11t-3 16l110 451-226-56q-2-1-7-1-10 0-17 7-10 8-7 21l112 461q2 8 9 13t15 5h183q11 0 18-7t7-17q0-4-2-10l-96-258 221 54q5 2 7 2 11 0 19-9z" horiz-adv-x="500" />
+
+<glyph glyph-name="barchart" unicode="&#xe81c;" d="M143 46v-107q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v107q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 72v-179q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v179q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 143v-322q0-8-5-13t-12-5h-108q-7 0-12 5t-5 13v322q0 8 5 13t12 5h108q7 0 12-5t5-13z m215 214v-536q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v536q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 286v-822q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v822q0 8 5 13t13 5h107q8 0 13-5t5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="down-dir" unicode="&#xe81d;" d="M571 457q0-14-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 11-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="up-dir" unicode="&#xe81e;" d="M571 171q0-14-10-25t-25-10h-500q-15 0-25 10t-11 25 11 26l250 250q10 10 25 10t25-10l250-250q10-11 10-26z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="left-dir" unicode="&#xe81f;" d="M357 600v-500q0-14-10-25t-26-11-25 11l-250 250q-10 11-10 25t10 25l250 250q11 11 25 11t26-11 10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="right-dir" unicode="&#xe820;" d="M321 350q0-14-10-25l-250-250q-11-11-25-11t-25 11-11 25v500q0 15 11 25t25 11 25-11l250-250q10-10 10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="down-open" unicode="&#xe821;" d="M939 399l-414-413q-10-11-25-11t-25 11l-414 413q-11 11-11 26t11 25l93 92q10 11 25 11t25-11l296-296 296 296q11 11 25 11t26-11l92-92q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+
+<glyph glyph-name="right-open" unicode="&#xe822;" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="up-open" unicode="&#xe823;" d="M939 107l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+
+<glyph glyph-name="left-open" unicode="&#xe824;" d="M654 682l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="up-big" unicode="&#xe825;" d="M899 308q0-28-21-50l-41-42q-22-21-51-21-30 0-50 21l-165 164v-393q0-29-20-47t-51-19h-71q-30 0-51 19t-21 47v393l-164-164q-20-21-50-21t-50 21l-42 42q-21 21-21 50 0 30 21 51l363 363q20 21 50 21 30 0 51-21l363-363q21-22 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="right-big" unicode="&#xe826;" d="M821 314q0-30-20-50l-363-364q-22-20-51-20-29 0-50 20l-42 42q-22 21-22 51t22 51l163 163h-393q-29 0-47 21t-18 51v71q0 30 18 51t47 20h393l-163 165q-22 20-22 50t22 50l42 42q21 21 50 21 29 0 51-21l363-363q20-20 20-51z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="left-big" unicode="&#xe827;" d="M857 350v-71q0-30-18-51t-47-21h-393l164-164q21-20 21-50t-21-50l-42-43q-21-20-51-20-29 0-50 20l-364 364q-20 21-20 50 0 29 20 51l364 363q21 21 50 21 29 0 51-21l42-41q21-22 21-51t-21-51l-164-164h393q29 0 47-20t18-51z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="down-big" unicode="&#xe828;" d="M899 386q0-30-21-50l-363-364q-22-21-51-21-29 0-50 21l-363 364q-21 20-21 50 0 29 21 51l41 41q22 21 51 21 29 0 50-21l164-164v393q0 29 21 50t51 22h71q29 0 50-22t21-50v-393l165 164q20 21 50 21 29 0 51-21l41-41q21-22 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="resize-full-alt" unicode="&#xe829;" d="M716 548l-198-198 198-198 80 80q17 18 39 8 22-9 22-33v-250q0-14-10-25t-26-11h-250q-23 0-32 23-10 21 7 38l81 81-198 198-198-198 80-81q17-17 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l80-80 198 198-198 198-80-80q-11-11-25-11-7 0-14 3-22 9-22 33v250q0 14 11 25t25 11h250q23 0 33-23 9-21-8-38l-80-81 198-198 198 198-81 81q-17 17-7 38 9 23 32 23h250q15 0 26-11t10-25v-250q0-24-22-33-7-3-14-3-14 0-25 11z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="resize-full" unicode="&#xe82a;" d="M421 261q0-7-5-13l-185-185 80-81q10-10 10-25t-10-25-25-11h-250q-15 0-25 11t-11 25v250q0 15 11 25t25 11 25-11l80-80 186 185q5 6 12 6t13-6l64-63q5-6 5-13z m436 482v-250q0-15-10-25t-26-11-25 11l-80 80-185-185q-6-6-13-6t-13 6l-64 64q-5 5-5 12t5 13l186 185-81 81q-10 10-10 25t10 25 25 11h250q15 0 26-11t10-25z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="resize-small" unicode="&#xe82b;" d="M429 314v-250q0-14-11-25t-25-10-25 10l-81 81-185-186q-5-5-13-5t-12 5l-64 64q-6 6-6 13t6 13l185 185-80 80q-11 11-11 25t11 25 25 11h250q14 0 25-11t11-25z m421 375q0-7-6-12l-185-186 80-80q11-11 11-25t-11-25-25-11h-250q-14 0-25 11t-10 25v250q0 14 10 25t25 10 25-10l81-80 185 185q6 5 13 5t13-5l63-64q6-5 6-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="move" unicode="&#xe82c;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-215v-215h72q14 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-11 10-11 25t11 25 25 10h72v215h-215v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h215v215h-72q-14 0-25 10t-11 25 11 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26t-11-25-25-10h-72v-215h215v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="resize-horizontal" unicode="&#xe82d;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-572v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h572v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="resize-vertical" unicode="&#xe82e;" d="M393 671q0-14-11-25t-25-10h-71v-572h71q15 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-10 10-10 25t10 25 25 10h72v572h-72q-14 0-25 10t-10 25 10 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26z" horiz-adv-x="428.6" />
+
+<glyph glyph-name="zoom-in" unicode="&#xe82f;" d="M571 404v-36q0-7-5-13t-12-5h-125v-125q0-7-6-13t-12-5h-36q-7 0-13 5t-5 13v125h-125q-7 0-12 5t-6 13v36q0 7 6 12t12 5h125v125q0 8 5 13t13 5h36q7 0 12-5t6-13v-125h125q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="block" unicode="&#xe830;" d="M732 352q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="zoom-out" unicode="&#xe831;" d="M571 404v-36q0-7-5-13t-12-5h-322q-7 0-12 5t-6 13v36q0 7 6 12t12 5h322q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="lightbulb" unicode="&#xe832;" d="M411 529q0-8-6-13t-12-5-13 5-5 13q0 25-30 39t-59 14q-7 0-13 5t-5 13 5 13 13 5q28 0 55-9t49-30 21-50z m89 0q0 40-19 74t-50 57-69 35-76 12-76-12-69-35-50-57-20-74q0-57 38-101 6-6 17-18t17-19q72-85 79-166h127q8 81 79 166 6 6 17 19t17 18q38 44 38 101z m71 0q0-87-57-150-25-27-42-48t-33-54-19-60q26-15 26-46 0-20-13-35 13-15 13-36 0-29-25-45 8-13 8-26 0-26-18-40t-43-14q-11-25-34-39t-48-15-49 15-33 39q-26 0-44 14t-17 40q0 13 7 26-25 16-25 45 0 21 14 36-14 15-14 35 0 31 26 46-2 28-19 60t-33 54-41 48q-58 63-58 150 0 55 25 103t65 79 92 49 104 19 104-19 91-49 66-79 24-103z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="clock" unicode="&#xe833;" d="M500 546v-250q0-7-5-12t-13-5h-178q-8 0-13 5t-5 12v36q0 8 5 13t13 5h125v196q0 8 5 13t12 5h36q8 0 13-5t5-13z m232-196q0 83-41 152t-110 111-152 41-153-41-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152z m125 0q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="volume-up" unicode="&#xe834;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z m214-304q0-42-24-79t-63-52q-5-3-14-3-14 0-25 10t-10 26q0 12 6 20t17 14 19 12 16 21 6 31-6 32-16 20-19 13-17 13-6 20q0 15 10 26t25 10q9 0 14-3 39-15 63-52t24-79z m143 0q0-85-48-158t-125-105q-7-3-14-3-15 0-26 11t-10 25q0 22 21 33 32 16 43 25 41 30 64 75t23 97-23 97-64 75q-11 9-43 25-21 11-21 33 0 14 10 25t25 11q8 0 15-3 78-33 125-105t48-158z m143 0q0-128-71-236t-189-158q-7-3-14-3-15 0-25 11t-11 25q0 20 22 33 4 2 12 6t13 6q25 14 46 28 68 51 107 127t38 161-38 161-107 127q-21 15-46 28-4 3-13 6t-12 6q-22 13-22 33 0 15 11 25t25 11q7 0 14-3 118-51 189-158t71-236z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="volume-down" unicode="&#xe835;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z m214-304q0-42-24-79t-63-52q-5-3-14-3-14 0-25 10t-10 26q0 12 6 20t17 14 19 12 16 21 6 31-6 32-16 20-19 13-17 13-6 20q0 15 10 26t25 10q9 0 14-3 39-15 63-52t24-79z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="volume-off" unicode="&#xe836;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z" horiz-adv-x="428.6" />
+
+<glyph glyph-name="mute" unicode="&#xe837;" d="M151 323l-56-57q-24 58-24 120v71q0 15 11 25t25 11 25-11 11-25v-71q0-30 8-63z m622 336l-202-202v-71q0-74-52-126t-126-53q-31 0-61 11l-53-54q54-28 114-28 103 0 177 73t73 177v71q0 15 11 25t25 11 25-11 10-25v-71q0-124-82-215t-203-104v-74h142q15 0 26-11t10-25-10-25-26-11h-357q-14 0-25 11t-10 25 10 25 25 11h143v74q-70 7-131 45l-142-142q-5-6-13-6t-12 6l-46 46q-6 5-6 13t6 12l689 689q5 6 12 6t13-6l46-46q6-5 6-13t-6-12z m-212 73l-347-346v285q0 74 53 127t126 52q57 0 103-33t65-85z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="mic" unicode="&#xe838;" d="M643 457v-71q0-124-82-215t-204-104v-74h143q15 0 25-11t11-25-11-25-25-11h-357q-15 0-25 11t-11 25 11 25 25 11h143v74q-121 13-204 104t-82 215v71q0 15 11 25t25 11 25-11 10-25v-71q0-103 74-177t176-73 177 73 73 177v71q0 15 11 25t25 11 25-11 11-25z m-143 214v-285q0-74-52-126t-127-53-126 53-52 126v285q0 74 52 127t126 52 127-52 52-127z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="endtime" unicode="&#xe839;" d="M661 350q0-14-11-25l-303-304q-11-10-26-10t-25 10-10 25v161h-250q-15 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 10 25t25 10 26-10l303-304q11-10 11-25z m196 196v-392q0-67-47-114t-114-47h-178q-7 0-13 5t-5 13q0 2-1 11t0 15 2 13 5 11 12 3h178q37 0 64 27t26 63v392q0 37-26 64t-64 26h-174t-6 0-6 2-5 3-4 5-1 8q0 2-1 11t0 15 2 13 5 11 12 3h178q67 0 114-47t47-114z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="starttime" unicode="&#xe83a;" d="M357 46q0-2 1-11t0-14-2-14-5-11-12-3h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="calendar-empty" unicode="&#xe83b;" d="M71-79h786v572h-786v-572z m215 679v161q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h36q8 0 13 5t5 13z m428 0v161q0 8-5 13t-13 5h-35q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="calendar" unicode="&#xe83c;" d="M71-79h161v161h-161v-161z m197 0h178v161h-178v-161z m-197 197h161v178h-161v-178z m197 0h178v178h-178v-178z m-197 214h161v161h-161v-161z m411-411h179v161h-179v-161z m-214 411h178v161h-178v-161z m428-411h161v161h-161v-161z m-214 197h179v178h-179v-178z m-196 482v161q0 7-6 12t-12 6h-36q-7 0-12-6t-6-12v-161q0-7 6-13t12-5h36q7 0 12 5t6 13z m410-482h161v178h-161v-178z m-214 214h179v161h-179v-161z m214 0h161v161h-161v-161z m18 268v161q0 7-5 12t-13 6h-35q-7 0-13-6t-5-12v-161q0-7 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="wrench" unicode="&#xe83d;" d="M214 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="sliders" unicode="&#xe83e;" d="M196 64v-71h-196v71h196z m197 72q14 0 25-11t11-25v-143q0-14-11-25t-25-11h-143q-14 0-25 11t-11 25v143q0 15 11 25t25 11h143z m89 214v-71h-482v71h482z m-357 286v-72h-125v72h125z m732-572v-71h-411v71h411z m-536 643q15 0 26-10t10-26v-142q0-15-10-25t-26-11h-142q-15 0-25 11t-11 25v142q0 15 11 26t25 10h142z m358-286q14 0 25-10t10-25v-143q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v143q0 14 11 25t25 10h143z m178-71v-71h-125v71h125z m0 286v-72h-482v72h482z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="services" unicode="&#xe83f;" d="M500 350q0 59-42 101t-101 42-101-42-42-101 42-101 101-42 101 42 42 101z m429-286q0 29-22 51t-50 21-50-21-21-51q0-29 21-50t50-21 51 21 21 50z m0 572q0 29-22 50t-50 21-50-21-21-50q0-30 21-51t50-21 51 21 21 51z m-215-235v-103q0-6-4-11t-8-6l-87-14q-6-19-18-42 19-27 50-64 4-6 4-11 0-7-4-11-12-17-46-50t-43-33q-7 0-12 4l-64 50q-21-11-43-17-6-60-13-87-4-13-17-13h-104q-6 0-11 4t-5 10l-13 85q-19 6-42 18l-66-50q-4-4-11-4-6 0-12 4-80 75-80 90 0 5 4 10 5 8 23 30t26 34q-13 24-20 46l-85 13q-5 1-9 5t-4 11v104q0 5 4 10t9 6l86 14q7 19 18 42-19 27-50 64-4 6-4 11 0 7 4 12 12 16 46 49t44 33q6 0 12-4l64-50q19 10 43 18 6 60 13 86 3 13 16 13h104q6 0 11-4t6-10l13-85q19-6 42-17l65 49q5 4 12 4 6 0 11-4 81-75 81-90 0-4-4-10-7-9-24-30t-25-34q13-27 19-46l85-12q6-2 9-6t4-11z m357-298v-78q0-9-83-17-6-15-16-29 28-63 28-77 0-2-2-4-68-40-69-40-5 0-26 27t-29 37q-11-1-17-1t-17 1q-7-11-29-37t-25-27q-1 0-69 40-3 2-3 4 0 14 29 77-10 14-17 29-83 8-83 17v78q0 9 83 18 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-38q12 1 17 1t17-1q28 40 51 63l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-9 83-18z m0 572v-78q0-9-83-18-6-15-16-29 28-63 28-77 0-2-2-4-68-39-69-39-5 0-26 26t-29 38q-11-1-17-1t-17 1q-7-12-29-38t-25-26q-1 0-69 39-3 2-3 4 0 14 29 77-10 14-17 29-83 9-83 18v78q0 9 83 17 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-37q12 1 17 1t17-1q28 39 51 62l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-8 83-17z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="service" unicode="&#xe840;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="phone" unicode="&#xe841;" d="M786 158q0-15-6-39t-12-38q-11-28-68-60-52-28-103-28-15 0-30 2t-32 7-26 8-31 11-28 10q-54 20-97 47-71 44-148 120t-120 148q-27 43-46 97-2 5-10 28t-12 31-8 26-7 32-2 29q0 52 29 104 31 57 59 68 14 6 38 12t39 6q8 0 12-2 10-3 30-42 6-11 16-31t20-35 17-30q2-2 10-14t12-20 4-16q0-11-16-27t-35-31-34-30-16-25q0-5 3-13t4-11 8-14 7-10q42-77 97-132t131-97q1 0 10-6t14-8 11-5 13-2q10 0 25 16t30 34 31 35 28 16q7 0 15-4t20-12 14-10q14-8 30-17t36-20 30-17q39-19 42-29 2-4 2-12z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="file-pdf" unicode="&#xe842;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-287 331q18-14 47-31 33 4 65 4 82 0 99-27 9-13 1-29 0-1-1-1l-1-2v0q-3-21-39-21-27 0-64 11t-73 29q-123-13-219-46-85-146-135-146-8 0-15 4l-14 7q0 0-3 2-6 6-4 20 5 23 32 51t73 54q8 5 13-3 1-1 1-2 29 47 60 110 38 76 58 146-13 46-17 89t4 71q6 22 23 22h12q13 0 20-8 10-12 5-38-1-3-2-4 0-2 0-5v-17q-1-68-8-107 31-91 82-133z m-321-229q29 13 76 88-29-22-49-47t-27-41z m222 513q-9-23-2-73 1 4 4 24 0 2 4 24 1 3 3 5-1 0-1 1-1 1-1 2 0 12-7 20 0-1 0-1v-2z m-70-368q76 30 159 45-1 0-7 5t-9 8q-43 37-71 98-15-48-46-110-17-31-26-46z m361 9q-13 13-78 13 42-16 69-16 8 0 10 1 0 0-1 2z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="file-word" unicode="&#xe843;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-656 500v-59h39l92-369h88l72 271q4 11 5 25 2 9 2 14h2l1-14q1-1 2-11t3-14l72-271h89l91 369h39v59h-167v-59h50l-55-245q-3-11-4-25l-1-12h-3q0 2 0 4t-1 4 0 4q-1 2-2 11t-3 14l-81 304h-63l-81-304q-1-5-2-13t-2-12l-2-12h-2l-2 12q-1 14-3 25l-56 245h50v59h-167z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="file-excel" unicode="&#xe844;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-547 131v-59h157v59h-42l58 90q3 4 5 9t5 8 2 2h1q0-2 3-6 1-2 2-4t3-4 4-5l60-90h-43v-59h163v59h-38l-107 152 108 158h38v59h-156v-59h41l-57-89q-2-4-6-9t-5-8l-1-1h-1q0 2-3 5-3 6-9 13l-59 89h42v59h-162v-59h38l106-152-109-158h-38z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="doc-text" unicode="&#xe845;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-572 483q0 7 5 12t13 5h393q8 0 13-5t5-12v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36z m411-125q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z m0-143q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="trash" unicode="&#xe846;" d="M286 439v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m143 0v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m142 0v-321q0-8-5-13t-12-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q7 0 12-5t5-13z m72-404v529h-500v-529q0-12 4-22t8-15 6-5h464q2 0 6 5t8 15 4 22z m-375 601h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q23 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="comment-empty" unicode="&#xe847;" d="M500 636q-114 0-213-39t-157-105-59-142q0-62 40-119t113-98l48-28-15-53q-13-51-39-97 85 36 154 96l24 21 32-3q38-5 72-5 114 0 213 39t157 105 59 142-59 142-157 105-213 39z m500-286q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12h-3q-8 0-15 6t-9 15v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 97 67 179t182 130 251 48 251-48 182-130 67-179z" horiz-adv-x="1000" />
+
+<glyph glyph-name="comment" unicode="&#xe848;" d="M1000 350q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12-10-1-17 5t-10 16v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 73 40 139t106 114 160 76 194 28q136 0 251-48t182-130 67-179z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chat" unicode="&#xe849;" d="M786 421q0-77-53-143t-143-104-197-38q-48 0-98 9-70-49-155-72-21-5-48-9h-2q-6 0-12 5t-6 12q-1 1-1 3t1 4 1 3l1 3t2 3 2 3 3 3 2 2q3 3 13 14t15 16 12 17 14 21 11 25q-69 40-108 98t-40 125q0 78 53 144t143 104 197 38 197-38 143-104 53-144z m214-142q0-67-40-126t-108-98q5-14 11-25t14-21 13-16 14-17 13-14q0 0 2-2t3-3 2-3 2-3l1-3t1-3 1-4-1-3q-2-8-7-13t-12-4q-28 4-48 9-86 23-156 72-50-9-98-9-151 0-263 74 32-3 49-3 90 0 172 25t148 72q69 52 107 119t37 141q0 43-13 85 72-39 114-99t42-128z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chat-empty" unicode="&#xe84a;" d="M393 636q-85 0-160-29t-118-79-44-107q0-45 30-88t83-73l54-32-19-46q19 11 34 21l25 18 30-6q43-8 85-8 85 0 160 29t118 79 43 106-43 107-118 79-160 29z m0 71q106 0 197-38t143-104 53-144-53-143-143-104-197-38q-48 0-98 9-70-49-155-72-21-5-48-9h-2q-6 0-12 5t-6 12q-1 1-1 3t1 4 1 3l1 3t2 3 2 3 3 3 2 2q3 3 13 14t15 16 12 17 14 21 11 25q-69 40-108 98t-40 125q0 78 53 144t143 104 197 38z m459-652q5-14 11-25t14-21 13-16 14-17 13-14q0 0 2-2t3-3 2-3 2-3l1-3t1-3 1-4-1-3q-2-8-7-13t-12-4q-28 4-48 9-86 23-156 72-50-9-98-9-151 0-263 74 32-3 49-3 90 0 172 25t148 72q69 52 107 119t37 141q0 43-13 85 72-39 114-99t42-128q0-67-40-126t-108-98z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bell" unicode="&#xe84b;" d="M509-96q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-372 160h726q-149 168-149 465 0 28-13 58t-39 58-67 45-95 17-95-17-67-45-39-58-13-58q0-297-149-465z m827 0q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bell-alt" unicode="&#xe84c;" d="M509-96q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m455 160q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="attention-alt" unicode="&#xe84d;" d="M286 154v-125q0-15-11-25t-25-11h-143q-14 0-25 11t-11 25v125q0 14 11 25t25 10h143q15 0 25-10t11-25z m17 589l-16-429q-1-14-12-25t-25-10h-143q-14 0-25 10t-12 25l-15 429q-1 14 10 25t24 11h179q14 0 25-11t10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="print" unicode="&#xe84e;" d="M214-7h500v143h-500v-143z m0 357h500v214h-89q-22 0-38 16t-16 38v89h-357v-357z m643-36q0 15-10 25t-26 11-25-11-10-25 10-25 25-10 26 10 10 25z m72 0v-232q0-7-6-12t-12-6h-125v-89q0-22-16-38t-38-16h-536q-22 0-37 16t-16 38v89h-125q-7 0-13 6t-5 12v232q0 44 32 76t75 31h36v304q0 22 16 38t37 16h375q23 0 50-12t42-26l85-85q15-16 27-43t11-49v-143h35q45 0 76-31t32-76z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="edit" unicode="&#xe84f;" d="M496 189l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="forward" unicode="&#xe850;" d="M1000 493q0-15-11-25l-285-286q-11-11-25-11t-25 11-11 25v143h-125q-55 0-98-3t-86-12-74-24-59-39-45-56-27-77-10-101q0-31 3-69 0-4 2-13t1-15q0-8-5-14t-13-6q-9 0-15 10-4 5-8 12t-7 17-6 13q-71 159-71 252 0 111 30 186 90 225 488 225h125v143q0 14 11 25t25 10 25-10l285-286q11-11 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="reply" unicode="&#xe851;" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
+
+<glyph glyph-name="reply-all" unicode="&#xe852;" d="M357 246v-39q0-23-22-33-7-3-14-3-15 0-25 11l-285 286q-11 10-11 25t11 25l285 286q17 17 39 8 22-10 22-33v-39l-221-222q-11-11-11-25t11-25z m643-21q0-32-9-74t-22-77-27-70-22-51l-11-22q-5-10-16-10-3 0-5 1-14 4-13 19 24 223-59 315-36 40-95 62t-150 29v-140q0-23-21-33-8-3-14-3-15 0-25 11l-286 286q-11 10-11 25t11 25l286 286q16 17 39 8 21-10 21-33v-147q230-15 335-123 94-96 94-284z" horiz-adv-x="1000" />
+
+<glyph glyph-name="eye" unicode="&#xe853;" d="M929 314q-85 132-213 197 34-58 34-125 0-103-73-177t-177-73-177 73-73 177q0 67 34 125-128-65-213-197 75-114 187-182t242-68 243 68 186 182z m-402 215q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m473-215q0-19-11-38-78-129-210-206t-279-77-279 77-210 206q-11 19-11 38t11 39q78 128 210 205t279 78 279-78 210-205q11-20 11-39z" horiz-adv-x="1000" />
+
+<glyph glyph-name="tag" unicode="&#xe854;" d="M250 600q0 30-21 51t-50 20-51-20-21-51 21-50 51-21 50 21 21 50z m595-321q0-30-20-51l-274-274q-22-21-51-21-30 0-50 21l-399 399q-21 21-36 57t-15 65v232q0 29 21 50t50 22h233q29 0 65-15t57-36l399-399q20-21 20-50z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="tags" unicode="&#xe855;" d="M250 600q0 30-21 51t-50 20-51-20-21-51 21-50 51-21 50 21 21 50z m595-321q0-30-20-51l-274-274q-22-21-51-21-30 0-50 21l-399 399q-21 21-36 57t-15 65v232q0 29 21 50t50 22h233q29 0 65-15t57-36l399-399q20-21 20-50z m215 0q0-30-21-51l-274-274q-22-21-51-21-20 0-33 8t-29 25l262 262q21 21 21 51 0 29-21 50l-399 399q-21 21-57 36t-65 15h125q29 0 65-15t57-36l399-399q21-21 21-50z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="lock-open-alt" unicode="&#xe856;" d="M589 421q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="lock-open" unicode="&#xe857;" d="M929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="lock" unicode="&#xe858;" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="home" unicode="&#xe859;" d="M786 296v-267q0-15-11-25t-25-11h-214v214h-143v-214h-214q-15 0-25 11t-11 25v267q0 1 0 2t0 2l321 264 321-264q1-1 1-4z m124 39l-34-41q-5-5-12-6h-2q-7 0-12 3l-386 322-386-322q-7-4-13-3-7 1-12 6l-35 41q-4 6-3 13t6 12l401 334q18 15 42 15t43-15l136-113v108q0 8 5 13t13 5h107q8 0 13-5t5-13v-227l122-102q6-4 6-12t-4-13z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="info" unicode="&#xe85a;" d="M357 100v-71q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v71q0 15 11 25t25 11h35v214h-35q-15 0-25 11t-11 25v71q0 15 11 25t25 11h214q15 0 25-11t11-25v-321h35q15 0 26-11t10-25z m-71 643v-107q0-15-11-25t-25-11h-143q-14 0-25 11t-11 25v107q0 14 11 25t25 11h143q15 0 25-11t11-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="help" unicode="&#xe85b;" d="M393 149v-134q0-9-7-15t-15-7h-134q-9 0-16 7t-7 15v134q0 9 7 16t16 6h134q9 0 15-6t7-16z m176 335q0-30-8-56t-20-43-31-33-32-25-34-19q-23-13-38-37t-15-37q0-10-7-18t-16-9h-134q-8 0-14 11t-6 20v26q0 46 37 87t79 60q33 16 47 32t14 42q0 24-26 41t-60 18q-36 0-60-16-20-14-60-64-7-9-17-9-7 0-14 4l-91 70q-8 6-9 14t3 16q89 148 259 148 45 0 90-17t81-46 59-72 23-88z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="search" unicode="&#xe85c;" d="M643 386q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="flapping" unicode="&#xe85d;" d="M372 582q-34-52-77-153-12 25-20 41t-23 35-28 32-36 19-45 8h-125q-8 0-13 5t-5 13v107q0 8 5 13t13 5h125q139 0 229-125z m628-446q0-8-5-13l-179-179q-5-5-12-5-8 0-13 6t-5 12v107q-18 0-48 0t-45-1-41 1-39 3-36 6-35 10-32 16-33 22-31 30-31 39q33 52 76 152 12-25 20-40t23-36 28-31 35-20 46-8h143v107q0 8 5 13t13 5q6 0 13-5l178-178q5-5 5-13z m0 500q0-8-5-13l-179-179q-5-5-12-5-8 0-13 6t-5 12v107h-143q-27 0-49-8t-38-25-29-34-25-44q-18-34-43-95-16-37-28-62t-30-59-36-55-41-47-50-38-60-23-71-10h-125q-8 0-13 5t-5 13v107q0 8 5 13t13 5h125q27 0 48 9t39 25 28 34 26 43q17 35 43 96 16 36 28 62t30 58 36 56 41 46 50 39 59 23 72 9h143v107q0 8 5 13t13 5q6 0 13-5l178-178q5-5 5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="rewind" unicode="&#xe85e;" d="M532 736q170 0 289-120t119-290-119-290-289-120q-142 0-252 88l70 74q84-60 182-60 126 0 216 90t90 218-90 218-216 90q-124 0-214-87t-92-211l142 0-184-204-184 204 124 0q2 166 122 283t286 117z" horiz-adv-x="940" />
+
+<glyph glyph-name="chart-line" unicode="&#xe85f;" d="M1143-7v-72h-1143v858h71v-786h1072z m-72 696v-242q0-12-10-17t-20 4l-68 68-353-353q-6-6-13-6t-13 6l-130 130-232-233-107 108 327 326q5 6 12 6t13-6l130-130 259 259-67 68q-9 8-5 19t17 11h243q7 0 12-5t5-13z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="bell-off" unicode="&#xe860;" d="M869 375q35-199 167-311 0-29-21-50t-51-21h-250q0-59-42-101t-101-42-100 42-42 100z m-298-480q9 0 9 9t-9 8q-32 0-56 24t-24 57q0 9-9 9t-9-9q0-41 29-70t69-28z m560 893q4-6 4-14t-6-12l-1045-905q-5-5-13-4t-12 6l-47 53q-4 6-4 14t6 12l104 90q-11 17-11 36 28 24 51 49t47 67 42 89 28 115 11 145q0 84 65 157t171 89q-4 10-4 21 0 23 16 38t37 16 38-16 16-38q0-11-4-21 69-10 122-46t82-88l234 202q5 5 13 4t12-6z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="bell-off-empty" unicode="&#xe861;" d="M580-96q0 8-9 8-32 0-56 24t-24 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-299 265l489 424q-23 49-74 82t-125 32q-51 0-94-17t-68-45-38-58-14-58q0-215-76-360z m755-105q0-29-21-50t-51-21h-250q0-59-42-101t-101-42-100 42-42 100l83 72h422q-92 105-126 256l61 55q35-199 167-311z m48 777l47-53q4-6 4-14t-6-12l-1045-905q-5-5-13-4t-12 6l-47 53q-4 6-4 14t6 12l104 90q-11 17-11 36 28 24 51 49t47 67 42 89 28 115 11 145q0 84 65 157t171 89q-4 10-4 21 0 23 16 38t37 16 38-16 16-38q0-11-4-21 69-10 122-46t82-88l234 202q5 5 13 4t12-6z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="plug" unicode="&#xe862;" d="M979 597q21-21 21-50t-21-51l-223-223 83-84-89-89q-91-91-217-104t-230 56l-202-202h-101v101l202 202q-69 103-56 230t104 217l89 89 84-83 223 223q21 21 51 21t50-21 21-50-21-51l-223-223 131-131 223 223q22 21 51 21t50-21z" horiz-adv-x="1000" />
+
+<glyph glyph-name="eye-off" unicode="&#xe863;" d="M310 105l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
+
+<glyph glyph-name="arrows-cw" unicode="&#xe864;" d="M843 261q0-3 0-4-36-150-150-243t-267-93q-81 0-157 31t-136 88l-72-72q-11-11-25-11t-25 11-11 25v250q0 14 11 25t25 11h250q14 0 25-11t10-25-10-25l-77-77q40-36 90-57t105-20q74 0 139 37t104 99q6 10 30 66 4 13 16 13h107q8 0 13-6t5-12z m14 446v-250q0-14-10-25t-26-11h-250q-14 0-25 11t-10 25 10 25l77 77q-82 77-194 77-75 0-140-37t-104-99q-6-10-29-66-5-13-17-13h-111q-7 0-13 6t-5 12v4q36 150 151 243t268 93q81 0 158-31t137-88l72 72q11 11 25 11t26-11 10-25z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="cw" unicode="&#xe865;" d="M408 760q168 0 287-116t123-282l122 0-184-206-184 206 144 0q-4 124-94 210t-214 86q-126 0-216-90t-90-218q0-126 90-216t216-90q104 0 182 60l70-76q-110-88-252-88-168 0-288 120t-120 290 120 290 288 120z" horiz-adv-x="940" />
+
+<glyph glyph-name="host" unicode="&#xe866;" d="M232 136q-37 0-63 26t-26 63v393q0 37 26 63t63 26h607q37 0 63-26t27-63v-393q0-37-27-63t-63-26h-607z m-18 482v-393q0-7 6-13t12-5h607q8 0 13 5t5 13v393q0 7-5 12t-13 6h-607q-7 0-12-6t-6-12z m768-518h89v-54q0-22-26-37t-63-16h-893q-36 0-63 16t-26 37v54h982z m-402-54q9 0 9 9t-9 9h-89q-9 0-9-9t9-9h89z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="thumbs-up" unicode="&#xe867;" d="M143 100q0 15-11 25t-25 11-25-11-11-25 11-25 25-11 25 11 11 25z m643 321q0 29-22 50t-50 22h-196q0 32 27 89t26 89q0 55-17 81t-72 27q-14-15-21-48t-17-70-33-61q-13-13-43-51-2-3-13-16t-18-23-19-24-22-25-22-19-22-15-20-6h-18v-357h18q7 0 18-1t18-4 21-6 20-7 20-6 16-6q118-41 191-41h67q107 0 107 93 0 15-2 31 16 9 26 30t10 41-10 38q29 28 29 67 0 14-5 31t-14 26q18 1 30 26t12 45z m71 1q0-50-27-91 5-18 5-38 0-43-21-81 1-12 1-24 0-56-33-99 0-78-48-123t-126-45h-72q-54 0-106 13t-121 36q-65 23-77 23h-161q-29 0-50 21t-21 50v357q0 30 21 51t50 21h153q20 13 77 86 32 42 60 72 13 14 19 48t17 70 35 60q22 21 50 21 47 0 84-18t57-57 20-104q0-51-27-107h98q58 0 101-42t42-100z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="thumbs-down" unicode="&#xe868;" d="M143 600q0 15-11 25t-25 11-25-11-11-25 11-25 25-11 25 11 11 25z m643-321q0 19-12 45t-30 26q8 10 14 27t5 31q0 38-29 66 10 17 10 38 0 21-10 41t-26 30q2 16 2 31 0 47-27 70t-76 23h-71q-73 0-191-41-3-1-16-5t-20-7-20-7-21-6-18-4-18-1h-18v-357h18q9 0 20-5t22-15 22-20 22-25 19-24 18-22 13-17q30-38 43-51 23-24 33-61t17-70 21-48q54 0 72 27t17 81q0 33-26 89t-27 89h196q28 0 50 22t22 50z m71-1q0-57-42-100t-101-42h-98q27-55 27-107 0-66-20-104-19-39-57-57t-84-18q-28 0-50 21-19 18-30 45t-14 51-10 47-17 36q-27 28-60 71-57 73-77 86h-153q-29 0-50 21t-21 51v357q0 29 21 50t50 21h161q12 0 77 23 72 24 125 36t111 13h63q78 0 126-44t48-121v-3q33-43 33-99 0-12-1-24 21-38 21-80 0-21-5-39 27-41 27-91z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="spinner" unicode="&#xe869;" d="M294 72q0-29-21-50t-51-21q-29 0-50 21t-21 50q0 30 21 51t50 21 51-21 21-51z m277-115q0-29-20-50t-51-21-50 21-21 50 21 51 50 21 51-21 20-51z m-392 393q0-30-21-50t-51-21-50 21-21 50 21 51 50 20 51-20 21-51z m670-278q0-29-21-50t-50-21q-30 0-51 21t-20 50 20 51 51 21 50-21 21-51z m-538 556q0-37-26-63t-63-26-63 26-26 63 26 63 63 26 63-26 26-63z m653-278q0-30-21-50t-50-21-51 21-21 50 21 51 51 20 50-20 21-51z m-357 393q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m296-115q0-52-37-88t-88-37q-52 0-88 37t-37 88q0 51 37 88t88 37q51 0 88-37t37-88z" horiz-adv-x="1000" />
+
+<glyph glyph-name="attach" unicode="&#xe86a;" d="M784 77q0-65-45-109t-109-44q-75 0-131 55l-434 434q-63 64-63 151 0 89 62 150t150 62q88 0 152-63l338-338q5-5 5-12 0-9-17-26t-26-17q-7 0-12 5l-339 339q-44 43-101 43-59 0-100-42t-40-101q0-58 42-101l433-433q35-36 81-36 36 0 59 24t24 59q0 46-35 81l-325 324q-14 14-33 14-16 0-27-11t-11-27q0-18 14-33l229-228q6-6 6-13 0-9-18-26t-26-17q-6 0-12 5l-229 229q-35 34-35 83 0 46 32 78t77 32q49 0 84-35l324-325q56-54 56-131z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="keyboard" unicode="&#xe86b;" d="M214 198v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m72 143v-53q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h125q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m572-286v-53q0-9-9-9h-482q-9 0-9 9v53q0 9 9 9h482q9 0 9-9z m-357 143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-8-9h-54q-9 0-9 9v53q0 9 9 9h54q8 0 8-9z m-71 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m215-143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-286 286v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-196q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h62v134q0 9 9 9h54q9 0 9-9z m71-420v500h-929v-500h929z m71 500v-500q0-29-20-50t-51-21h-929q-29 0-50 21t-21 50v500q0 30 21 51t50 21h929q30 0 51-21t20-51z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="menu" unicode="&#xe86c;" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="wifi" unicode="&#xe86d;" d="M571 0q-11 0-51 41t-41 52q0 18 35 30t57 13 58-13 35-30q0-11-41-52t-52-41z m151 151q-1 0-22 14t-57 28-72 14-71-14-57-28-22-14q-10 0-52 42t-42 52q0 7 5 13 44 43 109 67t130 25 131-25 109-67q5-6 5-13 0-10-42-52t-52-42z m152 152q-6 0-12 5-76 58-141 86t-150 27q-47 0-95-12t-83-29-63-35-44-30-18-12q-9 0-51 42t-42 52q0 7 6 12 74 74 178 115t212 40 213-40 178-115q6-5 6-12 0-10-42-52t-52-42z m152 151q-6 0-13 5-99 88-207 132t-235 45-234-45-207-132q-7-5-13-5-9 0-51 42t-43 52q0 7 6 13 104 104 248 161t294 57 295-57 248-161q5-6 5-13 0-10-42-52t-51-42z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="moon" unicode="&#xe86e;" d="M704 123q-30-5-61-5-102 0-188 50t-137 137-50 188q0 107 58 199-112-33-183-128t-72-214q0-72 29-139t76-113 114-77 139-28q80 0 152 34t123 96z m114 47q-53-113-159-181t-230-68q-87 0-167 34t-136 92-92 137-34 166q0 85 32 163t87 135 132 92 161 38q25 1 34-22 11-23-8-40-48-43-73-101t-26-122q0-83 41-152t111-111 152-41q66 0 127 29 23 10 40-7 8-8 10-19t-2-22z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="chart-pie" unicode="&#xe86f;" d="M429 353l304-304q-59-61-138-94t-166-34q-117 0-216 58t-155 156-58 215 58 215 155 156 216 58v-426z m104-3h431q0-88-33-167t-94-138z m396 71h-429v429q117 0 215-57t156-156 58-216z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chart-area" unicode="&#xe870;" d="M1143-7v-72h-1143v858h71v-786h1072z m-214 571l142-500h-928v322l250 321 321-321z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="chart-bar" unicode="&#xe871;" d="M357 350v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="beaker" unicode="&#xe872;" d="M852 42q31-50 12-85t-78-36h-643q-59 0-78 36t12 85l280 443v222h-36q-14 0-25 11t-10 25 10 25 25 11h286q15 0 25-11t11-25-11-25-25-11h-36v-222z m-435 405l-151-240h397l-152 240-11 17v243h-71v-243z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="magic" unicode="&#xe873;" d="M664 526l164 163-60 60-164-163z m250 163q0-15-10-25l-718-718q-10-10-25-10t-25 10l-111 111q-10 10-10 25t10 25l718 718q10 10 25 10t25-10l111-111q10-10 10-25z m-754 106l54-16-54-17-17-55-17 55-55 17 55 16 17 55z m195-90l109-34-109-33-34-109-33 109-109 33 109 34 33 109z m519-267l55-17-55-16-17-55-17 55-54 16 54 17 17 55z m-357 357l54-16-54-17-17-55-17 55-54 17 54 16 17 55z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="spin6" unicode="&#xe874;" d="M855 9c-189-190-520-172-705 13-190 190-200 494-28 695 11 13 21 26 35 34 36 23 85 18 117-13 30-31 35-76 16-112-5-9-9-15-16-22-140-151-145-379-8-516 153-153 407-121 542 34 106 122 142 297 77 451-83 198-305 291-510 222l0 1c236 82 492-24 588-252 71-167 37-355-72-493-11-15-23-29-36-42z" horiz-adv-x="1000" />
+
+<glyph glyph-name="down-small" unicode="&#xe875;" d="M505 346q15-15 15-37t-15-37l-245-245-245 245q-15 15-15 37t15 37 37 15 37-15l120-119 0 395q0 21 15 36t36 15 37-15 16-36l0-395 120 119q15 15 36 15t36-15z" horiz-adv-x="520" />
+
+<glyph glyph-name="left-small" unicode="&#xe876;" d="M595 403q21 0 36-16t15-37-15-37-36-15l-395 0 119-119q15-15 15-37t-15-37-36-15q-23 0-38 15l-245 245 245 245q15 15 37 15t37-15 15-37-15-37l-119-118 395 0z" horiz-adv-x="646" />
+
+<glyph glyph-name="right-small" unicode="&#xe877;" d="M328 595q15 15 36 15t37-15l245-245-245-245q-15-15-36-15-22 0-37 15t-15 37 15 37l120 119-395 0q-22 0-37 15t-16 37 16 37 37 16l395 0-120 118q-15 15-15 37t15 37z" horiz-adv-x="646" />
+
+<glyph glyph-name="up-small" unicode="&#xe878;" d="M260 673l245-245q15-15 15-37t-15-37-36-15-36 15l-120 120 0-395q0-21-16-37t-37-15-36 15-15 37l0 395-120-120q-15-15-37-15t-37 15-15 37 15 37z" horiz-adv-x="520" />
+
+<glyph glyph-name="pin" unicode="&#xe879;" d="M573 37q0-23-15-38t-37-15q-21 0-37 16l-169 169-315-236 236 315-168 169q-24 23-12 56 14 32 48 32 157 0 270 57 90 45 151 171 9 24 36 32t50-13l208-209q21-23 14-50t-32-36q-127-63-172-152-56-110-56-268z" horiz-adv-x="834" />
+
+<glyph glyph-name="angle-double-left" unicode="&#xe87a;" d="M350 82q0-7-6-13l-28-28q-5-5-12-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q5 6 13 6t12-6l28-28q6-5 6-13t-6-12l-219-220 219-219q6-6 6-13z m214 0q0-7-5-13l-28-28q-6-5-13-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q6 6 13 6t13-6l28-28q5-5 5-13t-5-12l-220-220 220-219q5-6 5-13z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="angle-double-right" unicode="&#xe87b;" d="M332 314q0-7-5-12l-261-261q-5-5-12-5t-13 5l-28 28q-6 6-6 13t6 13l219 219-219 220q-6 5-6 12t6 13l28 28q5 6 13 6t12-6l261-260q5-5 5-13z m214 0q0-7-5-12l-260-261q-6-5-13-5t-13 5l-28 28q-5 6-5 13t5 13l219 219-219 220q-5 5-5 12t5 13l28 28q6 6 13 6t13-6l260-260q5-5 5-13z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="circle" unicode="&#xe87c;" d="M857 350q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="info-circled" unicode="&#xe87d;" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m52-152q-42 0-65-24t-23-50q-2-28 15-44t49-16q38 0 61 22t23 54q0 58-60 58z m-120-594q30 0 84 26t106 78l-18 24q-48-36-72-36-14 0-4 38l42 160q26 96-22 96-30 0-89-29t-115-75l16-26q52 34 74 34 12 0 0-34l-36-152q-26-104 34-104z" horiz-adv-x="920" />
+
+<glyph glyph-name="twitter" unicode="&#xe87e;" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="facebook-squared" unicode="&#xe87f;" d="M696 779q67 0 114-48t47-113v-536q0-66-47-113t-114-48h-104v333h111l16 129h-127v83q0 31 13 46t51 16l68 1v115q-35 5-100 5-75 0-121-44t-45-127v-95h-112v-129h112v-333h-297q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="gplus-squared" unicode="&#xe880;" d="M512 345q0 15-4 36h-202v-74h122q-2-13-10-28t-21-29-37-25-54-10q-55 0-94 40t-39 95 39 95 94 40q52 0 86-33l58 57q-60 55-144 55-89 0-151-62t-63-152 63-151 151-63q92 0 149 58t57 151z m192-26h61v62h-61v61h-61v-61h-61v-62h61v-61h61v61z m153 299v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="attention-circled" unicode="&#xe881;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m71-696v106q0 8-5 13t-12 5h-107q-8 0-13-5t-6-13v-106q0-8 6-13t13-6h107q7 0 12 6t5 13z m-1 192l10 346q0 7-6 10-5 5-13 5h-123q-8 0-13-5-6-3-6-10l10-346q0-6 5-10t14-4h103q8 0 13 4t6 10z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="check" unicode="&#xe883;" d="M786 331v-177q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-6-5-13-5-1 0-5 1-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v141q0 8 5 13l36 35q6 6 13 6 3 0 7-2 11-4 11-16z m129 273l-455-454q-13-14-31-14t-32 14l-240 240q-14 13-14 31t14 32l61 62q14 13 32 13t32-13l147-147 361 361q13 13 31 13t32-13l62-61q13-14 13-32t-13-32z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="reschedule" unicode="&#xe884;" d="M186 140l116 116 0-292-276 16 88 86q-116 122-114 290t120 288q100 100 240 116l4-102q-100-16-172-88-88-88-90-213t84-217z m332 598l276-16-88-86q116-122 114-290t-120-288q-96-98-240-118l-2 104q98 16 170 88 88 88 90 213t-84 217l-114-116z" horiz-adv-x="820" />
+
+<glyph glyph-name="warning-empty" unicode="&#xe885;" d="M514 701q-49 0-81-55l-308-513q-32-55-11-95t87-40l625 0q65 0 87 40t-12 95l-307 513q-33 55-80 55z m0 105q106 0 169-107l308-513q63-105 12-199-52-93-177-93l-625 0q-123 0-177 93-53 92 11 199l309 513q62 107 170 107z m-69-652q0 69 69 69 67 0 67-69 0-67-67-67-69 0-69 67z m146 313q0-14-6-29l-71-179q-44 108-73 179-6 15-6 29 0 32 23 55t56 24 55-24 22-55z" horiz-adv-x="1026" />
+
+<glyph glyph-name="th-list" unicode="&#xf009;" d="M0 62q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m234-576q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z" horiz-adv-x="937.5" />
+
+<glyph glyph-name="th-thumb-empty" unicode="&#xf00b;" d="M0-66v286q0 22 15 37t37 16h286q21 0 37-16t15-37v-286q0-21-15-36t-37-15h-286q-22 0-37 15t-15 36z m0 546v286q0 21 15 36t37 15h286q21 0 37-15t15-36v-286q0-22-15-37t-37-16h-286q-21 0-37 16t-15 37z m88-510h214v214h-214v-214z m0 546h214v213h-214v-213z m459-582v286q0 22 15 37t37 16h286q21 0 37-16t15-37v-286q0-21-15-36t-37-15h-286q-21 0-37 15t-15 36z m0 546v286q0 21 15 36t37 15h286q22 0 37-15t15-36v-286q0-22-15-37t-37-16h-286q-21 0-37 16t-15 37z m88-510h215v214h-215v-214z m0 546h215v213h-215v-213z" horiz-adv-x="937.5" />
+
+<glyph glyph-name="github-circled" unicode="&#xf09b;" d="M429 779q116 0 215-58t156-156 57-215q0-140-82-252t-211-155q-15-3-22 4t-7 17q0 1 0 43t0 75q0 54-29 79 32 3 57 10t53 22 45 37 30 58 11 84q0 67-44 115 21 51-4 114-16 5-46-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22-47 7q-25-63-5-114-44-48-44-115 0-47 12-83t29-59 45-37 52-22 57-10q-21-20-27-58-12-5-25-8t-32-3-36 12-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7 5-8 7-6l4-3q12-6 24-21t18-29l6-13q7-21 24-34t37-17 39-3 31 1l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156 216 58z m-267-616q2 4-3 7-6 1-8-1-1-4 4-7 5-3 7 1z m18-19q4 3-1 9-6 5-9 2-4-3 1-9 5-6 9-2z m16-25q6 4 0 11-4 7-9 3-5-3 0-10t9-4z m24-23q4 4-2 10-7 7-11 2-5-5 2-11 6-6 11-1z m32-14q1 6-8 9-8 2-10-4t7-9q8-3 11 4z m35-3q0 7-10 6-9 0-9-6 0-7 10-6 9 0 9 6z m32 5q-1 7-10 5-9-1-8-8t10-4 8 7z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="angle-double-up" unicode="&#xf102;" d="M600 118q0-7-6-13l-28-28q-5-5-12-5t-13 5l-220 219-219-219q-5-5-13-5t-12 5l-28 28q-6 6-6 13t6 13l260 260q5 5 12 5t13-5l260-260q6-6 6-13z m0 214q0-7-6-13l-28-28q-5-5-12-5t-13 5l-220 220-219-220q-5-5-13-5t-12 5l-28 28q-6 6-6 13t6 13l260 260q5 6 12 6t13-6l260-260q6-6 6-13z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="angle-double-down" unicode="&#xf103;" d="M600 368q0-7-6-13l-260-260q-5-6-13-6t-12 6l-260 260q-6 6-6 13t6 13l28 28q5 5 12 5t13-5l219-220 220 220q5 5 13 5t12-5l28-28q6-6 6-13z m0 214q0-7-6-13l-260-260q-5-5-13-5t-12 5l-260 260q-6 6-6 13t6 13l28 28q5 6 12 6t13-6l219-219 220 219q5 6 13 6t12-6l28-28q6-6 6-13z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="angle-left" unicode="&#xf104;" d="M350 546q0-7-6-12l-219-220 219-219q6-6 6-13t-6-13l-28-28q-5-5-12-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q5 6 13 6t12-6l28-28q6-5 6-13z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="angle-right" unicode="&#xf105;" d="M332 314q0-7-5-12l-261-261q-5-5-12-5t-13 5l-28 28q-6 6-6 13t6 13l219 219-219 220q-6 5-6 12t6 13l28 28q5 6 13 6t12-6l261-260q5-5 5-13z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="angle-up" unicode="&#xf106;" d="M600 189q0-7-6-12l-28-28q-5-6-12-6t-13 6l-220 219-219-219q-5-6-13-6t-12 6l-28 28q-6 5-6 12t6 13l260 260q5 6 12 6t13-6l260-260q6-5 6-13z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="angle-down" unicode="&#xf107;" d="M600 439q0-7-6-12l-260-261q-5-5-13-5t-12 5l-260 261q-6 5-6 12t6 13l28 28q5 6 12 6t13-6l219-219 220 219q5 6 13 6t12-6l28-28q6-5 6-13z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="history" unicode="&#xf1da;" d="M857 350q0-87-34-166t-91-137-137-92-166-34q-96 0-183 41t-147 114q-4 6-4 13t5 11l76 77q6 5 14 5 9-1 13-7 41-53 100-82t126-29q58 0 110 23t92 61 61 91 22 111-22 111-61 91-92 61-110 23q-55 0-105-20t-90-57l77-77q17-16 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l72-72q60 57 137 88t159 31q87 0 166-34t137-92 91-137 34-166z m-357 161v-250q0-8-5-13t-13-5h-178q-8 0-13 5t-5 13v35q0 8 5 13t13 5h125v197q0 8 5 13t12 5h36q8 0 13-5t5-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
+</font>
+</defs>
+</svg> \ 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
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.ttf
Binary files 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
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.woff
Binary files 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
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.woff2
Binary files 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 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Account;
+
+use Icinga\Authentication\User\DbUserBackend;
+use Icinga\Data\Filter\Filter;
+use Icinga\User;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for changing user passwords
+ */
+class ChangePasswordForm extends Form
+{
+ /**
+ * The user backend
+ *
+ * @var DbUserBackend
+ */
+ protected $backend;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setSubmitLabel($this->translate('Update Account'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'password',
+ 'old_password',
+ array(
+ 'label' => $this->translate('Old Password'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_password',
+ array(
+ 'label' => $this->translate('New Password'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_password_confirmation',
+ array(
+ 'label' => $this->translate('Confirm New Password'),
+ 'required' => true,
+ 'validators' => array(
+ array('identical', false, array('new_password'))
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $backend = $this->getBackend();
+ $backend->update(
+ $backend->getBaseTable(),
+ array('password' => $this->getElement('new_password')->getValue()),
+ Filter::where('user_name', $this->Auth()->getUser()->getUsername())
+ );
+ Notification::success($this->translate('Account updated'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+ if (! $valid) {
+ return false;
+ }
+
+ $oldPasswordEl = $this->getElement('old_password');
+
+ if (! $this->backend->authenticate($this->Auth()->getUser(), $oldPasswordEl->getValue())) {
+ $oldPasswordEl->addError($this->translate('Old password is invalid'));
+ $this->markAsError();
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the user backend
+ *
+ * @return DbUserBackend
+ */
+ public function getBackend()
+ {
+ return $this->backend;
+ }
+
+ /**
+ * Set the user backend
+ *
+ * @param DbUserBackend $backend
+ *
+ * @return $this
+ */
+ public function setBackend(DbUserBackend $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+}
diff --git a/application/forms/AcknowledgeApplicationStateMessageForm.php b/application/forms/AcknowledgeApplicationStateMessageForm.php
new file mode 100644
index 0000000..61f5824
--- /dev/null
+++ b/application/forms/AcknowledgeApplicationStateMessageForm.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\Hook\ApplicationStateHook;
+use Icinga\Web\ApplicationStateCookie;
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class AcknowledgeApplicationStateMessageForm extends Form
+{
+ public function init()
+ {
+ $this->setAction(Url::fromPath('application-state/acknowledge-message'));
+ $this->setAttrib('class', 'application-state-acknowledge-message-control');
+ $this->setRedirectUrl('application-state/summary');
+ }
+
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ [
+ 'class' => 'link-button spinner',
+ 'decorators' => [
+ 'ViewHelper',
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']]
+ ],
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Acknowledge message'),
+ 'type' => 'submit'
+ ]
+ );
+ return $this;
+ }
+
+ public function createElements(array $formData = [])
+ {
+ $this->addElements(
+ [
+ [
+ 'hidden',
+ 'id',
+ [
+ 'required' => true,
+ 'validators' => ['NotEmpty'],
+ 'decorators' => ['ViewHelper']
+ ]
+ ]
+ ]
+ );
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $cookie = new ApplicationStateCookie();
+
+ $ack = $cookie->getAcknowledgedMessages();
+ $ack[] = $this->getValue('id');
+
+ $active = ApplicationStateHook::getAllMessages();
+
+ $cookie->setAcknowledgedMessages(array_keys(array_intersect_key($active, array_flip($ack))));
+
+ $this->getResponse()->setCookie($cookie);
+
+ return true;
+ }
+}
diff --git a/application/forms/ActionForm.php b/application/forms/ActionForm.php
new file mode 100644
index 0000000..5b5b6ed
--- /dev/null
+++ b/application/forms/ActionForm.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Web\Form;
+
+class ActionForm extends Form
+{
+ /**
+ * The icon shown on the button
+ *
+ * @var string
+ */
+ protected $icon = 'arrows-cw';
+
+ /**
+ * Set the icon to show on the button
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setIcon($name)
+ {
+ $this->icon = (string) $name;
+ return $this;
+ }
+
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ $this->setUidDisabled(true);
+ $this->setDecorators(['FormElements', 'Form']);
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'hidden',
+ 'identifier',
+ [
+ 'required' => true,
+ 'decorators' => ['ViewHelper']
+ ]
+ );
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ [
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => ['ViewHelper'],
+ 'label' => $this->getView()->icon($this->icon),
+ 'title' => $this->getDescription()
+ ]
+ );
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid) {
+ $valid = ConfigFormEventsHook::runIsValid($this);
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ ConfigFormEventsHook::runOnSuccess($this);
+ }
+}
diff --git a/application/forms/Announcement/AcknowledgeAnnouncementForm.php b/application/forms/Announcement/AcknowledgeAnnouncementForm.php
new file mode 100644
index 0000000..85fecdc
--- /dev/null
+++ b/application/forms/Announcement/AcknowledgeAnnouncementForm.php
@@ -0,0 +1,92 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Announcement;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Web\Announcement\AnnouncementCookie;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class AcknowledgeAnnouncementForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAction(Url::fromPath('announcements/acknowledge'));
+ $this->setAttrib('class', 'acknowledge-announcement-control');
+ $this->setRedirectUrl('layout/announcements');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'class' => 'link-button spinner',
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Acknowledge this announcement'),
+ 'type' => 'submit'
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElements(
+ array(
+ array(
+ 'hidden',
+ 'hash',
+ array(
+ 'required' => true,
+ 'validators' => array('NotEmpty'),
+ 'decorators' => array('ViewHelper')
+ )
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $cookie = new AnnouncementCookie();
+ $repo = new AnnouncementIniRepository();
+ $query = $repo->findActive();
+ $filter = array();
+ foreach ($cookie->getAcknowledged() as $hash) {
+ $filter[] = Filter::expression('hash', '=', $hash);
+ }
+ $query->addFilter(Filter::matchAny($filter));
+ $acknowledged = array();
+ foreach ($query as $row) {
+ $acknowledged[] = $row->hash;
+ }
+ $acknowledged[] = $this->getElement('hash')->getValue();
+ $cookie->setAcknowledged($acknowledged);
+ $this->getResponse()->setCookie($cookie);
+ return true;
+ }
+}
diff --git a/application/forms/Announcement/AnnouncementForm.php b/application/forms/Announcement/AnnouncementForm.php
new file mode 100644
index 0000000..4da47e2
--- /dev/null
+++ b/application/forms/Announcement/AnnouncementForm.php
@@ -0,0 +1,135 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Announcement;
+
+use DateTime;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+
+/**
+ * Create, update and delete announcements
+ */
+class AnnouncementForm extends RepositoryForm
+{
+ protected function fetchEntry()
+ {
+ $entry = parent::fetchEntry();
+ if ($entry !== false) {
+ if ($entry->start !== null) {
+ $entry->start = (new DateTime())->setTimestamp($entry->start);
+ }
+ if ($entry->end !== null) {
+ $entry->end = (new DateTime())->setTimestamp($entry->end);
+ }
+ }
+
+ return $entry;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'author',
+ array(
+ 'disabled' => ! $this->getRequest()->isApiRequest(),
+ 'required' => true,
+ 'value' => Auth::getInstance()->getUser()->getUsername()
+ )
+ );
+ $this->addElement(
+ 'textarea',
+ 'message',
+ array(
+ 'description' => $this->translate('The message to display to users'),
+ 'label' => $this->translate('Message'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'dateTimePicker',
+ 'start',
+ array(
+ 'description' => $this->translate('The time to display the announcement from'),
+ 'label' => $this->translate('Start'),
+ 'placeholder' => new DateTime('tomorrow'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'dateTimePicker',
+ 'end',
+ array(
+ 'description' => $this->translate('The time to display the announcement until'),
+ 'label' => $this->translate('End'),
+ 'placeholder' => new DateTime('tomorrow +1day'),
+ 'required' => true
+ )
+ );
+
+ $this->setTitle($this->translate('Create a new announcement'));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+ /**
+ * {@inheritDoc}
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+ $this->setTitle(sprintf($this->translate('Edit announcement %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove announcement %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-controls');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createFilter()
+ {
+ return Filter::where('id', $this->getIdentifier());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getInsertMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement created')
+ : $this->translate('Failed to create announcement');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getUpdateMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement updated')
+ : $this->translate('Failed to update announcement');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDeleteMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement removed')
+ : $this->translate('Failed to remove announcement');
+ }
+}
diff --git a/application/forms/Authentication/LoginForm.php b/application/forms/Authentication/LoginForm.php
new file mode 100644
index 0000000..87b32ab
--- /dev/null
+++ b/application/forms/Authentication/LoginForm.php
@@ -0,0 +1,214 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Authentication;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Hook\AuthenticationHook;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Authentication\User\ExternalBackend;
+use Icinga\Common\Database;
+use Icinga\Exception\Http\HttpBadRequestException;
+use Icinga\User;
+use Icinga\Web\Form;
+use Icinga\Web\RememberMe;
+use Icinga\Web\Url;
+
+/**
+ * Form for user authentication
+ */
+class LoginForm extends Form
+{
+ use Database;
+
+ const DEFAULT_CLASSES = 'icinga-controls';
+
+ /**
+ * Redirect URL
+ */
+ const REDIRECT_URL = 'dashboard';
+
+ public static $defaultElementDecorators = [
+ ['ViewHelper', ['separator' => '']],
+ ['Help', []],
+ ['Errors', ['separator' => '']],
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group']]
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setRequiredCue(null);
+ $this->setName('form_login');
+ $this->setSubmitLabel($this->translate('Login'));
+ $this->setProgressLabel($this->translate('Logging in'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'username',
+ array(
+ 'autocapitalize' => 'off',
+ 'autocomplete' => 'username',
+ 'class' => false === isset($formData['username']) ? 'autofocus' : '',
+ 'placeholder' => $this->translate('Username'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'autocomplete' => 'current-password',
+ 'placeholder' => $this->translate('Password'),
+ 'class' => isset($formData['username']) ? 'autofocus' : ''
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'rememberme',
+ [
+ 'label' => $this->translate('Stay logged in'),
+ 'decorators' => [
+ ['ViewHelper', ['separator' => '']],
+ ['Label', [
+ 'tag' => 'span',
+ 'separator' => '',
+ 'class' => 'control-label',
+ 'placement' => 'APPEND'
+ ]],
+ ['Help', []],
+ ['Errors', ['separator' => '']],
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group remember-me-box']]
+ ]
+ ]
+ );
+ if (! RememberMe::isSupported()) {
+ $this->getElement('rememberme')
+ ->setAttrib('disabled', true)
+ ->setDescription($this->translate(
+ 'Staying logged in requires a database configuration backend'
+ . ' and an appropriate OpenSSL encryption method'
+ ));
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'redirect',
+ array(
+ 'value' => Url::fromRequest()->getParam('redirect')
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrl()
+ {
+ $redirect = null;
+ if ($this->created) {
+ $redirect = $this->getElement('redirect')->getValue();
+ }
+
+ if (empty($redirect) || strpos($redirect, 'authentication/logout') !== false) {
+ $redirect = static::REDIRECT_URL;
+ }
+
+ $redirectUrl = Url::fromPath($redirect);
+ if ($redirectUrl->isExternal()) {
+ throw new HttpBadRequestException('nope');
+ }
+
+ return $redirectUrl;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $auth = Auth::getInstance();
+ $authChain = $auth->getAuthChain();
+ $authChain->setSkipExternalBackends(true);
+ $user = new User($this->getElement('username')->getValue());
+ if (! $user->hasDomain()) {
+ $user->setDomain(Config::app()->get('authentication', 'default_domain'));
+ }
+ $password = $this->getElement('password')->getValue();
+ $authenticated = $authChain->authenticate($user, $password);
+ if ($authenticated) {
+ $auth->setAuthenticated($user);
+ if ($this->getElement('rememberme')->isChecked()) {
+ try {
+ $rememberMe = RememberMe::fromCredentials($user->getUsername(), $password);
+ $this->getResponse()->setCookie($rememberMe->getCookie());
+ $rememberMe->persist();
+ } catch (Exception $e) {
+ Logger::error('Failed to let user "%s" stay logged in: %s', $user->getUsername(), $e);
+ }
+ }
+
+ // Call provided AuthenticationHook(s) after successful login
+ AuthenticationHook::triggerLogin($user);
+ $this->getResponse()->setRerenderLayout(true);
+ return true;
+ }
+ switch ($authChain->getError()) {
+ case $authChain::EEMPTY:
+ $this->addError($this->translate(
+ 'No authentication methods available.'
+ . ' Did you create authentication.ini when setting up Icinga Web 2?'
+ ));
+ break;
+ case $authChain::EFAIL:
+ $this->addError($this->translate(
+ 'All configured authentication methods failed.'
+ . ' Please check the system log or Icinga Web 2 log for more information.'
+ ));
+ break;
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case $authChain::ENOTALL:
+ $this->addError($this->translate(
+ 'Please note that not all authentication methods were available.'
+ . ' Check the system log or Icinga Web 2 log for more information.'
+ ));
+ // Move to default
+ default:
+ $this->getElement('password')->addError($this->translate('Incorrect username or password'));
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onRequest()
+ {
+ $auth = Auth::getInstance();
+ $onlyExternal = true;
+ // TODO(el): This may be set on the auth chain once iterated. See Auth::authExternal().
+ foreach ($auth->getAuthChain() as $backend) {
+ if (! $backend instanceof ExternalBackend) {
+ $onlyExternal = false;
+ }
+ }
+ if ($onlyExternal) {
+ $this->addError($this->translate(
+ 'You\'re currently not authenticated using any of the web server\'s authentication mechanisms.'
+ . ' Make sure you\'ll configure such, otherwise you\'ll not be able to login.'
+ ));
+ }
+ }
+}
diff --git a/application/forms/AutoRefreshForm.php b/application/forms/AutoRefreshForm.php
new file mode 100644
index 0000000..122f635
--- /dev/null
+++ b/application/forms/AutoRefreshForm.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\Logger;
+use Icinga\User\Preferences;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Session;
+use Icinga\Web\Url;
+
+/**
+ * Form class to adjust user auto refresh preferences
+ */
+class AutoRefreshForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_auto_refresh');
+ // Post against the current location
+ $this->setAction('');
+ }
+
+ /**
+ * Adjust preferences and persist them
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ /** @var Preferences $preferences */
+ $preferences = $this->getRequest()->getUser()->getPreferences();
+ $icingaweb = $preferences->get('icingaweb');
+
+ if ((bool) $preferences->getValue('icingaweb', 'auto_refresh', true) === false) {
+ $icingaweb['auto_refresh'] = '1';
+ $notification = $this->translate('Auto refresh successfully enabled');
+ } else {
+ $icingaweb['auto_refresh'] = '0';
+ $notification = $this->translate('Auto refresh successfully disabled');
+ }
+ $preferences->icingaweb = $icingaweb;
+
+ Session::getSession()->user->setPreferences($preferences);
+ Notification::success($notification);
+
+ $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes');
+ $this->setRedirectUrl(Url::fromRequest()->without('renderLayout'));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $preferences = $this->getRequest()->getUser()->getPreferences();
+
+ if ((bool) $preferences->getValue('icingaweb', 'auto_refresh', true) === false) {
+ $value = $this->translate('Enable auto refresh');
+ } else {
+ $value = $this->translate('Disable auto refresh');
+ }
+
+ $this->addElements(array(
+ array(
+ 'button',
+ 'btn_submit',
+ array(
+ 'ignore' => true,
+ 'type' => 'submit',
+ 'value' => $value,
+ 'decorators' => array('ViewHelper'),
+ 'escape' => false,
+ 'class' => 'link-like'
+ )
+ )
+ ));
+ }
+}
diff --git a/application/forms/Config/General/ApplicationConfigForm.php b/application/forms/Config/General/ApplicationConfigForm.php
new file mode 100644
index 0000000..21f76a1
--- /dev/null
+++ b/application/forms/Config/General/ApplicationConfigForm.php
@@ -0,0 +1,105 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\ResourceFactory;
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for general application options
+ *
+ * This form is not used directly but as subform to the {@link GeneralConfigForm}.
+ */
+class ApplicationConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_application');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'global_show_stacktraces',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Stacktraces'),
+ 'description' => $this->translate(
+ 'Set whether to show an exception\'s stacktrace by default. This can also'
+ . ' be set in a user\'s preferences with the appropriate permission.'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'global_show_application_state_messages',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Application State Messages'),
+ 'description' => $this->translate(
+ "Set whether to show application state messages."
+ . " This can also be set in a user's preferences."
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'security_use_strict_csp',
+ [
+ 'label' => $this->translate('Enable strict content security policy'),
+ 'description' => $this->translate(
+ 'Set whether to to use strict content security policy (CSP).'
+ . ' This setting helps to protect from cross-site scripting (XSS).'
+ )
+ ]
+ );
+
+ $this->addElement(
+ 'text',
+ 'global_module_path',
+ array(
+ 'label' => $this->translate('Module Path'),
+ 'required' => true,
+ 'value' => implode(':', Icinga::app()->getModuleManager()->getModuleDirs()),
+ 'description' => $this->translate(
+ 'Contains the directories that will be searched for available modules, separated by '
+ . 'colons. Modules that don\'t exist in these directories can still be symlinked in '
+ . 'the module folder, but won\'t show up in the list of disabled modules.'
+ )
+ )
+ );
+
+ $backends = array_keys(ResourceFactory::getResourceConfigs()->toArray());
+ $backends = array_combine($backends, $backends);
+
+ $this->addElement(
+ 'select',
+ 'global_config_resource',
+ array(
+ 'required' => true,
+ 'multiOptions' => array_merge(
+ ['' => sprintf(' - %s - ', $this->translate('Please choose'))],
+ $backends
+ ),
+ 'disable' => [''],
+ 'value' => '',
+ 'label' => $this->translate('Configuration Database')
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
new file mode 100644
index 0000000..0ff6c32
--- /dev/null
+++ b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
@@ -0,0 +1,46 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for the default domain for authentication
+ *
+ * This form is not used directly but as subform to the {@link GeneralConfigForm}.
+ */
+class DefaultAuthenticationDomainConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_authentication');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'authentication_default_domain',
+ array(
+ 'label' => $this->translate('Default Login Domain'),
+ 'description' => $this->translate(
+ 'If a user logs in without specifying any domain (e.g. "jdoe" instead of "jdoe@example.com"),'
+ . ' this default domain will be assumed for the user. Note that if none your LDAP authentication'
+ . ' backends are configured to be responsible for this domain or if none of your authentication'
+ . ' backends holds usernames with the domain part, users will not be able to login.'
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/LoggingConfigForm.php b/application/forms/Config/General/LoggingConfigForm.php
new file mode 100644
index 0000000..bbc7723
--- /dev/null
+++ b/application/forms/Config/General/LoggingConfigForm.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Logger;
+use Icinga\Application\Logger\Writer\SyslogWriter;
+use Icinga\Application\Platform;
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for logging options
+ *
+ * This form is not used directly but as subform for the {@link GeneralConfigForm}.
+ */
+class LoggingConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_logging');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $defaultType = getenv('ICINGAWEB_OFFICIAL_DOCKER_IMAGE') ? 'php' : 'syslog';
+
+ $this->addElement(
+ 'select',
+ 'logging_log',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Logging Type'),
+ 'description' => $this->translate('The type of logging to utilize.'),
+ 'value' => $defaultType,
+ 'multiOptions' => array(
+ 'syslog' => 'Syslog',
+ 'php' => $this->translate('Webserver Log', 'app.config.logging.type'),
+ 'file' => $this->translate('File', 'app.config.logging.type'),
+ 'none' => $this->translate('None', 'app.config.logging.type')
+ )
+ )
+ );
+
+ if (! isset($formData['logging_log']) || $formData['logging_log'] !== 'none') {
+ $this->addElement(
+ 'select',
+ 'logging_level',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Logging Level'),
+ 'description' => $this->translate('The maximum logging level to emit.'),
+ 'multiOptions' => array(
+ Logger::$levels[Logger::ERROR] => $this->translate('Error', 'app.config.logging.level'),
+ Logger::$levels[Logger::WARNING] => $this->translate('Warning', 'app.config.logging.level'),
+ Logger::$levels[Logger::INFO] => $this->translate('Information', 'app.config.logging.level'),
+ Logger::$levels[Logger::DEBUG] => $this->translate('Debug', 'app.config.logging.level')
+ )
+ )
+ );
+ }
+
+ if (! isset($formData['logging_log']) || in_array($formData['logging_log'], array('syslog', 'php'))) {
+ $this->addElement(
+ 'text',
+ 'logging_application',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Application Prefix'),
+ 'description' => $this->translate(
+ 'The name of the application by which to prefix log messages.'
+ ),
+ 'requirement' => $this->translate('The application prefix must not contain whitespace.'),
+ 'value' => 'icingaweb2',
+ 'validators' => array(
+ array(
+ 'Regex',
+ false,
+ array(
+ 'pattern' => '/^\S+$/',
+ 'messages' => array(
+ 'regexNotMatch' => $this->translate(
+ 'The application prefix must not contain whitespace.'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ if ((isset($formData['logging_log']) ? $formData['logging_log'] : $defaultType) === 'syslog') {
+ if (Platform::isWindows()) {
+ /* @see https://secure.php.net/manual/en/function.openlog.php */
+ $this->addElement(
+ 'hidden',
+ 'logging_facility',
+ array(
+ 'value' => 'user',
+ 'disabled' => true
+ )
+ );
+ } else {
+ $facilities = array_keys(SyslogWriter::$facilities);
+ $this->addElement(
+ 'select',
+ 'logging_facility',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Facility'),
+ 'description' => $this->translate('The syslog facility to utilize.'),
+ 'value' => 'user',
+ 'multiOptions' => array_combine($facilities, $facilities)
+ )
+ );
+ }
+ }
+ } elseif (isset($formData['logging_log']) && $formData['logging_log'] === 'file') {
+ $this->addElement(
+ 'text',
+ 'logging_file',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('File path'),
+ 'description' => $this->translate('The full path to the log file to write messages to.'),
+ 'value' => '/var/log/icingaweb2/icingaweb2.log',
+ 'validators' => array('WritablePathValidator')
+ )
+ );
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/ThemingConfigForm.php b/application/forms/Config/General/ThemingConfigForm.php
new file mode 100644
index 0000000..54ef2b1
--- /dev/null
+++ b/application/forms/Config/General/ThemingConfigForm.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Web\Form;
+use Icinga\Web\StyleSheet;
+
+/**
+ * Configuration form for theming options
+ *
+ * This form is not used directly but as subform for the {@link GeneralConfigForm}.
+ */
+class ThemingConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_theming');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $themes = Icinga::app()->getThemes();
+ $themes[StyleSheet::DEFAULT_THEME] .= ' (' . $this->translate('default') . ')';
+
+ $this->addElement(
+ 'select',
+ 'themes_default',
+ array(
+ 'description' => $this->translate('The default theme', 'Form element description'),
+ 'disabled' => count($themes) < 2 ? 'disabled' : null,
+ 'label' => $this->translate('Default Theme', 'Form element label'),
+ 'multiOptions' => $themes,
+ 'value' => StyleSheet::DEFAULT_THEME
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'themes_disabled',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for disallowing users to change the theme. If a default theme is set, it will be'
+ . ' used nonetheless',
+ 'Form element description'
+ ),
+ 'label' => $this->translate('Users Can\'t Change Theme', 'Form element label')
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ if ($values['themes_default'] === '' || $values['themes_default'] === StyleSheet::DEFAULT_THEME) {
+ $values['themes_default'] = null;
+ }
+ if (! $values['themes_disabled']) {
+ $values['themes_disabled'] = null;
+ }
+ return $values;
+ }
+}
diff --git a/application/forms/Config/GeneralConfigForm.php b/application/forms/Config/GeneralConfigForm.php
new file mode 100644
index 0000000..5f15512
--- /dev/null
+++ b/application/forms/Config/GeneralConfigForm.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Forms\Config\General\ApplicationConfigForm;
+use Icinga\Forms\Config\General\DefaultAuthenticationDomainConfigForm;
+use Icinga\Forms\Config\General\LoggingConfigForm;
+use Icinga\Forms\Config\General\ThemingConfigForm;
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Configuration form for application-wide options
+ */
+class GeneralConfigForm extends ConfigForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $appConfigForm = new ApplicationConfigForm();
+ $loggingConfigForm = new LoggingConfigForm();
+ $themingConfigForm = new ThemingConfigForm();
+ $domainConfigForm = new DefaultAuthenticationDomainConfigForm();
+ $this->addSubForm($appConfigForm->create($formData));
+ $this->addSubForm($loggingConfigForm->create($formData));
+ $this->addSubForm($themingConfigForm->create($formData));
+ $this->addSubForm($domainConfigForm->create($formData));
+ }
+}
diff --git a/application/forms/Config/Resource/DbResourceForm.php b/application/forms/Config/Resource/DbResourceForm.php
new file mode 100644
index 0000000..c9d7601
--- /dev/null
+++ b/application/forms/Config/Resource/DbResourceForm.php
@@ -0,0 +1,239 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Application\Platform;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying database resources
+ */
+class DbResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_db');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $dbChoices = array();
+ if (Platform::hasMysqlSupport()) {
+ $dbChoices['mysql'] = 'MySQL';
+ }
+ if (Platform::hasPostgresqlSupport()) {
+ $dbChoices['pgsql'] = 'PostgreSQL';
+ }
+ if (Platform::hasMssqlSupport()) {
+ $dbChoices['mssql'] = 'MSSQL';
+ }
+ if (Platform::hasIbmSupport()) {
+ $dbChoices['ibm'] = 'IBM (DB2)';
+ }
+ if (Platform::hasOracleSupport()) {
+ $dbChoices['oracle'] = 'Oracle';
+ }
+ if (Platform::hasOciSupport()) {
+ $dbChoices['oci'] = 'Oracle (OCI8)';
+ }
+ if (Platform::hasSqliteSupport()) {
+ $dbChoices['sqlite'] = 'SQLite';
+ }
+
+ $offerPostgres = false;
+ $offerMysql = false;
+ $dbChoice = isset($formData['db']) ? $formData['db'] : key($dbChoices);
+ if ($dbChoice === 'pgsql') {
+ $offerPostgres = true;
+ } elseif ($dbChoice === 'mysql') {
+ $offerMysql = true;
+ }
+
+ if ($dbChoice === 'oracle' || $dbChoice === 'oci') {
+ $hostIsRequired = false;
+ } else {
+ $hostIsRequired = true;
+ }
+
+ $socketInfo = '';
+ if ($offerPostgres) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify the path to the unix domain socket directory'
+ );
+ } elseif ($offerMysql) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify localhost'
+ );
+ }
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'db',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Database Type'),
+ 'description' => $this->translate('The type of SQL database'),
+ 'multiOptions' => $dbChoices
+ )
+ );
+ if ($dbChoice === 'sqlite') {
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ } else {
+ $this->addElement(
+ 'text',
+ 'host',
+ array (
+ 'required' => $hostIsRequired,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate('The hostname of the database')
+ . ($socketInfo ? '. ' . $socketInfo : ''),
+ 'value' => $hostIsRequired ? 'localhost' : ''
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'description' => $this->translate('The port to use'),
+ 'label' => $this->translate('Port'),
+ 'preserveDefault' => true,
+ 'required' => $offerPostgres,
+ 'value' => $offerPostgres ? 5432 : null
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'username',
+ array (
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate('The user name to use for authentication')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'renderPassword' => true,
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password to use for authentication'),
+ 'autocomplete' => 'new-password'
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'charset',
+ array (
+ 'description' => $this->translate('The character set for the database'),
+ 'label' => $this->translate('Character Set')
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'use_ssl',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('Use SSL'),
+ 'description' => $this->translate(
+ 'Whether to encrypt the connection or to authenticate using certificates'
+ )
+ )
+ );
+ if (isset($formData['use_ssl']) && $formData['use_ssl']) {
+ if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
+ $this->addElement(
+ 'checkbox',
+ 'ssl_do_not_verify_server_cert',
+ array(
+ 'label' => $this->translate('SSL Do Not Verify Server Certificate'),
+ 'description' => $this->translate(
+ 'Whether to disable verification of the server certificate'
+ )
+ )
+ );
+ }
+ $this->addElement(
+ 'text',
+ 'ssl_key',
+ array(
+ 'label' => $this->translate('SSL Key'),
+ 'description' => $this->translate('The client key file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cert',
+ array(
+ 'label' => $this->translate('SSL Certificate'),
+ 'description' => $this->translate('The certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_ca',
+ array(
+ 'label' => $this->translate('SSL CA'),
+ 'description' => $this->translate('The CA certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_capath',
+ array(
+ 'label' => $this->translate('SSL CA Path'),
+ 'description' => $this->translate(
+ 'The trusted CA certificates in PEM format directory path'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cipher',
+ array(
+ 'label' => $this->translate('SSL Cipher'),
+ 'description' => $this->translate('The list of permissible ciphers')
+ )
+ );
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/FileResourceForm.php b/application/forms/Config/Resource/FileResourceForm.php
new file mode 100644
index 0000000..b98f1b4
--- /dev/null
+++ b/application/forms/Config/Resource/FileResourceForm.php
@@ -0,0 +1,67 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Zend_Validate_Callback;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying file resources
+ */
+class FileResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_file');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filename',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Filepath'),
+ 'description' => $this->translate('The filename to fetch information from'),
+ 'validators' => array('ReadablePathValidator')
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'fields',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Pattern'),
+ 'description' => $this->translate('The pattern by which to identify columns.'),
+ 'requirement' => $this->translate('The column pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/LdapResourceForm.php b/application/forms/Config/Resource/LdapResourceForm.php
new file mode 100644
index 0000000..7ffccdc
--- /dev/null
+++ b/application/forms/Config/Resource/LdapResourceForm.php
@@ -0,0 +1,129 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+use Icinga\Protocol\Ldap\LdapConnection;
+
+/**
+ * Form class for adding/modifying ldap resources
+ */
+class LdapResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_ldap');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $defaultPort = ! array_key_exists('encryption', $formData) || $formData['encryption'] !== LdapConnection::LDAPS
+ ? 389
+ : 636;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'hostname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate(
+ 'The hostname or address of the LDAP server to use for authentication.'
+ . ' You can also provide multiple hosts separated by a space'
+ ),
+ 'value' => 'localhost'
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'required' => true,
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Port'),
+ 'description' => $this->translate('The port of the LDAP server to use for authentication'),
+ 'value' => $defaultPort
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'encryption',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Encryption'),
+ 'description' => $this->translate(
+ 'Whether to encrypt communication. Choose STARTTLS or LDAPS for encrypted communication or'
+ . ' none for unencrypted communication'
+ ),
+ 'multiOptions' => array(
+ 'none' => $this->translate('None', 'resource.ldap.encryption'),
+ LdapConnection::STARTTLS => 'STARTTLS',
+ LdapConnection::LDAPS => 'LDAPS'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'root_dn',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Root DN'),
+ 'description' => $this->translate(
+ 'Only the root and its child nodes will be accessible on this resource.'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'bind_dn',
+ array(
+ 'label' => $this->translate('Bind DN'),
+ 'description' => $this->translate(
+ 'The user dn to use for querying the ldap server. Leave the dn and password empty for attempting'
+ . ' an anonymous bind'
+ )
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'bind_pw',
+ array(
+ 'renderPassword' => true,
+ 'label' => $this->translate('Bind Password'),
+ 'description' => $this->translate('The password to use for querying the ldap server')
+ )
+ );
+
+ $this->addElement(
+ 'number',
+ 'timeout',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Timeout'),
+ 'description' => $this->translate('Connection timeout for every LDAP connection'),
+ 'value' => 5 // see LdapConnection::__construct()
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/SshResourceForm.php b/application/forms/Config/Resource/SshResourceForm.php
new file mode 100644
index 0000000..a15dc8c
--- /dev/null
+++ b/application/forms/Config/Resource/SshResourceForm.php
@@ -0,0 +1,148 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\ConfigObject;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Web\Form;
+use Icinga\Util\File;
+use Zend_Validate_Callback;
+
+/**
+ * Form class for adding/modifying ssh identity resources
+ */
+class SshResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_ssh');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('User'),
+ 'description' => $this->translate(
+ 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be'
+ . ' possible for this user'
+ )
+ )
+ );
+
+ if ($this->getRequest()->getActionName() != 'editresource') {
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ if (substr(ltrim($value), 0, 7) === 'file://'
+ || openssl_pkey_get_private($value) === false
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('The given SSH key is invalid'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'private_key',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Private Key'),
+ 'description' => $this->translate('The private key which will be used for the SSH connections'),
+ 'class' => 'resource ssh-identity',
+ 'validators' => array($callbackValidator)
+ )
+ );
+ } else {
+ $resourceName = $formData['name'];
+ $this->addElement(
+ 'note',
+ 'private_key_note',
+ array(
+ 'escape' => false,
+ 'label' => $this->translate('Private Key'),
+ 'value' => sprintf(
+ '<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>',
+ $this->getView()->url('config/removeresource', array('resource' => $resourceName)),
+ $this->getView()->escape(sprintf($this->translate(
+ 'Remove the %s resource'
+ ), $resourceName)),
+ $this->translate('To modify the private key you must recreate this resource.')
+ )
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove the assigned key to the resource
+ *
+ * @param ConfigObject $config
+ *
+ * @return bool
+ */
+ public static function beforeRemove(ConfigObject $config)
+ {
+ $file = $config->private_key;
+
+ if (file_exists($file)) {
+ unlink($file);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Creates the assigned key to the resource
+ *
+ * @param ResourceConfigForm $form
+ *
+ * @return bool
+ */
+ public static function beforeAdd(ResourceConfigForm $form)
+ {
+ $configDir = Icinga::app()->getConfigDir();
+ $user = $form->getElement('user')->getValue();
+
+ $filePath = join(DIRECTORY_SEPARATOR, [$configDir, 'ssh', sha1($user)]);
+ if (! file_exists($filePath)) {
+ $file = File::create($filePath, 0600);
+ } else {
+ $form->error(
+ sprintf($form->translate('The private key for the user "%s" already exists.'), $user)
+ );
+ return false;
+ }
+
+ $file->fwrite($form->getElement('private_key')->getValue());
+
+ $form->getElement('private_key')->setValue($filePath);
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/ResourceConfigForm.php b/application/forms/Config/ResourceConfigForm.php
new file mode 100644
index 0000000..c2d0d18
--- /dev/null
+++ b/application/forms/Config/ResourceConfigForm.php
@@ -0,0 +1,442 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Application\Config;
+use InvalidArgumentException;
+use Icinga\Application\Platform;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\Config\Resource\DbResourceForm;
+use Icinga\Forms\Config\Resource\FileResourceForm;
+use Icinga\Forms\Config\Resource\LdapResourceForm;
+use Icinga\Forms\Config\Resource\SshResourceForm;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Zend_Form_Element;
+
+class ResourceConfigForm extends ConfigForm
+{
+ /**
+ * Bogus password when inspecting password elements
+ *
+ * @var string
+ */
+ protected static $dummyPassword = '_web_form_5847ed1b5b8ca';
+
+ /**
+ * If the global config must be updated because a resource has been changed, this is the updated global config
+ *
+ * @var Config|null
+ */
+ protected $updatedAppConfig = null;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * Return a form object for the given resource type
+ *
+ * @param string $type The resource type for which to return a form
+ *
+ * @return Form
+ */
+ public function getResourceForm($type)
+ {
+ if ($type === 'db') {
+ return new DbResourceForm();
+ } elseif ($type === 'ldap') {
+ return new LdapResourceForm();
+ } elseif ($type === 'file') {
+ return new FileResourceForm();
+ } elseif ($type === 'ssh') {
+ return new SshResourceForm();
+ } else {
+ throw new InvalidArgumentException(sprintf($this->translate('Invalid resource type "%s" provided'), $type));
+ }
+ }
+
+ /**
+ * Add a particular resource
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $values The values to extend the configuration with
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case the resource does already exist
+ */
+ public function add(array $values)
+ {
+ $name = isset($values['name']) ? $values['name'] : '';
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif ($this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Resource already exists'));
+ }
+
+ unset($values['name']);
+ $this->config->setSection($name, $values);
+ return $this;
+ }
+
+ /**
+ * Edit a particular resource
+ *
+ * @param string $name The name of the resource to edit
+ * @param array $values The values to edit the configuration with
+ *
+ * @return ConfigObject The edited configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function edit($name, array $values)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Old resource name missing'));
+ } elseif (! ($newName = isset($values['name']) ? $values['name'] : '')) {
+ throw new InvalidArgumentException($this->translate('New resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $this->config->removeSection($name);
+ unset($values['name']);
+ $this->config->setSection($newName, $resourceConfig->merge($values));
+
+ if ($newName !== $name) {
+ $appConfig = Config::app();
+ $section = $appConfig->getSection('global');
+ if ($section->config_resource === $name) {
+ $section->config_resource = $newName;
+ $this->updatedAppConfig = $appConfig->setSection('global', $section);
+ }
+ }
+
+ return $resourceConfig;
+ }
+
+ /**
+ * Remove a particular resource
+ *
+ * @param string $name The name of the resource to remove
+ *
+ * @return ConfigObject The removed resource configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function remove($name)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $resourceForm = $this->getResourceForm($resourceConfig->type);
+ if (method_exists($resourceForm, 'beforeRemove')) {
+ $resourceForm::beforeRemove($resourceConfig);
+ }
+
+ $this->config->removeSection($name);
+ return $resourceConfig;
+ }
+
+ /**
+ * Add or edit a resource and save the configuration
+ *
+ * Performs a connectivity validation using the submitted values. A checkbox is
+ * added to the form to skip the check if it fails and redirection is aborted.
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $resourceForm = $this->getResourceForm($this->getElement('type')->getValue());
+
+ if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ $this->addElement($this->getForceCreationCheckbox());
+ return false;
+ }
+ }
+
+ $resource = $this->request->getQuery('resource');
+ try {
+ if ($resource === null) { // create new resource
+ if (method_exists($resourceForm, 'beforeAdd')) {
+ if (! $resourceForm::beforeAdd($this)) {
+ return false;
+ }
+ }
+ $this->add(static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully created');
+ } else { // edit existing resource
+ $this->edit($resource, static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully changed');
+ }
+ } catch (InvalidArgumentException $e) {
+ Notification::error($e->getMessage());
+ return false;
+ }
+
+ if ($this->save()) {
+ Notification::success(sprintf($message, $this->getElement('name')->getValue()));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Populate the form in case a resource is being edited
+ *
+ * @see Form::onRequest()
+ *
+ * @throws ConfigurationError In case the backend name is missing in the request or is invalid
+ */
+ public function onRequest()
+ {
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null) {
+ if ($resource === '') {
+ throw new ConfigurationError($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($resource)) {
+ throw new ConfigurationError($this->translate('Unknown resource provided'));
+ }
+ $configValues = $this->config->getSection($resource)->toArray();
+ $configValues['name'] = $resource;
+ $this->populate($configValues);
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password' && strlen($element->getValue())) {
+ $element->setValue(static::$dummyPassword);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a checkbox to be displayed at the beginning of the form
+ * which allows the user to skip the connection validation
+ *
+ * @return Zend_Form_Element
+ */
+ protected function getForceCreationCheckbox()
+ {
+ return $this->createElement(
+ 'checkbox',
+ 'force_creation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Force Changes'),
+ 'description' => $this->translate('Check this box to enforce changes without connectivity validation')
+ )
+ );
+ }
+
+ /**
+ * @see Form::createElemeents()
+ */
+ public function createElements(array $formData)
+ {
+ $resourceType = isset($formData['type']) ? $formData['type'] : 'db';
+
+ $resourceTypes = array(
+ 'file' => $this->translate('File'),
+ 'ssh' => $this->translate('SSH Identity'),
+ );
+ if ($resourceType === 'ldap' || Platform::hasLdapSupport()) {
+ $resourceTypes['ldap'] = 'LDAP';
+ }
+ if ($resourceType === 'db' || Platform::hasDatabaseSupport()) {
+ $resourceTypes['db'] = $this->translate('SQL Database');
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Resource Type'),
+ 'description' => $this->translate('The type of resource'),
+ 'multiOptions' => $resourceTypes,
+ 'value' => $resourceType
+ )
+ );
+
+ if (isset($formData['force_creation']) && $formData['force_creation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addElement($this->getForceCreationCheckbox());
+ }
+
+ $this->addElements($this->getResourceForm($resourceType)->createElements($formData)->getElements());
+ }
+
+ /**
+ * Create a resource by using the given form's values and return its inspection results
+ *
+ * @param Form $form
+ *
+ * @return ?Inspection
+ */
+ public static function inspectResource(Form $form)
+ {
+ if ($form->getValue('type') !== 'ssh') {
+ $resource = ResourceFactory::createResource(new ConfigObject($form->getValues()));
+ if ($resource instanceof Inspectable) {
+ return $resource->inspect();
+ }
+ }
+ }
+
+ /**
+ * Run the configured resource's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'resource_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if ($this->getElement('resource_validation')->isChecked() && parent::isValid($formData)) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'resource_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'resource-progress');
+ $this->addElement(
+ 'note',
+ 'resource-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'resource-progress'))
+ )
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('btn_submit', 'resource_validation', 'resource-progress'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null && $this->config->hasSection($resource)) {
+ $resourceConfig = $this->config->getSection($resource)->toArray();
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password') {
+ $name = $element->getName();
+ if (isset($values[$name]) && $values[$name] === static::$dummyPassword) {
+ if (isset($resourceConfig[$name])) {
+ $values[$name] = $resourceConfig[$name];
+ } else {
+ unset($values[$name]);
+ }
+ }
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function writeConfig(Config $config)
+ {
+ parent::writeConfig($config);
+ if ($this->updatedAppConfig !== null) {
+ $this->updatedAppConfig->saveIni();
+ }
+ }
+}
diff --git a/application/forms/Config/User/CreateMembershipForm.php b/application/forms/Config/User/CreateMembershipForm.php
new file mode 100644
index 0000000..6c74c8c
--- /dev/null
+++ b/application/forms/Config/User/CreateMembershipForm.php
@@ -0,0 +1,192 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\User;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for creating one or more group memberships
+ */
+class CreateMembershipForm extends Form
+{
+ /**
+ * The user group backends to fetch groups from
+ *
+ * Each backend must implement the Icinga\Data\Extensible and Icinga\Data\Selectable interface.
+ *
+ * @var array
+ */
+ protected $backends;
+
+ /**
+ * The username to create memberships for
+ *
+ * @var string
+ */
+ protected $userName;
+
+ /**
+ * Set the user group backends to fetch groups from
+ *
+ * @param array $backends
+ *
+ * @return $this
+ */
+ public function setBackends($backends)
+ {
+ $this->backends = $backends;
+ return $this;
+ }
+
+ /**
+ * Set the username to create memberships for
+ *
+ * @param string $userName
+ *
+ * @return $this
+ */
+ public function setUsername($userName)
+ {
+ $this->userName = $userName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $query = $this->createDataSource()->select()->from('group', array('group_name', 'backend_name'));
+
+ $options = array();
+ foreach ($query as $row) {
+ $options[$row->backend_name . ';' . $row->group_name] = $row->group_name . ' (' . $row->backend_name . ')';
+ }
+
+ $this->addElement(
+ 'multiselect',
+ 'groups',
+ array(
+ 'required' => true,
+ 'multiOptions' => $options,
+ 'label' => $this->translate('Groups'),
+ 'description' => sprintf(
+ $this->translate('Select one or more groups where to add %s as member'),
+ $this->userName
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Create memberships for %s'), $this->userName));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+
+ /**
+ * Instantly redirect back in case the user is already a member of all groups
+ */
+ public function onRequest()
+ {
+ if ($this->createDataSource()->select()->from('group')->count() === 0) {
+ Notification::info(sprintf($this->translate('User %s is already a member of all groups'), $this->userName));
+ $this->getResponse()->redirectAndExit($this->getRedirectUrl());
+ }
+ }
+
+ /**
+ * Create the memberships for the user
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $backendMap = array();
+ foreach ($this->backends as $backend) {
+ $backendMap[$backend->getName()] = $backend;
+ }
+
+ $single = null;
+ $groupName = null;
+ foreach ($this->getValue('groups') as $backendAndGroup) {
+ list($backendName, $groupName) = explode(';', $backendAndGroup, 2);
+ try {
+ $backendMap[$backendName]->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $groupName,
+ 'user_name' => $this->userName
+ )
+ );
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $this->userName,
+ $groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(
+ sprintf($this->translate('Membership for group %s created successfully'), $groupName)
+ );
+ } else {
+ Notification::success($this->translate('Memberships created successfully'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Create and return a data source to fetch all groups from all backends where the user is not already a member of
+ *
+ * @return ArrayDatasource
+ */
+ protected function createDataSource()
+ {
+ $groups = $failures = array();
+ foreach ($this->backends as $backend) {
+ try {
+ $memberships = $backend
+ ->select()
+ ->from('group_membership', array('group_name'))
+ ->where('user_name', $this->userName)
+ ->fetchColumn();
+ foreach ($backend->select(array('group_name')) as $row) {
+ if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter
+ $row->backend_name = $backend->getName();
+ $groups[] = $row;
+ }
+ }
+ } catch (Exception $e) {
+ $failures[] = array($backend->getName(), $e);
+ }
+ }
+
+ if (empty($groups) && !empty($failures)) {
+ // In case there are only failures, throw the very first exception again
+ throw $failures[0][1];
+ } elseif (! empty($failures)) {
+ foreach ($failures as $failure) {
+ Logger::error($failure[1]);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any groups from backend %s. Please check your log'),
+ $failure[0]
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+}
diff --git a/application/forms/Config/User/UserForm.php b/application/forms/Config/User/UserForm.php
new file mode 100644
index 0000000..fb2ef4d
--- /dev/null
+++ b/application/forms/Config/User/UserForm.php
@@ -0,0 +1,210 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\User;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Web\Notification;
+
+class UserForm extends RepositoryForm
+{
+ /**
+ * Create and add elements to this form to insert or update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'is_active',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Active'),
+ 'description' => $this->translate('Prevents the user from logging in if unchecked')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Password')
+ )
+ );
+
+ $this->setTitle($this->translate('Add a new user'));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Create and add elements to this form to update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'description' => $this->translate('Leave empty for not updating the user\'s password'),
+ 'label' => $this->translate('Password'),
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Edit user %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * Update a user
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('user_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('user', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * Strips off the password if null or the empty string.
+ *
+ * @param bool $suppressArrayNotation
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ // before checking if password values is empty
+ // we have to check that the password field is set
+ // otherwise an error is thrown
+ if (isset($values['password']) && ! $values['password']) {
+ unset($values['password']);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Create and add elements to this form to delete a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove user %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a user
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('user_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('User added successfully');
+ } else {
+ return $this->translate('Failed to add user');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}
diff --git a/application/forms/Config/UserBackend/DbBackendForm.php b/application/forms/Config/UserBackend/DbBackendForm.php
new file mode 100644
index 0000000..693ea14
--- /dev/null
+++ b/application/forms/Config/UserBackend/DbBackendForm.php
@@ -0,0 +1,82 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying database user backends
+ */
+class DbBackendForm extends Form
+{
+ /**
+ * The database resource names the user can choose from
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_db');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate(
+ 'The database connection to use for authenticating with this provider'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array()
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'db'
+ )
+ );
+ }
+}
diff --git a/application/forms/Config/UserBackend/ExternalBackendForm.php b/application/forms/Config/UserBackend/ExternalBackendForm.php
new file mode 100644
index 0000000..f4a4639
--- /dev/null
+++ b/application/forms/Config/UserBackend/ExternalBackendForm.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Zend_Validate_Callback;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying user backends of type "external"
+ */
+class ExternalBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_external');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'strip_username_regexp',
+ array(
+ 'label' => $this->translate('Filter Pattern'),
+ 'description' => $this->translate(
+ 'The filter to use to strip specific parts off from usernames.'
+ . ' Leave empty if you do not want to strip off anything.'
+ ),
+ 'requirement' => $this->translate('The filter pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'external'
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * Validate the configuration by creating a backend and requesting the user count
+ *
+ * Returns always true as backends of type "external" are just "passive" backends.
+ *
+ * @param Form $form The form to fetch the configuration values from
+ *
+ * @return bool Whether validation succeeded or not
+ */
+ public static function isValidUserBackend(Form $form)
+ {
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserBackend/LdapBackendForm.php b/application/forms/Config/UserBackend/LdapBackendForm.php
new file mode 100644
index 0000000..e7804cc
--- /dev/null
+++ b/application/forms/Config/UserBackend/LdapBackendForm.php
@@ -0,0 +1,414 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Exception;
+use Icinga\Data\ResourceFactory;
+use Icinga\Protocol\Ldap\LdapCapabilities;
+use Icinga\Protocol\Ldap\LdapConnection;
+use Icinga\Protocol\Ldap\LdapException;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying LDAP user backends
+ */
+class LdapBackendForm extends Form
+{
+ /**
+ * The ldap resource names the user can choose from
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * Default values for the form elements
+ *
+ * @var string[]
+ */
+ protected $suggestions = array();
+
+ /**
+ * Cache for {@link getLdapCapabilities()}
+ *
+ * @var LdapCapabilities
+ */
+ protected $ldapCapabilities;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_ldap');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $isAd = isset($formData['type']) ? $formData['type'] === 'msldap' : false;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others.'
+ ),
+ 'value' => $this->getSuggestion('name')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate(
+ 'The LDAP connection to use for authenticating with this provider.'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array(),
+ 'value' => $this->getSuggestion('resource')
+ )
+ );
+
+ if (! $isAd && !empty($this->resources)) {
+ $this->addElement(
+ 'button',
+ 'discovery_btn',
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => 'discovery_btn',
+ 'label' => $this->translate('Discover', 'A button to discover LDAP capabilities'),
+ 'title' => $this->translate(
+ 'Push to fill in the chosen connection\'s default settings.'
+ ),
+ 'decorators' => array(
+ array('ViewHelper', array('separator' => '')),
+ array('Spinner'),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ }
+
+ if ($isAd) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $this->getSuggestion('user_class', $userClass)
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'value' => $this->getSuggestion('filter', $filter),
+ 'label' => $this->translate('LDAP Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression.'
+ . ' (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ // This is not meant to be a full syntax check. It will just
+ // ensure that we can safely strip unnecessary parentheses.
+ $v = trim($v);
+ return ! $v || $v[0] !== '(' || (
+ strpos($v, ')(') !== false ? substr($v, -2) === '))' : substr($v, -1) === ')'
+ );
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter is invalid. Please check your syntax.')
+ )
+ )
+ )
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing the user name on the LDAP server.'
+ ),
+ 'value' => $this->getSuggestion('user_name_attribute', $userNameAttribute)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => $this->getSuggestion('backend', $isAd ? 'msldap' : 'ldap')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'required' => false,
+ 'label' => $this->translate('LDAP Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $this->getSuggestion('base_dn')
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Domain'),
+ 'description' => $this->translate(
+ 'The domain the LDAP server is responsible for upon authentication.'
+ . ' Note that if you specify a domain here,'
+ . ' the LDAP backend only authenticates users who specify a domain upon login.'
+ . ' If the domain of the user matches the domain configured here, this backend is responsible for'
+ . ' authenticating the user based on the username without the domain part.'
+ . ' If your LDAP backend holds usernames with a domain part or if it is not necessary in your setup'
+ . ' to authenticate users based on their domains, leave this field empty.'
+ ),
+ 'preserveDefault' => true,
+ 'value' => $this->getSuggestion('domain')
+ )
+ );
+
+ $this->addElement(
+ 'button',
+ 'btn_discover_domain',
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => 'discovery_btn',
+ 'label' => $this->translate('Discover the domain'),
+ 'title' => $this->translate(
+ 'Push to disover and fill in the domain of the LDAP server.'
+ ),
+ 'decorators' => array(
+ array('ViewHelper', array('separator' => '')),
+ array('Spinner'),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ }
+
+ public function isValidPartial(array $formData)
+ {
+ $isAd = isset($formData['type']) && $formData['type'] === 'msldap';
+ $baseDn = null;
+ $hasAdOid = false;
+ $discoverySuccessful = false;
+
+ if (! $isAd && ! empty($this->resources) && isset($formData['discovery_btn'])
+ && $formData['discovery_btn'] === 'discovery_btn') {
+ $discoverySuccessful = true;
+ try {
+ $capabilities = $this->getLdapCapabilities($formData);
+ $baseDn = $capabilities->getDefaultNamingContext();
+ $hasAdOid = $capabilities->isActiveDirectory();
+ } catch (Exception $e) {
+ $this->warning(sprintf(
+ $this->translate('Failed to discover the chosen LDAP connection: %s'),
+ $e->getMessage()
+ ));
+ $discoverySuccessful = false;
+ }
+ }
+
+ if ($discoverySuccessful) {
+ if ($isAd || $hasAdOid) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $formData['user_class'] = $userClass;
+
+ if (! isset($formData['filter']) || $formData['filter'] === '') {
+ $formData['filter'] = $filter;
+ }
+
+ $formData['user_name_attribute'] = $userNameAttribute;
+
+ if ($baseDn !== null && (! isset($formData['base_dn']) || $formData['base_dn'] === '')) {
+ $formData['base_dn'] = $baseDn;
+ }
+ }
+
+ if (isset($formData['btn_discover_domain']) && $formData['btn_discover_domain'] === 'discovery_btn') {
+ try {
+ $formData['domain'] = $this->discoverDomain($formData);
+ } catch (LdapException $e) {
+ $this->error($e->getMessage());
+ }
+ }
+
+ return parent::isValidPartial($formData);
+ }
+
+ /**
+ * Get the LDAP capabilities of either the resource specified by the user or the default one
+ *
+ * @param string[] $formData
+ *
+ * @return LdapCapabilities
+ */
+ protected function getLdapCapabilities(array $formData)
+ {
+ if ($this->ldapCapabilities === null) {
+ $this->ldapCapabilities = ResourceFactory::create(
+ isset($formData['resource']) ? $formData['resource'] : reset($this->resources)
+ )->bind()->getCapabilities();
+ }
+
+ return $this->ldapCapabilities;
+ }
+
+ /**
+ * Discover the domain the LDAP server is responsible for
+ *
+ * @param string[] $formData
+ *
+ * @return string
+ */
+ protected function discoverDomain(array $formData)
+ {
+ $cap = $this->getLdapCapabilities($formData);
+
+ if ($cap->isActiveDirectory()) {
+ $netBiosName = $cap->getNetBiosName();
+ if ($netBiosName !== null) {
+ return $netBiosName;
+ }
+ }
+
+ return $this->defaultNamingContextToFQDN($cap);
+ }
+
+ /**
+ * Get the default naming context as FQDN
+ *
+ * @param LdapCapabilities $cap
+ *
+ * @return string|null
+ */
+ protected function defaultNamingContextToFQDN(LdapCapabilities $cap)
+ {
+ $defaultNamingContext = $cap->getDefaultNamingContext();
+ if ($defaultNamingContext !== null) {
+ $validationMatches = array();
+ if (preg_match('/\bdc=[^,]+(?:,dc=[^,]+)*$/', strtolower($defaultNamingContext), $validationMatches)) {
+ $splitMatches = array();
+ preg_match_all('/dc=([^,]+)/', $validationMatches[0], $splitMatches);
+ return implode('.', $splitMatches[1]);
+ }
+ }
+ }
+
+ /**
+ * Get the default values for the form elements
+ *
+ * @return string[]
+ */
+ public function getSuggestions()
+ {
+ return $this->suggestions;
+ }
+
+ /**
+ * Get the default value for the given form element or the given default
+ *
+ * @param string $element
+ * @param string $default
+ *
+ * @return string
+ */
+ public function getSuggestion($element, $default = null)
+ {
+ return isset($this->suggestions[$element]) ? $this->suggestions[$element] : $default;
+ }
+
+ /**
+ * Set the default values for the form elements
+ *
+ * @param string[] $suggestions
+ *
+ * @return $this
+ */
+ public function setSuggestions(array $suggestions)
+ {
+ $this->suggestions = $suggestions;
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php
new file mode 100644
index 0000000..fdca657
--- /dev/null
+++ b/application/forms/Config/UserBackendConfigForm.php
@@ -0,0 +1,482 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
+use Icinga\Forms\Config\UserBackend\DbBackendForm;
+use Icinga\Forms\Config\UserBackend\LdapBackendForm;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing user backends
+ */
+class UserBackendConfigForm extends ConfigForm
+{
+ /**
+ * The available user backend resources split by type
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
+ /**
+ * The loaded custom backends list
+ *
+ * @var array
+ */
+ protected $customBackends;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ $this->customBackends = UserBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param Config $resourceConfig The resource configuration
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError In case there are no valid resources for authentication available
+ */
+ public function setResourceConfig(Config $resourceConfig)
+ {
+ $resources = array();
+ foreach ($resourceConfig as $name => $resource) {
+ if (in_array($resource->type, array('db', 'ldap'))) {
+ $resources[$resource->type][] = $name;
+ }
+ }
+
+ if (empty($resources)) {
+ $externalBackends = $this->config->toArray();
+ array_walk(
+ $externalBackends,
+ function (&$authBackendCfg) {
+ if (! isset($authBackendCfg['backend']) || $authBackendCfg['backend'] !== 'external') {
+ $authBackendCfg = null;
+ }
+ }
+ );
+ if (count(array_filter($externalBackends)) > 0 && (
+ $this->backendToLoad === null || !isset($externalBackends[$this->backendToLoad])
+ )) {
+ throw new ConfigurationError($this->translate(
+ 'Could not find any valid user backend resources.'
+ . ' Please configure a resource for authentication first.'
+ ));
+ }
+ }
+
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ $form = new DbBackendForm();
+ $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array());
+ break;
+ case 'ldap':
+ case 'msldap':
+ $form = new LdapBackendForm();
+ $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array());
+ break;
+ case 'external':
+ $form = new ExternalBackendForm();
+ break;
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+
+ return $form;
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException(
+ $this->translate('A user backend with the name "%s" does already exist'),
+ $backendName
+ );
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $backendConfig->merge($data);
+ $this->config->setSection($name, $backendConfig);
+ return $this;
+ }
+
+ /**
+ * Remove a user backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Move the given user backend up or down in order
+ *
+ * @param string $name The name of the backend to be moved
+ * @param int $position The new (absolute) position of the backend
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function move($name, $position)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendOrder = $this->config->keys();
+ array_splice($backendOrder, array_search($name, $backendOrder), 1);
+ array_splice($backendOrder, $position, 0, $name);
+
+ $newConfig = array();
+ foreach ($backendOrder as $backendName) {
+ $newConfig[$backendName] = $this->config->getSection($backendName);
+ }
+
+ $config = Config::fromArray($newConfig);
+ $this->config = $config->setConfigFile($this->config->getConfigFile());
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array();
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+
+ if (isset($this->resources['db'])) {
+ $backendTypes['db'] = $this->translate('Database');
+ }
+ if (isset($this->resources['ldap'])) {
+ $backendTypes['ldap'] = 'LDAP';
+ $backendTypes['msldap'] = 'ActiveDirectory';
+ }
+
+ $externalBackends = array_filter(
+ $this->config->toArray(),
+ function ($authBackendCfg) {
+ return isset($authBackendCfg['backend']) && $authBackendCfg['backend'] === 'external';
+ }
+ );
+ if ($backendType === 'external' || empty($externalBackends)) {
+ $backendTypes['external'] = $this->translate('External');
+ }
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate(
+ 'The type of the resource to use for this authenticaton provider'
+ ),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addSkipValidationCheckbox();
+ }
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['name'] = $this->backendToLoad;
+ $data['type'] = $data['backend'];
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($el = $this->getElement('skip_validation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ if ($el === null) {
+ $this->addSkipValidationCheckbox();
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a user backend by using the given form's values and return its inspection results
+ *
+ * Returns null for non-inspectable backends.
+ *
+ * @param Form $form
+ *
+ * @return Inspection|null
+ */
+ public static function inspectUserBackend(Form $form)
+ {
+ $backend = UserBackend::create(null, new ConfigObject($form->getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the connection validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this box to enforce changes without validating that authentication is possible.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (! parent::isValidPartial($formData)) {
+ return false;
+ }
+
+ if ($this->getElement('backend_validation')->isChecked() && parent::isValid($formData)) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendReorderForm.php b/application/forms/Config/UserBackendReorderForm.php
new file mode 100644
index 0000000..019c032
--- /dev/null
+++ b/application/forms/Config/UserBackendReorderForm.php
@@ -0,0 +1,86 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Application\Config;
+use Icinga\Forms\ConfigForm;
+use Icinga\Exception\NotFoundError;
+use Icinga\Web\Notification;
+
+class UserBackendReorderForm extends ConfigForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_reorder_authbackend');
+ $this->setViewScript('form/reorder-authbackend.phtml');
+ }
+
+ /**
+ * Return the ordered backend names
+ *
+ * @return array
+ */
+ public function getBackendOrder()
+ {
+ return $this->config->keys();
+ }
+
+ /**
+ * Return the ordered backend configuration
+ *
+ * @return Config
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ // This adds just a dummy element to be able to utilize Form::getValue as part of onSuccess()
+ $this->addElement('hidden', 'backend_newpos');
+ }
+
+ /**
+ * Update the user backend order and save the configuration
+ */
+ public function onSuccess()
+ {
+ $newPosData = $this->getValue('backend_newpos');
+ if ($newPosData) {
+ $configForm = $this->getConfigForm();
+ list($backendName, $position) = explode('|', $newPosData, 2);
+
+ try {
+ if ($configForm->move($backendName, $position)->save()) {
+ Notification::success($this->translate('Authentication order updated'));
+ } else {
+ return false;
+ }
+ } catch (NotFoundError $_) {
+ Notification::error(sprintf($this->translate('User backend "%s" not found'), $backendName));
+ }
+ }
+ }
+
+ /**
+ * Return the config form for user backends
+ *
+ * @return ConfigForm
+ */
+ protected function getConfigForm()
+ {
+ $form = new UserBackendConfigForm();
+ $form->setIniConfig($this->config);
+ return $form;
+ }
+}
diff --git a/application/forms/Config/UserGroup/AddMemberForm.php b/application/forms/Config/UserGroup/AddMemberForm.php
new file mode 100644
index 0000000..cda9d52
--- /dev/null
+++ b/application/forms/Config/UserGroup/AddMemberForm.php
@@ -0,0 +1,183 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Exception;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Selectable;
+use Icinga\Exception\NotFoundError;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for adding one or more group members
+ */
+class AddMemberForm extends Form
+{
+ /**
+ * The data source to fetch users from
+ *
+ * @var Selectable
+ */
+ protected $ds;
+
+ /**
+ * The user group backend to use
+ *
+ * @var Extensible
+ */
+ protected $backend;
+
+ /**
+ * The group to add members for
+ *
+ * @var string
+ */
+ protected $groupName;
+
+ /**
+ * Set the data source to fetch users from
+ *
+ * @param Selectable $ds
+ *
+ * @return $this
+ */
+ public function setDataSource(Selectable $ds)
+ {
+ $this->ds = $ds;
+ return $this;
+ }
+
+ /**
+ * Set the user group backend to use
+ *
+ * @param Extensible $backend
+ *
+ * @return $this
+ */
+ public function setBackend(Extensible $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ /**
+ * Set the group to add members for
+ *
+ * @param string $groupName
+ *
+ * @return $this
+ */
+ public function setGroupName($groupName)
+ {
+ $this->groupName = $groupName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ // TODO(jom): Fetching already existing members to prevent the user from mistakenly creating duplicate
+ // memberships (no matter whether the data source permits it or not, a member does never need to be
+ // added more than once) should be kept at backend level (GroupController::fetchUsers) but this does
+ // not work currently as our ldap protocol stuff is unable to handle our filter implementation..
+ $members = $this->backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $this->groupName)
+ ->fetchColumn();
+ $filter = empty($members) ? Filter::matchAll() : Filter::not(Filter::where('user_name', $members));
+
+ $users = $this->ds->select()->from('user', array('user_name'))->applyFilter($filter)->fetchColumn();
+ if (! empty($users)) {
+ $this->addElement(
+ 'multiselect',
+ 'user_name',
+ array(
+ 'multiOptions' => array_combine($users, $users),
+ 'label' => $this->translate('Backend Users'),
+ 'description' => $this->translate(
+ 'Select one or more users (fetched from your user backends) to add as group member'
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'required' => empty($users),
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Provide one or more usernames separated by comma to add as group member'
+ )
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Add members for group %s'), $this->groupName));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Insert the members for the group
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $userNames = $this->getValue('user_name') ?: array();
+ if (($users = $this->getValue('users'))) {
+ $userNames = array_merge($userNames, array_map('trim', explode(',', $users)));
+ }
+
+ if (empty($userNames)) {
+ $this->info($this->translate(
+ 'Please provide at least one username, either by choosing one '
+ . 'in the list or by manually typing one in the text box below'
+ ));
+ return false;
+ }
+
+ $single = null;
+ $userName = null;
+ foreach ($userNames as $userName) {
+ try {
+ $this->backend->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $this->groupName,
+ 'user_name' => $userName
+ )
+ );
+ } catch (NotFoundError $e) {
+ throw $e; // Trigger 404, the group name is initially accessed as GET parameter
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $userName,
+ $this->groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(sprintf($this->translate('Group member "%s" added successfully'), $userName));
+ } else {
+ Notification::success($this->translate('Group members added successfully'));
+ }
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserGroup/DbUserGroupBackendForm.php b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
new file mode 100644
index 0000000..daea8de
--- /dev/null
+++ b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
@@ -0,0 +1,79 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Data\ResourceFactory;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing database user group backends
+ */
+class DbUserGroupBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_dbusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getDatabaseResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate('The database connection to use for this backend'),
+ 'multiOptions' => empty($resourceNames) ? array() : array_combine($resourceNames, $resourceNames)
+ )
+ );
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => 'db'
+ )
+ );
+ }
+
+ /**
+ * Return the names of all configured database resources
+ *
+ * @return array
+ */
+ protected function getDatabaseResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (strtolower($config->type) === 'db') {
+ $names[] = $name;
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
new file mode 100644
index 0000000..10c069a
--- /dev/null
+++ b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
@@ -0,0 +1,370 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\UserGroup\LdapUserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Protocol\Ldap\LdapConnection;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for managing LDAP user group backends
+ */
+class LdapUserGroupBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_ldapusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getLdapResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate('The LDAP connection to use for this backend.'),
+ 'multiOptions' => array_combine($resourceNames, $resourceNames)
+ )
+ );
+ $resource = ResourceFactory::create(
+ isset($formData['resource']) && in_array($formData['resource'], $resourceNames)
+ ? $formData['resource']
+ : $resourceNames[0]
+ );
+
+ $userBackendNames = $this->getLdapUserBackendNames($resource);
+ if (! empty($userBackendNames)) {
+ $userBackends = array_combine($userBackendNames, $userBackendNames);
+ $userBackends['none'] = $this->translate('None', 'usergroupbackend.ldap.user_backend');
+ } else {
+ $userBackends = array('none' => $this->translate('None', 'usergroupbackend.ldap.user_backend'));
+ }
+ $this->addElement(
+ 'select',
+ 'user_backend',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Backend'),
+ 'description' => $this->translate('The user backend to link with this user group backend.'),
+ 'multiOptions' => $userBackends
+ )
+ );
+
+ $groupBackend = new LdapUserGroupBackend($resource);
+ if ($formData['type'] === 'ldap') {
+ $defaults = $groupBackend->getOpenLdapDefaults();
+ $groupConfigDisabled = $userConfigDisabled = null; // MUST BE null, do NOT change this to false!
+ } else { // $formData['type'] === 'msldap'
+ $defaults = $groupBackend->getActiveDirectoryDefaults();
+ $groupConfigDisabled = $userConfigDisabled = true;
+ }
+
+ if ($formData['type'] === 'msldap') {
+ $this->addElement(
+ 'checkbox',
+ 'nested_group_search',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for nested group search in Active Directory based on the user'
+ ),
+ 'label' => $this->translate('Nested Group Search')
+ )
+ );
+ } else {
+ // This is required to purge already present options
+ $this->addElement('hidden', 'nested_group_search', array('disabled' => true));
+ }
+
+ $this->createGroupConfigElements($defaults, $groupConfigDisabled);
+ if (count($userBackends) === 1 || (isset($formData['user_backend']) && $formData['user_backend'] === 'none')) {
+ $this->createUserConfigElements($defaults, $userConfigDisabled);
+ } else {
+ $this->createHiddenUserConfigElements();
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => $formData['type']
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the group configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createGroupConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'group_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Object Class'),
+ 'description' => $this->translate('The object class used for storing groups on the LDAP server.'),
+ 'value' => $defaults->group_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP Group Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up groups using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->group_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a group\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->group_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_member_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Member Attribute'),
+ 'description' => $this->translate('The attribute name used for storing a group\'s members.'),
+ 'value' => $defaults->group_member_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP Group Base DN'),
+ 'description' => $this->translate(
+ 'The path where groups can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->base_dn
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the user configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createUserConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $defaults->user_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP User Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->user_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a user\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->user_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP User Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->user_base_dn
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Domain'),
+ 'description' => $this->translate(
+ 'The domain the LDAP server is responsible for.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements for the user configuration as hidden inputs
+ *
+ * This is required to purge already present options when unlinking a group backend with a user backend.
+ */
+ protected function createHiddenUserConfigElements()
+ {
+ $this->addElement('hidden', 'user_class', array('disabled' => true));
+ $this->addElement('hidden', 'user_filter', array('disabled' => true));
+ $this->addElement('hidden', 'user_name_attribute', array('disabled' => true));
+ $this->addElement('hidden', 'user_base_dn', array('disabled' => true));
+ $this->addElement('hidden', 'domain', array('disabled' => true));
+ }
+
+ /**
+ * Return the names of all configured LDAP resources
+ *
+ * @return array
+ */
+ protected function getLdapResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (in_array(strtolower($config->type), array('ldap', 'msldap'))) {
+ $names[] = $name;
+ }
+ }
+
+ if (empty($names)) {
+ Notification::error(
+ $this->translate('No LDAP resources available. Please configure an LDAP resource first.')
+ );
+ $this->getResponse()->redirectAndExit('config/createresource');
+ }
+
+ return $names;
+ }
+
+ /**
+ * Return the names of all configured LDAP user backends
+ *
+ * @param LdapConnection $resource
+ *
+ * @return array
+ */
+ protected function getLdapUserBackendNames(LdapConnection $resource)
+ {
+ $names = array();
+ foreach (UserBackend::getBackendConfigs() as $name => $config) {
+ if (in_array(strtolower($config->backend), array('ldap', 'msldap'))) {
+ $backendResource = ResourceFactory::create($config->resource);
+ if ($backendResource->getHostname() === $resource->getHostname()
+ && $backendResource->getPort() === $resource->getPort()
+ ) {
+ $names[] = $name;
+ }
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php
new file mode 100644
index 0000000..9ee4032
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php
@@ -0,0 +1,314 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Authentication\UserGroup\UserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Web\Form;
+use InvalidArgumentException;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Form for managing user group backends
+ */
+class UserGroupBackendForm extends ConfigForm
+{
+ protected $validatePartial = true;
+
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
+ /**
+ * Known custom backends
+ *
+ * @var array
+ */
+ protected $customBackends;
+
+ /**
+ * Create a user group backend by using the given form's values and return its inspection results
+ *
+ * Returns null for non-inspectable backends.
+ *
+ * @param Form $form
+ *
+ * @return Inspection|null
+ */
+ public static function inspectUserBackend(Form $form)
+ {
+ $backend = UserGroupBackend::create(null, new ConfigObject($form->getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_usergroupbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->customBackends = UserGroupBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ return new DbUserGroupBackendForm();
+ case 'ldap':
+ case 'msldap':
+ return new LdapUserGroupBackendForm();
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user group backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException('A user group backend with the name "%s" does already exist', $backendName);
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user group backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $this->config->setSection($name, $backendConfig->merge($data));
+ return $this;
+ }
+
+ /**
+ * Remove a user group backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array(
+ 'db' => $this->translate('Database'),
+ 'ldap' => 'LDAP',
+ 'msldap' => 'ActiveDirectory'
+ );
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate('The type of this user group backend'),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['type'] = $data['backend'];
+ $data['name'] = $this->backendToLoad;
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupForm.php b/application/forms/Config/UserGroup/UserGroupForm.php
new file mode 100644
index 0000000..b944e97
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupForm.php
@@ -0,0 +1,158 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Web\Notification;
+
+class UserGroupForm extends RepositoryForm
+{
+ /**
+ * Create and add elements to this form to insert or update a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'group_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Group Name')
+ )
+ );
+
+ if ($this->shouldInsert()) {
+ $this->setTitle($this->translate('Add a new group'));
+ $this->setSubmitLabel($this->translate('Add'));
+ } else { // $this->shouldUpdate()
+ $this->setTitle(sprintf($this->translate('Edit group %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+ }
+
+ /**
+ * Update a group
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('group_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('group', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create and add elements to this form to delete a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove group %s?'), $this->getIdentifier()));
+ $this->addDescription($this->translate(
+ 'Note that all users that are currently a member of this group will'
+ . ' have their membership cleared automatically.'
+ ));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-form icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a group
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('group_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('Group added successfully');
+ } else {
+ return $this->translate('Failed to add group');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}
diff --git a/application/forms/ConfigForm.php b/application/forms/ConfigForm.php
new file mode 100644
index 0000000..8b0c5f9
--- /dev/null
+++ b/application/forms/ConfigForm.php
@@ -0,0 +1,192 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Exception;
+use Zend_Form_Decorator_Abstract;
+use Icinga\Application\Config;
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form base-class providing standard functionality for configuration forms
+ */
+class ConfigForm extends Form
+{
+ /**
+ * The configuration to work with
+ *
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * {@inheritdoc}
+ *
+ * Values from subforms are directly added to the returned values array instead of being grouped by the subforms'
+ * names.
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ foreach (array_keys($this->_subForms) as $name) {
+ // Zend returns values from subforms grouped by their names, but we want them flat
+ $values = array_merge($values, $values[$name]);
+ unset($values[$name]);
+ }
+ return $values;
+ }
+
+ /**
+ * Set the configuration to use when populating the form or when saving the user's input
+ *
+ * @param Config $config The configuration to use
+ *
+ * @return $this
+ */
+ public function setIniConfig(Config $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ $sections = array();
+ foreach (static::transformEmptyValuesToNull($this->getValues()) as $sectionAndPropertyName => $value) {
+ list($section, $property) = explode('_', $sectionAndPropertyName, 2);
+ $sections[$section][$property] = $value;
+ }
+
+ foreach ($sections as $section => $config) {
+ if ($this->isEmptyConfig($config)) {
+ $this->config->removeSection($section);
+ } else {
+ $this->config->setSection($section, $config);
+ }
+ }
+
+ if ($this->save()) {
+ Notification::success($this->translate('New configuration has successfully been stored'));
+ } else {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+
+ public function onRequest()
+ {
+ $values = array();
+ foreach ($this->config as $section => $properties) {
+ foreach ($properties as $name => $value) {
+ $values[$section . '_' . $name] = $value;
+ }
+ }
+
+ $this->populate($values);
+ }
+
+ /**
+ * Persist the current configuration to disk
+ *
+ * If an error occurs the user is shown a view describing the issue and displaying the raw INI configuration.
+ *
+ * @return bool Whether the configuration could be persisted
+ */
+ public function save()
+ {
+ try {
+ $this->writeConfig($this->config);
+ } catch (ConfigurationError $e) {
+ $this->addError($e->getMessage());
+
+ return false;
+ } catch (Exception $e) {
+ $this->addDecorator('ViewScript', array(
+ 'viewModule' => 'default',
+ 'viewScript' => 'showConfiguration.phtml',
+ 'errorMessage' => $e->getMessage(),
+ 'configString' => $this->config,
+ 'filePath' => $this->config->getConfigFile(),
+ 'placement' => Zend_Form_Decorator_Abstract::PREPEND
+ ));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Write the configuration to disk
+ *
+ * @param Config $config
+ */
+ protected function writeConfig(Config $config)
+ {
+ $config->saveIni();
+ }
+
+ /**
+ * Get whether the given config is empty or has only empty values
+ *
+ * @param array|Config $config
+ *
+ * @return bool
+ */
+ protected function isEmptyConfig($config)
+ {
+ if ($config instanceof Config) {
+ $config = $config->toArray();
+ }
+
+ foreach ($config as $value) {
+ if ($value !== null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Transform all empty values of the given array to null
+ *
+ * @param array $values
+ *
+ * @return array
+ */
+ public static function transformEmptyValuesToNull(array $values)
+ {
+ array_walk($values, function (&$v) {
+ if ($v === '' || $v === false || $v === array()) {
+ $v = null;
+ }
+ });
+
+ return $values;
+ }
+}
diff --git a/application/forms/ConfirmRemovalForm.php b/application/forms/ConfirmRemovalForm.php
new file mode 100644
index 0000000..39fc661
--- /dev/null
+++ b/application/forms/ConfirmRemovalForm.php
@@ -0,0 +1,38 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Web\Form;
+
+/**
+ * Form for confirming removal of an object
+ */
+class ConfirmRemovalForm extends Form
+{
+ const DEFAULT_CLASSES = 'icinga-controls';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_confirm_removal');
+ $this->getSubmitLabel() ?: $this->setSubmitLabel($this->translate('Confirm Removal'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton();
+
+ if (($submit = $this->getElement('btn_submit')) !== null) {
+ $class = $submit->getAttrib('class');
+ $submit->setAttrib('class', empty($class) ? 'autofocus' : $class . ' autofocus');
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Control/LimiterControlForm.php b/application/forms/Control/LimiterControlForm.php
new file mode 100644
index 0000000..88adf4b
--- /dev/null
+++ b/application/forms/Control/LimiterControlForm.php
@@ -0,0 +1,134 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Control;
+
+use Icinga\Web\Form;
+
+/**
+ * Limiter control form
+ */
+class LimiterControlForm extends Form
+{
+ /**
+ * CSS class for the limiter control
+ *
+ * @var string
+ */
+ const CSS_CLASS_LIMITER = 'limiter-control icinga-controls inline';
+
+ /**
+ * Default limit
+ *
+ * @var int
+ */
+ const DEFAULT_LIMIT = 50;
+
+ /**
+ * Selectable default limits
+ *
+ * @var int[]
+ */
+ public static $limits = array(
+ 10 => '10',
+ 25 => '25',
+ 50 => '50',
+ 100 => '100',
+ 500 => '500'
+ );
+
+ public static $defaultElementDecorators = [
+ ['Label', ['tag' => 'span', 'separator' => '']],
+ ['ViewHelper', ['separator' => '']],
+ ];
+
+ /**
+ * Default limit for this instance
+ *
+ * @var int|null
+ */
+ protected $defaultLimit;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', static::CSS_CLASS_LIMITER);
+ }
+
+ /**
+ * Get the default limit
+ *
+ * @return int
+ */
+ public function getDefaultLimit()
+ {
+ return $this->defaultLimit !== null ? $this->defaultLimit : static::DEFAULT_LIMIT;
+ }
+
+ /**
+ * Set the default limit
+ *
+ * @param int $defaultLimit
+ *
+ * @return $this
+ */
+ public function setDefaultLimit($defaultLimit)
+ {
+ $defaultLimit = (int) $defaultLimit;
+
+ if (! isset(static::$limits[$defaultLimit])) {
+ static::$limits[$defaultLimit] = $defaultLimit;
+ }
+
+ $this->defaultLimit = $defaultLimit;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrl()
+ {
+ return $this->getRequest()->getUrl()
+ ->setParam('limit', $this->getElement('limit')->getValue())
+ ->without('page');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $options = static::$limits;
+ $pageSize = (int) $this->getRequest()->getUrl()->getParam('limit', $this->getDefaultLimit());
+
+ if (! isset($options[$pageSize])) {
+ $options[$pageSize] = $pageSize;
+ }
+
+ $this->addElement(
+ 'select',
+ 'limit',
+ array(
+ 'autosubmit' => true,
+ 'escape' => false,
+ 'label' => '#',
+ 'multiOptions' => $options,
+ 'value' => $pageSize
+ )
+ );
+ }
+
+ /**
+ * Limiter control is always successful
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ return true;
+ }
+}
diff --git a/application/forms/Dashboard/DashletForm.php b/application/forms/Dashboard/DashletForm.php
new file mode 100644
index 0000000..1af65a9
--- /dev/null
+++ b/application/forms/Dashboard/DashletForm.php
@@ -0,0 +1,171 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Dashboard;
+
+use Icinga\Web\Form;
+use Icinga\Web\Form\Validator\InternalUrlValidator;
+use Icinga\Web\Form\Validator\UrlValidator;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Dashboard;
+use Icinga\Web\Widget\Dashboard\Dashlet;
+
+/**
+ * Form to add an url a dashboard pane
+ */
+class DashletForm extends Form
+{
+ /**
+ * @var Dashboard
+ */
+ private $dashboard;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_dashboard_addurl');
+ if (! $this->getSubmitLabel()) {
+ $this->setSubmitLabel($this->translate('Add To Dashboard'));
+ }
+ $this->setAction(Url::fromRequest());
+ }
+
+ /**
+ * Build AddUrl form elements
+ *
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $groupElements = array();
+ $panes = array();
+
+ if ($this->dashboard) {
+ $panes = $this->dashboard->getPaneKeyTitleArray();
+ }
+
+ $sectionNameValidator = ['Callback', true, [
+ 'callback' => function ($value) {
+ if (strpos($value, '[') === false && strpos($value, ']') === false) {
+ return true;
+ }
+ },
+ 'messages' => [
+ 'callbackValue' => $this->translate('Brackets ([, ]) cannot be used here')
+ ]
+ ]];
+
+ $this->addElement(
+ 'hidden',
+ 'org_pane',
+ array(
+ 'required' => false
+ )
+ );
+
+ $this->addElement(
+ 'hidden',
+ 'org_dashlet',
+ array(
+ 'required' => false
+ )
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'url',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters.'
+ ),
+ 'validators' => array(new UrlValidator(), new InternalUrlValidator())
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'dashlet',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Dashlet Title'),
+ 'description' => $this->translate('Enter a title for the dashlet.'),
+ 'validators' => [$sectionNameValidator]
+ )
+ );
+ $this->addElement(
+ 'note',
+ 'note',
+ array(
+ 'decorators' => array(
+ array('HtmlTag', array('tag' => 'hr'))
+ )
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'create_new_pane',
+ array(
+ 'autosubmit' => true,
+ 'required' => false,
+ 'label' => $this->translate('New dashboard'),
+ 'description' => $this->translate('Check this box if you want to add the dashlet to a new dashboard')
+ )
+ );
+ if (empty($panes) || ((isset($formData['create_new_pane']) && $formData['create_new_pane'] != false))) {
+ $this->addElement(
+ 'text',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('New Dashboard Title'),
+ 'description' => $this->translate('Enter a title for the new dashboard'),
+ 'validators' => [$sectionNameValidator]
+ )
+ );
+ } else {
+ $this->addElement(
+ 'select',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Dashboard'),
+ 'multiOptions' => $panes,
+ 'description' => $this->translate('Select a dashboard you want to add the dashlet to')
+ )
+ );
+ }
+ }
+
+ /**
+ * @param \Icinga\Web\Widget\Dashboard $dashboard
+ */
+ public function setDashboard(Dashboard $dashboard)
+ {
+ $this->dashboard = $dashboard;
+ }
+
+ /**
+ * @return \Icinga\Web\Widget\Dashboard
+ */
+ public function getDashboard()
+ {
+ return $this->dashboard;
+ }
+
+ /**
+ * @param Dashlet $dashlet
+ */
+ public function load(Dashlet $dashlet)
+ {
+ $this->populate(array(
+ 'pane' => $dashlet->getPane()->getTitle(),
+ 'org_pane' => $dashlet->getPane()->getName(),
+ 'dashlet' => $dashlet->getTitle(),
+ 'org_dashlet' => $dashlet->getName(),
+ 'url' => $dashlet->getUrl()->getRelativeUrl()
+ ));
+ }
+}
diff --git a/application/forms/LdapDiscoveryForm.php b/application/forms/LdapDiscoveryForm.php
new file mode 100644
index 0000000..5c7fc87
--- /dev/null
+++ b/application/forms/LdapDiscoveryForm.php
@@ -0,0 +1,34 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Web\Form;
+
+class LdapDiscoveryForm extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('form_ldap_discovery');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Search Domain'),
+ 'description' => $this->translate('Search this domain for records of available servers.'),
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/MigrationForm.php b/application/forms/MigrationForm.php
new file mode 100644
index 0000000..c5d517f
--- /dev/null
+++ b/application/forms/MigrationForm.php
@@ -0,0 +1,143 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\MigrationManager;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\CheckboxElement;
+use ipl\Html\FormElement\FieldsetElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\I18n\Translation;
+use ipl\Validator\CallbackValidator;
+use ipl\Web\Common\CsrfCounterMeasure;
+use ipl\Web\Common\FormUid;
+use ipl\Web\Compat\CompatForm;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use PDOException;
+
+class MigrationForm extends CompatForm
+{
+ use CsrfCounterMeasure;
+ use FormUid;
+ use Translation;
+
+ protected $defaultAttributes = [
+ 'class' => ['icinga-form', 'migration-form', 'icinga-controls'],
+ 'name' => 'migration-form'
+ ];
+
+ /** @var bool Whether to allow changing the current database user and password */
+ protected $renderDatabaseUserChange = false;
+
+ public function hasBeenSubmitted(): bool
+ {
+ if (! $this->hasBeenSent()) {
+ return false;
+ }
+
+ $pressedButton = $this->getPressedSubmitElement();
+
+ return $pressedButton && strpos($pressedButton->getName(), 'migrate-') !== false;
+ }
+
+ public function setRenderDatabaseUserChange(bool $value = true): self
+ {
+ $this->renderDatabaseUserChange = $value;
+
+ return $this;
+ }
+
+ public function hasDefaultElementDecorator()
+ {
+ // The base implementation registers a decorator we don't want here
+ return false;
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml($this->createUidElement());
+
+ if ($this->renderDatabaseUserChange) {
+ $mm = MigrationManager::instance();
+ $newDbSetup = new FieldsetElement('database_setup', ['required' => true]);
+ $newDbSetup
+ ->setDefaultElementDecorator(new IcingaFormDecorator())
+ ->addElement('text', 'username', [
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'A user which is able to create and/or alter the database schema.'
+ )
+ ])
+ ->addElement('password', 'password', [
+ 'required' => true,
+ 'autocomplete' => 'new-password',
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password for the database user defined above.'),
+ 'validators' => [
+ new CallbackValidator(function ($_, CallbackValidator $validator) use ($mm, $newDbSetup): bool {
+ /** @var array<string, string> $values */
+ $values = $this->getValue('database_setup');
+ /** @var CheckboxElement $checkBox */
+ $checkBox = $newDbSetup->getElement('grant_privileges');
+ $canIssueGrants = $checkBox->isChecked();
+ $elevationConfig = [
+ 'username' => $values['username'],
+ 'password' => $values['password']
+ ];
+
+ try {
+ if (! $mm->validateDatabasePrivileges($elevationConfig, $canIssueGrants)) {
+ $validator->addMessage(sprintf(
+ $this->translate(
+ 'The provided credentials cannot be used to execute "%s" SQL commands'
+ . ' and/or grant the missing privileges to other users.'
+ ),
+ implode(' ,', $mm->getRequiredDatabasePrivileges())
+ ));
+
+ return false;
+ }
+ } catch (PDOException $e) {
+ $validator->addMessage($e->getMessage());
+
+ return false;
+ }
+
+ return true;
+ })
+ ]
+ ])
+ ->addElement('checkbox', 'grant_privileges', [
+ 'required' => false,
+ 'label' => $this->translate('Grant Missing Privileges'),
+ 'description' => $this->translate(
+ 'Allows to automatically grant the required privileges to the database user specified'
+ . ' in the respective resource config. If you do not want to provide additional credentials'
+ . ' each time, you can enable this and Icinga Web will grant the active database user the'
+ . ' missing privileges.'
+ )
+ ]);
+
+ $this->addHtml(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'change-database-user-description']),
+ new HtmlElement('span', null, Text::create(sprintf(
+ $this->translate(
+ 'It seems that the currently used database user does not have the required privileges to'
+ . ' execute the %s SQL commands. Please provide an alternative user'
+ . ' that has the appropriate credentials to resolve this issue.'
+ ),
+ implode(', ', $mm->getRequiredDatabasePrivileges())
+ )))
+ )
+ );
+
+ $this->addElement($newDbSetup);
+ }
+ }
+}
diff --git a/application/forms/Navigation/DashletForm.php b/application/forms/Navigation/DashletForm.php
new file mode 100644
index 0000000..6575fd7
--- /dev/null
+++ b/application/forms/Navigation/DashletForm.php
@@ -0,0 +1,35 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+class DashletForm extends NavigationItemForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Pane'),
+ 'description' => $this->translate('The name of the dashboard pane in which to display this dashlet')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'url',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'The url to load in the dashlet. For external urls, make sure to prepend'
+ . ' an appropriate protocol identifier (e.g. http://example.tld)'
+ )
+ )
+ );
+ }
+}
diff --git a/application/forms/Navigation/MenuItemForm.php b/application/forms/Navigation/MenuItemForm.php
new file mode 100644
index 0000000..c9fa729
--- /dev/null
+++ b/application/forms/Navigation/MenuItemForm.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+class MenuItemForm extends NavigationItemForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $requiresParentSelection = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ parent::createElements($formData);
+
+ // Remove _self and _next as for menu entries only _main is valid
+ $this->getElement('target')->removeMultiOption('_self');
+ $this->getElement('target')->removeMultiOption('_next');
+
+ $parentElement = $this->getParent()->getElement('parent');
+ if ($parentElement !== null) {
+ $parentElement->setDescription($this->translate(
+ 'The parent menu to assign this menu entry to. Select "None" to make this a main menu entry'
+ ));
+ }
+ }
+}
diff --git a/application/forms/Navigation/NavigationConfigForm.php b/application/forms/Navigation/NavigationConfigForm.php
new file mode 100644
index 0000000..0c4ae32
--- /dev/null
+++ b/application/forms/Navigation/NavigationConfigForm.php
@@ -0,0 +1,853 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Forms\ConfigForm;
+use Icinga\User;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing navigation items
+ */
+class NavigationConfigForm extends ConfigForm
+{
+ /**
+ * The class namespace where to locate navigation type forms
+ *
+ * @var string
+ */
+ const FORM_NS = 'Forms\\Navigation';
+
+ /**
+ * The secondary configuration to write
+ *
+ * This is always the reduced configuration and is only written to
+ * disk once the main configuration has been successfully written.
+ *
+ * @var Config
+ */
+ protected $secondaryConfig;
+
+ /**
+ * The navigation item to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $itemToLoad;
+
+ /**
+ * The user for whom to manage navigation items
+ *
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * The user's navigation configuration
+ *
+ * @var Config
+ */
+ protected $userConfig;
+
+ /**
+ * The shared navigation configuration
+ *
+ * @var Config
+ */
+ protected $shareConfig;
+
+ /**
+ * The available navigation item types
+ *
+ * @var array
+ */
+ protected $itemTypes;
+
+ private $defaultUrl;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_navigation');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * Set the user for whom to manage navigation items
+ *
+ * @param User $user
+ *
+ * @return $this
+ */
+ public function setUser(User $user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * Return the user for whom to manage navigation items
+ *
+ * @return User
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the user's navigation configuration
+ *
+ * @param Config $config
+ *
+ * @return $this
+ */
+ public function setUserConfig(Config $config)
+ {
+ $config->getConfigObject()->setKeyColumn('name');
+ $this->userConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the user's navigation configuration
+ *
+ * @param string $type
+ *
+ * @return Config
+ */
+ public function getUserConfig($type = null)
+ {
+ if ($this->userConfig === null || $type !== null) {
+ if ($type === null) {
+ throw new ProgrammingError('You need to pass a type if no user configuration is set');
+ }
+
+ $this->setUserConfig(Config::navigation($type, $this->getUser()->getUsername()));
+ }
+
+ return $this->userConfig;
+ }
+
+ /**
+ * Set the shared navigation configuration
+ *
+ * @param Config $config
+ *
+ * @return $this
+ */
+ public function setShareConfig(Config $config)
+ {
+ $config->getConfigObject()->setKeyColumn('name');
+ $this->shareConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the shared navigation configuration
+ *
+ * @param string $type
+ *
+ * @return Config
+ */
+ public function getShareConfig($type = null)
+ {
+ if ($this->shareConfig === null) {
+ if ($type === null) {
+ throw new ProgrammingError('You need to pass a type if no share configuration is set');
+ }
+
+ $this->setShareConfig(Config::navigation($type));
+ }
+
+ return $this->shareConfig;
+ }
+
+ /**
+ * Set the available navigation item types
+ *
+ * @param array $itemTypes
+ *
+ * @return $this
+ */
+ public function setItemTypes(array $itemTypes)
+ {
+ $this->itemTypes = $itemTypes;
+ return $this;
+ }
+
+ /**
+ * Return the available navigation item types
+ *
+ * @return array
+ */
+ public function getItemTypes()
+ {
+ return $this->itemTypes ?: array();
+ }
+
+ /**
+ * Return a list of available parent items for the given type of navigation item
+ *
+ * @param string $type
+ * @param string $owner
+ *
+ * @return array
+ */
+ public function listAvailableParents($type, $owner = null)
+ {
+ $children = $this->itemToLoad ? $this->getFlattenedChildren($this->itemToLoad) : array();
+
+ $names = array();
+ foreach ($this->getShareConfig($type) as $sectionName => $sectionConfig) {
+ if ((string) $sectionName !== $this->itemToLoad
+ && $sectionConfig->owner === ($owner ?: $this->getUser()->getUsername())
+ && ! in_array($sectionName, $children, true)
+ ) {
+ $names[] = $sectionName;
+ }
+ }
+
+ foreach ($this->getUserConfig($type) as $sectionName => $sectionConfig) {
+ if ((string) $sectionName !== $this->itemToLoad
+ && ! in_array($sectionName, $children, true)
+ ) {
+ $names[] = $sectionName;
+ }
+ }
+
+ return $names;
+ }
+
+ /**
+ * Recursively return all children of the given navigation item
+ *
+ * @param string $name
+ *
+ * @return array
+ */
+ protected function getFlattenedChildren($name)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ return array();
+ }
+
+ $children = array();
+ foreach ($config->toArray() as $sectionName => $sectionConfig) {
+ if (isset($sectionConfig['parent']) && $sectionConfig['parent'] === $name) {
+ $children[] = $sectionName;
+ $children = array_merge($children, $this->getFlattenedChildren($sectionName));
+ }
+ }
+
+ return $children;
+ }
+
+ /**
+ * Populate the form with the given navigation item's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ */
+ public function load($name)
+ {
+ if ($this->getConfigForItem($name) === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $this->itemToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new navigation item
+ *
+ * The navigation item to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a navigation item name or type
+ * @throws IcingaException In case a navigation item with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ } elseif (! isset($data['type'])) {
+ throw new InvalidArgumentException('Key \'type\' missing');
+ }
+
+ $shared = false;
+ $config = $this->getUserConfig($data['type']);
+ if ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
+ if ($this->getUser()->can('user/share/navigation')) {
+ $data['owner'] = $this->getUser()->getUsername();
+ $config = $this->getShareConfig($data['type']);
+ $shared = true;
+ } else {
+ unset($data['users']);
+ unset($data['groups']);
+ }
+ } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'], $data['type'])) {
+ $data['owner'] = $this->getUser()->getUsername();
+ $config = $this->getShareConfig($data['type']);
+ $shared = true;
+ }
+
+ $itemName = $data['name'];
+ $exists = $config->hasSection($itemName);
+ if (! $exists) {
+ if ($shared) {
+ $exists = $this->getUserConfig($data['type'])->hasSection($itemName);
+ } else {
+ $exists = (bool) $this->getShareConfig($data['type'])
+ ->select()
+ ->where('name', $itemName)
+ ->where('owner', $this->getUser()->getUsername())
+ ->count();
+ }
+ }
+
+ if ($exists) {
+ throw new IcingaException(
+ $this->translate('A navigation item with the name "%s" does already exist'),
+ $itemName
+ );
+ }
+
+ unset($data['name']);
+ $config->setSection($itemName, $data);
+ $this->setIniConfig($config);
+ return $this;
+ }
+
+ /**
+ * Edit a navigation item
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case a navigation item with the same name already exists
+ */
+ public function edit($name, array $data)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ } else {
+ $itemConfig = $config->getSection($name);
+ }
+
+ $shared = false;
+ if ($this->hasBeenShared($name)) {
+ if (isset($data['parent']) && $data['parent']
+ ? ! $this->hasBeenShared($data['parent'])
+ : ((! isset($data['users']) || ! $data['users']) && (! isset($data['groups']) || ! $data['groups']))
+ ) {
+ // It is shared but shouldn't anymore
+ $config = $this->unshare($name, isset($data['parent']) ? $data['parent'] : null);
+ }
+ } elseif ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
+ if ($this->getUser()->can('user/share/navigation')) {
+ // It is not shared yet but should be
+ $this->secondaryConfig = $config;
+ $config = $this->getShareConfig();
+ $data['owner'] = $this->getUser()->getUsername();
+ $shared = true;
+ } else {
+ unset($data['users']);
+ unset($data['groups']);
+ }
+ } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'])) {
+ // Its parent is shared so should it itself
+ $this->secondaryConfig = $config;
+ $config = $this->getShareConfig();
+ $data['owner'] = $this->getUser()->getUsername();
+ $shared = true;
+ }
+
+ $oldName = null;
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $oldName = $name;
+ $name = $data['name'];
+
+ $exists = $config->hasSection($name);
+ if (! $exists) {
+ $ownerName = $itemConfig->owner ?: $this->getUser()->getUsername();
+ if ($shared || $this->hasBeenShared($oldName)) {
+ if ($ownerName === $this->getUser()->getUsername()) {
+ $exists = $this->getUserConfig()->hasSection($name);
+ } else {
+ $exists = Config::navigation($itemConfig->type, $ownerName)->hasSection($name);
+ }
+ } else {
+ $exists = (bool) $this->getShareConfig()
+ ->select()
+ ->where('name', $name)
+ ->where('owner', $ownerName)
+ ->count();
+ }
+ }
+
+ if ($exists) {
+ throw new IcingaException(
+ $this->translate('A navigation item with the name "%s" does already exist'),
+ $name
+ );
+ }
+ }
+
+ unset($data['name']);
+ }
+
+ $itemConfig->merge($data);
+
+ if ($shared) {
+ // Share all descendant children
+ foreach ($this->getFlattenedChildren($oldName ?: $name) as $child) {
+ $childConfig = $this->secondaryConfig->getSection($child);
+ $this->secondaryConfig->removeSection($child);
+ $childConfig->owner = $this->getUser()->getUsername();
+ $config->setSection($child, $childConfig);
+ }
+ }
+
+ if ($oldName) {
+ // Update the parent name on all direct children
+ foreach ($config as $sectionConfig) {
+ if ($sectionConfig->parent === $oldName) {
+ $sectionConfig->parent = $name;
+ }
+ }
+
+ $config->removeSection($oldName);
+ }
+
+ if ($this->secondaryConfig !== null) {
+ $this->secondaryConfig->removeSection($oldName ?: $name);
+ }
+
+ $config->setSection($name, $itemConfig);
+ $this->setIniConfig($config);
+ return $this;
+ }
+
+ /**
+ * Remove a navigation item
+ *
+ * @param string $name
+ *
+ * @return ConfigObject The navigation item's config
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case the navigation item has still children
+ */
+ public function delete($name)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $children = $this->getFlattenedChildren($name);
+ if (! empty($children)) {
+ throw new IcingaException(
+ $this->translate(
+ 'Unable to delete navigation item "%s". There'
+ . ' are other items dependent from it: %s'
+ ),
+ $name,
+ join(', ', $children)
+ );
+ }
+
+ $section = $config->getSection($name);
+ $config->removeSection($name);
+ $this->setIniConfig($config);
+ return $section;
+ }
+
+ /**
+ * Unshare the given navigation item
+ *
+ * @param string $name
+ * @param string $parent
+ *
+ * @return Config The new config of the given navigation item
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case the navigation item has a parent assigned to it
+ */
+ public function unshare($name, $parent = null)
+ {
+ $config = $this->getShareConfig();
+ if (! $config->hasSection($name)) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $itemConfig = $config->getSection($name);
+ if ($parent === null) {
+ $parent = $itemConfig->parent;
+ }
+
+ if ($parent && $this->hasBeenShared($parent)) {
+ throw new IcingaException(
+ $this->translate(
+ 'Unable to unshare navigation item "%s". It is dependent from item "%s".'
+ . ' Dependent items can only be unshared by unsharing their parent'
+ ),
+ $name,
+ $parent
+ );
+ }
+
+ $children = $this->getFlattenedChildren($name);
+ $config->removeSection($name);
+ $this->secondaryConfig = $config;
+
+ if (! $itemConfig->owner || $itemConfig->owner === $this->getUser()->getUsername()) {
+ $config = $this->getUserConfig();
+ } else {
+ $config = Config::navigation($itemConfig->type, $itemConfig->owner);
+ }
+
+ foreach ($children as $child) {
+ $childConfig = $this->secondaryConfig->getSection($child);
+ unset($childConfig->owner);
+ $this->secondaryConfig->removeSection($child);
+ $config->setSection($child, $childConfig);
+ }
+
+ unset($itemConfig->owner);
+ unset($itemConfig->users);
+ unset($itemConfig->groups);
+
+ $config->setSection($name, $itemConfig);
+ $this->setIniConfig($config);
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $shared = false;
+ $itemTypes = $this->getItemTypes();
+ $itemType = isset($formData['type']) ? $formData['type'] : key($itemTypes);
+ if ($itemType === null) {
+ throw new ProgrammingError(
+ 'This should actually not happen. Create a bug report at https://github.com/icinga/icingaweb2'
+ . ' or remove this assertion if you know what you\'re doing'
+ );
+ }
+
+ $itemForm = $this->getItemForm($itemType);
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Name'),
+ 'description' => $this->translate(
+ 'The name of this navigation item that is used to differentiate it from others'
+ )
+ )
+ );
+
+ if ((! $itemForm->requiresParentSelection() || ! isset($formData['parent']) || ! $formData['parent'])
+ && $this->getUser()->can('user/share/navigation')
+ ) {
+ $checked = isset($formData['shared']) ? null : (isset($formData['users']) || isset($formData['groups']));
+
+ $this->addElement(
+ 'checkbox',
+ 'shared',
+ array(
+ 'autosubmit' => true,
+ 'ignore' => true,
+ 'value' => $checked,
+ 'label' => $this->translate('Shared'),
+ 'description' => $this->translate('Tick this box to share this item with others')
+ )
+ );
+
+ if ($checked || (isset($formData['shared']) && $formData['shared'])) {
+ $shared = true;
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Comma separated list of usernames to share this item with'
+ )
+ )
+ );
+ $this->addElement(
+ 'textarea',
+ 'groups',
+ array(
+ 'label' => $this->translate('Groups'),
+ 'description' => $this->translate(
+ 'Comma separated list of group names to share this item with'
+ )
+ )
+ );
+ }
+ }
+
+ if (empty($itemTypes) || count($itemTypes) === 1) {
+ $this->addElement(
+ 'hidden',
+ 'type',
+ array(
+ 'value' => $itemType
+ )
+ );
+ } else {
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Type'),
+ 'description' => $this->translate('The type of this navigation item'),
+ 'multiOptions' => $itemTypes
+ )
+ );
+ }
+
+ if (! $shared && $itemForm->requiresParentSelection()) {
+ if ($this->itemToLoad && $this->hasBeenShared($this->itemToLoad)) {
+ $itemConfig = $this->getShareConfig()->getSection($this->itemToLoad);
+ $availableParents = $this->listAvailableParents($itemType, $itemConfig->owner);
+ } else {
+ $availableParents = $this->listAvailableParents($itemType);
+ }
+
+ $this->addElement(
+ 'select',
+ 'parent',
+ array(
+ 'allowEmpty' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Parent'),
+ 'description' => $this->translate(
+ 'The parent item to assign this navigation item to. '
+ . 'Select "None" to make this a main navigation item'
+ ),
+ 'multiOptions' => ['' => $this->translate('None', 'No parent for a navigation item')]
+ + (empty($availableParents) ? [] : array_combine($availableParents, $availableParents))
+ )
+ );
+ } else {
+ $this->addElement('hidden', 'parent', ['disabled' => true]);
+ }
+
+ $this->addSubForm($itemForm, 'item_form');
+ $itemForm->create($formData); // May require a parent which gets set by addSubForm()
+ }
+
+ /**
+ * DO NOT USE! This will be removed soon, very soon...
+ */
+ public function setDefaultUrl($url)
+ {
+ $this->defaultUrl = $url;
+ }
+
+ /**
+ * Populate the configuration of the navigation item to load
+ */
+ public function onRequest()
+ {
+ if ($this->itemToLoad) {
+ $data = $this->getConfigForItem($this->itemToLoad)->getSection($this->itemToLoad)->toArray();
+ $data['name'] = $this->itemToLoad;
+ $this->populate($data);
+ } elseif ($this->defaultUrl !== null) {
+ $this->populate(array('url' => $this->defaultUrl));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ $valid = true;
+ if (isset($formData['users']) && $formData['users']) {
+ $parsedUserRestrictions = array();
+ foreach (Auth::getInstance()->getRestrictions('application/share/users') as $userRestriction) {
+ $parsedUserRestrictions[] = array_map('trim', explode(',', $userRestriction));
+ }
+
+ if (! empty($parsedUserRestrictions)) {
+ $desiredUsers = array_map('trim', explode(',', $formData['users']));
+ array_unshift($parsedUserRestrictions, $desiredUsers);
+ $forbiddenUsers = call_user_func_array('array_diff', $parsedUserRestrictions);
+ if (! empty($forbiddenUsers)) {
+ $valid = false;
+ $this->getElement('users')->addError(
+ sprintf(
+ $this->translate(
+ 'You are not permitted to share this navigation item with the following users: %s'
+ ),
+ implode(', ', $forbiddenUsers)
+ )
+ );
+ }
+ }
+ }
+
+ if (isset($formData['groups']) && $formData['groups']) {
+ $parsedGroupRestrictions = array();
+ foreach (Auth::getInstance()->getRestrictions('application/share/groups') as $groupRestriction) {
+ $parsedGroupRestrictions[] = array_map('trim', explode(',', $groupRestriction));
+ }
+
+ if (! empty($parsedGroupRestrictions)) {
+ $desiredGroups = array_map('trim', explode(',', $formData['groups']));
+ array_unshift($parsedGroupRestrictions, $desiredGroups);
+ $forbiddenGroups = call_user_func_array('array_diff', $parsedGroupRestrictions);
+ if (! empty($forbiddenGroups)) {
+ $valid = false;
+ $this->getElement('groups')->addError(
+ sprintf(
+ $this->translate(
+ 'You are not permitted to share this navigation item with the following groups: %s'
+ ),
+ implode(', ', $forbiddenGroups)
+ )
+ );
+ }
+ }
+ }
+
+ return $valid;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function writeConfig(Config $config)
+ {
+ parent::writeConfig($config);
+
+ if ($this->secondaryConfig !== null) {
+ $this->config = $this->secondaryConfig; // Causes the config being displayed to the user in case of an error
+ parent::writeConfig($this->secondaryConfig);
+ }
+ }
+
+ /**
+ * Return the navigation configuration the given item is a part of
+ *
+ * @param string $name
+ *
+ * @return Config|null In case the item is not part of any configuration
+ */
+ protected function getConfigForItem($name)
+ {
+ if ($this->getUserConfig()->hasSection($name)) {
+ return $this->getUserConfig();
+ } elseif ($this->getShareConfig()->hasSection($name)) {
+ if ($this->getShareConfig()->get($name, 'owner') === $this->getUser()->getUsername()
+ || $this->getUser()->can('user/share/navigation')
+ ) {
+ return $this->getShareConfig();
+ }
+ }
+ }
+
+ /**
+ * Return whether the given navigation item has been shared
+ *
+ * @param string $name
+ * @param string $type
+ *
+ * @return bool
+ */
+ protected function hasBeenShared($name, $type = null)
+ {
+ return $this->getShareConfig($type) === $this->getConfigForItem($name);
+ }
+
+ /**
+ * Return the form for the given type of navigation item
+ *
+ * @param string $type
+ *
+ * @return Form
+ */
+ protected function getItemForm($type)
+ {
+ $className = StringHelper::cname($type, '-') . 'Form';
+
+ $form = null;
+ $classPath = null;
+ foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
+ $classPath = 'Icinga\\Module\\'
+ . ucfirst($module->getName())
+ . '\\'
+ . static::FORM_NS
+ . '\\'
+ . $className;
+ if (class_exists($classPath)) {
+ $form = new $classPath();
+ break;
+ }
+ }
+
+ if ($form === null) {
+ $classPath = 'Icinga\\' . static::FORM_NS . '\\' . $className;
+ if (class_exists($classPath)) {
+ $form = new $classPath();
+ }
+ }
+
+ if ($form === null) {
+ Logger::debug(
+ 'Failed to find custom navigation item form %s for item %s. Using form NavigationItemForm now',
+ $className,
+ $type
+ );
+
+ $form = new NavigationItemForm();
+ } elseif (! $form instanceof NavigationItemForm) {
+ throw new ProgrammingError('Class %s must inherit from NavigationItemForm', $classPath);
+ }
+
+ return $form;
+ }
+}
diff --git a/application/forms/Navigation/NavigationItemForm.php b/application/forms/Navigation/NavigationItemForm.php
new file mode 100644
index 0000000..6cf15e7
--- /dev/null
+++ b/application/forms/Navigation/NavigationItemForm.php
@@ -0,0 +1,114 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class NavigationItemForm extends Form
+{
+ /**
+ * Whether to create a select input to choose a parent for a navigation item of a particular type
+ *
+ * @var bool
+ */
+ protected $requiresParentSelection = false;
+
+ /**
+ * Return whether to create a select input to choose a parent for a navigation item of a particular type
+ *
+ * @return bool
+ */
+ public function requiresParentSelection()
+ {
+ return $this->requiresParentSelection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'select',
+ 'target',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Target'),
+ 'description' => $this->translate('The target where to open this navigation item\'s url'),
+ 'multiOptions' => array(
+ '_blank' => $this->translate('New Window'),
+ '_next' => $this->translate('New Column'),
+ '_main' => $this->translate('Single Column'),
+ '_self' => $this->translate('Current Column')
+ )
+ )
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'url',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'The url of this navigation item. Leave blank if only the name should be displayed.'
+ . ' For urls with username and password and for all external urls,'
+ . ' make sure to prepend an appropriate protocol identifier (e.g. http://example.tld)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($url) {
+ // Matches if the given url contains obviously
+ // a username but not any protocol identifier
+ return !preg_match('#^((?=[^/@]).)+@.*$#', $url);
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate(
+ 'Missing protocol identifier'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'icon',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Icon'),
+ 'description' => $this->translate(
+ 'The icon of this navigation item. Leave blank if you do not want a icon being displayed'
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ // The regex here specifically matches the port-macro as it's the only one preventing Url::fromPath() from
+ // successfully parsing the given url. Any other macro such as for the scheme or host simply gets identified
+ // as path which is just fine in this case.
+ if (isset($values['url']) && $values['url'] && !preg_match('~://.+:\d*?(\$.+\$)~', $values['url'])) {
+ $url = Url::fromPath($values['url']);
+ if ($url->getBasePath() === $this->getRequest()->getBasePath()) {
+ $values['url'] = $url->getRelativeUrl();
+ } else {
+ $values['url'] = $url->getAbsoluteUrl();
+ }
+ }
+
+ return $values;
+ }
+}
diff --git a/application/forms/PreferenceForm.php b/application/forms/PreferenceForm.php
new file mode 100644
index 0000000..3e431db
--- /dev/null
+++ b/application/forms/PreferenceForm.php
@@ -0,0 +1,485 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use DateTimeZone;
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\User\Preferences;
+use Icinga\User\Preferences\PreferencesStore;
+use Icinga\Util\TimezoneDetect;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Session;
+use Icinga\Web\StyleSheet;
+use ipl\Html\HtmlElement;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\Locale;
+use ipl\I18n\StaticTranslator;
+
+/**
+ * Form class to adjust user preferences
+ */
+class PreferenceForm extends Form
+{
+ /**
+ * The preferences to work with
+ *
+ * @var Preferences
+ */
+ protected $preferences;
+
+ /**
+ * The preference store to use
+ *
+ * @var PreferencesStore
+ */
+ protected $store;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_preferences');
+ $this->setSubmitLabel($this->translate('Save to the Preferences'));
+ }
+
+ /**
+ * Set preferences to work with
+ *
+ * @param Preferences $preferences The preferences to work with
+ *
+ * @return $this
+ */
+ public function setPreferences(Preferences $preferences)
+ {
+ $this->preferences = $preferences;
+ return $this;
+ }
+
+ /**
+ * Set the preference store to use
+ *
+ * @param PreferencesStore $store The preference store to use
+ *
+ * @return $this
+ */
+ public function setStore(PreferencesStore $store)
+ {
+ $this->store = $store;
+ return $this;
+ }
+
+ /**
+ * Persist preferences
+ *
+ * @return $this
+ */
+ public function save()
+ {
+ $this->store->save($this->preferences);
+ return $this;
+ }
+
+ /**
+ * Adjust preferences and persist them
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $currentPreferences = $this->Auth()->getUser()->getPreferences();
+ $oldTheme = $currentPreferences->getValue('icingaweb', 'theme');
+ $oldMode = $currentPreferences->getValue('icingaweb', 'theme_mode');
+ $oldLocale = $currentPreferences->getValue('icingaweb', 'language');
+ $defaultTheme = Config::app()->get('themes', 'default', StyleSheet::DEFAULT_THEME);
+
+ $this->preferences = new Preferences($this->store ? $this->store->load() : array());
+ $webPreferences = $this->preferences->get('icingaweb');
+ foreach ($this->getValues() as $key => $value) {
+ if ($value === ''
+ || $value === null
+ || $value === 'autodetect'
+ || ($key === 'theme' && $value === $defaultTheme)
+ ) {
+ if (isset($webPreferences[$key])) {
+ unset($webPreferences[$key]);
+ }
+ } else {
+ $webPreferences[$key] = $value;
+ }
+ }
+ $this->preferences->icingaweb = $webPreferences;
+ Session::getSession()->user->setPreferences($this->preferences);
+
+ if ((($theme = $this->getElement('theme')) !== null
+ && ($theme = $theme->getValue()) !== $oldTheme
+ && ($theme !== $defaultTheme || $oldTheme !== null))
+ || (($mode = $this->getElement('theme_mode')) !== null
+ && ($mode->getValue()) !== $oldMode)
+ ) {
+ $this->getResponse()->setReloadCss(true);
+ }
+
+ if (($locale = $this->getElement('language')) !== null
+ && $locale->getValue() !== 'autodetect'
+ && $locale->getValue() !== $oldLocale
+ ) {
+ $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes');
+ }
+
+ try {
+ if ($this->store && $this->getElement('btn_submit')->isChecked()) {
+ $this->save();
+ Notification::success($this->translate('Preferences successfully saved'));
+ } else {
+ Notification::success($this->translate('Preferences successfully saved for the current session'));
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::error($e->getMessage());
+ }
+ }
+
+ /**
+ * Populate preferences
+ *
+ * @see Form::onRequest()
+ */
+ public function onRequest()
+ {
+ $auth = Auth::getInstance();
+ $values = $auth->getUser()->getPreferences()->get('icingaweb');
+
+ if (! isset($values['language'])) {
+ $values['language'] = 'autodetect';
+ }
+
+ if (! isset($values['timezone'])) {
+ $values['timezone'] = 'autodetect';
+ }
+
+ if (! isset($values['auto_refresh'])) {
+ $values['auto_refresh'] = '1';
+ }
+
+ $this->populate($values);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ if (setlocale(LC_ALL, 0) === 'C') {
+ $this->warning(
+ $this->translate(
+ 'Your language setting is not applied because your platform is missing the corresponding locale.'
+ . ' Make sure to install the correct language pack and restart your web server afterwards.'
+ ),
+ false
+ );
+ }
+
+ $themeFile = StyleSheet::getThemeFile(Config::app()->get('themes', 'default'));
+ if (! (bool) Config::app()->get('themes', 'disabled', false)) {
+ $themes = Icinga::app()->getThemes();
+ if (count($themes) > 1) {
+ $defaultTheme = Config::app()->get('themes', 'default', StyleSheet::DEFAULT_THEME);
+ if (isset($themes[$defaultTheme])) {
+ $themes[$defaultTheme] .= ' (' . $this->translate('default') . ')';
+ }
+ $this->addElement(
+ 'select',
+ 'theme',
+ array(
+ 'label' => $this->translate('Theme', 'Form element label'),
+ 'multiOptions' => $themes,
+ 'autosubmit' => true,
+ 'value' => $this->preferences->getValue(
+ 'icingaweb',
+ 'theme',
+ $defaultTheme
+ )
+ )
+ );
+ }
+ }
+
+ if (isset($formData['theme'])) {
+ $themeFile = StyleSheet::getThemeFile($formData['theme']);
+ }
+
+ $disabled = [];
+ if ($themeFile !== null) {
+ $file = @file_get_contents($themeFile);
+ if ($file && strpos($file, StyleSheet::LIGHT_MODE_IDENTIFIER) === false) {
+ $disabled = ['', 'light', 'system'];
+ }
+ }
+
+ $this->addElement(
+ 'radio',
+ 'theme_mode',
+ [
+ 'class' => 'theme-mode-input',
+ 'label' => $this->translate('Theme Mode'),
+ 'multiOptions' => [
+ '' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-dark.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('Dark')),
+ 'light' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-light.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('Light')),
+ 'system' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-system.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('System'))
+ ],
+ 'disable' => $disabled,
+ 'escape' => false,
+ 'decorators' => array_merge(
+ array_slice(self::$defaultElementDecorators, 0, -1),
+ [['HtmlTag', ['tag' => 'div', 'class' => 'control-group theme-mode']]]
+ )
+ ]
+ );
+
+ /** @var GettextTranslator $translator */
+ $translator = StaticTranslator::$instance;
+
+ $languages = array();
+ $availableLocales = $translator->listLocales();
+
+ $locale = $this->getLocale($availableLocales);
+ if ($locale !== null) {
+ $languages['autodetect'] = sprintf($this->translate('Browser (%s)', 'preferences.form'), $locale);
+ }
+
+ $availableLocales[] = $translator->getDefaultLocale();
+ foreach ($availableLocales as $language) {
+ $languages[$language] = $language;
+ }
+
+ $tzList = array();
+ $tzList['autodetect'] = sprintf(
+ $this->translate('Browser (%s)', 'preferences.form'),
+ $this->getDefaultTimezone()
+ );
+ foreach (DateTimeZone::listIdentifiers() as $tz) {
+ $tzList[$tz] = $tz;
+ }
+
+ $this->addElement(
+ 'select',
+ 'language',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Your Current Language'),
+ 'description' => $this->translate('Use the following language to display texts and messages'),
+ 'multiOptions' => $languages,
+ 'value' => substr(setlocale(LC_ALL, 0), 0, 5)
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'timezone',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Your Current Timezone'),
+ 'description' => $this->translate('Use the following timezone for dates and times'),
+ 'multiOptions' => $tzList,
+ 'value' => $this->getDefaultTimezone()
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'show_application_state_messages',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Show application state messages'),
+ 'description' => $this->translate('Whether to show application state messages.'),
+ 'multiOptions' => [
+ 'system' => (bool) Config::app()->get('global', 'show_application_state_messages', true)
+ ? $this->translate('System (Yes)')
+ : $this->translate('System (No)'),
+ 1 => $this->translate('Yes'),
+ 0 => $this->translate('No')],
+ 'value' => 'system'
+ )
+ );
+
+ if (Auth::getInstance()->hasPermission('user/application/stacktraces')) {
+ $this->addElement(
+ 'checkbox',
+ 'show_stacktraces',
+ array(
+ 'value' => $this->getDefaultShowStacktraces(),
+ 'label' => $this->translate('Show Stacktraces'),
+ 'description' => $this->translate('Set whether to show an exception\'s stacktrace.')
+ )
+ );
+ }
+
+ $this->addElement(
+ 'checkbox',
+ 'show_benchmark',
+ array(
+ 'label' => $this->translate('Use benchmark')
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'auto_refresh',
+ array(
+ 'required' => false,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Enable auto refresh'),
+ 'description' => $this->translate(
+ 'This option allows you to enable or to disable the global page content auto refresh'
+ ),
+ 'value' => 1
+ )
+ );
+
+ if (isset($formData['auto_refresh']) && $formData['auto_refresh']) {
+ $this->addElement(
+ 'select',
+ 'auto_refresh_speed',
+ [
+ 'required' => false,
+ 'label' => $this->translate('Auto refresh speed'),
+ 'description' => $this->translate(
+ 'This option allows you to speed up or to slow down the global page content auto refresh'
+ ),
+ 'multiOptions' => [
+ '0.5' => $this->translate('Fast', 'refresh_speed'),
+ '' => $this->translate('Default', 'refresh_speed'),
+ '2' => $this->translate('Moderate', 'refresh_speed'),
+ '4' => $this->translate('Slow', 'refresh_speed')
+ ],
+ 'value' => ''
+ ]
+ );
+ }
+
+ $this->addElement(
+ 'number',
+ 'default_page_size',
+ array(
+ 'label' => $this->translate('Default page size'),
+ 'description' => $this->translate('Default number of items per page for list views'),
+ 'placeholder' => 25,
+ 'min' => 25,
+ 'step' => 1
+ )
+ );
+
+ if ($this->store) {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Save to the Preferences'),
+ 'decorators' => array('ViewHelper'),
+ 'class' => 'btn-primary'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'submit',
+ 'btn_submit_session',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Save for the current Session'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'preferences-progress');
+ $this->addElement(
+ 'note',
+ 'preferences-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'preferences-progress'))
+ )
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('btn_submit', 'btn_submit_session', 'preferences-progress'),
+ 'submit_buttons',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+ }
+
+ public function addSubmitButton()
+ {
+ return $this;
+ }
+
+ public function isSubmitted()
+ {
+ if (parent::isSubmitted()) {
+ return true;
+ }
+
+ return $this->getElement('btn_submit_session')->isChecked();
+ }
+
+ /**
+ * Return the current default timezone
+ *
+ * @return string
+ */
+ protected function getDefaultTimezone()
+ {
+ $detect = new TimezoneDetect();
+ if ($detect->success()) {
+ return $detect->getTimezoneName();
+ } else {
+ return @date_default_timezone_get();
+ }
+ }
+
+ /**
+ * Return the preferred locale based on the given HTTP header and the available translations
+ *
+ * @return string|null
+ */
+ protected function getLocale($availableLocales)
+ {
+ return isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])
+ ? (new Locale())->getPreferred($_SERVER['HTTP_ACCEPT_LANGUAGE'], $availableLocales)
+ : null;
+ }
+
+ /**
+ * Return the default global setting for show_stacktraces
+ *
+ * @return bool
+ */
+ protected function getDefaultShowStacktraces()
+ {
+ return Config::app()->get('global', 'show_stacktraces', true);
+ }
+}
diff --git a/application/forms/RepositoryForm.php b/application/forms/RepositoryForm.php
new file mode 100644
index 0000000..8e4665d
--- /dev/null
+++ b/application/forms/RepositoryForm.php
@@ -0,0 +1,453 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\NotFoundError;
+use Icinga\Repository\Repository;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form base-class providing standard functionality for extensible, updatable and reducible repositories
+ */
+abstract class RepositoryForm extends Form
+{
+ /**
+ * Insert mode
+ */
+ const MODE_INSERT = 0;
+
+ /**
+ * Update mode
+ */
+ const MODE_UPDATE = 1;
+
+ /**
+ * Delete mode
+ */
+ const MODE_DELETE = 2;
+
+ /**
+ * The repository being worked with
+ *
+ * @var Repository
+ */
+ protected $repository;
+
+ /**
+ * The target being worked with
+ *
+ * @var mixed
+ */
+ protected $baseTable;
+
+ /**
+ * How to interact with the repository
+ *
+ * @var int
+ */
+ protected $mode;
+
+ /**
+ * The name of the entry being handled when in mode update or delete
+ *
+ * @var string
+ */
+ protected $identifier;
+
+ /**
+ * The data of the entry to pre-populate the form with when in mode insert or update
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * Set the repository to work with
+ *
+ * @param Repository $repository
+ *
+ * @return $this
+ */
+ public function setRepository(Repository $repository)
+ {
+ $this->repository = $repository;
+ return $this;
+ }
+
+ /**
+ * Return the target being worked with
+ *
+ * @return mixed
+ */
+ protected function getBaseTable()
+ {
+ if ($this->baseTable === null) {
+ return $this->repository->getBaseTable();
+ }
+
+ return $this->baseTable;
+ }
+
+ /**
+ * Return the name of the entry to handle
+ *
+ * @return string
+ */
+ protected function getIdentifier()
+ {
+ return $this->identifier;
+ }
+
+ /**
+ * Return the current data of the entry being handled
+ *
+ * @return array
+ */
+ protected function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Return whether an entry should be inserted
+ *
+ * @return bool
+ */
+ public function shouldInsert()
+ {
+ return $this->mode === self::MODE_INSERT;
+ }
+
+ /**
+ * Return whether an entry should be udpated
+ *
+ * @return bool
+ */
+ public function shouldUpdate()
+ {
+ return $this->mode === self::MODE_UPDATE;
+ }
+
+ /**
+ * Return whether an entry should be deleted
+ *
+ * @return bool
+ */
+ public function shouldDelete()
+ {
+ return $this->mode === self::MODE_DELETE;
+ }
+
+ /**
+ * Add a new entry
+ *
+ * @param array $data The defaults to use, if any
+ *
+ * @return $this
+ */
+ public function add(array $data = null)
+ {
+ $this->mode = static::MODE_INSERT;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Edit an entry
+ *
+ * @param string $name The entry's name
+ * @param array $data The entry's current data
+ *
+ * @return $this
+ */
+ public function edit($name, array $data = null)
+ {
+ $this->mode = static::MODE_UPDATE;
+ $this->identifier = $name;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Remove an entry
+ *
+ * @param string $name The entry's name
+ *
+ * @return $this
+ */
+ public function remove($name)
+ {
+ $this->mode = static::MODE_DELETE;
+ $this->identifier = $name;
+ return $this;
+ }
+
+ /**
+ * Fetch and return the entry to pre-populate the form with when in mode update
+ *
+ * @return false|object
+ */
+ protected function fetchEntry()
+ {
+ return $this->repository
+ ->select()
+ ->from($this->getBaseTable())
+ ->applyFilter($this->createFilter())
+ ->fetchRow();
+ }
+
+ /**
+ * Return whether the entry supposed to be removed exists
+ *
+ * @return bool
+ */
+ protected function entryExists()
+ {
+ $count = $this->repository
+ ->select()
+ ->from($this->getBaseTable())
+ ->addFilter($this->createFilter())
+ ->count();
+ return $count > 0;
+ }
+
+ /**
+ * Insert the new entry
+ */
+ protected function insertEntry()
+ {
+ $this->repository->insert($this->getBaseTable(), $this->getValues());
+ }
+
+ /**
+ * Update the entry
+ */
+ protected function updateEntry()
+ {
+ $this->repository->update($this->getBaseTable(), $this->getValues(), $this->createFilter());
+ }
+
+ /**
+ * Delete the entry
+ */
+ protected function deleteEntry()
+ {
+ $this->repository->delete($this->getBaseTable(), $this->createFilter());
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ if ($this->shouldInsert()) {
+ $this->createInsertElements($formData);
+ } elseif ($this->shouldUpdate()) {
+ $this->createUpdateElements($formData);
+ } elseif ($this->shouldDelete()) {
+ $this->createDeleteElements($formData);
+ }
+ }
+
+ /**
+ * Prepare the form for the requested mode
+ */
+ public function onRequest()
+ {
+ if ($this->shouldInsert()) {
+ $this->onInsertRequest();
+ } elseif ($this->shouldUpdate()) {
+ $this->onUpdateRequest();
+ } elseif ($this->shouldDelete()) {
+ $this->onDeleteRequest();
+ }
+ }
+
+ /**
+ * Prepare the form for mode insert
+ *
+ * Populates the form with the data passed to add().
+ */
+ protected function onInsertRequest()
+ {
+ $data = $this->getData();
+ if (! empty($data)) {
+ $this->setDefaults($data);
+ }
+ }
+
+ /**
+ * Prepare the form for mode update
+ *
+ * Populates the form with either the data passed to edit() or tries to fetch it from the repository.
+ *
+ * @throws NotFoundError In case the entry to update cannot be found
+ */
+ protected function onUpdateRequest()
+ {
+ $data = $this->getData();
+ if ($data === null) {
+ $row = $this->fetchEntry();
+ if ($row === false) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+
+ $data = get_object_vars($row);
+ }
+
+ $this->setDefaults($data);
+ }
+
+ /**
+ * Prepare the form for mode delete
+ *
+ * Verifies that the repository contains the entry to delete.
+ *
+ * @throws NotFoundError In case the entry to delete cannot be found
+ */
+ protected function onDeleteRequest()
+ {
+ if (! $this->entryExists()) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Apply the requested mode on the repository
+ *
+ * @return ?bool
+ */
+ public function onSuccess()
+ {
+ if ($this->shouldInsert()) {
+ return $this->onInsertSuccess();
+ } elseif ($this->shouldUpdate()) {
+ return $this->onUpdateSuccess();
+ } elseif ($this->shouldDelete()) {
+ return $this->onDeleteSuccess();
+ }
+ }
+
+ /**
+ * Apply mode insert on the repository
+ *
+ * @return bool
+ */
+ protected function onInsertSuccess()
+ {
+ try {
+ $this->insertEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getInsertMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getInsertMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode update on the repository
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ try {
+ $this->updateEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getUpdateMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getUpdateMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode delete on the repository
+ *
+ * @return bool
+ */
+ protected function onDeleteSuccess()
+ {
+ try {
+ $this->deleteEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getDeleteMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getDeleteMessage(true));
+ return true;
+ }
+
+ /**
+ * Create and add elements to this form to insert an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createInsertElements(array $formData);
+
+ /**
+ * Create and add elements to this form to update an entry
+ *
+ * Calls createInsertElements() by default. Overwrite this to add different elements when in mode update.
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+ }
+
+ /**
+ * Create and add elements to this form to delete an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createDeleteElements(array $formData);
+
+ /**
+ * Create and return a filter to use when selecting, updating or deleting an entry
+ *
+ * @return Filter
+ */
+ abstract protected function createFilter();
+
+ /**
+ * Return a notification message to use when inserting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getInsertMessage($success);
+
+ /**
+ * Return a notification message to use when updating an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getUpdateMessage($success);
+
+ /**
+ * Return a notification message to use when deleting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getDeleteMessage($success);
+}
diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php
new file mode 100644
index 0000000..58387f7
--- /dev/null
+++ b/application/forms/Security/RoleForm.php
@@ -0,0 +1,632 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Security;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Manager;
+use Icinga\Authentication\AdmissionLoader;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Notification;
+use ipl\Web\Widget\Icon;
+
+/**
+ * Form for managing roles
+ */
+class RoleForm extends RepositoryForm
+{
+ /**
+ * The name to use instead of `*`
+ */
+ const WILDCARD_NAME = 'allAndEverything';
+
+ /**
+ * The prefix used to deny a permission
+ */
+ const DENY_PREFIX = 'no-';
+
+ /**
+ * Provided permissions by currently installed modules
+ *
+ * @var array
+ */
+ protected $providedPermissions;
+
+ /**
+ * Provided restrictions by currently installed modules
+ *
+ * @var array
+ */
+ protected $providedRestrictions;
+
+ public function init()
+ {
+ $this->setAttrib('class', self::DEFAULT_CLASSES . ' role-form');
+
+ list($this->providedPermissions, $this->providedRestrictions) = static::collectProvidedPrivileges();
+ }
+
+ protected function createFilter()
+ {
+ return Filter::where('name', $this->getIdentifier());
+ }
+
+ public function filterName($value, $allowBrackets = false)
+ {
+ return parent::filterName($value, $allowBrackets) . '_element';
+ }
+
+ public function createInsertElements(array $formData = array())
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ [
+ 'required' => true,
+ 'label' => $this->translate('Role Name'),
+ 'description' => $this->translate('The name of the role')
+ ]
+ );
+ $this->addElement(
+ 'select',
+ 'parent',
+ [
+ 'label' => $this->translate('Inherit From'),
+ 'description' => $this->translate('Choose a role from which to inherit privileges'),
+ 'value' => '',
+ 'multiOptions' => array_merge(
+ ['' => $this->translate('None', 'parent role')],
+ $this->collectRoles()
+ )
+ ]
+ );
+ $this->addElement(
+ 'textarea',
+ 'users',
+ [
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate('Comma-separated list of users that are assigned to the role')
+ ]
+ );
+ $this->addElement(
+ 'textarea',
+ 'groups',
+ [
+ 'label' => $this->translate('Groups'),
+ 'description' => $this->translate('Comma-separated list of groups that are assigned to the role')
+ ]
+ );
+ $this->addElement(
+ 'checkbox',
+ self::WILDCARD_NAME,
+ [
+ 'autosubmit' => true,
+ 'label' => $this->translate('Administrative Access'),
+ 'description' => $this->translate('Everything is allowed')
+ ]
+ );
+ $this->addElement(
+ 'checkbox',
+ 'unrestricted',
+ [
+ 'autosubmit' => true,
+ 'uncheckedValue' => null,
+ 'label' => $this->translate('Unrestricted Access'),
+ 'description' => $this->translate('Access to any data is completely unrestricted')
+ ]
+ );
+
+ $hasAdminPerm = isset($formData[self::WILDCARD_NAME]) && $formData[self::WILDCARD_NAME];
+ $isUnrestricted = isset($formData['unrestricted']) && $formData['unrestricted'];
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $this->sortPermissions($permissionList);
+
+ $anythingGranted = false;
+ $anythingRefused = false;
+ $anythingRestricted = false;
+
+ $elements = [$moduleName . '_header'];
+ // The actual element is added last
+
+ $elements[] = 'permission_header';
+ $this->addElement('note', 'permission_header', [
+ 'decorators' => [['Callback', ['callback' => function () {
+ return '<h4>' . $this->translate('Permissions') . '</h4>'
+ . $this->getView()->icon('ok', $this->translate(
+ 'Grant access by toggling a switch below'
+ ))
+ . $this->getView()->icon('cancel', $this->translate(
+ 'Deny access by toggling a switch below'
+ ));
+ }]], ['HtmlTag', ['tag' => 'div']]]
+ ]);
+
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+
+ if (isset($formData[$elementName]) && $formData[$elementName]) {
+ $anythingGranted = true;
+ }
+
+ if ($hasFullPerm || $hasAdminPerm) {
+ $elementName .= '_fake';
+ }
+
+ $denyCheckbox = null;
+ if (! isset($spec['isFullPerm'])
+ && substr($name, 0, strlen(self::DENY_PREFIX)) !== self::DENY_PREFIX
+ ) {
+ $denyCheckbox = $this->createElement('checkbox', $this->filterName(self::DENY_PREFIX . $name), [
+ 'decorators' => ['ViewHelper']
+ ]);
+ $this->addElement($denyCheckbox);
+ $this->removeFromIteration($denyCheckbox->getName());
+
+ if (isset($formData[$denyCheckbox->getName()]) && $formData[$denyCheckbox->getName()]) {
+ $anythingRefused = true;
+ }
+ }
+
+ $elements[] = $elementName;
+ $this->addElement(
+ 'checkbox',
+ $elementName,
+ [
+ 'ignore' => $hasFullPerm || $hasAdminPerm,
+ 'autosubmit' => isset($spec['isFullPerm']),
+ 'disabled' => $hasFullPerm || $hasAdminPerm ?: null,
+ 'value' => $hasFullPerm || $hasAdminPerm,
+ 'label' => isset($spec['label'])
+ ? $spec['label']
+ : join('', iterator_to_array(call_user_func(function ($segments) {
+ foreach ($segments as $segment) {
+ if ($segment[0] === '/') {
+ // Adds a zero-width char after each slash to help browsers break onto newlines
+ yield '/&#8203;';
+ yield '<span class="no-wrap">' . substr($segment, 1) . '</span>';
+ } else {
+ yield '<em>' . $segment . '</em>';
+ }
+ }
+ }, preg_split(
+ '~(/[^/]+)~',
+ $name,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ )))),
+ 'description' => isset($spec['description']) ? $spec['description'] : $name,
+ 'decorators' => array_merge(
+ array_slice(self::$defaultElementDecorators, 0, 3),
+ [['Callback', ['callback' => function () use ($denyCheckbox) {
+ return $denyCheckbox ? $denyCheckbox->render() : '';
+ }]]],
+ array_slice(self::$defaultElementDecorators, 3)
+ )
+ ]
+ )
+ ->getElement($elementName)
+ ->getDecorator('Label')
+ ->setOption('escape', false);
+
+ if ($hasFullPerm || $hasAdminPerm) {
+ // Add a hidden element to preserve the configured permission value
+ $this->addElement('hidden', $this->filterName($name));
+ }
+
+ if (isset($spec['isFullPerm'])) {
+ $filteredName = $this->filterName($name);
+ $hasFullPerm = isset($formData[$filteredName]) && $formData[$filteredName];
+ }
+ }
+
+ if (isset($this->providedRestrictions[$moduleName])) {
+ $elements[] = 'restriction_header';
+ $this->addElement('note', 'restriction_header', [
+ 'value' => '<h4>' . $this->translate('Restrictions') . '</h4>',
+ 'decorators' => ['ViewHelper']
+ ]);
+
+ foreach ($this->providedRestrictions[$moduleName] as $name => $spec) {
+ $elementName = $this->filterName($name);
+
+ if (isset($formData[$elementName]) && $formData[$elementName]) {
+ $anythingRestricted = true;
+ }
+
+ $elements[] = $elementName;
+ $this->addElement(
+ 'text',
+ $elementName,
+ [
+ 'label' => isset($spec['label'])
+ ? $spec['label']
+ : join('', iterator_to_array(call_user_func(function ($segments) {
+ foreach ($segments as $segment) {
+ if ($segment[0] === '/') {
+ // Add zero-width char after each slash to help browsers break onto newlines
+ yield '/&#8203;';
+ yield '<span class="no-wrap">' . substr($segment, 1) . '</span>';
+ } else {
+ yield '<em>' . $segment . '</em>';
+ }
+ }
+ }, preg_split(
+ '~(/[^/]+)~',
+ $name,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ )))),
+ 'description' => $spec['description'],
+ 'class' => $isUnrestricted ? 'unrestricted-role' : '',
+ 'readonly' => $isUnrestricted ?: null
+ ]
+ )
+ ->getElement($elementName)
+ ->getDecorator('Label')
+ ->setOption('escape', false);
+ }
+ }
+
+ $this->addElement(
+ 'note',
+ $moduleName . '_header',
+ [
+ 'decorators' => ['ViewHelper'],
+ 'value' => '<summary class="collapsible-control">'
+ . '<span>' . ($moduleName !== 'application'
+ ? sprintf('%s <em>%s</em>', $moduleName, $this->translate('Module'))
+ : 'Icinga Web 2'
+ ) . '</span>'
+ . '<span class="privilege-preview">'
+ . ($hasAdminPerm || $anythingGranted ? new Icon('check-circle', ['class' => 'granted']) : '')
+ . ($anythingRefused ? new Icon('times-circle', ['class' => 'refused']) : '')
+ . (! $isUnrestricted && $anythingRestricted
+ ? new Icon('filter', ['class' => 'restricted'])
+ : ''
+ )
+ . '</span>'
+ . new Icon('angles-down', ['class' => 'collapse-icon'])
+ . new Icon('angles-left', ['class' => 'expand-icon'])
+ . '</summary>'
+ ]
+ );
+
+ $this->addDisplayGroup($elements, $moduleName . '_elements', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', [
+ 'tag' => 'details',
+ 'class' => 'collapsible'
+ ]],
+ ['Fieldset']
+ ]
+ ]);
+ }
+ }
+
+ protected function createDeleteElements(array $formData)
+ {
+ }
+
+ public function fetchEntry()
+ {
+ $role = parent::fetchEntry();
+ if ($role === false) {
+ return false;
+ }
+
+ $values = [
+ 'parent' => $role->parent,
+ 'name' => $role->name,
+ 'users' => $role->users,
+ 'groups' => $role->groups,
+ 'unrestricted' => $role->unrestricted,
+ self::WILDCARD_NAME => $role->permissions && preg_match('~(?>^|,)\*(?>$|,)~', $role->permissions)
+ ];
+
+ if (! empty($role->permissions) || ! empty($role->refusals)) {
+ $permissions = StringHelper::trimSplit($role->permissions);
+ $refusals = StringHelper::trimSplit($role->refusals);
+
+ list($permissions, $newRefusals) = AdmissionLoader::migrateLegacyPermissions($permissions);
+ if (! empty($newRefusals)) {
+ array_push($refusals, ...$newRefusals);
+ }
+
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ if (in_array($name, $permissions, true)) {
+ $values[$this->filterName($name)] = 1;
+
+ if (isset($spec['isFullPerm'])) {
+ $hasFullPerm = true;
+ }
+ }
+
+ if (in_array($name, $refusals, true)) {
+ $values[$this->filterName(self::DENY_PREFIX . $name)] = 1;
+ }
+ }
+
+ if ($hasFullPerm) {
+ unset($values[$this->filterName(Manager::MODULE_PERMISSION_NS . $moduleName)]);
+ }
+ }
+ }
+
+ foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
+ foreach ($restrictionList as $name => $spec) {
+ if (isset($role->$name)) {
+ $values[$this->filterName($name)] = $role->$name;
+ }
+ }
+ }
+
+ return (object) $values;
+ }
+
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+
+ foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
+ foreach ($restrictionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+ if (isset($values[$elementName])) {
+ $values[$name] = $values[$elementName];
+ unset($values[$elementName]);
+ }
+ }
+ }
+
+ $permissions = [];
+ if (isset($values[self::WILDCARD_NAME]) && $values[self::WILDCARD_NAME]) {
+ $permissions[] = '*';
+ }
+
+ $refusals = [];
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+ if (isset($values[$elementName]) && $values[$elementName]) {
+ $permissions[] = $name;
+
+ if (isset($spec['isFullPerm'])) {
+ $hasFullPerm = true;
+ }
+ }
+
+ $denyName = $this->filterName(self::DENY_PREFIX . $name);
+ if (isset($values[$denyName]) && $values[$denyName]) {
+ $refusals[] = $name;
+ }
+
+ unset($values[$elementName], $values[$denyName]);
+ }
+
+ $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
+ if ($hasFullPerm && ! in_array($modulePermission, $permissions, true)) {
+ $permissions[] = $modulePermission;
+ }
+ }
+
+ unset($values[self::WILDCARD_NAME]);
+ $values['refusals'] = join(',', $refusals);
+ $values['permissions'] = join(',', $permissions);
+ return ConfigForm::transformEmptyValuesToNull($values);
+ }
+
+ protected function getInsertMessage($success)
+ {
+ return $success ? $this->translate('Role created') : $this->translate('Role creation failed');
+ }
+
+ protected function getUpdateMessage($success)
+ {
+ return $success ? $this->translate('Role updated') : $this->translate('Role update failed');
+ }
+
+ protected function getDeleteMessage($success)
+ {
+ return $success ? $this->translate('Role removed') : $this->translate('Role removal failed');
+ }
+
+ protected function sortPermissions(&$permissions)
+ {
+ return uksort($permissions, function ($a, $b) use ($permissions) {
+ if (isset($permissions[$a]['isUsagePerm'])) {
+ return isset($permissions[$b]['isFullPerm']) ? 1 : -1;
+ } elseif (isset($permissions[$b]['isUsagePerm'])) {
+ return isset($permissions[$a]['isFullPerm']) ? -1 : 1;
+ }
+
+ $aParts = explode('/', $a);
+ $bParts = explode('/', $b);
+
+ do {
+ $a = array_shift($aParts);
+ $b = array_shift($bParts);
+ } while ($a === $b);
+
+ return strnatcmp($a ?? '', $b ?? '');
+ });
+ }
+
+ protected function collectRoles()
+ {
+ // Function to get all connected children. Used to avoid reference loops
+ $getChildren = function ($name, $children = []) use (&$getChildren) {
+ foreach ($this->repository->select()->where('parent', $name) as $child) {
+ if (isset($children[$child->name])) {
+ // Don't follow already established loops here,
+ // the user should be able to solve such in the UI
+ continue;
+ }
+
+ $children[$child->name] = true;
+ $children = $getChildren($child->name, $children);
+ }
+
+ return $children;
+ };
+
+ $children = $this->getIdentifier() !== null ? $getChildren($this->getIdentifier()) : [];
+
+ $names = [];
+ foreach ($this->repository->select() as $role) {
+ if ($role->name !== $this->getIdentifier() && ! isset($children[$role->name])) {
+ $names[] = $role->name;
+ }
+ }
+
+ return array_combine($names, $names);
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if ($this->getIdentifier() && ($newName = $this->getValue('name')) !== $this->getIdentifier()) {
+ $this->repository->update(
+ $this->getBaseTable(),
+ ['parent' => $newName],
+ Filter::where('parent', $this->getIdentifier())
+ );
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+
+ /**
+ * Collect permissions and restrictions provided by Icinga Web 2 and modules
+ *
+ * @return array[$permissions, $restrictions]
+ */
+ public static function collectProvidedPrivileges()
+ {
+ $providedPermissions['application'] = [
+ 'application/announcements' => [
+ 'description' => t('Allow to manage announcements')
+ ],
+ 'application/log' => [
+ 'description' => t('Allow to view the application log')
+ ],
+ 'config/*' => [
+ 'description' => t('Allow full config access')
+ ],
+ 'config/general' => [
+ 'description' => t('Allow to adjust the general configuration')
+ ],
+ 'config/modules' => [
+ 'description' => t('Allow to enable/disable and configure modules')
+ ],
+ 'config/resources' => [
+ 'description' => t('Allow to manage resources')
+ ],
+ 'config/navigation' => [
+ 'description' => t('Allow to view and adjust shared navigation items')
+ ],
+ 'config/access-control/*' => [
+ 'description' => t('Allow to fully manage access-control')
+ ],
+ 'config/access-control/users' => [
+ 'description' => t('Allow to manage user accounts')
+ ],
+ 'config/access-control/groups' => [
+ 'description' => t('Allow to manage user groups')
+ ],
+ 'config/access-control/roles' => [
+ 'description' => t('Allow to manage roles')
+ ],
+ 'user/*' => [
+ 'description' => t('Allow all account related functionalities')
+ ],
+ 'user/password-change' => [
+ 'description' => t('Allow password changes in the account preferences')
+ ],
+ 'user/application/stacktraces' => [
+ 'description' => t('Allow to adjust in the preferences whether to show stacktraces')
+ ],
+ 'user/share/navigation' => [
+ 'description' => t('Allow to share navigation items')
+ ],
+ 'application/sessions' => [
+ 'description' => t('Allow to manage user sessions')
+ ],
+ 'application/migrations' => [
+ 'description' => t('Allow to apply pending application migrations')
+ ]
+ ];
+
+ $providedRestrictions['application'] = [
+ 'application/share/users' => [
+ 'description' => t('Restrict which users this role can share items and information with')
+ ],
+ 'application/share/groups' => [
+ 'description' => t('Restrict which groups this role can share items and information with')
+ ]
+ ];
+
+ $mm = Icinga::app()->getModuleManager();
+ foreach ($mm->listInstalledModules() as $moduleName) {
+ $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
+ $providedPermissions[$moduleName][$modulePermission] = [
+ 'isUsagePerm' => true,
+ 'label' => t('General Module Access'),
+ 'description' => sprintf(t('Allow access to module %s'), $moduleName)
+ ];
+
+ $module = $mm->getModule($moduleName, false);
+ $permissions = $module->getProvidedPermissions();
+
+ $providedPermissions[$moduleName][$moduleName . '/*'] = [
+ 'isFullPerm' => true,
+ 'label' => t('Full Module Access')
+ ];
+
+ foreach ($permissions as $permission) {
+ /** @var object $permission */
+ $providedPermissions[$moduleName][$permission->name] = [
+ 'description' => $permission->description
+ ];
+ }
+
+ foreach ($module->getProvidedRestrictions() as $restriction) {
+ $providedRestrictions[$moduleName][$restriction->name] = [
+ 'description' => $restriction->description
+ ];
+ }
+ }
+
+ return [$providedPermissions, $providedRestrictions];
+ }
+}
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 @@
+<?php
+
+use Icinga\Web\Url;
+use Icinga\Web\Notification;
+use Icinga\Authentication\Auth;
+use ipl\Html\HtmlString;
+use ipl\Web\Widget\Icon;
+
+$moduleName = $this->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';
+}
+
+?>
+<div id="header">
+ <div id="announcements" class="container">
+ <?= $this->widget('announcements') ?>
+ </div>
+</div>
+<div id="content-wrapper">
+<?php if (! $this->layout()->isIframe): ?>
+ <div id="sidebar">
+ <div id="header-logo-container">
+ <?= $this->qlink(
+ '',
+ Auth::getInstance()->isAuthenticated() ? 'dashboard' : '',
+ null,
+ array(
+ 'aria-hidden' => 'true',
+ 'data-base-target' => '_main',
+ 'id' => 'header-logo'
+ )
+ ); ?>
+ <div id="mobile-menu-toggle">
+ <button type="button"><?= $this->icon('menu') ?><?= $this->icon('cancel') ?></button>
+ </div>
+ </div>
+ <?= $this->render('parts/navigation.phtml'); ?>
+ </div>
+<?php endif ?>
+ <div id="main" role="main">
+ <div id="col1"
+ class="container<?= $moduleClass ?>"
+ <?php if ($moduleName): ?>
+ data-icinga-module="<?= $moduleName ?>"
+ <?php endif ?>
+ data-icinga-url="<?= $this->escape(Url::fromRequest()->without('renderLayout')->getAbsoluteUrl()); ?>"
+ <?= $refresh; ?>
+ >
+ <?= $this->render($inlineLayoutScript) ?>
+ </div>
+ <div id="col2" class="container"></div>
+ <div id="col3" class="container"></div>
+ </div>
+</div>
+<div id="footer">
+ <ul role="alert" id="notifications"><?php
+
+ $notifications = Notification::getInstance();
+ if ($notifications->hasMessages()) {
+ foreach ($notifications->popMessages() as $m) {
+ switch ($m->type) {
+ case 'success':
+ $icon = new HtmlString(new Icon('check-circle'));
+ break;
+ case 'error':
+ $icon = new HtmlString(new Icon('times'));
+ break;
+ case 'warning':
+ $icon = new HtmlString(new Icon('exclamation-triangle'));
+ break;
+ case 'info':
+ $icon = new HtmlString(new Icon('info-circle'));
+ break;
+ default:
+ $icon = '';
+ break;
+ }
+
+ echo '<li class="' . $m->type . '">' . $icon . $this->escape($m->message) . '</li>';
+ }
+ }
+ ?></ul>
+ <div id="application-state-summary" class="container" data-icinga-url="<?= $this->url('application-state/summary') ?>" data-last-update="-1" data-icinga-refresh="60"></div>
+</div>
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 @@
+<?php
+
+use ipl\I18n\Locale;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\StaticTranslator;
+
+/** @var GettextTranslator $translator */
+$translator = StaticTranslator::$instance;
+
+$lang = (new Locale())->parseLocale($translator->getLocale())->language;
+$showFullscreen = $this->layout()->showFullscreen;
+$innerLayoutScript = $this->layout()->innerLayout . '.phtml';
+
+?><!DOCTYPE html>
+<html class="no-js" lang="<?= $lang ?>">
+<head>
+ <meta charset="utf-8">
+ <meta name="google" value="notranslate">
+ <meta http-equiv="cleartype" content="on">
+ <title><?= $this->title ? $this->escape($this->title) : $this->defaultTitle ?></title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="application-name" content="Icinga Web 2">
+ <meta name="apple-mobile-web-app-title" content="Icinga">
+ <link rel="mask-icon" href="<?= $this->baseUrl('img/website-icon.svg') ?>" color="#0096BF">
+ <link type="image/png" rel="shortcut icon" href="<?= $this->baseUrl('img/favicon.png') ?>" />
+ <link rel="apple-touch-icon" href="<?= $this->baseUrl('img/touch-icon.png') ?>">
+</head>
+<body id="body">
+<div id="layout" class="default-layout<?php if ($showFullscreen): ?> fullscreen-layout<?php endif ?>">
+ <?= $this->render($innerLayoutScript); ?>
+</div>
+</body>
+</html>
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 @@
+<div id="guest-error">
+ <div class="centered-ghost">
+ <div class="centered-content">
+ <div id="icinga-logo" aria-hidden="true"></div>
+ <div id="guest-error-message">
+ <?= $this->render('inline.phtml') ?>
+ </div>
+ </div>
+ </div>
+</div>
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 @@
+<?php
+
+use ipl\I18n\Locale;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\StaticTranslator;
+use ipl\Web\Widget\Icon;
+
+if (array_key_exists('_dev', $_GET)) {
+ $jsfile = 'js/icinga.dev.js';
+ $cssfile = 'css/icinga.css';
+} else {
+ $jsfile = 'js/icinga.min.js';
+ $cssfile = 'css/icinga.min.css';
+}
+
+/** @var GettextTranslator $translator */
+$translator = StaticTranslator::$instance;
+
+$lang = (new Locale())->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';
+
+?><!DOCTYPE html>
+<html
+ class="no-js<?= $iframeClass ?>" lang="<?= $lang ?>"
+ data-icinga-window-name="<?= $this->protectId('Icinga') ?>"
+ data-icinga-timezone="<?= $timezone ?>"
+ data-icinga-base-url="<?= $this->baseUrl(); ?>"
+ <?php if ($isIframe): ?>
+ data-icinga-is-iframe
+ <?php endif ?>
+>
+<head>
+ <meta charset="utf-8">
+ <meta name="google" value="notranslate">
+ <meta http-equiv="cleartype" content="on">
+ <title><?= $this->title ? $this->escape($this->title) . ' :: ' : '' ?><?= $this->defaultTitle ?></title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="application-name" content="Icinga Web 2">
+ <meta name="apple-mobile-web-app-title" content="Icinga">
+ <link rel="mask-icon" href="<?= $this->baseUrl('img/website-icon.svg') ?>" color="#0096BF">
+<?php if ($isIframe): ?>
+ <base target="_parent">
+<?php else: ?>
+ <base href="<?= $this->baseUrl(); ?>/">
+<?php endif ?>
+ <link rel="stylesheet" href="<?= $this->href($cssfile) ?>" media="all" type="text/css" />
+ <link type="image/png" rel="shortcut icon" href="<?= $this->baseUrl('img/favicon.png') ?>" />
+ <link rel="apple-touch-icon" href="<?= $this->baseUrl('img/touch-icon.png') ?>">
+</head>
+<body id="body" class="loading">
+<pre id="responsive-debug"></pre>
+<div id="layout" class="default-layout<?php if ($showFullscreen): ?> fullscreen-layout<?php endif ?>">
+<?= $this->render($innerLayoutScript); ?>
+</div>
+<div id="collapsible-control-ghost" class="collapsible-control">
+ <button type="button">
+ <!-- TODO: Accessibility attributes are missing since usage of the Icon class -->
+ <?= new Icon('angle-double-down', ['class' => 'expand-icon', 'title' => $this->translate('Expand')]) ?>
+ <?= new Icon('angle-double-up', ['class' => 'collapse-icon', 'title' => $this->translate('Collapse')]) ?>
+ </button>
+</div>
+<div id="modal-ghost">
+ <div>
+ <section class="modal-window">
+ <div class="modal-header">
+ <h1></h1>
+ <button type="button"><?= $this->icon('cancel') ?></button>
+ </div>
+ <div class="modal-area">
+ <div id="modal-content" data-base-target="modal-content" tabindex data-no-icinga-ajax></div>
+ </div>
+ </section>
+ </div>
+</div>
+<script type="text/javascript" src="<?= $this->href($jsfile) ?>"></script>
+<script type="text/javascript" src="<?= $this->href('js/bootstrap.js') ?>"></script>
+</body>
+</html>
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 @@
+<?php
+
+use Icinga\Web\Menu;
+
+// Don't render a menu for unauthenticated users unless menu is auth aware
+if (! $this->auth()->isAuthenticated()) {
+ return;
+}
+
+?>
+<div class="skip-links">
+ <h1 class="sr-only"><?= t('Accessibility Skip Links') ?></h1>
+ <ul>
+ <li>
+ <a href="#main"><?= t('Skip to Content') ?></a>
+ </li>
+ <li>
+ <?= $this->layout()->autoRefreshForm ?>
+ </li>
+ </ul>
+</div>
+<div id="menu" data-last-update="-1" data-base-target="_main" class="container"
+ data-icinga-url="<?= $this->href('layout/menu') ?>" data-icinga-refresh="15">
+ <?= $this->partial(
+ 'layout/menu.phtml',
+ 'default',
+ array(
+ 'menuRenderer' => (new Menu())->getRenderer()->setUseStandardItemRenderer()
+ )
+ ) ?>
+</div>
+<button id="toggle-sidebar" title="<?= $this->translate('Toggle Menu') ?>">
+ <i id="close-sidebar" class="icon-angle-double-left"></i>
+ <i id="open-sidebar" class="icon-angle-double-right"></i>
+</button>
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 @@
+<?php
+
+use Icinga\Application\Icinga;
+use Icinga\Web\StyleSheet;
+
+
+$moduleName = $this->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));
+
+
+?><!DOCTYPE html>
+<html>
+<head>
+<style>
+<?= StyleSheet::forPdf() ?>
+</style>
+<base href="<?= $this->serverUrl() ?>">
+</head>
+<body>
+<div id="header">
+ <table>
+ <tbody>
+ <tr>
+ <th class="title"><?= $this->escape($this->title) ?></th>
+ <td style="text-align: right;"><img width="75" src="data:image/png;base64,<?= $logo ?>"></td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<div id="footer">
+ <div class="page-number"></div>
+</div>
+<div class="<?= $moduleClass ?>">
+ <?= $this->render('inline.phtml') ?>
+</div>
+</body>
+</html>
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 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+/**
+ * Helper for creating ticket links from ticket hooks
+ */
+class Zend_View_Helper_CreateTicketLinks extends Zend_View_Helper_Abstract
+{
+ /**
+ * Create ticket links form ticket hooks
+ *
+ * @param string $text
+ *
+ * @return string
+ * @see \Icinga\Application\Hook\TicketHook::createLinks()
+ */
+ public function createTicketLinks($text)
+ {
+ $tickets = $this->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 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+/**
+ * Render date input controls
+ */
+class Zend_View_Helper_FormDate extends Zend_View_Helper_FormElement
+{
+ /**
+ * Render the date input control
+ *
+ * @param string $name
+ * @param int $value
+ * @param array $attribs
+ *
+ * @return string The rendered date input control
+ */
+ public function formDate($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"';
+ }
+
+ /** @var \Icinga\Web\View $view */
+ $view = $this->view;
+
+ $html5 = sprintf(
+ '<input type="date" name="%s" id="%s" value="%s"%s%s%s',
+ $view->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 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * Render date-and-time input controls
+ */
+class Zend_View_Helper_FormDateTime extends Zend_View_Helper_FormElement
+{
+ /**
+ * Format date and time
+ *
+ * @param DateTime $dateTime
+ * @param bool $local
+ *
+ * @return string
+ */
+ public function formatDate(DateTime $dateTime, $local)
+ {
+ $format = (bool) $local === true ? 'Y-m-d\TH:i:s' : DateTime::RFC3339;
+ return $dateTime->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(
+ '<input type="%s" data-use-datetime-picker name="%s" id="%s" step="1" value="%s"%s%s%s',
+ $type,
+ $this->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 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * Render number input controls
+ */
+class Zend_View_Helper_FormNumber extends Zend_View_Helper_FormElement
+{
+ /**
+ * Format a number
+ *
+ * @param $number
+ *
+ * @return string
+ */
+ public function formatNumber($number)
+ {
+ if (empty($number)) {
+ return $number;
+ }
+ return $this->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(
+ '<input type="number" name="%s" id="%s" value="%s"%s%s%s%s%s%s',
+ $this->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 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+/**
+ * Render time input controls
+ */
+class Zend_View_Helper_FormTime extends Zend_View_Helper_FormElement
+{
+ /**
+ * Render the time input control
+ *
+ * @param string $name
+ * @param int $value
+ * @param array $attribs
+ *
+ * @return string The rendered time input control
+ */
+ public function formTime($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"';
+ }
+
+ /** @var \Icinga\Web\View $view */
+ $view = $this->view;
+
+ $html5 = sprintf(
+ '<input type="time" name="%s" id="%s" value="%s"%s%s%s',
+ $view->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 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+/**
+ * Class Zend_View_Helper_Util
+ */
+class Zend_View_Helper_ProtectId extends Zend_View_Helper_Abstract
+{
+ public function protectId($id)
+ {
+ return Zend_Controller_Front::getInstance()->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 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * Class Zend_View_Helper_Util
+ */
+class Zend_View_Helper_Util extends Zend_View_Helper_Abstract
+{
+ public function util()
+ {
+ return $this;
+ }
+
+ public static function showTimeSince($timestamp)
+ {
+ if (! $timestamp) {
+ return 'unknown';
+ }
+ $duration = time() - $timestamp;
+ if ($duration > 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 @@
+<?php
+
+use Icinga\Application\MigrationManager;
+use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
+use ipl\Html\HtmlElement;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\StateBadge;
+
+?>
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div id="about" class="content">
+
+ <?= $this->img('img/icinga-logo-big.svg', null, array('class' => 'icinga-logo', 'width' => 194)) ?>
+
+ <section>
+ <table class="name-value-table">
+ <?php if (isset($version['appVersion'])): ?>
+ <tr>
+ <th><?= $this->translate('Icinga Web 2 Version') ?></th>
+ <td><?= $this->escape($version['appVersion']) ?></td>
+ </tr>
+ <?php endif ?>
+ <?php if (isset($version['gitCommitID'])): ?>
+ <tr>
+ <th><?= $this->translate('Git commit') ?></th>
+ <td><?= $this->escape($version['gitCommitID']) ?></td>
+ </tr>
+ <?php endif ?>
+ <tr>
+ <th><?= $this->translate('PHP Version') ?></th>
+ <td><?= $this->escape(PHP_VERSION) ?></td>
+ </tr>
+ <?php if (isset($version['gitCommitDate'])): ?>
+ <tr>
+ <th><?= $this->translate('Git commit date') ?></th>
+ <td><?= $this->escape($version['gitCommitDate']) ?></td>
+ </tr>
+ <?php endif ?>
+ </table>
+
+ <div class="external-links">
+ <div class="col">
+ <?=
+ 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'),
+ ]
+ );
+ ?>
+ </div>
+ <div class="col">
+ <?=
+ HtmlElement::create('a', [
+ 'href' => 'https://icinga.com/community/',
+ 'target' => '_blank',
+ 'title' => $this->translate('Icinga Community')
+ ], [
+ new Icon('globe-europe'),
+ $this->translate('Icinga Community'),
+ ]
+ );
+ ?>
+ </div>
+ <div class="col">
+ <?=
+ 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'),
+ ]
+ );
+ ?>
+ </div>
+ <div class="col">
+ <?=
+ 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'),
+ ]
+ );
+ ?>
+ </div>
+ </div>
+
+ <?php $mm = MigrationManager::instance(); if ($mm->hasPendingMigrations()): ?>
+ <div class="pending-migrations clearfix">
+ <h2><?= $this->translate('Pending Migrations') ?></h2>
+ <table class="name-value-table migrations">
+ <?php foreach ($mm->getPendingMigrations() as $migration): ?>
+ <tr>
+ <th><?= $this->escape($migration->getName()) ?></th>
+ <td><?=
+ new StateBadge(
+ count($migration->getMigrations()),
+ BadgeNavigationItemRenderer::STATE_PENDING
+ );
+ ?></td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?= $this->qlink(
+ $this->translate('Show all'),
+ 'migrations',
+ null,
+ ['title' => $this->translate('Show all pending migrations')]
+ ) ?>
+ </div>
+ <?php endif ?>
+
+ <h2><?= $this->translate('Loaded Libraries') ?></h2>
+ <table class="name-value-table" data-base-target="_next">
+ <?php foreach ($libraries as $library): ?>
+ <tr>
+ <th>
+ <?= $this->escape($library->getName()) ?>
+ </th>
+ <td>
+ <?= $this->escape($library->getVersion()) ?: '-' ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <h2><?= $this->translate('Loaded Modules') ?></h2>
+ <table class="name-value-table" data-base-target="_next">
+ <?php foreach ($modules as $module): ?>
+ <tr>
+ <th>
+ <?= $this->escape($module->getName()) ?>
+ </th>
+ <td>
+ <td>
+ <?= $this->escape($module->getVersion()) ?>
+ </td>
+ <td>
+ <?php if ($this->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()))
+ ) ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ </section>
+
+ <footer>
+ <div class="about-copyright">
+ <?= $this->translate('Copyright') ?>
+ <span>&copy; 2013-<?= date('Y') ?></span>
+ <?= $this->qlink(
+ 'Icinga GmbH',
+ 'https://icinga.com',
+ null,
+ array(
+ 'target' => '_blank'
+ )
+ ) ?>
+ </div>
+ <div class="about-social">
+ <?= $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')
+ )
+ ) ?>
+ </div>
+ </footer>
+</div>
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 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+<?php if (isset($changePasswordForm)): ?>
+ <h1><?= $this->translate('Account') ?></h1>
+ <?= $changePasswordForm ?>
+<?php endif ?>
+ <h1><?= $this->translate('Preferences') ?></h1>
+ <?= $form ?>
+</div>
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 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if ($this->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')
+ )
+ );
+} ?>
+<?php if (empty($this->announcements)): ?>
+ <p><?= $this->translate('No announcements found.') ?></p>
+</div>
+<?php return; endif ?>
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Author') ?></th>
+ <th><?= $this->translate('Message') ?></th>
+ <th><?= $this->translate('Start') ?></th>
+ <th><?= $this->translate('End') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($this->announcements as $announcement): /** @var object $announcement */ ?>
+ <tr>
+ <td><?= $this->escape($announcement->author) ?></td>
+ <?php if ($this->hasPermission('application/announcements')): ?>
+ <td>
+ <a href="<?= $this->href('announcements/update', array('id' => $announcement->id)) ?>">
+ <?= $this->ellipsis($this->escape($announcement->message), 100) ?>
+ </a>
+ </td>
+ <?php else: ?>
+ <td><?= $this->ellipsis($this->escape($announcement->message), 100) ?></td>
+ <?php endif ?>
+ <td><?= $this->formatDateTime($announcement->start) ?></td>
+ <td><?= $this->formatDateTime($announcement->end) ?></td>
+ <?php if ($this->hasPermission('application/announcements')): ?>
+ <td class="icon-col"><?= $this->qlink(
+ null,
+ 'announcements/remove',
+ array('id' => $announcement->id),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => $this->translate('Remove this announcement')
+ )
+ ) ?></td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<div id="login">
+ <div class="login-form" data-base-target="layout">
+ <div role="status" class="sr-only">
+ <?= $this->translate(
+ 'Welcome to Icinga Web 2. For users of the screen reader Jaws full and expectant compliant'
+ . ' accessibility is possible only with use of the Firefox browser. VoiceOver on Mac OS X is tested on'
+ . ' Chrome, Safari and Firefox.'
+ ) ?>
+ </div>
+ <div class="logo-wrapper"><div id="icinga-logo" aria-hidden="true"></div></div>
+ <?php if ($requiresSetup): ?>
+ <p class="config-note"><?= sprintf(
+ $this->translate(
+ 'It appears that you did not configure Icinga Web 2 yet so it\'s not possible to log in without any defined '
+ . 'authentication method. Please define a authentication method by following the instructions in the'
+ . ' %1$sdocumentation%3$s or by using our %2$sweb-based setup-wizard%3$s.'
+ ),
+ '<a href="https://icinga.com/docs/icinga-web-2/latest/doc/05-Authentication/#authentication" title="'
+ . $this->translate('Icinga Web 2 Documentation') . '">',
+ '<a href="' . $this->href('setup') . '" title="' . $this->translate('Icinga Web 2 Setup-Wizard') . '">',
+ '</a>'
+ ) ?></p>
+ <?php endif ?>
+ <?= $this->form ?>
+ <div id="login-footer">
+ <p>Icinga Web 2 &copy; 2013-<?= date('Y') ?></p>
+ <?= $this->qlink($this->translate('icinga.com'), 'https://icinga.com') ?>
+ </div>
+ </div>
+ <ul id="social">
+ <li>
+ <?= $this->qlink(
+ null,
+ 'https://twitter.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'twitter',
+ 'title' => $this->translate('Icinga on Twitter')
+ )
+ ) ?>
+ </li>
+ <li>
+ <?= $this->qlink(
+ null,
+ 'https://www.facebook.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'facebook-squared',
+ 'title' => $this->translate('Icinga on Facebook')
+ )
+ ) ?>
+ </li>
+ <li><?= $this->qlink(
+ null,
+ 'https://github.com/Icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'github-circled',
+ 'title' => $this->translate('Icinga on GitHub')
+ )
+ ) ?>
+ </li>
+ </ul>
+</div>
+<div id="orb-analytics" class="orb" ><?= $this->img('img/orb-analytics.png'); ?></div>
+<div id="orb-automation" class="orb"><?= $this->img('img/orb-automation.png'); ?></div>
+<div id="orb-cloud" class="orb"><?= $this->img('img/orb-cloud.png'); ?></div>
+<div id="orb-icinga" class="orb"><?= $this->img('img/orb-icinga.png'); ?></div>
+<div id="orb-infrastructure" class="orb"><?= $this->img('img/orb-infrastructure.png'); ?></div>
+<div id="orb-metrics" class="orb" ><?= $this->img('img/orb-metrics.png'); ?></div>
+<div id="orb-notifactions" class="orb"><?= $this->img('img/orb-notifications.png'); ?></div>
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 @@
+<?php
+
+use Icinga\Util\Csp;
+
+?>
+<!--
+ This view provides a workaround to logout from an external authentication provider, in case external
+ authentication was configured (the default is to handle authentications internally in Icingaweb2).
+
+ The <a href="http://tools.ietf.org/html/rfc2617">Http Basic and Digest Authentication</a> is not
+ designed to handle logout. When the user has provided valid credentials, the client is adviced to include these
+ in every further request until the browser was closed. To allow logout and to allow the user to change the
+ logged-in user this JavaScript provides a workaround to force a new authentication prompt in most browsers.
+-->
+<div class="content">
+ <div id="icinga-logo" aria-hidden="true"></div>
+ <div class="alert alert-warning" id="logout-in-progress">
+ <b><?= $this->translate('Logging out...'); ?></b>
+ <p>
+ <?= $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.'
+ ); ?>
+ </p>
+ </div>
+ <div id="logout-successful" class="alert alert-success" hidden><?= $this->translate('Logout successful'); ?></div>
+
+ <div class="container">
+ <a href="<?= $this->href('dashboard'); ?>"><?= $this->translate('Login'); ?></a>
+ </div>
+</div>
+<script type="text/javascript" src="<?= $this->href('js/logout.js'); ?>"></script>
+<style type="text/css" nonce="<?= Csp::getStyleNonce(); ?>">
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ background-color: #0095bf;
+ color: white;
+ }
+ .content {
+ text-align: center;
+ }
+
+ #icinga-logo {
+ background-image: url('../img/icinga-logo-big.svg');
+ background-position: center bottom;
+ background-repeat: no-repeat;
+ background-size: contain;
+ height: 177px;
+ margin-top: 10em;
+ width: 100%;
+ }
+
+ #logout-in-progress {
+ margin: 2em 0 1em;
+ font-size: 2em;
+ font-weight: bold;
+ }
+
+ .container a {
+ color: white;
+ font-size: 1.5em;
+ }
+</style>
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 @@
+<div class="controls">
+<?= $this->tabs ?>
+</div>
+<table class="avp">
+<tr><th><?= $this->translate('UI Debug') ?></th><td><a href="javascript:void(0);" onclick="icinga.ui.toggleDebug();"><?= $this->translate('toggle') ?></td></tr>
+</table>
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 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form ?>
+</div>
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 @@
+<?php
+ $action = (isset($this->action)) ? $this->action : 'do something with';
+ $moduleName = $this->moduleName;
+ $exceptionMessage = $this->exceptionMessage;
+?>
+<?= $this->tabs->render($this); ?>
+<br/>
+<div>
+ <h1>Could not <?= $action; ?> module "<?= $moduleName; ?>"</h1>
+ <p>
+ While operation the following error occurred:
+ <br />
+ <?= $exceptionMessage; ?>
+ </p>
+</div>
+
+<p>
+ This could have one or more of the following reasons:
+<ul>
+ <li>No file permissions to write into module directory</li>
+ <li>Errors on filesystems: Mount points, operational errors </li>
+ <li>General application error</li>
+</ul>
+</p>
+
+<p>
+ Details can be seen in your application log (if you don't have access to this file, call your administrator in this case).
+</p> \ 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 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?php if (! $module): ?>
+ <?= $this->translate('There is no such module installed.') ?>
+ <?php return; endif ?>
+ <?php
+ $requiredMods = $module->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';
+ ?>
+ <table class="name-value-table">
+ <tr>
+ <th><?= $this->escape($this->translate('Name')) ?></th>
+ <td><?= $this->escape($module->getName()) ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('State') ?></th>
+ <td>
+ <?= $state ?>
+ <?php if (isset($this->toggleForm)): ?>
+ <?php if ($moduleData->enabled || ! $unmetDependencies): ?>
+ <?= $this->toggleForm ?>
+ <?php else: ?>
+ <?= $this->icon('attention-alt', $this->translate('Module can\'t be enabled due to unmet dependencies')) ?>
+ <?php endif ?>
+ <?php endif ?>
+ </td>
+ <tr>
+ <th><?= $this->escape($this->translate('Version')) ?></th>
+ <td><?= $this->escape($module->getVersion()) ?></td>
+ </tr>
+ <?php if (isset($moduleGitCommitId) && $moduleGitCommitId !== false): ?>
+ <tr>
+ <th><?= $this->escape($this->translate('Git commit')) ?></th>
+ <td><?= $this->escape($moduleGitCommitId) ?></td>
+ </tr>
+ <?php endif ?>
+ <tr>
+ <th><?= $this->escape($this->translate('Description')) ?></th>
+ <td>
+ <strong><?= $this->escape($module->getTitle()) ?></strong><br>
+ <?= nl2br($this->escape($module->getDescription())) ?>
+ </td>
+ </tr>
+ <tr>
+ <th><?= $this->escape($this->translate('Dependencies')) ?></th>
+ <td class="module-dependencies">
+ <?php if (empty($requiredLibs) && empty($requiredMods)): ?>
+ <?= $this->translate('This module has no dependencies') ?>
+ <?php else: ?>
+ <?php if ($unmetDependencies): ?>
+ <strong class="unmet-dependencies">
+ <?= $this->translate('Unmet dependencies found! Module can\'t be enabled unless all dependencies are met.') ?>
+ </strong>
+ <?php endif ?>
+ <?php if (! empty($requiredLibs)): ?>
+ <table class="name-value-table">
+ <caption><?= $this->translate('Libraries') ?></caption>
+ <?php foreach ($requiredLibs as $libraryName => $versionString): ?>
+ <tr>
+ <th><?= $this->escape($libraryName) ?></th>
+ <td>
+ <?php if ($libraries->has($libraryName, $versionString === true ? null : $versionString)): ?>
+ <?= $versionString === true ? '*' : $this->escape($versionString) ?>
+ <?php else: ?>
+ <span class="missing"><?= $versionString === true ? '*' : $this->escape($versionString) ?></span>
+ <?php if (($library = $libraries->get($libraryName)) !== null): ?>
+ (<?= $library->getVersion() ?>)
+ <?php endif ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+ <?php if (! empty($requiredMods)): ?>
+ <table class="name-value-table">
+ <caption><?= $this->translate('Modules') ?></caption>
+ <?php foreach ($requiredMods as $moduleName => $versionString): ?>
+ <?php if ($moduleName === 'monitoring' && $isIcingadbSupported && $moduleManager->has('icingadb', $requiredMods['icingadb'])) : ?>
+ <?php continue; ?>
+ <?php endif ?>
+ <tr>
+ <th><?= $this->escape($moduleName) ?></th>
+ <td>
+ <?php if ($moduleManager->has($moduleName, $versionString === true ? null : $versionString)): ?>
+ <?= $versionString === true ? '*' : $this->escape($versionString) ?>
+ <?php else: ?>
+ <span <?= ($moduleName === 'icingadb' && isset($requiredMods['monitoring']) && $moduleManager->has('monitoring', $requiredMods['monitoring'])) ? 'class="optional"' : 'class="missing"' ?>>
+ <?= $versionString === true ? '*' : $this->escape($versionString) ?>
+ </span>
+ <?php if (! $moduleManager->hasInstalled($moduleName)): ?>
+ (<?= $this->translate('not installed') ?>)
+ <?php else: ?>
+ (<?= $moduleManager->getModule($moduleName, false)->getVersion() ?><?= $moduleManager->hasEnabled($moduleName) ? '' : ', ' . $this->translate('disabled') ?>)
+ <?php endif ?>
+ <?php endif ?>
+ </td>
+ <?php if ($moduleName === 'monitoring' && $isIcingadbSupported) : ?>
+ <td class="or-separator"><?= $this->translate('or') ?></td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php if (! empty($permissions)): ?>
+ <tr>
+ <th><?= $this->escape($this->translate('Permissions')) ?></th>
+ <td>
+ <?php foreach ($permissions as $permission): ?>
+ <strong><?= $this->escape($permission->name) ?></strong>: <?= $this->escape($permission->description) ?><br />
+ <?php endforeach ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ <?php if (! empty($restrictions)): ?>
+ <tr>
+ <th><?= $this->escape($this->translate('Restrictions')) ?></th>
+ <td>
+ <?php foreach ($restrictions as $restriction): ?>
+ <strong><?= $this->escape($restriction->name) ?></strong>: <?= $this->escape($restriction->description) ?><br />
+ <?php endforeach ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ </table>
+</div>
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 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+</div>
+<?php endif ?>
+<div class="content">
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Module') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($modules as $module): ?>
+ <tr>
+ <td>
+ <?php if (! $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)
+ )
+ ); ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $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')
+ )
+ ) ?>
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Resource') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+<?php foreach ($this->resources as $name => $value): ?>
+ <tr>
+ <td>
+ <?php
+ switch ($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)
+ )
+ ) ?>
+ </td>
+ <td class="icon-col text-right">
+ <?= $this->qlink(
+ '',
+ 'config/removeresource',
+ array('resource' => $name),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove resource %s'), $name)
+ )
+ ) ?>
+ </td>
+ </tr>
+<?php endforeach ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form ?>
+</div>
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 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form ?>
+</div>
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 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form ?>
+</div>
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 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?php if ($this->auth()->hasPermission('config/access-control/users')): ?>
+ <h1><?= $this->translate('User Backends') ?></h1>
+ <?= $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 ?>
+ <?php endif ?>
+
+ <?php if ($this->auth()->hasPermission('config/access-control/groups')): ?>
+ <h1><?= $this->translate('User Group Backends') ?></h1>
+ <?= $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')
+ )
+ ) ?>
+<?php if (! count($backendNames)) { return; } ?>
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Backend') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+<?php foreach ($backendNames as $backendName => $config):
+ $type = $config->get('backend');
+?>
+ <tr>
+ <td>
+ <?= $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)
+ )
+ ); ?>
+ </td>
+ <td class="icon-col text-right">
+ <?= $this->qlink(
+ null,
+ 'usergroupbackend/remove',
+ array('backend' => $backendName),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove user group backend %s'), $backendName)
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+ <?php endif ?>
+</div>
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 @@
+<div class="content">
+ <h1><?= $this->translate('Could not save dashboard'); ?></h1>
+ <p>
+ <?= $this->translate('Please copy the following dashboard snippet to '); ?>
+ <strong><?= $this->config->getConfigFile(); ?>;</strong>.
+ <br>
+ <?= $this->translate('Make sure that the webserver can write to this file.'); ?>
+ </p>
+ <pre><?= $this->config; ?></pre>
+ <hr>
+ <h2><?= $this->translate('Error details'); ?></h2>
+ <p><?= $this->error->getMessage(); ?></p>
+</div> \ 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 @@
+<div class="controls">
+<?php if (! $this->compact): ?>
+<?= $this->tabs ?>
+<?php endif ?>
+</div>
+<?php if ($this->dashboard): ?>
+ <div class="dashboard content">
+ <?= $this->dashboard ?>
+ </div>
+<?php else: ?>
+ <div class="content">
+ <h1><?= $this->escape($this->translate('Welcome to Icinga Web!')) ?></h1>
+ <p>
+ <?php if (! $this->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')
+ );
+ } ?>
+ </p>
+ </div>
+<?php endif ?>
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 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ 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 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ 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 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ 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 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ 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 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <h1><?= t('Dashboard Settings'); ?></h1>
+
+ <table class="avp action" data-base-target="_next">
+ <thead>
+ <tr>
+ <th>
+ <strong><?= t('Dashlet Name') ?></strong>
+ </th>
+ <th>
+ <strong><?= t('Url') ?></strong>
+ </th>
+ <th class="icon-col">&nbsp;</th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($this->dashboard->getPanes() as $pane): ?>
+ <?php if ($pane->getDisabled()) continue; ?>
+ <tr>
+ <th colspan="2">
+ <?php if ($pane->isUserWidget()): ?>
+ <?= $this->qlink(
+ $pane->getName(),
+ 'dashboard/rename-pane',
+ array('pane' => $pane->getName()),
+ array('title' => sprintf($this->translate('Edit pane %s'), $pane->getName()))
+ ) ?>
+ <?php else: ?>
+ <?= $this->escape($pane->getName()) ?>
+ <?php endif ?>
+ </th>
+ <th>
+ <?= $this->qlink(
+ '',
+ 'dashboard/remove-pane',
+ array('pane' => $pane->getName()),
+ array(
+ 'icon' => 'trash',
+ 'title' => sprintf($this->translate('Remove pane %s'), $pane->getName())
+ )
+ ); ?>
+ </th>
+ </tr>
+ <?php $dashlets = $pane->getDashlets(); ?>
+ <?php if(empty($dashlets)): ?>
+ <tr>
+ <td colspan="3">
+ <?= $this->translate('No dashlets added to dashboard') ?>.
+ </td>
+ </tr>
+ <?php else: ?>
+ <?php foreach ($dashlets as $dashlet): ?>
+ <?php if ($dashlet->getDisabled()) continue; ?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $dashlet->getTitle(),
+ 'dashboard/update-dashlet',
+ array('pane' => $pane->getName(), 'dashlet' => $dashlet->getName()),
+ array('title' => sprintf($this->translate('Edit dashlet %s'), $dashlet->getTitle()))
+ ); ?>
+ </td>
+ <td>
+ <?= $this->qlink(
+ $dashlet->getUrl()->getRelativeUrl(),
+ $dashlet->getUrl()->getRelativeUrl(),
+ null,
+ array('title' => sprintf($this->translate('Show dashlet %s'), $dashlet->getTitle()))
+ ); ?>
+ </td>
+ <td>
+ <?= $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())
+ )
+ ); ?>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ <?php endif; ?>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ 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 @@
+<?php if (! $this->compact && ! $hideControls): ?>
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+if (isset($stackTraces)) {
+ foreach ($messages as $i => $message) {
+ echo '<p tabindex="-1" class="autofocus error-message">' . nl2br($this->escape($message)) . '</p>'
+ . '<hr>'
+ . '<pre>' . $this->escape($stackTraces[$i]) . '</pre>';
+ }
+} else {
+ foreach ($messages as $message) {
+ echo '<p tabindex="-1" class="autofocus error-message">' . nl2br($this->escape($message)) . '</p>';
+ }
+}
+
+$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
+?>
+<?php if (! empty($coreReason)): ?>
+<div class="error-reason">
+<?php endif ?>
+<?php foreach ($coreReason as $msg): ?>
+ <p><?= $msg ?></p>
+<?php endforeach ?>
+<?php if (! empty($coreReason)): ?>
+</div>
+<?php endif ?>
+
+<?php if (! empty($modReason)): ?>
+<div class="error-reason">
+<?php endif ?>
+<?php foreach ($modReason as $msg): ?>
+ <p><?= $msg ?></p>
+<?php endforeach ?>
+<?php if (! empty($modReason)): ?>
+</div>
+<?php endif ?>
+</div>
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 @@
+<?php
+
+echo $this->form;
+
+if ($this->tree) {
+ echo $this->tree->render($this);
+ echo '<br/><pre><code>';
+ echo $this->sqlString;
+ echo '</pre></code>';
+ 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 @@
+<form id="<?=
+$this->escape($form->getId())
+?>" name="<?=
+$this->escape($form->getName())
+?>" enctype="<?=
+$this->escape($form->getEncType())
+?>" method="<?=
+$this->escape($form->getMethod())
+?>" action="<?=
+$this->escape($form->getAction())
+?>">
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <th><?= $this->translate('Backend') ?></th>
+ <th></th>
+ <th></th>
+ </thead>
+ <tbody>
+<?php
+ $backendNames = $form->getBackendOrder();
+ $backendConfigs = $form->getConfig();
+ for ($i = 0; $i < count($backendNames); $i++):
+ $type = $backendConfigs->getSection($backendNames[$i])->get('backend');
+?>
+ <tr>
+ <td class="action">
+ <?= $this->qlink(
+ $backendNames[$i],
+ 'config/edituserbackend',
+ array('backend' => $backendNames[$i]),
+ array(
+ 'icon' => $type === 'external' ?
+ 'magic' : ($type === 'ldap' || $type === 'msldap' ? 'sitemap' : 'database'),
+ 'class' => 'rowaction',
+ 'title' => sprintf($this->translate('Edit user backend %s'), $backendNames[$i])
+ )
+ ) ?>
+ </td>
+ <td class="icon-col text-right">
+ <?= $this->qlink(
+ '',
+ 'config/removeuserbackend',
+ array('backend' => $backendNames[$i]),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove user backend %s'), $backendNames[$i])
+ )
+ ) ?>
+ </td>
+ <td class="icon-col text-right" data-base-target="_self">
+<?php if ($i > 0): ?>
+ <button type="submit" name="backend_newpos" class="link-button icon-only animated move-up" value="<?= $this->escape(
+ $backendNames[$i] . '|' . ($i - 1)
+ ) ?>" title="<?= $this->translate(
+ 'Move up in authentication order'
+ ) ?>" aria-label="<?= $this->escape(sprintf(
+ $this->translate('Move user backend %s upwards'),
+ $backendNames[$i]
+ )) ?>">
+ <?= $this->icon('up-small') ?>
+ </button>
+<?php endif ?>
+<?php if ($i + 1 < count($backendNames)): ?>
+ <button type="submit" name="backend_newpos" class="link-button icon-only animated move-down" value="<?= $this->escape(
+ $backendNames[$i] . '|' . ($i + 1)
+ ) ?>" title="<?= $this->translate(
+ 'Move down in authentication order'
+ ) ?>" aria-label="<?= $this->escape(sprintf(
+ $this->translate('Move user backend %s downwards'),
+ $backendNames[$i]
+ )) ?>">
+ <?= $this->icon('down-small') ?>
+ </button>
+<?php endif ?>
+ </td>
+ </tr>
+<?php endfor ?>
+ </tbody>
+ </table>
+ <?= $form->getElement($form->getTokenElementName()) ?>
+ <?= $form->getElement($form->getUidElementName()) ?>
+</form>
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 @@
+<div class="controls">
+ <?= $tabs->showOnlyCloseButton(); ?>
+</div>
+<div class="content">
+ <?= $form; ?>
+</div> \ 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 @@
+<?php
+
+use Icinga\Data\Extensible;
+use Icinga\Data\Reducible;
+
+if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->backendSelection ?>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+
+if (! isset($backend)) {
+ echo $this->translate('No backend found which is able to list user groups') . '</div>';
+ return;
+} else {
+ $extensible = $this->hasPermission('config/access-control/groups') && $backend instanceof Extensible;
+ $reducible = $this->hasPermission('config/access-control/groups') && $backend instanceof Reducible;
+}
+?>
+
+<?php if ($extensible): ?>
+ <?= $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'
+ )
+ ) ?>
+<?php endif ?>
+
+<?php if (! $groups->hasResult()): ?>
+ <p><?= $this->translate('No user groups found matching the filter'); ?></p>
+</div>
+<?php return; endif ?>
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('User Group'); ?></th>
+ <?php if ($reducible): ?>
+ <th><?= $this->translate('Remove'); ?></th>
+ <?php endif ?>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($groups as $group): ?>
+ <tr>
+ <td>
+ <?= $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
+ )
+ )
+ ); ?>
+ </td>
+ <?php if ($reducible): ?>
+ <td class="icon-col">
+ <?= $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'
+ )
+ ); ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<?php
+
+use Icinga\Data\Extensible;
+use Icinga\Data\Updatable;
+
+$extensible = $this->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'
+ )
+ );
+}
+
+?>
+<div class="controls separated">
+<?php if (! $this->compact): ?>
+ <?= $tabs; ?>
+<?php endif ?>
+ <h2 class="clearfix"><?= $this->escape($group->group_name) ?><span class="pull-right"><?= $editLink ?></span></h2>
+ <table class="name-value-table">
+ <tr>
+ <th><?= $this->translate('Created at'); ?></th>
+ <td><?= $group->created_at === null ? '-' : $this->formatDateTime($group->created_at); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Last modified'); ?></th>
+ <td><?= $group->last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?></td>
+ </tr>
+ </table>
+<?php if (! $this->compact): ?>
+ <h2><?= $this->translate('Members'); ?></h2>
+ <div class="sort-controls-container">
+ <?= $this->limiter; ?>
+ <?= $this->paginator; ?>
+ <?= $this->sortBox; ?>
+ </div>
+ <?= $this->filterEditor; ?>
+<?php endif ?>
+</div>
+<div class="content">
+<?php if ($extensible): ?>
+ <?= $this->qlink(
+ $this->translate('Add New Member'),
+ 'group/addmember',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'icon' => 'plus',
+ 'class' => 'button-link'
+ )
+ ) ?>
+<?php endif ?>
+
+<?php if (! $members->hasResult()): ?>
+ <p><?= $this->translate('No group member found matching the filter'); ?></p>
+</div>
+<?php return; endif ?>
+
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Username'); ?></th>
+ <?php if (isset($removeForm)): ?>
+ <th><?= $this->translate('Remove'); ?></th>
+ <?php endif ?>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($members as $member): ?>
+ <tr>
+ <td>
+ <?php if (
+ $this->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)
+ )); ?>
+ <?php else: ?>
+ <?= $this->escape($member->user_name); ?>
+ <?php endif ?>
+ </td>
+ <?php if (isset($removeForm)): ?>
+ <td class="icon-col" data-base-target="_self">
+ <?php $removeForm->getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<?php if (! $compact): ?>
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<?php endif ?>
+<div class="iframe-container">
+ <iframe src="<?= $this->escape($url) ?>" frameborder="no"></iframe>
+</div>
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 @@
+<h1>Welcome to Icinga!</h1>
+You should install/configure some <a href="<?= $this->href('config/modules');?>">modules</a> 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 @@
+<?php
+
+use Icinga\Web\Url;
+
+$showText = $this->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;
+
+?>
+
+<table class="joystick-pagination">
+ <tbody>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <?php if ($prevYAxisPage): ?>
+ <?= $this->qlink(
+ '',
+ Url::fromRequest(),
+ array(
+ 'page' => $currentXAxisPage . ',' . $prevYAxisPage
+ ),
+ array(
+ 'icon' => 'up-open',
+ 'data-base-target' => '_self',
+ 'title' => sprintf(
+ $showText,
+ $this->translate('Y-Axis', 'pagination.joystick'),
+ $this->translate('hosts', 'pagination.joystick'),
+ ($prevYAxisPage - 1) * $yAxisPages->itemCountPerPage + 1,
+ $prevYAxisPage * $yAxisPages->itemCountPerPage,
+ $yAxisPages->totalItemCount
+ )
+ )
+ ); ?>
+ <?php else: ?>
+ <?= $this->icon('up-open'); ?>
+ <?php endif ?>
+ </td>
+ <td>&nbsp;</td>
+ </tr>
+ <tr>
+ <td>
+ <?php if ($prevXAxisPage): ?>
+ <?= $this->qlink(
+ '',
+ Url::fromRequest(),
+ array(
+ 'page' => $prevXAxisPage . ',' . $currentYAxisPage
+ ),
+ array(
+ 'icon' => 'left-open',
+ 'data-base-target' => '_self',
+ 'title' => sprintf(
+ $showText,
+ $this->translate('X-Axis', 'pagination.joystick'),
+ $this->translate('services', 'pagination.joystick'),
+ ($prevXAxisPage - 1) * $xAxisPages->itemCountPerPage + 1,
+ $prevXAxisPage * $xAxisPages->itemCountPerPage,
+ $xAxisPages->totalItemCount
+ )
+ )
+ ); ?>
+ <?php else: ?>
+ <?= $this->icon('left-open'); ?>
+ <?php endif ?>
+ </td>
+ <?php if ($this->flippable): ?>
+ <td><?= $this->qlink(
+ '',
+ $flipUrl,
+ null,
+ array(
+ 'icon' => 'arrows-cw',
+ 'data-base-target' => '_self',
+ 'title' => $this->translate('Flip grid axes')
+ )
+ ) ?></td>
+ <?php else: ?>
+ <td>&nbsp;</td>
+ <?php endif ?>
+ <td>
+ <?php if ($nextXAxisPage): ?>
+ <?= $this->qlink(
+ '',
+ Url::fromRequest(),
+ array(
+ 'page' => $nextXAxisPage . ',' . $currentYAxisPage
+ ),
+ array(
+ 'icon' => 'right-open',
+ 'data-base-target' => '_self',
+ 'title' => sprintf(
+ $showText,
+ $this->translate('X-Axis', 'pagination.joystick'),
+ $this->translate('services', 'pagination.joystick'),
+ $currentXAxisPage * $xAxisPages->itemCountPerPage + 1,
+ $nextXAxisPage === $xAxisPages->last ? $xAxisPages->totalItemCount : $nextXAxisPage * $xAxisPages->itemCountPerPage,
+ $xAxisPages->totalItemCount
+ )
+ ),
+ false
+ ); ?>
+ <?php else: ?>
+ <?= $this->icon('right-open'); ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <?php if ($nextYAxisPage): ?>
+ <?= $this->qlink(
+ '',
+ Url::fromRequest(),
+ array(
+ 'page' => $currentXAxisPage . ',' . $nextYAxisPage
+ ),
+ array(
+ 'icon' => 'down-open',
+ 'data-base-target' => '_self',
+ 'title' => sprintf(
+ $showText,
+ $this->translate('Y-Axis', 'pagination.joystick'),
+ $this->translate('hosts', 'pagination.joystick'),
+ $currentYAxisPage * $yAxisPages->itemCountPerPage + 1,
+ $nextYAxisPage === $yAxisPages->last ? $yAxisPages->totalItemCount : $nextYAxisPage * $yAxisPages->itemCountPerPage,
+ $yAxisPages->totalItemCount
+ )
+ )
+ ); ?>
+ <?php else: ?>
+ <?= $this->icon('down-open'); ?>
+ <?php endif ?>
+ </td>
+ <td>&nbsp;</td>
+ </tr>
+ </tbody>
+</table>
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 @@
+<?php
+
+use Icinga\Web\Navigation\ConfigMenu;
+use Icinga\Web\Widget\SearchDashboard;
+
+$searchDashboard = new SearchDashboard();
+$searchDashboard->setUser($this->Auth()->getUser());
+
+if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?>
+ <form action="<?= $this->href('search') ?>" method="get" role="search" class="search-control">
+ <input type="text" name="q" id="search" class="search search-input" required
+ placeholder="<?= $this->translate('Search') ?> &hellip;"
+ autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
+ <button class="search-reset icon-cancel" type="reset"></button>
+ </form>
+<?php endif; ?>
+<?= $menuRenderer->setCssClass('primary-nav')->setElementTag('nav')->setHeading(t('Navigation')); ?>
+<nav class="config-menu">
+ <?= new ConfigMenu() ?>
+</nav>
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 @@
+<?php if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ </div>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if ($this->logData !== null): ?>
+ <table class="action">
+ <tbody>
+ <?php foreach ($this->logData as $value): ?>
+ <?php $datetime = new Datetime($value->datetime) ?>
+ <tr>
+ <td>
+ <?= $this->escape($datetime->format('d.m. H:i')) ?><br />
+ <?= $this->escape($value->loglevel) ?>
+ </td>
+ <td>
+ <?= nl2br($this->escape($value->message), false) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php endif ?>
+</div>
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 @@
+<?php if ($this->pageCount <= 1) return; ?>
+<div class="pagination-control" role="navigation">
+ <h2 id="<?= $this->protectId('pagination') ?>" class="sr-only" tabindex="-1"><?= $this->translate('Pagination') ?></h2>
+ <ul class="nav tab-nav">
+ <?php if (isset($this->previous)): ?>
+ <?php $label = sprintf(
+ $this->translate('Show rows %u to %u of %u'),
+ ($this->current - 2) * $this->itemCountPerPage + 1,
+ ($this->current - 1) * $this->itemCountPerPage,
+ $this->totalItemCount
+ ) ?>
+ <li class="nav-item">
+ <a href="<?= $this->escape($this->url()->overwriteParams(array('page' => $this->previous))->getAbsoluteUrl()) ?>"
+ title="<?= $label ?>"
+ aria-label="<?= $label ?>"
+ class="previous-page">
+ <?= $this->icon('angle-double-left') ?>
+ </a>
+ </li>
+ <?php else: ?>
+ <li class="nav-item disabled" aria-hidden="true">
+ <span class="previous-page">
+ <span class="sr-only"><?= $this->translate('Previous page') ?></span>
+ <?= $this->icon('angle-double-left') ?>
+ </span>
+ </li>
+ <?php endif ?>
+ <?php foreach ($this->pagesInRange as $page): ?>
+ <?php if ($page === '...'): ?>
+ <li class="nav-item disabled">
+ <span>...</span>
+ </li>
+ <?php else: ?>
+ <?php
+ $end = $page * $this->itemCountPerPage;
+ if ($end > $this->totalItemCount) {
+ $end = $this->totalItemCount;
+ }
+ $label = sprintf(
+ $this->translate('Show rows %u to %u of %u'),
+ ($page - 1) * $this->itemCountPerPage + 1,
+ $end,
+ $this->totalItemCount
+ );
+ ?>
+ <li<?= $page === $this->current ? ' class="active nav-item"' : ' class="nav-item"' ?>>
+ <a href="<?= $this->escape($this->url()->overwriteParams(array('page' => $page))->getAbsoluteUrl()) ?>"
+ title="<?= $label ?>"
+ aria-label="<?= $label ?>">
+ <?= $page ?>
+ </a>
+ </li>
+ <?php endif ?>
+ <?php endforeach ?>
+ <?php if (isset($this->next)): ?>
+ <?php $label = sprintf(
+ $this->translate('Show rows %u to %u of %u'),
+ $this->current * $this->itemCountPerPage + 1,
+ ($this->current + 1) * $this->itemCountPerPage,
+ $this->totalItemCount
+ ) ?>
+ <li class="nav-item">
+ <a href="<?= $this->escape($this->url()->overwriteParams(array('page' => $this->next))->getAbsoluteUrl()) ?>"
+ title="<?= $label ?>"
+ aria-label="<?= $label ?>"
+ class="next-page">
+ <?= $this->icon('angle-double-right') ?>
+ </a>
+ </li>
+ <?php else: ?>
+ <li class="disabled nav-item" aria-hidden="true">
+ <span class="next-page">
+ <span class="sr-only"><?= $this->translate('Next page') ?></span>
+ <?= $this->icon('angle-double-right') ?>
+ </span>
+ </li>
+ <?php endif ?>
+ </ul>
+</div>
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 @@
+<?php
+
+use ipl\Web\Widget\Icon;
+
+?>
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?php foreach ($navigation as $item): /** @var \Icinga\Web\Navigation\NavigationItem $item */?>
+ <a class="dashboard-link" href="<?= $this->url($item->getUrl(), $item->getUrlParameters()) ?>"<?= $this->propertiesToString($item->getAttributes()) ?>>
+ <div class="link-icon">
+ <?php
+ if (substr($item->getUrl()->getPath(), 0, 9) === 'icingadb/') {
+ echo new Icon($item->getIcon(), [ 'aria-hidden' => 1]);
+ } else {
+ echo $this->icon($item->getIcon() ?: 'forward', null, array('aria-hidden' => true));
+ }
+ ?>
+ </div>
+ <div class="link-meta">
+ <div class="link-label"><?= $this->escape($item->getLabel()) ?></div>
+ <div class="link-description"><?= $this->escape($item->getDescription() ?: sprintf('Open %s', strtolower($item->getLabel()))) ?></div>
+ </div>
+ </a>
+ <?php endforeach ?>
+</div>
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 @@
+<?php if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+ <div class="grid">
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+ <?= $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')
+ )
+ ) ?>
+<?php if (count($items) === 0): ?>
+ <p><?= $this->translate('You did not create any navigation item yet.') ?></p>
+</div>
+<?php return; endif ?>
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Navigation') ?></th>
+ <th><?= $this->translate('Type') ?></th>
+ <th><?= $this->translate('Shared') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+<?php foreach ($items as $item): ?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $item->name,
+ 'navigation/edit',
+ array(
+ 'name' => $item->name,
+ 'type' => $item->type
+ ),
+ array(
+ 'title' => sprintf($this->translate('Edit navigation item %s'), $item->name)
+ )
+ ) ?>
+ </td>
+ <td>
+ <?= $item->type && isset($types[$item->type])
+ ? $this->escape($types[$item->type])
+ : $this->escape($this->translate('Unknown')) ?>
+ </td>
+ <td class="icon-col">
+ <?= $item->owner ? $this->translate('Yes') : $this->translate('No') ?>
+ </td>
+ <td class="icon-col text-right">
+ <?= $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)
+ )
+ ) ?>
+ </td>
+ </tr>
+<?php endforeach ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<?php
+
+use Icinga\Web\Url;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs; ?>
+ <div class="grid">
+ <?= $this->sortBox ?>
+ </div>
+</div>
+<?php endif ?>
+<div class="content" data-base-target="_next">
+<?php if (count($items) === 0): ?>
+ <p><?= $this->translate('There are currently no navigation items being shared'); ?></p>
+<?php else: ?>
+ <table class="table-row-selectable common-table">
+ <thead>
+ <th><?= $this->translate('Shared Navigation'); ?></th>
+ <th><?= $this->translate('Type'); ?></th>
+ <th><?= $this->translate('Owner'); ?></th>
+ <th><?= $this->translate('Unshare'); ?></th>
+ </thead>
+ <tbody>
+ <?php foreach ($items as $item): ?>
+ <tr>
+ <td><?= $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)
+ )
+ ); ?></td>
+ <td><?= $item->type && isset($types[$item->type])
+ ? $this->escape($types[$item->type])
+ : $this->escape($this->translate('Unknown')); ?></td>
+ <td><?= $this->escape($item->owner); ?></td>
+ <?php if ($item->parent): ?>
+ <td><?= $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
+ )
+ ); ?></td>
+ <?php else: ?>
+ <td data-base-target="_self" class="remove-nav-item"><?= $removeForm
+ ->setDefault('name', $item->name)
+ ->setAction(Url::fromPath(
+ 'navigation/unshare',
+ array('type' => $item->type, 'owner' => $item->owner)
+ )); ?></td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php endif ?>
+</div>
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 @@
+<?php
+
+use Icinga\Web\Url;
+
+if ($xAxisPaginator->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');
+
+?>
+
+<div class="pivot-pagination">
+ <span><?= t('Navigation'); ?></span>
+ <table>
+ <tbody>
+<?php foreach ($yAxisPages->pagesInRange as $yAxisPage): ?>
+ <tr>
+<?php foreach ($xAxisPages->pagesInRange as $xAxisPage): ?>
+ <td<?= $xAxisPage === $xAxisPages->current && $yAxisPage === $yAxisPages->current ? ' class="active"' : ''; ?>>
+<?php if ($xAxisPage !== $xAxisPages->current || $yAxisPage !== $yAxisPages->current): ?>
+ <a href="<?= Url::fromRequest()->overwriteParams(
+ array('page' => $xAxisPage . ',' . $yAxisPage)
+ )->getAbsoluteUrl(); ?>" title="<?= sprintf(
+ $fromTo,
+ t('Hosts'),
+ ($yAxisPage - 1) * $yAxisPages->itemCountPerPage + 1,
+ $yAxisPage === $yAxisPages->last ? $yAxisPages->totalItemCount : $yAxisPage * $yAxisPages->itemCountPerPage,
+ $yAxisPages->totalItemCount,
+ 'y'
+ ) . '; ' . sprintf(
+ $fromTo,
+ t('Services'),
+ ($xAxisPage - 1) * $xAxisPages->itemCountPerPage + 1,
+ $xAxisPage === $xAxisPages->last ? $xAxisPages->totalItemCount : $xAxisPage * $xAxisPages->itemCountPerPage,
+ $xAxisPages->totalItemCount,
+ 'x'
+ ); ?>"></a>
+<?php endif ?>
+ </td>
+<?php endforeach ?>
+ </tr>
+<?php endforeach ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<div class="controls separated">
+ <?= $tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<div class="content">
+ <?= $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')
+ )
+ ) ?>
+<?php /** @var \Icinga\Application\Config $roles */ if (! $roles->hasResult()): ?>
+ <p><?= $this->translate('No roles found.') ?></p>
+<?php return; endif ?>
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Name') ?></th>
+ <th><?= $this->translate('Users') ?></th>
+ <th><?= $this->translate('Groups') ?></th>
+ <th><?= $this->translate('Inherits From') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+<?php foreach ($roles as $name => $role): /** @var object $role */ ?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $name,
+ 'role/edit',
+ array('role' => $name),
+ array('title' => sprintf($this->translate('Edit role %s'), $name))
+ ) ?>
+ </td>
+ <td><?= $this->escape($role->users) ?></td>
+ <td><?= $this->escape($role->groups) ?></td>
+ <td><?= $this->escape($role->parent) ?></td>
+ <td class="icon-col text-right">
+ <?= $this->qlink(
+ '',
+ 'role/remove',
+ array('role' => $name),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove role %s'), $name)
+ )
+ ) ?>
+ </td>
+ </tr>
+<?php endforeach ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<div class="content">
+<h1><?= $this->translate("I'm ready to search, waiting for your input") ?></h1>
+<p><strong><?= $this->translate('Hint') ?>: </strong><?= $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.'
+) ?></p>
+</div>
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 @@
+<div class="controls">
+<?= $this->dashboard->getTabs() ?>
+</div>
+
+<div class="content dashboard">
+<?= $this->dashboard ?>
+</div>
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 @@
+<div>
+ <h4><?= $this->translate('Saving Configuration Failed'); ?></h4>
+ <p>
+ <?= sprintf(
+ $this->translate('The file %s couldn\'t be stored. (Error: "%s")'),
+ $this->escape($filePath),
+ $this->escape($errorMessage)
+ ); ?>
+ <br>
+ <?= $this->translate('This could have one or more of the following reasons:'); ?>
+ </p>
+ <ul>
+ <li><?= $this->translate('You don\'t have file-system permissions to write to the file'); ?></li>
+ <li><?= $this->translate('Something went wrong while writing the file'); ?></li>
+ <li><?= $this->translate('There\'s an application error preventing you from persisting the configuration'); ?></li>
+ </ul>
+</div>
+<p>
+ <?= $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)'); ?>
+ <br>
+ <?= $this->translate('In case you can access the file by yourself, you can open it and insert the config manually:'); ?>
+</p>
+<p>
+ <pre>
+ <code><?= $this->escape($configString); ?></code>
+ </pre>
+</p>
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 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form->create()->setTitle(null) // @TODO(el): create() has to be called because the UserForm is setting the title there ?>
+</div>
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 @@
+<div class="controls">
+ <?= $tabs->showOnlyCloseButton(); ?>
+</div>
+<div class="content">
+ <?= $form; ?>
+</div> \ 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 @@
+<?php
+
+use Icinga\Data\Extensible;
+use Icinga\Data\Reducible;
+
+if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->backendSelection ?>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+
+if (! isset($backend)) {
+ echo $this->translate('No backend found which is able to list users') . '</div>';
+ return;
+} else {
+ $extensible = $this->hasPermission('config/access-control/users') && $backend instanceof Extensible;
+ $reducible = $this->hasPermission('config/access-control/users') && $backend instanceof Reducible;
+}
+?>
+
+<?php if ($extensible): ?>
+ <?= $this->qlink(
+ $this->translate('Add a New User') ,
+ 'user/add',
+ array('backend' => $backend->getName()),
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus'
+ )
+ ) ?>
+<?php endif ?>
+
+<?php if (! $users->hasResult()): ?>
+ <p><?= $this->translate('No users found matching the filter') ?></p>
+</div>
+<?php return; endif ?>
+
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Username') ?></th>
+ <?php if ($reducible): ?>
+ <th><?= $this->translate('Remove') ?></th>
+ <?php endif ?>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($users as $user): ?>
+ <tr>
+ <td><?= $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)
+ )
+ ) ?></td>
+ <?php if ($reducible): ?>
+ <td class="icon-col"><?= $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)
+ )
+ ) ?></td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
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 @@
+<?php
+
+use Icinga\Data\Updatable;
+use Icinga\Data\Reducible;
+use Icinga\Data\Selectable;
+
+?>
+<div class="controls separated">
+<?php if (! $this->compact): ?>
+ <?= $tabs; ?>
+<?php endif ?>
+ <h2><?= $this->escape($user->user_name) ?></h2>
+ <?php
+ if ($this->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)
+ )
+ );
+ }
+ ?>
+ <table class="name-value-table">
+ <tr>
+ <th><?= $this->translate('State'); ?></th>
+ <td><?= $user->is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Created at'); ?></th>
+ <td><?= $user->created_at === null ? '-' : $this->formatDateTime($user->created_at); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Last modified'); ?></th>
+ <td><?= $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Role Memberships'); ?></th>
+ <td>
+ <?php $roles = $userObj->getRoles(); ?>
+ <?php if (! empty($roles)): ?>
+ <ul class="role-memberships">
+ <?php foreach($roles as $role): ?>
+ <li>
+ <?php if ($this->allowedToEditRoles): ?>
+ <?= $this->qlink(
+ $role->getName(),
+ 'role/edit',
+ ['role' => $role->getName()],
+ ['title' => sprintf($this->translate('Edit role %s'), $role->getName())]
+ );
+ $role === end($roles) ? print '' : print ', '; ?>
+ <?php else: ?>
+ <?= $role->getName() ?>
+ <?php endif ?>
+ </li>
+ <?php endforeach ?>
+ </ul>
+ <?php else: ?>
+ <p><?= $this->translate('No memberships found'); ?></p>
+ <?php endif ?>
+ </td>
+ </tr>
+ </table>
+<?php if (! $this->compact): ?>
+ <h2><?= $this->translate('Group Memberships'); ?></h2>
+ <div class="sort-controls-container">
+ <?= $this->limiter; ?>
+ <?= $this->paginator; ?>
+ <?= $this->sortBox; ?>
+ </div>
+ <?= $this->filterEditor; ?>
+<?php endif ?>
+</div>
+<div class="content">
+<?php if ($showCreateMembershipLink): ?>
+ <?= $this->qlink(
+ $this->translate('Create New Membership'),
+ 'user/createmembership',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'icon' => 'plus',
+ 'class' => 'button-link'
+ )
+ ) ?>
+<?php endif ?>
+
+<?php if (! $memberships->hasResult()): ?>
+ <p><?= $this->translate('No memberships found matching the filter'); ?></p>
+</div>
+<?php return; endif ?>
+
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Group'); ?></th>
+ <th><?= $this->translate('Cancel', 'group.membership'); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($memberships as $membership): ?>
+ <tr>
+ <td>
+ <?php if ($this->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)
+ )); ?>
+ <?php else: ?>
+ <?= $this->escape($membership->group_name); ?>
+ <?php endif ?>
+ </td>
+ <td class="icon-col" data-base-target="_self">
+ <?php if (isset($removeForm) && $membership->backend instanceof Reducible): ?>
+ <?= $removeForm->setAction($this->url('group/removemember', array(
+ 'backend' => $membership->backend->getName(),
+ 'group' => $membership->group_name
+ ))); ?>
+ <?php else: ?>
+ -
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>