summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/Hook
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icingadb/Hook')
-rw-r--r--library/Icingadb/Hook/ActionsHook/ObjectActionsHook.php83
-rw-r--r--library/Icingadb/Hook/Common/HookUtils.php39
-rw-r--r--library/Icingadb/Hook/CustomVarRendererHook.php102
-rw-r--r--library/Icingadb/Hook/EventDetailExtensionHook.php21
-rw-r--r--library/Icingadb/Hook/ExtensionHook/BaseExtensionHook.php146
-rw-r--r--library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php118
-rw-r--r--library/Icingadb/Hook/ExtensionHook/ObjectsDetailExtensionHook.php102
-rw-r--r--library/Icingadb/Hook/HostActionsHook.php21
-rw-r--r--library/Icingadb/Hook/HostDetailExtensionHook.php21
-rw-r--r--library/Icingadb/Hook/HostsDetailExtensionHook.php28
-rw-r--r--library/Icingadb/Hook/IcingadbSupportHook.php50
-rw-r--r--library/Icingadb/Hook/PluginOutputHook.php63
-rw-r--r--library/Icingadb/Hook/ServiceActionsHook.php21
-rw-r--r--library/Icingadb/Hook/ServiceDetailExtensionHook.php21
-rw-r--r--library/Icingadb/Hook/ServicesDetailExtensionHook.php28
-rw-r--r--library/Icingadb/Hook/TabHook.php82
-rw-r--r--library/Icingadb/Hook/TabHook/HookActions.php148
-rw-r--r--library/Icingadb/Hook/UserDetailExtensionHook.php21
-rw-r--r--library/Icingadb/Hook/UsergroupDetailExtensionHook.php21
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;
+}