Compare commits

..

No commits in common. "main" and "v2.12.2" have entirely different histories.

64 changed files with 344 additions and 730 deletions

View File

@ -6,6 +6,15 @@ on:
- main - main
jobs: jobs:
update: trigger-update:
uses: icinga/github-actions/.github/workflows/L10n-update.yml@main name: L10n Update Trigger
secrets: inherit runs-on: ubuntu-latest
steps:
- name: Repository dispatch
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.ICINGABOT_TOKEN }}
repository: Icinga/L10n
event-type: update
client-payload: '{"origin": "${{ github.repository }}", "commit": "${{ github.sha }}"}'

View File

@ -100,9 +100,7 @@ jobs:
- name: Setup dependencies - name: Setup dependencies
run: | run: |
composer init -n --require mockery/mockery:* --require ipl/i18n:@dev --require ipl/web:@dev composer require -n --no-progress mockery/mockery ipl/i18n:@dev ipl/web:@dev
composer config platform.php 7.2.9
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

@ -4,47 +4,6 @@ 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.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.
#### 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.

View File

@ -1 +1 @@
v2.12.4 v2.12.2

View File

@ -3,108 +3,18 @@
namespace Icinga\Controllers; namespace Icinga\Controllers;
use Icinga\Web\Session; use Icinga\Web\Controller;
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 CompatController class IframeController extends Controller
{ {
/** /**
* Display iframe w/ the given URL * Display iframe w/ the given URL
*/ */
public function indexAction(): void public function indexAction()
{ {
$url = Url::fromPath($this->params->getRequired('url')); $this->view->url = $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

@ -184,6 +184,15 @@ 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,6 +28,18 @@
</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 "<?= $this->escape($moduleName); ?>"</h1> <h1>Could not <?= $action; ?> module "<?= $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.14.2', 'icinga-php-thirdparty' => '>= 0.12']; $coreDeps = ['icinga-php-library' => '>= 0.13.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

@ -0,0 +1,8 @@
<?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,7 +8,6 @@ $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.14.2) * [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥ 0.13.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,16 +3,6 @@
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 common ones: When filing a bug report please add the following information additionally to the
[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(getenv('ICINGAWEB_ENVIRONMENT') === 'dev' ? E_ALL : E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED); error_reporting(E_ALL | E_STRICT);
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,6 +579,7 @@ 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.4'; const VERSION = '2.12.2';
/** /**
* 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_DEPRECATED & ~E_USER_DEPRECATED); error_reporting(E_ALL | E_STRICT);
if (isset($_SERVER['REQUEST_URI'])) { if (isset($_SERVER['REQUEST_URI'])) {
$ruri = $_SERVER['REQUEST_URI']; $ruri = $_SERVER['REQUEST_URI'];

View File

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

View File

@ -11,7 +11,6 @@ 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
{ {
@ -180,28 +179,23 @@ 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 = ['password_hash' => new Zend_Db_Expr('ENCODE(password_hash, \'escape\')')]; $columns = array('password_hash' => 'ENCODE(password_hash, \'escape\')');
} else { } else {
// password_hash is intentionally not a valid query column, $columns = array('password_hash');
// by wrapping it in an expression it is not validated
$columns = ['password_hash' => new Zend_Db_Expr('password_hash')];
} }
$query = $this $nameColumn = 'name';
->select()
->from('user', $columns)
->where('active', true);
if ($this->ds->getDbType() === 'mysql') { if ($this->ds->getDbType() === 'mysql') {
$username = strtolower($username); $username = strtolower($username);
$nameColumn = new Zend_Db_Expr('BINARY LOWER(name)'); $nameColumn = 'BINARY LOWER(name)';
$query->getQuery()->where($nameColumn, $username);
} else { // pgsql
$query->where('user', $username);
} }
$statement = $this->ds->getDbAdapter()->prepare($query->getQuery()->getSelectQuery()); $query = $this->ds->select()
->from($this->prependTablePrefix('user'), $columns)
->where($nameColumn, $username)
->where('active', true);
$statement = $this->ds->getDbAdapter()->prepare($query->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', $user->getUsername()); ->where('user_name', $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 == '0' ? '0' : ($val ? str_replace('"', '""', $val) : '')) . '"'; $out[] = '"' . ($val ? str_replace('"', '""', $val) : '') . '"';
} }
$csv .= implode(',', $out) . "\r\n"; $csv .= implode(',', $out) . "\r\n";
} }

View File

@ -5,11 +5,9 @@ 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;
@ -66,17 +64,8 @@ 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,8 +3,6 @@
namespace Icinga\Util; namespace Icinga\Util;
use DateTimeZone;
/** /**
* Retrieve timezone information from cookie * Retrieve timezone information from cookie
*/ */
@ -17,6 +15,13 @@ class TimezoneDetect
*/ */
private static $success; private static $success;
/**
* Timezone offset in minutes
*
* @var int
*/
private static $offset = 0;
/** /**
* @var string * @var string
*/ */
@ -29,6 +34,13 @@ 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
*/ */
@ -38,13 +50,30 @@ class TimezoneDetect
return; return;
} }
if (in_array($_COOKIE[self::$cookieName] ?? null, DateTimeZone::listIdentifiers(), true)) { if (array_key_exists(self::$cookieName, $_COOKIE)) {
self::$timezoneName = $_COOKIE[self::$cookieName]; $matches = array();
self::$success = true; if (preg_match('/\A(-?\d+)[\-,](\d+)\z/', $_COOKIE[self::$cookieName], $matches)) {
} else { $offset = $matches[1];
self::$success = false; $timezoneName = timezone_name_from_abbr('', (int) $offset, (int) $matches[2]);
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
@ -73,5 +102,6 @@ class TimezoneDetect
{ {
self::$success = null; self::$success = null;
self::$timezoneName = null; self::$timezoneName = null;
self::$offset = 0;
} }
} }

View File

@ -6,7 +6,6 @@ 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
@ -150,14 +149,4 @@ class Menu extends Navigation
])); ]));
} }
} }
/**
* Create and return the renderer for this navigation
*
* @return RecursiveMenuNavigationRenderer
*/
public function getRenderer()
{
return new RecursiveMenuNavigationRenderer($this);
}
} }

