-
data-icinga-module="" data-icinga-url="" style="display: block"> +
data-icinga-module="" data-icinga-url="without('renderLayout') ?>" style="display: block"> render('inline.phtml') ?>
diff --git a/application/layouts/scripts/layout.phtml b/application/layouts/scripts/layout.phtml index b4fd504ed..32c4879b4 100644 --- a/application/layouts/scripts/layout.phtml +++ b/application/layouts/scripts/layout.phtml @@ -44,6 +44,7 @@ $iframeClass = $isIframe ? ' iframe' : ''; + diff --git a/application/layouts/scripts/parts/navigation.phtml b/application/layouts/scripts/parts/navigation.phtml index c689db54a..8b557e32f 100644 --- a/application/layouts/scripts/parts/navigation.phtml +++ b/application/layouts/scripts/parts/navigation.phtml @@ -14,5 +14,5 @@ if (! $this->auth()->isAuthenticated()) {
-order(), Url::fromRequest()->getRelativeUrl()); ?> +order(), Url::fromRequest()->without('renderLayout')->getRelativeUrl()); ?>
diff --git a/config/menu.ini b/config/menu.ini index 65959c93a..075f4230e 100644 --- a/config/menu.ini +++ b/config/menu.ini @@ -18,10 +18,15 @@ title = "Configuration" url = "config" priority = 300 +[System.Modules] +title = "Modules" +url = "config/modules" +priority = 400 + [System.ApplicationLog] title = "Application log" url = "list/applicationlog" -priority = 400 +priority = 500 [Logout] url = "authentication/logout" diff --git a/config/modules/doc/menu.ini b/config/modules/doc/menu.ini new file mode 100644 index 000000000..86889b239 --- /dev/null +++ b/config/modules/doc/menu.ini @@ -0,0 +1,5 @@ +[Documentation] +title = "Documentation" +icon = "img/icons/comment.png" +url = "doc" +priority = 80 diff --git a/config/modules/monitoring/config.ini b/config/modules/monitoring/config.ini new file mode 100644 index 000000000..9b69fe86f --- /dev/null +++ b/config/modules/monitoring/config.ini @@ -0,0 +1,2 @@ +[security] +protected_customvars = "*pw*,*pass*,community" diff --git a/config/modules/monitoring/menu.ini b/config/modules/monitoring/menu.ini index c66a611a9..c185fb87d 100644 --- a/config/modules/monitoring/menu.ini +++ b/config/modules/monitoring/menu.ini @@ -68,7 +68,7 @@ priority = 70 [Overview.Comments] title = "Comments" -url = "monitoring/list/comments" +url = "monitoring/list/comments?comment_type=(comment|ack)" priority = 70 [Overview.Contacts] diff --git a/config/modules/monitoring/menu.ini.in b/config/modules/monitoring/menu.ini.in deleted file mode 100644 index ef874f6a7..000000000 --- a/config/modules/monitoring/menu.ini.in +++ /dev/null @@ -1,69 +0,0 @@ -[menu] -;Remove component as of #4583 since it's not working -;Issues.title = "Issues" ; Extended version -;Issues.route = "/monitoring/list/services?problems=1&sort=severity" ; Explicit route -;Issues.key = "issues" ; When this key is set in the controller, the item is active - -;Remove component as of #4583 since it's not working -;Changes.title = "Recent Changes" -;Changes.route = "/monitoring/list/services?sort=service_last_state_change" -;_1 = 1 ;Spacer after this section - -Hosts.title = "Hosts" -Hosts.route = "/monitoring/list/hosts" -Hosts.iconClass = "icinga-icon-host-petrol" - -Services.title = "Services" -Services.route = "/monitoring/list/services" -Services.iconClass = "icinga-icon-service-petrol" - -Downtimes.title = "Downtimes" -Downtimes.route = "/monitoring/list/downtimes" -Downtimes.iconClass = "icinga-icon-down-petrol" - -Notifications.title = "Notifications" -Notifications.route = "/monitoring/list/notifications" -Notifications.iconClass = "icinga-icon-notification-petrol" - -Comments.title = "Comments" -Comments.route = "/monitoring/list/comments" -Comments.iconClass = "icinga-icon-comment-petrol" - -;Contacts = "/monitoring/list/contacts" - -;Contact Groups = "/monitoring/list/contactgroups" - -Servicegroups.title = "Servicegroups" -Servicegroups.route = "/monitoring/list/servicegroups" -Servicegroups.iconClass = "icinga-icon-servicegroup-petrol" - -Hostgroups.title = "Hostgroups" -Hostgroups.route = "/monitoring/list/hostgroups" -Hostgroups.iconClass = "icinga-icon-hostgroup-petrol" - -History.title = "History" -History.route = "/monitoring/list/eventhistory" -History.iconClass = "icinga-icon-history-petrol" - -Performance.title = "Performance" -Performance.route ="/monitoring/process/performance" - - -[Hosts] -; New section -; Title property unset means title is "Hosts" -url="#" -iconClass="icon-hosts" -priority=1 - -[Hosts.Problems] -; New section beneath section hosts -title="Problem Hosts" -url="/monitoring/list/hosts?problem=1" -priority=2 - -[Hosts.A Link] -title="Wiki" -url="https://wiki.somewhere.com" -priority=1 -icon="https://wiki.somewhere.com/icon.png" \ No newline at end of file diff --git a/icingaweb2.spec b/icingaweb2.spec index 0da22b8ea..cfdcdafc2 100644 --- a/icingaweb2.spec +++ b/icingaweb2.spec @@ -40,11 +40,12 @@ %endif # SLE 11 = 1110 %if 0%{?suse_version} == 1110 +%define phpname php53 %define apache2modphpname apache2-mod_php53 %define usermodparam -A %endif -%if "%{_vendor}" == "redhat" || 0%{?suse_version} == 1110 +%if "%{_vendor}" == "redhat" %define phpname php %define phpzendname php-ZendFramework %endif diff --git a/library/Icinga/Application/Benchmark.php b/library/Icinga/Application/Benchmark.php index c510c61bf..fd69dd510 100644 --- a/library/Icinga/Application/Benchmark.php +++ b/library/Icinga/Application/Benchmark.php @@ -142,6 +142,7 @@ class Benchmark // TODO: Move formatting to CSS file $html = '' . "\n" . ''; foreach ($data->columns as & $col) { + if ($col->title === 'Time') continue; $html .= sprintf( '', $col->align, @@ -153,6 +154,7 @@ class Benchmark foreach ($data->rows as & $row) { $html .= ''; foreach ($data->columns as $key => & $col) { + if ($col->title === 'Time') continue; $html .= sprintf( '', $col->align, diff --git a/library/Icinga/Application/Loader.php b/library/Icinga/Application/Loader.php index bc31cfbb1..56e59f4b2 100644 --- a/library/Icinga/Application/Loader.php +++ b/library/Icinga/Application/Loader.php @@ -40,9 +40,9 @@ class Loader { if (!is_dir($directory)) { throw new ProgrammingError(sprintf( - 'Namespace directory "%s" for "%s" does not exist', - $namespace, - $directory + 'Directory "%s" for namespace "%s" does not exist', + $directory, + $namespace )); } diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 8dd189097..3d5e3752e 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -5,6 +5,7 @@ namespace Icinga\Application\Modules; use Exception; +use Zend_Controller_Router_Route_Abstract; use Zend_Controller_Router_Route as Route; use Icinga\Application\ApplicationBootstrap; use Icinga\Application\Config; @@ -135,6 +136,16 @@ class Module */ private $app; + + /** + * Routes to add to the route chain + * + * @var array Array of name-route pairs + * + * @see addRoute() + */ + protected $routes = array(); + /** * Create a new module object * @@ -166,8 +177,7 @@ class Module */ public function register() { - $this->registerAutoloader() - ->registerWebIntegration(); + $this->registerAutoloader(); try { $this->launchRunScript(); } catch (Exception $e) { @@ -179,6 +189,7 @@ class Module ); return false; } + $this->registerWebIntegration(); return true; } @@ -658,24 +669,29 @@ class Module } /** - * Register routes for web access + * Add routes for static content and any route added via addRoute() to the route chain * - * @return self + * @return self + * @see addRoute() */ protected function registerRoutes() { - $this->app->getFrontController()->getRouter()->addRoute( + $router = $this->app->getFrontController()->getRouter(); + foreach ($this->routes as $name => $route) { + $router->addRoute($name, $route); + } + $router->addRoute( $this->name . '_jsprovider', new Route( 'js/' . $this->name . '/:file', array( 'controller' => 'static', 'action' =>'javascript', - 'module_name' => $this->name + 'module_name' => $this->name ) ) ); - $this->app->getFrontController()->getRouter()->addRoute( + $router->addRoute( $this->name . '_img', new Route( 'img/' . $this->name . '/:file', @@ -750,4 +766,19 @@ class Module 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 self + * @see registerRoutes() + */ + protected function addRoute($name, Zend_Controller_Router_Route_Abstract $route) + { + $this->routes[$name] = $route; + return $this; + } } diff --git a/library/Icinga/Cli/Command.php b/library/Icinga/Cli/Command.php index 081be318c..acb2462e4 100644 --- a/library/Icinga/Cli/Command.php +++ b/library/Icinga/Cli/Command.php @@ -7,6 +7,7 @@ namespace Icinga\Cli; use Icinga\Cli\Screen; use Icinga\Util\Translator; use Icinga\Cli\Params; +use Icinga\Application\Config; use Icinga\Application\ApplicationBootstrap as App; use Exception; @@ -23,6 +24,10 @@ abstract class Command protected $commandName; protected $actionName; + private $config; + + private $configs; + protected $defaultActionName = 'default'; public function __construct(App $app, $moduleName, $commandName, $actionName, $initialize = true) @@ -41,6 +46,51 @@ abstract class Command } } + public function Config($file = null) + { + if ($this->isModule()) { + return $this->getModuleConfig($file); + } else { + return $this->getMainConfig($file); + } + } + + private function getModuleConfig($file = null) + { + if ($file === null) { + if ($this->config === null) { + $this->config = Config::module($this->moduleName); + } + return $this->config; + } else { + if (! array_key_exists($file, $this->configs)) { + $this->configs[$file] = Config::module($this->moduleName, $file); + } + return $this->configs[$file]; + } + } + + private function getMainConfig($file = null) + { + if ($file === null) { + if ($this->config === null) { + $this->config = Config::app(); + } + return $this->config; + } else { + if (! array_key_exists($file, $this->configs)) { + $this->configs[$file] = Config::module($module, $file); + } + return $this->configs[$file]; + } + return $this->config; + } + + public function isModule() + { + return substr(get_class($this), 0, 14) === 'Icinga\\Module\\'; + } + public function setParams(Params $params) { $this->params = $params; diff --git a/library/Icinga/Data/Identifiable.php b/library/Icinga/Data/Identifiable.php new file mode 100644 index 000000000..cfa727a1d --- /dev/null +++ b/library/Icinga/Data/Identifiable.php @@ -0,0 +1,18 @@ +value = $value; + } + + /** + * Get the node's value + * + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * Create a new node from the given value and insert the node as the last child of this node + * + * @param mixed $value The node's value + * + * @return NodeInterface The appended node + */ + public function appendChild($value) + { + $child = new static($value); + $this->push($child); + return $child; + } + + /** + * Whether this node has child nodes + * + * @return bool + */ + public function hasChildren() + { + $current = $this->current(); + if ($current === null) { + $current = $this; + } + return ! $current->isEmpty(); + } + + /** + * Get the node's child nodes + * + * @return NodeInterface + */ + public function getChildren() + { + $current = $this->current(); + if ($current === null) { + $current = $this; + } + return $current; + } +} diff --git a/library/Icinga/Data/Tree/NodeInterface.php b/library/Icinga/Data/Tree/NodeInterface.php new file mode 100644 index 000000000..6953214dc --- /dev/null +++ b/library/Icinga/Data/Tree/NodeInterface.php @@ -0,0 +1,26 @@ +isXhr()) { + $redir = '__SELF__'; + } else { + // TODO: Ignore /? + $redir = $afterLogin->getRelativeUrl(); + } + } + $url = Url::fromPath('authentication/login'); - $url->setParam('redirect', $afterLogin); + + if ($redir) { + $url->setParam('redirect', $redir); + } + $this->rerenderLayout()->redirectNow($url); } @@ -273,6 +290,27 @@ class ActionController extends Zend_Controller_Action return $this->getRequest()->isXmlHttpRequest(); } + protected function redirectXhr($url) + { + if (! $url instanceof Url) { + $url = Url::fromPath($url); + } + + if ($this->rerenderLayout) { + $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes'); + } + if ($this->reloadCss) { + $this->getResponse()->setHeader('X-Icinga-Reload-Css', 'now'); + } + + $this->getResponse() + ->setHeader('X-Icinga-Redirect', rawurlencode($url->getAbsoluteUrl())) + ->sendHeaders(); + + // TODO: Session shutdown? + exit; + } + /** * Redirect to a specific url, updating the browsers URL field * @@ -280,26 +318,13 @@ class ActionController extends Zend_Controller_Action **/ public function redirectNow($url) { - if (! $url instanceof Url) { - $url = Url::fromPath($url); - } - $url = preg_replace('~&~', '&', $url); if ($this->isXhr()) { - if ($this->rerenderLayout) { - $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes'); - } - if ($this->reloadCss) { - $this->getResponse()->setHeader('X-Icinga-Reload-Css', 'now'); - } - - $this->getResponse() - ->setHeader('X-Icinga-Redirect', rawurlencode($url)) - ->sendHeaders(); - - // TODO: Session shutdown? - exit; + $this->redirectXhr($url); } else { - $this->_helper->Redirector->gotoUrlAndExit(Url::fromPath($url)->getRelativeUrl()); + if (! $url instanceof Url) { + $url = Url::fromPath($url); + } + $this->_helper->Redirector->gotoUrlAndExit($url->getRelativeUrl()); } } diff --git a/library/Icinga/Web/Url.php b/library/Icinga/Web/Url.php index ef5a15aa9..ed3acf6a7 100644 --- a/library/Icinga/Web/Url.php +++ b/library/Icinga/Web/Url.php @@ -127,10 +127,6 @@ class Url $baseUrl = $request->getBaseUrl(); $urlObject->setBaseUrl($baseUrl); - // Fetch fragment manually and remove it from the url, to 'help' the parse_url() function - // parsing the url properly. Otherwise calling the function with a fragment, but without a - // query will cause unpredictable behaviour. - $fragment = self::stripUrlFragment($url); $urlParts = parse_url($url); if (isset($urlParts['path'])) { if ($baseUrl !== '' && strpos($urlParts['path'], $baseUrl) === 0) { @@ -144,29 +140,14 @@ class Url $params = UrlParams::fromQueryString($urlParts['query'])->mergeValues($params); } - if ($fragment) { - $urlObject->setAnchor($fragment); + if (isset($urlParts['fragment'])) { + $urlObject->setAnchor($urlParts['fragment']); } $urlObject->setParams($params); return $urlObject; } - /** - * Remove the fragment-part of a given url and return it - * - * @param string $url The url to strip its fragment from - * - * @return null|string The stripped fragment, without the '#' - */ - protected static function stripUrlFragment(&$url) - { - if (preg_match('@#(.*)$@', $url, $matches)) { - $url = str_replace('#' . $matches[1], '', $url); - return $matches[1]; - } - } - /** * Overwrite the baseUrl * @@ -226,12 +207,12 @@ class Url * * @return string */ - public function getRelativeUrl() + public function getRelativeUrl($separator = '&') { if ($this->params->isEmpty()) { return $this->path . $this->anchor; } else { - return $this->path . '?' . $this->params->setSeparator('&') . $this->anchor; + return $this->path . '?' . $this->params->toString($separator) . $this->anchor; } } @@ -251,9 +232,9 @@ class Url * * @return string */ - public function getAbsoluteUrl() + public function getAbsoluteUrl($separator = '&') { - return $this->baseUrl . ($this->baseUrl !== '/' ? '/' : '') . $this->getRelativeUrl(); + return $this->baseUrl . ($this->baseUrl !== '/' ? '/' : '') . $this->getRelativeUrl($separator); } /** @@ -435,6 +416,6 @@ class Url */ public function __toString() { - return $this->getAbsoluteUrl(); + return $this->getAbsoluteUrl('&'); } } diff --git a/library/Icinga/Web/UrlParams.php b/library/Icinga/Web/UrlParams.php index 11fed8333..bdf776401 100644 --- a/library/Icinga/Web/UrlParams.php +++ b/library/Icinga/Web/UrlParams.php @@ -114,6 +114,18 @@ class UrlParams return $ret; } + public function addEncoded($param, $value = true) + { + $this->params[] = array($param, $this->cleanupValue($value)); + $this->indexLastOne(); + return $this; + } + + protected function urlEncode($value) + { + return rawurlencode((string) $value); + } + /** * Add the given parameter with the given value * @@ -127,9 +139,7 @@ class UrlParams */ public function add($param, $value = true) { - $this->params[] = array($param, $this->cleanupValue($value)); - $this->indexLastOne(); - return $this; + return $this->addEncoded($this->urlEncode($param), $this->urlEncode($value)); } /** @@ -198,7 +208,7 @@ class UrlParams */ public function unshift($param, $value) { - array_unshift($this->params, array($param, $this->cleanupValue($value))); + array_unshift($this->params, array($this->urlEncode($param), $this->urlEncode($value))); $this->reIndexAll(); return $this; } @@ -224,7 +234,10 @@ class UrlParams unset($this->params[$remove]); } - $this->params[$this->index[$param][0]] = array($param, $this->cleanupValue($value)); + $this->params[$this->index[$param][0]] = array( + $this->urlEncode($param), + $this->urlEncode($this->cleanupValue($value)) + ); $this->reIndexAll(); return $this; @@ -243,7 +256,7 @@ class UrlParams foreach ($this->index[$p] as $key) { unset($this->params[$key]); } - $this->changed = true; + $changed = true; } } @@ -303,10 +316,10 @@ class UrlParams protected function parseQueryStringPart($part) { if (strpos($part, '=') === false) { - $this->add($part, true); + $this->addEncoded($part, true); } else { list($key, $val) = preg_split('/=/', $part, 2); - $this->add($key, $val); + $this->addEncoded($key, $val); } } @@ -315,8 +328,11 @@ class UrlParams return $this->params; } - public function __toString() + public function toString($separator = null) { + if ($separator === null) { + $separator = $this->separator; + } $parts = array(); foreach ($this->params as $p) { if ($p[1] === true) { @@ -325,13 +341,18 @@ class UrlParams $parts[] = $p[0] . '=' . $p[1]; } } - return implode($this->separator, $parts); + return implode($separator, $parts); + } + + public function __toString() + { + return $this->toString(); } public static function fromQueryString($queryString = null) { if ($queryString === null) { - $queryString = $_SERVER['QUERY_STRING']; + $queryString = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; } $params = new static(); $params->parseQueryString($queryString); diff --git a/library/Icinga/Web/Widget/Limiter.php b/library/Icinga/Web/Widget/Limiter.php index 9c6a726ba..5afa7e4b8 100644 --- a/library/Icinga/Web/Widget/Limiter.php +++ b/library/Icinga/Web/Widget/Limiter.php @@ -81,7 +81,7 @@ class Limiter extends AbstractWidget $this->url->setParam('limit', $limit), null, array( - 'title' => t(sprintf('Show %s rows on one page', $caption)) + 'title' => sprintf(t('Show %s rows on one page'), $caption) ) ); } diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php new file mode 100644 index 000000000..967a2b768 --- /dev/null +++ b/modules/doc/application/controllers/IcingawebController.php @@ -0,0 +1,48 @@ +renderToc(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2', 'doc/icingaweb/chapter'); + } + + /** + * View a chapter of Icinga Web 2's documentation + * + * @throws Zend_Controller_Action_Exception If the required parameter 'chapterId' is missing + */ + public function chapterAction() + { + $chapterId = $this->getParam('chapterId'); + if ($chapterId === null) { + throw new Zend_Controller_Action_Exception( + $this->translate('Missing parameter \'chapterId\''), + 404 + ); + } + $this->renderChapter( + Icinga::app()->getApplicationDir('/../doc'), + $chapterId, + 'doc/icingaweb/toc', + 'doc/icingaweb/chapter' + ); + } + + /** + * View Icinga Web 2's documentation as PDF + */ + public function pdfAction() + { + $this->renderPdf(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2', 'doc/icingaweb/chapter'); + } +} diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php index f46fdad87..c83cfabab 100644 --- a/modules/doc/application/controllers/IndexController.php +++ b/modules/doc/application/controllers/IndexController.php @@ -2,34 +2,9 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -use Icinga\Module\Doc\Controller as DocController; - -use Icinga\Module\Doc\DocParser; +use Icinga\Module\Doc\DocController; class Doc_IndexController extends DocController { - protected $parser; - - - public function init() - { - $module = null; - $this->parser = new DocParser($module); - } - - - public function tocAction() - { - // Temporary workaround - list($html, $toc) = $this->parser->getDocumentation(); - $this->view->toc = $toc; - } - - /** - * Display the application's documentation - */ - public function indexAction() - { - $this->populateView(); - } + public function indexAction() {} } diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php index 41ba42db6..40913368c 100644 --- a/modules/doc/application/controllers/ModuleController.php +++ b/modules/doc/application/controllers/ModuleController.php @@ -2,44 +2,131 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} +use \Zend_Controller_Action_Exception; use Icinga\Application\Icinga; -use Icinga\Module\Doc\Controller as DocController; +use Icinga\Module\Doc\DocController; +use Icinga\Module\Doc\Exception\DocException; class Doc_ModuleController extends DocController { /** - * Display module documentations index + * List modules which are enabled and having the 'doc' directory */ public function indexAction() { - $this->view->enabledModules = Icinga::app()->getModuleManager()->listEnabledModules(); - } - - /** - * Display a module's documentation - */ - public function viewAction() - { - $this->populateView($this->getParam('name')); - } - - /** - * Provide run-time dispatching of module documentation - * - * @param string $methodName - * @param array $args - * - * @return mixed - */ - public function __call($methodName, $args) - { - // TODO(el): Setup routing to retrieve module name as param and point route to moduleAction - $moduleManager = Icinga::app()->getModuleManager(); - $moduleName = substr($methodName, 0, -6); // Strip 'Action' suffix - if (!$moduleManager->hasEnabled($moduleName)) { - // TODO(el): Throw a not found exception once the code has been moved to the moduleAction (see TODO above) - return parent::__call($methodName, $args); + $moduleManager = Icinga::app()->getModuleManager(); + $modules = array(); + foreach (Icinga::app()->getModuleManager()->listEnabledModules() as $enabledModule) { + $docDir = $moduleManager->getModuleDir($enabledModule, '/doc'); + if (is_dir($docDir)) { + $modules[] = $enabledModule; + } } - $this->_helper->redirector->gotoSimpleAndExit('view', null, null, array('name' => $moduleName)); + $this->view->modules = $modules; + } + + /** + * Assert that the given module is enabled + * + * @param $moduleName + * + * @throws Zend_Controller_Action_Exception If the required parameter 'moduleName' is empty or either if the + * given module is neither installed nor enabled + */ + protected function assertModuleEnabled($moduleName) + { + if (empty($moduleName)) { + throw new Zend_Controller_Action_Exception( + $this->translate('Missing parameter \'moduleName\''), + 404 + ); + } + $moduleManager = Icinga::app()->getModuleManager(); + if (! $moduleManager->hasInstalled($moduleName)) { + throw new Zend_Controller_Action_Exception( + sprintf($this->translate('Module \'%s\' is not installed'), $moduleName), + 404 + ); + } + if (! $moduleManager->hasEnabled($moduleName)) { + throw new Zend_Controller_Action_Exception( + sprintf($this->translate('Module \'%s\' is not enabled'), $moduleName), + 404 + ); + } + } + + /** + * View the toc of a module's documentation + * + * @see assertModuleEnabled() + */ + public function tocAction() + { + $moduleName = $this->getParam('moduleName'); + $this->assertModuleEnabled($moduleName); + $moduleManager = Icinga::app()->getModuleManager(); + try { + $this->renderToc( + $moduleManager->getModuleDir($moduleName, '/doc'), + $moduleName, + 'doc/module/chapter', + array('moduleName' => $moduleName) + ); + } catch (DocException $e) { + throw new Zend_Controller_Action_Exception($e->getMessage(), 404); + } + $this->view->moduleName = $moduleName; + } + + /** + * View a chapter of a module's documentation + * + * @throws Zend_Controller_Action_Exception If the required parameter 'chapterId' is missing or if an error in + * the documentation module's library occurs + * @see assertModuleEnabled() + */ + public function chapterAction() + { + $moduleName = $this->getParam('moduleName'); + $this->assertModuleEnabled($moduleName); + $chapterId = $this->getParam('chapterId'); + if ($chapterId === null) { + throw new Zend_Controller_Action_Exception( + $this->translate('Missing parameter \'chapterId\''), + 404 + ); + } + $moduleManager = Icinga::app()->getModuleManager(); + try { + $this->renderChapter( + $moduleManager->getModuleDir($moduleName, '/doc'), + $chapterId, + $this->_helper->url->url(array('moduleName' => $moduleName), 'doc/module/toc'), + 'doc/module/chapter', + array('moduleName' => $moduleName) + ); + } catch (DocException $e) { + throw new Zend_Controller_Action_Exception($e->getMessage(), 404); + } + $this->view->moduleName = $moduleName; + } + + /** + * View a module's documentation as PDF + * + * @see assertModuleEnabled() + */ + public function pdfAction() + { + $moduleName = $this->getParam('moduleName'); + $this->assertModuleEnabled($moduleName); + $moduleManager = Icinga::app()->getModuleManager(); + $this->renderPdf( + $moduleManager->getModuleDir($moduleName, '/doc'), + $moduleName, + 'doc/module/chapter', + array('moduleName' => $moduleName) + ); } } diff --git a/modules/doc/application/views/scripts/chapter.phtml b/modules/doc/application/views/scripts/chapter.phtml new file mode 100644 index 000000000..7657d69fb --- /dev/null +++ b/modules/doc/application/views/scripts/chapter.phtml @@ -0,0 +1,3 @@ +
+ render($this, $this->getHelper('Url')); ?> +
diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml index a178cc155..e4218bee2 100644 --- a/modules/doc/application/views/scripts/index/index.phtml +++ b/modules/doc/application/views/scripts/index/index.phtml @@ -1,5 +1,6 @@ -

