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
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trigger-update:
|
update:
|
||||||
name: L10n Update Trigger
|
uses: icinga/github-actions/.github/workflows/L10n-update.yml@main
|
||||||
runs-on: ubuntu-latest
|
secrets: inherit
|
||||||
|
|
||||||
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 }}"}'
|
|
||||||
|
14
.github/workflows/php.yml
vendored
14
.github/workflows/php.yml
vendored
@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- support/*
|
||||||
- release/*
|
- release/*
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
@ -17,7 +18,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
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']
|
os: ['ubuntu-latest']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -46,16 +47,13 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
phpunit-version: 9.5
|
phpunit-version: 9.6
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
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']
|
os: ['ubuntu-latest']
|
||||||
include:
|
|
||||||
- php: '7.2'
|
|
||||||
phpunit-version: 8.5
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
@ -100,7 +98,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup dependencies
|
- name: Setup dependencies
|
||||||
run: |
|
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
|
git clone --depth 1 --branch snapshot/nightly https://github.com/Icinga/icinga-php-thirdparty.git vendor/icinga-php-thirdparty
|
||||||
|
|
||||||
- name: PHPUnit
|
- 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>
|
Florian Strohmaier <florian.strohmaier@icinga.com> <florian.strohmaier@me.com>
|
||||||
Gunnar Beutner <gunnar.beutner@netways.de> <gunnar@beutner.name>
|
Gunnar Beutner <gunnar.beutner@netways.de> <gunnar@beutner.name>
|
||||||
Jannis Moßhammer <jannis.mosshammer@netways.de>
|
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>
|
Johannes Meyer <johannes.meyer@icinga.com> <johannes.meyer@netways.de>
|
||||||
Jennifer Mourek <jennifer.mourek@icinga.com> <jennifer.mourek@netways.de>
|
Jennifer Mourek <jennifer.mourek@icinga.com> <jennifer.mourek@netways.de>
|
||||||
Marius Hein <marius.hein@netways.de> <mhein@itsocks.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>
|
Tobias Bauriedel <tobias.bauriedel@netways.de> <tobias@bauriedel.de>
|
||||||
Yonas Habteab <yonas.habteab@icinga.com> <yonas.habteab@netways.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> <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> <54990055+sukhwinder33445@users.noreply.github.com>
|
||||||
Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com> <sukhwinder33445@gmail.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>
|
ignasr <ignas.linux@gmail.com>
|
||||||
Janne Heß <janne@hess.ooo>
|
Janne Heß <janne@hess.ooo>
|
||||||
Jannis Moßhammer <jannis.mosshammer@netways.de>
|
Jannis Moßhammer <jannis.mosshammer@netways.de>
|
||||||
|
Jan Schuppik <jan.schuppik@icinga.com>
|
||||||
Jennifer Mourek <jennifer.mourek@icinga.com>
|
Jennifer Mourek <jennifer.mourek@icinga.com>
|
||||||
Jiri Pejchal <jiri.pejchal@gmail.com>
|
Jiri Pejchal <jiri.pejchal@gmail.com>
|
||||||
Joe Doherty <git@pjuu.com>
|
Joe Doherty <git@pjuu.com>
|
||||||
Johannes Meyer <johannes.meyer@icinga.com>
|
Johannes Meyer <johannes.meyer@icinga.com>
|
||||||
|
Johannes Rauh <johannes.rauh@icinga.com>
|
||||||
Joonas Kylmälä <joonas.kylmala@kirjastot.fi>
|
Joonas Kylmälä <joonas.kylmala@kirjastot.fi>
|
||||||
Jorge Vallecillo <jorgevallecilloc@gmail.com>
|
Jorge Vallecillo <jorgevallecilloc@gmail.com>
|
||||||
Jo Rhett <jo@chegg.com>
|
Jo Rhett <jo@chegg.com>
|
||||||
@ -73,6 +75,7 @@ Marc DeTrano <marc@gridshield.net>
|
|||||||
Marcel Weinberg <marcel.weinberg@secucloud.com>
|
Marcel Weinberg <marcel.weinberg@secucloud.com>
|
||||||
Marcus Cobden <marcus@marcuscobden.co.uk>
|
Marcus Cobden <marcus@marcuscobden.co.uk>
|
||||||
Marian Rainer-Harbach <marian@rainer-harbach.at>
|
Marian Rainer-Harbach <marian@rainer-harbach.at>
|
||||||
|
marianrh <19990392+marianrh@users.noreply.github.com>
|
||||||
Mario Rimann <mario@rimann.org>
|
Mario Rimann <mario@rimann.org>
|
||||||
Marius Hein <marius.hein@netways.de>
|
Marius Hein <marius.hein@netways.de>
|
||||||
Markus Frosch <markus.frosch@icinga.com>
|
Markus Frosch <markus.frosch@icinga.com>
|
||||||
@ -125,6 +128,7 @@ Rune Darrud <theflyingcorpse@gmail.com>
|
|||||||
Russell Kubik <russkubik@3d-p.com>
|
Russell Kubik <russkubik@3d-p.com>
|
||||||
Sander Ferdinand <sa.ferdinand@gmail.com>
|
Sander Ferdinand <sa.ferdinand@gmail.com>
|
||||||
sant-swedge <simon.wedge@sant.ox.ac.uk>
|
sant-swedge <simon.wedge@sant.ox.ac.uk>
|
||||||
|
Silas <67681686+Tqnsls@users.noreply.github.com>
|
||||||
Simone Orsi <simahawk@users.noreply.github.com>
|
Simone Orsi <simahawk@users.noreply.github.com>
|
||||||
ss23 <stephen@zxsecurity.co.nz>
|
ss23 <stephen@zxsecurity.co.nz>
|
||||||
Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com>
|
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
|
||||||
|
|
||||||
|
### 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
|
### 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
|
#### General Fixes
|
||||||
|
|
||||||
@ -62,7 +133,7 @@ but it’s now slightly improved.
|
|||||||
|
|
||||||
### What's New in Version 2.12.1
|
### 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
|
#### PHP 8.3 Support
|
||||||
|
|
||||||
|
@ -3,18 +3,108 @@
|
|||||||
|
|
||||||
namespace Icinga\Controllers;
|
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
|
* Display external or internal links within an iframe
|
||||||
*/
|
*/
|
||||||
class IframeController extends Controller
|
class IframeController extends CompatController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display iframe w/ the given URL
|
* 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.'
|
. ' that has the appropriate credentials to resolve this issue.'
|
||||||
),
|
),
|
||||||
implode(', ', $mm->getRequiredDatabasePrivileges())
|
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>
|
||||||
<div class="about-social">
|
<div class="about-social">
|
||||||
<?= $this->qlink(
|
<?= $this->qlink(
|
||||||
null,
|
|
||||||
'https://www.twitter.com/icinga',
|
|
||||||
null,
|
|
||||||
array(
|
|
||||||
'target' => '_blank',
|
|
||||||
'icon' => 'twitter',
|
|
||||||
'title' => $this->translate('Icinga on Twitter')
|
|
||||||
)
|
|
||||||
) ?> <?= $this->qlink(
|
|
||||||
null,
|
null,
|
||||||
'https://www.facebook.com/icinga',
|
'https://www.facebook.com/icinga',
|
||||||
null,
|
null,
|
||||||
|
@ -28,18 +28,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul id="social">
|
<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>
|
<li>
|
||||||
<?= $this->qlink(
|
<?= $this->qlink(
|
||||||
null,
|
null,
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<?= $this->tabs->render($this); ?>
|
<?= $this->tabs->render($this); ?>
|
||||||
<br/>
|
<br/>
|
||||||
<div>
|
<div>
|
||||||
<h1>Could not <?= $action; ?> module "<?= $moduleName; ?>"</h1>
|
<h1>Could not <?= $action; ?> module "<?= $this->escape($moduleName); ?>"</h1>
|
||||||
<p>
|
<p>
|
||||||
While operation the following error occurred:
|
While operation the following error occurred:
|
||||||
<br />
|
<br />
|
||||||
|
@ -23,7 +23,7 @@ $modReason = [];
|
|||||||
|
|
||||||
if (isset($requiredVendor, $requiredProject) && $requiredVendor && $requiredProject) {
|
if (isset($requiredVendor, $requiredProject) && $requiredVendor && $requiredProject) {
|
||||||
// TODO: I don't like this, can we define requirements somewhere else?
|
// 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) {
|
foreach ($coreDeps as $libraryName => $requiredVersion) {
|
||||||
if (! $libraries->has($libraryName)) {
|
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()): ?>
|
if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?>
|
||||||
<form action="<?= $this->href('search') ?>" method="get" role="search" class="search-control">
|
<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
|
<input type="text" name="q" id="search" class="search search-input" required
|
||||||
placeholder="<?= $this->translate('Search') ?> …"
|
placeholder="<?= $this->translate('Search') ?> …"
|
||||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
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
|
monitor your infrastructure
|
||||||
* A web server, e.g. Apache or Nginx
|
* A web server, e.g. Apache or Nginx
|
||||||
* PHP version ≥ 7.2
|
* 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)
|
* [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 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
|
* 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
|
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.
|
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
|
## Upgrading to Icinga Web 2.12.2
|
||||||
|
|
||||||
**Framework changes affecting third-party code**
|
**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
|
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.
|
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
|
When filing a bug report please add the following information additionally to the common ones:
|
||||||
[common ones](https://icinga.com/icinga/faq/):
|
|
||||||
* Output of `semodule -l | grep -e icinga2 -e icingaweb2 -e nagios -e apache`
|
* Output of `semodule -l | grep -e icinga2 -e icingaweb2 -e nagios -e apache`
|
||||||
* Output of `semanage boolean -l | grep icinga`
|
* Output of `semanage boolean -l | grep icinga`
|
||||||
* Output of `ps -eZ | grep httpd`
|
* Output of `ps -eZ | grep httpd`
|
||||||
|
@ -568,7 +568,7 @@ abstract class ApplicationBootstrap
|
|||||||
*/
|
*/
|
||||||
protected function setupErrorHandling()
|
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_startup_errors', 1);
|
||||||
ini_set('display_errors', 1);
|
ini_set('display_errors', 1);
|
||||||
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
|
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
|
||||||
@ -579,7 +579,6 @@ abstract class ApplicationBootstrap
|
|||||||
switch ($errno) {
|
switch ($errno) {
|
||||||
case E_NOTICE:
|
case E_NOTICE:
|
||||||
case E_WARNING:
|
case E_WARNING:
|
||||||
case E_STRICT:
|
|
||||||
case E_RECOVERABLE_ERROR:
|
case E_RECOVERABLE_ERROR:
|
||||||
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ namespace Icinga\Application;
|
|||||||
*/
|
*/
|
||||||
class Version
|
class Version
|
||||||
{
|
{
|
||||||
const VERSION = '2.12.2';
|
const VERSION = '2.12.5';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the version of this instance of Icinga Web 2
|
* 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\JavaScript;
|
||||||
use Icinga\Web\StyleSheet;
|
use Icinga\Web\StyleSheet;
|
||||||
|
|
||||||
error_reporting(E_ALL | E_STRICT);
|
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
|
||||||
|
|
||||||
if (isset($_SERVER['REQUEST_URI'])) {
|
if (isset($_SERVER['REQUEST_URI'])) {
|
||||||
$ruri = $_SERVER['REQUEST_URI'];
|
$ruri = $_SERVER['REQUEST_URI'];
|
||||||
|
@ -42,4 +42,9 @@ class RolesConfig extends IniRepository
|
|||||||
|
|
||||||
return $columns;
|
return $columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function initializeSearchColumns(): array
|
||||||
|
{
|
||||||
|
return ['name'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ use Icinga\Exception\AuthenticationException;
|
|||||||
use Icinga\Repository\DbRepository;
|
use Icinga\Repository\DbRepository;
|
||||||
use Icinga\User;
|
use Icinga\User;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
use Zend_Db_Expr;
|
||||||
|
|
||||||
class DbUserBackend extends DbRepository implements UserBackendInterface, Inspectable
|
class DbUserBackend extends DbRepository implements UserBackendInterface, Inspectable
|
||||||
{
|
{
|
||||||
@ -179,23 +180,28 @@ class DbUserBackend extends DbRepository implements UserBackendInterface, Inspec
|
|||||||
{
|
{
|
||||||
if ($this->ds->getDbType() === 'pgsql') {
|
if ($this->ds->getDbType() === 'pgsql') {
|
||||||
// Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape'
|
// 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 {
|
} 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';
|
$query = $this
|
||||||
if ($this->ds->getDbType() === 'mysql') {
|
->select()
|
||||||
$username = strtolower($username);
|
->from('user', $columns)
|
||||||
$nameColumn = 'BINARY LOWER(name)';
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = $this->ds->select()
|
|
||||||
->from($this->prependTablePrefix('user'), $columns)
|
|
||||||
->where($nameColumn, $username)
|
|
||||||
->where('active', true);
|
->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->execute();
|
||||||
$statement->bindColumn(1, $lob, PDO::PARAM_LOB);
|
$statement->bindColumn(1, $lob, PDO::PARAM_LOB);
|
||||||
$statement->fetch(PDO::FETCH_BOUND);
|
$statement->fetch(PDO::FETCH_BOUND);
|
||||||
|
@ -204,7 +204,7 @@ class DbUserGroupBackend extends DbRepository implements Inspectable, UserGroupB
|
|||||||
$membershipQuery = $this
|
$membershipQuery = $this
|
||||||
->select()
|
->select()
|
||||||
->from('group_membership', array('group_name'))
|
->from('group_membership', array('group_name'))
|
||||||
->where('user_name', $user->getUsername());
|
->where('user', $user->getUsername());
|
||||||
|
|
||||||
$memberships = array();
|
$memberships = array();
|
||||||
foreach ($membershipQuery as $membership) {
|
foreach ($membershipQuery as $membership) {
|
||||||
|
@ -37,7 +37,7 @@ class Csv
|
|||||||
}
|
}
|
||||||
$out = array();
|
$out = array();
|
||||||
foreach ($row as & $val) {
|
foreach ($row as & $val) {
|
||||||
$out[] = '"' . ($val ? str_replace('"', '""', $val) : '') . '"';
|
$out[] = '"' . ($val == '0' ? '0' : ($val ? str_replace('"', '""', $val) : '')) . '"';
|
||||||
}
|
}
|
||||||
$csv .= implode(',', $out) . "\r\n";
|
$csv .= implode(',', $out) . "\r\n";
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,11 @@ namespace Icinga\File;
|
|||||||
|
|
||||||
use Dompdf\Dompdf;
|
use Dompdf\Dompdf;
|
||||||
use Dompdf\Options;
|
use Dompdf\Options;
|
||||||
|
use Exception;
|
||||||
use Icinga\Application\Icinga;
|
use Icinga\Application\Icinga;
|
||||||
use Icinga\Exception\ProgrammingError;
|
use Icinga\Exception\ProgrammingError;
|
||||||
use Icinga\Util\Environment;
|
use Icinga\Util\Environment;
|
||||||
|
use Icinga\Web\FileCache;
|
||||||
use Icinga\Web\Hook;
|
use Icinga\Web\Hook;
|
||||||
use Icinga\Web\Url;
|
use Icinga\Web\Url;
|
||||||
|
|
||||||
@ -64,8 +66,17 @@ class Pdf
|
|||||||
return;
|
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 = new Options();
|
||||||
$options->set('defaultPaperSize', 'A4');
|
$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 = new Dompdf($options);
|
||||||
$dompdf->loadHtml($html);
|
$dompdf->loadHtml($html);
|
||||||
$dompdf->render();
|
$dompdf->render();
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
namespace Icinga\Util;
|
namespace Icinga\Util;
|
||||||
|
|
||||||
|
use DateTimeZone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve timezone information from cookie
|
* Retrieve timezone information from cookie
|
||||||
*/
|
*/
|
||||||
@ -15,13 +17,6 @@ class TimezoneDetect
|
|||||||
*/
|
*/
|
||||||
private static $success;
|
private static $success;
|
||||||
|
|
||||||
/**
|
|
||||||
* Timezone offset in minutes
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
private static $offset = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
@ -34,13 +29,6 @@ class TimezoneDetect
|
|||||||
*/
|
*/
|
||||||
public static $cookieName = 'icingaweb2-tzo';
|
public static $cookieName = 'icingaweb2-tzo';
|
||||||
|
|
||||||
/**
|
|
||||||
* Timezone name
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private static $timezone;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new object and try to identify the timezone
|
* Create new object and try to identify the timezone
|
||||||
*/
|
*/
|
||||||
@ -50,31 +38,14 @@ class TimezoneDetect
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists(self::$cookieName, $_COOKIE)) {
|
if (in_array($_COOKIE[self::$cookieName] ?? null, DateTimeZone::listIdentifiers(), true)) {
|
||||||
$matches = array();
|
self::$timezoneName = $_COOKIE[self::$cookieName];
|
||||||
if (preg_match('/\A(-?\d+)[\-,](\d+)\z/', $_COOKIE[self::$cookieName], $matches)) {
|
self::$success = true;
|
||||||
$offset = $matches[1];
|
} else {
|
||||||
$timezoneName = timezone_name_from_abbr('', (int) $offset, (int) $matches[2]);
|
self::$success = false;
|
||||||
|
|
||||||
self::$success = (bool) $timezoneName;
|
|
||||||
if (self::$success) {
|
|
||||||
self::$offset = $offset;
|
|
||||||
self::$timezoneName = $timezoneName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get offset
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getOffset()
|
|
||||||
{
|
|
||||||
return self::$offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get timezone name
|
* Get timezone name
|
||||||
*
|
*
|
||||||
@ -102,6 +73,5 @@ class TimezoneDetect
|
|||||||
{
|
{
|
||||||
self::$success = null;
|
self::$success = null;
|
||||||
self::$timezoneName = null;
|
self::$timezoneName = null;
|
||||||
self::$offset = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ namespace Icinga\Web;
|
|||||||
use Icinga\Application\Logger;
|
use Icinga\Application\Logger;
|
||||||
use Icinga\Authentication\Auth;
|
use Icinga\Authentication\Auth;
|
||||||
use Icinga\Web\Navigation\Navigation;
|
use Icinga\Web\Navigation\Navigation;
|
||||||
|
use Icinga\Web\Navigation\Renderer\RecursiveMenuNavigationRenderer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main menu for Icinga Web 2
|
* 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\Exception\ProgrammingError;
|
||||||
use Icinga\Util\StringHelper;
|
use Icinga\Util\StringHelper;
|
||||||
use Icinga\Web\Navigation\NavigationItem;
|
use Icinga\Web\Navigation\NavigationItem;
|
||||||
|
use Icinga\Web\Session;
|
||||||
use Icinga\Web\Url;
|
use Icinga\Web\Url;
|
||||||
use Icinga\Web\View;
|
use Icinga\Web\View;
|
||||||
|
|
||||||
@ -190,6 +191,10 @@ class NavigationItemRenderer
|
|||||||
|
|
||||||
$target = $item->getTarget();
|
$target = $item->getTarget();
|
||||||
if ($url->isExternal() && (!$target || in_array($target, $this->internalLinkTargets, true))) {
|
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));
|
$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;
|
namespace Icinga\Web\Navigation\Renderer;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use RecursiveIteratorIterator;
|
|
||||||
use Icinga\Exception\IcingaException;
|
use Icinga\Exception\IcingaException;
|
||||||
use Icinga\Web\Navigation\Navigation;
|
use Icinga\Web\Navigation\Navigation;
|
||||||
use Icinga\Web\Navigation\NavigationItem;
|
use Icinga\Web\Navigation\NavigationItem;
|
||||||
use Icinga\Web\Navigation\Renderer\NavigationItemRenderer;
|
use RecursiveIteratorIterator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renderer for multi level navigation
|
* Renderer for multi level navigation
|
||||||
|
@ -139,6 +139,7 @@ class StyleSheet
|
|||||||
$this->lessCompiler->addLessFile($this->pubPath . '/' . $lessFile);
|
$this->lessCompiler->addLessFile($this->pubPath . '/' . $lessFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$auth = Auth::getInstance();
|
||||||
$mm = $this->app->getModuleManager();
|
$mm = $this->app->getModuleManager();
|
||||||
|
|
||||||
foreach ($mm->getLoadedModules() as $moduleName => $module) {
|
foreach ($mm->getLoadedModules() as $moduleName => $module) {
|
||||||
@ -157,7 +158,6 @@ class StyleSheet
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (! (bool) $themingConfig->get('disabled', false)) {
|
if (! (bool) $themingConfig->get('disabled', false)) {
|
||||||
$auth = Auth::getInstance();
|
|
||||||
if ($auth->isAuthenticated()) {
|
if ($auth->isAuthenticated()) {
|
||||||
$userTheme = $auth->getUser()->getPreferences()->getValue('icingaweb', 'theme');
|
$userTheme = $auth->getUser()->getPreferences()->getValue('icingaweb', 'theme');
|
||||||
if ($userTheme !== null) {
|
if ($userTheme !== null) {
|
||||||
@ -174,7 +174,7 @@ class StyleSheet
|
|||||||
Logger::warning(sprintf(
|
Logger::warning(sprintf(
|
||||||
'Theme "%s" set by user "%s" has not been found.',
|
'Theme "%s" set by user "%s" has not been found.',
|
||||||
$theme,
|
$theme,
|
||||||
($user = Auth::getInstance()->getUser()) !== null ? $user->getUsername() : 'anonymous'
|
$auth->isAuthenticated() ? $auth->getUser()->getUsername() : 'anonymous'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,10 +184,10 @@ class StyleSheet
|
|||||||
}
|
}
|
||||||
|
|
||||||
$mode = 'none';
|
$mode = 'none';
|
||||||
if ($user = Auth::getInstance()->getUser()) {
|
if ($auth->isAuthenticated()) {
|
||||||
$file = $themePath !== null ? @file_get_contents($themePath) : false;
|
$file = $themePath !== null ? @file_get_contents($themePath) : false;
|
||||||
if (! $file || strpos($file, self::LIGHT_MODE_IDENTIFIER) !== 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);
|
$urlParts = parse_url($url);
|
||||||
if (isset($urlParts['scheme']) && (
|
if ((isset($urlParts['scheme']) && $urlParts['scheme'] !== $request->getScheme())
|
||||||
$urlParts['scheme'] !== $request->getScheme()
|
|
||||||
|| (isset($urlParts['host']) && $urlParts['host'] !== $request->getServer('SERVER_NAME'))
|
|| (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();
|
$urlObject->setIsExternal();
|
||||||
}
|
}
|
||||||
|
@ -205,7 +205,8 @@ class View extends Zend_View_Abstract
|
|||||||
'th-thumb-empty' => true,
|
'th-thumb-empty' => true,
|
||||||
'github-circled' => true,
|
'github-circled' => true,
|
||||||
'history' => true,
|
'history' => true,
|
||||||
'binoculars' => true
|
'binoculars' => true,
|
||||||
|
'letter' => true
|
||||||
//</editor-fold>
|
//</editor-fold>
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
namespace Icinga\Web\Widget\Dashboard;
|
namespace Icinga\Web\Widget\Dashboard;
|
||||||
|
|
||||||
|
use Icinga\Web\Session;
|
||||||
use Icinga\Web\Url;
|
use Icinga\Web\Url;
|
||||||
use Icinga\Data\ConfigObject;
|
use Icinga\Data\ConfigObject;
|
||||||
use Icinga\Exception\IcingaException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dashboard pane dashlet
|
* A dashboard pane dashlet
|
||||||
@ -57,18 +57,15 @@ class Dashlet extends UserWidget
|
|||||||
*/
|
*/
|
||||||
private $template =<<<'EOD'
|
private $template =<<<'EOD'
|
||||||
|
|
||||||
<div class="container" data-icinga-url="{URL}">
|
<div class="container" data-icinga-url="{URL}" data-url-hash="{URL_HASH}">
|
||||||
<h1><a href="{FULL_URL}" aria-label="{TOOLTIP}" title="{TOOLTIP}" data-base-target="col1">{TITLE}</a></h1>
|
<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>
|
<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>
|
</div>
|
||||||
EOD;
|
EOD;
|
||||||
|
|
||||||
@ -250,13 +247,22 @@ EOD;
|
|||||||
|
|
||||||
$url = $this->getUrl();
|
$url = $this->getUrl();
|
||||||
$url->setParam('showCompact', true);
|
$url->setParam('showCompact', true);
|
||||||
$iframeUrl = clone $url;
|
$fullUrl = $url->getUrlWithout(['showCompact', 'limit', 'view']);
|
||||||
$iframeUrl->setParam('isIframe');
|
|
||||||
|
$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(
|
$searchTokens = array(
|
||||||
'{URL}',
|
'{URL}',
|
||||||
'{IFRAME_URL}',
|
'{URL_HASH}',
|
||||||
'{FULL_URL}',
|
'{FULL_URL}',
|
||||||
|
'{FULL_URL_HASH}',
|
||||||
'{TOOLTIP}',
|
'{TOOLTIP}',
|
||||||
'{TITLE}',
|
'{TITLE}',
|
||||||
'{TITLE_PREFIX}',
|
'{TITLE_PREFIX}',
|
||||||
@ -265,8 +271,9 @@ EOD;
|
|||||||
|
|
||||||
$replaceTokens = array(
|
$replaceTokens = array(
|
||||||
$url,
|
$url,
|
||||||
$iframeUrl,
|
$urlHash,
|
||||||
$url->getUrlWithout(['showCompact', 'limit', 'view']),
|
$fullUrl,
|
||||||
|
$fullUrlHash,
|
||||||
sprintf($view->translate('Show %s', 'dashboard.dashlet.tooltip'), $view->escape($this->getTitle())),
|
sprintf($view->translate('Show %s', 'dashboard.dashlet.tooltip'), $view->escape($this->getTitle())),
|
||||||
$view->escape($this->getTitle()),
|
$view->escape($this->getTitle()),
|
||||||
$view->translate('Dashlet') . ': ',
|
$view->translate('Dashlet') . ': ',
|
||||||
|
@ -3,17 +3,17 @@
|
|||||||
|
|
||||||
namespace Icinga\Web\Widget;
|
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\Filterable;
|
||||||
use Icinga\Data\FilterColumns;
|
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\Exception\ProgrammingError;
|
||||||
use Icinga\Web\Notification;
|
use Icinga\Web\Notification;
|
||||||
use Exception;
|
use Icinga\Web\Url;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter
|
* Filter
|
||||||
@ -737,10 +737,11 @@ class FilterEditor extends AbstractWidget
|
|||||||
$preservedUrl = $this->preservedUrl();
|
$preservedUrl = $this->preservedUrl();
|
||||||
|
|
||||||
$html = ' <form method="post" class="search inline" action="'
|
$html = ' <form method="post" class="search inline" action="'
|
||||||
. $preservedUrl
|
. $preservedUrl
|
||||||
. '"><input type="text" name="q" class="search search-input" value="" placeholder="'
|
. '"><i class="icon fa-search fa search-icon"></i>'
|
||||||
. t('Search...')
|
. '<input type="text" name="q" class="search search-input" value="" placeholder="'
|
||||||
. '" /></form>';
|
. t('Search...')
|
||||||
|
. '" /></form>';
|
||||||
|
|
||||||
if ($this->filter->isEmpty()) {
|
if ($this->filter->isEmpty()) {
|
||||||
$title = t('Filter this list');
|
$title = t('Filter this list');
|
||||||
|
@ -10,6 +10,7 @@ use ipl\Html\FormElement\InputElement;
|
|||||||
use ipl\Html\HtmlElement;
|
use ipl\Html\HtmlElement;
|
||||||
use ipl\Web\Control\SearchBar\Suggestions;
|
use ipl\Web\Control\SearchBar\Suggestions;
|
||||||
use ipl\Web\Url;
|
use ipl\Web\Url;
|
||||||
|
use ipl\Web\Widget\Icon;
|
||||||
|
|
||||||
class SingleValueSearchControl extends Form
|
class SingleValueSearchControl extends Form
|
||||||
{
|
{
|
||||||
@ -106,6 +107,8 @@ class SingleValueSearchControl extends Form
|
|||||||
{
|
{
|
||||||
$suggestionsId = Icinga::app()->getRequest()->protectId('single-value-suggestions');
|
$suggestionsId = Icinga::app()->getRequest()->protectId('single-value-suggestions');
|
||||||
|
|
||||||
|
$this->addHtml(new Icon('search', Attributes::create(['class' => 'search-icon'])));
|
||||||
|
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'text',
|
'text',
|
||||||
$this->searchParameter,
|
$this->searchParameter,
|
||||||
|
@ -112,7 +112,7 @@ class Window
|
|||||||
{
|
{
|
||||||
if (! isset(static::$window)) {
|
if (! isset(static::$window)) {
|
||||||
$id = Icinga::app()->getRequest()->getHeader('X-Icinga-WindowId');
|
$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();
|
Icinga::app()->getResponse()->setOverrideWindowId();
|
||||||
$id = static::generateId();
|
$id = static::generateId();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Module: doc
|
Module: doc
|
||||||
Version: 2.12.2
|
Version: 2.12.5
|
||||||
Description: Documentation module
|
Description: Documentation module
|
||||||
Extracts, shows and exports documentation for Icinga Web 2 and its modules.
|
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+ */
|
/*! 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
|
// General styles
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@ -84,7 +72,7 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tbody > tr:nth-child(odd) {
|
tbody > tr:nth-child(odd) {
|
||||||
.gradient()
|
background: @gray-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody > tr:nth-child(even) {
|
tbody > tr:nth-child(even) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Module: migrate
|
Module: migrate
|
||||||
Version: 2.12.2
|
Version: 2.12.5
|
||||||
Description: Migrate module
|
Description: Migrate module
|
||||||
This module was introduced with the domain-aware authentication feature in version 2.5.0.
|
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.
|
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'
|
. ' the host or service that is having problems. Make sure you enter a brief description of'
|
||||||
. ' what you are doing.'
|
. ' what you are doing.'
|
||||||
),
|
),
|
||||||
'attribs' => array('class' => 'autofocus')
|
'attribs' => array('class' => 'autofocus'),
|
||||||
|
'value' => $config->get('settings', 'acknowledge_comment_text')
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
|
@ -227,7 +227,7 @@ class BackendConfigForm extends ConfigForm
|
|||||||
'autosubmit' => true
|
'autosubmit' => true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$resourceName = isset($formData['resource']) ? $formData['resource'] : $this->getValue('resource');
|
$resourceName = $this->getView()->escape($formData['resource'] ?? $this->getValue('resource'));
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'note',
|
'note',
|
||||||
'resource_note',
|
'resource_note',
|
||||||
|
@ -284,10 +284,10 @@ $section->add(N_('Timeline'), array(
|
|||||||
/*
|
/*
|
||||||
* Reporting Section
|
* Reporting Section
|
||||||
*/
|
*/
|
||||||
$section = $this->menuSection(N_('Reporting'), array(
|
$section = $this->menuSection(N_('Reporting'), [
|
||||||
'icon' => 'barchart',
|
'icon' => 'fa-chart-simple',
|
||||||
'priority' => 100
|
'priority' => 100
|
||||||
));
|
]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Current Incidents
|
* Current Incidents
|
||||||
|
@ -21,6 +21,7 @@ by this module.
|
|||||||
|
|
||||||
Option | Description
|
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 | **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_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)**.
|
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_notification' => 'UNIX_TIMESTAMP(ss.last_notification)',
|
||||||
'service_last_state_change' => 'UNIX_TIMESTAMP(ss.last_state_change)',
|
'service_last_state_change' => 'UNIX_TIMESTAMP(ss.last_state_change)',
|
||||||
'service_last_state_change_ts' => 'ss.last_state_change',
|
'service_last_state_change_ts' => 'ss.last_state_change',
|
||||||
'service_last_time_critical' => 'ss.last_time_critical',
|
'service_last_time_critical' => 'UNIX_TIMESTAMP(ss.last_time_critical)',
|
||||||
'service_last_time_ok' => 'ss.last_time_ok',
|
'service_last_time_ok' => 'UNIX_TIMESTAMP(ss.last_time_ok)',
|
||||||
'service_last_time_unknown' => 'ss.last_time_unknown',
|
'service_last_time_unknown' => 'UNIX_TIMESTAMP(ss.last_time_unknown)',
|
||||||
'service_last_time_warning' => 'ss.last_time_warning',
|
'service_last_time_warning' => 'UNIX_TIMESTAMP(ss.last_time_warning)',
|
||||||
'service_long_output' => 'ss.long_output',
|
'service_long_output' => 'ss.long_output',
|
||||||
'service_max_check_attempts' => 'ss.max_check_attempts',
|
'service_max_check_attempts' => 'ss.max_check_attempts',
|
||||||
'service_modified_service_attributes' => 'ss.modified_service_attributes',
|
'service_modified_service_attributes' => 'ss.modified_service_attributes',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Module: monitoring
|
Module: monitoring
|
||||||
Version: 2.12.2
|
Version: 2.12.5
|
||||||
Description: Icinga monitoring module
|
Description: Icinga monitoring module
|
||||||
IDO accessor and UI for your monitoring. This is the initial instalment for a
|
IDO accessor and UI for your monitoring. This is the initial instalment for a
|
||||||
graphical presentation of Icinga environments. The predecessor of Icinga DB.
|
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)
|
* --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]
|
* --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 \
|
* icingacli setup config webserver apache \
|
||||||
* --file=/etc/apache2/conf.d/icingaweb2.conf
|
* --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 \
|
* icingacli setup config webserver nginx \
|
||||||
* --root=/usr/share/icingaweb2/public \
|
* --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()
|
public function webserverAction()
|
||||||
{
|
{
|
||||||
@ -157,10 +165,18 @@ class ConfigCommand extends Command
|
|||||||
|
|
||||||
$enableFpm = $this->params->shift('enable-fpm', $webserver->getEnableFpm());
|
$enableFpm = $this->params->shift('enable-fpm', $webserver->getEnableFpm());
|
||||||
|
|
||||||
$fpmUri = trim($this->params->get('fpm-uri', $webserver->getFpmUri()));
|
$fpmSocketPath = trim($this->params->get('fpm-socket-path', $webserver->getFpmSocketPath()));
|
||||||
if (empty($fpmUri)) {
|
$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(
|
$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
|
$webserver
|
||||||
@ -168,7 +184,8 @@ class ConfigCommand extends Command
|
|||||||
->setConfigDir($configDir)
|
->setConfigDir($configDir)
|
||||||
->setUrlPath($urlPath)
|
->setUrlPath($urlPath)
|
||||||
->setEnableFpm($enableFpm)
|
->setEnableFpm($enableFpm)
|
||||||
->setFpmUri($fpmUri);
|
->setFpmUrl($fpmUrl)
|
||||||
|
->setFpmSocketPath($fpmSocketPath);
|
||||||
$config = $webserver->generate() . "\n";
|
$config = $webserver->generate() . "\n";
|
||||||
if (($file = $this->params->get('file')) !== null) {
|
if (($file = $this->params->get('file')) !== null) {
|
||||||
if (file_exists($file) === true) {
|
if (file_exists($file) === true) {
|
||||||
|
@ -91,6 +91,19 @@ class DatabaseCreationPage extends Form
|
|||||||
*/
|
*/
|
||||||
public function createElements(array $formData)
|
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'];
|
$skipValidation = isset($formData['skip_validation']) && $formData['skip_validation'];
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'text',
|
'text',
|
||||||
|
@ -602,7 +602,7 @@ class WebWizard extends Wizard implements SetupWizard
|
|||||||
)));
|
)));
|
||||||
|
|
||||||
$set->add(new WebLibraryRequirement(array(
|
$set->add(new WebLibraryRequirement(array(
|
||||||
'condition' => ['icinga-php-library', '>=', '0.13.2'],
|
'condition' => ['icinga-php-library', '>=', '0.14.2'],
|
||||||
'alias' => 'Icinga PHP library',
|
'alias' => 'Icinga PHP library',
|
||||||
'description' => mt(
|
'description' => mt(
|
||||||
'setup',
|
'setup',
|
||||||
|
@ -33,11 +33,25 @@ abstract class Webserver
|
|||||||
protected $configDir;
|
protected $configDir;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address or path where to pass requests to FPM
|
* Address where to pass requests to FPM
|
||||||
*
|
*
|
||||||
* @var string
|
* @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
|
* Enable to pass requests to FPM
|
||||||
@ -72,6 +86,7 @@ abstract class Webserver
|
|||||||
public function generate()
|
public function generate()
|
||||||
{
|
{
|
||||||
$template = $this->getTemplate();
|
$template = $this->getTemplate();
|
||||||
|
$fpmUri = $this->createFpmUri();
|
||||||
|
|
||||||
$searchTokens = array(
|
$searchTokens = array(
|
||||||
'{urlPath}',
|
'{urlPath}',
|
||||||
@ -85,7 +100,7 @@ abstract class Webserver
|
|||||||
$this->getDocumentRoot(),
|
$this->getDocumentRoot(),
|
||||||
preg_match('~/$~', $this->getUrlPath()) ? $this->getDocumentRoot() . '/' : $this->getDocumentRoot(),
|
preg_match('~/$~', $this->getUrlPath()) ? $this->getDocumentRoot() . '/' : $this->getDocumentRoot(),
|
||||||
$this->getConfigDir(),
|
$this->getConfigDir(),
|
||||||
$this->getFpmUri()
|
$fpmUri
|
||||||
);
|
);
|
||||||
$template = str_replace($searchTokens, $replaceTokens, $template);
|
$template = str_replace($searchTokens, $replaceTokens, $template);
|
||||||
return $template;
|
return $template;
|
||||||
@ -98,6 +113,13 @@ abstract class Webserver
|
|||||||
*/
|
*/
|
||||||
abstract protected function getTemplate();
|
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
|
* 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
|
* @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
|
* @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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,21 @@ use Icinga\Module\Setup\Webserver;
|
|||||||
*/
|
*/
|
||||||
class Apache extends 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()
|
protected function getTemplate()
|
||||||
{
|
{
|
||||||
@ -23,7 +37,7 @@ Alias {urlPath} "{aliasDocumentRoot}"
|
|||||||
# # Forward PHP requests to FPM
|
# # Forward PHP requests to FPM
|
||||||
# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||||
# <LocationMatch "^{urlPath}/(.*\.php)$">
|
# <LocationMatch "^{urlPath}/(.*\.php)$">
|
||||||
# ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
|
# ProxyPassMatch "{fpmUri}/{documentRoot}/$1"
|
||||||
# </LocationMatch>
|
# </LocationMatch>
|
||||||
#</IfVersion>
|
#</IfVersion>
|
||||||
|
|
||||||
@ -71,7 +85,7 @@ Alias {urlPath} "{aliasDocumentRoot}"
|
|||||||
# # Forward PHP requests to FPM
|
# # Forward PHP requests to FPM
|
||||||
# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||||
# <FilesMatch "\.php$">
|
# <FilesMatch "\.php$">
|
||||||
# SetHandler "proxy:fcgi://{fpmUri}"
|
# SetHandler "proxy:{fpmUri}"
|
||||||
# ErrorDocument 503 {urlPath}/error_unavailable.html
|
# ErrorDocument 503 {urlPath}/error_unavailable.html
|
||||||
# </FilesMatch>
|
# </FilesMatch>
|
||||||
# </IfVersion>
|
# </IfVersion>
|
||||||
@ -85,7 +99,7 @@ Alias {urlPath} "{aliasDocumentRoot}"
|
|||||||
# Forward PHP requests to FPM
|
# Forward PHP requests to FPM
|
||||||
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||||
<LocationMatch "^{urlPath}/(.*\.php)$">
|
<LocationMatch "^{urlPath}/(.*\.php)$">
|
||||||
ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
|
ProxyPassMatch "{fpmUri}/{documentRoot}/$1"
|
||||||
</LocationMatch>
|
</LocationMatch>
|
||||||
</IfVersion>
|
</IfVersion>
|
||||||
|
|
||||||
@ -131,7 +145,7 @@ Alias {urlPath} "{aliasDocumentRoot}"
|
|||||||
# Forward PHP requests to FPM
|
# Forward PHP requests to FPM
|
||||||
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||||
<FilesMatch "\.php$">
|
<FilesMatch "\.php$">
|
||||||
SetHandler "proxy:fcgi://{fpmUri}"
|
SetHandler "proxy:{fpmUri}"
|
||||||
ErrorDocument 503 {urlPath}/error_unavailable.html
|
ErrorDocument 503 {urlPath}/error_unavailable.html
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
</IfVersion>
|
</IfVersion>
|
||||||
|
@ -10,10 +10,15 @@ use Icinga\Module\Setup\Webserver;
|
|||||||
*/
|
*/
|
||||||
class Nginx extends Webserver
|
class Nginx extends Webserver
|
||||||
{
|
{
|
||||||
protected $fpmUri = '127.0.0.1:9000';
|
protected $fpmUrl = '127.0.0.1:9000';
|
||||||
|
|
||||||
protected $enableFpm = true;
|
protected $enableFpm = true;
|
||||||
|
|
||||||
|
protected function createFpmUri()
|
||||||
|
{
|
||||||
|
return empty($this->fpmSocketPath) ? $this->fpmUrl : $this->fpmSocketSchema . $this->fpmSocketPath;
|
||||||
|
}
|
||||||
|
|
||||||
protected function getTemplate()
|
protected function getTemplate()
|
||||||
{
|
{
|
||||||
return <<<'EOD'
|
return <<<'EOD'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Module: setup
|
Module: setup
|
||||||
Version: 2.12.2
|
Version: 2.12.5
|
||||||
Description: Setup module
|
Description: Setup module
|
||||||
Web based wizard for setting up Icinga Web 2 and its modules.
|
Web based wizard for setting up Icinga Web 2 and its modules.
|
||||||
This includes the data backends (e.g. relational database, LDAP),
|
This includes the data backends (e.g. relational database, LDAP),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Module: test
|
Module: test
|
||||||
Version: 2.12.2
|
Version: 2.12.5
|
||||||
Description: Translation module
|
Description: Translation module
|
||||||
This module allows developers to run (unit) tests against Icinga Web 2 and
|
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.
|
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
|
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>
|
## Testing Translations <a id="module-translation-tests"></a>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Module: translation
|
Module: translation
|
||||||
Version: 2.12.2
|
Version: 2.12.5
|
||||||
Description: Translation module
|
Description: Translation module
|
||||||
This module allows developers and translators to translate modules for multiple
|
This module allows developers and translators to translate modules for multiple
|
||||||
languages. You do not need this module to run an internationalized web frontend.
|
languages. You do not need this module to run an internationalized web frontend.
|
||||||
|
@ -234,7 +234,7 @@
|
|||||||
border: 1px solid @gray-lighter;
|
border: 1px solid @gray-lighter;
|
||||||
background: @body-bg-color;
|
background: @body-bg-color;
|
||||||
box-shadow: 0 0 1em 0 rgba(0,0,0,.25);
|
box-shadow: 0 0 1em 0 rgba(0,0,0,.25);
|
||||||
z-index: 1;
|
z-index: 15;
|
||||||
.rounded-corners();
|
.rounded-corners();
|
||||||
|
|
||||||
a {
|
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,
|
.controls input.search,
|
||||||
input.search {
|
input.search {
|
||||||
.transition(border 0.3s ease);
|
.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;
|
outline: none;
|
||||||
padding-left: 1.5em;
|
padding-left: 1.5em;
|
||||||
width: 20em;
|
width: 20em;
|
||||||
|
|
||||||
&:focus {
|
|
||||||
background-image: url(../img/icons/search_icinga_blue.png);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus:not([readonly]) {
|
&:focus:not([readonly]) {
|
||||||
border-color: @icinga-blue;
|
border-color: @icinga-blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@light-mode: {
|
form:has(input.search:focus) > .search-icon {
|
||||||
#menu input.search,
|
color: @icinga-blue;
|
||||||
.controls input.search,
|
}
|
||||||
input.search {
|
|
||||||
background-image: url(../img/icons/search.png);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
.backend-selection,
|
.backend-selection,
|
||||||
.pagination-control,
|
.pagination-control,
|
||||||
|
@ -303,7 +303,7 @@
|
|||||||
|
|
||||||
> .badge {
|
> .badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: .5em;
|
right: 1em;
|
||||||
bottom: .25em;
|
bottom: .25em;
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -330,7 +330,8 @@
|
|||||||
padding-left: .75em;
|
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;
|
font-size: 1.5em;
|
||||||
margin-right: .5em;
|
margin-right: .5em;
|
||||||
}
|
}
|
||||||
|
@ -282,6 +282,39 @@ a:hover > .icon-cancel {
|
|||||||
|
|
||||||
// Responsive iFrames
|
// 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 {
|
.iframe-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 0;
|
height: 0;
|
||||||
@ -295,6 +328,7 @@ a:hover > .icon-cancel {
|
|||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
#menu [class^="icon-"],
|
#menu [class^="icon-"],
|
||||||
#menu [class*=" icon-"] {
|
#menu [class*=" icon-"] {
|
||||||
&:before {
|
&::before {
|
||||||
width: 1.5em;
|
width: 1.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@nav-item-height: 3.166666667em; // 38px
|
||||||
@icon-width: 1.7em; // 1.5em width + 0.2em right margin
|
@icon-width: 1.7em; // 1.5em width + 0.2em right margin
|
||||||
|
|
||||||
#menu {
|
#menu {
|
||||||
@ -15,6 +16,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu .nav-item {
|
#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
|
// Respect overflowing content
|
||||||
overflow: hidden;
|
.text-ellipsis();
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item {
|
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item {
|
||||||
@ -51,7 +53,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#menu .nav-level-1 > .nav-item {
|
#menu .nav-level-1 > .nav-item {
|
||||||
line-height: 2.167em; // 26 px
|
|
||||||
color: @menu-color;
|
color: @menu-color;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@ -69,7 +70,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> a {
|
> 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,
|
&.active:not(.selected) > a:focus,
|
||||||
@ -87,7 +90,7 @@
|
|||||||
opacity: .8;
|
opacity: .8;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > a > .icon-letter:before {
|
& > a > .icon-letter::before {
|
||||||
content: attr(data-letter);
|
content: attr(data-letter);
|
||||||
font-family: @font-family;
|
font-family: @font-family;
|
||||||
font-weight: 800;
|
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 {
|
#menu ul:not(.nav-level-2) > .selected > a {
|
||||||
background-color: @menu-highlight-color;
|
background-color: @menu-highlight-color;
|
||||||
color: @text-color-inverted;
|
color: @text-color-inverted;
|
||||||
@ -103,7 +110,7 @@
|
|||||||
background-color: @menu-highlight-hover-bg-color;
|
background-color: @menu-highlight-hover-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
.transform(rotate(45deg));
|
.transform(rotate(45deg));
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -114,8 +121,9 @@
|
|||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
height: 1.25em;
|
height: 1.25em;
|
||||||
margin-top: -1.75em;
|
|
||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
|
top: ~"calc(50% - (1.25em / 2))";
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +131,7 @@
|
|||||||
// Collapse menu by default
|
// Collapse menu by default
|
||||||
display: none;
|
display: none;
|
||||||
line-height: 1.833em; // 22px
|
line-height: 1.833em; // 22px
|
||||||
|
z-index: 12;
|
||||||
|
|
||||||
> a {
|
> a {
|
||||||
color: @menu-2ndlvl-color;
|
color: @menu-2ndlvl-color;
|
||||||
@ -140,7 +149,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Little caret on active level-2 item
|
// Little caret on active level-2 item
|
||||||
&.active:after {
|
&.active::after {
|
||||||
.transform(rotate(45deg));
|
.transform(rotate(45deg));
|
||||||
|
|
||||||
background-color: @body-bg-color;
|
background-color: @body-bg-color;
|
||||||
@ -150,7 +159,7 @@
|
|||||||
height: 1.25em;
|
height: 1.25em;
|
||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: .5em;
|
top: ~"calc(50% - (1.25em / 2))";
|
||||||
right: -.75em;
|
right: -.75em;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
@ -204,9 +213,12 @@
|
|||||||
opacity: .6;
|
opacity: .6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu .search-icon {
|
||||||
|
left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
#menu input.search {
|
#menu input.search {
|
||||||
background: transparent url('../img/icons/search_white.png') no-repeat 1em center;
|
background-color: @menu-bg-color;
|
||||||
background-size: 1em auto;
|
|
||||||
border: none;
|
border: none;
|
||||||
color: @menu-color;
|
color: @menu-color;
|
||||||
line-height: 2.167em;
|
line-height: 2.167em;
|
||||||
@ -233,7 +245,7 @@
|
|||||||
|
|
||||||
// Badge offset correction
|
// Badge offset correction
|
||||||
#menu > nav > .nav-level-1 > .badge-nav-item > a > .badge {
|
#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 {
|
#menu .nav-level-2 > .badge-nav-item > a > .badge {
|
||||||
@ -241,6 +253,10 @@
|
|||||||
margin-right: .5em
|
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
|
// Hovered menu
|
||||||
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item.hover,
|
#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 {
|
#layout:not(.minimal-layout) #menu .nav-level-1 > .nav-item:not(.active).hover {
|
||||||
@ -250,12 +266,15 @@
|
|||||||
border-color: @gray-light;
|
border-color: @gray-light;
|
||||||
border-radius: .25em;
|
border-radius: .25em;
|
||||||
box-shadow: 0 0 1em 0 rgba(0,0,0,.3);
|
box-shadow: 0 0 1em 0 rgba(0,0,0,.3);
|
||||||
padding: @vertical-padding 0;
|
padding-bottom: @vertical-padding;
|
||||||
width: 14em;
|
width: 14em;
|
||||||
position: fixed;
|
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));
|
.transform(rotate(45deg));
|
||||||
|
|
||||||
background-color: @body-bg-color;
|
background-color: @body-bg-color;
|
||||||
@ -263,17 +282,20 @@
|
|||||||
border-left: 1px solid @gray-light;
|
border-left: 1px solid @gray-light;
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
height: 1.1em;
|
height: var(--caretSide);
|
||||||
width: 1.1em;
|
width: var(--caretSide);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1em;
|
top: ~"calc(@{nav-item-height} / 2 - var(--caretSide) / 2)";
|
||||||
left: -.6em;
|
left: ~"calc(-1 * var(--caretSide) / 2 - 1px)";
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bottom-up:after {
|
&.bottom-up {
|
||||||
top: unset;
|
--caretY: 100%;
|
||||||
bottom: 1em;
|
margin-top: 1px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
top: ~"calc(var(--caretY) - (@{nav-item-height} / 2) - (var(--caretSide) / 2))";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .nav-item {
|
> .nav-item {
|
||||||
@ -303,7 +325,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide activity caret when displayed as flyout
|
// Hide activity caret when displayed as flyout
|
||||||
&:after {
|
&::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -320,17 +342,13 @@
|
|||||||
|
|
||||||
#layout:not(.minimal-layout) #menu .nav-level-1 > .nav-item:not(.active).hover {
|
#layout:not(.minimal-layout) #menu .nav-level-1 > .nav-item:not(.active).hover {
|
||||||
> .nav-level-2 {
|
> .nav-level-2 {
|
||||||
// Position relative to parent
|
|
||||||
margin-left: 16em;
|
margin-left: 16em;
|
||||||
margin-top: -3.167em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item.hover {
|
#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item.hover {
|
||||||
> .nav-level-2 {
|
> .nav-level-2 {
|
||||||
// Position relative to parent
|
|
||||||
margin-left: 4em;
|
margin-left: 4em;
|
||||||
margin-top: -3.333em;
|
|
||||||
|
|
||||||
> .badge-nav-item {
|
> .badge-nav-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -508,12 +526,34 @@ html.no-js #toggle-sidebar {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#open-sidebar:before,
|
#open-sidebar::before,
|
||||||
#close-sidebar:before {
|
#close-sidebar::before {
|
||||||
width: 1.4em;
|
width: 1.4em;
|
||||||
margin-right: 0;
|
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 {
|
#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item.active .nav-level-2 > li {
|
||||||
&.nav-item:not(.badge-nav-item) {
|
&.nav-item:not(.badge-nav-item) {
|
||||||
&:not(.selected):not(.active) a:hover,
|
&: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.active .nav-level-2 > li,
|
||||||
#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item:not(.active).hover .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 {
|
&.badge-nav-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
border-top: none;
|
border-top: none;
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
min-width: 14em;
|
min-width: 14em;
|
||||||
z-index: 10;
|
z-index: 1001;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs > .dropdown-nav-item > ul > li:hover > a {
|
.tabs > .dropdown-nav-item > ul > li:hover > a {
|
||||||
|
@ -83,7 +83,9 @@
|
|||||||
if ($container[0].contains(origFocus)
|
if ($container[0].contains(origFocus)
|
||||||
&& origFocus.form
|
&& origFocus.form
|
||||||
&& ! origFocus.matches(
|
&& ! 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');
|
this.icinga.logger.debug('Not changing content for ' + containerId + ' form has focus');
|
||||||
|
@ -6,15 +6,24 @@
|
|||||||
|
|
||||||
Icinga.Behaviors = Icinga.Behaviors || {};
|
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) {
|
var Navigation = function (icinga) {
|
||||||
Icinga.EventListener.call(this, icinga);
|
Icinga.EventListener.call(this, icinga);
|
||||||
this.on('click', '#menu a', this.linkClicked, this);
|
this.on('click', '#menu a', this.linkClicked, this);
|
||||||
this.on('click', '#menu tr[href]', this.linkClicked, this);
|
this.on('click', '#menu tr[href]', this.linkClicked, this);
|
||||||
this.on('rendered', '#menu', this.onRendered, 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('mouseleave', '#menu .primary-nav', this.hideFlyoutMenu, this);
|
||||||
this.on('click', '#toggle-sidebar', this.toggleSidebar, this);
|
this.on('click', '#toggle-sidebar', this.toggleSidebar, this);
|
||||||
|
|
||||||
this.on('click', '#menu .config-nav-item button', this.toggleConfigFlyout, 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('mouseenter', '#menu .config-menu .config-nav-item', this.showConfigFlyout, this);
|
||||||
this.on('mouseleave', '#menu .config-menu .config-nav-item', this.hideConfigFlyout, this);
|
this.on('mouseleave', '#menu .config-menu .config-nav-item', this.hideConfigFlyout, this);
|
||||||
@ -30,6 +39,21 @@
|
|||||||
*/
|
*/
|
||||||
this.active = null;
|
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
|
* 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
|
* @param e
|
||||||
*/
|
*/
|
||||||
Navigation.prototype.showFlyoutMenu = function(e) {
|
Navigation.prototype.onMouseEnter = function(e) {
|
||||||
var $layout = $('#layout');
|
const $layout = $('#layout');
|
||||||
|
const _this = e.data.self;
|
||||||
if ($layout.hasClass('minimal-layout')) {
|
if ($layout.hasClass('minimal-layout')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var $target = $(this);
|
const $target = $(this);
|
||||||
var $flyout = $target.find('.nav-level-2');
|
|
||||||
|
|
||||||
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');
|
$layout.removeClass('menu-hovered');
|
||||||
$target.siblings().not($target).removeClass('hover');
|
$target.siblings().not($target).removeClass('hover');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var delay = 300;
|
if (! $target.is(':hover')) {
|
||||||
|
return;
|
||||||
if ($layout.hasClass('menu-hovered')) {
|
|
||||||
delay = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(function() {
|
$layout.addClass('menu-hovered');
|
||||||
try {
|
_this.extendedFlyoutZone[0] = [e.clientX, e.clientY];
|
||||||
if (! $target.is(':hover')) {
|
_this.showFlyoutMenu($target);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
} catch(e) { /* Bypass because if IE8 */ }
|
|
||||||
|
|
||||||
$layout.addClass('menu-hovered');
|
/**
|
||||||
$target.siblings().not($target).removeClass('hover');
|
* Captures the mouse move events within the navigation item
|
||||||
$target.addClass('hover');
|
* 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();
|
const $target = $(this);
|
||||||
$flyout.css({
|
|
||||||
bottom: 'auto',
|
|
||||||
top: targetHeight
|
|
||||||
});
|
|
||||||
|
|
||||||
var rect = $flyout[0].getBoundingClientRect();
|
if (! $target[0].matches(':has(.nav-level-2)')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (rect.bottom > window.innerHeight) {
|
if (! $target.hasClass('hover')) {
|
||||||
$flyout.addClass('bottom-up');
|
if (
|
||||||
$flyout.css({
|
! _this.extendedFlyoutZone.includes(undefined)
|
||||||
bottom: window.innerHeight - targetHeight,
|
&& d3.polygonContains(_this.extendedFlyoutZone, [e.clientX, e.clientY])
|
||||||
top: 'auto'
|
) {
|
||||||
});
|
_this.flyoutTimer = setTimeout(function() {
|
||||||
|
_this.showFlyoutMenu($target);
|
||||||
|
}, 200);
|
||||||
} else {
|
} 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 $layout = $('#layout');
|
||||||
var $nav = $(e.currentTarget);
|
var $nav = $(e.currentTarget);
|
||||||
var $hovered = $nav.find('.nav-level-1 > .nav-item.hover');
|
var $hovered = $nav.find('.nav-level-1 > .nav-item.hover');
|
||||||
|
const _this = e.data.self;
|
||||||
|
_this.extendedFlyoutZone.fill(undefined);
|
||||||
|
|
||||||
if (! $hovered.length) {
|
if (! $hovered.length) {
|
||||||
$layout.removeClass('menu-hovered');
|
$layout.removeClass('menu-hovered');
|
||||||
|
@ -170,7 +170,21 @@
|
|||||||
var $dashlet = $(this);
|
var $dashlet = $(this);
|
||||||
var url = $dashlet.data('icingaUrl');
|
var url = $dashlet.data('icingaUrl');
|
||||||
if (typeof url !== 'undefined') {
|
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 $eventTarget = $(event.target);
|
||||||
var href = $a.attr('href');
|
var href = $a.attr('href');
|
||||||
var linkTarget = $a.attr('target');
|
var linkTarget = $a.attr('target');
|
||||||
|
const urlHash = this.dataset.urlHash;
|
||||||
var $target;
|
var $target;
|
||||||
var formerUrl;
|
var formerUrl;
|
||||||
|
|
||||||
@ -391,7 +406,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load link URL
|
// 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) {
|
if ($a.closest('#menu').length > 0) {
|
||||||
// Menu links should remove all but the first layout column
|
// Menu links should remove all but the first layout column
|
||||||
|
@ -242,6 +242,10 @@
|
|||||||
loadUrl: function (url, $target, data, method, action, autorefresh, progressTimer, extraHeaders) {
|
loadUrl: function (url, $target, data, method, action, autorefresh, progressTimer, extraHeaders) {
|
||||||
var id = null;
|
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
|
// Default method is GET
|
||||||
if ('undefined' === typeof method) {
|
if ('undefined' === typeof method) {
|
||||||
method = 'GET';
|
method = 'GET';
|
||||||
@ -1054,15 +1058,20 @@
|
|||||||
errorThrown + ':',
|
errorThrown + ':',
|
||||||
$(req.responseText).text().replace(/\s+/g, ' ').slice(0, 100)
|
$(req.responseText).text().replace(/\s+/g, ' ').slice(0, 100)
|
||||||
);
|
);
|
||||||
this.renderContentToContainer(
|
|
||||||
req.responseText,
|
if (req.status === 401) {
|
||||||
req.$target,
|
window.location.reload();
|
||||||
req.action,
|
} else {
|
||||||
req.autorefresh,
|
this.renderContentToContainer(
|
||||||
undefined,
|
req.responseText,
|
||||||
req.autosubmit,
|
req.$target,
|
||||||
req.scripted
|
req.action,
|
||||||
);
|
req.autorefresh,
|
||||||
|
undefined,
|
||||||
|
req.autosubmit,
|
||||||
|
req.scripted
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (errorThrown === 'abort') {
|
if (errorThrown === 'abort') {
|
||||||
this.icinga.logger.debug(
|
this.icinga.logger.debug(
|
||||||
|
@ -4,28 +4,6 @@
|
|||||||
|
|
||||||
'use strict';
|
'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
|
* Write timezone information into a cookie
|
||||||
*
|
*
|
||||||
@ -51,15 +29,11 @@
|
|||||||
* Write timezone information into cookie
|
* Write timezone information into cookie
|
||||||
*/
|
*/
|
||||||
writeTimezone: function() {
|
writeTimezone: function() {
|
||||||
var date = new Date();
|
|
||||||
var timezoneOffset = (date.getTimezoneOffset()*60) * -1;
|
|
||||||
var dst = date.isDst();
|
|
||||||
|
|
||||||
if (this.readCookie(this.cookieName)) {
|
if (this.readCookie(this.cookieName)) {
|
||||||
return;
|
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} name
|
||||||
* @param {String} value
|
* @param {String} value
|
||||||
* @param {Number} days
|
|
||||||
*/
|
*/
|
||||||
writeCookie: function(name, value, days) {
|
writeCookie: function(name, value) {
|
||||||
var expires = '';
|
document.cookie = name + '=' + value + '; path=/';
|
||||||
|
|
||||||
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=/';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,7 +21,7 @@ class LocalFileStorageTest extends BaseTestCase
|
|||||||
parent::__construct($name, $data, $dataName);
|
parent::__construct($name, $data, $dataName);
|
||||||
|
|
||||||
$this->oldErrorReportingLevel = error_reporting();
|
$this->oldErrorReportingLevel = error_reporting();
|
||||||
error_reporting(E_ALL | E_STRICT);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
|
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
|
||||||
if (error_reporting() === 0) {
|
if (error_reporting() === 0) {
|
||||||
@ -32,7 +32,6 @@ class LocalFileStorageTest extends BaseTestCase
|
|||||||
switch ($errno) {
|
switch ($errno) {
|
||||||
case E_NOTICE:
|
case E_NOTICE:
|
||||||
case E_WARNING:
|
case E_WARNING:
|
||||||
case E_STRICT:
|
|
||||||
case E_RECOVERABLE_ERROR:
|
case E_RECOVERABLE_ERROR:
|
||||||
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||||
}
|
}
|
||||||
|
@ -8,38 +8,42 @@ use Icinga\Util\TimezoneDetect;
|
|||||||
|
|
||||||
class TimezoneDetectTest extends BaseTestCase
|
class TimezoneDetectTest extends BaseTestCase
|
||||||
{
|
{
|
||||||
public function testPositiveTimezoneOffsetSeparatedByComma()
|
public function testInvalidTimezoneNameInCookie(): void
|
||||||
{
|
{
|
||||||
$this->assertTimezoneDetection('3600,0', 'Europe/Paris');
|
$tzDetect = new TimezoneDetect();
|
||||||
|
$tzDetect->reset();
|
||||||
|
|
||||||
|
$_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 testPositiveTimezoneOffsetSeparatedByHyphen()
|
public function testValidTimezoneNameInCookie(): void
|
||||||
{
|
|
||||||
$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)
|
|
||||||
{
|
{
|
||||||
$tzDetect = new TimezoneDetect();
|
$tzDetect = new TimezoneDetect();
|
||||||
$tzDetect->reset();
|
$tzDetect->reset();
|
||||||
|
|
||||||
$_COOKIE[TimezoneDetect::$cookieName] = $cookieValue;
|
$_COOKIE[TimezoneDetect::$cookieName] = "Europe/Berlin";
|
||||||
$tzDetect = new TimezoneDetect();
|
$tzDetect = new TimezoneDetect();
|
||||||
|
$this->assertTrue(
|
||||||
|
$tzDetect->success(),
|
||||||
|
true,
|
||||||
|
'Failed to assert that the valid timezone name is detected'
|
||||||
|
);
|
||||||
|
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
$tzDetect->getTimezoneName(),
|
$tzDetect->getTimezoneName(),
|
||||||
$expectedTimezoneName,
|
"Europe/Berlin",
|
||||||
'Failed asserting that the timezone "' . $expectedTimezoneName
|
'Failed to assert that the valid timezone name was correctly set'
|
||||||
. '" is being detected from the cookie value "' . $cookieValue . '"'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
public function testWhetherGetAbsoluteUrlReturnsTheGivenUsernameAndPassword()
|
||||||
{
|
{
|
||||||
$url = Url::fromPath('http://testusername:testpassword@testsite.com/path/to/my/url.html');
|
$url = Url::fromPath('http://testusername:testpassword@testsite.com/path/to/my/url.html');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user