Compare commits

..

63 Commits

Author SHA1 Message Date
Johannes Meyer
0070c807a7
Release v2.12.5 (#5391) 2025-07-16 10:30:33 +02:00
Johannes Meyer
8c5ffa1c61
Warn about db name wildcards (#5390) 2025-07-14 16:35:09 +02:00
Johannes Meyer
5f602113ee Raise version to v2.12.5 2025-07-14 13:35:37 +02:00
Johannes Meyer
a9cbc36213 Update CHANGELOG.md 2025-07-14 13:35:15 +02:00
Johannes Meyer
e2e9078d97 Update AUTHORS and .mailmap 2025-07-14 13:34:21 +02:00
Johannes Meyer
122ac8f600 setup: Warn users about wildcards in their database name 2025-07-14 11:32:18 +02:00
Johannes Meyer
451ec34775 migrations: Show a hint about wildcards in database names 2025-07-14 11:31:51 +02:00
Alexander A. Klimov
8345993480 Icinga.Loader#onFailure(): on 401, reload the whole page
instead of rendering 401 responses to all containers.
2025-07-08 16:56:52 +02:00
Sukhwinder Dhillon
09df8dcc5a Don't let active .nav-level-2 menu item overlap the config nav flyout
- Set higher z-index than `#sidebar.menu .nav-level-2`
2025-07-08 16:50:16 +02:00
Johannes Meyer
5d71d09e54 StyleSheet: Do not rely on config to trigger auth
fixes #5385
2025-07-08 16:47:02 +02:00
Johannes Meyer
fff524624e php: Fix workflow 2025-07-08 16:36:03 +02:00
Johannes Meyer
8f518e7bab php: Run on support branches, php84 and phpunit 9.6 2025-07-08 16:20:06 +02:00
Johannes Meyer
092571a17b L10n-update: Use template from github-actions 2025-06-03 16:20:35 +02:00
Florian Strohmaier
d56757f20f menu.less: Make caret appear connected to main viewport 2025-05-15 16:54:45 +02:00
Jan Schuppik
750948a729
Fix: spacing and text-breaking (#5355)
Fix #5244
2025-05-14 15:34:12 +02:00
Johannes Meyer
9a9113bd2b css: Center icons in collapsed menu with and without a scrollbar 2025-05-12 13:57:04 +02:00
Ravi Kumar Kempapura Srinivasa
63a73eab6f
Menu navigation: Improve flyout transition (#5367)
resolves #5160
2025-05-08 16:17:14 +02:00
Johannes Rauh
aa7a60c893 Replace search icon png with fontawesome icon 2025-05-08 08:37:37 +02:00
Sukhwinder Dhillon
0eea5cae51 ServicestatusQuery: Add missing support for relative time filter for (host|service)_last_time_* columns 2025-05-07 10:35:47 +02:00
Jan Schuppik
85621487e8
Fix: split fpm-uri flag in socket and url flag (#5364)
fixes #5249
2025-05-06 11:35:10 +02:00
Alexander Aleksandrovič Klimov
5333f2deb7
Disable deprecation warnings unless ICINGAWEB_ENVIRONMENT is set to "dev" (#5359)
in ApplicationBootstrap#setupErrorHandling(). In webrouter.php disable
them unconditionally (until ApplicationBootstrap#setupErrorHandling())
not to duplicate the check. ApplicationBootstrap#setupErrorHandling())
is called pretty early, so we won't miss much.

fixes #5117

* In addition, this provides #5269 and mitigates #5152 in production
where ICINGAWEB_ENVIRONMENT isn't set.
2025-05-05 17:58:28 +02:00
marianrh
b16cf68c86 Support setting a default comment for acknowledgements 2025-05-05 17:20:58 +02:00
Johannes Meyer
eafb6c95b7 menu: Align badges correctly and ellipsize text again 2025-04-30 16:46:31 +02:00
Johannes Rauh
05f9e7c5ee Show label as navigation flyout header 2025-04-30 15:29:59 +02:00
Johannes Meyer
6c57d32979
Fix menu caret positioning (#5363) 2025-04-30 15:18:14 +02:00
Johannes Meyer
6100281c89 menu: Always align the flyout caret with the menu item 2025-04-30 15:01:56 +02:00
Johannes Meyer
91396aeb73 css: Use a proper height for level 1 menu items
Helps with aligning the flyout caret next
2025-04-30 15:01:56 +02:00
Johannes Meyer
025ae944c5 css: Modernize pseudo element selectors in menu.less 2025-04-30 09:54:49 +02:00
Johannes Meyer
68c97fb6ff
Fix browser timezone information being lost (#5357)
fixes #5209
2025-04-28 16:30:53 +02:00
raviks789
1b363360f6
Add tests for detecting browser timezone name 2025-04-28 16:22:59 +02:00
raviks789
380b3688bd
TimezoneDetect: Verify the timezone name cookie 2025-04-28 15:12:37 +02:00
raviks789
ef4b59123e
timezone.js: Remove unnecessary days parameter in writeCookie method
Since the timezone cookie is not being expired, days parameter is not necessary in writeCookie method.
2025-04-24 10:38:43 +02:00
raviks789
2d265babf7
Fix browser timezone information being lost 2025-04-24 10:23:46 +02:00
Silas
ff04a2ea43
Fix that CSV exports contain empty strings instead of zeros 2025-04-07 13:46:21 +02:00
Johannes Meyer
6c7f1e5466 Dashlet: Properly embed iframe urls
fixes #5346
2025-04-01 11:19:00 +02:00
Johannes Meyer
219b11789b
Release/v2.12.4 (#5345) 2025-03-26 16:42:31 +01:00
Johannes Meyer
41bbf6e35d Update CHANGELOG.md 2025-03-26 16:41:23 +01:00
Johannes Meyer
e98a776509 Raise version to 2.12.4 2025-03-26 16:41:23 +01:00
Johannes Meyer
1ddd04df50 DbUserBackend: Fix broken password hash fetch routine
fixes #5343
2025-03-26 16:40:48 +01:00
Johannes Meyer
15e74ebb0c
Release/v2.12.3 (#5342) 2025-03-26 10:39:37 +01:00
Johannes Meyer
53fa6d57e1 Raise ipl requirement 2025-03-26 10:35:11 +01:00
Johannes Meyer
c07a45096c Update CHANGELOG.md 2025-03-26 10:31:04 +01:00
Johannes Meyer
01fb35dd4a Raise version to 2.12.3 2025-03-26 10:31:04 +01:00
Johannes Meyer
ec40efe157 Only open trusted iframe sources by default
Trusted in this case means, it was Icinga Web that
rendered a link and the user followed it. Whether
a source is trustworthy or not is detected by use
of the user's session id to hash it combined with
the source similar to how CSRF tokens are assembled.
2025-03-26 10:25:31 +01:00
Johannes Meyer
aad020511f js: Only load URLs prefixed by the base URL 2025-03-26 10:25:05 +01:00
Johannes Meyer
484bd26d63 Window: Only accept valid window IDs 2025-03-26 10:24:17 +01:00
Johannes Meyer
2b08d88edf Url: Always compare host and port to identify external urls 2025-03-26 10:23:31 +01:00
Johannes Meyer
191444ccd9 Pdf: Ensure dompdf can create temporary files
This is required since dompdf seems to automatically load
our custom font and complains otherwise.
2025-03-25 13:59:18 +01:00
Johannes Meyer
1a1f96be49 php: Install the same vendor dependencies for all php versions 2025-03-25 13:51:59 +01:00
Sukhwinder Dhillon
f1fe2525bd tabs.less: Don't let icinga-loader element overlap the dropdown nav 2025-03-20 17:48:37 +01:00
Johannes Meyer
d56d10c712 monitoring: Use the (new) icon for the reporting section
The same that the reporting module is using now
2025-03-20 16:58:45 +01:00
Markus Opolka
6c8453062f Fix doc module markdown table rows in light mode
- Replaced the gradient mixin with a simple color
  for odd rows. This fixes and markdown table rows
  in light mode and - since there are very gradients
  in general - it makes the overall look more uniform.

See https://github.com/Icinga/icingaweb2/issues/5320
2025-03-20 16:39:13 +01:00
Johannes Meyer
db851bbe33 Don't mention Twitter anymore, it's gone now for good 2025-03-20 16:36:52 +01:00
Johannes Meyer
d86ede517f
Fix case sensitive authentication with postgres (#5338)
fixes #5223
2025-03-20 16:34:30 +01:00
Johannes Meyer
92dad17a2b DbUserGroupBackend: Match memberships case-insensitive on pgsql 2025-03-20 16:24:21 +01:00
Johannes Meyer
13c9a73842 DbUserBackend: Match usernames case-insensitive on pgsql 2025-03-20 16:24:21 +01:00
Johannes Meyer
acfad5ae52
Fix unescaped error messages (#5329)
In both cases the input, which wasn't escaped before, comes from a form
element that doesn't allow any user to change its content. An ordinary
user would need to access the DOM in order to do that.

Both forms are protected by CSRF, so this mitigates any potential
exploit as well.
2025-03-20 16:20:21 +01:00
Johannes Meyer
14c0748693 Escape resource identifier in monitoring backend form 2025-03-20 14:56:53 +01:00
Johannes Meyer
02dece2a35 Escape invalid module name in error messages 2025-03-20 14:56:53 +01:00
Sukhwinder Dhillon
c6c1e28350 RolesConfig: Add missing column name for quick search 2025-03-20 14:30:45 +01:00
Blerim Sheqa
79971cb1a6 Fix broken links 2025-01-14 11:14:02 +01:00
Johannes Meyer
ca2778eb46 form.js: Ignore buttons again when being asked to render content
This slipped through, as I thought the CSS selector `:input`, which
was used previously, is invalid. Although, it's a jQuery specific
selector -.-, which was also previously used.

fixes #5293
2024-11-28 16:57:19 +01:00
Yoda-BZH
4eadfd0ace
View: Consider letter a legacy icon name 2024-11-28 16:55:59 +01:00
70 changed files with 818 additions and 368 deletions

View File

@ -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 }}"}'

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
v2.12.2 v2.12.5

View File

@ -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);
} }
} }

View File

@ -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'
))) )))
) )
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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') ?> &hellip;" placeholder="<?= $this->translate('Search') ?> &hellip;"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"> autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">

View File

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

View File

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

View File

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

View File

@ -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);
} }

View File

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

View File

@ -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'];

View File

@ -42,4 +42,9 @@ class RolesConfig extends IniRepository
return $columns; return $columns;
} }
protected function initializeSearchColumns(): array
{
return ['name'];
}
} }

View File

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

View File

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

View File

@ -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";
} }

View File

@ -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();

View File

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

View File

@ -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);
}
} }

View File

@ -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));
} }

View File

@ -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();
}
}

View File

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

View File

@ -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);
} }
} }

View File

@ -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();
} }

View File

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

View File

@ -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') . ': ',

View File

@ -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');

View File

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

View File

@ -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();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)**.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -130,7 +130,7 @@ below.
![Untranslated strings](img/poedit_005.png) ![Untranslated strings](img/poedit_005.png)
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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');

View File

@ -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');

View File

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

View File

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

View File

@ -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=/';
}, },
/** /**

View File

@ -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);
} }

View File

@ -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 . '"'
); );
} }
} }

View File

@ -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');