icingaweb2/library/Icinga/Application/Hook.php

329 lines
8.6 KiB
PHP
Raw Normal View History

<?php
/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
2013-08-20 19:33:15 +02:00
namespace Icinga\Application;
use Exception;
use Icinga\Authentication\Auth;
use Icinga\Application\Modules\Manager;
2014-02-18 18:48:13 +01:00
use Icinga\Exception\ProgrammingError;
/**
2015-11-12 17:58:19 +01:00
* 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();
2013-06-28 11:16:35 +02:00
/**
2013-08-20 19:33:15 +02:00
* Namespace prefix
*
2013-06-28 11:16:35 +02:00
* @var string
*/
2015-11-12 17:58:19 +01:00
public static $BASE_NS = 'Icinga\\Application\\Hook\\';
2013-06-28 11:16:35 +02:00
/**
* Append this string to base class
*
* All base classes renamed to *Hook
*
* @var string
*/
public static $classSuffix = 'Hook';
2013-08-20 19:33:15 +02:00
/**
* Reset object state
*/
2013-06-28 11:16:35 +02:00
public static function clean()
{
self::$hooks = array();
self::$instances = array();
2015-11-12 17:58:19 +01:00
self::$BASE_NS = 'Icinga\\Application\\Hook\\';
2013-06-28 11:16:35 +02:00
}
/**
* 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)
{
2015-11-12 19:52:20 +01:00
$name = self::normalizeHookName($name);
2018-07-26 11:51:23 +02:00
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;
}
}
2018-07-26 11:51:23 +02:00
return false;
}
2015-11-12 19:52:20 +01:00
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
2013-08-20 19:33:15 +02:00
*
* @return mixed
2013-06-28 11:16:35 +02:00
*/
public static function createInstance($name, $key)
{
2015-11-12 19:52:20 +01:00
$name = self::normalizeHookName($name);
if (!self::has($name)) {
2013-06-28 11:16:35 +02:00
return null;
}
2013-06-28 11:16:35 +02:00
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
);
}
2013-06-28 11:16:35 +02:00
try {
$instance = new $class();
} catch (Exception $e) {
Logger::debug(
2013-06-28 11:16:35 +02:00
'Hook "%s" (%s) (%s) failed, will be unloaded: %s',
$name,
$key,
$class,
$e->getMessage()
);
// TODO: Persist unloading for "some time" or "current session"
2013-06-28 11:16:35 +02:00
unset(self::$hooks[$name][$key]);
return null;
}
2013-06-28 11:16:35 +02:00
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)
{
2016-12-19 21:14:32 +01:00
if (Icinga::app()->isCli()) {
return true;
}
return Auth::getInstance()->hasPermission(
Manager::MODULE_PERMISSION_NS . self::extractModuleName($class)
);
}
2013-06-28 11:16:35 +02:00
/**
* Test for a valid class name
2013-08-20 19:33:15 +02:00
*
* @param mixed $instance
* @param string $name
*
* @throws ProgrammingError
2013-06-28 11:16:35 +02:00
*/
private static function assertValidHook($instance, $name)
2013-06-28 11:16:35 +02:00
{
2015-11-12 19:52:20 +01:00
$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)
2015-11-12 17:58:19 +01:00
. '\\Hook\\'
. ucfirst($name)
. $suffix;
}
2013-06-28 11:16:35 +02:00
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;
}
}
2013-06-28 11:16:35 +02:00
throw new ProgrammingError(
'%s is not an instance of %s',
get_class($instance),
$base_class
2013-06-28 11:16:35 +02:00
);
}
}
/**
* Return all instances of a specific name
2013-08-20 19:33:15 +02:00
*
* @param string $name One of the predefined hook names
2013-08-20 19:33:15 +02:00
*
* @return array
2013-06-28 11:16:35 +02:00
*/
public static function all($name): array
2013-06-28 11:16:35 +02:00
{
2015-11-13 14:27:46 +01:00
$name = self::normalizeHookName($name);
if (! self::has($name)) {
return [];
2013-06-28 11:16:35 +02:00
}
2013-06-28 11:16:35 +02:00
foreach (self::$hooks[$name] as $key => $hook) {
list($class, $alwaysRun) = $hook;
if ($alwaysRun || self::hasPermission($class)) {
self::createInstance($name, $key);
2013-06-28 11:16:35 +02:00
}
}
return self::$instances[$name] ?? [];
2013-06-28 11:16:35 +02:00
}
/**
2013-08-20 19:33:15 +02:00
* Get the first hook
*
* @param string $name One of the predefined hook names
2013-08-20 19:33:15 +02:00
*
* @return null|mixed
2013-06-28 11:16:35 +02:00
*/
public static function first($name)
{
2015-11-12 19:52:20 +01:00
$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);
}
}
}
2013-06-28 11:16:35 +02:00
}
/**
* 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)
{
2015-11-12 19:52:20 +01:00
$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];
}
}