Merge branch 'feature/theming-10705'

resolves #10705
This commit is contained in:
Eric Lippmann 2015-12-16 12:49:11 +01:00
commit 8006090108
69 changed files with 1584 additions and 1123 deletions

View File

@ -6,6 +6,7 @@ namespace Icinga\Controllers;
use Icinga\Application\Icinga;
use Icinga\Forms\Authentication\LoginForm;
use Icinga\Web\Controller;
use Icinga\Web\Helper\CookieHelper;
use Icinga\Web\Url;
/**
@ -37,13 +38,14 @@ class AuthenticationController extends Controller
$this->redirectNow($form->getRedirectUrl());
}
if (! $requiresSetup) {
if (! $this->getRequest()->hasCookieSupport()) {
$cookies = new CookieHelper($this->getRequest());
if (! $cookies->isSupported()) {
$this
->getResponse()
->setBody("Cookies must be enabled to run this application.\n")
->setHttpResponseCode(403)
->sendResponse();
exit();
exit;
}
$form->handleRequest();
}

View File

@ -36,7 +36,7 @@ class ErrorController extends ActionController
Logger::error('Stacktrace: %s', $exception->getTraceAsString());
if (! ($isAuthenticated = $this->Auth()->isAuthenticated())) {
$this->innerLayout = 'error';
$this->innerLayout = 'guest-error';
}
switch ($error->type) {
@ -89,6 +89,7 @@ class ErrorController extends ActionController
if ($this->getInvokeArg('displayExceptions')) {
$this->view->stackTrace = $exception->getTraceAsString();
}
break;
}

View File

@ -7,7 +7,6 @@ use Icinga\Web\Controller;
use Icinga\Application\Icinga;
use Icinga\Application\Logger;
use Icinga\Web\FileCache;
use Icinga\Web\LessCompiler;
/**
* Delivery static content to clients
@ -87,94 +86,4 @@ class StaticController extends Controller
readfile($filePath);
}
/**
* Return a javascript file from the application's or the module's public folder
*/
public function javascriptAction()
{
$module = $this->_getParam('module_name');
$file = $this->_getParam('file');
if ($module == 'app') {
$basedir = Icinga::app()->getApplicationDir('../public/js/icinga/components/');
$filePath = $basedir . $file;
} else {
if (! Icinga::app()->getModuleManager()->hasEnabled($module)) {
Logger::error(
'Non-existing frontend component "' . $module . '/' . $file
. '" was requested. The module "' . $module . '" does not exist or is not active.'
);
echo "/** Module not enabled **/";
return;
}
$basedir = Icinga::app()->getModuleManager()->getModule($module)->getBaseDir();
$filePath = $basedir . '/public/js/' . $file;
}
if (! file_exists($filePath)) {
Logger::error(
'Non-existing frontend component "' . $module . '/' . $file
. '" was requested, which would resolve to the the path: ' . $filePath
);
echo '/** Module has no js files **/';
return;
}
$response = $this->getResponse();
$response->setHeader('Content-Type', 'text/javascript');
$this->setCacheHeader();
$response->setHeader(
'Last-Modified',
gmdate('D, d M Y H:i:s', filemtime($filePath)) . ' GMT'
);
readfile($filePath);
}
/**
* Set cache header for the response
*
* @param int $maxAge The maximum age to set
*/
private function setCacheHeader($maxAge = 3600)
{
$maxAge = (int) $maxAge;
$this
->getResponse()
->setHeader('Cache-Control', sprintf('max-age=%d', $maxAge), true)
->setHeader('Pragma', 'cache', true)
->setHeader(
'Expires',
gmdate('D, d M Y H:i:s', time() + $maxAge) . ' GMT',
true
);
}
/**
* Send application's and modules' CSS
*/
public function stylesheetAction()
{
$lessCompiler = new LessCompiler();
$moduleManager = Icinga::app()->getModuleManager();
$publicDir = realpath(dirname($_SERVER['SCRIPT_FILENAME']));
$lessCompiler->addItem($publicDir . '/css/vendor');
$lessCompiler->addItem($publicDir . '/css/icinga');
foreach ($moduleManager->getLoadedModules() as $moduleName) {
$cssDir = $moduleName->getCssDir();
if (is_dir($cssDir)) {
$lessCompiler->addItem($cssDir);
}
}
$this->getResponse()->setHeader('Content-Type', 'text/css');
$this->setCacheHeader(3600);
$lessCompiler->printStack();
}
}

View File