Icinga 2 Documentation

-partial('module/view.phtml', 'doc', array( - 'toc' => $toc, - 'html' => $html -)); ?> \ No newline at end of file +
+

translate('Available documentations'); ?>

+ diff --git a/modules/doc/application/views/scripts/index/toc.phtml b/modules/doc/application/views/scripts/index/toc.phtml deleted file mode 100644 index 9188e21ff..000000000 --- a/modules/doc/application/views/scripts/index/toc.phtml +++ /dev/null @@ -1,14 +0,0 @@ -
-

Module documentations

-
-
-partial( - 'layout/menu.phtml', - 'default', - array( - 'items' => $toc->getChildren(), - 'sub' => false, - 'url' => '' - ) -) ?> -
diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml index 36f11e15e..cc184016f 100644 --- a/modules/doc/application/views/scripts/module/index.phtml +++ b/modules/doc/application/views/scripts/module/index.phtml @@ -1,6 +1,10 @@ -

Module documentations

+

translate('Module documentations'); ?>

diff --git a/modules/doc/application/views/scripts/module/view.phtml b/modules/doc/application/views/scripts/module/view.phtml deleted file mode 100644 index 291947ad7..000000000 --- a/modules/doc/application/views/scripts/module/view.phtml +++ /dev/null @@ -1,7 +0,0 @@ - -

