Fix CSP violations (#5060)

This commit is contained in:
Johannes Meyer 2023-08-28 17:15:39 +02:00 committed by GitHub
commit b201b030b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 514 additions and 176 deletions

View File

@ -111,7 +111,7 @@ jobs:
- name: Setup dependencies
run: |
composer require -n --no-progress mockery/mockery ipl/i18n:@dev
composer require -n --no-progress mockery/mockery ipl/i18n:@dev ipl/web:@dev
git clone --depth 1 --branch snapshot/nightly https://github.com/Icinga/icinga-php-thirdparty.git vendor/icinga-php-thirdparty
- name: PHPUnit

View File

@ -262,7 +262,7 @@ class RoleForm extends RepositoryForm
PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
)))),
'description' => $spec['description'],
'style' => $isUnrestricted ? 'text-decoration:line-through;' : '',
'class' => $isUnrestricted ? 'unrestricted-role' : '',
'readonly' => $isUnrestricted ?: null
]
)

View File

@ -24,7 +24,15 @@ $iframeClass = $isIframe ? ' iframe' : '';
$innerLayoutScript = $this->layout()->innerLayout . '.phtml';
?><!DOCTYPE html>
<html class="no-js<?= $iframeClass ?>" lang="<?= $lang ?>">
<html
class="no-js<?= $iframeClass ?>" lang="<?= $lang ?>"
data-icinga-window-name="<?= $this->protectId('Icinga') ?>"
data-icinga-timezone="<?= $timezone ?>"
data-icinga-base-url="<?= $this->baseUrl(); ?>"
<?php if ($isIframe): ?>
data-icinga-is-iframe
<?php endif ?>
>
<head>
<meta charset="utf-8">
<meta name="google" value="notranslate">
@ -39,12 +47,6 @@ $innerLayoutScript = $this->layout()->innerLayout . '.phtml';
<base target="_parent">
<?php else: ?>
<base href="<?= $this->baseUrl(); ?>/">
<script type="text/javascript">
(function() {
var html = document.getElementsByTagName('html')[0];
html.className = html.className.replace(/no-js/, 'js');
}());
</script>
<?php endif ?>
<link rel="stylesheet" href="<?= $this->href($cssfile) ?>" media="all" type="text/css" />
<link type="image/png" rel="shortcut icon" href="<?= $this->baseUrl('img/favicon.png') ?>" />
@ -55,25 +57,6 @@ $innerLayoutScript = $this->layout()->innerLayout . '.phtml';
<div id="layout" class="default-layout<?php if ($showFullscreen): ?> fullscreen-layout<?php endif ?>">
<?= $this->render($innerLayoutScript); ?>
</div>
<script type="text/javascript">
(function() {
if (document.defaultView && document.defaultView.getComputedStyle) {
var matched;
var html = document.getElementsByTagName('html')[0];
var element = document.getElementById('layout');
var name = document.defaultView
.getComputedStyle(html)['font-family']
.replace(/['",]/g, '');
if (null !== (matched = name.match(/^([a-z]+)-layout$/))) {
element.className = element.className.replace('default-layout', name);
if ('object' === typeof window.console) {
window.console.log('Icinga Web 2: setting initial layout to ' + name);
}
}
}
}());
</script>
<div id="collapsible-control-ghost" class="collapsible-control">
<button type="button">
<!-- TODO: Accessibility attributes are missing since usage of the Icon class -->
@ -95,13 +78,6 @@ $innerLayoutScript = $this->layout()->innerLayout . '.phtml';
</div>
</div>
<script type="text/javascript" src="<?= $this->href($jsfile) ?>"></script>
<script type="text/javascript">
window.name = '<?= $this->protectId('Icinga') ?>';
var icinga = new Icinga({
baseUrl: '<?= $this->baseUrl(); ?>',
locale: '<?= $lang; ?>',
timezone: '<?= $timezone ?>'
});
</script>
<script type="text/javascript" src="<?= $this->href('js/bootstrap.js') ?>"></script>
</body>
</html>

View File

@ -1,3 +1,8 @@
<?php
use Icinga\Util\Csp;
?>
<!--
This view provides a workaround to logout from an external authentication provider, in case external
authentication was configured (the default is to handle authentications internally in Icingaweb2).
@ -9,44 +14,24 @@
-->
<div class="content">
<div id="icinga-logo" aria-hidden="true"></div>
<div class="alert alert-warning" id="logout-status">
<div class="alert alert-warning" id="logout-in-progress">
<b><?= $this->translate('Logging out...'); ?></b>
<br>
<?= $this->translate(
'If this message does not disappear, it might be necessary to quit the'
. ' current session manually by clearing the cache, or by closing the current'
. ' browser session.'
); ?>
<p>
<?= $this->translate(
'If this message does not disappear, it might be necessary to quit the'
. ' current session manually by clearing the cache, or by closing the current'
. ' browser session.'
); ?>
</p>
</div>
<div id="logout-successful" class="alert alert-success" hidden><?= $this->translate('Logout successful'); ?></div>
<div class="container">
<a href="<?= $this->href('dashboard'); ?>"><?= $this->translate('Login'); ?></a>
</div>
</div>
<script type="text/javascript">
/*
* When JavaScript is available, trigger an XmlHTTPRequest with the non-existing user 'logout' and abort it
* before it is able to finish. This will cause the browser to show a new authentication prompt in the next
* request.
*/
document.addEventListener('DOMContentLoaded', function () {
var msg = document.getElementById('logout-status');
try {
if (navigator.userAgent.toLowerCase().indexOf('msie') !== -1) {
document.execCommand('ClearAuthenticationCache');
} else {
var xhttp = new XMLHttpRequest();
xhttp.open('GET', 'arbitrary url', true, 'logout', 'logout');
xhttp.send('');
xhttp.abort();
}
} catch (e) {
}
msg.innerHTML = '<?= $this->translate('Logout successful!'); ?>';
msg.className = 'alert alert-success';
});
</script>
<style type="text/css">
<script type="text/javascript" src="<?= $this->href('js/logout.js'); ?>"></script>
<style type="text/css" nonce="<?= Csp::getStyleNonce(); ?>">
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background-color: #0095bf;
@ -66,7 +51,7 @@
width: 100%;
}
#logout-status {
#logout-in-progress {
margin: 2em 0 1em;
font-size: 2em;
font-weight: bold;

View File

@ -7,20 +7,20 @@
<table class="avp action" data-base-target="_next">
<thead>
<tr>
<th style="width: 18em;">
<th>
<strong><?= t('Dashlet Name') ?></strong>
</th>
<th>
<strong><?= t('Url') ?></strong>
</th>
<th style="width: 1.4em;">&nbsp;</th>
<th class="icon-col">&nbsp;</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->dashboard->getPanes() as $pane): ?>
<?php if ($pane->getDisabled()) continue; ?>
<tr>
<th colspan="2" style="text-align: left; padding: 0.5em;">
<th colspan="2">
<?php if ($pane->isUserWidget()): ?>
<?= $this->qlink(
$pane->getName(),
@ -63,7 +63,7 @@
array('title' => sprintf($this->translate('Edit dashlet %s'), $dashlet->getTitle()))
); ?>
</td>
<td style="table-layout: fixed; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<td>
<?= $this->qlink(
$dashlet->getUrl()->getRelativeUrl(),
$dashlet->getUrl()->getRelativeUrl(),

View File

@ -13,8 +13,8 @@
<tbody>
<?php foreach ($this->logData as $value): ?>
<?php $datetime = new Datetime($value->datetime) ?>
<tr class="state">
<td style="width: 6em; text-align: center">
<tr>
<td>
<?= $this->escape($datetime->format('d.m. H:i')) ?><br />
<?= $this->escape($value->loglevel) ?>
</td>

View File

@ -17,9 +17,9 @@ if (! $this->compact): ?>
<table class="table-row-selectable common-table">
<thead>
<th><?= $this->translate('Shared Navigation'); ?></th>
<th style="width: 10em"><?= $this->translate('Type'); ?></th>
<th style="width: 10em"><?= $this->translate('Owner'); ?></th>
<th style="width: 5em"><?= $this->translate('Unshare'); ?></th>
<th><?= $this->translate('Type'); ?></th>
<th><?= $this->translate('Owner'); ?></th>
<th><?= $this->translate('Unshare'); ?></th>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
@ -53,7 +53,7 @@ if (! $this->compact): ?>
)
); ?></td>
<?php else: ?>
<td data-base-target="_self"><?= $removeForm
<td data-base-target="_self" class="remove-nav-item"><?= $removeForm
->setDefault('name', $item->name)
->setAction(Url::fromPath(
'navigation/unshare',

View File

@ -75,6 +75,7 @@ class Test extends Cli
// Conflicts with `Tests\Icinga\Module\...\Lib`. But it seems it's not needed anyway...
//$this->getLoader()->registerNamespace('Tests', $this->getBaseDir('test/php/library'));
$this->getLoader()->registerNamespace('Tests\\Icinga\\Lib', $this->getBaseDir('test/php/Lib'));
return $this;
}

View File

@ -112,7 +112,7 @@ class BarGraph extends Styleable implements Drawable
$rect->setAttribute('data-icinga-graph-index', $index);
}
$rect->setAttribute('data-icinga-graph-type', 'bar');
$rect->setAdditionalStyle('clip-path: url(#clip);');
$rect->setAdditionalStyle(['clip-path' => 'url(#clip)']);
return $rect;
}

