diff options
Diffstat (limited to 'application')
148 files changed, 17976 insertions, 0 deletions
diff --git a/application/VERSION b/application/VERSION new file mode 100644 index 0000000..cebeac5 --- /dev/null +++ b/application/VERSION @@ -0,0 +1 @@ +11453bfa92a70a44efbf7f966f5e7f27e9300a28 2023-01-26 12:54:15 +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..13c62dd --- /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..4254433 --- /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..30b92ec --- /dev/null +++ b/application/controllers/ConfigController.php @@ -0,0 +1,507 @@ +<?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->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..580015b --- /dev/null +++ b/application/controllers/ErrorController.php @@ -0,0 +1,158 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +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: + $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..dac1789 --- /dev/null +++ b/application/controllers/IndexController.php @@ -0,0 +1,31 @@ +<?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') { + // @TODO(el): Avoid landing page redirects: https://dev.icinga.com/issues/9656 + $this->redirectNow(Url::fromRequest()->setPath('dashboard')); + } + } + + /** + * 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/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(' '), + Html::tag('span', ['class' => 'badge'], $backend->getName()) + ], + $users + ]; + } + + $limit -= count($names); + } + + $groupBackends = $this->loadUserGroupBackends(Selectable::class); + + while ($limit > 0 && ! empty($groupBackends)) { + /** @var Repository $backend */ + $backend = array_shift($groupBackends); + $query = $backend->select() + ->from('group', ['group_name']) + ->where('group_name', $searchTerm) + ->limit($limit); + + try { + $names = $query->fetchColumn(); + } catch (Exception $e) { + continue; + } + + $groups = []; + foreach ($names as $name) { + $groups[] = [$name, ['type' => 'group']]; + } + + if (! empty($groups)) { + $suggestions[] = [ + [ + t('Groups'), + HtmlString::create(' '), + Html::tag('span', ['class' => 'badge'], $backend->getName()) + ], + $groups + ]; + } + + $limit -= count($names); + } + + if (empty($suggestions)) { + $suggestions[] = [t('Your search does not match any user or group'), []]; + } + + $this->document->add(SingleValueSearchControl::createSuggestions($suggestions)); + } + + /** + * Create the tabs to display when listing roles + */ + protected function createListTabs() + { + $tabs = $this->getTabs(); + $tabs->add( + 'role/list', + array( + 'baseTarget' => '_main', + 'label' => $this->translate('Roles'), + 'title' => $this->translate( + 'Configure roles to permit or restrict users and groups accessing Icinga Web 2' + ), + 'url' => 'role/list' + ) + ); + + $tabs->add( + 'role/audit', + [ + 'title' => $this->translate('Audit a user\'s or group\'s privileges'), + 'label' => $this->translate('Audit'), + 'url' => 'role/audit', + 'baseTarget' => '_main' + ] + ); + + if ($this->hasPermission('config/access-control/users')) { + $tabs->add( + 'user/list', + array( + 'title' => $this->translate('List users of authentication backends'), + 'label' => $this->translate('Users'), + 'url' => 'user/list' + ) + ); + } + + if ($this->hasPermission('config/access-control/groups')) { + $tabs->add( + 'group/list', + array( + 'title' => $this->translate('List groups of user group backends'), + 'label' => $this->translate('User Groups'), + 'url' => 'group/list' + ) + ); + } + + return $tabs; + } +} diff --git a/application/controllers/SearchController.php b/application/controllers/SearchController.php new file mode 100644 index 0000000..92aeabe --- /dev/null +++ b/application/controllers/SearchController.php @@ -0,0 +1,28 @@ +<?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..6e43476 --- /dev/null +++ b/application/controllers/StaticController.php @@ -0,0 +1,114 @@ +<?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(); + } + + public function gravatarAction() + { + $response = $this->getResponse(); + $response->setHeader('Cache-Control', 'public, max-age=1814400, stale-while-revalidate=604800', true); + $response->setHeader('Content-Type', 'image/png', true); + + $noCache = $this->getRequest()->getHeader('Cache-Control') === 'no-cache' + || $this->getRequest()->getHeader('Pragma') === 'no-cache'; + + $cache = FileCache::instance(); + $filename = md5(strtolower(trim($this->getParam('email')))); + $cacheFile = 'gravatar-' . $filename; + + if (! $noCache && $cache->has($cacheFile, time() - 1814400)) { + if ($cache->etagMatchesCachedFile($cacheFile)) { + $response->setHttpResponseCode(304); + return; + } + + $response->setHeader('Content-Type', 'image/jpg', true); + $response->setHeader('ETag', sprintf('"%s"', $cache->etagForCachedFile($cacheFile))); + $cache->send($cacheFile); + return; + } + + $img = @file_get_contents('http://www.gravatar.com/avatar/' . $filename . '?s=120&d=mm'); + if ($img === false) { + $this->httpNotFound('Unable to connect to gravatar.com'); + } + + $cache->store($cacheFile, $img); + $response->setHeader('ETag', sprintf('"%s"', $cache->etagForCachedFile($cacheFile))); + + echo $img; + } + + /** + * 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($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 = ' '); } +.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-ok { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-folder-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-git { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-cubes { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-sitemap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-sort-name-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-sort-name-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-megaphone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-filter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-book { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-scissors { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-barchart { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-full-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-full { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-horizontal { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-vertical { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-block { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-zoom-out { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lightbulb { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-clock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-volume-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-volume-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-mute { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-mic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-endtime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-starttime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-calendar-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-sliders { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-services { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-service { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-phone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-file-pdf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-file-word { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-file-excel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-attention-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-print { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reply-all { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-tag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-tags { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-home { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-help { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flapping { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-rewind { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bell-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bell-off-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-plug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-arrows-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-host { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-thumbs-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-thumbs-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-wifi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-moon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-pie { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-area { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-beaker { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-magic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-spin6 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-double-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-double-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-info-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-facebook-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-gplus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reschedule { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-warning-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-double-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-double-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ 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 = ' '); } +.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-ok { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-folder-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-git { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-cubes { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-sitemap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-sort-name-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-sort-name-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-megaphone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-filter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-book { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-scissors { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-barchart { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-full-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-full { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-horizontal { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-vertical { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-block { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-zoom-out { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lightbulb { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-clock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-volume-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-volume-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-mute { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-mic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-endtime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-starttime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-calendar-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-sliders { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-services { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-service { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-phone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-file-pdf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-file-word { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-file-excel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-attention-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-print { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reply-all { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-tag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-tags { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-home { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-help { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flapping { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-rewind { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bell-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bell-off-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-plug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-arrows-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-host { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-thumbs-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-thumbs-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-wifi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-moon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-pie { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-area { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-beaker { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-magic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-spin6 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-double-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-double-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-info-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-facebook-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-gplus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reschedule { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-warning-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-double-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-double-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-angle-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ 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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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 Binary files differnew file mode 100644 index 0000000..091db2f --- /dev/null +++ b/application/fonts/fontello-ifont/font/ifont.eot 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="" 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="" 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="" 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="" d="M352-10l-334 333 158 160 176-174 400 401 159-160z" horiz-adv-x="928" /> + +<glyph glyph-name="cancel" unicode="" 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="" 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="" d="M18 239l0 223 893 0 0-223-893 0z" horiz-adv-x="928" /> + +<glyph glyph-name="folder-empty" unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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 Binary files differnew file mode 100644 index 0000000..2853b70 --- /dev/null +++ b/application/fonts/fontello-ifont/font/ifont.ttf diff --git a/application/fonts/fontello-ifont/font/ifont.woff b/application/fonts/fontello-ifont/font/ifont.woff Binary files differnew file mode 100644 index 0000000..d6d485d --- /dev/null +++ b/application/fonts/fontello-ifont/font/ifont.woff diff --git a/application/fonts/fontello-ifont/font/ifont.woff2 b/application/fonts/fontello-ifont/font/ifont.woff2 Binary files differnew file mode 100644 index 0000000..948e103 --- /dev/null +++ b/application/fonts/fontello-ifont/font/ifont.woff2 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..0e5c700 --- /dev/null +++ b/application/forms/Config/General/ApplicationConfigForm.php @@ -0,0 +1,93 @@ +<?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( + '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..b9979ee --- /dev/null +++ b/application/forms/Config/Resource/DbResourceForm.php @@ -0,0 +1,238 @@ +<?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') { + $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') + ) + ); + $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..fe12aca --- /dev/null +++ b/application/forms/Config/ResourceConfigForm.php @@ -0,0 +1,441 @@ +<?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; + +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 array 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 array 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..2828e95 --- /dev/null +++ b/application/forms/Config/User/CreateMembershipForm.php @@ -0,0 +1,191 @@ +<?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; + 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..debb9b7 --- /dev/null +++ b/application/forms/Config/UserGroup/AddMemberForm.php @@ -0,0 +1,182 @@ +<?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; + 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/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..e9fecc8 --- /dev/null +++ b/application/forms/Navigation/NavigationConfigForm.php @@ -0,0 +1,852 @@ +<?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; + 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..23a7594 --- /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', array()); + 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']); + } + + 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')) + ], + 'value' => isset($value) ? $value : '', + 'disable' => isset($disabled) ? $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..22718ee --- /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..cc8b78c --- /dev/null +++ b/application/forms/Security/RoleForm.php @@ -0,0 +1,624 @@ +<?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 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', self::DENY_PREFIX . $name, [ + 'decorators' => ['ViewHelper'] + ]); + $this->addElement($denyCheckbox); + $this->removeFromIteration($denyCheckbox->getName()); + + if (isset($formData[$denyCheckbox->getName()]) && $formData[$denyCheckbox->getName()]) { + $anythingRefused = true; + } + } + + $elements[] = $elementName; + $this->addElement( + 'checkbox', + $elementName, + [ + 'ignore' => $hasFullPerm || $hasAdminPerm, + 'autosubmit' => isset($spec['isFullPerm']), + 'disabled' => $hasFullPerm || $hasAdminPerm ?: null, + 'value' => $hasFullPerm || $hasAdminPerm, + 'label' => isset($spec['label']) + ? $spec['label'] + : join('', iterator_to_array(call_user_func(function ($segments) { + foreach ($segments as $segment) { + if ($segment[0] === '/') { + // Adds a zero-width char after each slash to help browsers break onto newlines + yield '/​'; + yield '<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 '/​'; + 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'], + 'style' => $isUnrestricted ? 'text-decoration:line-through;' : '', + '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') + ] + ]; + + $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..880c2a9 --- /dev/null +++ b/application/layouts/scripts/layout.phtml @@ -0,0 +1,107 @@ +<?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 ?>"> +<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(); ?>/"> + <script type="text/javascript"> + (function() { + var html = document.getElementsByTagName('html')[0]; + html.className = html.className.replace(/no-js/, 'js'); + }()); + </script> +<?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> +<script type="text/javascript"> + (function() { + if (document.defaultView && document.defaultView.getComputedStyle) { + var matched; + var html = document.getElementsByTagName('html')[0]; + var element = document.getElementById('layout'); + var name = document.defaultView + .getComputedStyle(html)['font-family'] + .replace(/['",]/g, ''); + + if (null !== (matched = name.match(/^([a-z]+)-layout$/))) { + element.className = element.className.replace('default-layout', name); + if ('object' === typeof window.console) { + window.console.log('Icinga Web 2: setting initial layout to ' + name); + } + } + } + }()); +</script> +<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"> +window.name = '<?= $this->protectId('Icinga') ?>'; +var icinga = new Icinga({ + baseUrl: '<?= $this->baseUrl(); ?>', + locale: '<?= $lang; ?>', + timezone: '<?= $timezone ?>' +}); +</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..4f8a272 --- /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 $tickets */ + return isset($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..e80cd89 --- /dev/null +++ b/application/views/scripts/about/index.phtml @@ -0,0 +1,171 @@ +<?php + +use ipl\Html\HtmlElement; +use ipl\Web\Widget\Icon; + +?> +<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> + + <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>© 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 © 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..d4bd78e --- /dev/null +++ b/application/views/scripts/authentication/logout.phtml @@ -0,0 +1,79 @@ +<!-- + 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-status"> + <b><?= $this->translate('Logging out...'); ?></b> + <br> + <?= $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.' + ); ?> + </div> + + <div class="container"> + <a href="<?= $this->href('dashboard'); ?>"><?= $this->translate('Login'); ?></a> + </div> +</div> +<script type="text/javascript"> + /* + * When JavaScript is available, trigger an XmlHTTPRequest with the non-existing user 'logout' and abort it + * before it is able to finish. This will cause the browser to show a new authentication prompt in the next + * request. + */ + document.addEventListener('DOMContentLoaded', function () { + var msg = document.getElementById('logout-status'); + try { + if (navigator.userAgent.toLowerCase().indexOf('msie') !== -1) { + document.execCommand('ClearAuthenticationCache'); + } else { + var xhttp = new XMLHttpRequest(); + xhttp.open('GET', 'arbitrary url', true, 'logout', 'logout'); + xhttp.send(''); + xhttp.abort(); + } + } catch (e) { + } + msg.innerHTML = '<?= $this->translate('Logout successful!'); ?>'; + msg.className = 'alert alert-success'; + }); +</script> +<style type="text/css"> + 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-status { + 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..a6cfe83 --- /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 style="width: 18em;"> + <strong><?= t('Dashlet Name') ?></strong> + </th> + <th> + <strong><?= t('Url') ?></strong> + </th> + <th style="width: 1.4em;"> </th> + </tr> + </thead> + <tbody> + <?php foreach ($this->dashboard->getPanes() as $pane): ?> + <?php if ($pane->getDisabled()) continue; ?> + <tr> + <th colspan="2" style="text-align: left; padding: 0.5em;"> + <?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 style="table-layout: fixed; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"> + <?= $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..3c7462f --- /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.9', 'icinga-php-thirdparty' => '>= 0.11']; + + 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> </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> </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> </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> </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> </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') ?> …" + 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..aa1a90a --- /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 class="state"> + <td style="width: 6em; text-align: center"> + <?= $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..b6739fb --- /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 style="width: 10em"><?= $this->translate('Type'); ?></th> + <th style="width: 10em"><?= $this->translate('Owner'); ?></th> + <th style="width: 5em"><?= $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"><?= $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> |