diff options
Diffstat (limited to 'library/Icingadb/Hook')
19 files changed, 1136 insertions, 0 deletions
diff --git a/library/Icingadb/Hook/ActionsHook/ObjectActionsHook.php b/library/Icingadb/Hook/ActionsHook/ObjectActionsHook.php new file mode 100644 index 0000000..2f8df33 --- /dev/null +++ b/library/Icingadb/Hook/ActionsHook/ObjectActionsHook.php @@ -0,0 +1,83 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook\ActionsHook; + +use Exception; +use Icinga\Application\Hook; +use Icinga\Application\Logger; +use Icinga\Exception\IcingaException; +use Icinga\Module\Icingadb\Hook\Common\HookUtils; +use Icinga\Module\Icingadb\Hook\HostActionsHook; +use Icinga\Module\Icingadb\Hook\ServiceActionsHook; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use InvalidArgumentException; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Text; +use ipl\Orm\Model; +use ipl\Web\Widget\Link; + +use function ipl\Stdlib\get_php_type; + +abstract class ObjectActionsHook +{ + use HookUtils; + + /** + * Load all actions for the given object + * + * @param Host|Service $object + * + * @return HtmlElement + * + * @throws InvalidArgumentException If the given model is not supported + */ + final public static function loadActions(Model $object): HtmlElement + { + switch (true) { + case $object instanceof Host: + /** @var HostActionsHook $hook */ + $hookName = 'Icingadb\\HostActions'; + break; + case $object instanceof Service: + /** @var ServiceActionsHook $hook */ + $hookName = 'Icingadb\\ServiceActions'; + break; + default: + throw new InvalidArgumentException( + sprintf('%s is not a supported object type', get_php_type($object)) + ); + } + + $list = new HtmlElement('ul', Attributes::create(['class' => 'object-detail-actions'])); + foreach (Hook::all($hookName) as $hook) { + try { + foreach ($hook->getActionsForObject($object) as $link) { + if (! $link instanceof Link) { + continue; + } + + // 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. + $renderedLink = (string) $link; + $moduleName = $hook->getModule()->getName(); + + $list->addHtml(new HtmlElement('li', Attributes::create([ + 'class' => 'icinga-module module-' . $moduleName, + 'data-icinga-module' => $moduleName + ]), HtmlString::create($renderedLink))); + } + } catch (Exception $e) { + Logger::error("Failed to load object actions: %s\n%s", $e, $e->getTraceAsString()); + $list->addHtml(new HtmlElement('li', null, Text::create(IcingaException::describe($e)))); + } + } + + return $list; + } +} diff --git a/library/Icingadb/Hook/Common/HookUtils.php b/library/Icingadb/Hook/Common/HookUtils.php new file mode 100644 index 0000000..8778849 --- /dev/null +++ b/library/Icingadb/Hook/Common/HookUtils.php @@ -0,0 +1,39 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook\Common; + +use Icinga\Application\ClassLoader; +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Module; + +trait HookUtils +{ + final public function __construct() + { + $this->init(); + } + + /** + * Initialize this hook + * + * Override this in your concrete implementation for any initialization at construction time. + */ + protected function init() + { + } + + /** + * Get the module this hook belongs to + * + * @return Module + */ + final public function getModule(): Module + { + $moduleName = ClassLoader::extractModuleName(static::class); + + return Icinga::app()->getModuleManager() + ->getModule($moduleName); + } +} diff --git a/library/Icingadb/Hook/CustomVarRendererHook.php b/library/Icingadb/Hook/CustomVarRendererHook.php new file mode 100644 index 0000000..be3325b --- /dev/null +++ b/library/Icingadb/Hook/CustomVarRendererHook.php @@ -0,0 +1,102 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Closure; +use Exception; +use Icinga\Application\Hook; +use Icinga\Application\Logger; +use Icinga\Module\Icingadb\Hook\Common\HookUtils; +use ipl\Orm\Model; + +abstract class CustomVarRendererHook +{ + use HookUtils; + + /** + * Prefetch the data the hook needs to render custom variables + * + * @param Model $object The object for which they'll be rendered + * + * @return bool Return true if the hook can render variables for the given object, false otherwise + */ + abstract public function prefetchForObject(Model $object): bool; + + /** + * Render the given variable name + * + * @param string $key + * + * @return ?mixed + */ + abstract public function renderCustomVarKey(string $key); + + /** + * Render the given variable value + * + * @param string $key + * @param mixed $value + * + * @return ?mixed + */ + abstract public function renderCustomVarValue(string $key, $value); + + /** + * Return a group name for the given variable name + * + * @param string $key + * + * @return ?string + */ + abstract public function identifyCustomVarGroup(string $key): ?string; + + /** + * Prepare available hooks to render custom variables of the given object + * + * @param Model $object + * + * @return Closure A callback ($key, $value) which returns an array [$newKey, $newValue, $group] + */ + final public static function prepareForObject(Model $object): Closure + { + $hooks = []; + foreach (Hook::all('Icingadb/CustomVarRenderer') as $hook) { + /** @var self $hook */ + try { + if ($hook->prefetchForObject($object)) { + $hooks[] = $hook; + } + } catch (Exception $e) { + Logger::error('Failed to load hook %s:', get_class($hook), $e); + } + } + + return function (string $key, $value) use ($hooks, $object) { + $newKey = $key; + $newValue = $value; + $group = null; + foreach ($hooks as $hook) { + /** @var self $hook */ + + try { + $renderedKey = $hook->renderCustomVarKey($key); + $renderedValue = $hook->renderCustomVarValue($key, $value); + $group = $hook->identifyCustomVarGroup($key); + } catch (Exception $e) { + Logger::error('Failed to use hook %s:', get_class($hook), $e); + continue; + } + + if ($renderedKey !== null || $renderedValue !== null) { + $newKey = $renderedKey ?? $key; + $newValue = $renderedValue ?? $value; + break; + } + } + + return [$newKey, $newValue, $group]; + }; + } +} diff --git a/library/Icingadb/Hook/EventDetailExtensionHook.php b/library/Icingadb/Hook/EventDetailExtensionHook.php new file mode 100644 index 0000000..c348a0c --- /dev/null +++ b/library/Icingadb/Hook/EventDetailExtensionHook.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\History; +use ipl\Html\ValidHtml; + +abstract class EventDetailExtensionHook extends ObjectDetailExtensionHook +{ + /** + * Assemble and return an HTML representation of the given event + * + * @param History $event + * + * @return ValidHtml + */ + abstract public function getHtmlForObject(History $event): ValidHtml; +} 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; + } +} diff --git a/library/Icingadb/Hook/HostActionsHook.php b/library/Icingadb/Hook/HostActionsHook.php new file mode 100644 index 0000000..73c58f4 --- /dev/null +++ b/library/Icingadb/Hook/HostActionsHook.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Hook\ActionsHook\ObjectActionsHook; +use Icinga\Module\Icingadb\Model\Host; +use ipl\Web\Widget\Link; + +abstract class HostActionsHook extends ObjectActionsHook +{ + /** + * Assemble and return a list of HTML anchors for the given host + * + * @param Host $host + * + * @return Link[] + */ + abstract public function getActionsForObject(Host $host): array; +} diff --git a/library/Icingadb/Hook/HostDetailExtensionHook.php b/library/Icingadb/Hook/HostDetailExtensionHook.php new file mode 100644 index 0000000..a6e9ab0 --- /dev/null +++ b/library/Icingadb/Hook/HostDetailExtensionHook.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\Host; +use ipl\Html\ValidHtml; + +abstract class HostDetailExtensionHook extends ObjectDetailExtensionHook +{ + /** + * Assemble and return an HTML representation of the given host + * + * @param Host $host + * + * @return ValidHtml + */ + abstract public function getHtmlForObject(Host $host): ValidHtml; +} diff --git a/library/Icingadb/Hook/HostsDetailExtensionHook.php b/library/Icingadb/Hook/HostsDetailExtensionHook.php new file mode 100644 index 0000000..79c091e --- /dev/null +++ b/library/Icingadb/Hook/HostsDetailExtensionHook.php @@ -0,0 +1,28 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectsDetailExtensionHook; +use Icinga\Module\Icingadb\Model\Host; +use ipl\Html\ValidHtml; +use ipl\Orm\Query; + +abstract class HostsDetailExtensionHook extends ObjectsDetailExtensionHook +{ + /** + * Assemble and return an HTML representation of the given hosts + * + * The given query is already pre-filtered with the user's custom filter and restrictions. The base filter does + * only contain the user's custom filter, use this for e.g. subsidiary links. + * + * The query is also limited by default, use `$hosts->limit(null)` to clear that. But beware that this may yield + * a huge result set in case of a bulk selection. + * + * @param Query<Host> $hosts + * + * @return ValidHtml + */ + abstract public function getHtmlForObjects(Query $hosts): ValidHtml; +} diff --git a/library/Icingadb/Hook/IcingadbSupportHook.php b/library/Icingadb/Hook/IcingadbSupportHook.php new file mode 100644 index 0000000..cd43a5a --- /dev/null +++ b/library/Icingadb/Hook/IcingadbSupportHook.php @@ -0,0 +1,50 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Application\Icinga; +use Icinga\Module\Icingadb\Hook\Common\HookUtils; +use Icinga\Web\Session; + +abstract class IcingadbSupportHook +{ + use HookUtils; + + /** @var string key name of preference */ + const PREFERENCE_NAME = 'icingadb.as_backend'; + + /** + * Return whether your module supports IcingaDB or not + * + * @return bool + */ + public function supportsIcingaDb(): bool + { + return true; + } + + /** + * Whether icingadb is set as the preferred backend in preferences + * + * @return bool Return true if icingadb is set as backend, false otherwise + */ + final public static function isIcingaDbSetAsPreferredBackend(): bool + { + return (bool) Session::getSession() + ->getNamespace('icingadb') + ->get(self::PREFERENCE_NAME, false); + } + + /** + * Whether to use icingadb as the backend + * + * @return bool Returns true if monitoring module is disabled or icingadb is selected as backend, false otherwise. + */ + final public static function useIcingaDbAsBackend(): bool + { + return ! Icinga::app()->getModuleManager()->hasEnabled('monitoring') + || self::isIcingaDbSetAsPreferredBackend(); + } +} diff --git a/library/Icingadb/Hook/PluginOutputHook.php b/library/Icingadb/Hook/PluginOutputHook.php new file mode 100644 index 0000000..7c744ee --- /dev/null +++ b/library/Icingadb/Hook/PluginOutputHook.php @@ -0,0 +1,63 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Exception; +use Icinga\Application\Hook; +use Icinga\Application\Logger; +use Icinga\Module\Icingadb\Hook\Common\HookUtils; + +abstract class PluginOutputHook +{ + use HookUtils; + + /** + * Return whether the given command is supported or not + * + * @param string $commandName + * + * @return bool + */ + abstract public function isSupportedCommand(string $commandName): bool; + + /** + * Process the given plugin output based on the specified check command + * + * Try to process the output as efficient and fast as possible. + * Especially list view performance may suffer otherwise. + * + * @param string $output A host's or service's output + * @param string $commandName The name of the checkcommand that produced the output + * @param bool $enrichOutput Whether macros or other markup should be processed + * + * @return string + */ + abstract public function render(string $output, string $commandName, bool $enrichOutput): string; + + /** + * Let all hooks process the given plugin output based on the specified check command + * + * @param string $output + * @param string $commandName + * @param bool $enrichOutput + * + * @return string + */ + final public static function processOutput(string $output, string $commandName, bool $enrichOutput): string + { + foreach (Hook::all('Icingadb\\PluginOutput') as $hook) { + /** @var self $hook */ + try { + if ($hook->isSupportedCommand($commandName)) { + $output = $hook->render($output, $commandName, $enrichOutput); + } + } catch (Exception $e) { + Logger::error("Unable to process plugin output: %s\n%s", $e, $e->getTraceAsString()); + } + } + + return $output; + } +} diff --git a/library/Icingadb/Hook/ServiceActionsHook.php b/library/Icingadb/Hook/ServiceActionsHook.php new file mode 100644 index 0000000..988cdb6 --- /dev/null +++ b/library/Icingadb/Hook/ServiceActionsHook.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Hook\ActionsHook\ObjectActionsHook; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Web\Widget\Link; + +abstract class ServiceActionsHook extends ObjectActionsHook +{ + /** + * Assemble and return a list of HTML anchors for the given service + * + * @param Service $service + * + * @return Link[] + */ + abstract public function getActionsForObject(Service $service): array; +} diff --git a/library/Icingadb/Hook/ServiceDetailExtensionHook.php b/library/Icingadb/Hook/ServiceDetailExtensionHook.php new file mode 100644 index 0000000..5344620 --- /dev/null +++ b/library/Icingadb/Hook/ServiceDetailExtensionHook.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Html\ValidHtml; + +abstract class ServiceDetailExtensionHook extends ObjectDetailExtensionHook +{ + /** + * Assemble and return an HTML representation of the given service + * + * @param Service $service + * + * @return ValidHtml + */ + abstract public function getHtmlForObject(Service $service): ValidHtml; +} diff --git a/library/Icingadb/Hook/ServicesDetailExtensionHook.php b/library/Icingadb/Hook/ServicesDetailExtensionHook.php new file mode 100644 index 0000000..35ba8d3 --- /dev/null +++ b/library/Icingadb/Hook/ServicesDetailExtensionHook.php @@ -0,0 +1,28 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectsDetailExtensionHook; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Html\ValidHtml; +use ipl\Orm\Query; + +abstract class ServicesDetailExtensionHook extends ObjectsDetailExtensionHook +{ + /** + * Assemble and return an HTML representation of the given services + * + * The given query is already pre-filtered with the user's custom filter and restrictions. The base filter does + * only contain the user's custom filter, use this for e.g. subsidiary links. + * + * The query is also limited by default, use `$hosts->limit(null)` to clear that. But beware that this may yield + * a huge result set in case of a bulk selection. + * + * @param Query<Service> $services + * + * @return ValidHtml + */ + abstract public function getHtmlForObjects(Query $services): ValidHtml; +} diff --git a/library/Icingadb/Hook/TabHook.php b/library/Icingadb/Hook/TabHook.php new file mode 100644 index 0000000..0c5b676 --- /dev/null +++ b/library/Icingadb/Hook/TabHook.php @@ -0,0 +1,82 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Hook\Common\HookUtils; +use ipl\Html\ValidHtml; +use ipl\Orm\Model; + +abstract class TabHook +{ + use Auth; + use Database; + use HookUtils; + + /** + * Get the tab's name + * + * The name is used to identify this hook later on. It must be unique. + * Multiple words in the name should be separated by dashes. (-) + * + * @return string + */ + abstract public function getName(): string; + + /** + * Get the tab's label + * + * The label is shown on the tab and in the browser's title. + * + * @return string + */ + abstract public function getLabel(): string; + + /** + * Get tab content for the given object + * + * @param Model $object + * + * @return ValidHtml[] + */ + abstract public function getContent(Model $object): array; + + /** + * Get tab controls for the given object + * + * @param Model $object + * + * @return ValidHtml[] + */ + public function getControls(Model $object): array + { + return []; + } + + /** + * Get tab footer for the given object + * + * @param Model $object + * + * @return ValidHtml[] + */ + public function getFooter(Model $object): array + { + return []; + } + + /** + * Get whether this tab should be shown + * + * @param Model $object + * + * @return bool + */ + public function shouldBeShown(Model $object): bool + { + return true; + } +} diff --git a/library/Icingadb/Hook/TabHook/HookActions.php b/library/Icingadb/Hook/TabHook/HookActions.php new file mode 100644 index 0000000..d2801a5 --- /dev/null +++ b/library/Icingadb/Hook/TabHook/HookActions.php @@ -0,0 +1,148 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook\TabHook; + +use Exception; +use Generator; +use Icinga\Application\Hook; +use Icinga\Application\Logger; +use Icinga\Module\Icingadb\Hook\TabHook; +use ipl\Html\ValidHtml; +use ipl\Orm\Model; +use ipl\Stdlib\Str; + +/** + * Trait HookActions + */ +trait HookActions +{ + /** @var Model The object to load tabs for */ + protected $objectToLoadTabsFor; + + /** @var TabHook[] Loaded tab hooks */ + protected $tabHooks; + + /** + * Get default control elements + * + * @return ValidHtml[] + */ + abstract protected function getDefaultTabControls(): array; + + public function __call($methodName, $args) + { + if (substr($methodName, -6) === 'Action') { + $hookName = substr($methodName, 0, -6); + + $hooks = $this->loadTabHooks(); + if (isset($hooks[$hookName])) { + $this->showTabHook($hooks[$hookName]); + return; + } + } + + parent::__call($methodName, $args); + } + + /** + * Register the object for which to load additional tabs + * + * @param Model $object + * + * @return void + */ + protected function loadTabsForObject(Model $object) + { + $this->objectToLoadTabsFor = $object; + } + + /** + * Load tab hooks + * + * @return array<string, TabHook> + */ + protected function loadTabHooks(): array + { + if ($this->objectToLoadTabsFor === null) { + return []; + } elseif ($this->tabHooks !== null) { + return $this->tabHooks; + } + + $this->tabHooks = []; + foreach (Hook::all('Icingadb\\Tab') as $hook) { + /** @var TabHook $hook */ + try { + if ($hook->shouldBeShown($this->objectToLoadTabsFor)) { + $this->tabHooks[Str::camel($hook->getName())] = $hook; + } + } catch (Exception $e) { + Logger::error("Failed to load tab hook: %s\n%s", $e, $e->getTraceAsString()); + } + } + + return $this->tabHooks; + } + + /** + * Load additional tabs + * + * @return Generator<string, array{label: string, url: string}> + */ + protected function loadAdditionalTabs(): Generator + { + foreach ($this->loadTabHooks() as $hook) { + yield $hook->getName() => [ + 'label' => $hook->getLabel(), + 'url' => 'icingadb/' . $this->getRequest()->getControllerName() . '/' . $hook->getName() + ]; + } + } + + /** + * Render the given tab hook + * + * @param TabHook $hook + * + * @return void + */ + protected function showTabHook(TabHook $hook) + { + $moduleName = $hook->getModule()->getName(); + + foreach ($hook->getControls($this->objectToLoadTabsFor) as $control) { + $this->addControl($control); + } + + if (! empty($this->controls->getContent())) { + $this->controls->addAttributes([ + 'class' => ['icinga-module', 'module-' . $moduleName], + 'data-icinga-module' => $moduleName + ]); + } else { + foreach ($this->getDefaultTabControls() as $control) { + $this->addControl($control); + } + } + + foreach ($hook->getContent($this->objectToLoadTabsFor) as $content) { + $this->addContent($content); + } + + $this->content->addAttributes([ + 'class' => ['icinga-module', 'module-' . $moduleName], + 'data-icinga-module' => $moduleName + ]); + + foreach ($hook->getFooter($this->objectToLoadTabsFor) as $footer) { + $this->addFooter($footer); + } + + $this->footer->addAttributes([ + 'class' => ['icinga-module', 'module-' . $moduleName], + 'data-icinga-module' => $moduleName + ]); + } +} diff --git a/library/Icingadb/Hook/UserDetailExtensionHook.php b/library/Icingadb/Hook/UserDetailExtensionHook.php new file mode 100644 index 0000000..bb1bf7e --- /dev/null +++ b/library/Icingadb/Hook/UserDetailExtensionHook.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\User; +use ipl\Html\ValidHtml; + +abstract class UserDetailExtensionHook extends ObjectDetailExtensionHook +{ + /** + * Assemble and return an HTML representation of the given user + * + * @param User $user + * + * @return ValidHtml + */ + abstract public function getHtmlForObject(User $user): ValidHtml; +} diff --git a/library/Icingadb/Hook/UsergroupDetailExtensionHook.php b/library/Icingadb/Hook/UsergroupDetailExtensionHook.php new file mode 100644 index 0000000..da2264d --- /dev/null +++ b/library/Icingadb/Hook/UsergroupDetailExtensionHook.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Hook; + +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\Usergroup; +use ipl\Html\ValidHtml; + +abstract class UsergroupDetailExtensionHook extends ObjectDetailExtensionHook +{ + /** + * Assemble and return an HTML representation of the given usergroup + * + * @param Usergroup $usergroup + * + * @return ValidHtml + */ + abstract public function getHtmlForObject(Usergroup $usergroup): ValidHtml; +} |