@ -8,12 +8,14 @@ use Icinga\Data\ResourceFactory;
use Icinga\Web\Form;
/**
* Form class to modify the general application configuration
* Configuration form for general application options
*
* This form is not used directly but as subform to the {@link GeneralConfigForm}.
*/
class ApplicationConfigForm extends Form
{
/**
* Initialize this form
* {@inheritdoc}
*/
public function init()
{
@ -21,7 +23,9 @@ class ApplicationConfigForm extends Form
}
/**
* @see Form::createElements()
* {@inheritdoc}
*
* @return $this
*/
public function createElements(array $formData)
{
@ -68,6 +72,7 @@ class ApplicationConfigForm extends Form
)
)
);
if (isset($formData['global_config_backend']) && $formData['global_config_backend'] === 'db') {
$backends = array();
foreach (ResourceFactory::getResourceConfigs()->toArray() as $name => $resource) {

View File

@ -6,10 +6,15 @@ namespace Icinga\Forms\Config\General;
use Icinga\Application\Logger;
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
{
/**
* Initialize this form
* {@inheritdoc}
*/
public function init()
{
@ -17,8 +22,9 @@ class LoggingConfigForm extends Form
}
/**
* (non-PHPDoc)
* @see Form::createElements() For the method documentation.
* {@inheritdoc}
*
* @return $this
*/
public function createElements(array $formData)
{

View File

@ -0,0 +1,72 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\General;
use Icinga\Application\Icinga;
use Icinga\Application\Logger;
use Icinga\Web\Form;
/**
* 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)
{
$this->addElement(
'select',
'themes_default',
array(
'description' => $this->translate('The default theme', 'Form element description'),
'label' => $this->translate('Default Theme', 'Form element label'),
'multiOptions' => Icinga::app()->getThemes()
)
);
$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('Disable Themes', 'Form element label')
)
);
return $this;
}
/**
* {@inheritdoc}
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues($suppressArrayNotation);
if ($values['themes_default'] === 'Icinga') {
$values['themes_default'] = null;
}
if (! $values['themes_disabled']) {
$values['themes_disabled'] = null;
}
return $values;
}
}

View File

@ -5,15 +5,16 @@ namespace Icinga\Forms\Config;
use Icinga\Forms\Config\General\ApplicationConfigForm;
use Icinga\Forms\Config\General\LoggingConfigForm;
use Icinga\Forms\Config\General\ThemingConfigForm;
use Icinga\Forms\ConfigForm;
/**
* Form class for application-wide and logging specific settings
* Configuration form for application-wide options
*/
class GeneralConfigForm extends ConfigForm
{
/**
* Initialize this configuration form
* {@inheritdoc}
*/
public function init()
{
@ -22,13 +23,15 @@ class GeneralConfigForm extends ConfigForm
}
/**
* @see Form::createElements()
* {@inheritdoc}
*/
public function createElements(array $formData)
{
$appConfigForm = new ApplicationConfigForm();
$loggingConfigForm = new LoggingConfigForm();
$this->addElements($appConfigForm->createElements($formData)->getElements());
$this->addElements($loggingConfigForm->createElements($formData)->getElements());
$themingConfigForm = new ThemingConfigForm();
$this->addSubForm($appConfigForm->create($formData));
$this->addSubForm($loggingConfigForm->create($formData));
$this->addSubForm($themingConfigForm->create($formData));
}
}

View File

@ -321,21 +321,6 @@ class UserBackendConfigForm extends ConfigForm
}
}
/**
* Retrieve all form element values
*
* @param bool $suppressArrayNotation Ignored
*
* @return array
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues();
$values = array_merge($values, $values['backend_form']);
unset($values['backend_form']);
return $values;
}
/**
* Return whether the given values are valid
*

View File

@ -198,19 +198,4 @@ class UserGroupBackendForm extends ConfigForm
$this->populate($data);
}
}
/**
* Retrieve all form element values
*
* @param bool $suppressArrayNotation Ignored
*
* @return array
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues();
$values = array_merge($values, $values['backend_form']);
unset($values['backend_form']);
return $values;
}
}

View File

@ -5,9 +5,9 @@ namespace Icinga\Forms;
use Exception;
use Zend_Form_Decorator_Abstract;
use Icinga\Application\Config;
use Icinga\Web\Form;
use Icinga\Web\Notification;
use Icinga\Application\Config;
/**
* Form base-class providing standard functionality for configuration forms
@ -21,6 +21,23 @@ class ConfigForm extends Form
*/
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
*

View File

@ -761,17 +761,6 @@ class NavigationConfigForm extends ConfigForm
return $valid;
}
/**
* {@inheritdoc}
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues();
$values = array_merge($values, $values['item_form']);
unset($values['item_form']);
return $values;
}
/**
* {@inheritdoc}
*/

View File

@ -6,12 +6,14 @@ namespace Icinga\Forms;
use Exception;
use DateTimeZone;
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\Util\Translator;
use Icinga\Web\Cookie;
use Icinga\Web\Form;
use Icinga\Web\Notification;
use Icinga\Web\Session;
@ -103,6 +105,14 @@ class PreferenceForm extends Form
Session::getSession()->user->setPreferences($this->preferences);
if (($theme = $this->getElement('theme')) !== null
&& ($theme = $theme->getValue()) !== $this->getRequest()->getCookie('theme')
) {
$this->getResponse()
->setCookie(new Cookie('theme', $theme))
->setReloadCss(true);
}
try {
if ($this->store && $this->getElement('btn_submit_preferences')->isChecked()) {
$this->save();
@ -142,6 +152,20 @@ class PreferenceForm extends Form
*/
public function createElements(array $formData)
{
if (! (bool) Config::app()->get('themes', 'disabled', false)) {
$themes = Icinga::app()->getThemes();
if (count($themes) > 1) {
$this->addElement(
'select',
'theme',
array(
'label' => $this->translate('Theme', 'Form element label'),
'multiOptions' => $themes
)
);
}
}
$languages = array();
$languages['autodetect'] = sprintf($this->translate('Browser (%s)', 'preferences.form'), $this->getLocale());
foreach (Translator::getAvailableLocaleCodes() as $language) {

View File

@ -42,33 +42,7 @@ class RoleForm extends ConfigForm
'application/stacktraces' => $this->translate(
'Allow to adjust in the preferences whether to show stacktraces'
) . ' (application/stacktraces)',
'config/*' => $this->translate('Allow config access') . ' (config/*)',
/*
// [tg] seems excessive for me, hidden for rc1, tbd
'config/application/*' => 'config/application/*',
'config/application/general' => 'config/application/general',
'config/application/resources' => 'config/application/resources',
'config/application/userbackend' => 'config/application/userbackend',
'config/application/usergroupbackend' => 'config/application/usergroupbackend',
'config/application/navigation' => 'config/application/navigation',
'config/authentication/*' => 'config/authentication/*',
'config/authentication/users/*' => 'config/authentication/users/*',
'config/authentication/users/show' => 'config/authentication/users/show',
'config/authentication/users/add' => 'config/authentication/users/add',
'config/authentication/users/edit' => 'config/authentication/users/edit',
'config/authentication/users/remove' => 'config/authentication/users/remove',
'config/authentication/groups/*' => 'config/authentication/groups/*',
'config/authentication/groups/show' => 'config/authentication/groups/show',
'config/authentication/groups/add' => 'config/authentication/groups/add',
'config/authentication/groups/edit' => 'config/authentication/groups/edit',
'config/authentication/groups/remove' => 'config/authentication/groups/remove',
'config/authentication/roles/*' => 'config/authentication/roles/*',
'config/authentication/roles/show' => 'config/authentication/roles/show',
'config/authentication/roles/add' => 'config/authentication/roles/add',
'config/authentication/roles/edit' => 'config/authentication/roles/edit',
'config/authentication/roles/remove' => 'config/authentication/roles/remove',
'config/modules' => 'config/modules'
*/
'config/*' => $this->translate('Allow config access') . ' (config/*)'
);
$helper = new Zend_Form_Element('bogus');

View File

@ -18,22 +18,18 @@ if ($this->layout()->autorefreshInterval) {
?>
<div id="header">
<div id="logo">
<?php if (Auth::getInstance()->isAuthenticated()): ?>
<div id="header-logo-container">
<?= $this->qlink(
'',
'dashboard',
Auth::getInstance()->isAuthenticated() ? 'dashboard' : '',
null,
array(
'icon' => 'img/logo_icinga-inv.png',
'data-base-target' => '_main',
'aria-hidden' => 'true',
'data-base-target' => '_main',
'id' => 'header-logo',
'tabindex' => -1
)
); ?>
<?php else: ?>
<?= $this->icon('img/logo_icinga-inv.png'); ?>
<?php endif ?>
</div>
</div>
<?php if (! $this->layout()->isIframe): ?>

View File

@ -1,8 +0,0 @@
<div class="logo">
<div class="image">
<img aria-hidden="true" src="<?= $this->baseUrl('img/logo_icinga_big.png'); ?>" alt="<?= $this->translate('The Icinga logo'); ?>" >
</div>
</div>
<div class="below-logo">
<?= $this->render('inline.phtml') ?>
</div>

View File

@ -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>

View File

@ -1,4 +1,2 @@
<?= $this->layout()->moduleStart ?>
<?= $this->layout()->content ?>
<?= $this->layout()->benchmark ?>
<?= $this->layout()->moduleEnd ?>

View File

@ -41,15 +41,14 @@ $innerLayoutScript = $this->layout()->innerLayout . '.phtml';
}());
</script>
<?php endif ?>
<link rel="stylesheet" href="<?= $this->href($cssfile) ?>" media="screen" type="text/css" />
<link rel="stylesheet" href="<?= $this->href($cssfile) ?>" media="all" type="text/css" />
<!-- Respond.js IE8 support of media queries -->
<!--[if lt IE 9]>
<script src="<?= $this->baseUrl('js/vendor/respond.min.js');?>"></script>
<![endif]-->
<link type="image/png" rel="shortcut icon" href="<?= $this->baseUrl('img/favicon.png') ?>" />
</head>
<body id="body">
<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); ?>

View File

@ -15,7 +15,7 @@ if ($moduleName !== 'default') {
<html>
<head>
<style>
<?= StyleSheet::compileForPdf() ?>
<?= StyleSheet::forPdf() ?>
</style>
</head>

View File

@ -1,55 +1,51 @@
<div id="login">
<div class="logo">
<div class="image">
<img aria-hidden="true" class="fade-in one" src="<?= $this->baseUrl('img/logo_icinga_big.png'); ?>" alt="<?= $this->translate('The Icinga logo'); ?>" >
</div>
</div>
<div class="form" data-base-target="layout">
<?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="http://docs.icinga.org/" title="' . $this->translate('Icinga Web 2 Documentation') . '">', // TODO: More exact documentation link..
'<a href="' . $this->href('setup') . '" title="' . $this->translate('Icinga Web 2 Setup-Wizard') . '">',
'</a>'
); ?></p>
<?php endif ?>
<?= $this->form ?>
<div class="footer">
Icinga Web 2 &copy; 2013-<?= date('Y'); ?><br><br>
<?= $this->qlink($this->translate('The Icinga Project'), 'https://www.icinga.org'); ?>
<?= $this->qlink(
null,
'http://www.twitter.com/icinga',
null,
array(
'target' => '_blank',
'icon' => 'twitter',
'title' => $this->translate('Icinga on Twitter')
)
); ?>
<?= $this->qlink(
null,
'http://www.facebook.com/icinga',
null,
array(
'target' => '_blank',
'icon' => 'facebook-squared',
'title' => $this->translate('Icinga on Facebook')
)
) ?> <?= $this->qlink(
null,
'https://plus.google.com/+icinga',
null,
array(
'target' => '_blank',
'icon' => 'gplus-squared',
'title' => $this->translate('Icinga on Google+')
)
); ?>
<div id="login" class="centered-ghost">
<div class="centered-content" data-base-target="layout">
<div id="icinga-logo" aria-hidden="true"></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="http://docs.icinga.org/" title="' . $this->translate('Icinga Web 2 Documentation') . '">', // TODO: More exact documentation link
'<a href="' . $this->href('setup') . '" title="' . $this->translate('Icinga Web 2 Setup-Wizard') . '">',
'</a>'
) ?></p>
<?php endif ?>
<?= $this->form ?>
<div id="login-footer">
<p>Icinga Web 2 &copy; 2013-<?= date('Y') ?></p>
<?= $this->qlink($this->translate('The Icinga Project'), 'https://www.icinga.org') ?>
<?= $this->qlink(
null,
'http://www.twitter.com/icinga',
null,
array(
'target' => '_blank',
'icon' => 'twitter',
'title' => $this->translate('Icinga on Twitter')
)
) ?>
<?= $this->qlink(
null,
'http://www.facebook.com/icinga',
null,
array(
'target' => '_blank',
'icon' => 'facebook-squared',
'title' => $this->translate('Icinga on Facebook')
)
) ?> <?= $this->qlink(
null,
'https://plus.google.com/+icinga',
null,
array(
'target' => '_blank',
'icon' => 'gplus-squared',
'title' => $this->translate('Icinga on Google+')
)
) ?>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,10 +1,10 @@
<?php if (! $this->compact && !$hideControls): ?>
<?php if (! $this->compact && ! $hideControls): ?>
<div class="controls">
<?= $tabs->showOnlyCloseButton(); ?>
<?= $tabs->showOnlyCloseButton() ?>
</div>
<?php endif ?>
<div class="content">
<p><strong><?= nl2br($this->escape($message)); ?></strong></p>
<p class="error-message"><?= nl2br($this->escape($message)) ?></p>
<?php if (isset($stackTrace)): ?>
<hr />
<pre><?= $this->escape($stackTrace) ?></pre>

View File

@ -4,7 +4,7 @@ use Icinga\Data\Extensible;
use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls separated dont-print">
<div class="controls separated">
<?= $tabs; ?>
<div class="grid">
<?= $this->sortBox ?>

View File

@ -23,7 +23,7 @@ if ($this->hasPermission('config/authentication/groups/edit') && $backend instan
}
?>
<div class="controls separated dont-print">
<div class="controls separated">
<?php if (! $this->compact): ?>
<?= $tabs; ?>
<?php endif ?>

View File

@ -4,7 +4,7 @@ use Icinga\Data\Extensible;
use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls separated dont-print">
<div class="controls separated">
<?= $tabs ?>
<div class="grid">
<?= $this->sortBox ?>

View File

@ -22,7 +22,7 @@ if ($this->hasPermission('config/authentication/users/edit') && $backend instanc
}
?>
<div class="controls separated dont-print">
<div class="controls separated">
<?php if (! $this->compact): ?>
<?= $tabs; ?>
<?php endif ?>

View File

@ -20,7 +20,7 @@ use Icinga\Web\Response;
class EmbeddedWeb extends ApplicationBootstrap
{
/**
* Request object
* Request
*
* @var Request
*/
@ -57,6 +57,7 @@ class EmbeddedWeb extends ApplicationBootstrap
* Embedded bootstrap parts
*
* @see ApplicationBootstrap::bootstrap
*
* @return $this
*/
protected function bootstrap()
@ -65,6 +66,8 @@ class EmbeddedWeb extends ApplicationBootstrap
->setupZendAutoloader()
->setupErrorHandling()
->loadConfig()
->setupLogging()
->setupLogger()
->setupRequest()
->setupResponse()
->setupTimezone()

View File

@ -13,6 +13,7 @@ use Zend_Paginator;
use Zend_View_Helper_PaginationControl;
use Icinga\Authentication\Auth;
use Icinga\User;
use Icinga\Util\DirectoryIterator;
use Icinga\Util\TimezoneDetect;
use Icinga\Util\Translator;
use Icinga\Web\Controller\Dispatcher;
@ -20,6 +21,7 @@ use Icinga\Web\Navigation\Navigation;
use Icinga\Web\Notification;
use Icinga\Web\Session;
use Icinga\Web\Session\Session as BaseSession;
use Icinga\Web\StyleSheet;
use Icinga\Web\View;
/**
@ -98,6 +100,33 @@ class Web extends EmbeddedWeb
->setupPagination();
}
/**
* Get themes provided by Web 2 and all enabled modules
*
* @return string[] Array of theme names as keys and values
*/
public function getThemes()
{
$themes = array(StyleSheet::DEFAULT_THEME);
$applicationThemePath = $this->getBaseDir('public/css/themes');
if (DirectoryIterator::isReadable($applicationThemePath)) {
foreach (new DirectoryIterator($applicationThemePath, 'less') as $name => $theme) {
$themes[] = substr($name, 0, -5);
}
}
$mm = $this->getModuleManager();
foreach ($mm->listEnabledModules() as $moduleName) {
$moduleThemePath = $mm->getModule($moduleName)->getCssDir() . '/themes';
if (! DirectoryIterator::isReadable($moduleThemePath)) {
continue;
}
foreach (new DirectoryIterator($moduleThemePath, 'less') as $name => $theme) {
$themes[] = $moduleName . '/' . substr($name, 0, -5);
}
}
return array_combine($themes, $themes);
}
/**
* Prepare routing
*

View File

@ -61,7 +61,7 @@ if (in_array($path, $special)) {
Stylesheet::send();
exit;
case 'css/icinga.min.css':
Stylesheet::sendMinified();
Stylesheet::send(true);
exit;
case 'js/icinga.dev.js':

View File

@ -1,70 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\File;
use FilterIterator;
use Iterator;
/**
* Iterator over files having a specific file extension
*
* Usage example:
* <code>
* <?php
*
* namespace Icinga\Example;
*
* use RecursiveDirectoryIterator;
* use RecursiveIteratorIterator;
* use Icinga\File\FileExtensionFilterIterator;
*
* $markdownFiles = new FileExtensionFilterIterator(
* new RecursiveIteratorIterator(
* new RecursiveDirectoryIterator(__DIR__),
* RecursiveIteratorIterator::SELF_FIRST
* ),
* 'md'
* );
* </code>
*/
class FileExtensionFilterIterator extends FilterIterator
{
/**
* The extension to filter for
*
* @var string
*/
protected $extension;
/**
* Create a new FileExtensionFilterIterator
*
* @param Iterator $iterator Apply filter to this iterator
* @param string $extension The file extension to filter for. The file extension may not contain the leading dot
*/
public function __construct(Iterator $iterator, $extension)
{
$this->extension = '.' . ltrim(strtolower((string) $extension), '.');
parent::__construct($iterator);
}
/**
* Accept files which match the file extension to filter for
*
* @return bool Whether the current element of the iterator is acceptable
* through this filter
*/
public function accept()
{
$current = $this->current();
/** @var $current \SplFileInfo */
if (! $current->isFile()) {
return false;
}
// SplFileInfo::getExtension() is only available since PHP 5 >= 5.3.6
$filename = $current->getFilename();
$sfx = substr($filename, -strlen($this->extension));
return $sfx === false ? false : strtolower($sfx) === $this->extension;
}
}

View File

@ -1,48 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\File;
use FilterIterator;
/**
* Iterator over non-empty files
*
* Usage example:
* <code>
* <?php
*
* namespace Icinga\Example;
*
* use RecursiveDirectoryIterator;
* use RecursiveIteratorIterator;
* use Icinga\File\NonEmptyFilterIterator;
*
* $nonEmptyFiles = new NonEmptyFileIterator(
* new RecursiveIteratorIterator(
* new RecursiveDirectoryIterator(__DIR__),
* RecursiveIteratorIterator::SELF_FIRST
* )
* );
* </code>
*/
class NonEmptyFileIterator extends FilterIterator
{
/**
* Accept non-empty files
*
* @return bool Whether the current element of the iterator is acceptable
* through this filter
*/
public function accept()
{
$current = $this->current();
/** @var $current \SplFileInfo */
if (! $current->isFile()
|| $current->getSize() === 0
) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,191 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Util;
use InvalidArgumentException;
use Iterator;
/**
* Iterator for traversing a directory
*/
class DirectoryIterator implements Iterator
{
/**
* Current directory item
*
* @var string|false
*/
private $current;
/**
* The file extension to filter for
*
* @var string
*/
protected $extension;
/**
* Directory handle
*
* @var resource
*/
private $handle;
/**
* Current key
*
* @var string
*/
private $key;
/**
* The path of the directory to traverse
*
* @var string
*/
protected $path;
/**
* Whether to skip empty files
*
* Defaults to true.
*
* @var bool
*/
protected $skipEmpty = true;
/**
* Whether to skip hidden files
*
* Defaults to true.
*
* @var bool
*/
protected $skipHidden = true;
/**
* Create a new directory iterator from path
*
* The given path will not be validated whether it is readable. Use {@link isReadable()} before creating a new
* directory iterator instance.
*
* @param string $path The path of the directory to traverse
* @param string $extension The file extension to filter for. A leading dot is optional
*/
public function __construct($path, $extension = null)
{
if (empty($path)) {
throw new InvalidArgumentException('The path can\'t be empty');
}
$this->path = $path;
if (! empty($extension)) {
$this->extension = '.' . ltrim($extension, '.');
}
}
/**
* Check whether the given path is a directory and is readable
*
* @param string $path The path of the directory
*
* @return bool
*/
public static function isReadable($path)
{
return is_dir($path) && is_readable($path);
}
/**
* {@inheritdoc}
*/
public function current()
{
return $this->current;
}
/**
* {@inheritdoc}
*/
public function next()
{
do {
$file = readdir($this->handle);
if ($file === false) {
$key = false;
break;
} else {
$skip = false;
do {
if ($this->skipHidden && $file[0] === '.') {
$skip = true;
break;
}
$path = $this->path . '/' . $file;
if (is_dir($path)) {
$skip = true;
break;
}
if ($this->skipEmpty && ! filesize($path)) {
$skip = true;
break;
}
if ($this->extension && ! String::endsWith($file, $this->extension)) {
$skip = true;
break;
}
$key = $file;
$file = $path;
} while (0);
}
} while ($skip);
$this->current = $file;
/** @noinspection PhpUndefinedVariableInspection */
$this->key = $key;
}
/**
* {@inheritdoc}
*/
public function key()
{
return $this->key;
}
/**
* {@inheritdoc}
*/
public function valid()
{
return $this->current !== false;
}
/**
* {@inheritdoc}
*/
public function rewind()
{
if ($this->handle === null) {
$this->handle = opendir($this->path);
} else {
rewinddir($this->handle);
}
$this->next();
}
/**
* Close directory handle if created
*/
public function __destruct()
{
if ($this->handle !== null) {
closedir($this->handle);
}
}
}

View File

@ -101,17 +101,17 @@ class String
}
/**
* Check if a string ends with a different string
* Test whether the given string ends with the given suffix
*
* @param $haystack string The string to search for matches
* @param $needle string The string to match at the start of the haystack
* @param string $string The string to test
* @param string $suffix The suffix the string must end with
*
* @return bool Whether or not needle is at the beginning of haystack
* @return bool
*/
public static function endsWith($haystack, $needle)
public static function endsWith($string, $suffix)
{
return $needle === '' ||
(($temp = strlen($haystack) - strlen($needle)) >= 0 && false !== strpos($haystack, $needle, $temp));
$stringSuffix = substr($string, -strlen($suffix));
return $stringSuffix !== false ? $stringSuffix === $suffix : false;
}
/**

View File

@ -3,77 +3,260 @@
namespace Icinga\Web;
use Icinga\Application\Config;
use Icinga\Application\Icinga;
use InvalidArgumentException;
/**
* Helper Class Cookie
* A HTTP cookie
*/
class Cookie
{
/**
* The name of the control cookie
* Domain of the cookie
*
* @var string
*/
const CHECK_COOKIE = '_chc';
protected $domain;
/**
* The request
* The timestamp at which the cookie expires
*
* @var Request
* @var int
*/
protected $request;
protected $expire;
/**
* Whether to protect the cookie against client side script code attempts to read the cookie
*
* Defaults to true.
*
* @var bool
*/
protected $httpOnly = true;
/**
* Name of the cookie
*
* @var string
*/
protected $name;
/**
* The path on the web server where the cookie is available
*
* Defaults to the base URL.
*
* @var string
*/
protected $path;
/**
* Whether to send the cookie only over a secure connection
*
* Defaults to auto-detection so that if the current request was sent over a secure connection the secure flag will
* be set to true.
*
* @var bool
*/
protected $secure;
/**
* Value of the cookie
*
* @var string
*/
protected $value;
/**
* Create a new cookie
*
* @param Request $request
* @param string $name
* @param string $value
*/
public function __construct(Request $request)
public function __construct($name, $value = null)
{
$this->request = $request;
if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
throw new InvalidArgumentException(sprintf(
'Cookie name can\'t contain these characters: =,; \t\r\n\013\014 (%s)',
$name
));
}
if (empty($name)) {
throw new InvalidArgumentException('The cookie name can\'t be empty');
}
$this->name = $name;
$this->value = $value;
}
/**
* Check whether cookies are supported or not
* Get the domain of the cookie
*
* @return string
*/
public function getDomain()
{
return $this->domain;
}
/**
* Set the domain of the cookie
*
* @param string $domain
*
* @return $this
*/
public function setDomain($domain)
{
$this->domain = $domain;
return $this;
}
/**
* Get the timestamp at which the cookie expires
*
* @return int
*/
public function getExpire()
{
return $this->expire;
}
/**
* Set the timestamp at which the cookie expires
*
* @param int $expire
*
* @return $this
*/
public function setExpire($expire)
{
$this->expire = $expire;
return $this;
}
/**
* Get whether to protect the cookie against client side script code attempts to read the cookie
*
* @return bool
*/
public function isHttpOnly()
{
return $this->httpOnly;
}
/**
* Set whether to protect the cookie against client side script code attempts to read the cookie
*
* @param bool $httpOnly
*
* @return $this
*/
public function setHttpOnly($httpOnly)
{
$this->httpOnly = $httpOnly;
return $this;
}
/**
* Get the name of the cookie
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Get the path on the web server where the cookie is available
*
* If the path has not been set either via {@link setPath()} or via config, the base URL will be returned.
*
* @return string
*/
public function getPath()
{
if ($this->path === null) {
$path = Config::app()->get('cookie', 'path');
if ($path === null) {
// The following call could be used as default for ConfigObject::get(), but we prevent unnecessary
// function calls here, if the path is set in the config
$path = Icinga::app()->getRequest()->getBaseUrl();
}
return $path;
}
return $this->path;
}
/**
* Set the path on the web server where the cookie is available
*
* @param string $path
*
* @return $this
*/
public function setPath($path)
{
$this->path = $path;
return $this;
}
/**
* Get whether to send the cookie only over a secure connection
*
* If the secure flag has not been set either via {@link setSecure()} or via config and if the current request was
* sent over a secure connection, true will be returned.
*
* @return bool
*/
public function isSupported()
public function isSecure()
{
if (! empty($_COOKIE)) {
$this->cleanupCheck();
return true;
if ($this->secure === null) {
$secure = Config::app()->get('cookie', 'secure');
if ($secure === null) {
// The following call could be used as default for ConfigObject::get(), but we prevent unnecessary
// function calls here, if the secure flag is set in the config
$secure = Icinga::app()->getRequest()->isSecure();
}
return $secure;
}
$url = $this->request->getUrl();
if ($url->hasParam('_checkCookie') && empty($_COOKIE)) {
return false;
}
if (! $url->hasParam('_checkCookie')) {
$this->provideCheck();
}
return false;
return $this->secure;
}
/**
* Prepare check to detect cookie support
* Set whether to send the cookie only over a secure connection
*
* @param bool $secure
*
* @return $this
*/
public function provideCheck()
public function setSecure($secure)
{
setcookie(self::CHECK_COOKIE, '1');
$requestUri = $this->request->getUrl()->addParams(array('_checkCookie' => 1));
$this->request->getResponse()->redirectAndExit($requestUri);
$this->secure = $secure;
return $this;
}
/**
* Cleanup the cookie support check
* Get the value of the cookie
*
* @return string
*/
public function cleanupCheck()
public function getValue()
{
if ($this->request->getUrl()->hasParam('_checkCookie') && isset($_COOKIE[self::CHECK_COOKIE])) {
$requestUri =$this->request->getUrl()->without('_checkCookie');
$this->request->getResponse()->redirectAndExit($requestUri);
}
return $this->value;
}
/**
* Set the value of the cookie
*
* @param string $value
*
* @return $this
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
}

View File

@ -0,0 +1,57 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web;
use ArrayIterator;
use IteratorAggregate;
/**
* Maintain a set of cookies
*/
class CookieSet implements IteratorAggregate
{
/**
* Cookies in this set indexed by the cookie names
*
* @var Cookie[]
*/
protected $cookies = array();
/**
* Get an iterator for traversing the cookies in this set
*
* @return ArrayIterator An iterator for traversing the cookies in this set
*/
public function getIterator()
{
return new ArrayIterator($this->cookies);
}
/**
* Add a cookie to the set
*
* If a cookie with the same name already exists, the cookie will be overridden.
*
* @param Cookie $cookie The cookie to add
*
* @return $this
*/
public function add(Cookie $cookie)
{
$this->cookies[$cookie->getName()] = $cookie;
return $this;
}
/**
* Get the cookie with the given name from the set
*
* @param string $name The name of the cookie
*
* @return Cookie|null The cookie with the given name or null if the cookie does not exist
*/
public function get($name)
{
return isset($this->cookies[$name]) ? $this->cookies[$name] : null;
}
}

View File

@ -190,9 +190,6 @@ class FileCache
/**
* Whether the given ETag matchesspecific file(s) on disk
*
* If no ETag is given we'll try to fetch the one from the current
* HTTP request. Respects HTTP Cache-Control: no-cache, if set.
*
* @param string|array $files file(s) to check
* @param string $match ETag to match against
*
@ -208,9 +205,6 @@ class FileCache
if (! $match) {
return false;
}
if (isset($_SERVER['HTTP_CACHE_CONTROL']) && $_SERVER['HTTP_CACHE_CONTROL'] === 'no-cache') {
return false;
}
$etag = self::etagForFiles($files);
return $match === $etag ? $etag : false;

View File

@ -0,0 +1,81 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Helper;
use Icinga\Web\Request;
/**
* Helper Class Cookie
*/
class CookieHelper
{
/**
* The name of the control cookie
*/
const CHECK_COOKIE = '_chc';
/**
* The request
*
* @var Request
*/
protected $request;
/**
* Create a new cookie
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* Check whether cookies are supported or not
*
* @return bool
*/
public function isSupported()
{
if (! empty($_COOKIE)) {
$this->cleanupCheck();
return true;
}
$url = $this->request->getUrl();
if ($url->hasParam('_checkCookie') && empty($_COOKIE)) {
return false;
}
if (! $url->hasParam('_checkCookie')) {
$this->provideCheck();
}
return false;
}
/**
* Prepare check to detect cookie support
*/
public function provideCheck()
{
setcookie(self::CHECK_COOKIE, '1');
$requestUri = $this->request->getUrl()->addParams(array('_checkCookie' => 1));
$this->request->getResponse()->redirectAndExit($requestUri);
}
/**
* Cleanup the cookie support check
*/
public function cleanupCheck()
{
if ($this->request->getUrl()->hasParam('_checkCookie') && isset($_COOKIE[self::CHECK_COOKIE])) {
$requestUri =$this->request->getUrl()->without('_checkCookie');
$this->request->getResponse()->redirectAndExit($requestUri);
}
}
}

View File

@ -47,15 +47,22 @@ class JavaScript
public static function sendMinified()
{
return self::send(true);
self::send(true);
}
public static function sendForIe8()
{
self::$vendorFiles = self::$ie8VendorFiles;
return self::send();
self::send();
}
/**
* Send the client side script code to the client
*
* Does not cache the client side script code if the HTTP header Cache-Control or Pragma is set to no-cache.
*
* @param bool $minified Whether to compress the client side script code
*/
public static function send($minified = false)
{
header('Content-Type: application/javascript');
@ -87,7 +94,10 @@ class JavaScript
}
$files = array_merge($vendorFiles, $jsFiles);
if ($etag = FileCache::etagMatchesFiles($files)) {
$request = Icinga::app()->getRequest();
$noCache = $request->getHeader('Cache-Control') === 'no-cache' || $request->getHeader('Pragma') === 'no-cache';
if (! $noCache && FileCache::etagMatchesFiles($files)) {
header("HTTP/1.1 304 Not Modified");
return;
} else {
@ -99,7 +109,7 @@ class JavaScript
$cacheFile = 'icinga-' . $etag . $min . '.js';
$cache = FileCache::instance();
if ($cache->has($cacheFile)) {
if (! $noCache && $cache->has($cacheFile)) {
$cache->send($cacheFile);
return;
}

View File

@ -3,167 +3,170 @@
namespace Icinga\Web;
use Exception;
use RecursiveDirectoryIterator;
use Icinga\Application\Logger;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use RecursiveRegexIterator;
use Icinga\Application\Icinga;
use lessc;
/**
* Less compiler prints files or directories to stdout
* Compile LESS into CSS
*
* Comments will be removed always. lessc is messing them up.
*/
class LessCompiler
{
/**
* Collection of items: File or directories
*
* @var array
*/
private $items = array();
/**
* lessphp compiler
*
* @var \lessc
* @var lessc
*/
private $lessc;
private $source;
protected $lessc;
/**
* Create a new instance
* Array of LESS files
*
* @var string[]
*/
protected $lessFiles = array();
/**
* Array of module LESS files indexed by module names
*
* @var array[]
*/
protected $moduleLessFiles = array();
/**
* LESS source
*
* @var string
*/
protected $source;
/**
* Path of the LESS theme
*
* @var string
*/
protected $theme;
/**
* Create a new LESS compiler
*/
public function __construct()
{
require_once 'lessphp/lessc.inc.php';
$this->lessc = new lessc();
// Discourage usage of import because we're caching based on an explicit list of LESS files to compile
$this->lessc->importDisabled = true;
}
/**
* Disable the extendend import functionality
* Add a Web 2 LESS file
*
* @param string $lessFile Path to the LESS file
*
* @return $this
*/
public function disableExtendedImport()
public function addLessFile($lessFile)
{
$this->lessc->importDisabled = true;
$this->lessFiles[] = $lessFile;
return $this;
}
/**
* Add a module LESS file
*
* @param string $moduleName Name of the module
* @param string $lessFile Path to the LESS file
*
* @return $this
*/
public function addModuleLessFile($moduleName, $lessFile)
{
if (! isset($this->moduleLessFiles[$moduleName])) {
$this->moduleLessFiles[$moduleName] = array();
}
$this->moduleLessFiles[$moduleName][] = $lessFile;
return $this;
}
/**
* Get the list of LESS files added to the compiler
*
* @return string[]
*/
public function getLessFiles()
{
$lessFiles = iterator_to_array(new RecursiveIteratorIterator(new RecursiveArrayIterator(
$this->lessFiles + $this->moduleLessFiles
)));
if ($this->theme !== null) {
$lessFiles[] = $this->theme;
}
return $lessFiles;
}
/**
* Set the path to the LESS theme
*
* @param string $theme Path to the LESS theme
*
* @return $this
*/
public function setTheme($theme)
{
if (is_file($theme) && is_readable($theme)) {
$this->theme = $theme;
} else {
Logger::error('Can\t load theme %s. Make sure that the theme exists and is readable', $theme);
}
return $this;
}
/**
* Instruct the compiler to minify CSS
*
* @return $this
*/
public function compress()
{
$this->lessc->setPreserveComments(false);
$this->lessc->setFormatter('compressed');
return $this;
}
/**
* Add usable style item to stack
* Render to CSS
*
* @param string $item File or directory
* @return string
*/
public function addItem($item)
public function render()
{
$this->items[] = $item;
}
public function addLoadedModules()
{
foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $name => $module) {
$this->addModule($name, $module);
foreach ($this->lessFiles as $lessFile) {
$this->source .= file_get_contents($lessFile);
}
return $this;
}
public function addFile($filename)
{
$this->source .= "\n/* CSS: $filename */\n"
. file_get_contents($filename)
. "\n\n";
return $this;
}
$moduleCss = '';
foreach ($this->moduleLessFiles as $moduleName => $moduleLessFiles) {
$moduleCss .= '.icinga-module.module-' . $moduleName . ' {';
foreach ($moduleLessFiles as $moduleLessFile) {
$moduleCss .= file_get_contents($moduleLessFile);
}
$moduleCss .= '}';
}
$moduleCss = preg_replace(
'/(\.icinga-module\.module-[^\s]+) (#layout\.[^\s]+)/m',
'\2 \1',
$moduleCss
);
$this->source .= $moduleCss;
if ($this->theme !== null) {
$this->source .= file_get_contents($this->theme);
}
public function compile()
{
return $this->lessc->compile($this->source);
}
public function addModule($name, $module)
{
if ($module->hasCss()) {
$contents = array();
foreach ($module->getCssFiles() as $path) {
if (file_exists($path)) {
$contents[] = "/* CSS: modules/$name/$path */\n" . file_get_contents($path);
}
}
$this->source .= ''
. '.icinga-module.module-'
. $name
. " {\n"
. join("\n\n", $contents)
. "}\n\n";
}
return $this;
}
/**
* Compile and print a single file
*
* @param string $file
*/
public function printFile($file)
{
$ext = pathinfo($file, PATHINFO_EXTENSION);
echo PHP_EOL. '/* CSS: ' . $file . ' */' . PHP_EOL;
if ($ext === 'css') {
readfile($file);
} elseif ($ext === 'less') {
try {
echo $this->lessc->compileFile($file);
} catch (Exception $e) {
echo '/* ' . PHP_EOL . ' ===' . PHP_EOL;
echo ' Error in file ' . $file . PHP_EOL;
echo ' ' . $e->getMessage() . PHP_EOL . PHP_EOL;
echo ' ' . 'This file was dropped cause of errors.' . PHP_EOL;
echo ' ===' . PHP_EOL . '*/' . PHP_EOL;
}
}
echo PHP_EOL;
}
/**
* Compile and print a path content (recursive)
*
* @param string $path
*/
public function printPathRecursive($path)
{
$directoryInterator = new RecursiveDirectoryIterator($path);
$iterator = new RecursiveIteratorIterator($directoryInterator);
$filteredIterator = new RegexIterator($iterator, '/\.(css|less)$/', RecursiveRegexIterator::GET_MATCH);
foreach ($filteredIterator as $file => $extension) {
$this->printFile($file);
}
}
/**
* Compile and print the whole item stack
*/
public function printStack()
{
foreach ($this->items as $item) {
if (is_dir($item)) {
$this->printPathRecursive($item);
} elseif (is_file($item)) {
$this->printFile($item);
}
}
}
}

View File

@ -118,15 +118,4 @@ class Request extends Zend_Controller_Request_Http
}
return $id . '-' . $this->uniqueId;
}
/**
* Detect whether cookies are enabled
*
* @return bool
*/
public function hasCookieSupport()
{
$cookie = new Cookie($this);
return $cookie->isSupported();
}
}

View File

@ -7,8 +7,18 @@ use Zend_Controller_Response_Http;
use Icinga\Application\Icinga;
use Icinga\Web\Response\JsonResponse;
/**
* A HTTP response
*/
class Response extends Zend_Controller_Response_Http
{
/**
* Set of cookies which are to be sent to the client
*
* @var CookieSet
*/
protected $cookies;
/**
* Redirect URL
*
@ -23,6 +33,13 @@ class Response extends Zend_Controller_Response_Http
*/
protected $request;
/**
* Whether to instruct client side script code to reload CSS
*
* @var bool
*/
protected $reloadCss;
/**
* Whether to send the rerender layout header on XHR
*
@ -30,6 +47,44 @@ class Response extends Zend_Controller_Response_Http
*/
protected $rerenderLayout = false;
/**
* Get the set of cookies which are to be sent to the client
*
* @return CookieSet
*/
public function getCookies()
{
if ($this->cookies === null) {
$this->cookies = new CookieSet();
}
return $this->cookies;
}
/**
* Get the cookie with the given name from the set of cookies which are to be sent to the client
*
* @param string $name The name of the cookie
*
* @return Cookie|null The cookie with the given name or null if the cookie does not exist
*/
public function getCookie($name)
{
return $this->getCookies()->get($name);
}
/**
* Set the given cookie for sending it to the client
*
* @param Cookie $cookie The cookie to send to the client
*
* @return $this
*/
public function setCookie(Cookie $cookie)
{
$this->getCookies()->add($cookie);
return $this;
}
/**
* Get the redirect URL
*
@ -74,6 +129,29 @@ class Response extends Zend_Controller_Response_Http
return $this->request;
}
/**
* Get whether to instruct client side script code to reload CSS
*
* @return bool
*/
public function isReloadCss()
{
return $this->reloadCss;
}
/**
* Set whether to instruct client side script code to reload CSS
*
* @param bool $reloadCss
*
* @return $this
*/
public function setReloadCss($reloadCss)
{
$this->reloadCss = $reloadCss;
return $this;
}
/**
* Get whether to send the rerender layout header on XHR
*
@ -123,6 +201,9 @@ class Response extends Zend_Controller_Response_Http
if ($this->getRerenderLayout()) {
$this->setHeader('X-Icinga-Container', 'layout', true);
}
if ($this->isReloadCss()) {
$this->setHeader('X-Icinga-Reload-Css', 'now', true);
}
} else {
if ($redirectUrl !== null) {
$this->setRedirect($redirectUrl->getAbsoluteUrl());
@ -148,12 +229,34 @@ class Response extends Zend_Controller_Response_Http
exit;
}
/**
* Send the cookies to the client
*/
public function sendCookies()
{
foreach ($this->getCookies() as $cookie) {
/** @var Cookie $cookie */
setcookie(
$cookie->getName(),
$cookie->getValue(),
$cookie->getExpire(),
$cookie->getPath(),
$cookie->getDomain(),
$cookie->isSecure(),
$cookie->isHttpOnly()
);
}
}
/**
* {@inheritdoc}
*/
public function sendHeaders()
{
$this->prepare();
if (! $this->getRequest()->isApiRequest()) {
$this->sendCookies();
}
return parent::sendHeaders();
}
}

View File

@ -3,12 +3,28 @@
namespace Icinga\Web;
use Exception;
use Icinga\Application\Icinga;
use Icinga\Web\FileCache;
use Icinga\Web\LessCompiler;
use Icinga\Application\Logger;
use Icinga\Exception\IcingaException;
/**
* Send CSS for Web 2 and all loaded modules to the client
*/
class StyleSheet
{
/**
* The name of the default theme
*
* @var string
*/
const DEFAULT_THEME = 'Icinga';
/**
* Array of core LESS files Web 2 sends to the client
*
* @var string[]
*/
protected static $lessFiles = array(
'../application/fonts/fontello-ifont/css/ifont-embedded.css',
'css/vendor/normalize.css',
@ -21,11 +37,9 @@ class StyleSheet
'css/icinga/nav.less',
'css/icinga/main.less',
'css/icinga/animation.less',
'css/icinga/layout-colors.less',
'css/icinga/layout.less',
'css/icinga/layout-structure.less',
'css/icinga/menu.less',
'css/icinga/header-elements.less',
'css/icinga/footer-elements.less',
// 'css/icinga/main-content.less',
'css/icinga/tabs.less',
'css/icinga/forms.less',
@ -37,92 +51,180 @@ class StyleSheet
'css/icinga/dev.less',
// 'css/icinga/logo.less',
'css/icinga/spinner.less',
'css/icinga/compat.less'
'css/icinga/compat.less',
'css/icinga/print.less'
);
public static function compileForPdf()
{
self::checkPhp();
$less = new LessCompiler();
$basedir = Icinga::app()->getBootstrapDirectory();
foreach (self::$lessFiles as $file) {
$less->addFile($basedir . '/' . $file);
}
$less->addLoadedModules();
$less->addFile($basedir . '/css/pdf/pdfprint.less');
return $less->compile();
}
/**
* Application instance
*
* @var \Icinga\Application\EmbeddedWeb
*/
protected $app;
public static function sendMinified()
{
self::send(true);
}
/**
* Less compiler
*
* @var LessCompiler
*/
protected $lessCompiler;
protected static function fixModuleLayoutCss($css)
{
return preg_replace(
'/(\.icinga-module\.module-[^\s]+) (#layout\.[^\s]+)/m',
'\2 \1',
$css
);
}
/**
* Path to the public directory
*
* @var string
*/
protected $pubPath;
protected static function checkPhp()
/**
* Create the StyleSheet
*/
public function __construct()
{
// PHP had a rather conservative PCRE backtrack limit unless 5.3.7
if (version_compare(PHP_VERSION, '5.3.7') <= 0) {
ini_set('pcre.backtrack_limit', 1000000);
}
$app = Icinga::app();
$this->app = $app;
$this->lessCompiler = new LessCompiler();
$this->pubPath = $app->getBootstrapDirectory();
$this->collect();
}
public static function send($minified = false)
/**
* Collect Web 2 and module LESS files and add them to the LESS compiler
*/
protected function collect()
{
self::checkPhp();
$app = Icinga::app();
$basedir = $app->getBootstrapDirectory();
foreach (self::$lessFiles as $file) {
$lessFiles[] = $basedir . '/' . $file;
foreach (self::$lessFiles as $lessFile) {
$this->lessCompiler->addLessFile($this->pubPath . '/' . $lessFile);
}
$files = $lessFiles;
foreach ($app->getModuleManager()->getLoadedModules() as $name => $module) {
$mm = $this->app->getModuleManager();
foreach ($mm->getLoadedModules() as $moduleName => $module) {
if ($module->hasCss()) {
foreach ($module->getCssFiles() as $path) {
if (file_exists($path)) {
$files[] = $path;
}
foreach ($module->getCssFiles() as $lessFilePath) {
$this->lessCompiler->addModuleLessFile($moduleName, $lessFilePath);
}
}
}
if ($etag = FileCache::etagMatchesFiles($files)) {
header("HTTP/1.1 304 Not Modified");
return;
$themingConfig = $this->app->getConfig()->getSection('themes');
$defaultTheme = $themingConfig->get('default', self::DEFAULT_THEME);
$theme = null;
if ((bool) $themingConfig->get('disabled', false)) {
if ($defaultTheme !== self::DEFAULT_THEME) {
$theme = $defaultTheme;
}
} else {
$etag = FileCache::etagForFiles($files);
if (($userTheme = $this->app->getRequest()->getCookie('theme', $defaultTheme))
&& $userTheme !== $defaultTheme
) {
$theme = $userTheme;
}
}
header('Cache-Control: public');
header('ETag: "' . $etag . '"');
header('Content-Type: text/css');
$min = $minified ? '.min' : '';
$cacheFile = 'icinga-' . $etag . $min . '.css';
$cache = FileCache::instance();
if ($cache->has($cacheFile)) {
$cache->send($cacheFile);
if ($theme) {
if (($pos = strpos($theme, '/')) !== false) {
$moduleName = substr($theme, 0, $pos);
$theme = substr($theme, $pos + 1);
if ($mm->hasLoaded($moduleName)) {
$module = $mm->getModule($moduleName);
$this->lessCompiler->setTheme($module->getCssDir() . '/themes/' . $theme . '.less');
}
} else {
$this->lessCompiler->setTheme($this->pubPath . '/css/themes/' . $theme . '.less');
}
}
}
/**
* Get the stylesheet for PDF export
*
* @return $this
*/
public static function forPdf()
{
$styleSheet = new self();
$styleSheet->lessCompiler->addLessFile($styleSheet->pubPath . '/css/pdf/pdfprint.less');
// TODO(el): Caching
return $styleSheet;
}
/**
* Render the stylesheet
*
* @param bool $minified Whether to compress the stylesheet
*
* @return string CSS
*/
public function render($minified = false)
{
if ($minified) {
$this->lessCompiler->compress();
}
return $this->lessCompiler->render();
}
/**
* Send the stylesheet to the client
*
* Does not cache the stylesheet if the HTTP header Cache-Control or Pragma is set to no-cache.
*
* @param bool $minified Whether to compress the stylesheet
*/
public static function send($minified = false)
{
$styleSheet = new self();
$request = $styleSheet->app->getRequest();
$response = $styleSheet->app->getResponse();
$noCache = $request->getHeader('Cache-Control') === 'no-cache' || $request->getHeader('Pragma') === 'no-cache';
if (! $noCache && FileCache::etagMatchesFiles($styleSheet->lessCompiler->getLessFiles())) {
$response
->setHttpResponseCode(304)
->sendHeaders();
return;
}
$less = new LessCompiler();
$less->disableExtendedImport();
foreach ($lessFiles as $file) {
$less->addFile($file);
$etag = FileCache::etagForFiles($styleSheet->lessCompiler->getLessFiles());
$response
->setHeader('Cache-Control', 'public', true)
->setHeader('ETag', $etag, true)
->setHeader('Content-Type', 'text/css', true);
$cacheFile = 'icinga-' . $etag . ($minified ? '.min' : '') . '.css';
$cache = FileCache::instance();
if (! $noCache && $cache->has($cacheFile)) {
$response->setBody($cache->get($cacheFile));
} else {
$css = $styleSheet->render($minified);
$response->setBody($css);
$cache->store($cacheFile, $css);
}
$less->addLoadedModules();
if ($minified) {
$less->compress();
$response->sendResponse();
}
/**
* Render the stylesheet
*
* @return string
*/
public function __toString()
{
try {
return $this->render();
} catch (Exception $e) {
Logger::error($e);
return IcingaException::describe($e);
}
$out = self::fixModuleLayoutCss($less->compile());
$cache->store($cacheFile, $out);
echo $out;
}
}

View File

@ -703,7 +703,7 @@ class FilterEditor extends AbstractWidget
public function renderSearch()
{
$html = ' <form method="post" class="search inline dontprint" action="'
$html = ' <form method="post" class="search inline" action="'
. $this->preservedUrl()
. '"><input type="text" name="q" style="width: 8em" class="search" value="" placeholder="'
. t('Search...')

View File

@ -1,63 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Icinga\File\NonEmptyFileIterator;
use Icinga\File\FileExtensionFilterIterator;
/**
* Iterator over non-empty Markdown files ordered by the case insensitive "natural order" of file names
*/
class DocIterator implements Countable, IteratorAggregate
{
/**
* Ordered files
*
* @var array
*/
protected $fileInfo;
/**
* Create a new DocIterator
*
* @param string $path Path to the documentation
*/
public function __construct($path)
{
$it = new FileExtensionFilterIterator(
new NonEmptyFileIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::SELF_FIRST
)
),
'md'
);
// Unfortunately we have no chance to sort the iterator
$fileInfo = iterator_to_array($it);
natcasesort($fileInfo);
$this->fileInfo = $fileInfo;
}
/**
* {@inheritdoc}
*/
public function count()
{
return count($this->fileInfo);
}
/**
* {@inheritdoc}
*/
public function getIterator()
{
return new ArrayIterator($this->fileInfo);
}
}

View File

@ -4,11 +4,11 @@
namespace Icinga\Module\Doc;
use CachingIterator;
use LogicException;
use SplFileObject;
use SplStack;
use Icinga\Data\Tree\SimpleTree;
use Icinga\Exception\NotReadableError;
use Icinga\Module\Doc\Exception\DocEmptyException;
use Icinga\Util\DirectoryIterator;
use Icinga\Module\Doc\Exception\DocException;
/**
@ -40,7 +40,7 @@ class DocParser
/**
* Iterator over documentation files
*
* @var DocIterator
* @var DirectoryIterator
*/
protected $docIterator;
@ -51,34 +51,17 @@ class DocParser
*
* @throws DocException If the documentation directory does not exist
* @throws NotReadableError If the documentation directory is not readable
* @throws DocEmptyException If the documentation directory is empty
*/
public function __construct($path)
{
if (! is_dir($path)) {
if (! DirectoryIterator::isReadable($path)) {
throw new DocException(
sprintf(mt('doc', 'Documentation directory \'%s\' does not exist'), $path)
);
}
if (! is_readable($path)) {
throw new DocException(
sprintf(mt('doc', 'Documentation directory \'%s\' is not readable'), $path)
);
}
$docIterator = new DocIterator($path);
if ($docIterator->count() === 0) {
throw new DocEmptyException(
sprintf(
mt(
'doc',
'Documentation directory \'%s\' does not contain any non-empty Markdown file (\'.md\' suffix)'
),
$path
)
mt('doc', 'Documentation directory \'%s\' is not readable'),
$path
);
}
$this->path = $path;
$this->docIterator = $docIterator;
$this->docIterator = new DirectoryIterator($path, 'md');
}
/**
@ -145,9 +128,8 @@ class DocParser
public function getDocTree()
{
$tree = new SimpleTree();
foreach ($this->docIterator as $fileInfo) {
/** @var $fileInfo \SplFileInfo */
$file = $fileInfo->openFile();
foreach ($this->docIterator as $filename) {
$file = new SplFileObject($filename);
$lastLine = null;
$stack = new SplStack();
$cachingIterator = new CachingIterator($file, CachingIterator::TOSTRING_USE_CURRENT);

View File

@ -1,11 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc\Exception;
/**
* Exception thrown if a documentation directory is empty
*/
class DocEmptyException extends DocException
{
}

View File

@ -70,6 +70,9 @@ class DocTocRenderer extends DocRenderer
*/
public function render()
{
if (empty($this->content)) {
return '<p>' . mt('doc', 'Documentation is empty.') . '</p>';
}
$view = $this->getView();
$zendUrlHelper = $view->getHelper('Url');
foreach ($this as $section) {

View File

@ -258,19 +258,4 @@ class TransportConfigForm extends ConfigForm
$this->populate($data);
}
}
/**
* Retrieve all form element values
*
* @param bool $suppressArrayNotation Ignored
*
* @return array
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues();
$values = array_merge($values, $values['transport_form']);
unset($values['transport_form']);
return $values;
}
}

View File

@ -1,7 +1,7 @@
<?php if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<div style="float: right;" class="dontprint">
<div style="float: right;" class="dont-print">
<?= $intervalBox; ?>
</div>
<?= $this->limiter; ?>

View File

@ -1,5 +1,5 @@
<?php if (! $this->compact): ?>
<div class="controls separated dont-print">
<div class="controls separated">
<?= $tabs ?>
<?= $this->render('list/components/selectioninfo.phtml') ?>
<div class="grid">

View File

@ -7,7 +7,7 @@ if (! $stats instanceof stdClass) {
$stats = $stats->fetchRow();
}
?>
<div class="hosts-summary">
<div class="hosts-summary dont-print">
<span class="hosts-link"><?= $this->qlink(
sprintf($this->translatePlural('%u Host', '%u Hosts', $stats->hosts_total), $stats->hosts_total),
// @TODO(el): Fix that

View File

@ -7,7 +7,7 @@ if (! $stats instanceof stdClass) {
$stats = $stats->fetchRow();
}
?>
<div class="services-summary">
<div class="services-summary dont-print">
<span class="services-link"><?= $this->qlink(
sprintf($this->translatePlural(
'%u Service', '%u Services', $stats->services_total),

View File

@ -3,7 +3,7 @@ use Icinga\Module\Monitoring\Object\Host;
use Icinga\Module\Monitoring\Object\Service;
if (! $this->compact): ?>
<div class="controls separated dont-print">
<div class="controls separated">
<?= $tabs ?>
<?= $this->render('list/components/selectioninfo.phtml') ?>
<div class="grid">

View File

@ -20,13 +20,13 @@ if (! $this->compact): ?>
<?php return; endif ?>
<table class="table-row-selectable common-table" data-base-target="_next">
<thead>
<tr>
<th></th>
<th><?= $this->translate('Host Group') ?></th>
<th><?= $this->translate('Host States') ?></th>
<th></th>
<th><?= $this->translate('Service States') ?></th>
</tr>
<tr>
<th></th>
<th><?= $this->translate('Host Group') ?></th>
<th><?= $this->translate('Host States') ?></th>
<th></th>
<th><?= $this->translate('Service States') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($hostgroups->peekAhead($this->compact) as $hostgroup): ?>

View File

@ -18,9 +18,11 @@ if (! $this->compact): ?>
<?php return; endif ?>
<table class="table-row-selectable common-table" data-base-target="_next">
<thead>
<th></th>
<th><?= $this->translate('Service Group') ?></th>
<th><?= $this->translate('Service States') ?></th>
<tr>
<th></th>
<th><?= $this->translate('Service Group') ?></th>
<th><?= $this->translate('Service States') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($servicegroups->peekAhead($this->compact) as $serviceGroup): ?>

View File

@ -186,12 +186,14 @@
width: 100%;
tr[href] {
.transition(background 0.2s ease);
&.active {
background-color: @gray-lighter;
background-color: @tr-active-color;
}
&:hover {
background-color: @gray-lightest;
background-color: @tr-hover-color;
cursor: pointer;
}
}

View File

@ -116,21 +116,6 @@ class AuthBackendPage extends Form
$this->addSubForm($backendForm, 'backend_form');
}
/**
* Retrieve all form element values
*
* @param bool $suppressArrayNotation Ignored
*
* @return array
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues();
$values = array_merge($values, $values['backend_form']);
unset($values['backend_form']);
return $values;
}
/**
* Validate the given form data and check whether it's possible to authenticate using the configured backend
*

View File

@ -129,19 +129,4 @@ class UserGroupBackendPage extends Form
)
);
}
/**
* Retrieve all form element values
*
* @param bool $suppressArrayNotation Ignored
*
* @return array
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues();
$values = array_merge($values, $values['backend_form']);
unset($values['backend_form']);
return $values;
}
}

View File

@ -31,6 +31,9 @@
// Text color on <a>
@link-color: @text-color;
@tr-active-color: #E5F9FF;
@tr-hover-color: #F5FDFF;
// Font families
@font-family: Calibri, Helvetica, sans-serif;
@font-family-fixed: "Liberation Mono", "Lucida Console", Courier, monospace;
@ -138,19 +141,11 @@ td, th {
}
}
#layout {
background-color: @body-bg-color;
color: @text-color;
font-family: @font-family;
}
#main > .container, #menu, #header {
font-size: @font-size;
line-height: @line-height;
}
@media print {
.dont-print {
display: none;
}
// Styles for when the page is loading. JS will remove this class once the document is ready
.loading * {
// Disable all transition on page load
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}

View File

@ -1,50 +0,0 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
div#footer {
position: fixed;
left: 0px;
right: 0px;
bottom: 0px;
z-index: 9999;
}
/** Notifications **/
#notifications {
margin: 0;
padding: 0;
}
#notifications > li {
list-style-type: none;
display: block;
border-top: 1px solid #999;
color: white;
line-height: 2.5em;
padding-left: 3em;
background-repeat: no-repeat;
background-position: 1em center;
}
#notifications > li:hover {
cursor: pointer;
}
#notifications > li.info {
background-color: @colorFormNotificationInfo;
}
#notifications > li.warning {
background-color: @colorWarningHandled;
}
#notifications > li.error {
background-color: @colorCritical;
background-image: url(../img/icons/error_white.png);
}
#notifications > li.success {
background-color: #fe6;
background-image: url(../img/icons/success.png);
color: #333;
}
/** END of Notifications **/

View File

@ -1,18 +0,0 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
#logo {
height: 4.0em;
width: 13em;
display: inline-block;
}
#logo a {
display: block;
outline: 0;
}
#logo img {
width: 100px;
margin-left: 1.5em;
margin-top: 0.8em;
}

View File

@ -1,37 +0,0 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
/* Layout colors */
#sidebar {
background-color: @gray-lighter;
}
#header {
background-color: @icinga-blue;
color: #ddd;
color: #d0d0d0;
}
#header input {
background-color: #777;
}
#main {
background-color: white;
}
#col1.impact, #col2.impact, #col3.impact {
background-color: #ddd;
transition: background-color 2s 1s linear;
-moz-transition: background-color 2s 1s linear;
-o-transition: background-color 2s 1s linear;
-webkit-transition: background-color 2s 1s linear;
.controls {
background-color: #ddd;
transition: background-color 2s 1s linear;
-moz-transition: background-color 2s 1s linear;
-o-transition: background-color 2s 1s linear;
-webkit-transition: background-color 2s 1s linear;
}
}

