mirror of
https://github.com/Icinga/icingaweb2.git
synced 2025-07-31 01:34:09 +02:00
Compare commits
63 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0070c807a7 | ||
|
8c5ffa1c61 | ||
|
5f602113ee | ||
|
a9cbc36213 | ||
|
e2e9078d97 | ||
|
122ac8f600 | ||
|
451ec34775 | ||
|
8345993480 | ||
|
09df8dcc5a | ||
|
5d71d09e54 | ||
|
fff524624e | ||
|
8f518e7bab | ||
|
092571a17b | ||
|
d56757f20f | ||
|
750948a729 | ||
|
9a9113bd2b | ||
|
63a73eab6f | ||
|
aa7a60c893 | ||
|
0eea5cae51 | ||
|
85621487e8 | ||
|
5333f2deb7 | ||
|
b16cf68c86 | ||
|
eafb6c95b7 | ||
|
05f9e7c5ee | ||
|
6c57d32979 | ||
|
6100281c89 | ||
|
91396aeb73 | ||
|
025ae944c5 | ||
|
68c97fb6ff | ||
|
1b363360f6 | ||
|
380b3688bd | ||
|
ef4b59123e | ||
|
2d265babf7 | ||
|
ff04a2ea43 | ||
|
6c7f1e5466 | ||
|
219b11789b | ||
|
41bbf6e35d | ||
|
e98a776509 | ||
|
1ddd04df50 | ||
|
15e74ebb0c | ||
|
53fa6d57e1 | ||
|
c07a45096c | ||
|
01fb35dd4a | ||
|
ec40efe157 | ||
|
aad020511f | ||
|
484bd26d63 | ||
|
2b08d88edf | ||
|
191444ccd9 | ||
|
1a1f96be49 | ||
|
f1fe2525bd | ||
|
d56d10c712 | ||
|
6c8453062f | ||
|
db851bbe33 | ||
|
d86ede517f | ||
|
92dad17a2b | ||
|
13c9a73842 | ||
|
acfad5ae52 | ||
|
14c0748693 | ||
|
02dece2a35 | ||
|
c6c1e28350 | ||
|
79971cb1a6 | ||
|
ca2778eb46 | ||
|
4eadfd0ace |
15
.github/workflows/L10n-update.yml
vendored
15
.github/workflows/L10n-update.yml
vendored
@ -6,15 +6,6 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
trigger-update:
|
||||
name: L10n Update Trigger
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Repository dispatch
|
||||
uses: peter-evans/repository-dispatch@v1
|
||||
with:
|
||||
token: ${{ secrets.ICINGABOT_TOKEN }}
|
||||
repository: Icinga/L10n
|
||||
event-type: update
|
||||
client-payload: '{"origin": "${{ github.repository }}", "commit": "${{ github.sha }}"}'
|
||||
update:
|
||||
uses: icinga/github-actions/.github/workflows/L10n-update.yml@main
|
||||
secrets: inherit
|
||||
|
14
.github/workflows/php.yml
vendored
14
.github/workflows/php.yml
vendored
@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- support/*
|
||||
- release/*
|
||||
pull_request:
|
||||
branches:
|
||||
@ -17,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
|
||||
php: ['8.2', '8.3', '8.4']
|
||||
os: ['ubuntu-latest']
|
||||
|
||||
steps:
|
||||
@ -46,16 +47,13 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
env:
|
||||
phpunit-version: 9.5
|
||||
phpunit-version: 9.6
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
|
||||
php: ['8.2', '8.3', '8.4']
|
||||
os: ['ubuntu-latest']
|
||||
include:
|
||||
- php: '7.2'
|
||||
phpunit-version: 8.5
|
||||
|
||||
services:
|
||||
mysql:
|
||||
@ -100,7 +98,9 @@ jobs:
|
||||
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
composer require -n --no-progress mockery/mockery ipl/i18n:@dev ipl/web:@dev
|
||||
composer init -n --require mockery/mockery:* --require ipl/i18n:@dev --require ipl/web:@dev
|
||||
composer config platform.php 8.2
|
||||
composer install -n --no-progress
|
||||
git clone --depth 1 --branch snapshot/nightly https://github.com/Icinga/icinga-php-thirdparty.git vendor/icinga-php-thirdparty
|
||||
|
||||
- name: PHPUnit
|
||||
|
2
.mailmap
2
.mailmap
@ -17,6 +17,7 @@ Florian Strohmaier <florian.strohmaier@icinga.com> <hello@florianstrohmaier.com>
|
||||
Florian Strohmaier <florian.strohmaier@icinga.com> <florian.strohmaier@me.com>
|
||||
Gunnar Beutner <gunnar.beutner@netways.de> <gunnar@beutner.name>
|
||||
Jannis Moßhammer <jannis.mosshammer@netways.de>
|
||||
Jan Schuppik <jan.schuppik@icinga.com> <114286749+Jan-Schuppik@users.noreply.github.com>
|
||||
Johannes Meyer <johannes.meyer@icinga.com> <johannes.meyer@netways.de>
|
||||
Jennifer Mourek <jennifer.mourek@icinga.com> <jennifer.mourek@netways.de>
|
||||
Marius Hein <marius.hein@netways.de> <mhein@itsocks.de>
|
||||
@ -42,5 +43,6 @@ Thomas Gelf <thomas.gelf@icinga.com> <thomas@gelf.net>
|
||||
Tobias Bauriedel <tobias.bauriedel@netways.de> <tobias@bauriedel.de>
|
||||
Yonas Habteab <yonas.habteab@icinga.com> <yonas.habteab@netways.de>
|
||||
Ravi Kumar Kempapura Srinivasa <ravi.srinivasa@icinga.com> <33730024+raviks789@users.noreply.github.com>
|
||||
Ravi Kumar Kempapura Srinivasa <ravi.srinivasa@icinga.com> <raviks789@gmail.com>
|
||||
Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com> <54990055+sukhwinder33445@users.noreply.github.com>
|
||||
Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com> <sukhwinder33445@gmail.com>
|
||||
|
4
AUTHORS
4
AUTHORS
@ -54,10 +54,12 @@ Ian Shearin <ishearin@womply.com>
|
||||
ignasr <ignas.linux@gmail.com>
|
||||
Janne Heß <janne@hess.ooo>
|
||||
Jannis Moßhammer <jannis.mosshammer@netways.de>
|
||||
Jan Schuppik <jan.schuppik@icinga.com>
|
||||
Jennifer Mourek <jennifer.mourek@icinga.com>
|
||||
Jiri Pejchal <jiri.pejchal@gmail.com>
|
||||
Joe Doherty <git@pjuu.com>
|
||||
Johannes Meyer <johannes.meyer@icinga.com>
|
||||
Johannes Rauh <johannes.rauh@icinga.com>
|
||||
Joonas Kylmälä <joonas.kylmala@kirjastot.fi>
|
||||
Jorge Vallecillo <jorgevallecilloc@gmail.com>
|
||||
Jo Rhett <jo@chegg.com>
|
||||
@ -73,6 +75,7 @@ Marc DeTrano <marc@gridshield.net>
|
||||
Marcel Weinberg <marcel.weinberg@secucloud.com>
|
||||
Marcus Cobden <marcus@marcuscobden.co.uk>
|
||||
Marian Rainer-Harbach <marian@rainer-harbach.at>
|
||||
marianrh <19990392+marianrh@users.noreply.github.com>
|
||||
Mario Rimann <mario@rimann.org>
|
||||
Marius Hein <marius.hein@netways.de>
|
||||
Markus Frosch <markus.frosch@icinga.com>
|
||||
@ -125,6 +128,7 @@ Rune Darrud <theflyingcorpse@gmail.com>
|
||||
Russell Kubik <russkubik@3d-p.com>
|
||||
Sander Ferdinand <sa.ferdinand@gmail.com>
|
||||
sant-swedge <simon.wedge@sant.ox.ac.uk>
|
||||
Silas <67681686+Tqnsls@users.noreply.github.com>
|
||||
Simone Orsi <simahawk@users.noreply.github.com>
|
||||
ss23 <stephen@zxsecurity.co.nz>
|
||||
Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com>
|
||||
|
75
CHANGELOG.md
75
CHANGELOG.md
@ -4,9 +4,80 @@ Please make sure to always read our [Upgrading](doc/80-Upgrading.md) documentati
|
||||
|
||||
## What's New
|
||||
|
||||
### What's New in Version 2.12.5
|
||||
|
||||
You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/85?closed=1).
|
||||
|
||||
#### PHP 8.4 Support
|
||||
|
||||
We're again a little behind schedule, but now we support PHP 8.4! This means that installations on Ubuntu 25.04 and
|
||||
Fedora 42+ can now install Icinga Web without worrying about PHP related incompatibilities. Icinga packages will be
|
||||
available in the next few days.
|
||||
|
||||
#### Good Things Take Time
|
||||
|
||||
There's only a single (notable) recent issue that is fixed with this release. All the others are a bit older.
|
||||
|
||||
* External URLs set up as dashlets are not *embedded* the same as navigation items [#5346](https://github.com/Icinga/icingaweb2/issues/5346)
|
||||
|
||||
But the team sat together a few weeks ago and fixed a bug here and there. And of course, also in Icinga Web!
|
||||
|
||||
* Users who are not allowed to change the theme, cannot change the theme mode either [#5385](https://github.com/Icinga/icingaweb2/issues/5385)
|
||||
* Filtering for older-than events with relative time does not work [#5263](https://github.com/Icinga/icingaweb2/issues/5263)
|
||||
* External logout not working from the navigation dashboard [#5000](https://github.com/Icinga/icingaweb2/issues/5000)
|
||||
* Empty values are NULL in CSV exports [#5350](https://github.com/Icinga/icingaweb2/issues/5350)
|
||||
|
||||
#### Breaking, Somewhat
|
||||
|
||||
This is mainly for developers.
|
||||
|
||||
With the support of PHP 8.4, we introduced a new environment variable, `ICINGAWEB_ENVIRONMENT`. Unless set to `dev`,
|
||||
Icinga Web will not show nor log deprecation notices anymore.
|
||||
|
||||
### What's New in Version 2.12.4
|
||||
|
||||
This is a hotfix release which fixes the following issue:
|
||||
|
||||
Database login broken after upgrade [#5343](https://github.com/Icinga/icingaweb2/issues/5343)
|
||||
|
||||
### What's New in Version 2.12.3
|
||||
|
||||
**Notice:** This is a security release. It is recommended to upgrade _immediately_.
|
||||
|
||||
You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/83?closed=1).
|
||||
|
||||
#### Vulnerabilities, Closed
|
||||
|
||||
Cross site scripting is one of the worst attacks on web based platforms. Especially, if carrying it out is as easy as
|
||||
the first two mentioned here. You might recognize the open redirect on the login. You are correct, we attempted to fix
|
||||
it already with v2.11.3 but underestimated PHP's quirks. The last is difficult to exploit, hence the lowest severity
|
||||
of all, but don't be fooled by that!
|
||||
|
||||
All four of them are backported to v2.11.5.
|
||||
|
||||
* XSS in embedded content [CVE-2025-27405](https://github.com/Icinga/icingaweb2/security/advisories/GHSA-3x37-fjc3-ch8w)
|
||||
* DOM-based XSS [CVE-2025-27404](https://github.com/Icinga/icingaweb2/security/advisories/GHSA-c6pg-h955-wf66)
|
||||
* Open redirect on login page [CVE-2025-30164](https://github.com/Icinga/icingaweb2/security/advisories/GHSA-8r73-6686-wv8q)
|
||||
* Reflected XSS [CVE-2025-27609](https://github.com/Icinga/icingaweb2/security/advisories/GHSA-5cjw-fwjc-8j38)
|
||||
|
||||
Big thanks to all finders / reporters! :+1:
|
||||
|
||||
#### Bugs, Exterminated
|
||||
|
||||
Did you know, that we started [Icinga Notifications](https://icinga.com/docs/icinga-notifications/latest/) with support
|
||||
for PostgreSQL first? Reason for that is, we wanted to make sure we are fully compatible with it right away. To ensure
|
||||
things like logging in with a PostgreSQL authentication/group backend is case-insensitive, like it was always the case
|
||||
for MySQL. Now it **really** is case-insensitive! There are also two issues fixed, which many of you will probably have
|
||||
noticed since v2.12.2, sorry that it took so long :)
|
||||
|
||||
* Login against Postgres DB is case-sensitive [#5223](https://github.com/Icinga/icingaweb2/issues/5223)
|
||||
* Role list has no functioning quick search [#5300](https://github.com/Icinga/icingaweb2/issues/5300)
|
||||
* After clicking on Check now, the page does not refresh itself [#5293](https://github.com/Icinga/icingaweb2/issues/5293)
|
||||
* Service States display wrong since update to 2.12.2 [#5290](https://github.com/Icinga/icingaweb2/issues/5290)
|
||||
|
||||
### What's New in Version 2.12.2
|
||||
|
||||
You can find all issues related to this release on our Roadmap.
|
||||
You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/81?closed=1).
|
||||
|
||||
#### General Fixes
|
||||
|
||||
@ -62,7 +133,7 @@ but it’s now slightly improved.
|
||||
|
||||
### What's New in Version 2.12.1
|
||||
|
||||
You can find all issues related to this release on our Roadmap.
|
||||
You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/80?closed=1).
|
||||
|
||||
#### PHP 8.3 Support
|
||||
|
||||
|
@ -3,18 +3,108 @@
|
||||
|
||||
namespace Icinga\Controllers;
|
||||
|
||||
use Icinga\Web\Controller;
|
||||
use Icinga\Web\Session;
|
||||
use ipl\Html\BaseHtmlElement;
|
||||
use ipl\Html\Html;
|
||||
use ipl\Html\HtmlString;
|
||||
use ipl\Html\Text;
|
||||
use ipl\Web\Compat\CompatController;
|
||||
use ipl\Web\Url;
|
||||
use ipl\Web\Widget\Icon;
|
||||
use ipl\Web\Widget\Tabs;
|
||||
|
||||
/**
|
||||
* Display external or internal links within an iframe
|
||||
*/
|
||||
class IframeController extends Controller
|
||||
class IframeController extends CompatController
|
||||
{
|
||||
/**
|
||||
* Display iframe w/ the given URL
|
||||
*/
|
||||
public function indexAction()
|
||||
public function indexAction(): void
|
||||
{
|
||||
$this->view->url = $this->params->getRequired('url');
|
||||
$url = Url::fromPath($this->params->getRequired('url'));
|
||||
$urlHash = $this->getRequest()->getHeader('X-Icinga-URLHash');
|
||||
$expectedHash = hash('sha256', $url->getAbsoluteUrl() . Session::getSession()->getId());
|
||||
$iframeUrl = Url::fromPath('iframe', ['url' => $url->getAbsoluteUrl()]);
|
||||
|
||||
if (! in_array($url->getScheme(), ['http', 'https'], true)) {
|
||||
$this->httpBadRequest('Invalid URL scheme');
|
||||
}
|
||||
|
||||
$this->injectTabs();
|
||||
|
||||
$this->getTabs()->setRefreshUrl($iframeUrl);
|
||||
|
||||
if ($urlHash) {
|
||||
if ($urlHash !== $expectedHash) {
|
||||
$this->httpBadRequest('Invalid URL hash');
|
||||
}
|
||||
} else {
|
||||
$this->addContent(Html::tag('div', ['class' => 'iframe-warning'], [
|
||||
Html::tag('h2', $this->translate('Attention!')),
|
||||
Html::tag('p', ['class' => 'note'], $this->translate(
|
||||
'You are about to open untrusted content embedded in Icinga Web! Only proceed,'
|
||||
.' by clicking the link below, if you recognize and trust the source!'
|
||||
)),
|
||||
Html::tag('a', ['data-url-hash' => $expectedHash, 'href' => Html::escape($iframeUrl)], $url),
|
||||
Html::tag('p', ['class' => 'reason'], [
|
||||
new Icon('circle-info'),
|
||||
Text::create($this->translate(
|
||||
'You see this warning because you do not seem to have followed a link in Icinga Web.'
|
||||
. ' You can bypass this in the future by configuring a navigation item instead.'
|
||||
))
|
||||
])
|
||||
]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getTabs()->setHash($expectedHash);
|
||||
|
||||
$this->addContent(Html::tag(
|
||||
'div',
|
||||
['class' => 'iframe-container'],
|
||||
Html::tag('iframe', [
|
||||
'src' => $url,
|
||||
'sandbox' => 'allow-same-origin allow-scripts allow-popups allow-forms',
|
||||
])
|
||||
));
|
||||
}
|
||||
|
||||
private function injectTabs(): void
|
||||
{
|
||||
$this->tabs = new class extends Tabs {
|
||||
private $hash;
|
||||
|
||||
public function setHash($hash)
|
||||
{
|
||||
$this->hash = $hash;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function assemble()
|
||||
{
|
||||
$tabHtml = substr($this->tabs->render(), 34, -5);
|
||||
if ($this->refreshUrl !== null) {
|
||||
$tabHtml = preg_replace(
|
||||
[
|
||||
'/(?<=class="refresh-container-control spinner" href=")([^"]*)/',
|
||||
'/(\s)(?=href)/'
|
||||
],
|
||||
[
|
||||
$this->refreshUrl->getAbsoluteUrl(),
|
||||
' data-url-hash="' . $this->hash . '" '
|
||||
],
|
||||
$tabHtml
|
||||
);
|
||||
}
|
||||
|
||||
BaseHtmlElement::add(HtmlString::create($tabHtml));
|
||||
}
|
||||
};
|
||||
|
||||
$this->controls->setTabs($this->tabs);
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +133,18 @@ class MigrationForm extends CompatForm
|
||||
. ' that has the appropriate credentials to resolve this issue.'
|
||||
),
|
||||
implode(', ', $mm->getRequiredDatabasePrivileges())
|
||||
))),
|
||||
new HtmlElement('br'),
|
||||
new HtmlElement('br'),
|
||||
new HtmlElement('span', null, Text::create(sprintf(
|
||||
$this->translate(
|
||||
'The database name may contain either an underscore or a percent sign.'
|
||||
. ' In MySQL these characters represent a wildcard. If part of a database name,'
|
||||
. ' they might not have been escaped when manually granting privileges.'
|
||||
. ' Privileges might not be detected in this case. Check the documentation and'
|
||||
. ' update your grants accordingly: %s'
|
||||
),
|
||||
'https://dev.mysql.com/doc/refman/8.0/en/grant.html#grant-quoting'
|
||||
)))
|
||||
)
|
||||
);
|
||||
|
@ -184,15 +184,6 @@ use ipl\Web\Widget\StateBadge;
|
||||
</div>
|
||||
<div class="about-social">
|
||||
<?= $this->qlink(
|
||||
null,
|
||||
'https://www.twitter.com/icinga',
|
||||
null,
|
||||
array(
|
||||
'target' => '_blank',
|
||||
'icon' => 'twitter',
|
||||
'title' => $this->translate('Icinga on Twitter')
|
||||
)
|
||||
) ?> <?= $this->qlink(
|
||||
null,
|
||||
'https://www.facebook.com/icinga',
|
||||
null,
|
||||
|
@ -28,18 +28,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<ul id="social">
|
||||
<li>
|
||||
<?= $this->qlink(
|
||||
null,
|
||||
'https://twitter.com/icinga',
|
||||
null,
|
||||
array(
|
||||
'target' => '_blank',
|
||||
'icon' => 'twitter',
|
||||
'title' => $this->translate('Icinga on Twitter')
|
||||
)
|
||||
) ?>
|
||||
</li>
|
||||
<li>
|
||||
<?= $this->qlink(
|
||||
null,
|
||||
|
@ -6,7 +6,7 @@
|
||||
<?= $this->tabs->render($this); ?>
|
||||
<br/>
|
||||
<div>
|
||||
<h1>Could not <?= $action; ?> module "<?= $moduleName; ?>"</h1>
|
||||
<h1>Could not <?= $action; ?> module "<?= $this->escape($moduleName); ?>"</h1>
|
||||
<p>
|
||||
While operation the following error occurred:
|
||||
<br />
|
||||
|
@ -23,7 +23,7 @@ $modReason = [];
|
||||
|
||||
if (isset($requiredVendor, $requiredProject) && $requiredVendor && $requiredProject) {
|
||||
// TODO: I don't like this, can we define requirements somewhere else?
|
||||
$coreDeps = ['icinga-php-library' => '>= 0.13.2', 'icinga-php-thirdparty' => '>= 0.12'];
|
||||
$coreDeps = ['icinga-php-library' => '>= 0.14.2', 'icinga-php-thirdparty' => '>= 0.12'];
|
||||
|
||||
foreach ($coreDeps as $libraryName => $requiredVersion) {
|
||||
if (! $libraries->has($libraryName)) {
|
||||
|
@ -1,8 +0,0 @@
|
||||
<?php if (! $compact): ?>
|
||||
<div class="controls">
|
||||
<?= $tabs ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<div class="iframe-container">
|
||||
<iframe src="<?= $this->escape($url) ?>" frameborder="no"></iframe>
|
||||
</div>
|
@ -8,6 +8,7 @@ $searchDashboard->setUser($this->Auth()->getUser());
|
||||
|
||||
if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?>
|
||||
<form action="<?= $this->href('search') ?>" method="get" role="search" class="search-control">
|
||||
<i class="icon fa-search fa search-icon"></i>
|
||||
<input type="text" name="q" id="search" class="search search-input" required
|
||||
placeholder="<?= $this->translate('Search') ?> …"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
|
@ -399,7 +399,7 @@ You will need to install certain dependencies depending on your setup:
|
||||
monitor your infrastructure
|
||||
* A web server, e.g. Apache or Nginx
|
||||
* PHP version ≥ 7.2
|
||||
* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥ 0.13.2)
|
||||
* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥ 0.14.2)
|
||||
* [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (≥ 0.12)
|
||||
* The following PHP modules must be installed: cURL, json, gettext, fileinfo, intl, dom, OpenSSL and xml
|
||||
* The [pdfexport](https://github.com/Icinga/icingaweb2-module-pdfexport) module (≥0.10) is required for the
|
||||
|
@ -3,6 +3,16 @@
|
||||
Specific version upgrades are described below. Please note that upgrades are incremental. An upgrade from
|
||||
v2.6 to v2.8 requires to follow the instructions for v2.7 too.
|
||||
|
||||
## Upgrading to Icinga Web 2.13
|
||||
|
||||
**Breaking changes**
|
||||
|
||||
* The following columns of the `Servicestatus` table, which previously displayed the date time (string) as a fetched value, now display the unix timestamp to support relative time filters:
|
||||
* `service_last_time_ok`
|
||||
* `service_last_time_unknown`
|
||||
* `service_last_time_warning`
|
||||
* `service_last_time_critical`
|
||||
|
||||
## Upgrading to Icinga Web 2.12.2
|
||||
|
||||
**Framework changes affecting third-party code**
|
||||
|
@ -58,8 +58,8 @@ Disabled by default.
|
||||
If you experience any problems while running SELinux in enforcing mode try to reproduce it in permissive mode. If the
|
||||
problem persists, it is not related to SELinux because in permissive mode SELinux will not deny anything.
|
||||
|
||||
When filing a bug report please add the following information additionally to the
|
||||
[common ones](https://icinga.com/icinga/faq/):
|
||||
When filing a bug report please add the following information additionally to the common ones:
|
||||
|
||||
* Output of `semodule -l | grep -e icinga2 -e icingaweb2 -e nagios -e apache`
|
||||
* Output of `semanage boolean -l | grep icinga`
|
||||
* Output of `ps -eZ | grep httpd`
|
||||
|
@ -568,7 +568,7 @@ abstract class ApplicationBootstrap
|
||||
*/
|
||||
protected function setupErrorHandling()
|
||||
{
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
error_reporting(getenv('ICINGAWEB_ENVIRONMENT') === 'dev' ? E_ALL : E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
|
||||
ini_set('display_startup_errors', 1);
|
||||
ini_set('display_errors', 1);
|
||||
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
|
||||
@ -579,7 +579,6 @@ abstract class ApplicationBootstrap
|
||||
switch ($errno) {
|
||||
case E_NOTICE:
|
||||
case E_WARNING:
|
||||
case E_STRICT:
|
||||
case E_RECOVERABLE_ERROR:
|
||||
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ namespace Icinga\Application;
|
||||
*/
|
||||
class Version
|
||||
{
|
||||
const VERSION = '2.12.2';
|
||||
const VERSION = '2.12.5';
|
||||
|
||||
/**
|
||||
* Get the version of this instance of Icinga Web 2
|
||||
|
@ -8,7 +8,7 @@ use Icinga\Web\Controller\StaticController;
|
||||
use Icinga\Web\JavaScript;
|
||||
use Icinga\Web\StyleSheet;
|
||||
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
|
||||
|
||||
if (isset($_SERVER['REQUEST_URI'])) {
|
||||
$ruri = $_SERVER['REQUEST_URI'];
|
||||
|
@ -42,4 +42,9 @@ class RolesConfig extends IniRepository
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
protected function initializeSearchColumns(): array
|
||||
{
|
||||
return ['name'];
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use Icinga\Exception\AuthenticationException;
|
||||
use Icinga\Repository\DbRepository;
|
||||
use Icinga\User;
|
||||
use PDO;
|
||||
use Zend_Db_Expr;
|
||||
|
||||
class DbUserBackend extends DbRepository implements UserBackendInterface, Inspectable
|
||||
{
|
||||
@ -179,23 +180,28 @@ class DbUserBackend extends DbRepository implements UserBackendInterface, Inspec
|
||||
{
|
||||
if ($this->ds->getDbType() === 'pgsql') {
|
||||
// Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape'
|
||||
$columns = array('password_hash' => 'ENCODE(password_hash, \'escape\')');
|
||||
$columns = ['password_hash' => new Zend_Db_Expr('ENCODE(password_hash, \'escape\')')];
|
||||
} else {
|
||||
$columns = array('password_hash');
|
||||
// password_hash is intentionally not a valid query column,
|
||||
// by wrapping it in an expression it is not validated
|
||||
$columns = ['password_hash' => new Zend_Db_Expr('password_hash')];
|
||||
}
|
||||
|
||||
$nameColumn = 'name';
|
||||
if ($this->ds->getDbType() === 'mysql') {
|
||||
$username = strtolower($username);
|
||||
$nameColumn = 'BINARY LOWER(name)';
|
||||
}
|
||||
|
||||
$query = $this->ds->select()
|
||||
->from($this->prependTablePrefix('user'), $columns)
|
||||
->where($nameColumn, $username)
|
||||
$query = $this
|
||||
->select()
|
||||
->from('user', $columns)
|
||||
->where('active', true);
|
||||
|
||||
$statement = $this->ds->getDbAdapter()->prepare($query->getSelectQuery());
|
||||
if ($this->ds->getDbType() === 'mysql') {
|
||||
$username = strtolower($username);
|
||||
$nameColumn = new Zend_Db_Expr('BINARY LOWER(name)');
|
||||
|
||||
$query->getQuery()->where($nameColumn, $username);
|
||||
} else { // pgsql
|
||||
$query->where('user', $username);
|
||||
}
|
||||
|
||||
$statement = $this->ds->getDbAdapter()->prepare($query->getQuery()->getSelectQuery());
|
||||
$statement->execute();
|
||||
$statement->bindColumn(1, $lob, PDO::PARAM_LOB);
|
||||
$statement->fetch(PDO::FETCH_BOUND);
|
||||
|
@ -204,7 +204,7 @@ class DbUserGroupBackend extends DbRepository implements Inspectable, UserGroupB
|
||||
$membershipQuery = $this
|
||||
->select()
|
||||
->from('group_membership', array('group_name'))
|
||||
->where('user_name', $user->getUsername());
|
||||
->where('user', $user->getUsername());
|
||||
|
||||
$memberships = array();
|
||||
foreach ($membershipQuery as $membership) {
|
||||
|
@ -37,7 +37,7 @@ class Csv
|
||||
}
|
||||
$out = array();
|
||||
foreach ($row as & $val) {
|
||||
$out[] = '"' . ($val ? str_replace('"', '""', $val) : '') . '"';
|
||||
$out[] = '"' . ($val == '0' ? '0' : ($val ? str_replace('"', '""', $val) : '')) . '"';
|
||||
}
|
||||
$csv .= implode(',', $out) . "\r\n";
|
||||
}
|
||||
|
@ -5,9 +5,11 @@ namespace Icinga\File;
|
||||
|
||||
use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
use Exception;
|
||||
use Icinga\Application\Icinga;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Util\Environment;
|
||||
use Icinga\Web\FileCache;
|
||||
use Icinga\Web\Hook;
|
||||
use Icinga\Web\Url;
|
||||
|
||||
@ -64,8 +66,17 @@ class Pdf
|
||||
return;
|
||||
}
|
||||
|
||||
$tmpDir = FileCache::instance()->directory('legacy_pdf');
|
||||
if ($tmpDir === false) {
|
||||
throw new Exception('Could not create temporary directory for PDF rendering');
|
||||
}
|
||||
|
||||
$options = new Options();
|
||||
$options->set('defaultPaperSize', 'A4');
|
||||
$options->set('fontDir', $tmpDir);
|
||||
$options->set('fontCache', $tmpDir);
|
||||
$options->set('tempDir', $tmpDir);
|
||||
$options->set('chroot', $tmpDir);
|
||||
$dompdf = new Dompdf($options);
|
||||
$dompdf->loadHtml($html);
|
||||
$dompdf->render();
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
namespace Icinga\Util;
|
||||
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
* Retrieve timezone information from cookie
|
||||
*/
|
||||
@ -15,13 +17,6 @@ class TimezoneDetect
|
||||
*/
|
||||
private static $success;
|
||||
|
||||
/**
|
||||
* Timezone offset in minutes
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $offset = 0;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
@ -34,13 +29,6 @@ class TimezoneDetect
|
||||
*/
|
||||
public static $cookieName = 'icingaweb2-tzo';
|
||||
|
||||
/**
|
||||
* Timezone name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $timezone;
|
||||
|
||||
/**
|
||||
* Create new object and try to identify the timezone
|
||||
*/
|
||||
@ -50,31 +38,14 @@ class TimezoneDetect
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_key_exists(self::$cookieName, $_COOKIE)) {
|
||||
$matches = array();
|
||||
if (preg_match('/\A(-?\d+)[\-,](\d+)\z/', $_COOKIE[self::$cookieName], $matches)) {
|
||||
$offset = $matches[1];
|
||||
$timezoneName = timezone_name_from_abbr('', (int) $offset, (int) $matches[2]);
|
||||
|
||||
self::$success = (bool) $timezoneName;
|
||||
if (self::$success) {
|
||||
self::$offset = $offset;
|
||||
self::$timezoneName = $timezoneName;
|
||||
}
|
||||
}
|
||||
if (in_array($_COOKIE[self::$cookieName] ?? null, DateTimeZone::listIdentifiers(), true)) {
|
||||
self::$timezoneName = $_COOKIE[self::$cookieName];
|
||||
self::$success = true;
|
||||
} else {
|
||||
self::$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offset
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOffset()
|
||||
{
|
||||
return self::$offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone name
|
||||
*
|
||||
@ -102,6 +73,5 @@ class TimezoneDetect
|
||||
{
|
||||
self::$success = null;
|
||||
self::$timezoneName = null;
|
||||
self::$offset = 0;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ namespace Icinga\Web;
|
||||
use Icinga\Application\Logger;
|
||||
use Icinga\Authentication\Auth;
|
||||
use Icinga\Web\Navigation\Navigation;
|
||||
use Icinga\Web\Navigation\Renderer\RecursiveMenuNavigationRenderer;
|
||||
|
||||
/**
|
||||
* Main menu for Icinga Web 2
|
||||
@ -149,4 +150,14 @@ class Menu extends Navigation
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return the renderer for this navigation
|
||||
*
|
||||
* @return RecursiveMenuNavigationRenderer
|
||||
*/
|
||||
public function getRenderer()
|
||||
{
|
||||
return new RecursiveMenuNavigationRenderer($this);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use Icinga\Application\Icinga;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Util\StringHelper;
|
||||
use Icinga\Web\Navigation\NavigationItem;
|
||||
use Icinga\Web\Session;
|
||||
use Icinga\Web\Url;
|
||||
use Icinga\Web\View;
|
||||
|
||||
@ -190,6 +191,10 @@ class NavigationItemRenderer
|
||||
|
||||
$target = $item->getTarget();
|
||||
if ($url->isExternal() && (!$target || in_array($target, $this->internalLinkTargets, true))) {
|
||||
$item->setAttribute('data-url-hash', hash(
|
||||
'sha256',
|
||||
$url->getAbsoluteUrl() . Session::getSession()->getId()
|
||||
));
|
||||
$url = Url::fromPath('iframe', array('url' => $url));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/* Icinga Web 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
namespace Icinga\Web\Navigation\Renderer;
|
||||
|
||||
use Icinga\Web\Navigation\NavigationItem;
|
||||
|
||||
/**
|
||||
* Renderer for the multi level navigation in the sidebar menu
|
||||
*/
|
||||
class RecursiveMenuNavigationRenderer extends RecursiveNavigationRenderer
|
||||
{
|
||||
public function beginChildren(): void
|
||||
{
|
||||
parent::beginChildren();
|
||||
|
||||
$parentItem = $this->getInnerIterator()->current()->getParent();
|
||||
$item = new NavigationItem($parentItem->getName());
|
||||
$item->setLabel($parentItem->getLabel());
|
||||
$item->setCssClass('nav-item-header');
|
||||
|
||||
$renderer = new NavigationItemRenderer();
|
||||
$renderer->setEscapeLabel(false);
|
||||
$content = $renderer->render($item);
|
||||
|
||||
$this->content[] = $this->getInnerIterator()->beginItemMarkup($item);
|
||||
$this->content[] = $content;
|
||||
$this->content[] = $this->getInnerIterator()->endItemMarkup();
|
||||
}
|
||||
}
|
@ -4,11 +4,10 @@
|
||||
namespace Icinga\Web\Navigation\Renderer;
|
||||
|
||||
use Exception;
|
||||
use RecursiveIteratorIterator;
|
||||
use Icinga\Exception\IcingaException;
|
||||
use Icinga\Web\Navigation\Navigation;
|
||||
use Icinga\Web\Navigation\NavigationItem;
|
||||
use Icinga\Web\Navigation\Renderer\NavigationItemRenderer;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
/**
|
||||
* Renderer for multi level navigation
|
||||
|
@ -139,6 +139,7 @@ class StyleSheet
|
||||
$this->lessCompiler->addLessFile($this->pubPath . '/' . $lessFile);
|
||||
}
|
||||
|
||||
$auth = Auth::getInstance();
|
||||
$mm = $this->app->getModuleManager();
|
||||
|
||||
foreach ($mm->getLoadedModules() as $moduleName => $module) {
|
||||
@ -157,7 +158,6 @@ class StyleSheet
|
||||
}
|
||||
|
||||
if (! (bool) $themingConfig->get('disabled', false)) {
|
||||
$auth = Auth::getInstance();
|
||||
if ($auth->isAuthenticated()) {
|
||||
$userTheme = $auth->getUser()->getPreferences()->getValue('icingaweb', 'theme');
|
||||
if ($userTheme !== null) {
|
||||
@ -174,7 +174,7 @@ class StyleSheet
|
||||
Logger::warning(sprintf(
|
||||
'Theme "%s" set by user "%s" has not been found.',
|
||||
$theme,
|
||||
($user = Auth::getInstance()->getUser()) !== null ? $user->getUsername() : 'anonymous'
|
||||
$auth->isAuthenticated() ? $auth->getUser()->getUsername() : 'anonymous'
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -184,10 +184,10 @@ class StyleSheet
|
||||
}
|
||||
|
||||
$mode = 'none';
|
||||
if ($user = Auth::getInstance()->getUser()) {
|
||||
if ($auth->isAuthenticated()) {
|
||||
$file = $themePath !== null ? @file_get_contents($themePath) : false;
|
||||
if (! $file || strpos($file, self::LIGHT_MODE_IDENTIFIER) !== false) {
|
||||
$mode = $user->getPreferences()->getValue('icingaweb', 'theme_mode', self::DEFAULT_MODE);
|
||||
$mode = $auth->getUser()->getPreferences()->getValue('icingaweb', 'theme_mode', self::DEFAULT_MODE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,10 +179,9 @@ class Url
|
||||
}
|
||||
|
||||
$urlParts = parse_url($url);
|
||||
if (isset($urlParts['scheme']) && (
|
||||
$urlParts['scheme'] !== $request->getScheme()
|
||||
if ((isset($urlParts['scheme']) && $urlParts['scheme'] !== $request->getScheme())
|
||||
|| (isset($urlParts['host']) && $urlParts['host'] !== $request->getServer('SERVER_NAME'))
|
||||
|| (isset($urlParts['port']) && $urlParts['port'] != $request->getServer('SERVER_PORT')))
|
||||
|| (isset($urlParts['port']) && $urlParts['port'] != $request->getServer('SERVER_PORT'))
|
||||
) {
|
||||
$urlObject->setIsExternal();
|
||||
}
|
||||
|
@ -205,7 +205,8 @@ class View extends Zend_View_Abstract
|
||||
'th-thumb-empty' => true,
|
||||
'github-circled' => true,
|
||||
'history' => true,
|
||||
'binoculars' => true
|
||||
'binoculars' => true,
|
||||
'letter' => true
|
||||
//</editor-fold>
|
||||
];
|
||||
|
||||
|
@ -3,9 +3,9 @@
|
||||
|
||||
namespace Icinga\Web\Widget\Dashboard;
|
||||
|
||||
use Icinga\Web\Session;
|
||||
use Icinga\Web\Url;
|
||||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Exception\IcingaException;
|
||||
|
||||
/**
|
||||
* A dashboard pane dashlet
|
||||
@ -57,18 +57,15 @@ class Dashlet extends UserWidget
|
||||
*/
|
||||
private $template =<<<'EOD'
|
||||
|
||||
<div class="container" data-icinga-url="{URL}">
|
||||
<h1><a href="{FULL_URL}" aria-label="{TOOLTIP}" title="{TOOLTIP}" data-base-target="col1">{TITLE}</a></h1>
|
||||
<div class="container" data-icinga-url="{URL}" data-url-hash="{URL_HASH}">
|
||||
<h1><a
|
||||
href="{FULL_URL}"
|
||||
aria-label="{TOOLTIP}"
|
||||
title="{TOOLTIP}"
|
||||
data-url-hash="{FULL_URL_HASH}"
|
||||
data-base-target="col1"
|
||||
>{TITLE}</a></h1>
|
||||
<p class="progress-label">{PROGRESS_LABEL}<span>.</span><span>.</span><span>.</span></p>
|
||||
<noscript>
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
src="{IFRAME_URL}"
|
||||
frameborder="no"
|
||||
title="{TITLE_PREFIX}{TITLE}">
|
||||
</iframe>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
EOD;
|
||||
|
||||
@ -250,13 +247,22 @@ EOD;
|
||||
|
||||
$url = $this->getUrl();
|
||||
$url->setParam('showCompact', true);
|
||||
$iframeUrl = clone $url;
|
||||
$iframeUrl->setParam('isIframe');
|
||||
$fullUrl = $url->getUrlWithout(['showCompact', 'limit', 'view']);
|
||||
|
||||
$urlHash = '';
|
||||
$fullUrlHash = '';
|
||||
if ($url->getPath() === 'iframe') {
|
||||
$urlHash = hash('sha256', Url::fromPath($url->getParam('url'))->getAbsoluteUrl()
|
||||
. Session::getSession()->getId());
|
||||
$fullUrlHash = hash('sha256', Url::fromPath($fullUrl->getParam('url'))->getAbsoluteUrl()
|
||||
. Session::getSession()->getId());
|
||||
}
|
||||
|
||||
$searchTokens = array(
|
||||
'{URL}',
|
||||
'{IFRAME_URL}',
|
||||
'{URL_HASH}',
|
||||
'{FULL_URL}',
|
||||
'{FULL_URL_HASH}',
|
||||
'{TOOLTIP}',
|
||||
'{TITLE}',
|
||||
'{TITLE_PREFIX}',
|
||||
@ -265,8 +271,9 @@ EOD;
|
||||
|
||||
$replaceTokens = array(
|
||||
$url,
|
||||
$iframeUrl,
|
||||
$url->getUrlWithout(['showCompact', 'limit', 'view']),
|
||||
$urlHash,
|
||||
$fullUrl,
|
||||
$fullUrlHash,
|
||||
sprintf($view->translate('Show %s', 'dashboard.dashlet.tooltip'), $view->escape($this->getTitle())),
|
||||
$view->escape($this->getTitle()),
|
||||
$view->translate('Dashlet') . ': ',
|
||||
|
@ -3,17 +3,17 @@
|
||||
|
||||
namespace Icinga\Web\Widget;
|
||||
|
||||
use Exception;
|
||||
use Icinga\Application\Icinga;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Data\Filter\FilterChain;
|
||||
use Icinga\Data\Filter\FilterExpression;
|
||||
use Icinga\Data\Filter\FilterOr;
|
||||
use Icinga\Data\Filterable;
|
||||
use Icinga\Data\FilterColumns;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Data\Filter\FilterExpression;
|
||||
use Icinga\Data\Filter\FilterChain;
|
||||
use Icinga\Data\Filter\FilterOr;
|
||||
use Icinga\Web\Url;
|
||||
use Icinga\Application\Icinga;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Web\Notification;
|
||||
use Exception;
|
||||
use Icinga\Web\Url;
|
||||
|
||||
/**
|
||||
* Filter
|
||||
@ -737,10 +737,11 @@ class FilterEditor extends AbstractWidget
|
||||
$preservedUrl = $this->preservedUrl();
|
||||
|
||||
$html = ' <form method="post" class="search inline" action="'
|
||||
. $preservedUrl
|
||||
. '"><input type="text" name="q" class="search search-input" value="" placeholder="'
|
||||
. t('Search...')
|
||||
. '" /></form>';
|
||||
. $preservedUrl
|
||||
. '"><i class="icon fa-search fa search-icon"></i>'
|
||||
. '<input type="text" name="q" class="search search-input" value="" placeholder="'
|
||||
. t('Search...')
|
||||
. '" /></form>';
|
||||
|
||||
if ($this->filter->isEmpty()) {
|
||||
$title = t('Filter this list');
|
||||
|
@ -10,6 +10,7 @@ use ipl\Html\FormElement\InputElement;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Web\Control\SearchBar\Suggestions;
|
||||
use ipl\Web\Url;
|
||||
use ipl\Web\Widget\Icon;
|
||||
|
||||
class SingleValueSearchControl extends Form
|
||||
{
|
||||
@ -106,6 +107,8 @@ class SingleValueSearchControl extends Form
|
||||
{
|
||||
$suggestionsId = Icinga::app()->getRequest()->protectId('single-value-suggestions');
|
||||
|
||||
$this->addHtml(new Icon('search', Attributes::create(['class' => 'search-icon'])));
|
||||
|
||||
$this->addElement(
|
||||
'text',
|
||||
$this->searchParameter,
|
||||
|
@ -112,7 +112,7 @@ class Window
|
||||
{
|
||||
if (! isset(static::$window)) {
|
||||
$id = Icinga::app()->getRequest()->getHeader('X-Icinga-WindowId');
|
||||
if (empty($id) || $id === static::UNDEFINED) {
|
||||
if (empty($id) || $id === static::UNDEFINED || ! preg_match('/^\w+$/', $id)) {
|
||||
Icinga::app()->getResponse()->setOverrideWindowId();
|
||||
$id = static::generateId();
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
Module: doc
|
||||
Version: 2.12.2
|
||||
Version: 2.12.5
|
||||
Description: Documentation module
|
||||
Extracts, shows and exports documentation for Icinga Web 2 and its modules.
|
||||
|
@ -1,17 +1,5 @@
|
||||
/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
|
||||
|
||||
// Mixins
|
||||
|
||||
.gradient(@a: @gray-lighter; @b: @gray-lightest) {
|
||||
background: @a;
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(@a), to(@b));
|
||||
background: -webkit-linear-gradient(top, @a, @b);
|
||||
background: -moz-linear-gradient(top, @a, @b);
|
||||
background: -ms-linear-gradient(top, @a, @b);
|
||||
background: -o-linear-gradient(top, @a, @b);
|
||||
background: linear-gradient(to bottom, @a, @b);
|
||||
}
|
||||
|
||||
// General styles
|
||||
|
||||
code {
|
||||
@ -84,7 +72,7 @@ table {
|
||||
}
|
||||
|
||||
tbody > tr:nth-child(odd) {
|
||||
.gradient()
|
||||
background: @gray-light;
|
||||
}
|
||||
|
||||
tbody > tr:nth-child(even) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
Module: migrate
|
||||
Version: 2.12.2
|
||||
Version: 2.12.5
|
||||
Description: Migrate module
|
||||
This module was introduced with the domain-aware authentication feature in version 2.5.0.
|
||||
It helps you migrating users and user configurations according to a given domain.
|
||||
|
@ -57,7 +57,8 @@ class AcknowledgeProblemCommandForm extends ObjectsCommandForm
|
||||
. ' the host or service that is having problems. Make sure you enter a brief description of'
|
||||
. ' what you are doing.'
|
||||
),
|
||||
'attribs' => array('class' => 'autofocus')
|
||||
'attribs' => array('class' => 'autofocus'),
|
||||
'value' => $config->get('settings', 'acknowledge_comment_text')
|
||||
)
|
||||
),
|
||||
array(
|
||||
|
@ -227,7 +227,7 @@ class BackendConfigForm extends ConfigForm
|
||||
'autosubmit' => true
|
||||
)
|
||||
);
|
||||
$resourceName = isset($formData['resource']) ? $formData['resource'] : $this->getValue('resource');
|
||||
$resourceName = $this->getView()->escape($formData['resource'] ?? $this->getValue('resource'));
|
||||
$this->addElement(
|
||||
'note',
|
||||
'resource_note',
|
||||
|
@ -284,10 +284,10 @@ $section->add(N_('Timeline'), array(
|
||||
/*
|
||||
* Reporting Section
|
||||
*/
|
||||
$section = $this->menuSection(N_('Reporting'), array(
|
||||
'icon' => 'barchart',
|
||||
$section = $this->menuSection(N_('Reporting'), [
|
||||
'icon' => 'fa-chart-simple',
|
||||
'priority' => 100
|
||||
));
|
||||
]);
|
||||
|
||||
/*
|
||||
* Current Incidents
|
||||
|
@ -21,6 +21,7 @@ by this module.
|
||||
|
||||
Option | Description
|
||||
----------------------------------|-----------------------------------------------
|
||||
acknowledge_comment_text | **Optional.** Set default text for "Comment" in Acknowledgement dialog by default.
|
||||
acknowledge_expire | **Optional.** Check "Use Expire Time" in Acknowledgement dialog by default. Defaults to **0 (false)**.
|
||||
acknowledge_expire_time | **Optional.** Set default value for "Expire Time" in Acknowledgement dialog, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**.
|
||||
acknowledge_notify | **Optional.** Check "Send Notification" in Acknowledgement dialog by default. Defaults to **1 (true)**.
|
||||
|
@ -202,10 +202,10 @@ class ServicestatusQuery extends IdoQuery
|
||||
'service_last_notification' => 'UNIX_TIMESTAMP(ss.last_notification)',
|
||||
'service_last_state_change' => 'UNIX_TIMESTAMP(ss.last_state_change)',
|
||||
'service_last_state_change_ts' => 'ss.last_state_change',
|
||||
'service_last_time_critical' => 'ss.last_time_critical',
|
||||
'service_last_time_ok' => 'ss.last_time_ok',
|
||||
'service_last_time_unknown' => 'ss.last_time_unknown',
|
||||
'service_last_time_warning' => 'ss.last_time_warning',
|
||||
'service_last_time_critical' => 'UNIX_TIMESTAMP(ss.last_time_critical)',
|
||||
'service_last_time_ok' => 'UNIX_TIMESTAMP(ss.last_time_ok)',
|
||||
'service_last_time_unknown' => 'UNIX_TIMESTAMP(ss.last_time_unknown)',
|
||||
'service_last_time_warning' => 'UNIX_TIMESTAMP(ss.last_time_warning)',
|
||||
'service_long_output' => 'ss.long_output',
|
||||
'service_max_check_attempts' => 'ss.max_check_attempts',
|
||||
'service_modified_service_attributes' => 'ss.modified_service_attributes',
|
||||
|
@ -1,5 +1,5 @@
|
||||
Module: monitoring
|
||||
Version: 2.12.2
|
||||
Version: 2.12.5
|
||||
Description: Icinga monitoring module
|
||||
IDO accessor and UI for your monitoring. This is the initial instalment for a
|
||||
graphical presentation of Icinga environments. The predecessor of Icinga DB.
|
||||
|
@ -102,7 +102,11 @@ class ConfigCommand extends Command
|
||||
*
|
||||
* --enable-fpm Enable FPM handler for Apache (Nginx is always enabled)
|
||||
*
|
||||
* --fpm-uri=<uri> Address or path where to pass requests to FPM [127.0.0.1:9000]
|
||||
* --fpm-url=<url> Address where to pass requests to FPM [127.0.0.1:9000]
|
||||
*
|
||||
* --fpm-uri=<uri> Alias for --fpm-url
|
||||
*
|
||||
* --fpm-socket-path=<socketpath> Socket path where to pass requests to FPM, overrides --fpm-url
|
||||
*
|
||||
* --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
|
||||
*
|
||||
@ -120,9 +124,13 @@ class ConfigCommand extends Command
|
||||
* icingacli setup config webserver apache \
|
||||
* --file=/etc/apache2/conf.d/icingaweb2.conf
|
||||
*
|
||||
* icingacli setup config webserver apache \
|
||||
* --file=/etc/apache2/conf.d/icingaweb2.conf
|
||||
* --fpm-url=localhost:9000
|
||||
*
|
||||
* icingacli setup config webserver nginx \
|
||||
* --root=/usr/share/icingaweb2/public \
|
||||
* --fpm-uri=unix:/var/run/php5-fpm.sock
|
||||
* --fpm-socket-path=/var/run/php8.3-fpm.sock
|
||||
*/
|
||||
public function webserverAction()
|
||||
{
|
||||
@ -157,10 +165,18 @@ class ConfigCommand extends Command
|
||||
|
||||
$enableFpm = $this->params->shift('enable-fpm', $webserver->getEnableFpm());
|
||||
|
||||
$fpmUri = trim($this->params->get('fpm-uri', $webserver->getFpmUri()));
|
||||
if (empty($fpmUri)) {
|
||||
$fpmSocketPath = trim($this->params->get('fpm-socket-path', $webserver->getFpmSocketPath()));
|
||||
$fpmUrl = trim($this->params->get('fpm-url', $webserver->getFpmUrl()));
|
||||
if (empty($fpmUrl)) {
|
||||
$fpmUrl = trim($this->params->get('fpm-uri', $webserver->getFpmUrl()));
|
||||
}
|
||||
if (empty($fpmSocketPath) && empty($fpmUrl)) {
|
||||
$this->fail($this->translate(
|
||||
'The argument --fpm-uri expects an address or path where to pass requests to FPM'
|
||||
'One of the arguments --fpm-socket-path or --fpm-url must be set to pass requests to FPM'
|
||||
));
|
||||
} elseif (!empty($fpmSocketPath) && !empty($fpmUrl)) {
|
||||
$this->fail($this->translate(
|
||||
'Only one of the arguments --fpm-socket-path or --fpm-url must be set to pass requests to FPM'
|
||||
));
|
||||
}
|
||||
$webserver
|
||||
@ -168,7 +184,8 @@ class ConfigCommand extends Command
|
||||
->setConfigDir($configDir)
|
||||
->setUrlPath($urlPath)
|
||||
->setEnableFpm($enableFpm)
|
||||
->setFpmUri($fpmUri);
|
||||
->setFpmUrl($fpmUrl)
|
||||
->setFpmSocketPath($fpmSocketPath);
|
||||
$config = $webserver->generate() . "\n";
|
||||
if (($file = $this->params->get('file')) !== null) {
|
||||
if (file_exists($file) === true) {
|
||||
|
@ -91,6 +91,19 @@ class DatabaseCreationPage extends Form
|
||||
*/
|
||||
public function createElements(array $formData)
|
||||
{
|
||||
if ($this->config['db'] === 'mysql' && preg_match('/[_%]/', $this->config['dbname'])) {
|
||||
$this->warning(sprintf(
|
||||
$this->translate(
|
||||
'The database name may contain either an underscore or a percent sign.'
|
||||
. ' In MySQL these characters represent a wildcard. If part of a database name,'
|
||||
. ' they might not have been escaped when manually granting privileges.'
|
||||
. ' Privileges might not be detected in this case. Check the documentation and'
|
||||
. ' update your grants accordingly: %s'
|
||||
),
|
||||
'https://dev.mysql.com/doc/refman/8.0/en/grant.html#grant-quoting'
|
||||
));
|
||||
}
|
||||
|
||||
$skipValidation = isset($formData['skip_validation']) && $formData['skip_validation'];
|
||||
$this->addElement(
|
||||
'text',
|
||||
|
@ -602,7 +602,7 @@ class WebWizard extends Wizard implements SetupWizard
|
||||
)));
|
||||
|
||||
$set->add(new WebLibraryRequirement(array(
|
||||
'condition' => ['icinga-php-library', '>=', '0.13.2'],
|
||||
'condition' => ['icinga-php-library', '>=', '0.14.2'],
|
||||
'alias' => 'Icinga PHP library',
|
||||
'description' => mt(
|
||||
'setup',
|
||||
|
@ -33,11 +33,25 @@ abstract class Webserver
|
||||
protected $configDir;
|
||||
|
||||
/**
|
||||
* Address or path where to pass requests to FPM
|
||||
* Address where to pass requests to FPM
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fpmUri;
|
||||
protected $fpmUrl;
|
||||
|
||||
/**
|
||||
* Socket path where to pass requests to FPM
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fpmSocketPath;
|
||||
|
||||
/**
|
||||
* FPM socket connection schema
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fpmSocketSchema = 'unix:';
|
||||
|
||||
/**
|
||||
* Enable to pass requests to FPM
|
||||
@ -72,6 +86,7 @@ abstract class Webserver
|
||||
public function generate()
|
||||
{
|
||||
$template = $this->getTemplate();
|
||||
$fpmUri = $this->createFpmUri();
|
||||
|
||||
$searchTokens = array(
|
||||
'{urlPath}',
|
||||
@ -85,7 +100,7 @@ abstract class Webserver
|
||||
$this->getDocumentRoot(),
|
||||
preg_match('~/$~', $this->getUrlPath()) ? $this->getDocumentRoot() . '/' : $this->getDocumentRoot(),
|
||||
$this->getConfigDir(),
|
||||
$this->getFpmUri()
|
||||
$fpmUri
|
||||
);
|
||||
$template = str_replace($searchTokens, $replaceTokens, $template);
|
||||
return $template;
|
||||
@ -98,6 +113,13 @@ abstract class Webserver
|
||||
*/
|
||||
abstract protected function getTemplate();
|
||||
|
||||
/**
|
||||
* Creates the connection string for the respective web server
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function createFpmUri();
|
||||
|
||||
/**
|
||||
* Set the URL path of Icinga Web 2
|
||||
*
|
||||
@ -208,25 +230,47 @@ abstract class Webserver
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address or path where to pass requests to FPM
|
||||
* Get the address where to pass requests to FPM
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFpmUri()
|
||||
public function getFpmUrl()
|
||||
{
|
||||
return $this->fpmUri;
|
||||
return $this->fpmUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the address or path where to pass requests to FPM
|
||||
* Set the address where to pass requests to FPM
|
||||
*
|
||||
* @param string $uri
|
||||
* @param string $url
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setFpmUri($uri)
|
||||
public function setFpmUrl($url)
|
||||
{
|
||||
$this->fpmUri = (string) $uri;
|
||||
$this->fpmUrl = (string) $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the socket path where to pass requests to FPM
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFpmSocketPath()
|
||||
{
|
||||
return $this->fpmSocketPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the socket path where to pass requests to FPM
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setFpmSocketPath($socketPath)
|
||||
{
|
||||
$this->fpmSocketPath = (string) $socketPath;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -10,7 +10,21 @@ use Icinga\Module\Setup\Webserver;
|
||||
*/
|
||||
class Apache extends Webserver
|
||||
{
|
||||
protected $fpmUri = '127.0.0.1:9000';
|
||||
protected $fpmUrl = '127.0.0.1:9000';
|
||||
|
||||
protected $fpmUrlSchema = 'fcgi://';
|
||||
|
||||
protected function createFpmUri()
|
||||
{
|
||||
$apacheFpmUri = "";
|
||||
if (empty($this->fpmSocketPath)) {
|
||||
$apacheFpmUri = $this->fpmUrlSchema . $this->fpmUrl;
|
||||
} else {
|
||||
$apacheFpmUri = $this->fpmSocketSchema . $this->fpmSocketPath . '|' . $this->fpmUrlSchema . 'localhost';
|
||||
}
|
||||
|
||||
return $apacheFpmUri;
|
||||
}
|
||||
|
||||
protected function getTemplate()
|
||||
{
|
||||
@ -23,7 +37,7 @@ Alias {urlPath} "{aliasDocumentRoot}"
|
||||
# # Forward PHP requests to FPM
|
||||
# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||
# <LocationMatch "^{urlPath}/(.*\.php)$">
|
||||
# ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
|
||||
# ProxyPassMatch "{fpmUri}/{documentRoot}/$1"
|
||||
# </LocationMatch>
|
||||
#</IfVersion>
|
||||
|
||||
@ -71,7 +85,7 @@ Alias {urlPath} "{aliasDocumentRoot}"
|
||||
# # Forward PHP requests to FPM
|
||||
# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||
# <FilesMatch "\.php$">
|
||||
# SetHandler "proxy:fcgi://{fpmUri}"
|
||||
# SetHandler "proxy:{fpmUri}"
|
||||
# ErrorDocument 503 {urlPath}/error_unavailable.html
|
||||
# </FilesMatch>
|
||||
# </IfVersion>
|
||||
@ -85,7 +99,7 @@ Alias {urlPath} "{aliasDocumentRoot}"
|
||||
# Forward PHP requests to FPM
|
||||
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||
<LocationMatch "^{urlPath}/(.*\.php)$">
|
||||
ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
|
||||
ProxyPassMatch "{fpmUri}/{documentRoot}/$1"
|
||||
</LocationMatch>
|
||||
</IfVersion>
|
||||
|
||||
@ -131,7 +145,7 @@ Alias {urlPath} "{aliasDocumentRoot}"
|
||||
# Forward PHP requests to FPM
|
||||
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||
<FilesMatch "\.php$">
|
||||
SetHandler "proxy:fcgi://{fpmUri}"
|
||||
SetHandler "proxy:{fpmUri}"
|
||||
ErrorDocument 503 {urlPath}/error_unavailable.html
|
||||
</FilesMatch>
|
||||
</IfVersion>
|
||||
|
@ -10,10 +10,15 @@ use Icinga\Module\Setup\Webserver;
|
||||
*/
|
||||
class Nginx extends Webserver
|
||||
{
|
||||
protected $fpmUri = '127.0.0.1:9000';
|
||||
protected $fpmUrl = '127.0.0.1:9000';
|
||||
|
||||
protected $enableFpm = true;
|
||||
|
||||
protected function createFpmUri()
|
||||
{
|
||||
return empty($this->fpmSocketPath) ? $this->fpmUrl : $this->fpmSocketSchema . $this->fpmSocketPath;
|
||||
}
|
||||
|
||||
protected function getTemplate()
|
||||
{
|
||||
return <<<'EOD'
|
||||
|
@ -1,5 +1,5 @@
|
||||
Module: setup
|
||||
Version: 2.12.2
|
||||
Version: 2.12.5
|
||||
Description: Setup module
|
||||
Web based wizard for setting up Icinga Web 2 and its modules.
|
||||
This includes the data backends (e.g. relational database, LDAP),
|
||||
|
@ -1,5 +1,5 @@
|
||||
Module: test
|
||||
Version: 2.12.2
|
||||
Version: 2.12.5
|
||||
Description: Translation module
|
||||
This module allows developers to run (unit) tests against Icinga Web 2 and
|
||||
any of its modules. Usually you do not need to enable this.
|
||||
|
@ -130,7 +130,7 @@ below.
|
||||

|
||||
|
||||
And when you want to test your changes, please read more about under the chapter
|
||||
[Testing Translations](Testing Translations).
|
||||
[Testing Translations](03-Translation.md#module-translation-tests).
|
||||
|
||||
## Testing Translations <a id="module-translation-tests"></a>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
Module: translation
|
||||
Version: 2.12.2
|
||||
Version: 2.12.5
|
||||
Description: Translation module
|
||||
This module allows developers and translators to translate modules for multiple
|
||||
languages. You do not need this module to run an internationalized web frontend.
|
||||
|
@ -234,7 +234,7 @@
|
||||
border: 1px solid @gray-lighter;
|
||||
background: @body-bg-color;
|
||||
box-shadow: 0 0 1em 0 rgba(0,0,0,.25);
|
||||
z-index: 1;
|
||||
z-index: 15;
|
||||
.rounded-corners();
|
||||
|
||||
a {
|
||||
|
@ -21,35 +21,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
form > .search-icon {
|
||||
position: absolute;
|
||||
left: 0.25em;
|
||||
top: ~"calc(50% - 0.5em)";
|
||||
pointer-events: none;
|
||||
color: @menu-color;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
form:has(> .search-icon) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.controls input.search,
|
||||
input.search {
|
||||
.transition(border 0.3s ease);
|
||||
.transition(background-image 0.2s ease);
|
||||
|
||||
background-image: url(../img/icons/search_white.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1em 1em;
|
||||
background-position: .25em center;
|
||||
outline: none;
|
||||
padding-left: 1.5em;
|
||||
width: 20em;
|
||||
|
||||
&:focus {
|
||||
background-image: url(../img/icons/search_icinga_blue.png);
|
||||
}
|
||||
|
||||
&:focus:not([readonly]) {
|
||||
border-color: @icinga-blue;
|
||||
}
|
||||
}
|
||||
|
||||
@light-mode: {
|
||||
#menu input.search,
|
||||
.controls input.search,
|
||||
input.search {
|
||||
background-image: url(../img/icons/search.png);
|
||||
}
|
||||
};
|
||||
form:has(input.search:focus) > .search-icon {
|
||||
color: @icinga-blue;
|
||||
}
|
||||
|
||||
.backend-selection,
|
||||
.pagination-control,
|
||||
|
@ -303,7 +303,7 @@
|
||||
|
||||
> .badge {
|
||||
position: absolute;
|
||||
right: .5em;
|
||||
right: 1em;
|
||||
bottom: .25em;
|
||||
font-size: 75%;
|
||||
overflow: hidden;
|
||||
@ -330,7 +330,8 @@
|
||||
padding-left: .75em;
|
||||
}
|
||||
|
||||
.nav-level-1 > .nav-item i {
|
||||
.nav-level-1 > .nav-item > a > i,
|
||||
.nav-level-1 > .nav-item > span > i {
|
||||
font-size: 1.5em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
@ -282,6 +282,39 @@ a:hover > .icon-cancel {
|
||||
|
||||
// Responsive iFrames
|
||||
|
||||
.iframe-warning {
|
||||
h2, p, a {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
font-size: 200%;
|
||||
margin: 0 auto;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1000%;
|
||||
color: @state-warning;
|
||||
}
|
||||
|
||||
.note {
|
||||
background: @gray-lighter;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.reason {
|
||||
.icon {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
font-size: 100%;
|
||||
background: @gray-lightest;
|
||||
color: @text-color-light;
|
||||
}
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
position: relative;
|
||||
height: 0;
|
||||
@ -295,6 +328,7 @@ a:hover > .icon-cancel {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,12 @@
|
||||
|
||||
#menu [class^="icon-"],
|
||||
#menu [class*=" icon-"] {
|
||||
&:before {
|
||||
&::before {
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
@nav-item-height: 3.166666667em; // 38px
|
||||
@icon-width: 1.7em; // 1.5em width + 0.2em right margin
|
||||
|
||||
#menu {
|
||||
@ -15,6 +16,7 @@
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
#menu .nav-item {
|
||||
@ -33,11 +35,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
#layout:not(.sidebar-collapsed) #menu .nav-item > a:first-of-type {
|
||||
#layout:not(.sidebar-collapsed) #menu .nav-item > a:first-of-type,
|
||||
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-2 > .nav-item > a:first-of-type,
|
||||
#layout.minimal-layout #menu .nav-level-1 > .nav-item > a:first-of-type {
|
||||
// Respect overflowing content
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.text-ellipsis();
|
||||
}
|
||||
|
||||
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item {
|
||||
@ -51,7 +53,6 @@
|
||||
}
|
||||
|
||||
#menu .nav-level-1 > .nav-item {
|
||||
line-height: 2.167em; // 26 px
|
||||
color: @menu-color;
|
||||
|
||||
&.active {
|
||||
@ -69,7 +70,9 @@
|
||||
}
|
||||
|
||||
> a {
|
||||
padding: 0.5em 0.5em 0.5em .75em;
|
||||
// To center the content, padding top is: height - line-height / 2
|
||||
padding: ~"calc((@{nav-item-height} - 1.5em) / 2) .5em .5em .75em";
|
||||
height: @nav-item-height;
|
||||
}
|
||||
|
||||
&.active:not(.selected) > a:focus,
|
||||
@ -87,7 +90,7 @@
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
& > a > .icon-letter:before {
|
||||
& > a > .icon-letter::before {
|
||||
content: attr(data-letter);
|
||||
font-family: @font-family;
|
||||
font-weight: 800;
|
||||
@ -95,6 +98,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item > a {
|
||||
padding: ~"calc((@{nav-item-height} - 2em) / 2) .5em .5em .75em";
|
||||
}
|
||||
|
||||
#menu ul:not(.nav-level-2) > .selected > a {
|
||||
background-color: @menu-highlight-color;
|
||||
color: @text-color-inverted;
|
||||
@ -103,7 +110,7 @@
|
||||
background-color: @menu-highlight-hover-bg-color;
|
||||
}
|
||||
|
||||
&:after {
|
||||
&::after {
|
||||
.transform(rotate(45deg));
|
||||
|
||||
position: absolute;
|
||||
@ -114,8 +121,9 @@
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1.25em;
|
||||
margin-top: -1.75em;
|
||||
width: 1.25em;
|
||||
top: ~"calc(50% - (1.25em / 2))";
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,6 +131,7 @@
|
||||
// Collapse menu by default
|
||||
display: none;
|
||||
line-height: 1.833em; // 22px
|
||||
z-index: 12;
|
||||
|
||||
> a {
|
||||
color: @menu-2ndlvl-color;
|
||||
@ -140,7 +149,7 @@
|
||||
}
|
||||
|
||||
// Little caret on active level-2 item
|
||||
&.active:after {
|
||||
&.active::after {
|
||||
.transform(rotate(45deg));
|
||||
|
||||
background-color: @body-bg-color;
|
||||
@ -150,7 +159,7 @@
|
||||
height: 1.25em;
|
||||
width: 1.25em;
|
||||
position: absolute;
|
||||
top: .5em;
|
||||
top: ~"calc(50% - (1.25em / 2))";
|
||||
right: -.75em;
|
||||
z-index: 3;
|
||||
}
|
||||
@ -204,9 +213,12 @@
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
#menu .search-icon {
|
||||
left: 1em;
|
||||
}
|
||||
|
||||
#menu input.search {
|
||||
background: transparent url('../img/icons/search_white.png') no-repeat 1em center;
|
||||
background-size: 1em auto;
|
||||
background-color: @menu-bg-color;
|
||||
border: none;
|
||||
color: @menu-color;
|
||||
line-height: 2.167em;
|
||||
@ -233,7 +245,7 @@
|
||||
|
||||
// Badge offset correction
|
||||
#menu > nav > .nav-level-1 > .badge-nav-item > a > .badge {
|
||||
margin-top: 0.2em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#menu .nav-level-2 > .badge-nav-item > a > .badge {
|
||||
@ -241,6 +253,10 @@
|
||||
margin-right: .5em
|
||||
}
|
||||
|
||||
#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item:not(.active).hover > .nav-level-2 {
|
||||
padding-top: @vertical-padding;
|
||||
}
|
||||
|
||||
// Hovered menu
|
||||
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item.hover,
|
||||
#layout:not(.minimal-layout) #menu .nav-level-1 > .nav-item:not(.active).hover {
|
||||
@ -250,12 +266,15 @@
|
||||
border-color: @gray-light;
|
||||
border-radius: .25em;
|
||||
box-shadow: 0 0 1em 0 rgba(0,0,0,.3);
|
||||
padding: @vertical-padding 0;
|
||||
padding-bottom: @vertical-padding;
|
||||
width: 14em;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
z-index: 11;
|
||||
margin-top: -1px; // Align content with the menu item, not its border
|
||||
|
||||
&::after {
|
||||
--caretSide: 1.25em;
|
||||
|
||||
&:after {
|
||||
.transform(rotate(45deg));
|
||||
|
||||
background-color: @body-bg-color;
|
||||
@ -263,17 +282,20 @@
|
||||
border-left: 1px solid @gray-light;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1.1em;
|
||||
width: 1.1em;
|
||||
height: var(--caretSide);
|
||||
width: var(--caretSide);
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
left: -.6em;
|
||||
z-index: -1;
|
||||
top: ~"calc(@{nav-item-height} / 2 - var(--caretSide) / 2)";
|
||||
left: ~"calc(-1 * var(--caretSide) / 2 - 1px)";
|
||||
}
|
||||
|
||||
&.bottom-up:after {
|
||||
top: unset;
|
||||
bottom: 1em;
|
||||
&.bottom-up {
|
||||
--caretY: 100%;
|
||||
margin-top: 1px;
|
||||
|
||||
&::after {
|
||||
top: ~"calc(var(--caretY) - (@{nav-item-height} / 2) - (var(--caretSide) / 2))";
|
||||
}
|
||||
}
|
||||
|
||||
> .nav-item {
|
||||
@ -303,7 +325,7 @@
|
||||
}
|
||||
|
||||
// Hide activity caret when displayed as flyout
|
||||
&:after {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -320,17 +342,13 @@
|
||||
|
||||
#layout:not(.minimal-layout) #menu .nav-level-1 > .nav-item:not(.active).hover {
|
||||
> .nav-level-2 {
|
||||
// Position relative to parent
|
||||
margin-left: 16em;
|
||||
margin-top: -3.167em;
|
||||
}
|
||||
}
|
||||
|
||||
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item.hover {
|
||||
> .nav-level-2 {
|
||||
// Position relative to parent
|
||||
margin-left: 4em;
|
||||
margin-top: -3.333em;
|
||||
|
||||
> .badge-nav-item {
|
||||
display: flex;
|
||||
@ -508,12 +526,34 @@ html.no-js #toggle-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#open-sidebar:before,
|
||||
#close-sidebar:before {
|
||||
#open-sidebar::before,
|
||||
#close-sidebar::before {
|
||||
width: 1.4em;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#layout.sidebar-collapsed #menu .nav-level-1 > .nav-item.hover .nav-level-2 > .nav-item-header {
|
||||
background-color: @menu-bg-color;
|
||||
border-bottom: 1px solid @gray-light;
|
||||
border-top-left-radius: .25em;
|
||||
border-top-right-radius: .25em;
|
||||
|
||||
span {
|
||||
padding-left: 1.375em;
|
||||
padding-right: 0.545em;
|
||||
height: @nav-item-height;
|
||||
line-height: @nav-item-height;
|
||||
display: block;
|
||||
|
||||
font-weight: @font-weight-bold;
|
||||
.text-ellipsis();
|
||||
|
||||
> .badge {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item.active .nav-level-2 > li {
|
||||
&.nav-item:not(.badge-nav-item) {
|
||||
&:not(.selected):not(.active) a:hover,
|
||||
@ -525,6 +565,10 @@ html.no-js #toggle-sidebar {
|
||||
|
||||
#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item.active .nav-level-2 > li,
|
||||
#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item:not(.active).hover .nav-level-2 > li {
|
||||
&.nav-item-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.badge-nav-item {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -70,7 +70,7 @@
|
||||
border-top: none;
|
||||
margin-left: -1px;
|
||||
min-width: 14em;
|
||||
z-index: 10;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.tabs > .dropdown-nav-item > ul > li:hover > a {
|
||||
|
@ -83,7 +83,9 @@
|
||||
if ($container[0].contains(origFocus)
|
||||
&& origFocus.form
|
||||
&& ! origFocus.matches(
|
||||
'input[type=submit], input[type=reset], input[type=button], .autofocus, .autosubmit:not(:hover)'
|
||||
'input[type=submit], input[type=reset], input[type=button]'
|
||||
+ ', button[type=submit], button[type=reset], button[type=button]'
|
||||
+ ', .autofocus, .autosubmit:not(:hover)'
|
||||
)
|
||||
) {
|
||||
this.icinga.logger.debug('Not changing content for ' + containerId + ' form has focus');
|
||||
|
@ -6,15 +6,24 @@
|
||||
|
||||
Icinga.Behaviors = Icinga.Behaviors || {};
|
||||
|
||||
try {
|
||||
var d3 = require("icinga/icinga-php-thirdparty/mbostock/d3");
|
||||
} catch (e) {
|
||||
console.warn('D3.js library is unavailable. Navigation to flyout may not work as expected.');
|
||||
}
|
||||
|
||||
var Navigation = function (icinga) {
|
||||
Icinga.EventListener.call(this, icinga);
|
||||
this.on('click', '#menu a', this.linkClicked, this);
|
||||
this.on('click', '#menu tr[href]', this.linkClicked, this);
|
||||
this.on('rendered', '#menu', this.onRendered, this);
|
||||
this.on('mouseenter', '#menu .primary-nav .nav-level-1 > .nav-item', this.showFlyoutMenu, this);
|
||||
if (typeof d3 !== "undefined") {
|
||||
this.on('mousemove', '#menu .primary-nav .nav-level-1 > .nav-item', this.onMouseMove, this);
|
||||
}
|
||||
|
||||
this.on('mouseenter', '#menu .primary-nav .nav-level-1 > .nav-item', this.onMouseEnter, this);
|
||||
this.on('mouseleave', '#menu .primary-nav', this.hideFlyoutMenu, this);
|
||||
this.on('click', '#toggle-sidebar', this.toggleSidebar, this);
|
||||
|
||||
this.on('click', '#menu .config-nav-item button', this.toggleConfigFlyout, this);
|
||||
this.on('mouseenter', '#menu .config-menu .config-nav-item', this.showConfigFlyout, this);
|
||||
this.on('mouseleave', '#menu .config-menu .config-nav-item', this.hideConfigFlyout, this);
|
||||
@ -30,6 +39,21 @@
|
||||
*/
|
||||
this.active = null;
|
||||
|
||||
/**
|
||||
* Represents the extended flyout zone, an area formed by the previous cursor position, and top-left
|
||||
* and bottom-left flyout points.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
this.extendedFlyoutZone = new Array(3);
|
||||
|
||||
/**
|
||||
* Timer for managing the delay in showing a flyout on mouse movement.
|
||||
*
|
||||
* @type {null|number}
|
||||
*/
|
||||
this.flyoutTimer = null;
|
||||
|
||||
/**
|
||||
* The menu
|
||||
*
|
||||
@ -282,61 +306,111 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the fly-out menu
|
||||
* Captures the mouse enter events to the navigation item and show the flyout.
|
||||
*
|
||||
* @param e
|
||||
*/
|
||||
Navigation.prototype.showFlyoutMenu = function(e) {
|
||||
var $layout = $('#layout');
|
||||
|
||||
Navigation.prototype.onMouseEnter = function(e) {
|
||||
const $layout = $('#layout');
|
||||
const _this = e.data.self;
|
||||
if ($layout.hasClass('minimal-layout')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $target = $(this);
|
||||
var $flyout = $target.find('.nav-level-2');
|
||||
const $target = $(this);
|
||||
|
||||
if (! $flyout.length) {
|
||||
if (
|
||||
typeof d3 !== "undefined"
|
||||
&& ! _this.extendedFlyoutZone.includes(undefined)
|
||||
&& d3.polygonContains(_this.extendedFlyoutZone, [e.clientX, e.clientY])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $target[0].matches(':has(.nav-level-2)')) {
|
||||
$layout.removeClass('menu-hovered');
|
||||
$target.siblings().not($target).removeClass('hover');
|
||||
return;
|
||||
}
|
||||
|
||||
var delay = 300;
|
||||
|
||||
if ($layout.hasClass('menu-hovered')) {
|
||||
delay = 0;
|
||||
if (! $target.is(':hover')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
try {
|
||||
if (! $target.is(':hover')) {
|
||||
return;
|
||||
}
|
||||
} catch(e) { /* Bypass because if IE8 */ }
|
||||
$layout.addClass('menu-hovered');
|
||||
_this.extendedFlyoutZone[0] = [e.clientX, e.clientY];
|
||||
_this.showFlyoutMenu($target);
|
||||
}
|
||||
|
||||
$layout.addClass('menu-hovered');
|
||||
$target.siblings().not($target).removeClass('hover');
|
||||
$target.addClass('hover');
|
||||
/**
|
||||
* Captures the mouse move events within the navigation item
|
||||
* and show the flyout if needed.
|
||||
*
|
||||
* @param e
|
||||
*/
|
||||
Navigation.prototype.onMouseMove = function(e) {
|
||||
const _this = e.data.self;
|
||||
clearTimeout(_this.flyoutTimer);
|
||||
|
||||
var targetHeight = $target.offset().top + $target.outerHeight();
|
||||
$flyout.css({
|
||||
bottom: 'auto',
|
||||
top: targetHeight
|
||||
});
|
||||
const $target = $(this);
|
||||
|
||||
var rect = $flyout[0].getBoundingClientRect();
|
||||
if (! $target[0].matches(':has(.nav-level-2)')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rect.bottom > window.innerHeight) {
|
||||
$flyout.addClass('bottom-up');
|
||||
$flyout.css({
|
||||
bottom: window.innerHeight - targetHeight,
|
||||
top: 'auto'
|
||||
});
|
||||
if (! $target.hasClass('hover')) {
|
||||
if (
|
||||
! _this.extendedFlyoutZone.includes(undefined)
|
||||
&& d3.polygonContains(_this.extendedFlyoutZone, [e.clientX, e.clientY])
|
||||
) {
|
||||
_this.flyoutTimer = setTimeout(function() {
|
||||
_this.showFlyoutMenu($target);
|
||||
}, 200);
|
||||
} else {
|
||||
$flyout.removeClass('bottom-up');
|
||||
// The extended flyout zone keeps shrinking when the mouse moves towards the target's flyout.
|
||||
// Hence, if the mouse is moved and stopped over a new target, sometimes it could be slightly outside
|
||||
// the extended flyout zone. This in turn will not trigger the flyoutTimer.
|
||||
// Hence, the showFlyoutMenu should be manually called.
|
||||
_this.showFlyoutMenu($target);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
_this.extendedFlyoutZone[0] = [e.clientX, e.clientY];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Show the fly-out menu for the given target navigation item
|
||||
*
|
||||
* @param $target
|
||||
*/
|
||||
Navigation.prototype.showFlyoutMenu = function($target) {
|
||||
const $flyout = $target.find('.nav-level-2');
|
||||
$target.siblings().not($target).removeClass('hover');
|
||||
$target.addClass('hover');
|
||||
|
||||
const targetRect = $target[0].getBoundingClientRect();
|
||||
const flyoutRect = $flyout[0].getBoundingClientRect();
|
||||
|
||||
const css = { "--caretY": "" };
|
||||
if (targetRect.top + flyoutRect.height > window.innerHeight) {
|
||||
css.top = targetRect.bottom - flyoutRect.height;
|
||||
if (css.top < 10) {
|
||||
css.top = 10;
|
||||
// Not sure why -2, but it aligns the caret perfectly with the menu item
|
||||
css["--caretY"] = `${targetRect.bottom - 10 - 2}px`;
|
||||
}
|
||||
|
||||
$flyout.addClass('bottom-up');
|
||||
} else {
|
||||
$flyout.removeClass('bottom-up');
|
||||
css.top = targetRect.top;
|
||||
}
|
||||
|
||||
$flyout.css(css);
|
||||
|
||||
this.extendedFlyoutZone[1] = [flyoutRect.left, css.top];
|
||||
this.extendedFlyoutZone[2] = [flyoutRect.left, css.top + flyoutRect.height];
|
||||
};
|
||||
|
||||
/**
|
||||
@ -348,6 +422,8 @@
|
||||
var $layout = $('#layout');
|
||||
var $nav = $(e.currentTarget);
|
||||
var $hovered = $nav.find('.nav-level-1 > .nav-item.hover');
|
||||
const _this = e.data.self;
|
||||
_this.extendedFlyoutZone.fill(undefined);
|
||||
|
||||
if (! $hovered.length) {
|
||||
$layout.removeClass('menu-hovered');
|
||||
|
@ -170,7 +170,21 @@
|
||||
var $dashlet = $(this);
|
||||
var url = $dashlet.data('icingaUrl');
|
||||
if (typeof url !== 'undefined') {
|
||||
_this.icinga.loader.loadUrl(url, $dashlet).autorefresh = true;
|
||||
const urlHash = this.dataset.urlHash;
|
||||
if (urlHash) {
|
||||
_this.icinga.loader.loadUrl(
|
||||
url,
|
||||
$dashlet,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
undefined,
|
||||
{ "X-Icinga-URLHash": urlHash }
|
||||
);
|
||||
} else {
|
||||
_this.icinga.loader.loadUrl(url, $dashlet).autorefresh = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -281,6 +295,7 @@
|
||||
var $eventTarget = $(event.target);
|
||||
var href = $a.attr('href');
|
||||
var linkTarget = $a.attr('target');
|
||||
const urlHash = this.dataset.urlHash;
|
||||
var $target;
|
||||
var formerUrl;
|
||||
|
||||
@ -391,7 +406,20 @@
|
||||
}
|
||||
|
||||
// Load link URL
|
||||
icinga.loader.loadUrl(href, $target);
|
||||
if (urlHash) {
|
||||
icinga.loader.loadUrl(
|
||||
href,
|
||||
$target,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ "X-Icinga-URLHash": urlHash }
|
||||
);
|
||||
} else {
|
||||
icinga.loader.loadUrl(href, $target);
|
||||
}
|
||||
|
||||
if ($a.closest('#menu').length > 0) {
|
||||
// Menu links should remove all but the first layout column
|
||||
|
@ -242,6 +242,10 @@
|
||||
loadUrl: function (url, $target, data, method, action, autorefresh, progressTimer, extraHeaders) {
|
||||
var id = null;
|
||||
|
||||
if (url.startsWith('//') || ! url.startsWith(this.baseUrl + '/')) {
|
||||
throw new Error('URL ' + url + ' is not relative to ' + this.baseUrl);
|
||||
}
|
||||
|
||||
// Default method is GET
|
||||
if ('undefined' === typeof method) {
|
||||
method = 'GET';
|
||||
@ -1054,15 +1058,20 @@
|
||||
errorThrown + ':',
|
||||
$(req.responseText).text().replace(/\s+/g, ' ').slice(0, 100)
|
||||
);
|
||||
this.renderContentToContainer(
|
||||
req.responseText,
|
||||
req.$target,
|
||||
req.action,
|
||||
req.autorefresh,
|
||||
undefined,
|
||||
req.autosubmit,
|
||||
req.scripted
|
||||
);
|
||||
|
||||
if (req.status === 401) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.renderContentToContainer(
|
||||
req.responseText,
|
||||
req.$target,
|
||||
req.action,
|
||||
req.autorefresh,
|
||||
undefined,
|
||||
req.autosubmit,
|
||||
req.scripted
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (errorThrown === 'abort') {
|
||||
this.icinga.logger.debug(
|
||||
|
@ -4,28 +4,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Get the maximum timezone offset
|
||||
*
|
||||
* @returns {Number}
|
||||
*/
|
||||
Date.prototype.getStdTimezoneOffset = function() {
|
||||
var year = new Date().getFullYear();
|
||||
var offsetInJanuary = new Date(year, 0, 2).getTimezoneOffset();
|
||||
var offsetInJune = new Date(year, 5, 2).getTimezoneOffset();
|
||||
|
||||
return Math.max(offsetInJanuary, offsetInJune);
|
||||
};
|
||||
|
||||
/**
|
||||
* Test for daylight saving time zone
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
Date.prototype.isDst = function() {
|
||||
return this.getStdTimezoneOffset() !== this.getTimezoneOffset();
|
||||
};
|
||||
|
||||
/**
|
||||
* Write timezone information into a cookie
|
||||
*
|
||||
@ -51,15 +29,11 @@
|
||||
* Write timezone information into cookie
|
||||
*/
|
||||
writeTimezone: function() {
|
||||
var date = new Date();
|
||||
var timezoneOffset = (date.getTimezoneOffset()*60) * -1;
|
||||
var dst = date.isDst();
|
||||
|
||||
if (this.readCookie(this.cookieName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.writeCookie(this.cookieName, timezoneOffset + '-' + Number(dst), 1);
|
||||
this.writeCookie(this.cookieName, Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -67,17 +41,9 @@
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {String} value
|
||||
* @param {Number} days
|
||||
*/
|
||||
writeCookie: function(name, value, days) {
|
||||
var expires = '';
|
||||
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime()+(days*24*60*60*1000));
|
||||
var expires = '; expires=' + date.toGMTString();
|
||||
}
|
||||
document.cookie = name + '=' + value + expires + '; path=/';
|
||||
writeCookie: function(name, value) {
|
||||
document.cookie = name + '=' + value + '; path=/';
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -21,7 +21,7 @@ class LocalFileStorageTest extends BaseTestCase
|
||||
parent::__construct($name, $data, $dataName);
|
||||
|
||||
$this->oldErrorReportingLevel = error_reporting();
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
|
||||
if (error_reporting() === 0) {
|
||||
@ -32,7 +32,6 @@ class LocalFileStorageTest extends BaseTestCase
|
||||
switch ($errno) {
|
||||
case E_NOTICE:
|
||||
case E_WARNING:
|
||||
case E_STRICT:
|
||||
case E_RECOVERABLE_ERROR:
|
||||
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||
}
|
||||
|
@ -8,38 +8,42 @@ use Icinga\Util\TimezoneDetect;
|
||||
|
||||
class TimezoneDetectTest extends BaseTestCase
|
||||
{
|
||||
public function testPositiveTimezoneOffsetSeparatedByComma()
|
||||
{
|
||||
$this->assertTimezoneDetection('3600,0', 'Europe/Paris');
|
||||
}
|
||||
|
||||
public function testPositiveTimezoneOffsetSeparatedByHyphen()
|
||||
{
|
||||
$this->assertTimezoneDetection('3600-0', 'Europe/Paris');
|
||||
}
|
||||
|
||||
public function testNegativeTimezoneOffsetSeparatedByComma()
|
||||
{
|
||||
$this->assertTimezoneDetection('-3600,0', 'Atlantic/Azores');
|
||||
}
|
||||
|
||||
public function testNegativeTimezoneOffsetSeparatedByHyphen()
|
||||
{
|
||||
$this->assertTimezoneDetection('-3600-0', 'Atlantic/Azores');
|
||||
}
|
||||
|
||||
protected function assertTimezoneDetection($cookieValue, $expectedTimezoneName)
|
||||
public function testInvalidTimezoneNameInCookie(): void
|
||||
{
|
||||
$tzDetect = new TimezoneDetect();
|
||||
$tzDetect->reset();
|
||||
|
||||
$_COOKIE[TimezoneDetect::$cookieName] = $cookieValue;
|
||||
$_COOKIE[TimezoneDetect::$cookieName] = 'ABC';
|
||||
$tzDetect = new TimezoneDetect();
|
||||
$this->assertFalse(
|
||||
$tzDetect->success(),
|
||||
false,
|
||||
'Failed to assert invalid timezone name is detected'
|
||||
);
|
||||
|
||||
$this->assertNull(
|
||||
$tzDetect->getTimezoneName(),
|
||||
'Failed to assert that the timezone name will not be set for invalid timezone'
|
||||
);
|
||||
}
|
||||
|
||||
public function testValidTimezoneNameInCookie(): void
|
||||
{
|
||||
$tzDetect = new TimezoneDetect();
|
||||
$tzDetect->reset();
|
||||
|
||||
$_COOKIE[TimezoneDetect::$cookieName] = "Europe/Berlin";
|
||||
$tzDetect = new TimezoneDetect();
|
||||
$this->assertTrue(
|
||||
$tzDetect->success(),
|
||||
true,
|
||||
'Failed to assert that the valid timezone name is detected'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
$tzDetect->getTimezoneName(),
|
||||
$expectedTimezoneName,
|
||||
'Failed asserting that the timezone "' . $expectedTimezoneName
|
||||
. '" is being detected from the cookie value "' . $cookieValue . '"'
|
||||
"Europe/Berlin",
|
||||
'Failed to assert that the valid timezone name was correctly set'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,11 @@ class UrlTest extends BaseTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testWhetherProtocolRelativeUrlsAreDetectedAsBeingExternal()
|
||||
{
|
||||
$this->assertTrue(Url::fromPath('//testhost/path/to/my/url.html')->isExternal());
|
||||
}
|
||||
|
||||
public function testWhetherGetAbsoluteUrlReturnsTheGivenUsernameAndPassword()
|
||||
{
|
||||
$url = Url::fromPath('http://testusername:testpassword@testsite.com/path/to/my/url.html');
|
||||
|
Loading…
x
Reference in New Issue
Block a user