diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php index cd6ae4f574..6ba8730528 100644 --- a/library/Icinga/Application/ApplicationBootstrap.php +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -741,7 +741,7 @@ public function hasLocales() */ protected function registerApplicationHooks(): self { - Hook::register('DbMigration', DbMigration::class, DbMigration::class); + DbMigration::register(); return $this; } diff --git a/library/Icinga/Application/Hook.php b/library/Icinga/Application/Hook.php index 95998bc071..3b8b9f24a9 100644 --- a/library/Icinga/Application/Hook.php +++ b/library/Icinga/Application/Hook.php @@ -68,6 +68,8 @@ public static function clean() * @param string $name One of the predefined hook names * * @return bool + * + * @deprecated Use {@link Hook\HookEssentials::isRegistered} instead */ public static function has($name) { @@ -267,6 +269,8 @@ private static function assertValidHook($instance, $name) * @param string $name One of the predefined hook names * * @return array + * + * @deprecated Use {@link Hook\HookEssentials::all} instead */ public static function all($name): array { @@ -291,6 +295,8 @@ public static function all($name): array * @param string $name One of the predefined hook names * * @return null|mixed + * + * @deprecated Use {@link Hook\HookEssentials::first} instead */ public static function first($name) { @@ -314,6 +320,8 @@ public static function first($name) * @param string $class Your class name, must inherit one of the * classes in the Icinga/Application/Hook folder * @param bool $alwaysRun To run the hook always (e.g. without permission check) + * + * @deprecated Use {@link Hook\HookEssentials::register} instead */ public static function register($name, $key, $class, $alwaysRun = false) { diff --git a/library/Icinga/Application/Hook/ApplicationStateHook.php b/library/Icinga/Application/Hook/ApplicationStateHook.php index afa061d532..5a6ccd3865 100644 --- a/library/Icinga/Application/Hook/ApplicationStateHook.php +++ b/library/Icinga/Application/Hook/ApplicationStateHook.php @@ -5,7 +5,6 @@ namespace Icinga\Application\Hook; -use Icinga\Application\Hook; use Icinga\Application\Logger; /** @@ -13,10 +12,17 @@ */ abstract class ApplicationStateHook { + use HookEssentials; + const ERROR = 'error'; private $messages = []; + final protected static function getHookName(): string + { + return 'ApplicationState'; + } + final public function hasMessages() { return ! empty($this->messages); @@ -66,12 +72,11 @@ final public static function getAllMessages() { $messages = []; - if (! Hook::has('ApplicationState')) { + if (! static::isRegistered()) { return $messages; } - foreach (Hook::all('ApplicationState') as $hook) { - /** @var self $hook */ + foreach (static::all() as $hook) { try { $hook->collectMessages(); } catch (\Exception $e) { diff --git a/library/Icinga/Application/Hook/AuditHook.php b/library/Icinga/Application/Hook/AuditHook.php index 6a019c2a03..ec38d8da20 100644 --- a/library/Icinga/Application/Hook/AuditHook.php +++ b/library/Icinga/Application/Hook/AuditHook.php @@ -8,11 +8,17 @@ use Exception; use InvalidArgumentException; use Icinga\Authentication\Auth; -use Icinga\Application\Hook; use Icinga\Application\Logger; abstract class AuditHook { + use HookEssentials; + + final protected static function getHookName(): string + { + return 'audit'; + } + /** * Log an activity to the audit log * @@ -27,7 +33,7 @@ abstract class AuditHook */ public static function logActivity($type, $message, ?array $data = null, $identity = null, $time = null) { - if (! Hook::has('audit')) { + if (! static::isRegistered()) { return; } @@ -39,8 +45,7 @@ public static function logActivity($type, $message, ?array $data = null, $identi $time = time(); } - foreach (Hook::all('audit') as $hook) { - /** @var self $hook */ + foreach (static::all() as $hook) { try { $formattedMessage = $message; if ($data !== null) { diff --git a/library/Icinga/Application/Hook/AuthenticationHook.php b/library/Icinga/Application/Hook/AuthenticationHook.php index 553e792b30..034c600b09 100644 --- a/library/Icinga/Application/Hook/AuthenticationHook.php +++ b/library/Icinga/Application/Hook/AuthenticationHook.php @@ -6,7 +6,6 @@ namespace Icinga\Application\Hook; use Icinga\User; -use Icinga\Web\Hook; use Icinga\Application\Logger; use Throwable; @@ -18,11 +17,18 @@ */ abstract class AuthenticationHook { + use HookEssentials; + /** * Name of the hook */ const NAME = 'authentication'; + final protected static function getHookName(): string + { + return static::NAME; + } + /** * Triggered after login in Icinga Web and when calling login action * @@ -57,8 +63,7 @@ public function onLogout(User $user) */ public static function triggerAuthFromSession(User $user): void { - /** @var static $hook */ - foreach (Hook::all(self::NAME) as $hook) { + foreach (static::all() as $hook) { try { $hook->onAuthFromSession($user); } catch (Throwable $e) { @@ -75,8 +80,7 @@ public static function triggerAuthFromSession(User $user): void */ public static function triggerLogin(User $user) { - /** @var AuthenticationHook $hook */ - foreach (Hook::all(self::NAME) as $hook) { + foreach (static::all() as $hook) { try { $hook->onLogin($user); } catch (\Exception $e) { @@ -93,8 +97,7 @@ public static function triggerLogin(User $user) */ public static function triggerLogout(User $user) { - /** @var AuthenticationHook $hook */ - foreach (Hook::all(self::NAME) as $hook) { + foreach (static::all() as $hook) { try { $hook->onLogout($user); } catch (\Exception $e) { diff --git a/library/Icinga/Application/Hook/ConfigFormEventsHook.php b/library/Icinga/Application/Hook/ConfigFormEventsHook.php index 6e83742c3a..c1f0ca9024 100644 --- a/library/Icinga/Application/Hook/ConfigFormEventsHook.php +++ b/library/Icinga/Application/Hook/ConfigFormEventsHook.php @@ -5,7 +5,6 @@ namespace Icinga\Application\Hook; -use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Exception\IcingaException; use Icinga\Web\Form; @@ -15,9 +14,16 @@ */ abstract class ConfigFormEventsHook { + use HookEssentials; + /** @var array Array of errors found while processing the form event hooks */ private static $lastErrors = []; + final protected static function getHookName(): string + { + return 'ConfigFormEvents'; + } + /** * Get whether the hook applies to the given config form * @@ -96,14 +102,13 @@ private static function runEventMethod($eventMethod, Form $form) { self::$lastErrors = []; - if (! Hook::has('ConfigFormEvents')) { + if (! static::isRegistered()) { return true; } $success = true; - foreach (Hook::all('ConfigFormEvents') as $hook) { - /** @var self $hook */ + foreach (static::all() as $hook) { if (! $hook->runAppliesTo($form)) { continue; } diff --git a/library/Icinga/Application/Hook/DbMigrationHook.php b/library/Icinga/Application/Hook/DbMigrationHook.php index fead618588..85ac3be23b 100644 --- a/library/Icinga/Application/Hook/DbMigrationHook.php +++ b/library/Icinga/Application/Hook/DbMigrationHook.php @@ -35,6 +35,7 @@ */ abstract class DbMigrationHook implements Countable { + use HookEssentials; use Translation; public const MYSQL_UPGRADE_DIR = 'schema/mysql-upgrades'; @@ -55,6 +56,11 @@ abstract class DbMigrationHook implements Countable /** @var ?string The current version of this hook */ protected $version; + final protected static function getHookName(): string + { + return 'DbMigration'; + } + /** * Get whether the specified table exists in the given database * diff --git a/library/Icinga/Application/Hook/GrapherHook.php b/library/Icinga/Application/Hook/GrapherHook.php index 2f9abcac43..71459733b8 100644 --- a/library/Icinga/Application/Hook/GrapherHook.php +++ b/library/Icinga/Application/Hook/GrapherHook.php @@ -16,6 +16,8 @@ */ abstract class GrapherHook extends WebBaseHook { + use HookEssentials; + /** * Whether this grapher provides previews * @@ -30,6 +32,11 @@ abstract class GrapherHook extends WebBaseHook */ protected $hasTinyPreviews = false; + final protected static function getHookName(): string + { + return 'grapher'; + } + /** * Constructor must live without arguments right now * diff --git a/library/Icinga/Application/Hook/HealthHook.php b/library/Icinga/Application/Hook/HealthHook.php index 6c0ade7668..af15f6aba1 100644 --- a/library/Icinga/Application/Hook/HealthHook.php +++ b/library/Icinga/Application/Hook/HealthHook.php @@ -6,7 +6,6 @@ namespace Icinga\Application\Hook; use Exception; -use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\Exception\IcingaException; @@ -15,6 +14,8 @@ abstract class HealthHook { + use HookEssentials; + /** @var int */ const STATE_OK = 0; @@ -39,6 +40,11 @@ abstract class HealthHook /** @var Url Url to a graphical representation of the available metrics */ protected $url; + final protected static function getHookName(): string + { + return 'health'; + } + /** * Get overall state * @@ -143,9 +149,7 @@ public function setUrl(Url $url) final public static function collectHealthData() { $checks = []; - foreach (Hook::all('health') as $hook) { - /** @var self $hook */ - + foreach (static::all() as $hook) { try { $hook->checkHealth(); $url = $hook->getUrl(); diff --git a/library/Icinga/Application/Hook/HookEssentials.php b/library/Icinga/Application/Hook/HookEssentials.php new file mode 100644 index 0000000000..22fa6d56ea --- /dev/null +++ b/library/Icinga/Application/Hook/HookEssentials.php @@ -0,0 +1,98 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Application\Hook; + +use Icinga\Application\ClassLoader; +use Icinga\Application\Hook; +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Module; + +/** + * Provides the mechanism for hook registration and retrieval + */ +trait HookEssentials +{ + /** + * Get the name that identifies this hook type + * + * @return string + */ + abstract protected static function getHookName(): string; + + /** + * Get whether the hook is registered + * + * @return bool + */ + public static function isRegistered(): bool + { + return Hook::has(static::getHookName()); + } + + /** + * Get all instances of the hook + * + * @return static[] + */ + public static function all(): array + { + return Hook::all(static::getHookName()); + } + + /** + * Get the first hook if any + * + * @return ?static + */ + public static function first(): ?static + { + return Hook::first(static::getHookName()); + } + + /** + * Get the module this hook belongs to, if any + * + * Returns null when the hook class is not part of a module namespace. + * + * @return ?Module + */ + public function getModule(): ?Module + { + if (! ClassLoader::classBelongsToModule(static::class)) { + return null; + } + + $moduleName = ClassLoader::extractModuleName(static::class); + + return Icinga::app()->getModuleManager()->getModule($moduleName); + } + + /** + * Register the hook + * + * @param ?bool $alwaysRun Whether to always run the hook without permission check. + * Defaults to {@see isAlwaysRun()}. + */ + public static function register(?bool $alwaysRun = null): void + { + Hook::register( + static::getHookName(), + static::class, + static::class, + $alwaysRun ?? static::isAlwaysRun() + ); + } + + /** + * Get whether the hook always runs without a permission check + * + * Override this in a hook base class to change the default. + */ + protected static function isAlwaysRun(): bool + { + return false; + } +} diff --git a/library/Icinga/Application/Hook/LoginButtonHook.php b/library/Icinga/Application/Hook/LoginButtonHook.php index fc845834b3..18edd6cf15 100644 --- a/library/Icinga/Application/Hook/LoginButtonHook.php +++ b/library/Icinga/Application/Hook/LoginButtonHook.php @@ -5,7 +5,6 @@ namespace Icinga\Application\Hook; -use Icinga\Application\Hook; use Icinga\Authentication\LoginButton; /** @@ -17,33 +16,33 @@ */ abstract class LoginButtonHook { + use HookEssentials; + /** - * Get the buttons to display below the login form + * Always runs without a permission check * - * Implement this method to return any number of {@link LoginButton} instances. - * Each button is rendered below the standard login form in the order provided. + * Login button hooks are intended to render on the login page, where + * permissions are not applicable. * - * @return LoginButton[] + * @return bool */ - abstract public function getButtons(): array; + protected static function isAlwaysRun(): bool + { + return true; + } - /** - * Get all registered implementations - * - * @return static[] - */ - public static function all(): array + final protected static function getHookName(): string { - return Hook::all('LoginButton'); + return 'LoginButton'; } /** - * Register the class as a LoginButton hook implementation + * Get the buttons to display below the login form + * + * Implement this method to return any number of {@link LoginButton} instances. + * Each button is rendered below the standard login form in the order provided. * - * Call this method on your implementation during module initialization to make Icinga Web aware of your hook. + * @return LoginButton[] */ - public static function register(): void - { - Hook::register('LoginButton', static::class, static::class, true); - } + abstract public function getButtons(): array; } diff --git a/library/Icinga/Application/Hook/PdfexportHook.php b/library/Icinga/Application/Hook/PdfexportHook.php index 2c1d6b3d4e..0ae0c530df 100644 --- a/library/Icinga/Application/Hook/PdfexportHook.php +++ b/library/Icinga/Application/Hook/PdfexportHook.php @@ -10,6 +10,13 @@ */ abstract class PdfexportHook { + use HookEssentials; + + final protected static function getHookName(): string + { + return 'Pdfexport'; + } + /** * Get whether PDF export is supported * diff --git a/library/Icinga/Application/Hook/RequestHook.php b/library/Icinga/Application/Hook/RequestHook.php index 2f14255576..07f2f27655 100644 --- a/library/Icinga/Application/Hook/RequestHook.php +++ b/library/Icinga/Application/Hook/RequestHook.php @@ -5,13 +5,32 @@ namespace Icinga\Application\Hook; -use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Web\Request; use Throwable; abstract class RequestHook { + use HookEssentials; + + /** + * Always runs without a permission check + * + * Request hooks are system-level concerns that fire on every request as + * part of the framework dispatch cycle, independent of user permissions. + * + * @return bool + */ + protected static function isAlwaysRun(): bool + { + return true; + } + + final protected static function getHookName(): string + { + return 'RequestHook'; + } + /** * Triggered after a request has been dispatched * @@ -38,24 +57,4 @@ final public static function postDispatch(Request $request): void } } } - - /** - * Get all registered implementations - * - * @return static[] - */ - public static function all(): array - { - return Hook::all('RequestHook'); - } - - /** - * Register the class as a RequestHook implementation - * - * Call this method on your implementation during module initialization to make Icinga Web aware of your hook. - */ - public static function register(): void - { - Hook::register('RequestHook', static::class, static::class, true); - } } diff --git a/library/Icinga/Application/Hook/ThemeLoaderHook.php b/library/Icinga/Application/Hook/ThemeLoaderHook.php index cfd2bce92a..b9683398c1 100644 --- a/library/Icinga/Application/Hook/ThemeLoaderHook.php +++ b/library/Icinga/Application/Hook/ThemeLoaderHook.php @@ -13,6 +13,13 @@ */ abstract class ThemeLoaderHook { + use HookEssentials; + + final protected static function getHookName(): string + { + return 'ThemeLoader'; + } + /** * Get the path for the given theme * diff --git a/library/Icinga/Application/Hook/TicketHook.php b/library/Icinga/Application/Hook/TicketHook.php index 7aac600b65..cbece9d679 100644 --- a/library/Icinga/Application/Hook/TicketHook.php +++ b/library/Icinga/Application/Hook/TicketHook.php @@ -19,6 +19,8 @@ */ abstract class TicketHook { + use HookEssentials; + /** * Last error, if any * @@ -26,6 +28,11 @@ abstract class TicketHook */ protected $lastError; + final protected static function getHookName(): string + { + return 'ticket'; + } + /** * Create a new ticket hook * diff --git a/library/Icinga/Application/MigrationManager.php b/library/Icinga/Application/MigrationManager.php index 1f03eace1b..2edf267d7d 100644 --- a/library/Icinga/Application/MigrationManager.php +++ b/library/Icinga/Application/MigrationManager.php @@ -302,8 +302,7 @@ protected function load(): void { $this->pendingMigrations = []; - /** @var DbMigrationHook $hook */ - foreach (Hook::all('DbMigration') as $hook) { + foreach (DbMigrationHook::all() as $hook) { if (empty($hook->getMigrations())) { continue; } diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 5fb7428af9..d048ca8178 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -1432,6 +1432,8 @@ protected function slashesToNamespace($class) * @param bool $alwaysRun To run the hook always (e.g. without permission check) * * @return $this + * + * @deprecated Use {@link Hook\HookEssentials::register} instead */ protected function provideHook($name, $implementation = null, $alwaysRun = false) { diff --git a/library/Icinga/File/Pdf.php b/library/Icinga/File/Pdf.php index d3c764b2a4..3212cddc44 100644 --- a/library/Icinga/File/Pdf.php +++ b/library/Icinga/File/Pdf.php @@ -8,11 +8,11 @@ use Dompdf\Dompdf; use Dompdf\Options; use Exception; +use Icinga\Application\Hook\PdfexportHook; use Icinga\Application\Icinga; use Icinga\Exception\ProgrammingError; use Icinga\Util\Environment; use Icinga\Web\FileCache; -use Icinga\Web\Hook; use Icinga\Web\Url; class Pdf @@ -56,8 +56,8 @@ public function renderControllerAction($controller) $request = $controller->getRequest(); - if (Hook::has('Pdfexport')) { - $pdfexport = Hook::first('Pdfexport'); + if (PdfexportHook::isRegistered()) { + $pdfexport = PdfexportHook::first(); $pdfexport->streamPdfFromHtml($html, sprintf( '%s-%s-%d', $request->getControllerName(), diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php index fc7a025f12..70b9b7acb4 100644 --- a/library/Icinga/Web/StyleSheet.php +++ b/library/Icinga/Web/StyleSheet.php @@ -6,6 +6,7 @@ namespace Icinga\Web; use Exception; +use Icinga\Application\Hook\ThemeLoaderHook; use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\Authentication\Auth; @@ -309,9 +310,9 @@ public static function getThemeFile($theme) $app = Icinga::app(); if ($theme && $theme !== self::DEFAULT_THEME) { - if (Hook::has('ThemeLoader')) { + if (ThemeLoaderHook::isRegistered()) { try { - $path = Hook::first('ThemeLoader')->getThemeFile($theme); + $path = ThemeLoaderHook::first()->getThemeFile($theme); } catch (Exception $e) { Logger::error('Failed to call ThemeLoader hook: %s', $e); $path = null; diff --git a/library/Icinga/Web/Widget/Tabextension/OutputFormat.php b/library/Icinga/Web/Widget/Tabextension/OutputFormat.php index 7fd2802e97..729d77cd1e 100644 --- a/library/Icinga/Web/Widget/Tabextension/OutputFormat.php +++ b/library/Icinga/Web/Widget/Tabextension/OutputFormat.php @@ -5,6 +5,7 @@ namespace Icinga\Web\Widget\Tabextension; +use Icinga\Application\Hook\PdfexportHook; use Icinga\Application\Platform; use Icinga\Application\Hook; use Icinga\Web\Url; @@ -84,7 +85,7 @@ public function getSupportedTypes() { $supportedTypes = array(); - $pdfexport = Hook::has('Pdfexport'); + $pdfexport = PdfexportHook::isRegistered(); if ($pdfexport || Platform::extensionLoaded('gd')) { $supportedTypes[self::TYPE_PDF] = array(