View File

@ -7,7 +7,6 @@ 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;
@ -191,10 +190,6 @@ 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

@ -1,30 +0,0 @@
<?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,10 +4,11 @@
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 RecursiveIteratorIterator; use Icinga\Web\Navigation\Renderer\NavigationItemRenderer;
/** /**
* Renderer for multi level navigation * Renderer for multi level navigation

View File

@ -179,9 +179,10 @@ class Url
} }
$urlParts = parse_url($url); $urlParts = parse_url($url);
if ((isset($urlParts['scheme']) && $urlParts['scheme'] !== $request->getScheme()) if (isset($urlParts['scheme']) && (
$urlParts['scheme'] !== $request->getScheme()
|| (isset($urlParts['host']) && $urlParts['host'] !== $request->getServer('SERVER_NAME')) || (isset($urlParts['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,8 +205,7 @@ 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,15 +57,18 @@ class Dashlet extends UserWidget
*/ */
private $template =<<<'EOD' private $template =<<<'EOD'
<div class="container" data-icinga-url="{URL}" data-url-hash="{URL_HASH}"> <div class="container" data-icinga-url="{URL}">
<h1><a <h1><a href="{FULL_URL}" aria-label="{TOOLTIP}" title="{TOOLTIP}" data-base-target="col1">{TITLE}</a></h1>
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;
@ -247,22 +250,13 @@ EOD;
$url = $this->getUrl(); $url = $this->getUrl();
$url->setParam('showCompact', true); $url->setParam('showCompact', true);
$fullUrl = $url->getUrlWithout(['showCompact', 'limit', 'view']); $iframeUrl = clone $url;
$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}',
'{URL_HASH}', '{IFRAME_URL}',
'{FULL_URL}', '{FULL_URL}',
'{FULL_URL_HASH}',
'{TOOLTIP}', '{TOOLTIP}',
'{TITLE}', '{TITLE}',
'{TITLE_PREFIX}', '{TITLE_PREFIX}',
@ -271,9 +265,8 @@ EOD;
$replaceTokens = array( $replaceTokens = array(
$url, $url,
$urlHash, $iframeUrl,
$fullUrl, $url->getUrlWithout(['showCompact', 'limit', 'view']),
$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 Icinga\Web\Url; use Exception;
/** /**
* Filter * Filter
@ -738,8 +738,7 @@ class FilterEditor extends AbstractWidget
$html = ' <form method="post" class="search inline" action="' $html = ' <form method="post" class="search inline" action="'
. $preservedUrl . $preservedUrl
. '"><i class="icon fa-search fa search-icon"></i>' . '"><input type="text" name="q" class="search search-input" value="" placeholder="'
. '<input type="text" name="q" class="search search-input" value="" placeholder="'
. t('Search...') . t('Search...')
. '" /></form>'; . '" /></form>';

View File

@ -10,7 +10,6 @@ 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
{ {
@ -107,8 +106,6 @@ 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 || ! preg_match('/^\w+$/', $id)) { if (empty($id) || $id === static::UNDEFINED) {
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.4 Version: 2.12.2
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,5 +1,17 @@
/*! 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 {
@ -72,7 +84,7 @@ table {
} }
tbody > tr:nth-child(odd) { tbody > tr:nth-child(odd) {
background: @gray-light; .gradient()
} }
tbody > tr:nth-child(even) { tbody > tr:nth-child(even) {

View File

@ -1,5 +1,5 @@
Module: migrate Module: migrate
Version: 2.12.4 Version: 2.12.2
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,8 +57,7 @@ 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 = $this->getView()->escape($formData['resource'] ?? $this->getValue('resource')); $resourceName = isset($formData['resource']) ? $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'), [ $section = $this->menuSection(N_('Reporting'), array(
'icon' => 'fa-chart-simple', 'icon' => 'barchart',
'priority' => 100 'priority' => 100
]); ));
/* /*
* Current Incidents * Current Incidents

View File

@ -21,7 +21,6 @@ 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' => 'UNIX_TIMESTAMP(ss.last_time_critical)', 'service_last_time_critical' => 'ss.last_time_critical',
'service_last_time_ok' => 'UNIX_TIMESTAMP(ss.last_time_ok)', 'service_last_time_ok' => 'ss.last_time_ok',
'service_last_time_unknown' => 'UNIX_TIMESTAMP(ss.last_time_unknown)', 'service_last_time_unknown' => 'ss.last_time_unknown',
'service_last_time_warning' => 'UNIX_TIMESTAMP(ss.last_time_warning)', 'service_last_time_warning' => '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.4 Version: 2.12.2
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,11 +102,7 @@ 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-url=<url> Address where to pass requests to FPM [127.0.0.1:9000] * --fpm-uri=<uri> Address or path 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]
* *
@ -124,13 +120,9 @@ 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-socket-path=/var/run/php8.3-fpm.sock * --fpm-uri=unix:/var/run/php5-fpm.sock
*/ */
public function webserverAction() public function webserverAction()
{ {
@ -165,18 +157,10 @@ class ConfigCommand extends Command
$enableFpm = $this->params->shift('enable-fpm', $webserver->getEnableFpm()); $enableFpm = $this->params->shift('enable-fpm', $webserver->getEnableFpm());
$fpmSocketPath = trim($this->params->get('fpm-socket-path', $webserver->getFpmSocketPath())); $fpmUri = trim($this->params->get('fpm-uri', $webserver->getFpmUri()));
$fpmUrl = trim($this->params->get('fpm-url', $webserver->getFpmUrl())); if (empty($fpmUri)) {
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(
'One of the arguments --fpm-socket-path or --fpm-url must be set to pass requests to FPM' 'The argument --fpm-uri expects an address or path where 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
@ -184,8 +168,7 @@ class ConfigCommand extends Command
->setConfigDir($configDir) ->setConfigDir($configDir)
->setUrlPath($urlPath) ->setUrlPath($urlPath)
->setEnableFpm($enableFpm) ->setEnableFpm($enableFpm)
->setFpmUrl($fpmUrl) ->setFpmUri($fpmUri);
->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

@ -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.14.2'], 'condition' => ['icinga-php-library', '>=', '0.13.2'],
'alias' => 'Icinga PHP library', 'alias' => 'Icinga PHP library',
'description' => mt( 'description' => mt(
'setup', 'setup',

View File

@ -33,25 +33,11 @@ abstract class Webserver
protected $configDir; protected $configDir;
/** /**
* Address where to pass requests to FPM * Address or path where to pass requests to FPM
* *
* @var string * @var string
*/ */
protected $fpmUrl; protected $fpmUri;
/**
* 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
@ -86,7 +72,6 @@ abstract class Webserver
public function generate() public function generate()
{ {
$template = $this->getTemplate(); $template = $this->getTemplate();
$fpmUri = $this->createFpmUri();
$searchTokens = array( $searchTokens = array(
'{urlPath}', '{urlPath}',
@ -100,7 +85,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(),
$fpmUri $this->getFpmUri()
); );
$template = str_replace($searchTokens, $replaceTokens, $template); $template = str_replace($searchTokens, $replaceTokens, $template);
return $template; return $template;
@ -113,13 +98,6 @@ 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
* *
@ -230,47 +208,25 @@ abstract class Webserver
} }
/** /**
* Get the address where to pass requests to FPM * Get the address or path where to pass requests to FPM
* *
* @return string * @return string
*/ */
public function getFpmUrl() public function getFpmUri()
{ {
return $this->fpmUrl; return $this->fpmUri;
} }
/** /**
* Set the address where to pass requests to FPM * Set the address or path where to pass requests to FPM
* *
* @param string $url * @param string $uri
* *
* @return $this * @return $this
*/ */
public function setFpmUrl($url) public function setFpmUri($uri)
{ {
$this->fpmUrl = (string) $url; $this->fpmUri = (string) $uri;
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,21 +10,7 @@ use Icinga\Module\Setup\Webserver;
*/ */
class Apache extends Webserver class Apache extends Webserver
{ {
protected $fpmUrl = '127.0.0.1:9000'; protected $fpmUri = '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()
{ {
@ -37,7 +23,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 "{fpmUri}/{documentRoot}/$1" # ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
# </LocationMatch> # </LocationMatch>
#</IfVersion> #</IfVersion>
@ -85,7 +71,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:{fpmUri}" # SetHandler "proxy:fcgi://{fpmUri}"
# ErrorDocument 503 {urlPath}/error_unavailable.html # ErrorDocument 503 {urlPath}/error_unavailable.html
# </FilesMatch> # </FilesMatch>
# </IfVersion> # </IfVersion>
@ -99,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
<LocationMatch "^{urlPath}/(.*\.php)$"> <LocationMatch "^{urlPath}/(.*\.php)$">
ProxyPassMatch "{fpmUri}/{documentRoot}/$1" ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
</LocationMatch> </LocationMatch>
</IfVersion> </IfVersion>
@ -145,7 +131,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:{fpmUri}" SetHandler "proxy:fcgi://{fpmUri}"
ErrorDocument 503 {urlPath}/error_unavailable.html ErrorDocument 503 {urlPath}/error_unavailable.html
</FilesMatch> </FilesMatch>
</IfVersion> </IfVersion>

View File

@ -10,15 +10,10 @@ use Icinga\Module\Setup\Webserver;
*/ */
class Nginx extends Webserver class Nginx extends Webserver
{ {
protected $fpmUrl = '127.0.0.1:9000'; protected $fpmUri = '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.4 Version: 2.12.2
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.4 Version: 2.12.2
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](03-Translation.md#module-translation-tests). [Testing Translations](Testing Translations).
## 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.4 Version: 2.12.2
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

@ -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;
} }
} }
form:has(input.search:focus) > .search-icon { @light-mode: {
color: @icinga-blue; #menu input.search,
} .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: 1em; right: .5em;
bottom: .25em; bottom: .25em;
font-size: 75%; font-size: 75%;
overflow: hidden; overflow: hidden;
@ -330,8 +330,7 @@
padding-left: .75em; padding-left: .75em;
} }
.nav-level-1 > .nav-item > a > i, .nav-level-1 > .nav-item 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,39 +282,6 @@ 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;
@ -328,7 +295,6 @@ a:hover > .icon-cancel {
top: 0; top: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
border: none;
} }
} }

View File

@ -2,12 +2,11 @@
#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 {
@ -16,7 +15,6 @@
flex: 1; flex: 1;
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-width: thin;
} }
#menu .nav-item { #menu .nav-item {
@ -35,11 +33,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
.text-ellipsis(); overflow: hidden;
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 {
@ -53,6 +51,7 @@
} }
#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 {
@ -70,9 +69,7 @@
} }
> a { > a {
// To center the content, padding top is: height - line-height / 2 padding: 0.5em 0.5em 0.5em .75em;
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,
@ -90,7 +87,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;
@ -98,10 +95,6 @@
} }
} }
#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;
@ -110,7 +103,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;
@ -121,9 +114,8 @@
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;
} }
} }
@ -131,7 +123,6 @@
// 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;
@ -149,7 +140,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;
@ -159,7 +150,7 @@
height: 1.25em; height: 1.25em;
width: 1.25em; width: 1.25em;
position: absolute; position: absolute;
top: ~"calc(50% - (1.25em / 2))"; top: .5em;
right: -.75em; right: -.75em;
z-index: 3; z-index: 3;
} }
@ -213,12 +204,9 @@
opacity: .6; opacity: .6;
} }
#menu .search-icon {
left: 1em;
}
#menu input.search { #menu input.search {
background-color: @menu-bg-color; background: transparent url('../img/icons/search_white.png') no-repeat 1em center;
background-size: 1em auto;
border: none; border: none;
color: @menu-color; color: @menu-color;
line-height: 2.167em; line-height: 2.167em;
@ -245,7 +233,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-left: auto; margin-top: 0.2em;
} }
#menu .nav-level-2 > .badge-nav-item > a > .badge { #menu .nav-level-2 > .badge-nav-item > a > .badge {
@ -253,10 +241,6 @@
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 {
@ -266,15 +250,12 @@
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-bottom: @vertical-padding; padding: @vertical-padding 0;
width: 14em; width: 14em;
position: fixed; position: fixed;
z-index: 11; z-index: 1;
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;
@ -282,20 +263,17 @@
border-left: 1px solid @gray-light; border-left: 1px solid @gray-light;
content: ""; content: "";
display: block; display: block;
height: var(--caretSide); height: 1.1em;
width: var(--caretSide); width: 1.1em;
position: absolute; position: absolute;
top: ~"calc(@{nav-item-height} / 2 - var(--caretSide) / 2)"; top: 1em;
left: ~"calc(-1 * var(--caretSide) / 2 - 1px)"; left: -.6em;
z-index: -1;
} }
&.bottom-up { &.bottom-up:after {
--caretY: 100%; top: unset;
margin-top: 1px; bottom: 1em;
&::after {
top: ~"calc(var(--caretY) - (@{nav-item-height} / 2) - (var(--caretSide) / 2))";
}
} }
> .nav-item { > .nav-item {
@ -325,7 +303,7 @@
} }
// Hide activity caret when displayed as flyout // Hide activity caret when displayed as flyout
&::after { &:after {
display: none; display: none;
} }
} }
@ -342,13 +320,17 @@
#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;
@ -526,34 +508,12 @@ 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,
@ -565,10 +525,6 @@ 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: 1001; z-index: 10;
} }
.tabs > .dropdown-nav-item > ul > li:hover > a { .tabs > .dropdown-nav-item > ul > li:hover > a {

View File

@ -83,9 +83,7 @@
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]' 'input[type=submit], input[type=reset], input[type=button], .autofocus, .autosubmit:not(:hover)'
+ ', 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,24 +6,15 @@
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);
if (typeof d3 !== "undefined") { this.on('mouseenter', '#menu .primary-nav .nav-level-1 > .nav-item', this.showFlyoutMenu, this);
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);
@ -39,21 +30,6 @@
*/ */
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
* *
@ -306,111 +282,61 @@
}; };
/** /**
* Captures the mouse enter events to the navigation item and show the flyout. * Show the fly-out menu
* *
* @param e * @param e
*/ */
Navigation.prototype.onMouseEnter = function(e) { Navigation.prototype.showFlyoutMenu = function(e) {
const $layout = $('#layout'); var $layout = $('#layout');
const _this = e.data.self;
if ($layout.hasClass('minimal-layout')) { if ($layout.hasClass('minimal-layout')) {
return; return;
} }
const $target = $(this); var $target = $(this);
var $flyout = $target.find('.nav-level-2');
if ( if (! $flyout.length) {
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 ($layout.hasClass('menu-hovered')) {
delay = 0;
}
setTimeout(function() {
try {
if (! $target.is(':hover')) { if (! $target.is(':hover')) {
return; return;
} }
} catch(e) { /* Bypass because if IE8 */ }
$layout.addClass('menu-hovered'); $layout.addClass('menu-hovered');
_this.extendedFlyoutZone[0] = [e.clientX, e.clientY];
_this.showFlyoutMenu($target);
}
/**
* Captures the mouse move events within the navigation item
* and show the flyout if needed.
*
* @param e
*/
Navigation.prototype.onMouseMove = function(e) {
const _this = e.data.self;
clearTimeout(_this.flyoutTimer);
const $target = $(this);
if (! $target[0].matches(':has(.nav-level-2)')) {
return;
}
if (! $target.hasClass('hover')) {
if (
! _this.extendedFlyoutZone.includes(undefined)
&& d3.polygonContains(_this.extendedFlyoutZone, [e.clientX, e.clientY])
) {
_this.flyoutTimer = setTimeout(function() {
_this.showFlyoutMenu($target);
}, 200);
} else {
// 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);
}
}
_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.siblings().not($target).removeClass('hover');
$target.addClass('hover'); $target.addClass('hover');
const targetRect = $target[0].getBoundingClientRect(); var targetHeight = $target.offset().top + $target.outerHeight();
const flyoutRect = $flyout[0].getBoundingClientRect(); $flyout.css({
bottom: 'auto',
top: targetHeight
});
const css = { "--caretY": "" }; var rect = $flyout[0].getBoundingClientRect();
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`;
}
if (rect.bottom > window.innerHeight) {
$flyout.addClass('bottom-up'); $flyout.addClass('bottom-up');
$flyout.css({
bottom: window.innerHeight - targetHeight,
top: 'auto'
});
} else { } else {
$flyout.removeClass('bottom-up'); $flyout.removeClass('bottom-up');
css.top = targetRect.top;
} }
}, delay);
$flyout.css(css);
this.extendedFlyoutZone[1] = [flyoutRect.left, css.top];
this.extendedFlyoutZone[2] = [flyoutRect.left, css.top + flyoutRect.height];
}; };
/** /**
@ -422,8 +348,6 @@
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,22 +170,8 @@
var $dashlet = $(this); var $dashlet = $(this);
var url = $dashlet.data('icingaUrl'); var url = $dashlet.data('icingaUrl');
if (typeof url !== 'undefined') { if (typeof url !== 'undefined') {
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; _this.icinga.loader.loadUrl(url, $dashlet).autorefresh = true;
} }
}
}); });
} }
}, },
@ -295,7 +281,6 @@
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;
@ -406,20 +391,7 @@
} }
// Load link URL // Load link URL
if (urlHash) {
icinga.loader.loadUrl(
href,
$target,
undefined,
undefined,
undefined,
undefined,
undefined,
{ "X-Icinga-URLHash": urlHash }
);
} else {
icinga.loader.loadUrl(href, $target); 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,10 +242,6 @@
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';

View File

@ -4,6 +4,28 @@
'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
* *
@ -29,11 +51,15 @@
* 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, Intl.DateTimeFormat().resolvedOptions().timeZone); this.writeCookie(this.cookieName, timezoneOffset + '-' + Number(dst), 1);
}, },
/** /**
@ -41,9 +67,17 @@
* *
* @param {String} name * @param {String} name
* @param {String} value * @param {String} value
* @param {Number} days
*/ */
writeCookie: function(name, value) { writeCookie: function(name, value, days) {
document.cookie = name + '=' + value + '; path=/'; var expires = '';
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = '; expires=' + date.toGMTString();
}
document.cookie = name + '=' + value + expires + '; path=/';
}, },
/** /**

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); error_reporting(E_ALL | E_STRICT);
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,6 +32,7 @@ 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,42 +8,38 @@ use Icinga\Util\TimezoneDetect;
class TimezoneDetectTest extends BaseTestCase class TimezoneDetectTest extends BaseTestCase
{ {
public function testInvalidTimezoneNameInCookie(): void public function testPositiveTimezoneOffsetSeparatedByComma()
{ {
$tzDetect = new TimezoneDetect(); $this->assertTimezoneDetection('3600,0', 'Europe/Paris');
$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 testValidTimezoneNameInCookie(): void public function testPositiveTimezoneOffsetSeparatedByHyphen()
{
$this->assertTimezoneDetection('3600-0', 'Europe/Paris');
}
public function testNegativeTimezoneOffsetSeparatedByComma()
{
$this->assertTimezoneDetection('-3600,0', 'Atlantic/Azores');
}
public function testNegativeTimezoneOffsetSeparatedByHyphen()
{
$this->assertTimezoneDetection('-3600-0', 'Atlantic/Azores');
}
protected function assertTimezoneDetection($cookieValue, $expectedTimezoneName)
{ {
$tzDetect = new TimezoneDetect(); $tzDetect = new TimezoneDetect();
$tzDetect->reset(); $tzDetect->reset();
$_COOKIE[TimezoneDetect::$cookieName] = "Europe/Berlin"; $_COOKIE[TimezoneDetect::$cookieName] = $cookieValue;
$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(),
"Europe/Berlin", $expectedTimezoneName,
'Failed to assert that the valid timezone name was correctly set' 'Failed asserting that the timezone "' . $expectedTimezoneName
. '" is being detected from the cookie value "' . $cookieValue . '"'
); );
} }
} }

View File

@ -71,11 +71,6 @@ 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');