View File

@ -165,8 +165,8 @@ class LineGraph extends Styleable implements Drawable
$path->setFill($this->fill);
}
$path->setAdditionalStyle('clip-path: url(#clip);');
$path->setId($this->id);
$path->setAdditionalStyle(['clip-path' => 'url(#clip)']);
$path->setId($this->id ?? uniqid('line-graph-'));
$group = $path->toSvg($ctx);
foreach ($this->dataset as $x => $point) {
@ -180,7 +180,7 @@ class LineGraph extends Styleable implements Drawable
if (isset($this->tooltips[$x])) {
$invisible = new Circle($point[0], $point[1], 20);
$invisible->setFill($this->strokeColor);
$invisible->setAdditionalStyle('opacity: 0.0;');
$invisible->setAdditionalStyle(['opacity' => '0.0']);
$data = array(
'label' => isset($this->graphs[$this->order]['label']) ?
strtolower($this->graphs[$this->order]['label']) : '',

View File

@ -4,6 +4,7 @@
namespace Icinga\Chart\Primitive;
use DOMDocument;
use DOMElement;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Format;
@ -61,8 +62,23 @@ class Circle extends Styleable implements Drawable
$circle->setAttribute('cx', Format::formatSVGNumber($coords[0]));
$circle->setAttribute('cy', Format::formatSVGNumber($coords[1]));
$circle->setAttribute('r', $this->radius);
$circle->setAttribute('style', $this->getStyle());
$id = $this->id ?? uniqid('circle-');
$circle->setAttribute('id', $id);
$this->setId($id);
$this->applyAttributes($circle);
$style = new DOMDocument();
$style->loadHTML($this->getStyle());
$circle->appendChild(
$circle->ownerDocument->importNode(
$style->getElementsByTagName('style')->item(0),
true
)
);
return $circle;
}
}

View File

@ -3,6 +3,7 @@
namespace Icinga\Chart\Primitive;
use DOMDocument;
use DOMElement;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Format;
@ -80,8 +81,23 @@ class Line extends Styleable implements Drawable
$line->setAttribute('x2', Format::formatSVGNumber($x2));
$line->setAttribute('y1', Format::formatSVGNumber($y1));
$line->setAttribute('y2', Format::formatSVGNumber($y2));
$line->setAttribute('style', $this->getStyle());
$id = $this->id ?? uniqid('line-');
$line->setAttribute('id', $id);
$this->setId($id);
$this->applyAttributes($line);
$style = new DOMDocument();
$style->loadHTML($this->getStyle());
$line->appendChild(
$line->ownerDocument->importNode(
$style->getElementsByTagName('style')->item(0),
true
)
);
return $line;
}
}

