* Hook::register('grapher', 'My\\Grapher\\Class'); * */ 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]; } }