summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Application/Modules
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Application/Modules')
-rw-r--r--library/Icinga/Application/Modules/DashboardContainer.php58
-rw-r--r--library/Icinga/Application/Modules/Manager.php698
-rw-r--r--library/Icinga/Application/Modules/MenuItemContainer.php55
-rw-r--r--library/Icinga/Application/Modules/Module.php1451
-rw-r--r--library/Icinga/Application/Modules/NavigationItemContainer.php117
5 files changed, 2379 insertions, 0 deletions
diff --git a/library/Icinga/Application/Modules/DashboardContainer.php b/library/Icinga/Application/Modules/DashboardContainer.php
new file mode 100644
index 0000000..f3c8bc6
--- /dev/null
+++ b/library/Icinga/Application/Modules/DashboardContainer.php
@@ -0,0 +1,58 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+/**
+ * Container for module dashboards
+ */
+class DashboardContainer extends NavigationItemContainer
+{
+ /**
+ * This dashboard's dashlets
+ *
+ * @var array
+ */
+ protected $dashlets;
+
+ /**
+ * Set this dashboard's dashlets
+ *
+ * @param array $dashlets
+ *
+ * @return $this
+ */
+ public function setDashlets(array $dashlets)
+ {
+ $this->dashlets = $dashlets;
+ return $this;
+ }
+
+ /**
+ * Return this dashboard's dashlets
+ *
+ * @return array
+ */
+ public function getDashlets()
+ {
+ return $this->dashlets ?: array();
+ }
+
+ /**
+ * Add a new dashlet
+ *
+ * @param string $name
+ * @param string $url
+ * @param int $priority
+ *
+ * @return $this
+ */
+ public function add($name, $url, $priority = null)
+ {
+ $this->dashlets[$name] = [
+ 'url' => $url,
+ 'priority' => $priority
+ ];
+ return $this;
+ }
+}
diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php
new file mode 100644
index 0000000..55d074d
--- /dev/null
+++ b/library/Icinga/Application/Modules/Manager.php
@@ -0,0 +1,698 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\SimpleQuery;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\SystemPermissionException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\NotReadableError;
+
+/**
+ * Module manager that handles detecting, enabling and disabling of modules
+ *
+ * Modules can have 3 states:
+ * * installed, module exists but is disabled
+ * * enabled, module enabled and should be loaded
+ * * loaded, module enabled and loaded via the autoloader
+ *
+ */
+class Manager
+{
+ /**
+ * Namespace for module permissions
+ *
+ * @var string
+ */
+ const MODULE_PERMISSION_NS = 'module/';
+
+ /**
+ * Array of all installed module's base directories
+ *
+ * @var array
+ */
+ private $installedBaseDirs = array();
+
+ /**
+ * Array of all enabled modules base dirs
+ *
+ * @var array
+ */
+ private $enabledDirs = array();
+
+ /**
+ * Array of all module names that have been loaded
+ *
+ * @var array
+ */
+ private $loadedModules = array();
+
+ /**
+ * Reference to Icinga::app
+ *
+ * @var Icinga
+ */
+ private $app;
+
+ /**
+ * The directory that is used to detect enabled modules
+ *
+ * @var string
+ */
+ private $enableDir;
+
+ /**
+ * All paths to look for installed modules that can be enabled
+ *
+ * @var array
+ */
+ private $modulePaths = array();
+
+ /**
+ * Whether we loaded all enabled modules
+ *
+ * @var bool
+ */
+ private $loadedAllEnabledModules = false;
+
+ /**
+ * Create a new instance of the module manager
+ *
+ * @param ApplicationBootstrap $app
+ * @param string $enabledDir Enabled modules location. The application maintains symlinks within
+ * the given path
+ * @param array $availableDirs Installed modules location
+ **/
+ public function __construct($app, $enabledDir, array $availableDirs)
+ {
+ $this->app = $app;
+ $this->modulePaths = $availableDirs;
+ $this->enableDir = $enabledDir;
+ }
+
+ /**
+ * Query interface for the module manager
+ *
+ * @return SimpleQuery
+ */
+ public function select()
+ {
+ $source = new ArrayDatasource($this->getModuleInfo());
+ return $source->select();
+ }
+
+ /**
+ * Check for enabled modules
+ *
+ * Update the internal $enabledDirs property with the enabled modules.
+ *
+ * @throws ConfigurationError If module dir does not exist, is not a directory or not readable
+ */
+ private function detectEnabledModules()
+ {
+ if (! file_exists($parent = dirname($this->enableDir))) {
+ return;
+ }
+ if (! is_readable($parent)) {
+ throw new NotReadableError(
+ 'Cannot read enabled modules. Config directory "%s" is not readable',
+ $parent
+ );
+ }
+
+ if (! file_exists($this->enableDir)) {
+ return;
+ }
+ if (! is_dir($this->enableDir)) {
+ throw new NotReadableError(
+ 'Cannot read enabled modules. Module directory "%s" is not a directory',
+ $this->enableDir
+ );
+ }
+ if (! is_readable($this->enableDir)) {
+ throw new NotReadableError(
+ 'Cannot read enabled modules. Module directory "%s" is not readable',
+ $this->enableDir
+ );
+ }
+ if (($dh = opendir($this->enableDir)) !== false) {
+ $isPhar = substr($this->enableDir, 0, 8) === 'phar:///';
+ $this->enabledDirs = array();
+ while (($file = readdir($dh)) !== false) {
+ if ($file[0] === '.' || $file === 'README') {
+ continue;
+ }
+
+ $link = $this->enableDir . DIRECTORY_SEPARATOR . $file;
+ if (! $isPhar && ! is_link($link)) {
+ Logger::warning(
+ 'Found invalid module in enabledModule directory "%s": "%s" is not a symlink',
+ $this->enableDir,
+ $link
+ );
+ continue;
+ }
+
+ $dir = $isPhar ? $link : realpath($link);
+ if ($dir !== false && is_dir($dir)) {
+ $this->enabledDirs[$file] = $dir;
+ } else {
+ $this->enabledDirs[$file] = null;
+
+ Logger::warning(
+ 'Found invalid module in enabledModule directory "%s": "%s" points to non existing path "%s"',
+ $this->enableDir,
+ $link,
+ $dir
+ );
+ }
+
+ ksort($this->enabledDirs);
+ }
+ closedir($dh);
+ }
+ }
+
+ /**
+ * Try to set all enabled modules in loaded sate
+ *
+ * @return $this
+ * @see Manager::loadModule()
+ */
+ public function loadEnabledModules()
+ {
+ if (! $this->loadedAllEnabledModules) {
+ foreach ($this->listEnabledModules() as $name) {
+ $this->loadModule($name);
+ }
+
+ $this->loadedAllEnabledModules = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Whether we loaded all enabled modules
+ *
+ * @return bool
+ */
+ public function loadedAllEnabledModules()
+ {
+ return $this->loadedAllEnabledModules;
+ }
+
+
+ /**
+ * Try to load the module and register it in the application
+ *
+ * @param string $name The name of the module to load
+ * @param mixed $basedir Optional module base directory
+ *
+ * @return $this
+ */
+ public function loadModule($name, $basedir = null)
+ {
+ if ($this->hasLoaded($name)) {
+ return $this;
+ }
+
+ $module = null;
+ if ($basedir === null) {
+ $module = new Module($this->app, $name, $this->getModuleDir($name));
+ } else {
+ $module = new Module($this->app, $name, $basedir);
+ }
+
+ if ($name !== 'ipl' && $name !== 'reactbundle') {
+ $module->register();
+ }
+
+ $this->loadedModules[$name] = $module;
+ return $this;
+ }
+
+ /**
+ * Set the given module to the enabled state
+ *
+ * @param string $name The module to enable
+ * @param bool $force Whether to ignore unmet dependencies
+ *
+ * @return $this
+ * @throws ConfigurationError When trying to enable a module that is not installed
+ * @throws SystemPermissionException When insufficient permissions for the application exist
+ */
+ public function enableModule($name, $force = false)
+ {
+ if (! $this->hasInstalled($name)) {
+ throw new ConfigurationError(
+ 'Cannot enable module "%s". Module is not installed.',
+ $name
+ );
+ }
+
+ if (strtolower(substr($name, 0, 18)) === 'icingaweb2-module-') {
+ throw new ConfigurationError(
+ 'Cannot enable module "%s": Directory name does not match the module\'s name.'
+ . ' Please rename the module to "%s" before enabling.',
+ $name,
+ substr($name, 18)
+ );
+ }
+
+ if ($this->hasUnmetDependencies($name)) {
+ if ($force) {
+ Logger::warning(t('Enabling module "%s" although it has unmet dependencies'), $name);
+ } else {
+ throw new ConfigurationError(
+ t('Module "%s" can\'t be enabled. Module has unmet dependencies'),
+ $name
+ );
+ }
+ }
+
+ clearstatcache(true);
+ $target = $this->installedBaseDirs[$name];
+ $link = $this->enableDir . DIRECTORY_SEPARATOR . $name;
+
+ if (! is_dir($this->enableDir)) {
+ if (!@mkdir($this->enableDir, 0777, true)) {
+ $error = error_get_last();
+ throw new SystemPermissionException(
+ 'Failed to create enabledModules directory "%s" (%s)',
+ $this->enableDir,
+ $error['message']
+ );
+ }
+
+ chmod($this->enableDir, 02770);
+ } elseif (! is_writable($this->enableDir)) {
+ throw new SystemPermissionException(
+ 'Cannot enable module "%s". Check the permissions for the enabledModules directory: %s',
+ $name,
+ $this->enableDir
+ );
+ }
+
+ $this->loadedAllEnabledModules = false;
+
+ if (file_exists($link) && is_link($link)) {
+ return $this;
+ }
+
+ if (! @symlink($target, $link)) {
+ $error = error_get_last();
+ if (strstr($error["message"], "File exists") === false) {
+ throw new SystemPermissionException(
+ 'Cannot enable module "%s" at %s due to file system errors. '
+ . 'Please check path and mounting points because this is not a permission error. '
+ . 'Primary error was: %s',
+ $name,
+ $this->enableDir,
+ $error['message']
+ );
+ }
+ }
+
+ $this->enabledDirs[$name] = $link;
+ $this->loadModule($name);
+ return $this;
+ }
+
+ /**
+ * Disable the given module and remove its enabled state
+ *
+ * @param string $name The name of the module to disable
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError When the module is not installed or it's not a symlink
+ * @throws SystemPermissionException When insufficient permissions for the application exist
+ */
+ public function disableModule($name)
+ {
+ if (! $this->hasEnabled($name)) {
+ throw new ConfigurationError(
+ 'Cannot disable module "%s". Module is not installed.',
+ $name
+ );
+ }
+
+ if (! is_writable($this->enableDir)) {
+ throw new SystemPermissionException(
+ 'Cannot disable module "%s". Check the permissions for the enabledModules directory: %s',
+ $name,
+ $this->enableDir
+ );
+ }
+
+ $link = $this->enableDir . DIRECTORY_SEPARATOR . $name;
+ if (! is_link($link)) {
+ throw new ConfigurationError(
+ 'Cannot disable module %s at %s. '
+ . 'It looks like you have installed this module manually and moved it to your module folder. '
+ . 'In order to dynamically enable and disable modules, you have to create a symlink to '
+ . 'the enabledModules folder.',
+ $name,
+ $this->enableDir
+ );
+ }
+
+ if (is_link($link)) {
+ if (! @unlink($link)) {
+ $error = error_get_last();
+ throw new SystemPermissionException(
+ 'Cannot enable module "%s" at %s due to file system errors. '
+ . 'Please check path and mounting points because this is not a permission error. '
+ . 'Primary error was: %s',
+ $name,
+ $this->enableDir,
+ $error['message']
+ );
+ }
+ }
+
+ unset($this->enabledDirs[$name]);
+ return $this;
+ }
+
+ /**
+ * Return the directory of the given module as a string, optionally with a given sub directoy
+ *
+ * @param string $name The module name to return the module directory of
+ * @param string $subdir The sub directory to append to the path
+ *
+ * @return string
+ *
+ * @throws ProgrammingError When the module is not installed or existing
+ */
+ public function getModuleDir($name, $subdir = '')
+ {
+ if ($this->hasLoaded($name)) {
+ return $this->getModule($name)->getBaseDir() . $subdir;
+ }
+
+ if ($this->hasEnabled($name)) {
+ return $this->enabledDirs[$name]. $subdir;
+ }
+
+ if ($this->hasInstalled($name)) {
+ return $this->installedBaseDirs[$name] . $subdir;
+ }
+
+ throw new ProgrammingError(
+ 'Trying to access uninstalled module dir: %s',
+ $name
+ );
+ }
+
+ /**
+ * Return true when the module with the given name is installed, otherwise false
+ *
+ * @param string $name The module to check for being installed
+ *
+ * @return bool
+ */
+ public function hasInstalled($name)
+ {
+ if (!count($this->installedBaseDirs)) {
+ $this->detectInstalledModules();
+ }
+ return array_key_exists($name, $this->installedBaseDirs);
+ }
+
+ /**
+ * Return true when the given module is in enabled state, otherwise false
+ *
+ * @param string $name The module to check for being enabled
+ *
+ * @return bool
+ */
+ public function hasEnabled($name)
+ {
+ return array_key_exists($name, $this->enabledDirs);
+ }
+
+ /**
+ * Return true when the module is in loaded state, otherwise false
+ *
+ * @param string $name The module to check for being loaded
+ *
+ * @return bool
+ */
+ public function hasLoaded($name)
+ {
+ return array_key_exists($name, $this->loadedModules);
+ }
+
+ /**
+ * Check if a module with the given name is enabled
+ *
+ * Passing a version constraint also verifies that the module's version matches.
+ *
+ * @param string $name
+ * @param string $version
+ *
+ * @return bool
+ */
+ public function has($name, $version = null)
+ {
+ if (! $this->hasEnabled($name)) {
+ return false;
+ } elseif ($version === null || $version === true) {
+ return true;
+ }
+
+ $operator = '=';
+ if (preg_match('/^([<>=]{1,2})\s*v?((?:[\d.]+)(?:.+)?)$/', $version, $match)) {
+ $operator = $match[1];
+ $version = $match[2];
+ }
+
+ $modVersion = ltrim($this->getModule($name)->getVersion(), 'v');
+ return version_compare($modVersion, $version, $operator);
+ }
+
+ /**
+ * Get the currently loaded modules
+ *
+ * @return Module[]
+ */
+ public function getLoadedModules()
+ {
+ return $this->loadedModules;
+ }
+
+ /**
+ * Get a module
+ *
+ * @param string $name Name of the module
+ * @param bool $assertLoaded Whether or not to throw an exception if the module hasn't been loaded
+ *
+ * @return Module
+ * @throws ProgrammingError If the module hasn't been loaded
+ */
+ public function getModule($name, $assertLoaded = true)
+ {
+ if ($this->hasLoaded($name)) {
+ return $this->loadedModules[$name];
+ } elseif (! (bool) $assertLoaded) {
+ return new Module($this->app, $name, $this->getModuleDir($name));
+ }
+ throw new ProgrammingError(
+ 'Can\'t access module %s because it hasn\'t been loaded',
+ $name
+ );
+ }
+
+ /**
+ * Return an array containing information objects for each available module
+ *
+ * Each entry has the following fields
+ * * name, name of the module as a string
+ * * path, path where the module is located as a string
+ * * installed, whether the module is installed or not as a boolean
+ * * enabled, whether the module is enabled or not as a boolean
+ * * loaded, whether the module is loaded or not as a boolean
+ *
+ * @return array
+ */
+ public function getModuleInfo()
+ {
+ $info = array();
+
+ $installed = $this->listInstalledModules();
+ foreach ($installed as $name) {
+ $info[$name] = (object) array(
+ 'name' => $name,
+ 'path' => $this->installedBaseDirs[$name],
+ 'installed' => true,
+ 'enabled' => $this->hasEnabled($name),
+ 'loaded' => $this->hasLoaded($name)
+ );
+ }
+
+ $enabled = $this->listEnabledModules();
+ foreach ($enabled as $name) {
+ $info[$name] = (object) array(
+ 'name' => $name,
+ 'path' => $this->enabledDirs[$name],
+ 'installed' => $this->enabledDirs[$name] !== null,
+ 'enabled' => true,
+ 'loaded' => $this->hasLoaded($name)
+ );
+ }
+
+ return $info;
+ }
+
+ /**
+ * Check if the given module has unmet dependencies
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function hasUnmetDependencies($name)
+ {
+ $module = $this->getModule($name, false);
+
+ $requiredMods = $module->getRequiredModules();
+
+ if (isset($requiredMods['monitoring'], $requiredMods['icingadb'])) {
+ if (! $this->has('monitoring', $requiredMods['monitoring'])
+ && ! $this->has('icingadb', $requiredMods['icingadb'])
+ ) {
+ return true;
+ }
+
+ unset($requiredMods['monitoring'], $requiredMods['icingadb']);
+ }
+
+ foreach ($requiredMods as $moduleName => $moduleVersion) {
+ if (! $this->has($moduleName, $moduleVersion)) {
+ return true;
+ }
+ }
+
+ $libraries = Icinga::app()->getLibraries();
+
+ $requiredLibs = $module->getRequiredLibraries();
+ foreach ($requiredLibs as $libraryName => $libraryVersion) {
+ if (! $libraries->has($libraryName, $libraryVersion)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return an array containing all enabled module names as strings
+ *
+ * @return array
+ */
+ public function listEnabledModules()
+ {
+ if (count($this->enabledDirs) === 0) {
+ $this->detectEnabledModules();
+ }
+
+ return array_keys($this->enabledDirs);
+ }
+
+ /**
+ * Return an array containing all loaded module names as strings
+ *
+ * @return array
+ */
+ public function listLoadedModules()
+ {
+ return array_keys($this->loadedModules);
+ }
+
+ /**
+ * Return an array of module names from installed modules
+ *
+ * Calls detectInstalledModules() if no module discovery has been performed yet
+ *
+ * @return array
+ *
+ * @see detectInstalledModules()
+ */
+ public function listInstalledModules()
+ {
+ if (!count($this->installedBaseDirs)) {
+ $this->detectInstalledModules();
+ }
+
+ if (count($this->installedBaseDirs)) {
+ return array_keys($this->installedBaseDirs);
+ }
+
+ return array();
+ }
+
+ /**
+ * Detect installed modules from every path provided in modulePaths
+ *
+ * @param array $availableDirs Installed modules location
+ *
+ * @return $this
+ */
+ public function detectInstalledModules(array $availableDirs = null)
+ {
+ $modulePaths = $availableDirs !== null ? $availableDirs : $this->modulePaths;
+ foreach ($modulePaths as $basedir) {
+ $canonical = realpath($basedir);
+ if ($canonical === false) {
+ Logger::warning('Module path "%s" does not exist', $basedir);
+ continue;
+ }
+ if (!is_dir($canonical)) {
+ Logger::error('Module path "%s" is not a directory', $canonical);
+ continue;
+ }
+ if (!is_readable($canonical)) {
+ Logger::error('Module path "%s" is not readable', $canonical);
+ continue;
+ }
+ if (($dh = opendir($canonical)) !== false) {
+ while (($file = readdir($dh)) !== false) {
+ if ($file[0] === '.') {
+ continue;
+ }
+ if (is_dir($canonical . '/' . $file)) {
+ if (! array_key_exists($file, $this->installedBaseDirs)) {
+ $this->installedBaseDirs[$file] = $canonical . '/' . $file;
+ } else {
+ Logger::debug(
+ 'Module "%s" already exists in installation path "%s" and is ignored.',
+ $canonical . '/' . $file,
+ $this->installedBaseDirs[$file]
+ );
+ }
+ }
+ }
+ closedir($dh);
+ }
+ }
+ ksort($this->installedBaseDirs);
+ return $this;
+ }
+
+ /**
+ * Get the directories where to look for installed modules
+ *
+ * @return array
+ */
+ public function getModuleDirs()
+ {
+ return $this->modulePaths;
+ }
+}
diff --git a/library/Icinga/Application/Modules/MenuItemContainer.php b/library/Icinga/Application/Modules/MenuItemContainer.php
new file mode 100644
index 0000000..88599e6
--- /dev/null
+++ b/library/Icinga/Application/Modules/MenuItemContainer.php
@@ -0,0 +1,55 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+/**
+ * Container for module menu items
+ */
+class MenuItemContainer extends NavigationItemContainer
+{
+ /**
+ * This menu item's children
+ *
+ * @var MenuItemContainer[]
+ */
+ protected $children;
+
+ /**
+ * Set this menu item's children
+ *
+ * @param MenuItemContainer[] $children
+ *
+ * @return $this
+ */
+ public function setChildren(array $children)
+ {
+ $this->children = $children;
+ return $this;
+ }
+
+ /**
+ * Return this menu item's children
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ return $this->children ?: array();
+ }
+
+ /**
+ * Add a new sub menu
+ *
+ * @param string $name
+ * @param array $properties
+ *
+ * @return MenuItemContainer The newly added sub menu
+ */
+ public function add($name, array $properties = array())
+ {
+ $child = new MenuItemContainer($name, $properties);
+ $this->children[] = $child;
+ return $child;
+ }
+}
diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php
new file mode 100644
index 0000000..6a5afb8
--- /dev/null
+++ b/library/Icinga/Application/Modules/Module.php
@@ -0,0 +1,1451 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+use Exception;
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Application\Config;
+use Icinga\Application\Hook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Setup\SetupWizard;
+use Icinga\Util\File;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Widget;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\StaticTranslator;
+use ipl\I18n\Translation;
+use Zend_Controller_Router_Route;
+use Zend_Controller_Router_Route_Abstract;
+use Zend_Controller_Router_Route_Regex;
+
+/**
+ * Module handling
+ *
+ * Register modules and initialize it
+ */
+class Module
+{
+ use Translation {
+ translate as protected;
+ translatePlural as protected;
+ }
+
+ /**
+ * Module name
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * Base directory of module
+ *
+ * @var string
+ */
+ private $basedir;
+
+ /**
+ * Directory for styles
+ *
+ * @var string
+ */
+ private $cssdir;
+
+ /**
+ * Directory for Javascript
+ *
+ * @var string
+ */
+ private $jsdir;
+
+ /**
+ * Base application directory
+ *
+ * @var string
+ */
+ private $appdir;
+
+ /**
+ * Library directory
+ *
+ * @var string
+ */
+ private $libdir;
+
+ /**
+ * Config directory
+ *
+ * @var string
+ */
+ private $configdir;
+
+ /**
+ * Directory containing translations
+ *
+ * @var string
+ */
+ private $localedir;
+
+ /**
+ * Directory where controllers reside
+ *
+ * @var string
+ */
+ private $controllerdir;
+
+ /**
+ * Directory containing form implementations
+ *
+ * @var string
+ */
+ private $formdir;
+
+ /**
+ * Module bootstrapping script
+ *
+ * @var string
+ */
+ private $runScript;
+
+ /**
+ * Module configuration script
+ *
+ * @var string
+ */
+ private $configScript;
+
+ /**
+ * Module metadata filename
+ *
+ * @var string
+ */
+ private $metadataFile;
+
+ /**
+ * Module metadata (version...)
+ *
+ * @var object
+ */
+ private $metadata;
+
+ /**
+ * Whether we already tried to include the module configuration script
+ *
+ * @var bool
+ */
+ private $triedToLaunchConfigScript = false;
+
+ /**
+ * Whether the module's namespaces have been registered on our autoloader
+ *
+ * @var bool
+ */
+ protected $registeredAutoloader = false;
+
+ /**
+ * Whether this module has been registered
+ *
+ * @var bool
+ */
+ private $registered = false;
+
+ /**
+ * Provided permissions
+ *
+ * @var array
+ */
+ private $permissionList = array();
+
+ /**
+ * Provided restrictions
+ *
+ * @var array
+ */
+ private $restrictionList = array();
+
+ /**
+ * Provided config tabs
+ *
+ * @var array
+ */
+ private $configTabs = array();
+
+ /**
+ * Provided setup wizard
+ *
+ * @var string
+ */
+ private $setupWizard;
+
+ /**
+ * Icinga application
+ *
+ * @var \Icinga\Application\Web
+ */
+ private $app;
+
+ /**
+ * The CSS/LESS files this module provides
+ *
+ * @var array
+ */
+ protected $cssFiles = array();
+
+ /**
+ * The Javascript files this module provides
+ *
+ * @var array
+ */
+ protected $jsFiles = array();
+
+ /**
+ * Routes to add to the route chain
+ *
+ * @var array Array of name-route pairs
+ *
+ * @see addRoute()
+ */
+ protected $routes = array();
+
+ /**
+ * A set of menu elements
+ *
+ * @var MenuItemContainer[]
+ */
+ protected $menuItems = array();
+
+ /**
+ * A set of Pane elements
+ *
+ * @var array
+ */
+ protected $paneItems = array();
+
+ /**
+ * A set of objects representing a searchUrl configuration
+ *
+ * @var array
+ */
+ protected $searchUrls = array();
+
+ /**
+ * This module's user backends providing several authentication mechanisms
+ *
+ * @var array
+ */
+ protected $userBackends = array();
+
+ /**
+ * This module's user group backends
+ *
+ * @var array
+ */
+ protected $userGroupBackends = array();
+
+ /**
+ * This module's configurable navigation items
+ *
+ * @var array
+ */
+ protected $navigationItems = array();
+
+ /**
+ * Create a new module object
+ *
+ * @param ApplicationBootstrap $app
+ * @param string $name
+ * @param string $basedir
+ */
+ public function __construct(ApplicationBootstrap $app, $name, $basedir)
+ {
+ $this->app = $app;
+ $this->name = $name;
+ $this->basedir = $basedir;
+ $this->cssdir = $basedir . '/public/css';
+ $this->jsdir = $basedir . '/public/js';
+ $this->libdir = $basedir . '/library';
+ $this->configdir = $app->getConfigDir('modules/' . $name);
+ $this->appdir = $basedir . '/application';
+ $this->localedir = $basedir . '/application/locale';
+ $this->formdir = $basedir . '/application/forms';
+ $this->controllerdir = $basedir . '/application/controllers';
+ $this->runScript = $basedir . '/run.php';
+ $this->configScript = $basedir . '/configuration.php';
+ $this->metadataFile = $basedir . '/module.info';
+
+ $this->translationDomain = $name;
+ }
+
+ /**
+ * Provide a search URL
+ *
+ * @param string $title
+ * @param string $url
+ * @param int $priority
+ *
+ * @return $this
+ */
+ public function provideSearchUrl($title, $url, $priority = 0)
+ {
+ $this->searchUrls[] = (object) array(
+ 'title' => (string) $title,
+ 'url' => (string) $url,
+ 'priority' => (int) $priority
+ );
+
+ return $this;
+ }
+
+ /**
+ * Get this module's search urls
+ *
+ * @return array
+ */
+ public function getSearchUrls()
+ {
+ $this->launchConfigScript();
+ return $this->searchUrls;
+ }
+
+ /**
+ * Return this module's dashboard
+ *
+ * @return Navigation
+ */
+ public function getDashboard()
+ {
+ $this->launchConfigScript();
+ return $this->createDashboard($this->paneItems);
+ }
+
+ /**
+ * Create and return a new navigation for the given dashboard panes
+ *
+ * @param DashboardContainer[] $panes
+ *
+ * @return Navigation
+ */
+ public function createDashboard(array $panes)
+ {
+ $navigation = new Navigation();
+ foreach ($panes as $pane) {
+ /** @var DashboardContainer $pane */
+ $dashlets = [];
+ foreach ($pane->getDashlets() as $dashletName => $dashletConfig) {
+ $dashlets[$dashletName] = [
+ 'label' => $this->translate($dashletName),
+ 'url' => $dashletConfig['url'],
+ 'priority' => $dashletConfig['priority']
+ ];
+ }
+
+ $navigation->addItem(
+ $pane->getName(),
+ array_merge(
+ $pane->getProperties(),
+ array(
+ 'label' => $this->translate($pane->getName()),
+ 'type' => 'dashboard-pane',
+ 'children' => $dashlets
+ )
+ )
+ );
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * Add or get a dashboard pane
+ *
+ * @param string $name
+ * @param array $properties
+ *
+ * @return DashboardContainer
+ */
+ protected function dashboard($name, array $properties = array())
+ {
+ if (array_key_exists($name, $this->paneItems)) {
+ $this->paneItems[$name]->setProperties($properties);
+ } else {
+ $this->paneItems[$name] = new DashboardContainer($name, $properties);
+ }
+
+ return $this->paneItems[$name];
+ }
+
+ /**
+ * Return this module's menu
+ *
+ * @return Navigation
+ */
+ public function getMenu()
+ {
+ $this->launchConfigScript();
+ return Navigation::fromArray($this->createMenu($this->menuItems));
+ }
+
+ /**
+ * Create and return an array structure for the given menu items
+ *
+ * @param MenuItemContainer[] $items
+ *
+ * @return array
+ */
+ private function createMenu(array $items)
+ {
+ $navigation = array();
+ foreach ($items as $item) {
+ /** @var MenuItemContainer $item */
+ $properties = $item->getProperties();
+ $properties['children'] = $this->createMenu($item->getChildren());
+ if (! isset($properties['label'])) {
+ $properties['label'] = $this->translate($item->getName());
+ }
+
+ $navigation[$item->getName()] = $properties;
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * Add or get a menu section
+ *
+ * @param string $name
+ * @param array $properties
+ *
+ * @return MenuItemContainer
+ */
+ protected function menuSection($name, array $properties = array())
+ {
+ if (array_key_exists($name, $this->menuItems)) {
+ $this->menuItems[$name]->setProperties($properties);
+ } else {
+ $this->menuItems[$name] = new MenuItemContainer($name, $properties);
+ }
+
+ return $this->menuItems[$name];
+ }
+
+ /**
+ * Register module
+ *
+ * @return bool
+ */
+ public function register()
+ {
+ if ($this->registered) {
+ return true;
+ }
+
+ $this->registerAutoloader();
+ try {
+ $this->launchRunScript();
+ } catch (Exception $e) {
+ Logger::warning(
+ 'Launching the run script %s for module %s failed with the following exception: %s',
+ $this->runScript,
+ $this->name,
+ $e->getMessage()
+ );
+ return false;
+ }
+ $this->registerWebIntegration();
+ $this->registered = true;
+
+ return true;
+ }
+
+ /**
+ * Get whether this module has been registered
+ *
+ * @return bool
+ */
+ public function isRegistered()
+ {
+ return $this->registered;
+ }
+
+ /**
+ * Test for an enabled module by name
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public static function exists($name)
+ {
+ return Icinga::app()->getModuleManager()->hasEnabled($name);
+ }
+
+ /**
+ * Get a module by name
+ *
+ * @param string $name
+ * @param bool $autoload
+ *
+ * @return self
+ *
+ * @throws ProgrammingError When the module is not yet loaded
+ */
+ public static function get($name, $autoload = false)
+ {
+ $manager = Icinga::app()->getModuleManager();
+ if (!$manager->hasLoaded($name)) {
+ if ($autoload === true && $manager->hasEnabled($name)) {
+ $manager->loadModule($name);
+ }
+ }
+ // Throws ProgrammingError when the module is not yet loaded
+ return $manager->getModule($name);
+ }
+
+ /**
+ * Provide an additional CSS/LESS file
+ *
+ * @param string $path The path to the file, relative to self::$cssdir
+ *
+ * @return $this
+ */
+ protected function provideCssFile($path)
+ {
+ $this->cssFiles[] = $this->cssdir . DIRECTORY_SEPARATOR . $path;
+ return $this;
+ }
+
+ /**
+ * Test if module provides css
+ *
+ * @return bool
+ */
+ public function hasCss()
+ {
+ if (file_exists($this->getCssFilename())) {
+ return true;
+ }
+
+ $this->launchConfigScript();
+ return !empty($this->cssFiles);
+ }
+
+ /**
+ * Returns the complete less file name
+ *
+ * @return string
+ */
+ public function getCssFilename()
+ {
+ return $this->cssdir . '/module.less';
+ }
+
+ /**
+ * Return the CSS/LESS files this module provides
+ *
+ * @return array
+ */
+ public function getCssFiles()
+ {
+ $this->launchConfigScript();
+ $files = $this->cssFiles;
+ if (file_exists($this->getCssFilename())) {
+ $files[] = $this->getCssFilename();
+ }
+ return $files;
+ }
+
+ /**
+ * Provide an additional Javascript file
+ *
+ * @param string $path The path to the file, relative to self::$jsdir
+ *
+ * @return $this
+ */
+ protected function provideJsFile($path)
+ {
+ $this->jsFiles[] = $this->jsdir . DIRECTORY_SEPARATOR . $path;
+ return $this;
+ }
+
+ /**
+ * Test if module provides js
+ *
+ * @return bool
+ */
+ public function hasJs()
+ {
+ if (file_exists($this->getJsFilename())) {
+ return true;
+ }
+
+ $this->launchConfigScript();
+ return !empty($this->jsFiles);
+ }
+
+ /**
+ * Returns the complete js file name
+ *
+ * @return string
+ */
+ public function getJsFilename()
+ {
+ return $this->jsdir . '/module.js';
+ }
+
+ /**
+ * Return the Javascript files this module provides
+ *
+ * @return array
+ */
+ public function getJsFiles()
+ {
+ $this->launchConfigScript();
+ $files = $this->jsFiles;
+ $files[] = $this->getJsFilename();
+ return $files;
+ }
+
+ /**
+ * Get the module name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get the module namespace
+ *
+ * @return string
+ */
+ public function getNamespace()
+ {
+ return 'Icinga\\Module\\' . ucfirst($this->getName());
+ }
+
+ /**
+ * Get the module version
+ *
+ * @return string
+ */
+ public function getVersion()
+ {
+ return $this->metadata()->version;
+ }
+
+ /**
+ * Get the module description
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->metadata()->description;
+ }
+
+ /**
+ * Get the module title (short description)
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->metadata()->title;
+ }
+
+ /**
+ * Get the module dependencies
+ *
+ * @return array
+ * @deprecated Use method getRequiredModules() instead
+ */
+ public function getDependencies()
+ {
+ return $this->metadata()->depends;
+ }
+
+ /**
+ * Get required libraries
+ *
+ * @return array
+ */
+ public function getRequiredLibraries()
+ {
+ $requiredLibraries = $this->metadata()->libraries;
+
+ // Register module requirements for ipl and reactbundle as library requirements
+ $requiredModules = $this->metadata()->modules ?: $this->metadata()->depends;
+ if (isset($requiredModules['ipl']) && ! isset($requiredLibraries['icinga-php-library'])) {
+ $requiredLibraries['icinga-php-library'] = $requiredModules['ipl'];
+ }
+
+ if (isset($requiredModules['reactbundle']) && ! isset($requiredLibraries['icinga-php-thirdparty'])) {
+ $requiredLibraries['icinga-php-thirdparty'] = $requiredModules['reactbundle'];
+ }
+
+ return $requiredLibraries;
+ }
+
+ /**
+ * Get required modules
+ *
+ * @return array
+ */
+ public function getRequiredModules()
+ {
+ $requiredModules = $this->metadata()->modules ?: $this->metadata()->depends;
+
+ $hasIcingadb = isset($requiredModules['icingadb']);
+ if (isset($requiredModules['monitoring']) && ($this->isSupportingIcingadb() || $hasIcingadb)) {
+ $requiredMods = [];
+ $icingadbVersion = true;
+ if ($hasIcingadb) {
+ $icingadbVersion = isset($requiredModules['icingadb']) ? $requiredModules['icingadb'] : true;
+ unset($requiredModules['icingadb']);
+ }
+
+ foreach ($requiredModules as $name => $version) {
+ $requiredMods[$name] = $version;
+ if ($name === 'monitoring') {
+ $requiredMods['icingadb'] = $icingadbVersion;
+ }
+ }
+
+ $requiredModules = $requiredMods;
+ }
+
+ // Both modules are deprecated and their successors are now dependencies of web itself
+ unset($requiredModules['ipl'], $requiredModules['reactbundle']);
+
+ return $requiredModules;
+ }
+
+ /**
+ * Check whether module supports icingadb
+ *
+ * @return bool
+ */
+ protected function isSupportingIcingadb()
+ {
+ $icingadbSupportingModules = [
+ 'cube' => '1.2.0',
+ 'jira' => '1.2.0',
+ 'graphite' => '1.2.0',
+ 'director' => '1.9.0',
+ 'toplevelview' => '0.4.0',
+ 'businessprocess' => '2.4.0'
+ ];
+
+ return array_key_exists($this->getName(), $icingadbSupportingModules)
+ && version_compare($this->getVersion(), $icingadbSupportingModules[$this->getName()], '>=');
+ }
+
+ /**
+ * Fetch module metadata
+ *
+ * @return object
+ */
+ protected function metadata()
+ {
+ if ($this->metadata === null) {
+ $metadata = (object) [
+ 'name' => $this->getName(),
+ 'version' => '0.0.0',
+ 'title' => null,
+ 'description' => '',
+ 'depends' => [],
+ 'libraries' => [],
+ 'modules' => []
+ ];
+
+ if (file_exists($this->metadataFile)) {
+ $key = null;
+ $simpleRequires = false;
+ $file = new File($this->metadataFile, 'r');
+ foreach ($file as $lineno => $line) {
+ $line = rtrim($line);
+
+ if ($key === 'description') {
+ if (empty($line)) {
+ $metadata->description .= "\n";
+ continue;
+ } elseif ($line[0] === ' ') {
+ $metadata->description .= $line;
+ continue;
+ }
+ } elseif (empty($line)) {
+ continue;
+ }
+
+ if (strpos($line, ':') === false) {
+ Logger::debug(
+ "Can't process line %d in %s: Line does not specify a key:value pair"
+ . " nor is it part of the description (indented with a single space)",
+ $lineno,
+ $this->metadataFile
+ );
+
+ break;
+ }
+
+ $parts = preg_split('/:\s+/', $line, 2);
+ if (count($parts) === 1) {
+ $parts[] = '';
+ }
+
+ list($key, $val) = $parts;
+
+ $key = strtolower($key);
+ switch ($key) {
+ case 'requires':
+ if ($val) {
+ $simpleRequires = true;
+ $key = 'libraries';
+ } else {
+ break;
+ }
+
+ // Shares the syntax with `Depends`
+ case ' libraries':
+ case ' modules':
+ if ($simpleRequires && $key[0] === ' ') {
+ Logger::debug(
+ 'Can\'t process line %d in %s: Requirements already registered by a previous line',
+ $lineno,
+ $this->metadataFile
+ );
+ break;
+ }
+
+ $key = ltrim($key);
+ // Shares the syntax with `Depends`
+ case 'depends':
+ if (strpos($val, ' ') === false) {
+ $metadata->{$key}[$val] = true;
+ continue 2;
+ }
+
+ $parts = preg_split('/,\s+/', $val);
+ foreach ($parts as $part) {
+ if (preg_match('/^([\w\-\/]+)\s+\((.+)\)$/', $part, $m)) {
+ $metadata->{$key}[$m[1]] = $m[2];
+ } else {
+ $metadata->{$key}[$part] = true;
+ }
+ }
+
+ break;
+ case 'description':
+ if ($metadata->title === null) {
+ $metadata->title = $val;
+ } else {
+ $metadata->description = $val;
+ }
+ break;
+
+ default:
+ $metadata->{$key} = $val;
+ }
+ }
+ }
+
+ if ($metadata->title === null) {
+ $metadata->title = $this->getName();
+ }
+
+ if ($metadata->description === '') {
+ $metadata->description = t(
+ 'This module has no description'
+ );
+ }
+
+ $this->metadata = $metadata;
+ }
+ return $this->metadata;
+ }
+
+ /**
+ * Get the module's CSS directory
+ *
+ * @return string
+ */
+ public function getCssDir()
+ {
+ return $this->cssdir;
+ }
+
+ /**
+ * Get the module's JS directory
+ *
+ * @return string
+ */
+ public function getJsDir()
+ {
+ return $this->jsdir;
+ }
+
+ /**
+ * Get the module's controller directory
+ *
+ * @return string
+ */
+ public function getControllerDir()
+ {
+ return $this->controllerdir;
+ }
+
+ /**
+ * Get the module's base directory
+ *
+ * @return string
+ */
+ public function getBaseDir()
+ {
+ return $this->basedir;
+ }
+
+ /**
+ * Get the module's application directory
+ *
+ * @return string
+ */
+ public function getApplicationDir()
+ {
+ return $this->appdir;
+ }
+
+ /**
+ * Get the module's library directory
+ *
+ * @return string
+ */
+ public function getLibDir()
+ {
+ return $this->libdir;
+ }
+
+ /**
+ * Get the module's configuration directory
+ *
+ * @return string
+ */
+ public function getConfigDir()
+ {
+ return $this->configdir;
+ }
+
+ /**
+ * Get the module's form directory
+ *
+ * @return string
+ */
+ public function getFormDir()
+ {
+ return $this->formdir;
+ }
+
+ /**
+ * Get the module config
+ *
+ * @param string $file
+ *
+ * @return Config
+ */
+ public function getConfig($file = 'config')
+ {
+ return $this->app->getConfig()->module($this->name, $file);
+ }
+
+ /**
+ * Get provided permissions
+ *
+ * @return array
+ */
+ public function getProvidedPermissions()
+ {
+ $this->launchConfigScript();
+ return $this->permissionList;
+ }
+
+ /**
+ * Get provided restrictions
+ *
+ * @return array
+ */
+ public function getProvidedRestrictions()
+ {
+ $this->launchConfigScript();
+ return $this->restrictionList;
+ }
+
+ /**
+ * Whether the module provides the given restriction
+ *
+ * @param string $name Restriction name
+ *
+ * @return bool
+ */
+ public function providesRestriction($name)
+ {
+ $this->launchConfigScript();
+ return array_key_exists($name, $this->restrictionList);
+ }
+
+ /**
+ * Whether the module provides the given permission
+ *
+ * @param string $name Permission name
+ *
+ * @return bool
+ */
+ public function providesPermission($name)
+ {
+ $this->launchConfigScript();
+ return array_key_exists($name, $this->permissionList);
+ }
+
+ /**
+ * Get the module configuration tabs
+ *
+ * @return \Icinga\Web\Widget\Tabs
+ */
+ public function getConfigTabs()
+ {
+ $this->launchConfigScript();
+ $tabs = Widget::create('tabs');
+ /** @var \Icinga\Web\Widget\Tabs $tabs */
+ $tabs->add('info', array(
+ 'url' => 'config/module',
+ 'urlParams' => array('name' => $this->getName()),
+ 'label' => 'Module: ' . $this->getName()
+ ));
+
+ if ($this->app->getModuleManager()->hasEnabled($this->name)) {
+ foreach ($this->configTabs as $name => $config) {
+ $tabs->add($name, $config);
+ }
+ }
+
+ return $tabs;
+ }
+
+ /**
+ * Whether the module provides a setup wizard
+ *
+ * @return bool
+ */
+ public function providesSetupWizard()
+ {
+ $this->launchConfigScript();
+ if ($this->setupWizard && class_exists($this->setupWizard)) {
+ $wizard = new $this->setupWizard;
+ return $wizard instanceof SetupWizard;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the module's setup wizard
+ *
+ * @return SetupWizard
+ */
+ public function getSetupWizard()
+ {
+ return new $this->setupWizard;
+ }
+
+ /**
+ * Get the module's user backends
+ *
+ * @return array
+ */
+ public function getUserBackends()
+ {
+ $this->launchConfigScript();
+ return $this->userBackends;
+ }
+
+ /**
+ * Get the module's user group backends
+ *
+ * @return array
+ */
+ public function getUserGroupBackends()
+ {
+ $this->launchConfigScript();
+ return $this->userGroupBackends;
+ }
+
+ /**
+ * Return this module's configurable navigation items
+ *
+ * @return array
+ */
+ public function getNavigationItems()
+ {
+ $this->launchConfigScript();
+ return $this->navigationItems;
+ }
+
+ /**
+ * Provide a named permission
+ *
+ * @param string $name Unique permission name
+ * @param string $description Permission description
+ *
+ * @throws IcingaException If the permission is already provided
+ */
+ protected function providePermission($name, $description)
+ {
+ if ($this->providesPermission($name)) {
+ throw new IcingaException(
+ 'Cannot provide permission "%s" twice',
+ $name
+ );
+ }
+ $this->permissionList[$name] = (object) array(
+ 'name' => $name,
+ 'description' => $description
+ );
+ }
+
+ /**
+ * Provide a named restriction
+ *
+ * @param string $name Unique restriction name
+ * @param string $description Restriction description
+ *
+ * @throws IcingaException If the restriction is already provided
+ */
+ protected function provideRestriction($name, $description)
+ {
+ if ($this->providesRestriction($name)) {
+ throw new IcingaException(
+ 'Cannot provide restriction "%s" twice',
+ $name
+ );
+ }
+ $this->restrictionList[$name] = (object) array(
+ 'name' => $name,
+ 'description' => $description
+ );
+ }
+
+ /**
+ * Provide a module config tab
+ *
+ * @param string $name Unique tab name
+ * @param array $config Tab config
+ *
+ * @return $this
+ * @throws ProgrammingError If $config lacks the key 'url'
+ */
+ protected function provideConfigTab($name, $config = array())
+ {
+ if (! array_key_exists('url', $config)) {
+ throw new ProgrammingError('A module config tab MUST provide a "url"');
+ }
+ $config['url'] = $this->getName() . '/' . ltrim($config['url'], '/');
+ $this->configTabs[$name] = $config;
+ return $this;
+ }
+
+ /**
+ * Provide a setup wizard
+ *
+ * @param string $className The name of the class
+ *
+ * @return $this
+ */
+ protected function provideSetupWizard($className)
+ {
+ $this->setupWizard = $className;
+ return $this;
+ }
+
+ /**
+ * Provide a user backend capable of authenticating users
+ *
+ * @param string $identifier The identifier of the new backend type
+ * @param string $className The name of the class
+ *
+ * @return $this
+ */
+ protected function provideUserBackend($identifier, $className)
+ {
+ $this->userBackends[strtolower($identifier)] = $className;
+ return $this;
+ }
+
+ /**
+ * Provide a user group backend
+ *
+ * @param string $identifier The identifier of the new backend type
+ * @param string $className The name of the class
+ *
+ * @return $this
+ */
+ protected function provideUserGroupBackend($identifier, $className)
+ {
+ $this->userGroupBackends[strtolower($identifier)] = $className;
+ return $this;
+ }
+
+ /**
+ * Provide a new type of configurable navigation item with a optional label and config filename
+ *
+ * @param string $type
+ * @param string $label
+ * @param string $config
+ *
+ * @return $this
+ */
+ protected function provideNavigationItem($type, $label = null, $config = null)
+ {
+ $this->navigationItems[$type] = array(
+ 'label' => $label,
+ 'config' => $config
+ );
+
+ return $this;
+ }
+
+ /**
+ * Register module namespaces on our class loader
+ *
+ * @return $this
+ */
+ protected function registerAutoloader()
+ {
+ if ($this->registeredAutoloader) {
+ return $this;
+ }
+
+ $moduleName = ucfirst($this->getName());
+
+ $this->app->getLoader()->registerNamespace(
+ 'Icinga\\Module\\' . $moduleName,
+ $this->getLibDir() . '/'. $moduleName,
+ $this->getApplicationDir()
+ );
+
+ $this->registeredAutoloader = true;
+
+ return $this;
+ }
+
+ /**
+ * Bind text domain for i18n
+ *
+ * @return $this
+ */
+ protected function registerLocales()
+ {
+ if ($this->hasLocales() && StaticTranslator::$instance instanceof GettextTranslator) {
+ StaticTranslator::$instance->addTranslationDirectory($this->localedir, $this->name);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get whether the module has translations
+ */
+ public function hasLocales()
+ {
+ return file_exists($this->localedir) && is_dir($this->localedir);
+ }
+
+ /**
+ * List all available locales
+ *
+ * @return array Locale list
+ */
+ public function listLocales()
+ {
+ $locales = array();
+ if (! $this->hasLocales()) {
+ return $locales;
+ }
+
+ $dh = opendir($this->localedir);
+ while (false !== ($file = readdir($dh))) {
+ $filename = $this->localedir . DIRECTORY_SEPARATOR . $file;
+ if (preg_match('/^[a-z]{2}_[A-Z]{2}$/', $file) && is_dir($filename)) {
+ $locales[] = $file;
+ }
+ }
+ closedir($dh);
+ sort($locales);
+ return $locales;
+ }
+
+ /**
+ * Register web integration
+ *
+ * Add controller directory to mvc
+ *
+ * @return $this
+ */
+ protected function registerWebIntegration()
+ {
+ if (! $this->app->isWeb()) {
+ return $this;
+ }
+
+ return $this
+ ->registerLocales()
+ ->registerRoutes();
+ }
+
+ /**
+ * Add routes for static content and any route added via {@link addRoute()} to the route chain
+ *
+ * @return $this
+ */
+ protected function registerRoutes()
+ {
+ $router = $this->app->getFrontController()->getRouter();
+
+ // TODO: We should not be required to do this. Please check dispatch()
+ $this->app->getFrontController()->addControllerDirectory(
+ $this->getControllerDir(),
+ $this->getName()
+ );
+
+ /** @var \Zend_Controller_Router_Rewrite $router */
+ foreach ($this->routes as $name => $route) {
+ $router->addRoute($name, $route);
+ }
+ $router->addRoute(
+ $this->name . '_jsprovider',
+ new Zend_Controller_Router_Route(
+ 'js/' . $this->name . '/:file',
+ array(
+ 'action' => 'javascript',
+ 'controller' => 'static',
+ 'module' => 'default',
+ 'module_name' => $this->name
+ )
+ )
+ );
+ $router->addRoute(
+ $this->name . '_img',
+ new Zend_Controller_Router_Route_Regex(
+ 'img/' . $this->name . '/(.+)',
+ array(
+ 'action' => 'img',
+ 'controller' => 'static',
+ 'module' => 'default',
+ 'module_name' => $this->name
+ ),
+ array(
+ 1 => 'file'
+ )
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * Run module bootstrap script
+ *
+ * @return $this
+ */
+ protected function launchRunScript()
+ {
+ return $this->includeScript($this->runScript);
+ }
+
+ /**
+ * Include a php script if it is readable
+ *
+ * @param string $file File to include
+ *
+ * @return $this
+ */
+ protected function includeScript($file)
+ {
+ if (file_exists($file) && is_readable($file)) {
+ include $file;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Run module config script
+ *
+ * @return $this
+ */
+ protected function launchConfigScript()
+ {
+ if ($this->triedToLaunchConfigScript) {
+ return $this;
+ }
+ $this->triedToLaunchConfigScript = true;
+ $this->registerAutoloader();
+ return $this->includeScript($this->configScript);
+ }
+
+ protected function slashesToNamespace($class)
+ {
+ $list = explode('/', $class);
+ foreach ($list as &$part) {
+ $part = ucfirst($part);
+ }
+
+ return implode('\\', $list);
+ }
+
+ /**
+ * Provide a hook implementation
+ *
+ * @param string $name Name of the hook for which to provide an implementation
+ * @param string $implementation Fully qualified name of the class providing the hook implementation.
+ * Defaults to the module's ProvidedHook namespace plus the hook's name for the
+ * class name
+ * @param bool $alwaysRun To run the hook always (e.g. without permission check)
+ *
+ * @return $this
+ */
+ protected function provideHook($name, $implementation = null, $alwaysRun = false)
+ {
+ if ($implementation === null) {
+ $implementation = $name;
+ }
+
+ if (strpos($implementation, '\\') === false) {
+ $class = $this->getNamespace()
+ . '\\ProvidedHook\\'
+ . $this->slashesToNamespace($implementation);
+ } else {
+ $class = $implementation;
+ }
+
+ Hook::register($name, $class, $class, $alwaysRun);
+ return $this;
+ }
+
+ /**
+ * Add a route which will be added to the route chain
+ *
+ * @param string $name Name of the route
+ * @param Zend_Controller_Router_Route_Abstract $route Instance of the route
+ *
+ * @return $this
+ * @see registerRoutes()
+ */
+ protected function addRoute($name, Zend_Controller_Router_Route_Abstract $route)
+ {
+ $this->routes[$name] = $route;
+ return $this;
+ }
+}
diff --git a/library/Icinga/Application/Modules/NavigationItemContainer.php b/library/Icinga/Application/Modules/NavigationItemContainer.php
new file mode 100644
index 0000000..c906ccb
--- /dev/null
+++ b/library/Icinga/Application/Modules/NavigationItemContainer.php
@@ -0,0 +1,117 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Container for module navigation items
+ */
+abstract class NavigationItemContainer
+{
+ /**
+ * This navigation item's name
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * This navigation item's properties
+ *
+ * @var array
+ */
+ protected $properties;
+
+ /**
+ * Create a new NavigationItemContainer
+ *
+ * @param string $name
+ * @param array $properties
+ */
+ public function __construct($name, array $properties = array())
+ {
+ $this->name = $name;
+ $this->properties = $properties;
+ }
+
+ /**
+ * Set this menu item's name
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * Return this menu item's name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set this menu item's properties
+ *
+ * @param array $properties
+ *
+ * @return $this
+ */
+ public function setProperties(array $properties)
+ {
+ $this->properties = $properties;
+ return $this;
+ }
+
+ /**
+ * Return this menu item's properties
+ *
+ * @return array
+ */
+ public function getProperties()
+ {
+ return $this->properties ?: array();
+ }
+
+ /**
+ * Allow dynamic setters and getters for properties
+ *
+ * @param string $name
+ * @param array $arguments
+ *
+ * @return mixed
+ *
+ * @throws ProgrammingError In case the called method is not supported
+ */
+ public function __call($name, $arguments)
+ {
+ if (method_exists($this, $name)) {
+ return call_user_func(array($this, $name), $this, $arguments);
+ }
+
+ $type = substr($name, 0, 3);
+ if ($type !== 'set' && $type !== 'get') {
+ throw new ProgrammingError(
+ 'Dynamic method %s is not supported. Only getters (get*) and setters (set*) are.',
+ $name
+ );
+ }
+
+ $propertyName = strtolower(join('_', preg_split('~(?=[A-Z])~', lcfirst(substr($name, 3)))));
+ if ($type === 'set') {
+ $this->properties[$propertyName] = $arguments[0];
+ return $this;
+ } else { // $type === 'get'
+ return array_key_exists($propertyName, $this->properties) ? $this->properties[$propertyName] : null;
+ }
+ }
+}