Compare commits

..

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

39 changed files with 104 additions and 303 deletions

View File

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

View File

@ -4,47 +4,6 @@ Please make sure to always read our [Upgrading](doc/80-Upgrading.md) documentati
## 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
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;
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;
use Icinga\Web\Controller;
/**
* Display external or internal links within an iframe
*/
class IframeController extends CompatController
class IframeController extends Controller
{
/**
* Display iframe w/ the given URL
*/
public function indexAction(): void
public function indexAction()
{
$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);
$this->view->url = $this->params->getRequired('url');
}
}

View File

@ -184,6 +184,15 @@ use ipl\Web\Widget\StateBadge;
</div>
<div class="about-social">
<?= $this->qlink(
null,
'https://www.twitter.com/icinga',
null,
array(
'target' => '_blank',
'icon' => 'twitter',
'title' => $this->translate('Icinga on Twitter')
)
) ?> <?= $this->qlink(
null,
'https://www.facebook.com/icinga',
null,

View File

@ -28,6 +28,18 @@
</div>
</div>
<ul id="social">
<li>
<?= $this->qlink(
null,
'https://twitter.com/icinga',
null,
array(
'target' => '_blank',
'icon' => 'twitter',
'title' => $this->translate('Icinga on Twitter')
)
) ?>
</li>
<li>
<?= $this->qlink(
null,

View File

@ -6,7 +6,7 @@
<?= $this->tabs->render($this); ?>
<br/>
<div>
<h1>Could not <?= $action; ?> module "<?= $this->escape($moduleName); ?>"</h1>
<h1>Could not <?= $action; ?> module "<?= $moduleName; ?>"</h1>
<p>
While operation the following error occurred:
<br />

View File

@ -23,7 +23,7 @@ $modReason = [];
if (isset($requiredVendor, $requiredProject) && $requiredVendor && $requiredProject) {
// TODO: I don't like this, can we define requirements somewhere else?
$coreDeps = ['icinga-php-library' => '>= 0.14.2', 'icinga-php-thirdparty' => '>= 0.12'];
$coreDeps = ['icinga-php-library' => '>= 0.13.2', 'icinga-php-thirdparty' => '>= 0.12'];
foreach ($coreDeps as $libraryName => $requiredVersion) {
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

@ -399,7 +399,7 @@ You will need to install certain dependencies depending on your setup:
monitor your infrastructure
* A web server, e.g. Apache or Nginx
* PHP version ≥ 7.2
* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥ 0.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)
* 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

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
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 `semanage boolean -l | grep icinga`
* Output of `ps -eZ | grep httpd`

View File

@ -8,7 +8,7 @@ namespace Icinga\Application;
*/
class Version
{
const VERSION = '2.12.4';
const VERSION = '2.12.2';
/**
* Get the version of this instance of Icinga Web 2

View File

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

View File

@ -11,7 +11,6 @@ use Icinga\Exception\AuthenticationException;
use Icinga\Repository\DbRepository;
use Icinga\User;
use PDO;
use Zend_Db_Expr;
class DbUserBackend extends DbRepository implements UserBackendInterface, Inspectable
{
@ -180,28 +179,23 @@ class DbUserBackend extends DbRepository implements UserBackendInterface, Inspec
{
if ($this->ds->getDbType() === 'pgsql') {
// Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape'
$columns = ['password_hash' => new Zend_Db_Expr('ENCODE(password_hash, \'escape\')')];
$columns = array('password_hash' => 'ENCODE(password_hash, \'escape\')');
} else {
// 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')];
$columns = array('password_hash');
}
$query = $this
->select()
->from('user', $columns)
->where('active', true);
$nameColumn = 'name';
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);
$nameColumn = 'BINARY LOWER(name)';
}
$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->bindColumn(1, $lob, PDO::PARAM_LOB);
$statement->fetch(PDO::FETCH_BOUND);

View File

@ -204,7 +204,7 @@ class DbUserGroupBackend extends DbRepository implements Inspectable, UserGroupB
$membershipQuery = $this
->select()
->from('group_membership', array('group_name'))
->where('user', $user->getUsername());
->where('user_name', $user->getUsername());
$memberships = array();
foreach ($membershipQuery as $membership) {

View File

@ -37,7 +37,7 @@ class Csv
}
$out = array();
foreach ($row as & $val) {
$out[] = '"' . ($val == '0' ? '0' : ($val ? str_replace('"', '""', $val) : '')) . '"';
$out[] = '"' . ($val ? str_replace('"', '""', $val) : '') . '"';
}
$csv .= implode(',', $out) . "\r\n";
}

View File

@ -5,11 +5,9 @@ namespace Icinga\File;
use Dompdf\Dompdf;
use Dompdf\Options;
use Exception;
use Icinga\Application\Icinga;
use Icinga\Exception\ProgrammingError;
use Icinga\Util\Environment;
use Icinga\Web\FileCache;
use Icinga\Web\Hook;
use Icinga\Web\Url;
@ -66,17 +64,8 @@ class Pdf
return;
}
$tmpDir = FileCache::instance()->directory('legacy_pdf');
if ($tmpDir === false) {
throw new Exception('Could not create temporary directory for PDF rendering');
}
$options = new Options();
$options->set('defaultPaperSize', 'A4');
$options->set('fontDir', $tmpDir);
$options->set('fontCache', $tmpDir);
$options->set('tempDir', $tmpDir);
$options->set('chroot', $tmpDir);
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->render();

View File

@ -7,7 +7,6 @@ use Icinga\Application\Icinga;
use Icinga\Exception\ProgrammingError;
use Icinga\Util\StringHelper;
use Icinga\Web\Navigation\NavigationItem;
use Icinga\Web\Session;
use Icinga\Web\Url;
use Icinga\Web\View;
@ -191,10 +190,6 @@ class NavigationItemRenderer
$target = $item->getTarget();
if ($url->isExternal() && (!$target || in_array($target, $this->internalLinkTargets, true))) {
$item->setAttribute('data-url-hash', hash(
'sha256',
$url->getAbsoluteUrl() . Session::getSession()->getId()
));
$url = Url::fromPath('iframe', array('url' => $url));
}

View File

@ -179,9 +179,10 @@ class Url
}
$urlParts = parse_url($url);
if ((isset($urlParts['scheme']) && $urlParts['scheme'] !== $request->getScheme())
if (isset($urlParts['scheme']) && (
$urlParts['scheme'] !== $request->getScheme()
|| (isset($urlParts['host']) && $urlParts['host'] !== $request->getServer('SERVER_NAME'))
|| (isset($urlParts['port']) && $urlParts['port'] != $request->getServer('SERVER_PORT'))
|| (isset($urlParts['port']) && $urlParts['port'] != $request->getServer('SERVER_PORT')))
) {
$urlObject->setIsExternal();
}

View File

@ -205,8 +205,7 @@ class View extends Zend_View_Abstract
'th-thumb-empty' => true,
'github-circled' => true,
'history' => true,
'binoculars' => true,
'letter' => true
'binoculars' => true
//</editor-fold>
];

View File

@ -3,9 +3,9 @@
namespace Icinga\Web\Widget\Dashboard;
use Icinga\Web\Session;
use Icinga\Web\Url;
use Icinga\Data\ConfigObject;
use Icinga\Exception\IcingaException;
/**
* A dashboard pane dashlet
@ -57,15 +57,18 @@ class Dashlet extends UserWidget
*/
private $template =<<<'EOD'
<div class="container" data-icinga-url="{URL}" data-url-hash="{URL_HASH}">
<h1><a
href="{FULL_URL}"
aria-label="{TOOLTIP}"
title="{TOOLTIP}"
data-url-hash="{FULL_URL_HASH}"
data-base-target="col1"
>{TITLE}</a></h1>
<div class="container" data-icinga-url="{URL}">
<h1><a href="{FULL_URL}" aria-label="{TOOLTIP}" title="{TOOLTIP}" data-base-target="col1">{TITLE}</a></h1>
<p class="progress-label">{PROGRESS_LABEL}<span>.</span><span>.</span><span>.</span></p>
<noscript>
<div class="iframe-container">
<iframe
src="{IFRAME_URL}"
frameborder="no"
title="{TITLE_PREFIX}{TITLE}">
</iframe>
</div>
</noscript>
</div>
EOD;
@ -247,22 +250,13 @@ EOD;
$url = $this->getUrl();
$url->setParam('showCompact', true);
$fullUrl = $url->getUrlWithout(['showCompact', 'limit', 'view']);
$urlHash = '';
$fullUrlHash = '';
if ($url->getPath() === 'iframe') {
$urlHash = hash('sha256', Url::fromPath($url->getParam('url'))->getAbsoluteUrl()
. Session::getSession()->getId());
$fullUrlHash = hash('sha256', Url::fromPath($fullUrl->getParam('url'))->getAbsoluteUrl()
. Session::getSession()->getId());
}
$iframeUrl = clone $url;
$iframeUrl->setParam('isIframe');
$searchTokens = array(
'{URL}',
'{URL_HASH}',
'{IFRAME_URL}',
'{FULL_URL}',
'{FULL_URL_HASH}',
'{TOOLTIP}',
'{TITLE}',
'{TITLE_PREFIX}',
@ -271,9 +265,8 @@ EOD;
$replaceTokens = array(
$url,
$urlHash,
$fullUrl,
$fullUrlHash,
$iframeUrl,
$url->getUrlWithout(['showCompact', 'limit', 'view']),
sprintf($view->translate('Show %s', 'dashboard.dashlet.tooltip'), $view->escape($this->getTitle())),
$view->escape($this->getTitle()),
$view->translate('Dashlet') . ': ',

View File

@ -112,7 +112,7 @@ class Window
{
if (! isset(static::$window)) {
$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();
$id = static::generateId();
}

View File

@ -1,4 +1,4 @@
Module: doc
Version: 2.12.4
Version: 2.12.2
Description: Documentation module
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+ */
// Mixins
.gradient(@a: @gray-lighter; @b: @gray-lightest) {
background: @a;
background: -webkit-gradient(linear, left top, left bottom, from(@a), to(@b));
background: -webkit-linear-gradient(top, @a, @b);
background: -moz-linear-gradient(top, @a, @b);
background: -ms-linear-gradient(top, @a, @b);
background: -o-linear-gradient(top, @a, @b);
background: linear-gradient(to bottom, @a, @b);
}
// General styles
code {
@ -72,7 +84,7 @@ table {
}
tbody > tr:nth-child(odd) {
background: @gray-light;
.gradient()
}
tbody > tr:nth-child(even) {

View File

@ -1,5 +1,5 @@
Module: migrate
Version: 2.12.4
Version: 2.12.2
Description: Migrate module
This module was introduced with the domain-aware authentication feature in version 2.5.0.
It helps you migrating users and user configurations according to a given domain.

View File

@ -227,7 +227,7 @@ class BackendConfigForm extends ConfigForm
'autosubmit' => true
)
);
$resourceName = $this->getView()->escape($formData['resource'] ?? $this->getValue('resource'));
$resourceName = isset($formData['resource']) ? $formData['resource'] : $this->getValue('resource');
$this->addElement(
'note',
'resource_note',

View File

@ -284,10 +284,10 @@ $section->add(N_('Timeline'), array(
/*
* Reporting Section
*/
$section = $this->menuSection(N_('Reporting'), [
'icon' => 'fa-chart-simple',
$section = $this->menuSection(N_('Reporting'), array(
'icon' => 'barchart',
'priority' => 100
]);
));
/*
* Current Incidents

View File

@ -1,5 +1,5 @@
Module: monitoring
Version: 2.12.4
Version: 2.12.2
Description: Icinga monitoring module
IDO accessor and UI for your monitoring. This is the initial instalment for a
graphical presentation of Icinga environments. The predecessor of Icinga DB.

View File

@ -602,7 +602,7 @@ class WebWizard extends Wizard implements SetupWizard
)));
$set->add(new WebLibraryRequirement(array(
'condition' => ['icinga-php-library', '>=', '0.14.2'],
'condition' => ['icinga-php-library', '>=', '0.13.2'],
'alias' => 'Icinga PHP library',
'description' => mt(
'setup',

View File

@ -1,5 +1,5 @@
Module: setup
Version: 2.12.4
Version: 2.12.2
Description: Setup module
Web based wizard for setting up Icinga Web 2 and its modules.
This includes the data backends (e.g. relational database, LDAP),

View File

@ -1,5 +1,5 @@
Module: test
Version: 2.12.4
Version: 2.12.2
Description: Translation module
This module allows developers to run (unit) tests against Icinga Web 2 and
any of its modules. Usually you do not need to enable this.

View File

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

View File

@ -1,5 +1,5 @@
Module: translation
Version: 2.12.4
Version: 2.12.2
Description: Translation module
This module allows developers and translators to translate modules for multiple
languages. You do not need this module to run an internationalized web frontend.

View File

@ -282,39 +282,6 @@ a:hover > .icon-cancel {
// Responsive iFrames
.iframe-warning {
h2, p, a {
display: block;
width: fit-content;
font-size: 200%;
margin: 0 auto;
padding: 1em;
}
h2 {
font-size: 1000%;
color: @state-warning;
}
.note {
background: @gray-lighter;
}
a {
text-decoration: underline;
}
.reason {
.icon {
color: @text-color;
}
font-size: 100%;
background: @gray-lightest;
color: @text-color-light;
}
}
.iframe-container {
position: relative;
height: 0;
@ -328,7 +295,6 @@ a:hover > .icon-cancel {
top: 0;
height: 100%;
width: 100%;
border: none;
}
}

View File

@ -70,7 +70,7 @@
border-top: none;
margin-left: -1px;
min-width: 14em;
z-index: 1001;
z-index: 10;
}
.tabs > .dropdown-nav-item > ul > li:hover > a {

View File

@ -83,9 +83,7 @@
if ($container[0].contains(origFocus)
&& origFocus.form
&& ! origFocus.matches(
'input[type=submit], input[type=reset], input[type=button]'
+ ', button[type=submit], button[type=reset], button[type=button]'
+ ', .autofocus, .autosubmit:not(:hover)'
'input[type=submit], input[type=reset], input[type=button], .autofocus, .autosubmit:not(:hover)'
)
) {
this.icinga.logger.debug('Not changing content for ' + containerId + ' form has focus');

View File

@ -170,21 +170,7 @@
var $dashlet = $(this);
var url = $dashlet.data('icingaUrl');
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 href = $a.attr('href');
var linkTarget = $a.attr('target');
const urlHash = this.dataset.urlHash;
var $target;
var formerUrl;
@ -406,20 +391,7 @@
}
// 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) {
// 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) {
var id = null;
if (url.startsWith('//') || ! url.startsWith(this.baseUrl + '/')) {
throw new Error('URL ' + url + ' is not relative to ' + this.baseUrl);
}
// Default method is GET
if ('undefined' === typeof method) {
method = 'GET';

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()
{
$url = Url::fromPath('http://testusername:testpassword@testsite.com/path/to/my/url.html');