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; } }