diff options
Diffstat (limited to 'library/Icinga/Application/Modules')
-rw-r--r-- | library/Icinga/Application/Modules/DashboardContainer.php | 58 | ||||
-rw-r--r-- | library/Icinga/Application/Modules/Manager.php | 698 | ||||
-rw-r--r-- | library/Icinga/Application/Modules/MenuItemContainer.php | 55 | ||||
-rw-r--r-- | library/Icinga/Application/Modules/Module.php | 1451 | ||||
-rw-r--r-- | library/Icinga/Application/Modules/NavigationItemContainer.php | 117 |
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; + } + } +} |