diff options
Diffstat (limited to 'library/Icinga/Application/Hook.php')
-rw-r--r-- | library/Icinga/Application/Hook.php | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/library/Icinga/Application/Hook.php b/library/Icinga/Application/Hook.php new file mode 100644 index 0000000..9720c6a --- /dev/null +++ b/library/Icinga/Application/Hook.php @@ -0,0 +1,328 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Application; + +use Exception; +use Icinga\Authentication\Auth; +use Icinga\Application\Modules\Manager; +use Icinga\Exception\ProgrammingError; + +/** + * Icinga Hook registry + * + * Modules making use of predefined hooks have to use this registry + * + * Usage: + * <code> + * Hook::register('grapher', 'My\\Grapher\\Class'); + * </code> + */ +class Hook +{ + /** + * Our hook name registry + * + * @var array + */ + protected static $hooks = array(); + + /** + * Hooks that have already been instantiated + * + * @var array + */ + protected static $instances = array(); + + /** + * Namespace prefix + * + * @var string + */ + public static $BASE_NS = 'Icinga\\Application\\Hook\\'; + + /** + * Append this string to base class + * + * All base classes renamed to *Hook + * + * @var string + */ + public static $classSuffix = 'Hook'; + + /** + * Reset object state + */ + public static function clean() + { + self::$hooks = array(); + self::$instances = array(); + self::$BASE_NS = 'Icinga\\Application\\Hook\\'; + } + + /** + * Whether someone registered itself for the given hook name + * + * @param string $name One of the predefined hook names + * + * @return bool + */ + public static function has($name) + { + $name = self::normalizeHookName($name); + + if (! array_key_exists($name, self::$hooks)) { + return false; + } + + foreach (self::$hooks[$name] as $hook) { + list($class, $alwaysRun) = $hook; + if ($alwaysRun || self::hasPermission($class)) { + return true; + } + } + + return false; + } + + protected static function normalizeHookName($name) + { + if (strpos($name, '\\') === false) { + $parts = explode('/', $name); + foreach ($parts as & $part) { + $part = ucfirst($part); + } + + return implode('\\', $parts); + } + + return $name; + } + + /** + * Create or return an instance of a given hook + * + * TODO: Should return some kind of a hook interface + * + * @param string $name One of the predefined hook names + * @param string $key The identifier of a specific subtype + * + * @return mixed + */ + public static function createInstance($name, $key) + { + $name = self::normalizeHookName($name); + + if (!self::has($name)) { + return null; + } + + if (isset(self::$instances[$name][$key])) { + return self::$instances[$name][$key]; + } + + $class = self::$hooks[$name][$key][0]; + + if (! class_exists($class)) { + throw new ProgrammingError( + 'Erraneous hook implementation, class "%s" does not exist', + $class + ); + } + try { + $instance = new $class(); + } catch (Exception $e) { + Logger::debug( + 'Hook "%s" (%s) (%s) failed, will be unloaded: %s', + $name, + $key, + $class, + $e->getMessage() + ); + // TODO: Persist unloading for "some time" or "current session" + unset(self::$hooks[$name][$key]); + return null; + } + + self::assertValidHook($instance, $name); + self::$instances[$name][$key] = $instance; + return $instance; + } + + protected static function splitHookName($name) + { + $sep = '\\'; + if (false === $module = strpos($name, $sep)) { + return array(null, $name); + } + return array( + substr($name, 0, $module), + substr($name, $module + 1) + ); + } + + /** + * Extract the Icinga module name from a given namespaced class name + * + * Does no validation, prefix must have been checked before + * + * Shameless copy of ClassLoader::extractModuleName() + * + * @param string $class The hook's class path + * + * @return string + */ + protected static function extractModuleName($class) + { + return lcfirst( + substr( + $class, + ClassLoader::MODULE_PREFIX_LENGTH, + strpos( + $class, + ClassLoader::NAMESPACE_SEPARATOR, + ClassLoader::MODULE_PREFIX_LENGTH + 1 + ) - ClassLoader::MODULE_PREFIX_LENGTH + ) + ); + } + + /** + * Return whether the user has the permission to access the module which provides the given hook + * + * @param string $class The hook's class path + * + * @return bool + */ + protected static function hasPermission($class) + { + if (Icinga::app()->isCli()) { + return true; + } + + return Auth::getInstance()->hasPermission( + Manager::MODULE_PERMISSION_NS . self::extractModuleName($class) + ); + } + + /** + * Test for a valid class name + * + * @param mixed $instance + * @param string $name + * + * @throws ProgrammingError + */ + private static function assertValidHook($instance, $name) + { + $name = self::normalizeHookName($name); + + $suffix = self::$classSuffix; // 'Hook' + $base = self::$BASE_NS; // 'Icinga\\Web\\Hook\\' + + list($module, $name) = self::splitHookName($name); + + if ($module === null) { + $base_class = $base . ucfirst($name) . 'Hook'; + + // I'm unsure whether this makes sense. Unused and Wrong. + if (strpos($base_class, $suffix) === false) { + $base_class .= $suffix; + } + } else { + $base_class = 'Icinga\\Module\\' + . ucfirst($module) + . '\\Hook\\' + . ucfirst($name) + . $suffix; + } + + if (!$instance instanceof $base_class) { + // This is a compatibility check. Should be removed one far day: + if ($module !== null) { + $compat_class = 'Icinga\\Module\\' + . ucfirst($module) + . '\\Web\\Hook\\' + . ucfirst($name) + . $suffix; + + if ($instance instanceof $compat_class) { + return; + } + } + + throw new ProgrammingError( + '%s is not an instance of %s', + get_class($instance), + $base_class + ); + } + } + + /** + * Return all instances of a specific name + * + * @param string $name One of the predefined hook names + * + * @return array + */ + public static function all($name): array + { + $name = self::normalizeHookName($name); + if (! self::has($name)) { + return []; + } + + foreach (self::$hooks[$name] as $key => $hook) { + list($class, $alwaysRun) = $hook; + if ($alwaysRun || self::hasPermission($class)) { + self::createInstance($name, $key); + } + } + + return self::$instances[$name] ?? []; + } + + /** + * Get the first hook + * + * @param string $name One of the predefined hook names + * + * @return null|mixed + */ + public static function first($name) + { + $name = self::normalizeHookName($name); + + if (self::has($name)) { + foreach (self::$hooks[$name] as $key => $hook) { + list($class, $alwaysRun) = $hook; + if ($alwaysRun || self::hasPermission($class)) { + return self::createInstance($name, $key); + } + } + } + } + + /** + * Register a class + * + * @param string $name One of the predefined hook names + * @param string $key The identifier of a specific subtype + * @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) + */ + public static function register($name, $key, $class, $alwaysRun = false) + { + $name = self::normalizeHookName($name); + + if (!isset(self::$hooks[$name])) { + self::$hooks[$name] = array(); + } + + $class = ltrim($class, ClassLoader::NAMESPACE_SEPARATOR); + + self::$hooks[$name][$key] = [$class, $alwaysRun]; + } +} |