View File

@ -3,6 +3,7 @@
namespace Icinga\Chart\Primitive;
use DOMDocument;
use DOMElement;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Format;
@ -162,12 +163,24 @@ class Path extends Styleable implements Drawable
}
$path = $doc->createElement('path');
if ($this->id) {
$path->setAttribute('id', $this->id);
}
$id = $this->id ?? uniqid('path-');
$path->setAttribute('id', $id);
$this->setId($id);
$path->setAttribute('d', $pathDescription);
$path->setAttribute('style', $this->getStyle());
$this->applyAttributes($path);
$style = new DOMDocument();
$style->loadHTML($this->getStyle());
$path->appendChild(
$path->ownerDocument->importNode(
$style->getElementsByTagName('style')->item(0),
true
)
);
$group->appendChild($path);
return $group;
}

View File

@ -3,6 +3,7 @@
namespace Icinga\Chart\Primitive;
use DOMDocument;
use DOMElement;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Format;
@ -279,9 +280,22 @@ class PieSlice extends Animatable implements Drawable
$slicePath = $doc->createElement('path');
$slicePath->setAttribute('d', $this->getPieSlicePath($x, $y, $r));
$slicePath->setAttribute('style', $this->getStyle());
$slicePath->setAttribute('data-icinga-graph-type', 'pieslice');
$id = $this->id ?? uniqid('slice-');
$slicePath->setAttribute('id', $id);
$this->setId($id);
$style = new DOMDocument();
$style->loadHTML($this->getStyle());
$slicePath->appendChild(
$slicePath->ownerDocument->importNode(
$style->getElementsByTagName('style')->item(0),
true
)
);
$this->applyAttributes($slicePath);
$group->appendChild($slicePath);
if ($this->caption != "") {

View File

@ -4,6 +4,7 @@
namespace Icinga\Chart\Primitive;
use DOMElement;
use DOMDocument;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Format;
@ -95,11 +96,24 @@ class Rect extends Animatable implements Drawable
$rect->setAttribute('y', Format::formatSVGNumber($y));
$rect->setAttribute('width', Format::formatSVGNumber($width));
$rect->setAttribute('height', Format::formatSVGNumber($height));
$rect->setAttribute('style', $this->getStyle());
$id = $this->id ?? uniqid('rect-');
$rect->setAttribute('id', $id);
$this->setId($id);
$this->applyAttributes($rect);
$this->appendAnimation($rect, $ctx);
$style = new DOMDocument();
$style->loadHTML($this->getStyle());
$rect->appendChild(
$rect->ownerDocument->importNode(
$style->getElementsByTagName('style')->item(0),
true
)
);
return $rect;
}
}

View File

