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

View File

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

View File

@ -7,7 +7,6 @@ use Icinga\Web\Controller;
use Icinga\Application\Icinga; use Icinga\Application\Icinga;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Web\FileCache; use Icinga\Web\FileCache;
use Icinga\Web\LessCompiler;
/** /**
* Delivery static content to clients * Delivery static content to clients
@ -87,94 +86,4 @@ class StaticController extends Controller
readfile($filePath); 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; 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 class ApplicationConfigForm extends Form
{ {
/** /**
* Initialize this form * {@inheritdoc}
*/ */
public function init() public function init()
{ {
@ -21,7 +23,9 @@ class ApplicationConfigForm extends Form
} }
/** /**
* @see Form::createElements() * {@inheritdoc}
*
* @return $this
*/ */
public function createElements(array $formData) public function createElements(array $formData)
{ {
@ -68,6 +72,7 @@ class ApplicationConfigForm extends Form
) )
) )
); );
if (isset($formData['global_config_backend']) && $formData['global_config_backend'] === 'db') { if (isset($formData['global_config_backend']) && $formData['global_config_backend'] === 'db') {
$backends = array(); $backends = array();
foreach (ResourceFactory::getResourceConfigs()->toArray() as $name => $resource) { foreach (ResourceFactory::getResourceConfigs()->toArray() as $name => $resource) {

View File

@ -6,10 +6,15 @@ namespace Icinga\Forms\Config\General;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Web\Form; 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 class LoggingConfigForm extends Form
{ {
/** /**
* Initialize this form * {@inheritdoc}
*/ */
public function init() public function init()
{ {
@ -17,8 +22,9 @@ class LoggingConfigForm extends Form
} }
/** /**
* (non-PHPDoc) * {@inheritdoc}
* @see Form::createElements() For the method documentation. *
* @return $this
*/ */
public function createElements(array $formData) 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\ApplicationConfigForm;
use Icinga\Forms\Config\General\LoggingConfigForm; use Icinga\Forms\Config\General\LoggingConfigForm;
use Icinga\Forms\Config\General\ThemingConfigForm;
use Icinga\Forms\ConfigForm; use Icinga\Forms\ConfigForm;
/** /**
* Form class for application-wide and logging specific settings * Configuration form for application-wide options
*/ */
class GeneralConfigForm extends ConfigForm class GeneralConfigForm extends ConfigForm
{ {
/** /**
* Initialize this configuration form * {@inheritdoc}
*/ */
public function init() public function init()
{ {
@ -22,13 +23,15 @@ class GeneralConfigForm extends ConfigForm
} }
/** /**
* @see Form::createElements() * {@inheritdoc}
*/ */
public function createElements(array $formData) public function createElements(array $formData)
{ {
$appConfigForm = new ApplicationConfigForm(); $appConfigForm = new ApplicationConfigForm();
$loggingConfigForm = new LoggingConfigForm(); $loggingConfigForm = new LoggingConfigForm();
$this->addElements($appConfigForm->createElements($formData)->getElements()); $themingConfigForm = new ThemingConfigForm();
$this->addElements($loggingConfigForm->createElements($formData)->getElements()); $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 * Return whether the given values are valid
* *

View File

@ -198,19 +198,4 @@ class UserGroupBackendForm extends ConfigForm
$this->populate($data); $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 Exception;
use Zend_Form_Decorator_Abstract; use Zend_Form_Decorator_Abstract;
use Icinga\Application\Config;
use Icinga\Web\Form; use Icinga\Web\Form;
use Icinga\Web\Notification; use Icinga\Web\Notification;
use Icinga\Application\Config;
/** /**
* Form base-class providing standard functionality for configuration forms * Form base-class providing standard functionality for configuration forms
@ -21,6 +21,23 @@ class ConfigForm extends Form
*/ */
protected $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 * 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; 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} * {@inheritdoc}
*/ */

View File

@ -6,12 +6,14 @@ namespace Icinga\Forms;
use Exception; use Exception;
use DateTimeZone; use DateTimeZone;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Authentication\Auth; use Icinga\Authentication\Auth;
use Icinga\User\Preferences; use Icinga\User\Preferences;
use Icinga\User\Preferences\PreferencesStore; use Icinga\User\Preferences\PreferencesStore;
use Icinga\Util\TimezoneDetect; use Icinga\Util\TimezoneDetect;
use Icinga\Util\Translator; use Icinga\Util\Translator;
use Icinga\Web\Cookie;
use Icinga\Web\Form; use Icinga\Web\Form;
use Icinga\Web\Notification; use Icinga\Web\Notification;
use Icinga\Web\Session; use Icinga\Web\Session;
@ -103,6 +105,14 @@ class PreferenceForm extends Form
Session::getSession()->user->setPreferences($this->preferences); 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 { try {
if ($this->store && $this->getElement('btn_submit_preferences')->isChecked()) { if ($this->store && $this->getElement('btn_submit_preferences')->isChecked()) {
$this->save(); $this->save();
@ -142,6 +152,20 @@ class PreferenceForm extends Form
*/ */
public function createElements(array $formData) 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 = array();
$languages['autodetect'] = sprintf($this->translate('Browser (%s)', 'preferences.form'), $this->getLocale()); $languages['autodetect'] = sprintf($this->translate('Browser (%s)', 'preferences.form'), $this->getLocale());
foreach (Translator::getAvailableLocaleCodes() as $language) { foreach (Translator::getAvailableLocaleCodes() as $language) {

View File

@ -42,33 +42,7 @@ class RoleForm extends ConfigForm
'application/stacktraces' => $this->translate( 'application/stacktraces' => $this->translate(
'Allow to adjust in the preferences whether to show stacktraces' 'Allow to adjust in the preferences whether to show stacktraces'
) . ' (application/stacktraces)', ) . ' (application/stacktraces)',
'config/*' => $this->translate('Allow config access') . ' (config/*)', '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'
*/
); );
$helper = new Zend_Form_Element('bogus'); $helper = new Zend_Form_Element('bogus');

View File

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

View File

@ -41,15 +41,14 @@ $innerLayoutScript = $this->layout()->innerLayout . '.phtml';
}()); }());
</script> </script>
<?php endif ?> <?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 --> <!-- Respond.js IE8 support of media queries -->
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="<?= $this->baseUrl('js/vendor/respond.min.js');?>"></script> <script src="<?= $this->baseUrl('js/vendor/respond.min.js');?>"></script>
<![endif]--> <![endif]-->
<link type="image/png" rel="shortcut icon" href="<?= $this->baseUrl('img/favicon.png') ?>" /> <link type="image/png" rel="shortcut icon" href="<?= $this->baseUrl('img/favicon.png') ?>" />
</head> </head>
<body id="body"> <body id="body" class="loading">
<pre id="responsive-debug"></pre> <pre id="responsive-debug"></pre>
<div id="layout" class="default-layout<?php if ($showFullscreen): ?> fullscreen-layout<?php endif ?>"> <div id="layout" class="default-layout<?php if ($showFullscreen): ?> fullscreen-layout<?php endif ?>">
<?= $this->render($innerLayoutScript); ?> <?= $this->render($innerLayoutScript); ?>

View File

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

View File

@ -1,10 +1,6 @@
<div id="login"> <div id="login" class="centered-ghost">
<div class="logo"> <div class="centered-content" data-base-target="layout">
<div class="image"> <div id="icinga-logo" aria-hidden="true"></div>
<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): ?> <?php if ($requiresSetup): ?>
<p class="config-note"><?= sprintf( <p class="config-note"><?= sprintf(
$this->translate( $this->translate(
@ -12,15 +8,15 @@
. 'authentication method. Please define a authentication method by following the instructions in the' . '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.' . ' %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="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 href="' . $this->href('setup') . '" title="' . $this->translate('Icinga Web 2 Setup-Wizard') . '">',
'</a>' '</a>'
); ?></p> ) ?></p>
<?php endif ?> <?php endif ?>
<?= $this->form ?> <?= $this->form ?>
<div class="footer"> <div id="login-footer">
Icinga Web 2 &copy; 2013-<?= date('Y'); ?><br><br> <p>Icinga Web 2 &copy; 2013-<?= date('Y') ?></p>
<?= $this->qlink($this->translate('The Icinga Project'), 'https://www.icinga.org'); ?> <?= $this->qlink($this->translate('The Icinga Project'), 'https://www.icinga.org') ?>
<?= $this->qlink( <?= $this->qlink(
null, null,
'http://www.twitter.com/icinga', 'http://www.twitter.com/icinga',
@ -30,7 +26,7 @@
'icon' => 'twitter', 'icon' => 'twitter',
'title' => $this->translate('Icinga on Twitter') 'title' => $this->translate('Icinga on Twitter')
) )
); ?> ) ?>
<?= $this->qlink( <?= $this->qlink(
null, null,
'http://www.facebook.com/icinga', 'http://www.facebook.com/icinga',
@ -49,7 +45,7 @@
'icon' => 'gplus-squared', 'icon' => 'gplus-squared',
'title' => $this->translate('Icinga on Google+') 'title' => $this->translate('Icinga on Google+')
) )
); ?> ) ?>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ use Zend_Paginator;
use Zend_View_Helper_PaginationControl; use Zend_View_Helper_PaginationControl;
use Icinga\Authentication\Auth; use Icinga\Authentication\Auth;
use Icinga\User; use Icinga\User;
use Icinga\Util\DirectoryIterator;
use Icinga\Util\TimezoneDetect; use Icinga\Util\TimezoneDetect;
use Icinga\Util\Translator; use Icinga\Util\Translator;
use Icinga\Web\Controller\Dispatcher; use Icinga\Web\Controller\Dispatcher;
@ -20,6 +21,7 @@ use Icinga\Web\Navigation\Navigation;
use Icinga\Web\Notification; use Icinga\Web\Notification;
use Icinga\Web\Session; use Icinga\Web\Session;
use Icinga\Web\Session\Session as BaseSession; use Icinga\Web\Session\Session as BaseSession;
use Icinga\Web\StyleSheet;
use Icinga\Web\View; use Icinga\Web\View;
/** /**
@ -98,6 +100,33 @@ class Web extends EmbeddedWeb
->setupPagination(); ->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 * Prepare routing
* *

View File

@ -61,7 +61,7 @@ if (in_array($path, $special)) {
Stylesheet::send(); Stylesheet::send();
exit; exit;
case 'css/icinga.min.css': case 'css/icinga.min.css':
Stylesheet::sendMinified(); Stylesheet::send(true);
exit; exit;
case 'js/icinga.dev.js': 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 string $string The string to test
* @param $needle string The string to match at the start of the haystack * @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 === '' || $stringSuffix = substr($string, -strlen($suffix));
(($temp = strlen($haystack) - strlen($needle)) >= 0 && false !== strpos($haystack, $needle, $temp)); return $stringSuffix !== false ? $stringSuffix === $suffix : false;
} }
/** /**

View File

@ -3,77 +3,260 @@
namespace Icinga\Web; namespace Icinga\Web;
use Icinga\Application\Config;
use Icinga\Application\Icinga;
use InvalidArgumentException;
/** /**
* Helper Class Cookie * A HTTP cookie
*/ */
class 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 * 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 * @return bool
*/ */
public function isSupported() public function isHttpOnly()
{ {
if (! empty($_COOKIE)) { return $this->httpOnly;
$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 * Set whether to protect the cookie against client side script code attempts to read the cookie
*
* @param bool $httpOnly
*
* @return $this
*/ */
public function provideCheck() public function setHttpOnly($httpOnly)
{ {
setcookie(self::CHECK_COOKIE, '1'); $this->httpOnly = $httpOnly;
return $this;
$requestUri = $this->request->getUrl()->addParams(array('_checkCookie' => 1));
$this->request->getResponse()->redirectAndExit($requestUri);
} }
/** /**
* Cleanup the cookie support check * Get the name of the cookie
*
* @return string
*/ */
public function cleanupCheck() public function getName()
{ {
if ($this->request->getUrl()->hasParam('_checkCookie') && isset($_COOKIE[self::CHECK_COOKIE])) { return $this->name;
$requestUri =$this->request->getUrl()->without('_checkCookie'); }
$this->request->getResponse()->redirectAndExit($requestUri);
} /**
* 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 isSecure()
{
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;
}
return $this->secure;
}
/**
* Set whether to send the cookie only over a secure connection
*
* @param bool $secure
*
* @return $this
*/
public function setSecure($secure)
{
$this->secure = $secure;
return $this;
}
/**
* Get the value of the cookie
*
* @return string
*/
public function getValue()
{
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 * 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|array $files file(s) to check
* @param string $match ETag to match against * @param string $match ETag to match against
* *
@ -208,9 +205,6 @@ class FileCache
if (! $match) { if (! $match) {
return false; return false;
} }
if (isset($_SERVER['HTTP_CACHE_CONTROL']) && $_SERVER['HTTP_CACHE_CONTROL'] === 'no-cache') {
return false;
}
$etag = self::etagForFiles($files); $etag = self::etagForFiles($files);
return $match === $etag ? $etag : false; 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() public static function sendMinified()
{ {
return self::send(true); self::send(true);
} }
public static function sendForIe8() public static function sendForIe8()
{ {
self::$vendorFiles = self::$ie8VendorFiles; 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) public static function send($minified = false)
{ {
header('Content-Type: application/javascript'); header('Content-Type: application/javascript');
@ -87,7 +94,10 @@ class JavaScript
} }
$files = array_merge($vendorFiles, $jsFiles); $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"); header("HTTP/1.1 304 Not Modified");
return; return;
} else { } else {
@ -99,7 +109,7 @@ class JavaScript
$cacheFile = 'icinga-' . $etag . $min . '.js'; $cacheFile = 'icinga-' . $etag . $min . '.js';
$cache = FileCache::instance(); $cache = FileCache::instance();
if ($cache->has($cacheFile)) { if (! $noCache && $cache->has($cacheFile)) {
$cache->send($cacheFile); $cache->send($cacheFile);
return; return;
} }

View File

@ -3,167 +3,170 @@
namespace Icinga\Web; namespace Icinga\Web;
use Exception; use Icinga\Application\Logger;
use RecursiveDirectoryIterator; use RecursiveArrayIterator;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
use RegexIterator;
use RecursiveRegexIterator;
use Icinga\Application\Icinga;
use lessc; 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 class LessCompiler
{ {
/**
* Collection of items: File or directories
*
* @var array
*/
private $items = array();
/** /**
* lessphp compiler * lessphp compiler
* *
* @var \lessc * @var lessc
*/ */
private $lessc; protected $lessc;
private $source;
/** /**
* 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() public function __construct()
{ {
require_once 'lessphp/lessc.inc.php'; require_once 'lessphp/lessc.inc.php';
$this->lessc = new lessc(); $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 * @return $this
*/ */
public function disableExtendedImport() public function addLessFile($lessFile)
{ {
$this->lessc->importDisabled = true; $this->lessFiles[] = $lessFile;
return $this; 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() public function compress()
{ {
$this->lessc->setPreserveComments(false);
$this->lessc->setFormatter('compressed'); $this->lessc->setFormatter('compressed');
return $this; 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; foreach ($this->lessFiles as $lessFile) {
$this->source .= file_get_contents($lessFile);
} }
public function addLoadedModules() $moduleCss = '';
{ foreach ($this->moduleLessFiles as $moduleName => $moduleLessFiles) {
foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $name => $module) { $moduleCss .= '.icinga-module.module-' . $moduleName . ' {';
$this->addModule($name, $module); foreach ($moduleLessFiles as $moduleLessFile) {
$moduleCss .= file_get_contents($moduleLessFile);
} }
return $this; $moduleCss .= '}';
} }
public function addFile($filename) $moduleCss = preg_replace(
{ '/(\.icinga-module\.module-[^\s]+) (#layout\.[^\s]+)/m',
$this->source .= "\n/* CSS: $filename */\n" '\2 \1',
. file_get_contents($filename) $moduleCss
. "\n\n"; );
return $this;
$this->source .= $moduleCss;
if ($this->theme !== null) {
$this->source .= file_get_contents($this->theme);
} }
public function compile()
{
return $this->lessc->compile($this->source); 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; 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\Application\Icinga;
use Icinga\Web\Response\JsonResponse; use Icinga\Web\Response\JsonResponse;
/**
* A HTTP response
*/
class Response extends Zend_Controller_Response_Http class Response extends Zend_Controller_Response_Http
{ {
/**
* Set of cookies which are to be sent to the client
*
* @var CookieSet
*/
protected $cookies;
/** /**
* Redirect URL * Redirect URL
* *
@ -23,6 +33,13 @@ class Response extends Zend_Controller_Response_Http
*/ */
protected $request; 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 * Whether to send the rerender layout header on XHR
* *
@ -30,6 +47,44 @@ class Response extends Zend_Controller_Response_Http
*/ */
protected $rerenderLayout = false; 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 * Get the redirect URL
* *
@ -74,6 +129,29 @@ class Response extends Zend_Controller_Response_Http
return $this->request; 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 * Get whether to send the rerender layout header on XHR
* *
@ -123,6 +201,9 @@ class Response extends Zend_Controller_Response_Http
if ($this->getRerenderLayout()) { if ($this->getRerenderLayout()) {
$this->setHeader('X-Icinga-Container', 'layout', true); $this->setHeader('X-Icinga-Container', 'layout', true);
} }
if ($this->isReloadCss()) {
$this->setHeader('X-Icinga-Reload-Css', 'now', true);
}
} else { } else {
if ($redirectUrl !== null) { if ($redirectUrl !== null) {
$this->setRedirect($redirectUrl->getAbsoluteUrl()); $this->setRedirect($redirectUrl->getAbsoluteUrl());
@ -148,12 +229,34 @@ class Response extends Zend_Controller_Response_Http
exit; 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} * {@inheritdoc}
*/ */
public function sendHeaders() public function sendHeaders()
{ {
$this->prepare(); $this->prepare();
if (! $this->getRequest()->isApiRequest()) {
$this->sendCookies();
}
return parent::sendHeaders(); return parent::sendHeaders();
} }
} }

View File

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

View File

@ -703,7 +703,7 @@ class FilterEditor extends AbstractWidget
public function renderSearch() public function renderSearch()
{ {
$html = ' <form method="post" class="search inline dontprint" action="' $html = ' <form method="post" class="search inline" action="'
. $this->preservedUrl() . $this->preservedUrl()
. '"><input type="text" name="q" style="width: 8em" class="search" value="" placeholder="' . '"><input type="text" name="q" style="width: 8em" class="search" value="" placeholder="'
. t('Search...') . 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; namespace Icinga\Module\Doc;
use CachingIterator; use CachingIterator;
use LogicException; use SplFileObject;
use SplStack; use SplStack;
use Icinga\Data\Tree\SimpleTree; use Icinga\Data\Tree\SimpleTree;
use Icinga\Exception\NotReadableError; use Icinga\Exception\NotReadableError;
use Icinga\Module\Doc\Exception\DocEmptyException; use Icinga\Util\DirectoryIterator;
use Icinga\Module\Doc\Exception\DocException; use Icinga\Module\Doc\Exception\DocException;
/** /**
@ -40,7 +40,7 @@ class DocParser
/** /**
* Iterator over documentation files * Iterator over documentation files
* *
* @var DocIterator * @var DirectoryIterator
*/ */
protected $docIterator; protected $docIterator;
@ -51,34 +51,17 @@ class DocParser
* *
* @throws DocException If the documentation directory does not exist * @throws DocException If the documentation directory does not exist
* @throws NotReadableError If the documentation directory is not readable * @throws NotReadableError If the documentation directory is not readable
* @throws DocEmptyException If the documentation directory is empty
*/ */
public function __construct($path) public function __construct($path)
{ {
if (! is_dir($path)) { if (! DirectoryIterator::isReadable($path)) {
throw new DocException( throw new DocException(
sprintf(mt('doc', 'Documentation directory \'%s\' does not exist'), $path) mt('doc', 'Documentation directory \'%s\' is not readable'),
);
}
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 $path
)
); );
} }
$this->path = $path; $this->path = $path;
$this->docIterator = $docIterator; $this->docIterator = new DirectoryIterator($path, 'md');
} }
/** /**
@ -145,9 +128,8 @@ class DocParser
public function getDocTree() public function getDocTree()
{ {
$tree = new SimpleTree(); $tree = new SimpleTree();
foreach ($this->docIterator as $fileInfo) { foreach ($this->docIterator as $filename) {
/** @var $fileInfo \SplFileInfo */ $file = new SplFileObject($filename);
$file = $fileInfo->openFile();
$lastLine = null; $lastLine = null;
$stack = new SplStack(); $stack = new SplStack();
$cachingIterator = new CachingIterator($file, CachingIterator::TOSTRING_USE_CURRENT); $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() public function render()
{ {
if (empty($this->content)) {
return '<p>' . mt('doc', 'Documentation is empty.') . '</p>';
}
$view = $this->getView(); $view = $this->getView();
$zendUrlHelper = $view->getHelper('Url'); $zendUrlHelper = $view->getHelper('Url');
foreach ($this as $section) { foreach ($this as $section) {

View File

@ -258,19 +258,4 @@ class TransportConfigForm extends ConfigForm
$this->populate($data); $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): ?> <?php if (! $this->compact): ?>
<div class="controls"> <div class="controls">
<?= $this->tabs; ?> <?= $this->tabs; ?>
<div style="float: right;" class="dontprint"> <div style="float: right;" class="dont-print">
<?= $intervalBox; ?> <?= $intervalBox; ?>
</div> </div>
<?= $this->limiter; ?> <?= $this->limiter; ?>

View File

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

View File

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

View File

@ -7,7 +7,7 @@ if (! $stats instanceof stdClass) {
$stats = $stats->fetchRow(); $stats = $stats->fetchRow();
} }
?> ?>
<div class="services-summary"> <div class="services-summary dont-print">
<span class="services-link"><?= $this->qlink( <span class="services-link"><?= $this->qlink(
sprintf($this->translatePlural( sprintf($this->translatePlural(
'%u Service', '%u Services', $stats->services_total), '%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; use Icinga\Module\Monitoring\Object\Service;
if (! $this->compact): ?> if (! $this->compact): ?>
<div class="controls separated dont-print"> <div class="controls separated">
<?= $tabs ?> <?= $tabs ?>
<?= $this->render('list/components/selectioninfo.phtml') ?> <?= $this->render('list/components/selectioninfo.phtml') ?>
<div class="grid"> <div class="grid">

View File

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

View File

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

View File

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

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 { .controls > ul.tabs {
margin-top: 0; margin-top: 0;
height: 1.5em; height: 1.5em;
background-color: @icinga-blue;
font-size: 0.75em; font-size: 0.75em;
padding: 0.2em 0 0; padding: 0.2em 0 0;
} }
@ -138,7 +137,6 @@ html {
*/ */
.container .controls { .container .controls {
top: 0; top: 0;
background-color: white;
padding: 1em 1em 0; padding: 1em 1em 0;
z-index: 100; z-index: 100;
@ -267,42 +265,9 @@ html {
margin-right: 1%; margin-right: 1%;
} }
#logo img {
/* TODO: Quickfix, this needs improvement */
width: 0 !important;
top: -100px;
position: absolute;
}
#main { #main {
left: 0; 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+ */ /*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
// Login page styles
#login { #login {
#icinga-logo {
.fadein();
}
background-color: @icinga-blue; background-color: @icinga-blue;
width: 100%;
height: 100%;
.form-controls { .control-label {
margin: 0; color: @body-bg-color;
} font-size: 1em;
line-height: @line-height;
.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;
} }
.errors { .errors {
font-size: 0.9em; background-color: @color-critical;
margin-bottom: 0.5em; color: @body-bg-color;
margin-left:auto; font-size: @font-size-small;
margin-right:auto; margin: 2em 0 0 0;
padding: 0.5em;
}
.image img { > li {
width: 355px; padding: 1em;
}
.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 {
} }
} }
.form label { .control-group {
color: @text-color-inverted; margin: 0 auto; // Center horizontally
font-weight: normal; width: 24em;
}
.control-label-group {
display: block; display: block;
line-height: 2.5em; padding: 0;
width: 15em;
margin-right: 2.5em;
text-align: left; text-align: left;
width: 100%;
} }
form { input[type=password],
margin-left: auto; input[type=text] {
margin-right: auto; background-color: @gray-light; // IE8 fallback
width: 18em; background-color: rgba(255,255,255,0.2);
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;
border: none; border: none;
border-bottom: solid 3px @body-bg-color; border-bottom: solid 3px @body-bg-color;
-webkit-transition: border 0.3s; color: @body-bg-color;
-moz-transition: border 0.3s; display: block;
-o-transition: border 0.3s; margin: 0;
transition: border 0.3s; width: 100%;
} }
form input:focus { .form-controls {
border-bottom: solid 3px @body-bg-color; margin-bottom: 2em;
background: rgba(255,255,255,0.4); margin-top: 2em;
}
ul.errors {
} }
input[type=submit] { input[type=submit] {
color: @text-color-inverted; .button(@icinga-blue, @body-bg-color);
background: none; margin: 0;
border: none; width: 100%;
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;
} }
input[type=submit]:hover, a.button:hover, input[type=submit]:focus { .config-note {
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;
color: white; color: white;
background-color: @color-critical; background-color: @color-critical;
margin: 0 auto 2em auto; // Center horizontally w/ bottom margin
max-width: 50%;
min-width: 24em;
padding: 1em;
a { a {
color: @text-color-inverted; color: @text-color-inverted;
font-weight: bold; font-weight: bold;
} }
} }
p.info-box {
width: 50%;
height: 2.2em;
margin: 2em auto 2.5em;
i.icon-info {
float: left;
height: 100%;
} }
em { #login-footer {
text-decoration: underline; color: @body-bg-color;
font-size: @font-size-small;
a {
font-weight: @font-weight-bold;
// Social links
&:hover > i {
color: @text-color;
} }
} }
} }
/* 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; } }
.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;
-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;
}

View File

@ -3,8 +3,12 @@
// Width for the name column--th--of name-value-table // Width for the name column--th--of name-value-table
@name-value-table-name-width: 14em; @name-value-table-name-width: 14em;
.action-link { .action-link(@color: @icinga-blue) {
color: @icinga-blue; color: @color;
}
.error-message {
font-weight: @font-weight-bold;
} }
.large-icon { .large-icon {
@ -58,9 +62,9 @@ a:hover > .icon-cancel {
// Link styles // Link styles
.button-link { .button-link(@background-color: @body-bg-color, @color: @icinga-blue) {
.action-link(); .action-link(@color);
.button(); .button(@background-color, @color);
display: inline-block; display: inline-block;
height: 35px; height: 35px;
line-height: 20px; line-height: 20px;
@ -164,13 +168,16 @@ a:hover > .icon-cancel {
} }
tr[href] { tr[href] {
.transition(background 0.2s ease);
&.active { &.active {
background-color: @gray-lighter; background-color: @tr-active-color;
border-left-color: @icinga-blue; border-left-color: @icinga-blue;
transition: none;
} }
&:hover { &:hover {
background-color: @gray-lightest; background-color: @tr-hover-color;
cursor: pointer; cursor: pointer;
} }
} }
@ -227,3 +234,35 @@ a:hover > .icon-cancel {
margin-left: 0.5em; 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; box-shadow: @arguments;
} }
.button() { .button(@background-color: @body-bg-color, @color: @icinga-blue) {
.rounded-corners(3px); .rounded-corners(3px);
background-color: @body-bg-color;
border: 2px solid @icinga-blue; background-color: @background-color;
color: @icinga-blue; border: 2px solid @color;
color: @color;
cursor: pointer; cursor: pointer;
padding: @vertical-padding @horizontal-padding; padding: @vertical-padding @horizontal-padding;
// Transition mixin does not work w/ comma-separated transistions
-webkit-transition: background 0.3s ease, color 0.3s ease; -webkit-transition: background 0.3s ease, color 0.3s ease;
-moz-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; -o-transition: background 0.3s ease, color 0.3s ease;
transition: background 0.3s ease, color 0.3s ease; transition: background 0.3s ease, color 0.3s ease;
&:hover { &:hover {
background-color: @icinga-blue; background-color: @color;
color: @text-color-inverted; color: @background-color;
text-decoration: none; text-decoration: none;
} }
} }
@ -79,3 +81,49 @@
.visible { .visible {
visibility: 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+ */ /*! 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; display: none !important;
} }
@ -93,4 +98,3 @@ h1 form {
body { body {
margin: 1cm 1cm 1.5cm 1cm; margin: 1cm 1cm 1.5cm 1cm;
} }

View File

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