summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Application/Hook
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Application/Hook')
-rw-r--r--library/Icinga/Application/Hook/ApplicationStateHook.php90
-rw-r--r--library/Icinga/Application/Hook/AuditHook.php123
-rw-r--r--library/Icinga/Application/Hook/AuthenticationHook.php75
-rw-r--r--library/Icinga/Application/Hook/ConfigFormEventsHook.php137
-rw-r--r--library/Icinga/Application/Hook/GrapherHook.php111
-rw-r--r--library/Icinga/Application/Hook/HealthHook.php222
-rw-r--r--library/Icinga/Application/Hook/PdfexportHook.php25
-rw-r--r--library/Icinga/Application/Hook/ThemeLoaderHook.php22
-rw-r--r--library/Icinga/Application/Hook/Ticket/TicketPattern.php140
-rw-r--r--library/Icinga/Application/Hook/TicketHook.php210
-rw-r--r--library/Icinga/Application/Hook/WebBaseHook.php54
11 files changed, 1209 insertions, 0 deletions
diff --git a/library/Icinga/Application/Hook/ApplicationStateHook.php b/library/Icinga/Application/Hook/ApplicationStateHook.php
new file mode 100644
index 0000000..be973fe
--- /dev/null
+++ b/library/Icinga/Application/Hook/ApplicationStateHook.php
@@ -0,0 +1,90 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+
+/**
+ * Application state hook base class
+ */
+abstract class ApplicationStateHook
+{
+ const ERROR = 'error';
+
+ private $messages = [];
+
+ final public function hasMessages()
+ {
+ return ! empty($this->messages);
+ }
+
+ final public function getMessages()
+ {
+ return $this->messages;
+ }
+
+ /**
+ * Add an error message
+ *
+ * The timestamp of the message is used for deduplication and thus must refer to the time when the error first
+ * occurred. Don't use {@link time()} here!
+ *
+ * @param string $id ID of the message. The ID must be prefixed with the module name
+ * @param int $timestamp Timestamp when the error first occurred
+ * @param string $message Error message
+ *
+ * @return $this
+ */
+ final public function addError($id, $timestamp, $message)
+ {
+ $id = trim($id);
+ $timestamp = (int) $timestamp;
+
+ if (! strlen($id)) {
+ throw new \InvalidArgumentException('ID expected.');
+ }
+
+ if (! $timestamp) {
+ throw new \InvalidArgumentException('Timestamp expected.');
+ }
+
+ $this->messages[sha1($id . $timestamp)] = [self::ERROR, $timestamp, $message];
+
+ return $this;
+ }
+
+ /**
+ * Override this method in order to provide application state messages
+ */
+ abstract public function collectMessages();
+
+ final public static function getAllMessages()
+ {
+ $messages = [];
+
+ if (! Hook::has('ApplicationState')) {
+ return $messages;
+ }
+
+ foreach (Hook::all('ApplicationState') as $hook) {
+ /** @var self $hook */
+ try {
+ $hook->collectMessages();
+ } catch (\Exception $e) {
+ Logger::error(
+ "Failed to collect messages from hook '%s'. An error occurred: %s",
+ get_class($hook),
+ $e
+ );
+ }
+
+ if ($hook->hasMessages()) {
+ $messages += $hook->getMessages();
+ }
+ }
+
+ return $messages;
+ }
+}
diff --git a/library/Icinga/Application/Hook/AuditHook.php b/library/Icinga/Application/Hook/AuditHook.php
new file mode 100644
index 0000000..e6209da
--- /dev/null
+++ b/library/Icinga/Application/Hook/AuditHook.php
@@ -0,0 +1,123 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Exception;
+use InvalidArgumentException;
+use Icinga\Authentication\Auth;
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+
+abstract class AuditHook
+{
+ /**
+ * Log an activity to the audit log
+ *
+ * Propagates the given message details to all known hook implementations.
+ *
+ * @param string $type An arbitrary name identifying the type of activity
+ * @param string $message A detailed description possibly referencing parameters in $data
+ * @param array $data Additional information (How this is stored or used is up to each implementation)
+ * @param string $identity An arbitrary name identifying the responsible subject, defaults to the current user
+ * @param int $time A timestamp defining when the activity occurred, defaults to now
+ */
+ public static function logActivity($type, $message, array $data = null, $identity = null, $time = null)
+ {
+ if (! Hook::has('audit')) {
+ return;
+ }
+
+ if ($identity === null) {
+ $identity = Auth::getInstance()->getUser()->getUsername();
+ }
+
+ if ($time === null) {
+ $time = time();
+ }
+
+ foreach (Hook::all('audit') as $hook) {
+ /** @var self $hook */
+ try {
+ $formattedMessage = $message;
+ if ($data !== null) {
+ // Calling formatMessage on each hook is intended and allows
+ // intercepting message formatting while keeping it implicit
+ $formattedMessage = $hook->formatMessage($message, $data);
+ }
+
+ $hook->logMessage($time, $identity, $type, $formattedMessage, $data);
+ } catch (Exception $e) {
+ Logger::error(
+ 'Failed to propagate audit message to hook "%s". An error occurred: %s',
+ get_class($hook),
+ $e
+ );
+ }
+ }
+ }
+
+ /**
+ * Log a message to the audit log
+ *
+ * @param int $time A timestamp defining when the activity occurred
+ * @param string $identity An arbitrary name identifying the responsible subject
+ * @param string $type An arbitrary name identifying the type of activity
+ * @param string $message A detailed description of the activity
+ * @param array $data Additional activity information
+ */
+ abstract public function logMessage($time, $identity, $type, $message, array $data = null);
+
+ /**
+ * Substitute the given message with its accompanying data
+ *
+ * @param string $message
+ * @param array $messageData
+ *
+ * @return string
+ */
+ public function formatMessage($message, array $messageData)
+ {
+ return preg_replace_callback('/{{(.+?)}}/', function ($match) use ($messageData) {
+ return $this->extractMessageValue(explode('.', $match[1]), $messageData);
+ }, $message);
+ }
+
+ /**
+ * Extract the given value path from the given message data
+ *
+ * @param array $path
+ * @param array $messageData
+ *
+ * @return mixed
+ *
+ * @throws InvalidArgumentException In case of an invalid or missing format parameter
+ */
+ protected function extractMessageValue(array $path, array $messageData)
+ {
+ $key = array_shift($path);
+ if (array_key_exists($key, $messageData)) {
+ $value = $messageData[$key];
+ } else {
+ throw new InvalidArgumentException("Missing format parameter '$key'");
+ }
+
+ if (empty($path)) {
+ if (! is_scalar($value)) {
+ throw new InvalidArgumentException(
+ 'Invalid format parameter. Expected scalar for path "' . join('.', $path) . '".'
+ . ' Got "' . gettype($value) . '" instead'
+ );
+ }
+
+ return $value;
+ } elseif (! is_array($value)) {
+ throw new InvalidArgumentException(
+ 'Invalid format parameter. Expected array for path "'. join('.', $path) . '".'
+ . ' Got "' . gettype($value) . '" instead'
+ );
+ }
+
+ return $this->extractMessageValue($path, $value);
+ }
+}
diff --git a/library/Icinga/Application/Hook/AuthenticationHook.php b/library/Icinga/Application/Hook/AuthenticationHook.php
new file mode 100644
index 0000000..41cc661
--- /dev/null
+++ b/library/Icinga/Application/Hook/AuthenticationHook.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Icinga\Application\Hook;
+
+use Icinga\User;
+use Icinga\Web\Hook;
+use Icinga\Application\Logger;
+
+/**
+ * Icinga Web Authentication Hook base class
+ *
+ * This hook can be used to authenticate the user in a third party application.
+ * Extend this class if you want to perform arbitrary actions during the login and logout.
+ */
+abstract class AuthenticationHook
+{
+ /**
+ * Name of the hook
+ */
+ const NAME = 'authentication';
+
+ /**
+ * Triggered after login in Icinga Web and when calling login action even if already authenticated in Icinga Web
+ *
+ * @param User $user
+ */
+ public function onLogin(User $user)
+ {
+ }
+
+ /**
+ * Triggered before logout from Icinga Web
+ *
+ * @param User $user
+ */
+ public function onLogout(User $user)
+ {
+ }
+
+ /**
+ * Call the onLogin() method of all registered AuthHook(s)
+ *
+ * @param User $user
+ */
+ public static function triggerLogin(User $user)
+ {
+ /** @var AuthenticationHook $hook */
+ foreach (Hook::all(self::NAME) as $hook) {
+ try {
+ $hook->onLogin($user);
+ } catch (\Exception $e) {
+ // Avoid error propagation if login failed in third party application
+ Logger::error($e);
+ }
+ }
+ }
+
+ /**
+ * Call the onLogout() method of all registered AuthHook(s)
+ *
+ * @param User $user
+ */
+ public static function triggerLogout(User $user)
+ {
+ /** @var AuthenticationHook $hook */
+ foreach (Hook::all(self::NAME) as $hook) {
+ try {
+ $hook->onLogout($user);
+ } catch (\Exception $e) {
+ // Avoid error propagation if login failed in third party application
+ Logger::error($e);
+ }
+ }
+ }
+}
diff --git a/library/Icinga/Application/Hook/ConfigFormEventsHook.php b/library/Icinga/Application/Hook/ConfigFormEventsHook.php
new file mode 100644
index 0000000..aa0cadc
--- /dev/null
+++ b/library/Icinga/Application/Hook/ConfigFormEventsHook.php
@@ -0,0 +1,137 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Form;
+
+/**
+ * Base class for config form event hooks
+ */
+abstract class ConfigFormEventsHook
+{
+ /** @var array Array of errors found while processing the form event hooks */
+ private static $lastErrors = [];
+
+ /**
+ * Get whether the hook applies to the given config form
+ *
+ * @param Form $form
+ *
+ * @return bool
+ */
+ public function appliesTo(Form $form)
+ {
+ return false;
+ }
+
+ /**
+ * isValid event hook
+ *
+ * Implement this method in order to run code after the form has been validated successfully.
+ * Throw an exception here if either the form is not valid or you want interrupt the form handling.
+ * The exception's message will be automatically added as form error message so that it will be
+ * displayed in the frontend.
+ *
+ * @param Form $form
+ *
+ * @throws \Exception If either the form is not valid or to interrupt the form handling
+ */
+ public function isValid(Form $form)
+ {
+ }
+
+ /**
+ * onSuccess event hook
+ *
+ * Implement this method in order to run code after the configuration form has been stored successfully.
+ * You can't interrupt the form handling here. Any exception will be caught, logged and notified.
+ *
+ * @param Form $form
+ */
+ public function onSuccess(Form $form)
+ {
+ }
+
+ /**
+ * Get an array of errors found while processing the form event hooks
+ *
+ * @return array
+ */
+ final public static function getLastErrors()
+ {
+ return static::$lastErrors;
+ }
+
+ /**
+ * Run all isValid hooks
+ *
+ * @param Form $form
+ *
+ * @return bool Returns false if any hook threw an exception
+ */
+ final public static function runIsValid(Form $form)
+ {
+ return self::runEventMethod('isValid', $form);
+ }
+
+ /**
+ * Run all onSuccess hooks
+ *
+ * @param Form $form
+ *
+ * @return bool Returns false if any hook threw an exception
+ */
+ final public static function runOnSuccess(Form $form)
+ {
+ return self::runEventMethod('onSuccess', $form);
+ }
+
+ private static function runEventMethod($eventMethod, Form $form)
+ {
+ static::$lastErrors = [];
+
+ if (! Hook::has('ConfigFormEvents')) {
+ return true;
+ }
+
+ $success = true;
+
+ foreach (Hook::all('ConfigFormEvents') as $hook) {
+ /** @var self $hook */
+ if (! $hook->runAppliesTo($form)) {
+ continue;
+ }
+
+ try {
+ $hook->$eventMethod($form);
+ } catch (\Exception $e) {
+ static::$lastErrors[] = $e->getMessage();
+
+ Logger::error("%s\n%s", $e, IcingaException::getConfidentialTraceAsString($e));
+
+ $success = false;
+ }
+ }
+
+ return $success;
+ }
+
+ private function runAppliesTo(Form $form)
+ {
+ try {
+ $appliesTo = $this->appliesTo($form);
+ } catch (\Exception $e) {
+ // Don't save exception to last errors because we do not want to disturb the user for messed up
+ // appliesTo checks
+ Logger::error("%s\n%s", $e, IcingaException::getConfidentialTraceAsString($e));
+
+ $appliesTo = false;
+ }
+
+ return $appliesTo === true;
+ }
+}
diff --git a/library/Icinga/Application/Hook/GrapherHook.php b/library/Icinga/Application/Hook/GrapherHook.php
new file mode 100644
index 0000000..dfb2135
--- /dev/null
+++ b/library/Icinga/Application/Hook/GrapherHook.php
@@ -0,0 +1,111 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+/**
+ * Icinga Web Grapher Hook base class
+ *
+ * Extend this class if you want to integrate your graphing solution nicely into
+ * Icinga Web.
+ */
+abstract class GrapherHook extends WebBaseHook
+{
+ /**
+ * Whether this grapher provides previews
+ *
+ * @var bool
+ */
+ protected $hasPreviews = false;
+
+ /**
+ * Whether this grapher provides tiny previews
+ *
+ * @var bool
+ */
+ protected $hasTinyPreviews = false;
+
+ /**
+ * Constructor must live without arguments right now
+ *
+ * Therefore the constructor is final, we might change our opinion about
+ * this one far day
+ */
+ final public function __construct()
+ {
+ $this->init();
+ }
+
+ /**
+ * Overwrite this function if you want to do some initialization stuff
+ *
+ * @return void
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Whether this grapher provides previews
+ *
+ * @return bool
+ */
+ public function hasPreviews()
+ {
+ return $this->hasPreviews;
+ }
+
+ /**
+ * Whether this grapher provides tiny previews
+ *
+ * @return bool
+ */
+ public function hasTinyPreviews()
+ {
+ return $this->hasTinyPreviews;
+ }
+
+ /**
+ * Whether a graph for the monitoring object exist
+ *
+ * @param MonitoredObject $object
+ *
+ * @return bool
+ */
+ abstract public function has(MonitoredObject $object);
+
+ /**
+ * Get a preview for the given object
+ *
+ * This function must return an empty string if no graph exists.
+ *
+ * @param MonitoredObject $object
+ *
+ * @return string
+ * @throws ProgrammingError
+ *
+ */
+ public function getPreviewHtml(MonitoredObject $object)
+ {
+ throw new ProgrammingError('This hook provide previews but it is not implemented');
+ }
+
+
+ /**
+ * Get a tiny preview for the given object
+ *
+ * This function must return an empty string if no graph exists.
+ *
+ * @param MonitoredObject $object
+ *
+ * @return string
+ * @throws ProgrammingError
+ */
+ public function getTinyPreviewHtml(MonitoredObject $object)
+ {
+ throw new ProgrammingError('This hook provide tiny previews but it is not implemented');
+ }
+}
diff --git a/library/Icinga/Application/Hook/HealthHook.php b/library/Icinga/Application/Hook/HealthHook.php
new file mode 100644
index 0000000..f6420b5
--- /dev/null
+++ b/library/Icinga/Application/Hook/HealthHook.php
@@ -0,0 +1,222 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Exception\IcingaException;
+use ipl\Web\Url;
+use LogicException;
+
+abstract class HealthHook
+{
+ /** @var int */
+ const STATE_OK = 0;
+
+ /** @var int */
+ const STATE_WARNING = 1;
+
+ /** @var int */
+ const STATE_CRITICAL = 2;
+
+ /** @var int */
+ const STATE_UNKNOWN = 3;
+
+ /** @var int The overall state */
+ protected $state;
+
+ /** @var string Message describing the overall state */
+ protected $message;
+
+ /** @var array Available metrics */
+ protected $metrics;
+
+ /** @var Url Url to a graphical representation of the available metrics */
+ protected $url;
+
+ /**
+ * Get overall state
+ *
+ * @return int
+ */
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ /**
+ * Set overall state
+ *
+ * @param int $state
+ *
+ * @return $this
+ */
+ public function setState($state)
+ {
+ $this->state = $state;
+
+ return $this;
+ }
+
+ /**
+ * Get the message describing the overall state
+ *
+ * @return string
+ */
+ public function getMessage()
+ {
+ return $this->message;
+ }
+
+ /**
+ * Set the message describing the overall state
+ *
+ * @param string $message
+ *
+ * @return $this
+ */
+ public function setMessage($message)
+ {
+ $this->message = $message;
+
+ return $this;
+ }
+
+ /**
+ * Get available metrics
+ *
+ * @return array
+ */
+ public function getMetrics()
+ {
+ return $this->metrics;
+ }
+
+ /**
+ * Set available metrics
+ *
+ * @param array $metrics
+ *
+ * @return $this
+ */
+ public function setMetrics(array $metrics)
+ {
+ $this->metrics = $metrics;
+
+ return $this;
+ }
+
+ /**
+ * Get the url to a graphical representation of the available metrics
+ *
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Set the url to a graphical representation of the available metrics
+ *
+ * @param Url $url
+ *
+ * @return $this
+ */
+ public function setUrl(Url $url)
+ {
+ $this->url = $url;
+
+ return $this;
+ }
+
+ /**
+ * Collect available health data from hooks
+ *
+ * @return ArrayDatasource
+ */
+ final public static function collectHealthData()
+ {
+ $checks = [];
+ foreach (Hook::all('health') as $hook) {
+ /** @var self $hook */
+
+ try {
+ $hook->checkHealth();
+ $url = $hook->getUrl();
+ $state = $hook->getState();
+ $message = $hook->getMessage();
+ $metrics = $hook->getMetrics();
+ } catch (Exception $e) {
+ Logger::error('Failed to check health: %s', $e);
+
+ $state = self::STATE_UNKNOWN;
+ $message = IcingaException::describe($e);
+ $metrics = null;
+ $url = null;
+ }
+
+ $checks[] = (object) [
+ 'module' => $hook->getModuleName(),
+ 'name' => $hook->getName(),
+ 'url' => $url ? $url->getAbsoluteUrl() : null,
+ 'state' => $state,
+ 'message' => $message,
+ 'metrics' => (object) $metrics
+ ];
+ }
+
+ return (new ArrayDatasource($checks))
+ ->setKeyColumn('name');
+ }
+
+ /**
+ * Get the name of the hook
+ *
+ * Only used in API responses to differentiate it from other hooks of the same module.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ $classPath = get_class($this);
+ $parts = explode('\\', $classPath);
+ $className = array_pop($parts);
+
+ if (substr($className, -4) === 'Hook') {
+ $className = substr($className, 1, -4);
+ }
+
+ return strtolower($className[0]) . substr($className, 1);
+ }
+
+ /**
+ * Get the name of the module providing this hook
+ *
+ * @return string
+ *
+ * @throws LogicException
+ */
+ public function getModuleName()
+ {
+ $classPath = get_class($this);
+ if (substr($classPath, 0, 14) !== 'Icinga\\Module\\') {
+ throw new LogicException('Not a module hook');
+ }
+
+ $withoutPrefix = substr($classPath, 14);
+ return strtolower(substr($withoutPrefix, 0, strpos($withoutPrefix, '\\')));
+ }
+
+ /**
+ * Check health
+ *
+ * Implement this method and set the overall state, message, url and metrics.
+ *
+ * @return void
+ */
+ abstract public function checkHealth();
+}
diff --git a/library/Icinga/Application/Hook/PdfexportHook.php b/library/Icinga/Application/Hook/PdfexportHook.php
new file mode 100644
index 0000000..36e9f51
--- /dev/null
+++ b/library/Icinga/Application/Hook/PdfexportHook.php
@@ -0,0 +1,25 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+/**
+ * Base class for the PDF Export Hook
+ */
+abstract class PdfexportHook
+{
+ /**
+ * Get whether PDF export is supported
+ *
+ * @return bool
+ */
+ abstract public function isSupported();
+
+ /**
+ * Render the specified HTML to PDF and stream it to the client
+ *
+ * @param string $html The HTML to render to PDF
+ * @param string $filename The filename for the generated PDF
+ */
+ abstract public function streamPdfFromHtml($html, $filename);
+}
diff --git a/library/Icinga/Application/Hook/ThemeLoaderHook.php b/library/Icinga/Application/Hook/ThemeLoaderHook.php
new file mode 100644
index 0000000..5320dd5
--- /dev/null
+++ b/library/Icinga/Application/Hook/ThemeLoaderHook.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+/**
+ * Provide an implementation of this hook to dynamically provide themes.
+ * Note that only the first registered hook is utilized. Also note that
+ * for ordinary themes this hook is not required. Place such in your
+ * module's theme path: <module-path>/public/css/themes
+ */
+abstract class ThemeLoaderHook
+{
+ /**
+ * Get the path for the given theme
+ *
+ * @param ?string $theme
+ *
+ * @return ?string The path or NULL if the theme is unknown
+ */
+ abstract public function getThemeFile(?string $theme): ?string;
+}
diff --git a/library/Icinga/Application/Hook/Ticket/TicketPattern.php b/library/Icinga/Application/Hook/Ticket/TicketPattern.php
new file mode 100644
index 0000000..e37fcc1
--- /dev/null
+++ b/library/Icinga/Application/Hook/Ticket/TicketPattern.php
@@ -0,0 +1,140 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook\Ticket;
+
+use ArrayAccess;
+
+/**
+ * A ticket pattern
+ *
+ * This class should be used by modules which provide implementations for the Web 2 ticket hook.
+ * Have a look at the GenericTTS module for a possible use case.
+ */
+class TicketPattern implements ArrayAccess
+{
+ /**
+ * The result of a performed ticket match
+ *
+ * @var array
+ */
+ protected $match = array();
+
+ /**
+ * The name of the TTS integration
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The ticket pattern
+ *
+ * @var string
+ */
+ protected $pattern;
+
+ public function offsetExists($offset): bool
+ {
+ return isset($this->match[$offset]);
+ }
+
+ public function offsetGet($offset): ?string
+ {
+ return array_key_exists($offset, $this->match) ? $this->match[$offset] : null;
+ }
+
+ public function offsetSet($offset, $value): void
+ {
+ if ($offset === null) {
+ $this->match[] = $value;
+ } else {
+ $this->match[$offset] = $value;
+ }
+ }
+
+ public function offsetUnset($offset): void
+ {
+ unset($this->match[$offset]);
+ }
+
+
+ /**
+ * Get the result of a performed ticket match
+ *
+ * @return array
+ */
+ public function getMatch()
+ {
+ return $this->match;
+ }
+
+ /**
+ * Set the result of a performed ticket match
+ *
+ * @param array $match
+ *
+ * @return $this
+ */
+ public function setMatch(array $match)
+ {
+ $this->match = $match;
+ return $this;
+ }
+
+ /**
+ * Get the name of the TTS integration
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the name of the TTS integration
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * Get the ticket pattern
+ *
+ * @return string
+ */
+ public function getPattern()
+ {
+ return $this->pattern;
+ }
+
+ /**
+ * Set the ticket pattern
+ *
+ * @param string $pattern
+ *
+ * @return $this
+ */
+ public function setPattern($pattern)
+ {
+ $this->pattern = $pattern;
+ return $this;
+ }
+
+ /**
+ * Whether the integration is properly configured, i.e. the pattern and the URL are not empty
+ *
+ * @return bool
+ */
+ public function isValid()
+ {
+ return ! empty($this->pattern);
+ }
+}
diff --git a/library/Icinga/Application/Hook/TicketHook.php b/library/Icinga/Application/Hook/TicketHook.php
new file mode 100644
index 0000000..ceb3738
--- /dev/null
+++ b/library/Icinga/Application/Hook/TicketHook.php
@@ -0,0 +1,210 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use ArrayIterator;
+use ErrorException;
+use Exception;
+use Icinga\Application\Hook\Ticket\TicketPattern;
+use Icinga\Application\Logger;
+use Icinga\Exception\IcingaException;
+
+/**
+ * Base class for ticket hooks
+ *
+ * Extend this class if you want to integrate your ticketing solution into Icinga Web 2.
+ */
+abstract class TicketHook
+{
+ /**
+ * Last error, if any
+ *
+ * @var string|null
+ */
+ protected $lastError;
+
+ /**
+ * Create a new ticket hook
+ *
+ * @see init() For hook initialization.
+ */
+ final public function __construct()
+ {
+ $this->init();
+ }
+
+ /**
+ * Overwrite this function for hook initialization, e.g. loading the hook's config
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Create a link for each matched element in the subject text
+ *
+ * @param array|TicketPattern $match Matched element according to {@link getPattern()}
+ *
+ * @return string Replacement string
+ */
+ abstract public function createLink($match);
+
+ /**
+ * Get the pattern(s) to search for
+ *
+ * Return an array of TicketPattern instances here to support multiple TTS integrations.
+ *
+ * @return string|TicketPattern[]
+ */
+ abstract public function getPattern();
+
+ /**
+ * Apply ticket patterns to the given text
+ *
+ * @param string $text
+ * @param TicketPattern[] $ticketPatterns
+ *
+ * @return string
+ */
+ private function applyTicketPatterns($text, array $ticketPatterns)
+ {
+ $out = '';
+ $start = 0;
+
+ $iterator = new ArrayIterator($ticketPatterns);
+ $iterator->rewind();
+
+ while ($iterator->valid()) {
+ $ticketPattern = $iterator->current();
+
+ try {
+ preg_match($ticketPattern->getPattern(), $text, $match, PREG_OFFSET_CAPTURE, $start);
+ } catch (ErrorException $e) {
+ $this->fail('Can\'t create ticket links: Pattern is invalid: %s', IcingaException::describe($e));
+ $iterator->next();
+ continue;
+ }
+
+ if (empty($match)) {
+ $iterator->next();
+ continue;
+ }
+
+ // Remove preg_offset from match for the ticket pattern
+ $carry = array();
+ array_walk($match, function ($value, $key) use (&$carry) {
+ $carry[$key] = $value[0];
+ }, $carry);
+ $ticketPattern->setMatch($carry);
+
+ $offsetLeft = $match[0][1];
+ $matchLength = strlen($match[0][0]);
+
+ $out .= substr($text, $start, $offsetLeft - $start);
+
+ try {
+ $out .= $this->createLink($ticketPattern);
+ } catch (Exception $e) {
+ $this->fail('Can\'t create ticket links: %s', IcingaException::describe($e));
+ return $text;
+ }
+
+ $start = $offsetLeft + $matchLength;
+ }
+
+ $out .= substr($text, $start);
+
+ return $out;
+ }
+
+ /**
+ * Helper function to create a TicketPattern instance
+ *
+ * @param string $name Name of the TTS integration
+ * @param string $pattern Ticket pattern
+ *
+ * @return TicketPattern
+ */
+ protected function createTicketPattern($name, $pattern)
+ {
+ $ticketPattern = new TicketPattern();
+ $ticketPattern
+ ->setName($name)
+ ->setPattern($pattern);
+ return $ticketPattern;
+ }
+
+ /**
+ * Set the hook as failed w/ the given message
+ *
+ * @param string $message Error message or error format string
+ * @param mixed ...$arg Format string argument
+ */
+ private function fail($message)
+ {
+ $args = array_slice(func_get_args(), 1);
+ $lastError = vsprintf($message, $args);
+ Logger::debug($lastError);
+ $this->lastError = $lastError;
+ }
+
+ /**
+ * Get the last error, if any
+ *
+ * @return string|null
+ */
+ public function getLastError()
+ {
+ return $this->lastError;
+ }
+
+ /**
+ * Create links w/ {@link createLink()} in the given text that matches to the subject from {@link getPattern()}
+ *
+ * In case of errors a debug message is recorded to the log and any subsequent call to {@link createLinks()} will
+ * be a no-op.
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ final public function createLinks($text)
+ {
+ if ($this->lastError !== null) {
+ return $text;
+ }
+
+ try {
+ $pattern = $this->getPattern();
+ } catch (Exception $e) {
+ $this->fail('Can\'t create ticket links: Retrieving the pattern failed: %s', IcingaException::describe($e));
+ return $text;
+ }
+
+ if (empty($pattern)) {
+ $this->fail('Can\'t create ticket links: Pattern is empty');
+ return $text;
+ }
+
+ if (is_array($pattern)) {
+ $text = $this->applyTicketPatterns($text, $pattern);
+ } else {
+ try {
+ $text = preg_replace_callback(
+ $pattern,
+ array($this, 'createLink'),
+ $text
+ );
+ } catch (ErrorException $e) {
+ $this->fail('Can\'t create ticket links: Pattern is invalid: %s', IcingaException::describe($e));
+ return $text;
+ } catch (Exception $e) {
+ $this->fail('Can\'t create ticket links: %s', IcingaException::describe($e));
+ return $text;
+ }
+ }
+
+ return $text;
+ }
+}
diff --git a/library/Icinga/Application/Hook/WebBaseHook.php b/library/Icinga/Application/Hook/WebBaseHook.php
new file mode 100644
index 0000000..09e8f4f
--- /dev/null
+++ b/library/Icinga/Application/Hook/WebBaseHook.php
@@ -0,0 +1,54 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Zend_Controller_Action_HelperBroker;
+use Zend_View;
+
+/**
+ * Base class for web hooks
+ *
+ * The class provides access to the view
+ */
+class WebBaseHook
+{
+ /**
+ * View instance
+ *
+ * @var Zend_View
+ */
+ private $view;
+
+ /**
+ * Set the view instance
+ *
+ * @param Zend_View $view
+ *
+ * @return $this
+ */
+ public function setView(Zend_View $view)
+ {
+ $this->view = $view;
+
+ return $this;
+ }
+
+ /**
+ * Get the view instance
+ *
+ * @return Zend_View
+ */
+ public function getView()
+ {
+ if ($this->view === null) {
+ $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
+ if ($viewRenderer->view === null) {
+ $viewRenderer->initView();
+ }
+ $this->view = $viewRenderer->view;
+ }
+
+ return $this->view;
+ }
+}