@ -5,6 +5,8 @@
namespace Icinga\Chart\Primitive;
use DOMElement;
use Icinga\Util\Csp;
use ipl\Web\Style;
/**
* Base class for stylable drawables
@ -36,14 +38,14 @@ class Styleable
/**
* Additional styles to be appended to the style attribute
*
* @var string
* @var array<string, string>
*/
public $additionalStyle = '';
public $additionalStyle = [];
/**
* The id of this element
*
* @var string
* @var ?string
*/
public $id = null;
@ -83,7 +85,7 @@ class Styleable
/**
* Set additional styles for this drawable
*
* @param string $styles The styles to set additionally
* @param array<string, string> $styles The styles to set additionally
*
* @return $this Fluid interface
*/
@ -121,15 +123,20 @@ class Styleable
}
/**
* Return the content of the style attribute as a string
* Return the ruleset used for styling the DOMNode
*
* @return string A string containing styles
* @return Style A ruleset containing styles
*/
public function getStyle()
{
$base = sprintf("fill: %s; stroke: %s;stroke-width: %s;", $this->fill, $this->strokeColor, $this->strokeWidth);
$base .= ';' . $this->additionalStyle . ';';
return $base;
$styles = $this->additionalStyle;
$styles['fill'] = $this->fill;
$styles['stroke'] = $this->strokeColor;
$styles['stroke-width'] = (string) $this->strokeWidth;
return (new Style())
->setNonce(Csp::getStyleNonce())
->add("#$this->id", $styles);
}
/**

View File

@ -4,10 +4,12 @@
namespace Icinga\Chart\Primitive;
use DOMDocument;
use DOMElement;
use DOMText;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Format;
use ipl\Html\HtmlDocument;
/**
* Wrapper for the SVG text element
@ -97,6 +99,15 @@ class Text extends Styleable implements Drawable
$this->y = $y;
$this->text = $text;
$this->fontSize = $fontSize;
$this->setAdditionalStyle([
'font-size' => $this->fontSize,
'font-family' => 'Ubuntu, Calibri, Trebuchet MS, Helvetica, Verdana, sans-serif',
'font-weight' => $this->fontWeight,
'font-stretch' => $this->fontStretch,
'font-style' => 'normal',
'text-anchor' => $this->alignment
]);
}
/**
@ -150,19 +161,24 @@ class Text extends Styleable implements Drawable
list($x, $y) = $ctx->toAbsolute($this->x, $this->y);
$text = $ctx->getDocument()->createElement('text');
$text->setAttribute('x', Format::formatSVGNumber($x - 15));
$text->setAttribute(
'style',
$this->getStyle()
. ';font-size:' . $this->fontSize
. '; font-family: Ubuntu, Calibri, Trebuchet MS, Helvetica, Verdana, sans-serif'
. ';font-weight: ' . $this->fontWeight
. ';font-stretch: ' . $this->fontStretch
. '; font-style: normal;'
. 'text-anchor: ' . $this->alignment
);
$id = $this->id ?? uniqid('text-');
$text->setAttribute('id', $id);
$this->setId($id);
$text->setAttribute('y', Format::formatSVGNumber($y));
$text->appendChild(new DOMText($this->text));
$style = new DOMDocument();
$style->loadHTML($this->getStyle());
$text->appendChild(
$text->ownerDocument->importNode(
$style->getElementsByTagName('style')->item(0),
true
)
);
return $text;
}
}

View File

@ -4,8 +4,10 @@
namespace Icinga\Test {
use Exception;
use Icinga\Util\Csp;
use Icinga\Web\Request;
use Icinga\Web\Response;
use Icinga\Web\Session;
use ipl\I18n\NoopTranslator;
use ipl\I18n\StaticTranslator;
use RuntimeException;
@ -14,6 +16,7 @@ namespace Icinga\Test {
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Data\Db\DbConnection;
use Tests\Icinga\Lib\FakeSession;
/**
* Class BaseTestCase
@ -108,6 +111,8 @@ namespace Icinga\Test {
parent::setUp();
$this->setupRequestMock();
$this->setupResponseMock();
Session::create(new FakeSession());
Csp::createNonce();
StaticTranslator::$instance = new NoopTranslator();
}

View File

@ -50,6 +50,6 @@ class Announcements extends AbstractWidget
return $html;
}
// Force container update on XHR
return '<div style="display: none;"></div>';
return '<div hidden></div>';
}
}

View File

@ -39,14 +39,14 @@ class ApplicationStateMessages extends AbstractWidget
}
if (! (bool) $enabled) {
return '<div style="display: none;"></div>';
return '<div hidden></div>';
}
$active = $this->getMessages();
if (empty($active)) {
// Force container update on XHR
return '<div style="display: none;"></div>';
return '<div hidden></div>';
}
$html = '<div>';

View File

@ -6,7 +6,9 @@ namespace Icinga\Web\Widget\Chart;
use DateInterval;
use DateTime;
use Icinga\Util\Color;
use Icinga\Util\Csp;
use Icinga\Web\Widget\AbstractWidget;
use ipl\Web\Style;
/**
* Display a colored grid that visualizes a set of values for each day
@ -32,6 +34,9 @@ class HistoryColorGrid extends AbstractWidget
private $color;
public $opacity = 1.0;
/** @var array<string, array<string, string>> History grid css rulesets */
protected $rulesets = [];
public function __construct($color = '#51e551', $start = null, $end = null)
{
$this->setColor($color);
@ -123,18 +128,30 @@ class HistoryColorGrid extends AbstractWidget
{
if (array_key_exists($day, $this->data) && $this->data[$day]['value'] > 0) {
$entry = $this->data[$day];
return '<a ' .
'style="background-color: ' . $this->calculateColor($entry['value']) . ';'
. ' opacity: ' . $this->opacity . ';" ' .
'aria-label="' . $entry['caption'] . '" ' .
'title="' . $entry['caption'] . '" ' .
'href="' . $entry['url'] . '" ' .
'></a>';
$this->rulesets['.grid-day-with-entry-' . $entry['value']] = [
'background-color' => $this->calculateColor($entry['value']),
'opacity' => $this->opacity
];
return '<a class="grid-day-with-entry-'
. $entry['value']
. '" '
. 'aria-label="' . $entry['caption']
. '" '
. 'title="' . $entry['caption']
. '" '
. 'href="' . $entry['url']
. '" '
. '"></a>';
} else {
return '<span ' .
'style="background-color: ' . $this->calculateColor(0) . '; opacity: ' . $this->opacity . ';" ' .
'title="No entries for ' . $day . '" ' .
'></span>';
if (! isset($this->rulesets['.grid-day-no-entry'])) {
$this->rulesets['.grid-day-no-entry'] = [
'background-color' => $this->calculateColor(0),
'opacity' => $this->opacity
];
}
return '<span class="grid-day-no-entry"' . ' title="No entries for ' . $day . '"></span>';
}
}
@ -366,8 +383,18 @@ class HistoryColorGrid extends AbstractWidget
}
$grid = $this->createGrid();
if ($this->orientation === self::ORIENTATION_HORIZONTAL) {
return $this->renderHorizontal($grid);
$html = $this->renderHorizontal($grid);
} else {
$html = $this->renderVertical($grid);
}
return $this->renderVertical($grid);
$historyGridStyle = new Style();
$historyGridStyle->setNonce(Csp::getStyleNonce());
foreach ($this->rulesets as $selector => $properties) {
$historyGridStyle->add($selector, $properties);
}
return $html . $historyGridStyle;
}
}

