diff options
Diffstat (limited to 'library/Icingadb/Hook/ExtensionHook')
3 files changed, 366 insertions, 0 deletions
diff --git a/library/Icingadb/Hook/ExtensionHook/BaseExtensionHook.php b/library/Icingadb/Hook/ExtensionHook/BaseExtensionHook.php new file mode 100644 index 0000000..dfefdcd --- /dev/null +++ b/library/Icingadb/Hook/ExtensionHook/BaseExtensionHook.php @@ -0,0 +1,146 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook\ExtensionHook; + +use Icinga\Module\Icingadb\Hook\Common\HookUtils; + +abstract class BaseExtensionHook +{ + use HookUtils; + + /** @var int Used as default return value for {@see BaseExtensionHook::getLocation()} */ + const IDENTIFY_LOCATION_BY_SECTION = -1; + + /** @var string Output section, right at the top */ + const OUTPUT_SECTION = 'output'; + + /** @var string Graph section, below output */ + const GRAPH_SECTION = 'graph'; + + /** @var string Detail section, below graphs */ + const DETAIL_SECTION = 'detail'; + + /** @var string Action section, below action and note urls */ + const ACTION_SECTION = 'action'; + + /** @var string Problem section, below comments and downtimes */ + const PROBLEM_SECTION = 'problem'; + + /** @var string Related section, below groups and notification recipients */ + const RELATED_SECTION = 'related'; + + /** @var string State section, below check statistics and performance data */ + const STATE_SECTION = 'state'; + + /** @var string Config section, below custom variables and feature toggles */ + const CONFIG_SECTION = 'config'; + + /** + * Base locations for all known sections + * + * @var array<string, int> + */ + const BASE_LOCATIONS = [ + self::OUTPUT_SECTION => 1000, + self::GRAPH_SECTION => 1100, + self::DETAIL_SECTION => 1200, + self::ACTION_SECTION => 1300, + self::PROBLEM_SECTION => 1400, + self::RELATED_SECTION => 1500, + self::STATE_SECTION => 1600, + self::CONFIG_SECTION => 1700 + ]; + + /** @var int This hook's location */ + private $location = self::IDENTIFY_LOCATION_BY_SECTION; + + /** @var string This hook's section */ + private $section = self::DETAIL_SECTION; + + /** + * Set this hook's location + * + * Note that setting the location explicitly may override other widgets using the same location. But beware that + * this applies to this hook's widget as well. + * + * Also, while the sections are guaranteed to always refer to the same general location, this guarantee is lost + * when setting a location explicitly. The core and base locations may change at any time and any explicitly set + * location will **not** adjust accordingly. + * + * @param int $location + * + * @return void + */ + final public function setLocation(int $location) + { + $this->location = $location; + } + + /** + * Get this hook's location + * + * @return int + */ + final public function getLocation(): int + { + return $this->location; + } + + /** + * Set this hook's section + * + * Sections are used to place widgets loosely in a general location. Using e.g. the `state` section this hook's + * widget will always appear after the check statistics and performance data widgets. + * + * @param string $section + * + * @return void + */ + final public function setSection(string $section) + { + $this->section = $section; + } + + /** + * Get this hook's section + * + * @return string + */ + final public function getSection(): string + { + return $this->section; + } + + /** + * Union both arrays and sort the result by key + * + * @param array $coreElements + * @param array $extensions + * + * @return array + */ + final public static function injectExtensions(array $coreElements, array $extensions): array + { + $extensions += $coreElements; + + uksort($extensions, function ($a, $b) { + if ($a < 1000 && $b >= 1000) { + $b -= 1000; + if (abs($a - $b) < 10 && abs($a % 100 - $b % 100) < 10) { + return -1; + } + } elseif ($b < 1000 && $a >= 1000) { + $a -= 1000; + if (abs($a - $b) < 10 && abs($a % 100 - $b % 100) < 10) { + return 1; + } + } + + return $a < $b ? -1 : ($a > $b ? 1 : 0); + }); + + return $extensions; + } +} diff --git a/library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php b/library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php new file mode 100644 index 0000000..4f0881d --- /dev/null +++ b/library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php @@ -0,0 +1,118 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook\ExtensionHook; + +use Exception; +use Icinga\Application\Hook; +use Icinga\Application\Logger; +use Icinga\Exception\IcingaException; +use Icinga\Module\Icingadb\Hook\EventDetailExtensionHook; +use Icinga\Module\Icingadb\Hook\HostDetailExtensionHook; +use Icinga\Module\Icingadb\Hook\ServiceDetailExtensionHook; +use Icinga\Module\Icingadb\Hook\UserDetailExtensionHook; +use Icinga\Module\Icingadb\Hook\UsergroupDetailExtensionHook; +use Icinga\Module\Icingadb\Model\History; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Icingadb\Model\User; +use Icinga\Module\Icingadb\Model\Usergroup; +use InvalidArgumentException; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Text; +use ipl\Html\ValidHtml; +use ipl\Orm\Model; + +use function ipl\Stdlib\get_php_type; + +abstract class ObjectDetailExtensionHook extends BaseExtensionHook +{ + /** + * Load all extensions for the given object + * + * @param Host|Service|User|Usergroup|History $object + * + * @return array<int, ValidHtml> + * + * @throws InvalidArgumentException If the given model is not supported + */ + final public static function loadExtensions(Model $object): array + { + switch (true) { + case $object instanceof Host: + /** @var HostDetailExtensionHook $hook */ + $hookName = 'Icingadb\\HostDetailExtension'; + break; + case $object instanceof Service: + /** @var ServiceDetailExtensionHook $hook */ + $hookName = 'Icingadb\\ServiceDetailExtension'; + break; + case $object instanceof User: + /** @var UserDetailExtensionHook $hook */ + $hookName = 'Icingadb\\UserDetailExtension'; + break; + case $object instanceof Usergroup: + /** @var UsergroupDetailExtensionHook $hook */ + $hookName = 'Icingadb\\UsergroupDetailExtension'; + break; + case $object instanceof History: + /** @var EventDetailExtensionHook $hook */ + $hookName = 'Icingadb\\EventDetailExtension'; + break; + default: + throw new InvalidArgumentException( + sprintf('%s is not a supported object type', get_php_type($object)) + ); + } + + $extensions = []; + $lastUsedLocations = []; + foreach (Hook::all($hookName) as $hook) { + $location = $hook->getLocation(); + if ($location < 0) { + $location = null; + } + + if ($location === null) { + $section = $hook->getSection(); + if (! isset(self::BASE_LOCATIONS[$section])) { + Logger::error('Detail extension %s is using an invalid section: %s', get_class($hook), $section); + $section = self::DETAIL_SECTION; + } + + if (isset($lastUsedLocations[$section])) { + $location = ++$lastUsedLocations[$section]; + } else { + $location = self::BASE_LOCATIONS[$section]; + $lastUsedLocations[$section] = $location; + } + } + + try { + // It may be ValidHtml, but modules shouldn't be able to break our views. + // That's why it needs to be rendered instantly, as any error will then + // be caught here. + $extension = (string) $hook->getHtmlForObject(clone $object); + + $moduleName = $hook->getModule()->getName(); + + $extensions[$location] = new HtmlElement( + 'div', + Attributes::create([ + 'class' => 'icinga-module module-' . $moduleName, + 'data-icinga-module' => $moduleName + ]), + HtmlString::create($extension) + ); + } catch (Exception $e) { + Logger::error("Failed to load detail extension: %s\n%s", $e, $e->getTraceAsString()); + $extensions[$location] = Text::create(IcingaException::describe($e)); + } + } + + return $extensions; + } +} diff --git a/library/Icingadb/Hook/ExtensionHook/ObjectsDetailExtensionHook.php b/library/Icingadb/Hook/ExtensionHook/ObjectsDetailExtensionHook.php new file mode 100644 index 0000000..5fe7c6c --- /dev/null +++ b/library/Icingadb/Hook/ExtensionHook/ObjectsDetailExtensionHook.php @@ -0,0 +1,102 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook\ExtensionHook; + +use Exception; +use Icinga\Application\Hook; +use Icinga\Application\Logger; +use Icinga\Exception\IcingaException; +use Icinga\Module\Icingadb\Common\BaseFilter; +use Icinga\Module\Icingadb\Hook\HostsDetailExtensionHook; +use Icinga\Module\Icingadb\Hook\ServicesDetailExtensionHook; +use InvalidArgumentException; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Text; +use ipl\Html\ValidHtml; +use ipl\Orm\Query; +use ipl\Stdlib\Filter; + +abstract class ObjectsDetailExtensionHook extends BaseExtensionHook +{ + use BaseFilter; + + /** + * Load all extensions for the given objects + * + * @param string $objectType + * @param Query $query + * @param Filter\Rule $baseFilter + * + * @return array<int, ValidHtml> + * + * @throws InvalidArgumentException If the given object type is not supported + */ + final public static function loadExtensions(string $objectType, Query $query, Filter\Rule $baseFilter): array + { + switch ($objectType) { + case 'host': + /** @var HostsDetailExtensionHook $hook */ + $hookName = 'Icingadb\\HostsDetailExtension'; + break; + case 'service': + /** @var ServicesDetailExtensionHook $hook */ + $hookName = 'Icingadb\\ServicesDetailExtension'; + break; + default: + throw new InvalidArgumentException( + sprintf('%s is not a supported object type', $objectType) + ); + } + + $extensions = []; + $lastUsedLocations = []; + foreach (Hook::all($hookName) as $hook) { + $location = $hook->getLocation(); + if ($location < 0) { + $location = null; + } + + if ($location === null) { + $section = $hook->getSection(); + if (! isset(self::BASE_LOCATIONS[$section])) { + Logger::error('Detail extension %s is using an invalid section: %s', get_class($hook), $section); + $section = self::DETAIL_SECTION; + } + + if (isset($lastUsedLocations[$section])) { + $location = ++$lastUsedLocations[$section]; + } else { + $location = self::BASE_LOCATIONS[$section]; + $lastUsedLocations[$section] = $location; + } + } + + try { + // It may be ValidHtml, but modules shouldn't be able to break our views. + // That's why it needs to be rendered instantly, as any error will then + // be caught here. + $extension = (string) $hook->setBaseFilter($baseFilter)->getHtmlForObjects(clone $query); + + $moduleName = $hook->getModule()->getName(); + + $extensions[$location] = new HtmlElement( + 'div', + Attributes::create([ + 'class' => 'icinga-module module-' . $moduleName, + 'data-icinga-module' => $moduleName + ]), + HtmlString::create($extension) + ); + } catch (Exception $e) { + Logger::error("Failed to load details extension: %s\n%s", $e, $e->getTraceAsString()); + $extensions[$location] = Text::create(IcingaException::describe($e)); + } + } + + return $extensions; + } +} |