View File

@ -96,7 +96,6 @@ html {
.controls > ul.tabs {
margin-top: 0;
height: 1.5em;
background-color: @icinga-blue;
font-size: 0.75em;
padding: 0.2em 0 0;
}
@ -138,7 +137,6 @@ html {
*/
.container .controls {
top: 0;
background-color: white;
padding: 1em 1em 0;
z-index: 100;
@ -267,42 +265,9 @@ html {
margin-right: 1%;
}
#logo img {
/* TODO: Quickfix, this needs improvement */
width: 0 !important;
top: -100px;
position: absolute;
}
#main {
left: 0;
}
#login {
.below-logo label {
width: 100%;
margin: 0;
text-align: center;
display: inline-block;
}
.footer {
margin-left: 0;
}
h1 {
margin-left: 0px;
text-align: center;
}
form {
margin-left: auto;
margin-right: auto;
}
form input {
margin: auto;
display: block;
}
form input[type=submit] {
margin-top: 1.5em;
}
}
}

View File

@ -0,0 +1,121 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
#footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
#guest-error {
background-color: @icinga-blue;
height: 100%;
overflow: auto;
}
#guest-error #icinga-logo {
.fadein();
}
#guest-error-message {
.fadein();
color: @body-bg-color;
font-size: 2em;
}
#header,
#main > .container,
#menu {
font-size: @font-size;
line-height: @line-height;
}
#header {
background-color: @icinga-blue;
}
#header-logo-container {
height: 4em;
margin-left: 1.5em;
margin-top: 0.2em;
margin-bottom: 0.2em;
width: 100px;
}
#header-logo {
background-image: url('../img/logo_icinga-inv.png');
background-position: center;
background-repeat: no-repeat;
background-size: contain;
display: block;
height: 100%;
}
#icinga-logo {
background-image: url('../img/logo_icinga_big.png');
background-position: center;
background-repeat: no-repeat;
background-size: contain; // Does not work in IE < 10
height: 177px;
margin-bottom: 2em;
width: 100%;
}
#layout {
background-color: @body-bg-color;
color: @text-color;
font-family: @font-family;
}
#login {
overflow: auto;
}
#sidebar {
background-color: @gray-lighter;
}
.controls {
background-color: @body-bg-color;
}
// Notification styles
#notifications {
margin: 0;
padding: 0;
}
#notifications > li {
list-style-type: none;
display: block;
border-top: 1px solid #999;
color: white;
line-height: 2.5em;
padding-left: 3em;
background-repeat: no-repeat;
background-position: 1em center;
}
#notifications > li:hover {
cursor: pointer;
}
#notifications > li.info {
background-color: @colorFormNotificationInfo;
}
#notifications > li.warning {
background-color: @colorWarningHandled;
}
#notifications > li.error {
background-color: @colorCritical;
background-image: url(../img/icons/error_white.png);
}
#notifications > li.success {
background-color: #fe6;
background-image: url(../img/icons/success.png);
color: #333;
}