View File

@ -471,7 +471,7 @@ class FilterEditor extends AbstractWidget
}
if ($this->addTo && $this->addTo == $filter->getId()) {
$parts[] = '<li style="background: #ffb">' . $this->renderNewFilter() .$this->cancelLink(). '</li>';
$parts[] = '<li class="new-filter">' . $this->renderNewFilter() .$this->cancelLink(). '</li>';
}
$class = $level === 0 ? ' class="datafilter"' : '';
@ -575,7 +575,7 @@ class FilterEditor extends AbstractWidget
$this->elementId('operator', $filter),
$ops,
$filter === null ? null : $filter->getOperatorName(),
array('style' => 'width: 5em')
['class' => 'filter-operator']
);
}
@ -594,7 +594,7 @@ class FilterEditor extends AbstractWidget
$this->elementId('sign', $filter),
$signs,
$filter === null ? null : $filter->getSign(),
array('style' => 'width: 4em')
['class' => 'filter-rule']
);
}
@ -738,7 +738,7 @@ class FilterEditor extends AbstractWidget
$html = ' <form method="post" class="search inline" action="'
. $preservedUrl
. '"><input type="text" name="q" style="width: 8em" class="search" value="" placeholder="'
. '"><input type="text" name="q" class="search search-input" value="" placeholder="'
. t('Search...')
. '" /></form>';
@ -779,7 +779,7 @@ class FilterEditor extends AbstractWidget
. '<form action="'
. Url::fromRequest()
. '" class="editor" method="POST">'
. '<input type="submit" name="submit" value="Apply" style="display:none;"/>'
. '<input type="submit" name="submit" value="Apply" hidden/>'
. '<ul class="tree"><li>'
. $this->renderFilter($this->filter)
. '</li></ul>'

View File

@ -52,7 +52,7 @@ EOT;
* @var string
*/
private $closeTpl = <<< 'EOT'
<li style="float: right;">
<li class="close-container-btn">
<a href="#" title="{TITLE}" aria-label="{TITLE}" class="close-container-control">
<i aria-hidden="true" class="icon-cancel"></i>
</a>

View File

@ -5,11 +5,10 @@
<div class="content">
<?php foreach ($this->font->glyphs as $icon): ?>
<!-- TODO: move CSS away //-->
<div style="width: 33%; font-size: 1.5em; height: 2em; float: left;" class="<?=
<div class="icon <?=
$this->font->css_prefix_text . $icon->css
?>">
<?= $this->escape($icon->css) ?> <span style="font-size: 0.6em">(0x<?= dechex($icon->code) ?>)</span>
<?= $this->escape($icon->css) ?> <span class="icon-code">(0x<?= dechex($icon->code) ?>)</span>
</div>
<?php endforeach ?>
</div>

View File

@ -107,3 +107,14 @@ th {
text-align: left;
text-transform: uppercase;
}
.icon {
width: 33%;
font-size: 1.5em;
height: 2em;
float: left;
}
.icon-code {
font-size: 0.6em;
}

View File

@ -63,7 +63,7 @@ class Zend_View_Helper_PluginOutput extends Zend_View_Helper_Abstract
protected static $htmlReplacements = array(
"\t",
"\n",
'<table style="font-size: 0.75em"'
'<table class="output-table"'
);
/** @var \Icinga\Module\Monitoring\Web\Helper\PluginOutputHookRenderer */

View File

@ -112,9 +112,9 @@ for ($i = 0; $i < $diff->days; $i += $step) {
$f->add(new DateInterval('P' . $step . 'D'));
}
?>
<div style="width: 33.5em;">
<div class="event-grid">
<?php foreach (array_reverse($grids) as $key => $grid): ?>
<div style=" <?= $this->orientation === 'horizontal' ? '' : 'display: inline-block; vertical-align: top; top; margin: 0.5em;' ?>">
<div class=" <?= $this->orientation === 'horizontal' ? '' : 'vertical' ?>">
<?= $grid; ?>
<?= $this->orientation === 'horizontal' ? '<br />' : '' ?>
</div>

