diff --git a/application/controllers/PhperrorController.php b/application/controllers/PhperrorController.php index f434b6c0e..40a32c1a7 100644 --- a/application/controllers/PhperrorController.php +++ b/application/controllers/PhperrorController.php @@ -3,7 +3,8 @@ namespace Icinga\Module\Director\Controllers; use Icinga\Application\Icinga; -use Icinga\Application\Modules\Manager; +use Icinga\Module\Director\Application\DependencyChecker; +use Icinga\Module\Director\Web\Table\Dependency\DependencyInfoTable; use Icinga\Web\Controller; class PhperrorController extends Controller @@ -24,39 +25,19 @@ public function errorAction() public function dependenciesAction() { - $dependencies = $this->view->dependencies = $this->Module()->getDependencies(); - $modules = $this->view->modules = Icinga::app()->getModuleManager(); - // Hint: we're duplicating some code here - $satisfied = true; - foreach ($dependencies as $module => $required) { - /** @var Manager $this ->modules */ - if ($modules->hasEnabled($module)) { - $installed = $modules->getModule($module, false)->getVersion(); - $installed = \ltrim($installed, 'v'); // v0.6.0 VS 0.6.0 - if (\preg_match('/^([<>=]+)\s*v?(\d+\.\d+\.\d+)$/', $required, $match)) { - $operator = $match[1]; - $vRequired = $match[2]; - if (\version_compare($installed, $vRequired, $operator)) { - continue; - } - } - } - $satisfied = false; - } - - if ($satisfied) { + $checker = new DependencyChecker(Icinga::app()); + if ($checker->satisfiesDependencies($this->Module())) { $this->redirectNow('director'); } - $this->setAutorefreshInterval(15); - $this->getTabs()->add('error', array( + $this->getTabs()->add('error', [ 'label' => $this->translate('Error'), 'url' => $this->getRequest()->getUrl() - ))->activate('error'); - $msg = $this->translate( + ])->activate('error'); + $this->view->title = $this->translate('Unsatisfied dependencies'); + $this->view->table = (new DependencyInfoTable($checker, $this->Module()))->render(); + $this->view->message = $this->translate( "Icinga Director depends on the following modules, please install/upgrade as required" ); - $this->view->title = $this->translate('Unsatisfied dependencies'); - $this->view->message = sprintf($msg, PHP_VERSION); } } diff --git a/application/views/scripts/phperror/dependencies.phtml b/application/views/scripts/phperror/dependencies.phtml index c8d4c143c..7492a1445 100644 --- a/application/views/scripts/phperror/dependencies.phtml +++ b/application/views/scripts/phperror/dependencies.phtml @@ -10,62 +10,5 @@ use Icinga\Application\Modules\Manager;

escape($this->message) ?>

- - - - - - - - - -dependencies as $module => $required) { - /** @var Manager $this->modules */ - if ($modules->hasEnabled($module)) { - $installed = $modules->getModule($module, false)->getVersion(); - $installed = \ltrim($installed, 'v'); // v0.6.0 VS 0.6.0 - if (\preg_match('/^([<>=]+)\s*v?(\d+\.\d+\.\d+)$/', $required, $match)) { - $operator = $match[1]; - $vRequired = $match[2]; - if (\version_compare($installed, $vRequired, $operator)) { - $icon = 'ok'; - } else { - $icon = 'cancel'; - } - } else { - $icon = 'cancel'; - } - $link = $this->qlink( - $module, - 'config/module', - ['name' => $module], - ['class' => "icon-$icon"] - ); - } elseif ($modules->hasInstalled($module)) { - $installed = $this->translate('disabled'); - $link = $this->qlink($module, 'config/module', ['name' => $module], ['class' => 'icon-cancel']); - } else { - $installed = $this->translate('missing'); - $link = sprintf( - '%s (%s)', - $this->escape($module), - $this->escape($module), - $this->translate('more') - ); - } - - \printf( - '', - $link, - $this->escape($required), - $this->escape($installed) - ); -} - -?> - -
translate('Module name') ?>translate('Required') ?>translate('Installed') ?>
%s%s%s
+table ?>
diff --git a/doc/82-Changelog.md b/doc/82-Changelog.md index 76bb240a0..656ea5fb9 100644 --- a/doc/82-Changelog.md +++ b/doc/82-Changelog.md @@ -48,6 +48,7 @@ next patch release (will be 1.8.1) * FIX: show "deactivated" services as such also for read-only users (#2344) * FIX: Overrides for Services belonging to Sets on root Host Templates (#2333) * FIX: show no header tabs for search result in web 2.8+ (#2141) +* FIX: show and link dependencies for web 2.9+ (#2354) ### Icinga Configuration * FIX: rare race condition, where generated config might miss some files (#2351) diff --git a/library/Director/Application/Dependency.php b/library/Director/Application/Dependency.php new file mode 100644 index 000000000..0100e69fa --- /dev/null +++ b/library/Director/Application/Dependency.php @@ -0,0 +1,113 @@ +=1.7.0 + * @param string $installedVersion + * @param bool $enabled + */ + public function __construct($name, $requirement, $installedVersion = null, $enabled = null) + { + $this->name = $name; + $this->setRequirement($requirement); + if ($installedVersion !== null) { + $this->setInstalledVersion($installedVersion); + } + if ($enabled !== null) { + $this->setEnabled($enabled); + } + } + + public function setRequirement($requirement) + { + if (preg_match('/^([<>=]+)\s*v?(\d+\.\d+\.\d+)$/', $requirement, $match)) { + $this->operator = $match[1]; + $this->requiredVersion = $match[2]; + $this->requirement = $requirement; + } else { + throw new \InvalidArgumentException("'$requirement' is not a valid version constraint"); + } + } + + /** + * @return bool + */ + public function isInstalled() + { + return $this->installedVersion !== null; + } + + /** + * @return string|null + */ + public function getInstalledVersion() + { + return $this->installedVersion; + } + + /** + * @param string $version + */ + public function setInstalledVersion($version) + { + $this->installedVersion = ltrim($version, 'v'); // v0.6.0 VS 0.6.0 + } + + /** + * @return bool + */ + public function isEnabled() + { + return $this->enabled === true; + } + + /** + * @param bool $enabled + */ + public function setEnabled($enabled = true) + { + $this->enabled = $enabled; + } + + public function isSatisfied() + { + if (! $this->isInstalled() || ! $this->isEnabled()) { + return false; + } + + return version_compare($this->installedVersion, $this->requiredVersion, $this->operator); + } + + public function getName() + { + return $this->name; + } + + public function getRequirement() + { + return $this->requirement; + } +} diff --git a/library/Director/Application/DependencyChecker.php b/library/Director/Application/DependencyChecker.php new file mode 100644 index 000000000..d726b0b6a --- /dev/null +++ b/library/Director/Application/DependencyChecker.php @@ -0,0 +1,73 @@ +app = $app; + $this->modules = $app->getModuleManager(); + } + + /** + * @param Module $module + * @return Dependency[] + */ + public function getDependencies(Module $module) + { + $dependencies = []; + $isV290 = version_compare(Version::VERSION, '2.9.0', '>='); + foreach ($module->getDependencies() as $moduleName => $required) { + if ($isV290 && in_array($moduleName, ['ipl', 'reactbundle'], true)) { + continue; + } + $dependency = new Dependency($moduleName, $required); + $dependency->setEnabled($this->modules->hasEnabled($moduleName)); + if ($this->modules->hasInstalled($moduleName)) { + $dependency->setInstalledVersion($this->modules->getModule($moduleName, false)->getVersion()); + } + $dependencies[] = $dependency; + } + if ($isV290) { + $libs = $this->app->getLibraries(); + foreach ($module->getRequiredLibraries() as $libraryName => $required) { + $dependency = new Dependency($libraryName, $required); + if ($libs->has($libraryName)) { + $dependency->setInstalledVersion($libs->get($libraryName)->getVersion()); + $dependency->setEnabled(); + } + $dependencies[] = $dependency; + } + } + + return $dependencies; + } + + // if (version_compare(Version::VERSION, '2.9.0', 'ge')) { + // } + /** + * @param Module $module + * @return bool + */ + public function satisfiesDependencies(Module $module) + { + foreach ($this->getDependencies($module) as $dependency) { + if (! $dependency->isSatisfied()) { + return false; + } + } + + return true; + } +} diff --git a/library/Director/Web/Table/Dependency/DependencyInfoTable.php b/library/Director/Web/Table/Dependency/DependencyInfoTable.php new file mode 100644 index 000000000..28aa856d9 --- /dev/null +++ b/library/Director/Web/Table/Dependency/DependencyInfoTable.php @@ -0,0 +1,101 @@ +module = $module; + $this->checker = $checker; + } + + protected function linkToModule($name, $icon) + { + return Html::link( + Html::escape($name), + Html::webUrl('config/module', ['name' => $name]), + [ + 'class' => "icon-$icon" + ] + ); + } + + public function render() + { + $html = ' + + + + + + + + +'; + foreach ($this->checker->getDependencies($this->module) as $dependency) { + $name = $dependency->getName(); + $isLibrary = substr($name, 0, 11) === 'icinga-php-'; + $rowAttributes = $isLibrary ? ['data-base-target' => '_self'] : null; + if ($dependency->isSatisfied()) { + if ($dependency->isSatisfied()) { + $icon = 'ok'; + } else { + $icon = 'cancel'; + } + $link = $isLibrary ? $this->noLink($name, $icon) : $this->linkToModule($name, $icon); + $installed = $dependency->getInstalledVersion(); + } elseif ($dependency->isInstalled()) { + $installed = sprintf('%s (%s)', $dependency->getInstalledVersion(), $this->translate('disabled')); + $link = $this->linkToModule($name, 'cancel'); + } else { + $installed = $this->translate('missing'); + $repository = $isLibrary ? $name : "icingaweb2-module-$name"; + $link = sprintf( + '%s (%s)', + $this->noLink($name, 'cancel'), + Html::linkToGitHub(Html::escape($this->translate('more')), 'Icinga', $repository) + ); + } + + $html .= $this->htmlRow([ + $link, + Html::escape($dependency->getRequirement()), + Html::escape($installed) + ], $rowAttributes); + } + + return $html . ' +
' . Html::escape($this->translate('Module name')) . '' . Html::escape($this->translate('Required')) . '' . Html::escape($this->translate('Installed')) . '
+'; + } + + protected function noLink($label, $icon) + { + return Html::link(Html::escape($label), Url::fromRequest()->with('rnd', rand(1, 100000)), [ + 'class' => "icon-$icon" + ]); + } + + protected function translate($string) + { + return \mt('director', $string); + } + + protected function htmlRow(array $cols, $rowAttributes) + { + $content = ''; + foreach ($cols as $escapedContent) { + $content .= Html::tag('td', null, $escapedContent); + } + return Html::tag('tr', $rowAttributes, $content); + } +} diff --git a/library/Director/Web/Table/Dependency/Html.php b/library/Director/Web/Table/Dependency/Html.php new file mode 100644 index 000000000..092f7997f --- /dev/null +++ b/library/Director/Web/Table/Dependency/Html.php @@ -0,0 +1,74 @@ + $value) { + if (! preg_match('/^[a-z][a-z0-9:-]*$/i', $name)) { + throw new InvalidArgumentException("Invalid attribute name: '$name'"); + } + + $result .= " $name=\"" . self::escapeAttributeValue($value) . '"'; + } + } + + return "$result>$escapedContent"; + } + + public static function webUrl($path, $params) + { + return Url::fromPath($path, $params); + } + + public static function link($escapedLabel, $url, $attributes = []) + { + return static::tag('a', [ + 'href' => $url, + ] + $attributes, $escapedLabel); + } + + public static function linkToGitHub($escapedLabel, $namespace, $repository) + { + return static::link( + $escapedLabel, + 'https://github.com/' . urlencode($namespace) . '/' . urlencode($repository), + [ + 'target' => '_blank', + 'rel' => 'noreferrer', + 'class' => 'icon-forward' + ] + ); + } + + protected static function escapeAttributeValue($value) + { + $value = str_replace('"', '"', $value); + // Escape ambiguous ampersands + return preg_replace_callback('/&[0-9A-Z]+;/i', function ($match) { + $subject = $match[0]; + + if (htmlspecialchars_decode($subject, ENT_COMPAT | ENT_HTML5) === $subject) { + // Ambiguous ampersand + return str_replace('&', '&', $subject); + } + + return $subject; + }, $value); + } + + public static function escape($any) + { + return htmlspecialchars($any); + } +} diff --git a/run.php b/run.php index f26bb479c..8f821ed14 100644 --- a/run.php +++ b/run.php @@ -1,6 +1,7 @@ app->getModuleManager(); -foreach ($this->getDependencies() as $module => $required) { - if ($modules->hasEnabled($module)) { - $installed = $modules->getModule($module, false)->getVersion(); - $installed = ltrim($installed, 'v'); // v0.6.0 VS 0.6.0 - if (preg_match('/^([<>=]+)\s*v?(\d+\.\d+\.\d+)$/', $required, $match)) { - $operator = $match[1]; - $vRequired = $match[2]; - if (version_compare($installed, $vRequired, $operator)) { - continue; - } - } - } - +$checker = new DependencyChecker($this->app); +if (! $checker->satisfiesDependencies($this)) { include __DIR__ . '/run-missingdeps.php'; return; } diff --git a/test/php/library/Director/Application/DependencyTest.php b/test/php/library/Director/Application/DependencyTest.php new file mode 100644 index 000000000..cc6047eee --- /dev/null +++ b/test/php/library/Director/Application/DependencyTest.php @@ -0,0 +1,72 @@ +=0.3.0'); + $this->assertFalse($dependency->isInstalled()); + } + + public function testNotSatisfiedWhenNotInstalled() + { + $dependency = new Dependency('something', '>=0.3.0'); + $this->assertFalse($dependency->isSatisfied()); + } + + public function testIsInstalled() + { + $dependency = new Dependency('something', '>=0.3.0'); + $dependency->setInstalledVersion('1.10.0'); + $this->assertTrue($dependency->isInstalled()); + } + + public function testNotEnabled() + { + $dependency = new Dependency('something', '>=0.3.0'); + $this->assertFalse($dependency->isEnabled()); + } + + public function testIsEnabled() + { + $dependency = new Dependency('something', '>=0.3.0'); + $dependency->setEnabled(); + $this->assertTrue($dependency->isEnabled()); + } + + public function testNotSatisfiedWhenNotEnabled() + { + $dependency = new Dependency('something', '>=0.3.0'); + $dependency->setInstalledVersion('1.10.0'); + $this->assertFalse($dependency->isSatisfied()); + } + + public function testSatisfiedWhenEqual() + { + $dependency = new Dependency('something', '>=0.3.0'); + $dependency->setInstalledVersion('0.3.0'); + $dependency->setEnabled(); + $this->assertTrue($dependency->isSatisfied()); + } + + public function testSatisfiedWhenGreater() + { + $dependency = new Dependency('something', '>=0.3.0'); + $dependency->setInstalledVersion('0.10.0'); + $dependency->setEnabled(); + $this->assertTrue($dependency->isSatisfied()); + } + + public function testNotSatisfiedWhenSmaller() + { + $dependency = new Dependency('something', '>=20.3.0'); + $dependency->setInstalledVersion('4.999.999'); + $dependency->setEnabled(); + $this->assertFalse($dependency->isSatisfied()); + } +}