No documentation available.

- -
- -
- diff --git a/modules/doc/application/views/scripts/pdf.phtml b/modules/doc/application/views/scripts/pdf.phtml new file mode 100644 index 000000000..72d77f3c0 --- /dev/null +++ b/modules/doc/application/views/scripts/pdf.phtml @@ -0,0 +1,7 @@ +

translate('Documentation'); ?>

+
+ render($this, $this->getHelper('Url')); ?> +
+
+ render($this, $this->getHelper('Url')); ?> +
diff --git a/modules/doc/application/views/scripts/toc.phtml b/modules/doc/application/views/scripts/toc.phtml new file mode 100644 index 000000000..ca6283d67 --- /dev/null +++ b/modules/doc/application/views/scripts/toc.phtml @@ -0,0 +1,6 @@ +
+

+
+
+ render($this, $this->getHelper('Url')); ?> +
diff --git a/modules/doc/library/Doc/Controller.php b/modules/doc/library/Doc/Controller.php deleted file mode 100644 index 2c5a07d49..000000000 --- a/modules/doc/library/Doc/Controller.php +++ /dev/null @@ -1,23 +0,0 @@ -getDocumentation(); - $this->view->html = $html; - $this->view->toc = $toc; - } -} diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php new file mode 100644 index 000000000..42f8dce9b --- /dev/null +++ b/modules/doc/library/Doc/DocController.php @@ -0,0 +1,76 @@ +view->sectionRenderer = new SectionRenderer( + $parser->getDocTree(), + SectionRenderer::decodeUrlParam($chapterId), + $tocUrl, + $url, + $urlParams + ); + $this->view->title = $chapterId; + $this->_helper->viewRenderer('chapter', null, true); + } + + /** + * Render a toc + * + * @param string $path Path to the documentation + * @param string $name Name of the documentation + * @param string $url + * @param array $urlParams + */ + protected function renderToc($path, $name, $url, array $urlParams = array()) + { + $parser = new DocParser($path); + $this->view->tocRenderer = new TocRenderer($parser->getDocTree(), $url, $urlParams); + $name = ucfirst($name); + $this->view->docName = $name; + $this->view->title = sprintf($this->translate('%s Documentation'), $name); + $this->_helper->viewRenderer('toc', null, true); + } + + /** + * Render a pdf + * + * @param string $path Path to the documentation + * @param string $name Name of the documentation + * @param string $url + * @param array $urlParams + */ + protected function renderPdf($path, $name, $url, array $urlParams = array()) + { + $parser = new DocParser($path); + $docTree = $parser->getDocTree(); + $this->view->tocRenderer = new TocRenderer($docTree, $url, $urlParams); + $this->view->sectionRenderer = new SectionRenderer( + $docTree, + null, + null, + $url, + $urlParams + ); + $this->view->docName = $name; + $this->_helper->viewRenderer('pdf', null, true); + $this->_request->setParam('format', 'pdf'); + } +} diff --git a/modules/doc/library/Doc/DocException.php b/modules/doc/library/Doc/DocException.php deleted file mode 100644 index cb7134045..000000000 --- a/modules/doc/library/Doc/DocException.php +++ /dev/null @@ -1,11 +0,0 @@ -fileInfo = $fileInfo; + } + + /** + * (non-PHPDoc) + * @see Countable::count() + */ + public function count() + { + return count($this->fileInfo); + } + + /** + * (non-PHPDoc) + * @see IteratorAggregate::getIterator() + */ + public function getIterator() + { + return new ArrayIterator($this->fileInfo); + } +} diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php index d4e6875d1..c63532dc1 100644 --- a/modules/doc/library/Doc/DocParser.php +++ b/modules/doc/library/Doc/DocParser.php @@ -4,146 +4,65 @@ namespace Icinga\Module\Doc; -use RecursiveIteratorIterator; -use RecursiveDirectoryIterator; -use Parsedown; -use Icinga\Application\Icinga; -use Icinga\Web\Menu; -use Icinga\Web\Url; - -require_once 'IcingaVendor/Parsedown/Parsedown.php'; +use SplDoublyLinkedList; +use Icinga\Exception\NotReadableError; +use Icinga\Module\Doc\Exception\DocEmptyException; +use Icinga\Module\Doc\Exception\DocException; /** * Parser for documentation written in Markdown */ class DocParser { - protected $dir; - - protected $module; + /** + * Path to the documentation + * + * @var string + */ + protected $path; /** - * Create a new documentation parser for the given module or the application + * Iterator over documentation files * - * @param string $module - * - * @throws DocException + * @var DocIterator */ - public function __construct($module = null) - { - if ($module === null) { - $dir = Icinga::app()->getApplicationDir('/../doc'); - } else { - $mm = Icinga::app()->getModuleManager(); - if (!$mm->hasInstalled($module)) { - throw new DocException('Module is not installed'); - } - if (!$mm->hasEnabled($module)) { - throw new DocException('Module is not enabled'); - } - $dir = $mm->getModuleDir($module, '/doc'); - } - if (!is_dir($dir)) { - throw new DocException('Doc directory does not exist'); - } - $this->dir = $dir; - $this->module = $module; - } + protected $docIterator; /** - * Retrieve table of contents and HTML converted from markdown files sorted by filename + * Create a new documentation parser for the given path * - * @return array - * @throws DocException + * @param string $path Path to the documentation + * + * @throws DocException If the documentation directory does not exist + * @throws NotReadableError If the documentation directory is not readable + * @throws DocEmptyException If the documentation directory is empty */ - public function getDocumentation() + public function __construct($path) { - $iter = new RecursiveIteratorIterator( - new MarkdownFileIterator( - new RecursiveDirectoryIterator($this->dir) - ) - ); - $fileInfos = iterator_to_array($iter); - natcasesort($fileInfos); - $cat = array(); - $toc = array((object) array( - 'level' => 0, - 'item' => new Menu('doc') - )); - $itemPriority = 1; - foreach ($fileInfos as $fileInfo) { - try { - $fileObject = $fileInfo->openFile(); - } catch (RuntimeException $e) { - throw new DocException($e->getMessage()); - } - if ($fileObject->flock(LOCK_SH) === false) { - throw new DocException('Couldn\'t get the lock'); - } - $line = null; - while (!$fileObject->eof()) { - // Save last line for setext-style headers - $lastLine = $line; - $line = $fileObject->fgets(); - $header = $this->extractHeader($line, $lastLine); - if ($header !== null) { - list($header, $level) = $header; - $id = $this->extractHeaderId($header); - $attribs = array(); - $this->reduceToc($toc, $level); - if ($id === null) { - $path = array(); - foreach (array_slice($toc, 1) as $entry) { - $path[] = $entry->item->getTitle(); - } - $path[] = $header; - $id = implode('-', $path); - $attribs['rel'] = 'nofollow'; - } - $id = urlencode(str_replace('.', '.', strip_tags($id))); - $item = end($toc)->item->addChild( - $id, - array( - 'url' => Url::fromPath( - 'doc/module/view', - array( - 'name' => $this->module - ) - )->setAnchor($id)->getRelativeUrl(), - 'title' => htmlspecialchars($header), - 'priority' => $itemPriority++, - 'attribs' => $attribs - ) - ); - $toc[] = ((object) array( - 'level' => $level, - 'item' => $item - )); - $line = '' . PHP_EOL . $line; - } - $cat[] = $line; - } - $fileObject->flock(LOCK_UN); + if (! is_dir($path)) { + throw new DocException( + sprintf(mt('doc', 'Documentation directory \'%s\' does not exist'), $path) + ); } - $html = Parsedown::instance()->parse(implode('', $cat)); - $html = preg_replace_callback( - '#
(.*?)\
#s', - array($this, 'highlight'), - $html - ); - return array($html, $toc[0]->item); - } - - /** - * Syntax highlighting for PHP code - * - * @param $match - * - * @return string - */ - protected function highlight($match) - { - return highlight_string(htmlspecialchars_decode($match[1]), true); + if (! is_readable($path)) { + throw new DocException( + sprintf(mt('doc', 'Documentation directory \'%s\' is not readable'), $path) + ); + } + $docIterator = new DocIterator($path); + if ($docIterator->count() === 0) { + throw new DocEmptyException( + sprintf( + mt( + 'doc', + 'Documentation directory \'%s\' does not contain any non-empty Markdown file (\'.md\' suffix)' + ), + $path + ) + ); + } + $this->path = $path; + $this->docIterator = $docIterator; } /** @@ -156,28 +75,28 @@ class DocParser */ protected function extractHeader($line, $lastLine) { - if (!$line) { + if (! $line) { return null; } $header = null; - if ($line && - $line[0] === '#' && - preg_match('/^#+/', $line, $match) === 1 + if ($line + && $line[0] === '#' + && preg_match('/^#+/', $line, $match) === 1 ) { - // Atx-style + // Atx $level = strlen($match[0]); $header = trim(substr($line, $level)); - if (!$header) { + if (! $header) { return null; } } elseif ( - $line && - ($line[0] === '=' || $line[0] === '-') && - preg_match('/^[=-]+\s*$/', $line, $match) === 1 + $line + && ($line[0] === '=' || $line[0] === '-') + && preg_match('/^[=-]+\s*$/', $line, $match) === 1 ) { // Setext $header = trim($lastLine); - if (!$header) { + if (! $header) { return null; } if ($match[0][0] === '=') { @@ -189,36 +108,67 @@ class DocParser if ($header === null) { return null; } - return array($header, $level); - } - - /** - * Extract header id in an a or a span tag - * - * @param string &$header - * - * @return id|null - */ - protected function extractHeaderId(&$header) - { - if ($header[0] === '<' && - preg_match('#(?:<(?Pa|span) id="(?P.+)">)#u', $header, $match) + if ($header[0] === '<' + && preg_match('#(?:<(?Pa|span) (?:id|name)="(?P.+)">)\s*#u', $header, $match) ) { $header = str_replace($match[0], '', $header); - return $match['id']; + $id = $match['id']; + } else { + $id = null; } - return null; + return array($header, $id, $level); } /** - * Reduce the toc to the given level + * Get the documentation tree * - * @param array &$toc - * @param int $level + * @return DocTree */ - protected function reduceToc(array &$toc, $level) { - while (end($toc)->level >= $level) { - array_pop($toc); + public function getDocTree() + { + $tree = new DocTree(); + $stack = new SplDoublyLinkedList(); + foreach ($this->docIterator as $fileInfo) { + /* @var $file \SplFileInfo */ + $file = $fileInfo->openFile(); + /* @var $file \SplFileObject */ + $lastLine = null; + foreach ($file as $line) { + $header = $this->extractHeader($line, $lastLine); + if ($header !== null) { + list($title, $id, $level) = $header; + while (! $stack->isEmpty() && $stack->top()->getLevel() >= $level) { + $stack->pop(); + } + if ($id === null) { + $path = array(); + foreach ($stack as $section) { + /* @var $section Section */ + $path[] = $section->getTitle(); + } + $path[] = $title; + $id = implode('-', $path); + $noFollow = true; + } else { + $noFollow = false; + } + if ($stack->isEmpty()) { + $chapterId = $id; + $section = new Section($id, $title, $level, $noFollow, $chapterId); + $tree->addRoot($section); + } else { + $chapterId = $stack->bottom()->getId(); + $section = new Section($id, $title, $level, $noFollow, $chapterId); + $tree->addChild($section, $stack->top()); + } + $stack->push($section); + } else { + $stack->top()->appendContent($line); + } + // Save last line for setext-style headers + $lastLine = $line; + } } + return $tree; } } diff --git a/modules/doc/library/Doc/DocTree.php b/modules/doc/library/Doc/DocTree.php new file mode 100644 index 000000000..1b112649c --- /dev/null +++ b/modules/doc/library/Doc/DocTree.php @@ -0,0 +1,80 @@ +getId(); + if (isset($this->nodes[$rootId])) { + $rootId = uniqid($rootId); +// throw new LogicException( +// sprintf('Can\'t add root node: a root node with the id \'%s\' already exists', $rootId) +// ); + } + $this->nodes[$rootId] = $this->appendChild($root); + } + + /** + * Append a child node to a parent node + * + * @param Identifiable $child + * @param Identifiable $parent + * + * @throws LogicException If the the tree does not contain the parent node + */ + public function addChild(Identifiable $child, Identifiable $parent) + { + $childId = $child->getId(); + $parentId = $parent->getId(); + if (isset($this->nodes[$childId])) { + $childId = uniqid($childId); +// throw new LogicException( +// sprintf('Can\'t add child node: a child node with the id \'%s\' already exists', $childId) +// ); + } + if (! isset($this->nodes[$parentId])) { + throw new LogicException( + sprintf(mt('doc', 'Can\'t add child node: there\'s no parent node having the id \'%s\''), $parentId) + ); + } + $this->nodes[$childId] = $this->nodes[$parentId]->appendChild($child); + } + + /** + * Get a node + * + * @param mixed $id + * + * @return Node|null + */ + public function getNode($id) + { + if (! isset($this->nodes[$id])) { + return null; + } + return $this->nodes[$id]; + } +} diff --git a/modules/doc/library/Doc/Exception/ChapterNotFoundException.php b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php new file mode 100644 index 000000000..cd048a162 --- /dev/null +++ b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php @@ -0,0 +1,10 @@ +getInnerIterator()->current(); - if (!$current->isFile()) { + /* @var $current \SplFileInfo */ + if (! $current->isFile()) { return false; } $filename = $current->getFilename(); diff --git a/modules/doc/library/Doc/NonEmptyFileIterator.php b/modules/doc/library/Doc/NonEmptyFileIterator.php new file mode 100644 index 000000000..71bf5acfa --- /dev/null +++ b/modules/doc/library/Doc/NonEmptyFileIterator.php @@ -0,0 +1,31 @@ +getInnerIterator()->current(); + /* @var $current \SplFileInfo */ + if (! $current->isFile() + || $current->getSize() === 0 + ) { + return false; + } + return true; + } +} diff --git a/modules/doc/library/Doc/Renderer.php b/modules/doc/library/Doc/Renderer.php new file mode 100644 index 000000000..0aebb89b9 --- /dev/null +++ b/modules/doc/library/Doc/Renderer.php @@ -0,0 +1,75 @@ +id = $id; + $this->title = $title; + $this->level = $level; + $this->noFollow = $noFollow; + $this->chapterId= $chapterId; + } + + /** + * Get the ID of the section + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Get the title of the section + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Get the header level + * + * @return int + */ + public function getLevel() + { + return $this->level; + } + + /** + * Whether to instruct search engines to not index the link to the section + * + * @return bool + */ + public function isNoFollow() + { + return $this->noFollow; + } + + /** + * The ID of the chapter the section is part of + * + * @return string + */ + public function getChapterId() + { + return $this->chapterId; + } + + /** + * Append content + * + * @param string $content + */ + public function appendContent($content) + { + $this->content[] = $content; + } + + /** + * Get the content of the section + * + * @return array + */ + public function getContent() + { + return $this->content; + } +} diff --git a/modules/doc/library/Doc/SectionFilterIterator.php b/modules/doc/library/Doc/SectionFilterIterator.php new file mode 100644 index 000000000..e20d80359 --- /dev/null +++ b/modules/doc/library/Doc/SectionFilterIterator.php @@ -0,0 +1,68 @@ +chapterId = $chapterId; + } + + /** + * Accept sections that are part of the given chapter + * + * @return bool Whether the current element of the iterator is acceptable + * through this filter + */ + public function accept() + { + $section = $this->getInnerIterator()->current()->getValue(); + /* @var $section \Icinga\Module\Doc\Section */ + if ($section->getChapterId() === $this->chapterId) { + return true; + } + return false; + } + + /** + * (non-PHPDoc) + * @see RecursiveFilterIterator::getChildren() + */ + public function getChildren() + { + return new static($this->getInnerIterator()->getChildren(), $this->chapterId); + } + + /** + * (non-PHPDoc) + * @see Countable::count() + */ + public function count() + { + return iterator_count($this); + } +} diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php new file mode 100644 index 000000000..938e5ed7b --- /dev/null +++ b/modules/doc/library/Doc/SectionRenderer.php @@ -0,0 +1,292 @@ +docTree = $docTree; + $this->view = $view; + $this->zendUrlHelper = $zendUrlHelper; + $this->url = $url; + $this->urlParams = $urlParams; + } + + public function render($match) + { + $node = $this->docTree->getNode(Renderer::decodeAnchor($match['fragment'])); + /* @var $node \Icinga\Data\Tree\Node */ + if ($node === null) { + return $match[0]; + } + $section = $node->getValue(); + /* @var $section \Icinga\Module\Doc\Section */ + $path = $this->zendUrlHelper->url( + array_merge( + $this->urlParams, + array( + 'chapterId' => SectionRenderer::encodeUrlParam($section->getChapterId()) + ) + ), + $this->url, + false, + false + ); + $url = $this->view->url($path); + $url->setAnchor(SectionRenderer::encodeAnchor($section->getId())); + return sprintf( + 'isNoFollow() ? 'rel="nofollow" ' : '', + $url->getAbsoluteUrl() + ); + } +} + +/** + * Section renderer + */ +class SectionRenderer extends Renderer +{ + /** + * The documentation tree + * + * @var DocTree + */ + protected $docTree; + + protected $tocUrl; + + /** + * The URL to replace links with + * + * @var string + */ + protected $url; + + /** + * Additional URL parameters + * + * @var array + */ + protected $urlParams; + + /** + * Parsedown instance + * + * @var Parsedown + */ + protected $parsedown; + + /** + * Content + * + * @var array + */ + protected $content = array(); + + /** + * Create a new section renderer + * + * @param DocTree $docTree The documentation tree + * @param string|null $chapterId If not null, the chapter ID to filter for + * @param string $tocUrl + * @param string $url The URL to replace links with + * @param array $urlParams Additional URL parameters + * + * @throws ChapterNotFoundException If the chapter to filter for was not found + */ + public function __construct(DocTree $docTree, $chapterId, $tocUrl, $url, array $urlParams) + { + if ($chapterId !== null) { + $filter = new SectionFilterIterator($docTree, $chapterId); + if ($filter->count() === 0) { + throw new ChapterNotFoundException( + sprintf(mt('doc', 'Chapter \'%s\' not found'), $chapterId) + ); + } + parent::__construct( + $filter, + RecursiveIteratorIterator::SELF_FIRST + ); + } else { + parent::__construct($docTree, RecursiveIteratorIterator::SELF_FIRST); + } + $this->docTree = $docTree; + $this->tocUrl = $tocUrl; + $this->url = $url; + $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams); + $this->parsedown = Parsedown::instance(); + } + + /** + * Syntax highlighting for PHP code + * + * @param $match + * + * @return string + */ + protected function highlightPhp($match) + { + return '
' . highlight_string(htmlspecialchars_decode($match[1]), true) . '
'; + } + + /** + * Replace img src tags + * + * @param $match + * + * @return string + */ + protected function replaceImg($match) + { + $doc = new DOMDocument(); + $doc->loadHTML($match[0]); + $xpath = new DOMXPath($doc); + $img = $xpath->query('//img[1]')->item(0); + /* @var $img \DOMElement */ + $img->setAttribute('src', Url::fromPath($img->getAttribute('src'))->getAbsoluteUrl()); + return substr_replace($doc->saveXML($img), '', -2, 1); // Replace '/>' with '>' + } + + /** + * Render the section + * + * @param View $view + * @param Zend_View_Helper_Url $zendUrlHelper + * @param bool $renderNavigation + * + * @return string + */ + public function render(View $view, Zend_View_Helper_Url $zendUrlHelper, $renderNavigation = true) + { + $callback = new Callback($this->docTree, $view, $zendUrlHelper, $this->url, $this->urlParams); + $content = array(); + foreach ($this as $node) { + $section = $node->getValue(); + /* @var $section \Icinga\Module\Doc\Section */ + $content[] = sprintf( + '
%3$s', + Renderer::encodeAnchor($section->getId()), + $section->getLevel(), + $view->escape($section->getTitle()) + ); + $html = preg_replace_callback( + '#
(.*?)
#s', + array($this, 'highlightPhp'), + $this->parsedown->text(implode('', $section->getContent())) + ); + $html = preg_replace_callback( + '/]+>/', + array($this, 'replaceImg'), + $html + ); + $content[] = preg_replace_callback( + '/[^>]*?\s+)?href="#(?P[^"]+)"/', + array($callback, 'render'), + $html + ); + } + if ($renderNavigation) { + foreach ($this->docTree as $chapter) { + if ($chapter->getValue()->getId() === $section->getChapterId()) { + $navigation = array(''; + $content = array_merge($navigation, $content, $navigation); + break; + } + } + } + return implode("\n", $content); + } +} diff --git a/modules/doc/library/Doc/TocRenderer.php b/modules/doc/library/Doc/TocRenderer.php new file mode 100644 index 000000000..4061e80e3 --- /dev/null +++ b/modules/doc/library/Doc/TocRenderer.php @@ -0,0 +1,109 @@ +url = $url; + $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams); + } + + public function beginIteration() + { + $this->content[] = ''; + } + + public function beginChildren() + { + $this->content[] = '
    '; + } + + public function endChildren() + { + $this->content[] = '
'; + } + + /** + * Render the toc + * + * @param View $view + * @param Zend_View_Helper_Url $zendUrlHelper + * + * @return string + */ + public function render(View $view, Zend_View_Helper_Url $zendUrlHelper) + { + foreach ($this as $node) { + $section = $node->getValue(); + /* @var $section \Icinga\Module\Doc\Section */ + $path = $zendUrlHelper->url( + array_merge( + $this->urlParams, + array( + 'chapterId' => $this->encodeUrlParam($section->getChapterId()) + ) + ), + $this->url, + false, + false + ); + $url = $view->url($path); + $url->setAnchor($this->encodeAnchor($section->getId())); + $this->content[] = sprintf( + '
  • %s', + $section->isNoFollow() ? 'rel="nofollow" ' : '', + $url->getAbsoluteUrl(), + $view->escape($section->getTitle()) + ); + if (! $this->getInnerIterator()->current()->hasChildren()) { + $this->content[] = '
  • '; + } + } + return implode("\n", $this->content); + } +} diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less new file mode 100644 index 000000000..d6d0d2a94 --- /dev/null +++ b/modules/doc/public/css/module.less @@ -0,0 +1,62 @@ +// W3C Recommendation (except h4) +h1 { font-size: 2em !important; } +h2 { font-size: 1.5em !important; } +h3 { font-size: 1.17em !important; } +h4 { font-size: 1em !important; } +h5 { font-size: .83em !important; } +h6 { font-size: .75em !important; } + +div.chapter { + padding-left: 5px; +} + +table th { + text-align: left; +} + +table th, +table td { + border: solid 1px lightgray; + padding-left: 5px; + padding-right: 5px; +} + +code { + width: 100%; + overflow-x: auto; + padding: 0.2em; + display: inline; +} + +pre > code { + display: inline-block; +} + +div.chapter > ul.navigation { + margin: 0; + padding: 0.4em; + text-align: center; + background-color: #888; + + li { + list-style: none; + display: inline; + margin: 0.2em; + padding: 0; + + a { + color: #fff; + text-decoration: none; + } + + &.prev { + padding-right: 0.6em; + border-right: 2px solid #fff; + } + + &.next { + padding-left: 0.6em; + border-left: 2px solid #fff; + } + } +} diff --git a/modules/doc/run.php b/modules/doc/run.php new file mode 100644 index 000000000..7392e4c22 --- /dev/null +++ b/modules/doc/run.php @@ -0,0 +1,50 @@ +isCli()) { + return; +} + +$docModuleChapter = new Zend_Controller_Router_Route( + 'doc/module/:moduleName/chapter/:chapterId', + array( + 'controller' => 'module', + 'action' => 'chapter', + 'module' => 'doc' + ) +); + +$docIcingaWebChapter = new Zend_Controller_Router_Route( + 'doc/icingaweb/chapter/:chapterId', + array( + 'controller' => 'icingaweb', + 'action' => 'chapter', + 'module' => 'doc' + ) +); + +$docModuleToc = new Zend_Controller_Router_Route( + 'doc/module/:moduleName/toc', + array( + 'controller' => 'module', + 'action' => 'toc', + 'module' => 'doc' + ) +); + +$docModulePdf = new Zend_Controller_Router_Route( + 'doc/module/:moduleName/pdf', + array( + 'controller' => 'module', + 'action' => 'pdf', + 'module' => 'doc' + ) +); + +$this->addRoute('doc/module/chapter', $docModuleChapter); +$this->addRoute('doc/icingaweb/chapter', $docIcingaWebChapter); +$this->addRoute('doc/module/toc', $docModuleToc); +$this->addRoute('doc/module/pdf', $docModulePdf); + diff --git a/modules/monitoring/application/controllers/ConfigController.php b/modules/monitoring/application/controllers/ConfigController.php index c06ded57f..f0982c29a 100644 --- a/modules/monitoring/application/controllers/ConfigController.php +++ b/modules/monitoring/application/controllers/ConfigController.php @@ -14,6 +14,7 @@ use Icinga\Module\Monitoring\Form\Config\Backend\EditBackendForm; use Icinga\Module\Monitoring\Form\Config\Backend\CreateBackendForm; use Icinga\Module\Monitoring\Form\Config\Instance\EditInstanceForm; use Icinga\Module\Monitoring\Form\Config\Instance\CreateInstanceForm; +use Icinga\Module\Monitoring\Form\Config\SecurityForm; use Icinga\Exception\NotReadableError; @@ -216,7 +217,7 @@ class Monitoring_ConfigController extends ModuleActionController /** * Display a form to remove the instance identified by the 'instance' parameter */ - private function writeConfiguration($config, $file) + private function writeConfiguration($config, $file = null) { $target = $this->Config($file)->getConfigFile(); $writer = new PreservingIniWriter(array('filename' => $target, 'config' => $config)); @@ -258,4 +259,25 @@ class Monitoring_ConfigController extends ModuleActionController $instanceCfg = $this->Config('instances'); return $instanceCfg && $instanceCfg->get($instance); } + + public function securityAction() + { + $this->view->tabs = $this->Module()->getConfigTabs()->activate('security'); + + $form = new SecurityForm(); + $form->setConfiguration($this->Config()->get('security')); + $form->setRequest($this->getRequest()); + if ($form->isSubmittedAndValid()) { + $config = $this->Config()->toArray(); + $config['security'] = $form->getConfig(); + if ($this->writeConfiguration(new Zend_Config($config))) { + Notification::success('Configuration modified successfully'); + $this->redirectNow('monitoring/config/security'); + } else { + $this->render('show-configuration'); + return; + } + } + $this->view->form = $form; + } } diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index f0cdb2767..3e02a4757 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -94,7 +94,6 @@ class Monitoring_ListController extends Controller 'host_last_check', 'host_last_state_change' => $stateChangeColumn, 'host_notifications_enabled', - // 'host_unhandled_service_count', 'host_unhandled_services', 'host_action_url', 'host_notes_url', @@ -222,6 +221,7 @@ class Monitoring_ListController extends Controller 'author' => 'downtime_author', 'start' => 'downtime_start', 'scheduled_start' => 'downtime_scheduled_start', + 'scheduled_end' => 'downtime_scheduled_end', 'end' => 'downtime_end', 'duration' => 'downtime_duration', 'is_flexible' => 'downtime_is_flexible', @@ -229,7 +229,9 @@ class Monitoring_ListController extends Controller 'is_in_effect' => 'downtime_is_in_effect', 'entry_time' => 'downtime_entry_time', 'host' => 'downtime_host', - 'service' => 'downtime_service' + 'service' => 'downtime_service', + 'host_state' => 'downtime_host_state', + 'service_state' => 'downtime_service_state' ))->order('downtime_is_in_effect', 'DESC') ->order('downtime_scheduled_start', 'DESC'); diff --git a/modules/monitoring/application/controllers/MultiController.php b/modules/monitoring/application/controllers/MultiController.php index a623dd88b..6459edd59 100644 --- a/modules/monitoring/application/controllers/MultiController.php +++ b/modules/monitoring/application/controllers/MultiController.php @@ -24,7 +24,6 @@ class Monitoring_MultiController extends Controller array( 'host_name', 'host_in_downtime', - 'host_unhandled_service_count', 'host_passive_checks_enabled', 'host_obsessing', 'host_state', diff --git a/modules/monitoring/application/forms/Config/SecurityForm.php b/modules/monitoring/application/forms/Config/SecurityForm.php new file mode 100644 index 000000000..b0749868d --- /dev/null +++ b/modules/monitoring/application/forms/Config/SecurityForm.php @@ -0,0 +1,60 @@ +addElement( + 'text', + 'protected_customvars', + array( + 'label' => 'Protected Custom Variables', + 'required' => true, + 'value' => $this->config ? $this->config->get('protected_customvars', $default) : $default, + 'helptext' => 'Comma separated case insensitive list of protected custom variables.' + . ' Use * as a placeholder for zero or more wildcard characters.' + . ' Existance of those custom variables will be shown, but their values will be masked.' + ) + ); + $this->setSubmitLabel('Save'); + } + + /** + * Set the configuration to be used for initial population of the form + */ + public function setConfiguration($config) + { + $this->config = $config; + } + + /** + * Return the configuration set by this form + * + * @return Zend_Config The configuration set in this form + */ + public function getConfig() + { + $values = $this->getValues(); + return new Zend_Config(array( + 'protected_customvars' => $values['protected_customvars'] + )); + } +} diff --git a/modules/monitoring/application/views/scripts/config/security.phtml b/modules/monitoring/application/views/scripts/config/security.phtml new file mode 100644 index 000000000..71f2a341a --- /dev/null +++ b/modules/monitoring/application/views/scripts/config/security.phtml @@ -0,0 +1,6 @@ +
    + tabs ?> +
    +
    + form ?> +
    diff --git a/modules/monitoring/application/views/scripts/list/comments.phtml b/modules/monitoring/application/views/scripts/list/comments.phtml index 3ed671fbe..645c35fb6 100644 --- a/modules/monitoring/application/views/scripts/list/comments.phtml +++ b/modules/monitoring/application/views/scripts/list/comments.phtml @@ -1,98 +1,114 @@ +getHelper('CommandForm'); + +?> + +compact): ?>
    -tabs ?> -
    -sortControl->render($this); ?> -
    -paginationControl($comments, null, null, array('preserve' => $this->preserve)); ?> + tabs->render($this); ?> +
    + translate('Sort by'); ?> sortControl->render($this); ?> +
    + widget('limiter', array('url' => $this->url, 'max' => $comments->count())); ?> + paginationControl($comments, null, null, array('preserve' => $this->preserve)); ?>
    +
    -
    %s
    %s
    - - + translate('No comments matching the filter') ?> + + -$cf = $this->getHelper('CommandForm'); - -if (count($comments) === 0) { - echo t('No comments matching the filter'); -} - -foreach ($comments as $comment): - -?> - - + + + +
    + + + type) { + switch ($comment->type) { case 'flapping': - $icon = 'flapping'; - $tooltip = 'Comment was caused by a flapping host or service.'; - break; + $icon = 'flapping'; + $title = $this->translate('Flapping'); + $tooltip = $this->translate('Comment was caused by a flapping host or service.'); + break; case 'comment': - $icon = 'user'; - $tooltip = 'Comment was created by an user.'; - break; + $icon = 'user'; + $title = $this->translate('User Comment'); + $tooltip = $this->translate('Comment was created by an user.'); + break; case 'downtime': - $icon = 'down'; - $tooltip = 'Comment was caused by a downtime.'; + $icon = 'down'; + $title = $this->translate('Downtime'); + $tooltip = $this->translate('Comment was caused by a downtime.'); case 'ack': - $icon = 'acknowledgement'; - $tooltip = 'Comment was caused by an acknowledgement.'; - } + $icon = 'acknowledgement'; + $title = $this->translate('Acknowledgement'); + $tooltip = $this->translate('Comment was caused by an acknowledgement.'); + } ?> - icon($icon . '.png', $tooltip) ?>
    - timeSince($comment->timestamp) ?> - - + + + $comment->id, + 'host' => $comment->host + ); + if ($comment->objecttype === 'service') { + $data['service'] = $comment->service; + } ?> - - - - - -
    - objecttype === 'service'): ?>icon('service.png', 'Service comment') ?> qlink( - $comment->service, - 'monitoring/show/service', - array( +
    + icon($icon . '.png', $tooltip) ?> +
    + escape($title); ?> +
    + prefixedTimeSince($comment->timestamp); ?> +
    + objecttype === 'service'): ?> + icon('service.png'); ?> + service; ?> + + + translate('on') . ' ' . $comment->host; ?> + + + icon('host.png'); ?> + host; ?> + + +
    + icon('comment.png'); ?> author) + ? '[' . $comment->author . '] ' + : ''; + ?>escape($comment->comment); ?> +
    + persistent + ? $this->translate('This comment is persistent.') + : $this->translate('This comment is not persistent.'); + ?> +
    + expiration ? sprintf( + $this->translate('This comment expires on %s at %s.'), + date('d.m.y', $comment-expiration), + date('H:i', $comment->expiration) + ) : $this->translate('This comment does not expire.'); ?> +
    - $comment->id, - 'host' => $comment->host - ); - - if ($comment->objecttype === 'service') { - $data['service'] = $comment->service; - } - - // echo $cf->iconSubmitForm( - // 'img/icons/remove.png', - echo $cf->labelSubmitForm( - 'X', - 'Remove comment', - 'link-like', - 'removecomment', - $data - ); - -?> -
    - - +
    + labelSubmitForm( + 'X', + $this->translate('Remove Comment'), + 'link-like', + 'removecomment', + $data + ); ?> +
    +
    \ No newline at end of file diff --git a/modules/monitoring/application/views/scripts/list/downtimes.phtml b/modules/monitoring/application/views/scripts/list/downtimes.phtml index 995a66001..babb76074 100644 --- a/modules/monitoring/application/views/scripts/list/downtimes.phtml +++ b/modules/monitoring/application/views/scripts/list/downtimes.phtml @@ -1,70 +1,121 @@ getHelper('CommandForm'); ?> -
    -tabs ?> -
    -sortControl->render($this); ?> -
    -paginationControl($downtimes, null, null, array('preserve' => $this->preserve)); ?> -
    -
    - - -downtimes as $downtime): ?> - - - - - - -
    - dateFormat()->formatDateTime($downtime->start); ?> - - dateFormat()->formatDateTime($downtime->end); ?> -
    - Duration: util()->showHourMin($downtime->duration); ?> -
    - The is_flexible): ?>flexiblefixed downtime is is_in_effect): ?>not in effect -
    - service)): ?> - service ?> - on host ?> - - host ?> - -
    - author ?>: comment ?> -
    - Entry Time: entry_time) ? $this->dateFormat()->formatDateTime((int) $downtime->entry_time) : ''; ?> - -
    - $downtime->id, - 'host' => $downtime->host - ); - if (isset($downtime->service)) { - $data['service'] = $downtime->service; - } - // echo $helper->iconSubmitForm( - // 'img/icons/remove.png', - echo $helper->labelSubmitForm( - 'X', - 'Remove Downtime', - 'link-like', - 'removedowntime', - $data - ); - ?> -
    +compact): ?> +
    + tabs->render($this); ?> +
    + translate('Sort by'); ?> sortControl->render($this); ?> +
    + widget('limiter', array('url' => $this->url, 'max' => $downtimes->count())); ?> + paginationControl($downtimes, null, null, array('preserve' => $this->preserve)); ?> +
    + + +
    + + translate('No downtimes matching the filter'); ?> +
    + + + + + + service)) { + $stateName = strtolower($this->util()->getServiceStateName($downtime->service_state)); + } else { + $stateName = strtolower($this->util()->getHostStateName($downtime->host_state)); + } + ?> + + + + $downtime->id, + 'host' => $downtime->host + ); + if (isset($downtime->service)) { + $data['service'] = $downtime->service; + } + ?> + + + + +
    + is_in_effect ? $this->translate('Expires') : $this->translate('Starts'); ?> +
    + prefixedTimeUntil($downtime->is_in_effect ? $downtime->end : $downtime->start); ?> +
    + service)): ?> + + service; ?> + + + translate('on'); ?> host; ?> + + + + host; ?> + + +
    + icon('comment.png'); ?> [author; ?>] comment; ?> +
    + + is_flexible): ?> + is_in_effect): ?> + translate('This flexible downtime was started on %s at %s and lasts for %s until %s at %s.'), + date('d.m.y', $downtime->start), + date('H:i', $downtime->start), + $this->format()->duration($downtime->duration), + date('d.m.y', $downtime->end), + date('H:i', $downtime->end) + ); ?> + + translate('This flexible downtime has been scheduled to start between %s - %s and to last for %s.'), + date('d.m.y H:i', $downtime->scheduled_start), + date('d.m.y H:i', $downtime->scheduled_end), + $this->format()->duration($downtime->duration) + ); ?> + + + is_in_effect): ?> + translate('This fixed downtime was started on %s at %s and expires on %s at %s.'), + date('d.m.y', $downtime->start), + date('H:i', $downtime->start), + date('d.m.y', $downtime->end), + date('H:i', $downtime->end) + ); ?> + + translate('This fixed downtime has been scheduled to start on %s at %s and to end on %s at %s.'), + date('d.m.y', $downtime->scheduled_start), + date('H:i', $downtime->scheduled_start), + date('d.m.y', $downtime->scheduled_end), + date('H:i', $downtime->scheduled_end) + ); ?> + + + +
    + labelSubmitForm( + 'X', + 'Remove Downtime', + 'link-like', + 'removedowntime', + $data + ); ?> +
    diff --git a/modules/monitoring/application/views/scripts/list/eventhistory.phtml b/modules/monitoring/application/views/scripts/list/eventhistory.phtml index 55257bae6..56c2198e9 100644 --- a/modules/monitoring/application/views/scripts/list/eventhistory.phtml +++ b/modules/monitoring/application/views/scripts/list/eventhistory.phtml @@ -1,122 +1,115 @@ - -compact): ?> -
    - tabs->render($this); ?> -
    - translate('Sort by') ?> sortControl->render($this); ?> -
    - paginationControl($history, null, null, array('preserve' => $this->preserve)); ?> -
    - +compact): ?> +
    + tabs->render($this); ?> +
    + translate('Sort by'); ?> sortControl->render($this); ?> +
    + widget('limiter', array('url' => $this->url, 'max' => $this->history->count())); ?> + paginationControl($history, null, null, array('preserve' => $this->preserve)); ?> +
    +
    -translate('No entries found') ?> + translate('No history events matching the filter') ?>
    - - - - - type) { - case 'notify': - $icon = 'notification'; - $title = 'Notification'; - $msg = $event->output; - break; - case 'comment': - $icon = 'comment'; - $title = 'Comment'; - $msg = $event->output; - break; - case 'ack': - $icon = 'acknowledgement'; - $title = 'Acknowledgement'; - $msg = $event->output; - break; - case 'dt_comment': - $icon = 'in-downtime'; - $title = 'In Downtime'; - $msg = $event->output; - break; - case 'flapping': - $icon = 'flapping'; - $title = 'Flapping'; - $msg = $event->output; - break; - case 'hard_state': - $icon = '{{HARDSTATE_ICON}}'; - $title = 'Hard State'; - $msg = $event->output . '
    Attempt ' . $event->attempt . '/' . $event->max_attempts . ' (Hard)'; - $class = 'border-status-' . ( - $isService ? - strtolower($this->util()->getServiceStateName($event->state)) : - strtolower($this->util()->getHostStateName($event->state)) - ); - break; - case 'soft_state': - $icon = '{{SOFTSTATE_ICON}}'; - $title = 'Soft State'; - $msg = $event->output . '
    Attempt ' . $event->attempt . '/' . $event->max_attempts . ' (Soft)'; - $class = 'border-status-' . ( - $isService ? - strtolower($this->util()->getServiceStateName($event->state)) : - strtolower($this->util()->getHostStateName($event->state)) - ); - break; - case 'dt_start': - $icon = 'downtime-start'; - $title = 'Downtime Start'; - $msg = $event->output; - break; - case 'dt_end': - $icon = 'downtime-end'; - $title = 'Downtime End'; - $msg = $event->output; - break; - } - ?> - - - +
    "> - timestamp); ?> - - service)): ?> - - service ?> - - - on host ?> - - - - - host ?> - - -
    -
    - - -
    -
    + + + service); + switch ($event->type) { + case 'notify': + $icon = 'notification'; + $title = $this->translate('Notification'); + $msg = $event->output; + break; + case 'comment': + $icon = 'comment'; + $title = $this->translate('Comment'); + $msg = $event->output; + break; + case 'ack': + $icon = 'acknowledgement'; + $title = $this->translate('Acknowledgement'); + $msg = $event->output; + break; + case 'dt_comment': + $icon = 'in_downtime'; + $title = $this->translate('In Downtime'); + $msg = $event->output; + break; + case 'flapping': + $icon = 'flapping'; + $title = $this->translate('Flapping'); + $msg = $event->output; + break; + case 'hard_state': + $icon = $isService ? 'service' : 'host'; + $msg = '[ ' . $event->attempt . '/' . $event->max_attempts . ' ] ' . $event->output; + $stateName = ( + $isService + ? strtolower($this->util()->getServiceStateName($event->state)) + : strtolower($this->util()->getHostStateName($event->state)) + ); + $title = strtoupper($stateName); // TODO: Should be translatable! + break; + case 'soft_state': + $icon = 'softstate'; + $msg = '[ ' . $event->attempt . '/' . $event->max_attempts . ' ] ' . $event->output; + $stateName = ( + $isService + ? strtolower($this->util()->getServiceStateName($event->state)) + : strtolower($this->util()->getHostStateName($event->state)) + ); + $title = strtoupper($stateName); // TODO: Should be translatable! + break; + case 'dt_start': + $icon = 'downtime_start'; + $title = $this->translate('Downtime Start'); + $msg = $event->output; + break; + case 'dt_end': + $icon = 'downtime_end'; + $title = $this->translate('Downtime End'); + $msg = $event->output; + break; + } + ?> + + + - - -
    + escape($title); ?> +
    + timestamp); ?> +
    + + + service; ?> + + + translate('on') . ' ' . $event->host; ?> + + + + host; ?> + + +
    +
    + icon($icon . '.png', $title); ?> +
    +
    - + + +