View File

@ -16,7 +16,7 @@
<table class="name-value-table">
<tbody>
<tr>
<th style="width: 20%"></th>
<th></th>
<td><strong><?= $this->escape($contact->contact_alias) ?></strong> (<?= $contact->contact_name ?>)</td>
</tr>
<?php if ($contact->contact_email): ?>

View File

@ -1,9 +1,15 @@
<?php
use Icinga\Util\Csp;
use Icinga\Web\Url;
use Icinga\Util\Color;
use ipl\Web\Style;
$groupInfo = $timeline->getGroupInfo();
$firstRow = ! $beingExtended;
$timelineStyle = (new Style())
->setNonce(Csp::getStyleNonce())
->setModule('monitoring');
if (! $beingExtended && !$this->compact): ?>
<div class="controls">
@ -79,24 +85,72 @@ if (! $beingExtended && !$this->compact): ?>
<?php foreach ($groupInfo as $groupName => $labelAndColor): ?>
<?php if (array_key_exists($groupName, $timeInfo[1])): ?>
<?php
$styleId = uniqid();
$circleWidth = $timeline->calculateCircleWidth($timeInfo[1][$groupName], 2);
$extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$groupName], 2);
?>
<?php if ($firstRow && $extrapolatedCircleWidth !== $circleWidth): ?>
<div class="circle-box" style="width: <?= $extrapolatedCircleWidth; ?>;">
<div class="outer-circle extrapolated <?= $timeInfo[1][$groupName]->getClass() ?>" style="<?= sprintf(
'width: %2$s; height: %2$s; margin-top: -%1$Fem;',
(float) substr($extrapolatedCircleWidth, 0, -2) / 2,
$extrapolatedCircleWidth
); ?>">
<?php
$timelineStyle->add(
"#circle-box-$styleId",
['width' => $extrapolatedCircleWidth]
);
$timelineStyle->add(
"#outer-circle-$styleId",
[
'width' => $extrapolatedCircleWidth,
'height' => $extrapolatedCircleWidth,
'margin-top' => sprintf(
'-%Fem',
(float)substr($extrapolatedCircleWidth, 0, -2) / 2
)
]
);
?>
<div id="circle-box-<?= $styleId ?>" class="circle-box">
<div id="outer-circle-<?= $styleId ?>" class="outer-circle extrapolated <?= $timeInfo[1][$groupName]->getClass() ?>">
<?php else: ?>
<div class="circle-box" style="width: <?= $circleWidth; ?>;">
<div class="outer-circle" style="<?= sprintf(
'width: %2$s; height: %2$s; margin-top: -%1$Fem;',
(float) substr($circleWidth, 0, -2) / 2,
$circleWidth
); ?>">
<?php
$timelineStyle->add(
"#circle-box-$styleId",
['width' => $circleWidth]
);
$timelineStyle->add(
"#outer-circle-$styleId",
[
'width' => $circleWidth,
'height' => $circleWidth,
'margin-top' => sprintf(
'-%Fem',
(float)substr($circleWidth, 0, -2) / 2
)
]
);
?>
<div id="circle-box-<?= $styleId ?>" class="circle-box">
<div id="outer-circle-<?= $styleId ?>" class="outer-circle">
<?php endif ?>
<?php
$timelineStyle->add(
"#inner-circle-$styleId",
[
'width' => $circleWidth,
'height' => $circleWidth,
'margin-top' => sprintf(
'-%Fem',
(float)substr($circleWidth, 0, -2) / 2
),
'margin-left' => sprintf(
'-%Fem',
(float)substr($circleWidth, 0, -2) / 2
),
]
);
?>
<?= $this->qlink(
'',
$timeInfo[1][$groupName]->getDetailUrl(),
@ -112,12 +166,8 @@ $extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$g
strtolower($labelAndColor['label']),
$titleTime
),
'class' => 'inner-circle ' . $timeInfo[1][$groupName]->getClass(),
'style' => sprintf(
'width: %2$s; height: %2$s; margin-top: -%1$Fem; margin-left: -%1$Fem;',
(float) substr($circleWidth, 0, -2) / 2,
(string) $circleWidth
)
'id' => "inner-circle-$styleId",
'class' => "inner-circle " . $timeInfo[1][$groupName]->getClass()
)
); ?>
</div>
@ -143,3 +193,4 @@ $extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$g
</div>
</div>
<?php endif ?>
<?= $timelineStyle; ?>

View File