View File

@ -1,189 +1,91 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
// Login page styles
#login {
#icinga-logo {
.fadein();
}
background-color: @icinga-blue;
width: 100%;
height: 100%;
.form-controls {
margin: 0;
}
.control-group {
padding: 0;
}
.logo {
text-align: center;
}
.image {
padding-top: 5%;
margin-left: auto;
margin-right: auto;
text-align: center;
font-size: 0.9em;
.control-label {
color: @body-bg-color;
font-size: 1em;
line-height: @line-height;
}
.errors {
font-size: 0.9em;
margin-bottom: 0.5em;
margin-left:auto;
margin-right:auto;
padding: 0.5em;
}
background-color: @color-critical;
color: @body-bg-color;
font-size: @font-size-small;
margin: 2em 0 0 0;
.image img {
width: 355px;
}
.form {
position: absolute;
font-size: 0.9em;
top: 35%;
left: 0;
bottom: 0;
right: 0;
}
.form h1 {
text-align: center;
font-size: 1.5em;
margin-left: 2.3em;
border: none;
color: @text-color-inverted;
font-variant: unset;
}
.form div.element {
margin: 0;
ul.errors {
> li {
padding: 1em;
}
}
.form label {
color: @text-color-inverted;
font-weight: normal;
.control-group {
margin: 0 auto; // Center horizontally
width: 24em;
}
.control-label-group {
display: block;
line-height: 2.5em;
width: 15em;
margin-right: 2.5em;
padding: 0;
text-align: left;
width: 100%;
}
form {
margin-left: auto;
margin-right: auto;
width: 18em;
font-size: 20px;
}
form input {
color: @text-color-inverted;
width: 18em;
padding: 0.5em;
background: rgba(255,255,255,0.2);
margin-left: 0;
padding: 6px 10px;
input[type=password],
input[type=text] {
background-color: @gray-light; // IE8 fallback
background-color: rgba(255,255,255,0.2);
border: none;
border-bottom: solid 3px @body-bg-color;
-webkit-transition: border 0.3s;
-moz-transition: border 0.3s;
-o-transition: border 0.3s;
transition: border 0.3s;
color: @body-bg-color;
display: block;
margin: 0;
width: 100%;
}
form input:focus {
border-bottom: solid 3px @body-bg-color;
background: rgba(255,255,255,0.4);
}
ul.errors {
.form-controls {
margin-bottom: 2em;
margin-top: 2em;
}
input[type=submit] {
color: @text-color-inverted;
background: none;
border: none;
margin-top: 3em;
margin-right: 5px;
border: solid 3px @body-bg-color;
-webkit-transition: border 0.3s;
-moz-transition: border 0.3s;
-o-transition: border 0.3s;
.button(@icinga-blue, @body-bg-color);
margin: 0;
width: 100%;
}
input[type=submit]:hover, a.button:hover, input[type=submit]:focus {
background: @body-bg-color;
color: @icinga-blue;
}
.footer {
color: @text-color-inverted;
margin-top: 5em;
font-size: 0.9em;
text-align: center;
a {
color: @text-color-inverted;
font-weight: bold;
}
}
p.config-note {
width: 50%;
padding: 1em;
margin: 0 auto 2.5em;
text-align: center;
.config-note {
color: white;
background-color: @color-critical;
margin: 0 auto 2em auto; // Center horizontally w/ bottom margin
max-width: 50%;
min-width: 24em;
padding: 1em;
a {
color: @text-color-inverted;
font-weight: bold;
}
}
p.info-box {
width: 50%;
height: 2.2em;
margin: 2em auto 2.5em;
i.icon-info {
float: left;
height: 100%;
}
em {
text-decoration: underline;
}
}
}
/* make keyframes that tell the start state and the end state of our object */
@-webkit-keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
@-moz-keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
#login-footer {
color: @body-bg-color;
font-size: @font-size-small;
.fade-in {
opacity:0; /* make things invisible upon start */
-webkit-animation:fadeIn ease-in 1; /* call our keyframe named fadeIn, use animattion ease-in and repeat it only 1 time */
-moz-animation:fadeIn ease-in 1;
animation:fadeIn ease-in 1;
a {
font-weight: @font-weight-bold;
-webkit-animation-fill-mode:forwards; /* this makes sure that after animation is done we remain at the last keyframe value (opacity: 1)*/
-moz-animation-fill-mode:forwards;
animation-fill-mode:forwards;
-webkit-animation-duration:1s;
-moz-animation-duration:1s;
animation-duration:1s;
}
.fade-in.one {
-webkit-animation-delay: 0.7s;
-moz-animation-delay: 0.7s;
animation-delay: 0.7s;
// Social links
&:hover > i {
color: @text-color;
}
}
}

View File

@ -3,8 +3,12 @@
// Width for the name column--th--of name-value-table
@name-value-table-name-width: 14em;
.action-link {
color: @icinga-blue;
.action-link(@color: @icinga-blue) {
color: @color;
}
.error-message {
font-weight: @font-weight-bold;
}
.large-icon {
@ -58,9 +62,9 @@ a:hover > .icon-cancel {
// Link styles
.button-link {
.action-link();
.button();
.button-link(@background-color: @body-bg-color, @color: @icinga-blue) {
.action-link(@color);
.button(@background-color, @color);
display: inline-block;
height: 35px;
line-height: 20px;
@ -164,13 +168,16 @@ a:hover > .icon-cancel {
}
tr[href] {
.transition(background 0.2s ease);
&.active {
background-color: @gray-lighter;
background-color: @tr-active-color;
border-left-color: @icinga-blue;
transition: none;
}
&:hover {
background-color: @gray-lightest;
background-color: @tr-hover-color;
cursor: pointer;
}
}
@ -227,3 +234,35 @@ a:hover > .icon-cancel {
margin-left: 0.5em;
}
}
/* Styles for centering content of unknown width and height both horizontally and vertically
*
* Example markup:
* <div class="centered-ghost">
* <div class="centered-content">
* <p>I'm centered.</p>
* </div>
* </div>
*/
.centered-content {
display: inline-block;
vertical-align: middle;
}
.centered-ghost {
height: 100%;
text-align: center;
letter-spacing: -1em; // Remove gap between content and ghost
}
.centered-ghost > * {
letter-spacing: normal;
}
.centered-ghost:after {
content: '';
display: inline-block;
height: 100%;
vertical-align: middle;
}

View File

@ -12,22 +12,24 @@
box-shadow: @arguments;
}
.button() {
.button(@background-color: @body-bg-color, @color: @icinga-blue) {
.rounded-corners(3px);
background-color: @body-bg-color;
border: 2px solid @icinga-blue;
color: @icinga-blue;
background-color: @background-color;
border: 2px solid @color;
color: @color;
cursor: pointer;
padding: @vertical-padding @horizontal-padding;
// Transition mixin does not work w/ comma-separated transistions
-webkit-transition: background 0.3s ease, color 0.3s ease;
-moz-transition: background 0.3s ease, color 0.3s ease;
-o-transition: background 0.3s ease, color 0.3s ease;
transition: background 0.3s ease, color 0.3s ease;
&:hover {
background-color: @icinga-blue;
color: @text-color-inverted;
background-color: @color;
color: @background-color;
text-decoration: none;
}
}
@ -79,3 +81,49 @@
.visible {
visibility: visible;
}
// Fadein animation
/* Chrome, WebKit */
@-webkit-keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
/* FF < 16 */
@-moz-keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
/* IE */
@-ms-keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
/* Opera < 12.1 */
@-o-keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
.fadein() {
opacity: 0;
-webkit-animation: fadein 2s ease-in; /* Chrome, WebKit */
-moz-animation: fadein 2s ease-in; /* FF < 16 */
-o-animation: fadein 2s ease-in; /* Opera < 12.1 */
animation: fadein 2s ease-in;
// Make sure that after animation is done we remain at the last keyframe value (opacity: 1)
-webkit-animation-fill-mode: forwards;
-moz-animation-fill-mode: forwards;
-o-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}

View File

@ -0,0 +1,31 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
// Print styles
@media print {
#fileupload-frame-target,
#header,
#responsive-debug,
#sidebar,
.controls > .tabs,
.controls .filter,
.controls .limiter-control,
.controls .pagination-control,
.controls .selection-info,
.controls .sort-control,
.dontprint, // Compat only, use dont-print instead
.dont-print {
display: none;
}
#layout,
#main,
.controls {
position: static;
}
.container {
float: none !important;
width: 100% !important;
}
}

View File

@ -1,6 +1,11 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
.controls form, .controls .pagination, .controls .widgetLimiter, .controls > .tabs, .dontprint {
.controls form,
.controls .pagination,
.controls .widgetLimiter,
.controls > .tabs,
.dontprint, // Compat only, use dont-print instead
.dont-print {
display: none !important;
}
@ -10,7 +15,7 @@ table.action img.inlinepie {
}
@page {
margin: 2cm;
margin: 2cm;
}
.container * {
@ -26,15 +31,15 @@ body > h1 {
font-weight: bold;
position: fixed;
left: 1em;
right: 1em;
color: #aaa;
font-size: 0.9em;
right: 1em;
color: #aaa;
font-size: 0.9em;
}
#page-header table, #page-footer table {
width: 100%;
border-collapse: collapse;
border: none;
width: 100%;
border-collapse: collapse;
border: none;
}
#page-header table {
@ -45,12 +50,12 @@ body > h1 {
font-family: fixed;
padding: 0;
color: #aaa;
width: 50%;
width: 50%;
}
#page-header {
top: 0.5em;
border-bottom: 0.2pt solid #aaa;
border-bottom: 0.2pt solid #aaa;
}
#page-footer {
@ -93,4 +98,3 @@ h1 form {
body {
margin: 1cm 1cm 1.5cm 1cm;
}

View File

@ -161,6 +161,7 @@
},
onLoad: function (event) {
$('body').removeClass('loading');
//$('.container').trigger('rendered');
},