@ -427,5 +427,6 @@ $dashboard->add(
/*
* CSS
*/
$this->provideCssFile('event-grid.less');
$this->provideCssFile('service-grid.less');
$this->provideCssFile('tables.less');

View File

@ -0,0 +1,9 @@
.event-grid {
width: 33.5em;
.vertical {
display: inline-block;
vertical-align: top;
margin: 0.5em;
}
}

View File

@ -654,6 +654,9 @@ form.instance-features span.description, form.object-features span.description {
.plugin-output {
border-left: 5px solid @gray-lighter;
padding: 0.66em 0.33em;
.output-table {
font-size: 0.75em;
}
.state-critical {
background-color: @color-critical;

View File

@ -1,12 +1,17 @@
<?php
use Icinga\Util\Csp;
use Icinga\Web\Notification;
use ipl\Web\Style;
$pages = $wizard->getPages();
$finished = isset($success);
$configPages = array_slice($pages, 3, count($pages) - 4, true);
$currentPos = array_search($wizard->getCurrentPage(), $pages, true);
list($configPagesLeft, $configPagesRight) = array_chunk($configPages, (int)(count($configPages) / 2), true);
$setupStyle = (new Style())
->setSelector('.setup-header > .progress-bar')
->setNonce(Csp::getStyleNonce());
$visitedPages = array_keys($wizard->getPageData());
$maxProgress = max(array_merge([0], array_keys(array_filter(
@ -14,12 +19,19 @@ $maxProgress = max(array_merge([0], array_keys(array_filter(
function ($page) use ($visitedPages) { return in_array($page->getName(), $visitedPages); }
))));
$setupStyle->add(
'.width-percent-10',
['width' => '10%']
)->add(
'.width-percent-60',
['width' => '60%']
);
?>
<div id="setup-content-wrapper" data-base-target="layout">
<div class="setup-header">
<?= $this->img('img/icinga-logo-big.png'); ?>
<div class="progress-bar">
<div class="step" style="width: 10%;">
<div class="step width-percent-10">
<h1><?= $this->translate('Welcome', 'setup.progress'); ?></h1>
<?php $stateClass = $finished || $currentPos > 0 ? 'complete' : (
$maxProgress > 0 ? 'visited' : 'active'
@ -30,7 +42,7 @@ $maxProgress = max(array_merge([0], array_keys(array_filter(
<td class="right"><div class="line right <?= $stateClass; ?>"></div></td>
</tr></tbody></table>
</div>
<div class="step" style="width: 10%;">
<div class="step width-percent-10">
<h1><?= $this->translate('Modules', 'setup.progress'); ?></h1>
<?php $stateClass = $finished || $currentPos > 1 ? ' complete' : (
$maxProgress > 1 ? ' visited' : (
@ -46,7 +58,7 @@ $maxProgress = max(array_merge([0], array_keys(array_filter(
<?= $this->restartForm ?>
<?php endif ?>
</div>
<div class="step" style="width: 10%;">
<div class="step width-percent-10">
<h1><?= $this->translate('Requirements', 'setup.progress'); ?></h1>
<?php $stateClass = $finished || $currentPos > 2 ? ' complete' : (
$maxProgress > 2 ? ' visited' : (
@ -62,7 +74,7 @@ $maxProgress = max(array_merge([0], array_keys(array_filter(
<?= $this->restartForm ?>
<?php endif ?>
</div>
<div class="step" style="width: 60%;">
<div class="step width-percent-60">
<h1><?= $this->translate('Configuration', 'setup.progress'); ?></h1>
<table><tbody><tr>
<td class="left">
@ -76,14 +88,43 @@ $maxProgress = max(array_merge([0], array_keys(array_filter(
$pos < $maxProgress ? ' visited' : ($currentPos > 2 ? ' active' : '')
); ?>
<?php if ($page === $firstPage): ?>
<div class="line left<?= $stateClass; ?>" style="float: left; width: <?= sprintf(
'%.2F',
100 - (count($configPagesLeft) - 1) * $lineWidth
); ?>%; margin-right: 0"></div>
<?php
$setupStyle->add(
'.step .left-line-' . $pos,
[
'float' => 'left',
'width' => sprintf(
'%.2F%%',
100 - (count($configPagesLeft) - 1) * $lineWidth
),
'margin-right' => 0
]
);
?>
<div class="line left<?= $stateClass; ?> left-line-<?= $pos; ?>"></div>
<?php elseif ($page === $lastPage): ?>
<div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%; margin-right: -0.1em;"></div>
<?php
$setupStyle->add(
'.step .left-line-' . $pos,
[
'float' => 'left',
'width' => $lineWidth . '%',
'margin-right' => '-0.1em'
]
);
?>
<div class="line<?= $stateClass; ?> left-line-<?= $pos; ?>"></div>
<?php else: ?>
<div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%;"></div>
<?php
$setupStyle->add(
'.step .left-line-' . $pos,
[
'float' => 'left',
'width' => $lineWidth . '%'
]
);
?>
<div class="line<?= $stateClass; ?> left-line-<?= $pos; ?>"></div>
<?php endif ?>
<?php endforeach ?>
</td>
@ -106,14 +147,43 @@ $maxProgress = max(array_merge([0], array_keys(array_filter(
$pos < $maxProgress ? ' visited' : ($currentPos > 2 ? ' active' : '')
); ?>
<?php if ($page === $firstPage): ?>
<div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%; margin-left: -0.1em;"></div>
<?php
$setupStyle->add(
'.step .right-line-' . $pos,
[
'float' => 'left',
'width' => $lineWidth . '%',
'margin-right' => '-0.1em'
]
);
?>
<div class="line<?= $stateClass; ?> right-line-<?= $pos; ?>"></div>
<?php elseif ($page === $lastPage): ?>
<div class="line right<?= $stateClass; ?>" style="float: left; width: <?= sprintf(
'%.2F',
100 - (count($configPagesRight) - 1) * $lineWidth
); ?>%; margin-left: 0;"></div>
<?php
$setupStyle->add(
'.step .right-line-' . $pos,
[
'float' => 'left',
'width' => sprintf(
'%.2F%%',
100 - (count($configPagesRight) - 1) * $lineWidth
),
'margin-right' => 0
]
);
?>
<div class="line right<?= $stateClass; ?> right-line-<?= $pos; ?>"></div>
<?php else: ?>
<div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%;"></div>
<?php
$setupStyle->add(
'.step .right-line-' . $pos,
[
'float' => 'left',
'width' => $lineWidth . '%'
]
);
?>
<div class="line<?= $stateClass; ?> right-line-<?= $pos; ?>"></div>
<?php endif ?>
<?php endforeach ?>
</td>
@ -122,7 +192,7 @@ $maxProgress = max(array_merge([0], array_keys(array_filter(
<?= $this->restartForm ?>
<?php endif ?>
</div>
<div class="step" style="width: 10%;">
<div class="step width-percent-10">
<h1><?= $this->translate('Finish', 'setup.progress'); ?></h1>
<?php $stateClass = $finished ? ' complete' : ($pages[$currentPos] === end($pages) ? ' active' : ''); ?>
<table><tbody><tr>
@ -151,3 +221,4 @@ $maxProgress = max(array_merge([0], array_keys(array_filter(
}
?></ul>
</div>
<?= $setupStyle; ?>

View File

@ -26,6 +26,7 @@ parameters:
ignoreErrors:
- '#Unsafe usage of new static\(\)#'
- '#. but return statement is missing#'
- '#Cannot call method importNode\(\) on DOMDocument\|null.#'
# ldap_connect() returns `LDAP\Connection` in php >= 81
-

View File

@ -48,3 +48,7 @@
.primary-nav a {
font-weight: 500;
}
.remove-nav-item {
padding-left: 2em;
}

View File

@ -103,3 +103,7 @@
#layout.twocols .tabs > li > .close-container-control {
display: block;
}
.close-container-btn {
float: right;
}

View File

@ -208,6 +208,10 @@ table.multiselect tr[href] td {
}
#main div.filter {
.search.search-input {
width: 8em;
}
form.editor {
input[type=text], select {
width: 12em;
@ -238,6 +242,20 @@ table.multiselect tr[href] td {
.buttons input:not(:last-child) {
margin-right:.5em;
}
ul.tree {
.filter-operator {
width: 5em;
}
.new-filter {
background: #ffb;
}
.filter-rule {
width: 4em;
}
}
}
}
@ -251,6 +269,10 @@ form.role-form {
font-style: normal;
}
.unrestricted-role {
text-decoration: line-through;
}
.control-label > * {
display: inline-block;
}
@ -641,3 +663,4 @@ ul.tree li a.error:hover {
html.no-js .progress-label {
display: none;
}

28
public/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,28 @@
;(function () {
let html = document.documentElement;
window.name = html.dataset.icingaWindowName;
window.icinga = new Icinga({
baseUrl: html.dataset.icingaBaseUrl,
locale: html.lang,
timezone: html.dataset.icingaTimezone
});
if (! ('icingaIsIframe' in document.documentElement.dataset)) {
html.classList.replace('no-js', 'js');
}
if (window.getComputedStyle) {
let matched;
let element = document.getElementById('layout');
let name = window
.getComputedStyle(html)['font-family']
.replace(/['",]/g, '');
if (null !== (matched = name.match(/^([a-z]+)-layout$/))) {
element.classList.replace('default-layout', name);
if ('object' === typeof window.console) {
window.console.log('Icinga Web 2: setting initial layout to ' + name);
}
}
}
})();

View File

@ -28,7 +28,7 @@
var _this = e.data.self;
$('#layout').append(
'<div id="application-state" class="container" style="display: none" data-icinga-url="'
'<div id="application-state" class="container" hidden data-icinga-url="'
+ _this.icinga.loader.baseUrl
+ '/application-state" data-icinga-refresh="60"></div>'
);

16
public/js/logout.js Normal file
View File

@ -0,0 +1,16 @@
;(function () {
/**
* When JavaScript is available, trigger an XmlHTTPRequest with the non-existing user 'logout' and abort it
* before it is able to finish. This will cause the browser to show a new authentication prompt in the next
* request.
*/
document.getElementById('logout-in-progress').hidden = true;
document.getElementById('logout-successful').hidden = false;
try {
var xhttp = new XMLHttpRequest();
xhttp.open('GET', 'arbitrary url', true, 'logout', 'logout');
xhttp.send('');
xhttp.abort();
} catch (e) {
}
})();

View File

@ -0,0 +1,29 @@
<?php
namespace Tests\Icinga\Lib;
use Icinga\Web\Session\Session;
class FakeSession extends Session
{
public function read()
{
}
public function exists()
{
}
public function purge()
{
}
public function refreshId()
{
}
public function getId()
{
return '1234567890';
}
}

View File

@ -120,8 +120,6 @@ class FormTest extends BaseTestCase
*/
public function testWhetherACsrfCounterMeasureIsBeingAdded()
{
Mockery::mock('alias:Icinga\Web\Session')->shouldReceive('getSession->getId')->andReturn('1234567890');
$form = new Form();
$form->create();