summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Web/Controller
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Web/Controller')
-rw-r--r--library/Icinga/Web/Controller/ActionController.php587
-rw-r--r--library/Icinga/Web/Controller/AuthBackendController.php149
-rw-r--r--library/Icinga/Web/Controller/BasePreferenceController.php39
-rw-r--r--library/Icinga/Web/Controller/ControllerTabCollector.php97
-rw-r--r--library/Icinga/Web/Controller/Dispatcher.php93
-rw-r--r--library/Icinga/Web/Controller/ModuleActionController.php80
-rw-r--r--library/Icinga/Web/Controller/StaticController.php87
7 files changed, 1132 insertions, 0 deletions
diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php
new file mode 100644
index 0000000..76ff650
--- /dev/null
+++ b/library/Icinga/Web/Controller/ActionController.php
@@ -0,0 +1,587 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Common\PdfExport;
+use Icinga\File\Pdf;
+use ipl\I18n\Translation;
+use Zend_Controller_Action;
+use Zend_Controller_Action_HelperBroker;
+use Zend_Controller_Request_Abstract;
+use Zend_Controller_Response_Abstract;
+use Icinga\Application\Benchmark;
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\Http\HttpMethodNotAllowedException;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Forms\AutoRefreshForm;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Session;
+use Icinga\Web\Url;
+use Icinga\Web\UrlParams;
+use Icinga\Web\Widget\Tabs;
+use Icinga\Web\Window;
+
+/**
+ * Base class for all core action controllers
+ *
+ * All Icinga Web core controllers should extend this class
+ *
+ * @method \Icinga\Web\Request getRequest() {
+ * {@inheritdoc}
+ * @return \Icinga\Web\Request
+ * }
+ *
+ * @method \Icinga\Web\Response getResponse() {
+ * {@inheritdoc}
+ * @return \Icinga\Web\Response
+ * }
+ */
+class ActionController extends Zend_Controller_Action
+{
+ use Translation;
+ use PdfExport {
+ sendAsPdf as private newSendAsPdf;
+ }
+
+ /**
+ * The login route to use when requiring authentication
+ */
+ const LOGIN_ROUTE = 'authentication/login';
+
+ /**
+ * The default page title to use
+ */
+ const DEFAULT_TITLE = 'Icinga Web';
+
+ /**
+ * Whether the controller requires the user to be authenticated
+ *
+ * @var bool
+ */
+ protected $requiresAuthentication = true;
+
+ /**
+ * The current module's name
+ *
+ * @var string
+ */
+ protected $moduleName;
+
+ /**
+ * A page's automatic refresh interval
+ *
+ * The initial value will not be subject to a user's preferences.
+ *
+ * @var int
+ */
+ protected $autorefreshInterval;
+
+ protected $reloadCss = false;
+
+ protected $window;
+
+ protected $rerenderLayout = false;
+
+ /**
+ * The inline layout (inside columns) to use
+ *
+ * @var string
+ */
+ protected $inlineLayout = 'inline';
+
+ /**
+ * The inner layout (inside the body) to use
+ *
+ * @var string
+ */
+ protected $innerLayout = 'body';
+
+ /**
+ * Authentication manager
+ *
+ * @var Auth|null
+ */
+ protected $auth;
+
+ /**
+ * URL parameters
+ *
+ * @var UrlParams
+ */
+ protected $params;
+
+ /**
+ * The constructor starts benchmarking, loads the configuration and sets
+ * other useful controller properties
+ *
+ * @param Zend_Controller_Request_Abstract $request
+ * @param Zend_Controller_Response_Abstract $response
+ * @param array $invokeArgs Any additional invocation arguments
+ */
+ public function __construct(
+ Zend_Controller_Request_Abstract $request,
+ Zend_Controller_Response_Abstract $response,
+ array $invokeArgs = array()
+ ) {
+ /** @var \Icinga\Web\Request $request */
+ /** @var \Icinga\Web\Response $response */
+ $this->params = UrlParams::fromQueryString();
+
+ $this->setRequest($request)
+ ->setResponse($response)
+ ->_setInvokeArgs($invokeArgs);
+ $this->_helper = new Zend_Controller_Action_HelperBroker($this);
+
+ $moduleName = $this->getModuleName();
+ $this->view->defaultTitle = static::DEFAULT_TITLE;
+ $this->translationDomain = $moduleName !== 'default' ? $moduleName : 'icinga';
+ $this->view->translationDomain = $this->translationDomain;
+ $this->_helper->layout()->isIframe = $request->getUrl()->shift('isIframe');
+ $this->_helper->layout()->showFullscreen = $request->getUrl()->shift('showFullscreen');
+ $this->_helper->layout()->moduleName = $moduleName;
+
+ $this->view->compact = false;
+ if ($request->getUrl()->getParam('view') === 'compact') {
+ $request->getUrl()->remove('view');
+ $this->view->compact = true;
+ }
+ if ($request->getUrl()->shift('showCompact')) {
+ $this->view->compact = true;
+ }
+ $this->rerenderLayout = $request->getUrl()->shift('renderLayout');
+ if ($request->getUrl()->shift('_disableLayout')) {
+ $this->_helper->layout()->disableLayout();
+ }
+
+ // $auth->authenticate($request, $response, $this->requiresLogin());
+ if ($this->requiresLogin()) {
+ if (! $request->isXmlHttpRequest() && $request->isApiRequest()) {
+ Auth::getInstance()->challengeHttp();
+ }
+ $this->redirectToLogin(Url::fromRequest());
+ }
+
+ $this->view->tabs = new Tabs();
+ $this->prepareInit();
+ $this->init();
+ }
+
+ /**
+ * Prepare controller initialization
+ *
+ * As it should not be required for controllers to call the parent's init() method, base controllers should use
+ * prepareInit() in order to prepare the controller initialization.
+ *
+ * @see \Zend_Controller_Action::init() For the controller initialization method.
+ */
+ protected function prepareInit()
+ {
+ }
+
+ /**
+ * Get the authentication manager
+ *
+ * @return Auth
+ */
+ public function Auth()
+ {
+ if ($this->auth === null) {
+ $this->auth = Auth::getInstance();
+ }
+ return $this->auth;
+ }
+
+ /**
+ * Whether the current user has the given permission
+ *
+ * @param string $permission Name of the permission
+ *
+ * @return bool
+ */
+ public function hasPermission($permission)
+ {
+ return $this->Auth()->hasPermission($permission);
+ }
+
+ /**
+ * Assert that the current user has the given permission
+ *
+ * @param string $permission Name of the permission
+ *
+ * @throws SecurityException If the current user lacks the given permission
+ */
+ public function assertPermission($permission)
+ {
+ if (! $this->Auth()->hasPermission($permission)) {
+ throw new SecurityException('No permission for %s', $permission);
+ }
+ }
+
+ /**
+ * Return the current module's name
+ *
+ * @return string
+ */
+ public function getModuleName()
+ {
+ if ($this->moduleName === null) {
+ $this->moduleName = $this->getRequest()->getModuleName();
+ }
+
+ return $this->moduleName;
+ }
+
+ public function Config($file = null)
+ {
+ if ($file === null) {
+ return Config::app();
+ } else {
+ return Config::app($file);
+ }
+ }
+
+ public function Window()
+ {
+ if ($this->window === null) {
+ $this->window = Window::getInstance();
+ }
+
+ return $this->window;
+ }
+
+ protected function reloadCss()
+ {
+ $this->reloadCss = true;
+ return $this;
+ }
+
+ /**
+ * Respond with HTTP 405 if the current request's method is not one of the given methods
+ *
+ * @param string $httpMethod Unlimited number of allowed HTTP methods
+ *
+ * @throws HttpMethodNotAllowedException If the request method is not one of the given methods
+ */
+ public function assertHttpMethod($httpMethod)
+ {
+ $httpMethods = array_flip(array_map('strtoupper', func_get_args()));
+ if (! isset($httpMethods[$this->getRequest()->getMethod()])) {
+ $e = new HttpMethodNotAllowedException($this->translate('Method Not Allowed'));
+ $e->setAllowedMethods(implode(', ', array_keys($httpMethods)));
+ throw $e;
+ }
+ }
+
+ /**
+ * Return restriction information for an eventually authenticated user
+ *
+ * @param string $name Restriction name
+ *
+ * @return array
+ */
+ public function getRestrictions($name)
+ {
+ return $this->Auth()->getRestrictions($name);
+ }
+
+ /**
+ * Check whether the controller requires a login. That is when the controller requires authentication and the
+ * user is currently not authenticated
+ *
+ * @return bool
+ */
+ protected function requiresLogin()
+ {
+ if (! $this->requiresAuthentication) {
+ return false;
+ }
+
+ return ! $this->Auth()->isAuthenticated();
+ }
+
+ /**
+ * Return the tabs
+ *
+ * @return Tabs
+ */
+ public function getTabs()
+ {
+ return $this->view->tabs;
+ }
+
+ protected function ignoreXhrBody()
+ {
+ if ($this->isXhr()) {
+ $this->getResponse()->setHeader('X-Icinga-Container', 'ignore');
+ }
+ }
+
+ /**
+ * Set the interval (in seconds) at which the page should automatically refresh
+ *
+ * This may be adjusted based on the user's preferences. The result could be a
+ * lower or higher rate of the page's automatic refresh. If this is not desired,
+ * the only way to bypass this is to initialize the {@see ActionController::$autorefreshInterval}
+ * property or to set the `autorefreshInterval` property of the layout directly.
+ *
+ * @param int $interval
+ *
+ * @return $this
+ */
+ public function setAutorefreshInterval($interval)
+ {
+ if (! is_int($interval) || $interval < 1) {
+ throw new ProgrammingError(
+ 'Setting autorefresh interval smaller than 1 second is not allowed'
+ );
+ }
+
+ $user = $this->getRequest()->getUser();
+ if ($user !== null) {
+ $speed = (float) $user->getPreferences()->getValue('icingaweb', 'auto_refresh_speed', 1.0);
+ $interval = max(round($interval * $speed), min($interval, 5));
+ }
+
+ $this->autorefreshInterval = $interval;
+
+ return $this;
+ }
+
+ public function disableAutoRefresh()
+ {
+ $this->autorefreshInterval = null;
+
+ return $this;
+ }
+
+ /**
+ * Redirect to login
+ *
+ * XHR will always redirect to __SELF__ if an URL to redirect to after successful login is set. __SELF__ instructs
+ * JavaScript to redirect to the current window's URL if it's an auto-refresh request or to redirect to the URL
+ * which required login if it's not an auto-refreshing one.
+ *
+ * XHR will respond with HTTP status code 403 Forbidden.
+ *
+ * @param Url|string $redirect URL to redirect to after successful login
+ */
+ protected function redirectToLogin($redirect = null)
+ {
+ $login = Url::fromPath(static::LOGIN_ROUTE);
+ if ($this->isXhr()) {
+ if ($redirect !== null) {
+ $login->setParam('redirect', '__SELF__');
+ }
+
+ $this->_response->setHttpResponseCode(403);
+ } elseif ($redirect !== null) {
+ if (! $redirect instanceof Url) {
+ $redirect = Url::fromPath($redirect);
+ }
+
+ if (($relativeUrl = $redirect->getRelativeUrl())) {
+ $login->setParam('redirect', $relativeUrl);
+ }
+ }
+
+ $this->rerenderLayout()->redirectNow($login);
+ }
+
+ protected function rerenderLayout()
+ {
+ $this->rerenderLayout = true;
+ return $this;
+ }
+
+ public function isXhr()
+ {
+ return $this->getRequest()->isXmlHttpRequest();
+ }
+
+ protected function redirectXhr($url)
+ {
+ $response = $this->getResponse();
+
+ if ($this->reloadCss) {
+ $response->setReloadCss(true);
+ }
+
+ if ($this->rerenderLayout) {
+ $response->setRerenderLayout(true);
+ }
+
+ $response->redirectAndExit($url);
+ }
+
+ protected function redirectHttp($url)
+ {
+ if ($this->isXhr()) {
+ $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes');
+ }
+
+ $this->getResponse()->redirectAndExit($url);
+ }
+
+ /**
+ * Redirect to a specific url, updating the browsers URL field
+ *
+ * @param Url|string $url The target to redirect to
+ **/
+ public function redirectNow($url)
+ {
+ if ($this->isXhr()) {
+ $this->redirectXhr($url);
+ } else {
+ $this->redirectHttp($url);
+ }
+ }
+
+ /**
+ * @see Zend_Controller_Action::preDispatch()
+ */
+ public function preDispatch()
+ {
+ $form = new AutoRefreshForm();
+ if (! $this->getRequest()->isApiRequest()) {
+ $form->handleRequest();
+ }
+ $this->_helper->layout()->autoRefreshForm = $form;
+ }
+
+ /**
+ * Detect whether the current request requires changes in the layout and apply them before rendering
+ *
+ * @see Zend_Controller_Action::postDispatch()
+ */
+ public function postDispatch()
+ {
+ Benchmark::measure('Action::postDispatch()');
+
+ $req = $this->getRequest();
+ $layout = $this->_helper->layout();
+ $layout->innerLayout = $this->innerLayout;
+ $layout->inlineLayout = $this->inlineLayout;
+
+ if ($user = $req->getUser()) {
+ if ((bool) $user->getPreferences()->getValue('icingaweb', 'show_benchmark', false)) {
+ if ($this->_helper->layout()->isEnabled()) {
+ $layout->benchmark = $this->renderBenchmark();
+ }
+ }
+
+ if (! (bool) $user->getPreferences()->getValue('icingaweb', 'auto_refresh', true)) {
+ $this->disableAutoRefresh();
+ }
+ }
+
+ if ($this->autorefreshInterval !== null) {
+ $layout->autorefreshInterval = $this->autorefreshInterval;
+ }
+
+ if ($req->getParam('error_handler') === null && $req->getParam('format') === 'pdf') {
+ $this->sendAsPdf();
+ $this->shutdownSession();
+ exit;
+ }
+
+ if ($this->isXhr()) {
+ $this->postDispatchXhr();
+ }
+
+ $this->shutdownSession();
+ }
+
+ protected function postDispatchXhr()
+ {
+ $resp = $this->getResponse();
+
+ if ($this->reloadCss) {
+ $resp->setReloadCss(true);
+ }
+
+ if ($this->view->title) {
+ if (preg_match('~[\r\n]~', $this->view->title)) {
+ // TODO: Innocent exception and error log for hack attempts
+ throw new IcingaException('No way, guy');
+ }
+ $resp->setHeader(
+ 'X-Icinga-Title',
+ rawurlencode($this->view->title . ' :: ' . $this->view->defaultTitle),
+ true
+ );
+ } else {
+ $resp->setHeader('X-Icinga-Title', rawurlencode($this->view->defaultTitle), true);
+ }
+
+ $layout = $this->_helper->layout();
+ if ($this->rerenderLayout) {
+ $layout->setLayout($this->innerLayout);
+ $resp->setRerenderLayout(true);
+ } else {
+ // The layout may be disabled and there's no indication that the layout is explicitly desired,
+ // that's why we're passing false as second parameter to setLayout
+ $layout->setLayout($this->inlineLayout, false);
+ }
+
+ if ($this->autorefreshInterval !== null) {
+ $resp->setAutoRefreshInterval($this->autorefreshInterval);
+ }
+ }
+
+ protected function sendAsPdf()
+ {
+ if (Module::exists('pdfexport')) {
+ $this->newSendAsPdf();
+ } else {
+ $pdf = new Pdf();
+ $pdf->renderControllerAction($this);
+ }
+ }
+
+ protected function shutdownSession()
+ {
+ $session = Session::getSession();
+ if ($session->hasChanged()) {
+ $session->write();
+ }
+ }
+
+ /**
+ * Render the benchmark
+ *
+ * @return string Benchmark HTML
+ */
+ protected function renderBenchmark()
+ {
+ $this->_helper->viewRenderer->postDispatch();
+ Benchmark::measure('Response ready');
+ return Benchmark::renderToHtml();
+ }
+
+ /**
+ * Try to call compatible methods from older zend versions
+ *
+ * Methods like getParam and redirect are _getParam/_redirect in older Zend versions (which reside for example
+ * in Debian Wheezy). Using those methods without the "_" causes the application to fail on those platforms, but
+ * using the version with "_" forces us to use deprecated code. So we try to catch this issue by looking for methods
+ * with the same name, but with a "_" prefix prepended.
+ *
+ * @param string $name The method name to check
+ * @param mixed $params The method parameters
+ * @return mixed Anything the method returns
+ */
+ public function __call($name, $params)
+ {
+ $deprecatedMethod = '_' . $name;
+
+ if (method_exists($this, $deprecatedMethod)) {
+ return call_user_func_array(array($this, $deprecatedMethod), $params);
+ }
+
+ return parent::__call($name, $params);
+ }
+}
diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php
new file mode 100644
index 0000000..97dc4b3
--- /dev/null
+++ b/library/Icinga/Web/Controller/AuthBackendController.php
@@ -0,0 +1,149 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use ipl\Web\Compat\CompatController;
+use Zend_Controller_Action_Exception;
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\User\UserBackendInterface;
+use Icinga\Authentication\UserGroup\UserGroupBackend;
+use Icinga\Authentication\UserGroup\UserGroupBackendInterface;
+
+/**
+ * Base class for authentication backend controllers
+ */
+class AuthBackendController extends CompatController
+{
+ public function init()
+ {
+ parent::init();
+
+ $this->tabs->disableLegacyExtensions();
+ }
+
+ /**
+ * Redirect to this controller's list action
+ */
+ public function indexAction()
+ {
+ $this->redirectNow($this->getRequest()->getControllerName() . '/list');
+ }
+
+ /**
+ * Return all user backends implementing the given interface
+ *
+ * @param string $interface The class path of the interface, or null if no interface check should be made
+ *
+ * @return array
+ */
+ protected function loadUserBackends($interface = null)
+ {
+ $backends = array();
+ foreach (Config::app('authentication') as $backendName => $backendConfig) {
+ $candidate = UserBackend::create($backendName, $backendConfig);
+ if (! $interface || $candidate instanceof $interface) {
+ $backends[] = $candidate;
+ }
+ }
+
+ return $backends;
+ }
+
+ /**
+ * Return the given user backend or the first match in order
+ *
+ * @param string $name The name of the backend, or null in case the first match should be returned
+ * @param string $interface The interface the backend should implement, no interface check if null
+ *
+ * @return UserBackendInterface
+ *
+ * @throws Zend_Controller_Action_Exception In case the given backend name is invalid
+ */
+ protected function getUserBackend($name = null, $interface = 'Icinga\Data\Selectable')
+ {
+ if ($name !== null) {
+ $config = Config::app('authentication');
+ if (! $config->hasSection($name)) {
+ $this->httpNotFound(sprintf($this->translate('Authentication backend "%s" not found'), $name));
+ } else {
+ $backend = UserBackend::create($name, $config->getSection($name));
+ if ($interface && !$backend instanceof $interface) {
+ $interfaceParts = explode('\\', strtolower($interface));
+ throw new Zend_Controller_Action_Exception(
+ sprintf(
+ $this->translate('Authentication backend "%s" is not %s'),
+ $name,
+ array_pop($interfaceParts)
+ ),
+ 400
+ );
+ }
+ }
+ } else {
+ $backends = $this->loadUserBackends($interface);
+ $backend = array_shift($backends);
+ }
+
+ return $backend;
+ }
+
+ /**
+ * Return all user group backends implementing the given interface
+ *
+ * @param string $interface The class path of the interface, or null if no interface check should be made
+ *
+ * @return array
+ */
+ protected function loadUserGroupBackends($interface = null)
+ {
+ $backends = array();
+ foreach (Config::app('groups') as $backendName => $backendConfig) {
+ $candidate = UserGroupBackend::create($backendName, $backendConfig);
+ if (! $interface || $candidate instanceof $interface) {
+ $backends[] = $candidate;
+ }
+ }
+
+ return $backends;
+ }
+
+ /**
+ * Return the given user group backend or the first match in order
+ *
+ * @param string $name The name of the backend, or null in case the first match should be returned
+ * @param string $interface The interface the backend should implement, no interface check if null
+ *
+ * @return UserGroupBackendInterface
+ *
+ * @throws Zend_Controller_Action_Exception In case the given backend name is invalid
+ */
+ protected function getUserGroupBackend($name = null, $interface = 'Icinga\Data\Selectable')
+ {
+ if ($name !== null) {
+ $config = Config::app('groups');
+ if (! $config->hasSection($name)) {
+ $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $name));
+ } else {
+ $backend = UserGroupBackend::create($name, $config->getSection($name));
+ if ($interface && !$backend instanceof $interface) {
+ $interfaceParts = explode('\\', strtolower($interface));
+ throw new Zend_Controller_Action_Exception(
+ sprintf(
+ $this->translate('User group backend "%s" is not %s'),
+ $name,
+ array_pop($interfaceParts)
+ ),
+ 400
+ );
+ }
+ }
+ } else {
+ $backends = $this->loadUserGroupBackends($interface);
+ $backend = array_shift($backends);
+ }
+
+ return $backend;
+ }
+}
diff --git a/library/Icinga/Web/Controller/BasePreferenceController.php b/library/Icinga/Web/Controller/BasePreferenceController.php
new file mode 100644
index 0000000..8f2da8f
--- /dev/null
+++ b/library/Icinga/Web/Controller/BasePreferenceController.php
@@ -0,0 +1,39 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+/**
+ * Base class for Preference Controllers
+ *
+ * Module preferences use this class to make sure they are automatically
+ * added to the general preferences dialog. If you create a subclass of
+ * BasePreferenceController and overwrite @see init(), make sure you call
+ * parent::init(), otherwise you won't have the $tabs property in your view.
+ *
+ */
+class BasePreferenceController extends ActionController
+{
+ /**
+ * Return an array of tabs provided by this preference controller.
+ *
+ * Those tabs will automatically be added to the application's preference dialog
+ *
+ * @return array
+ */
+ public static function createProvidedTabs()
+ {
+ return array();
+ }
+
+ /**
+ * Initialize the controller and collect all tabs for it from the application and its modules
+ *
+ * @see ActionController::init()
+ */
+ public function init()
+ {
+ parent::init();
+ $this->view->tabs = ControllerTabCollector::collectControllerTabs('PreferenceController');
+ }
+}
diff --git a/library/Icinga/Web/Controller/ControllerTabCollector.php b/library/Icinga/Web/Controller/ControllerTabCollector.php
new file mode 100644
index 0000000..b452a20
--- /dev/null
+++ b/library/Icinga/Web/Controller/ControllerTabCollector.php
@@ -0,0 +1,97 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Application\Icinga;
+use Icinga\Web\Widget\Tabs;
+
+/**
+ * Static helper class that collects tabs provided by the 'createProvidedTabs' method of controllers
+ */
+class ControllerTabCollector
+{
+ /**
+ * Scan all controllers with given name in the application and (loaded) module folders and collects their provided
+ * tabs
+ *
+ * @param string $controllerName The name of the controllers to use for tab collection
+ *
+ * @return Tabs A {@link Tabs} instance containing the application tabs first followed by the
+ * tabs provided from the modules
+ */
+ public static function collectControllerTabs($controllerName)
+ {
+ $controller = '\Icinga\\' . Dispatcher::CONTROLLER_NAMESPACE . '\\' . $controllerName;
+ $applicationTabs = $controller::createProvidedTabs();
+ $moduleTabs = self::collectModuleTabs($controllerName);
+
+ $tabs = new Tabs();
+ foreach ($applicationTabs as $name => $tab) {
+ $tabs->add($name, $tab);
+ }
+
+ foreach ($moduleTabs as $name => $tab) {
+ // Don't overwrite application tabs if the module wants to
+ if ($tabs->has($name)) {
+ continue;
+ }
+ $tabs->add($name, $tab);
+ }
+ return $tabs;
+ }
+
+ /**
+ * Collect module tabs for all modules containing the given controller
+ *
+ * @param string $controller The controller name to use for tab collection
+ *
+ * @return array An array of Tabs objects or arrays containing Tab descriptions
+ */
+ private static function collectModuleTabs($controller)
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ $modules = $moduleManager->listEnabledModules();
+ $tabs = array();
+ foreach ($modules as $module) {
+ $tabs += self::createModuleConfigurationTabs($controller, $moduleManager->getModule($module));
+ }
+
+ return $tabs;
+ }
+
+ /**
+ * Collects the tabs from the createProvidedTabs() method in the configuration controller
+ *
+ * If the module doesn't have the given controller or createProvidedTabs method in the controller an empty array
+ * will be returned
+ *
+ * @param string $controllerName The name of the controller that provides tabs via createProvidedTabs
+ * @param Module $module The module instance that provides the controller
+ *
+ * @return array
+ */
+ private static function createModuleConfigurationTabs($controllerName, Module $module)
+ {
+ // TODO(el): Only works for controllers w/o namepsace: https://dev.icinga.com/issues/4149
+ $controllerDir = $module->getControllerDir();
+ $name = $module->getName();
+
+ $controllerDir = $controllerDir . '/' . $controllerName . '.php';
+ $controllerName = ucfirst($name) . '_' . $controllerName;
+
+ if (is_readable($controllerDir)) {
+ require_once(realpath($controllerDir));
+ if (! method_exists($controllerName, 'createProvidedTabs')) {
+ return array();
+ }
+ $tab = $controllerName::createProvidedTabs();
+ if (! is_array($tab)) {
+ $tab = array($name => $tab);
+ }
+ return $tab;
+ }
+ return array();
+ }
+}
diff --git a/library/Icinga/Web/Controller/Dispatcher.php b/library/Icinga/Web/Controller/Dispatcher.php
new file mode 100644
index 0000000..e2dfb80
--- /dev/null
+++ b/library/Icinga/Web/Controller/Dispatcher.php
@@ -0,0 +1,93 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Exception;
+use Icinga\Util\StringHelper;
+use Zend_Controller_Action;
+use Zend_Controller_Action_Interface;
+use Zend_Controller_Dispatcher_Exception;
+use Zend_Controller_Dispatcher_Standard;
+use Zend_Controller_Request_Abstract;
+use Zend_Controller_Response_Abstract;
+
+/**
+ * Dispatcher supporting Zend-style and namespaced controllers
+ *
+ * Does not support a namespaced default controller in combination w/ the Zend parameter useDefaultControllerAlways.
+ */
+class Dispatcher extends Zend_Controller_Dispatcher_Standard
+{
+ /**
+ * Controller namespace
+ *
+ * @var string
+ */
+ const CONTROLLER_NAMESPACE = 'Controllers';
+
+ /**
+ * Dispatch request to a controller and action
+ *
+ * @param Zend_Controller_Request_Abstract $request
+ * @param Zend_Controller_Response_Abstract $response
+ *
+ * @throws Zend_Controller_Dispatcher_Exception If the controller is not an instance of
+ * Zend_Controller_Action_Interface
+ * @throws Exception If dispatching the request fails
+ */
+ public function dispatch(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response)
+ {
+ $this->setResponse($response);
+ $controllerName = $request->getControllerName();
+ if (! $controllerName) {
+ parent::dispatch($request, $response);
+ return;
+ }
+ $controllerName = StringHelper::cname($controllerName, '-') . 'Controller';
+ $moduleName = $request->getModuleName();
+ if ($moduleName === null || $moduleName === $this->_defaultModule) {
+ $controllerClass = 'Icinga\\' . self::CONTROLLER_NAMESPACE . '\\' . $controllerName;
+ } else {
+ $controllerClass = 'Icinga\\Module\\' . ucfirst($moduleName) . '\\' . self::CONTROLLER_NAMESPACE . '\\'
+ . $controllerName;
+ }
+ if (! class_exists($controllerClass)) {
+ parent::dispatch($request, $response);
+ return;
+ }
+ $controller = new $controllerClass($request, $response, $this->getParams());
+ if (! $controller instanceof Zend_Controller_Action
+ && ! $controller instanceof Zend_Controller_Action_Interface
+ ) {
+ throw new Zend_Controller_Dispatcher_Exception(
+ 'Controller "' . $controllerClass . '" is not an instance of Zend_Controller_Action_Interface'
+ );
+ }
+ $action = $this->getActionMethod($request);
+ $request->setDispatched(true);
+ // Buffer output by default
+ $disableOb = $this->getParam('disableOutputBuffering');
+ $obLevel = ob_get_level();
+ if (empty($disableOb)) {
+ ob_start();
+ }
+ try {
+ $controller->dispatch($action);
+ } catch (Exception $e) {
+ // Clean output buffer on error
+ $curObLevel = ob_get_level();
+ if ($curObLevel > $obLevel) {
+ do {
+ ob_get_clean();
+ $curObLevel = ob_get_level();
+ } while ($curObLevel > $obLevel);
+ }
+ throw $e;
+ }
+ if (empty($disableOb)) {
+ $content = ob_get_clean();
+ $response->appendBody($content);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Controller/ModuleActionController.php b/library/Icinga/Web/Controller/ModuleActionController.php
new file mode 100644
index 0000000..ad66264
--- /dev/null
+++ b/library/Icinga/Web/Controller/ModuleActionController.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Manager;
+use Icinga\Application\Modules\Module;
+
+/**
+ * Base class for module action controllers
+ */
+class ModuleActionController extends ActionController
+{
+ protected $config;
+
+ protected $configs = array();
+
+ protected $module;
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Controller\ActionController For the method documentation.
+ */
+ protected function prepareInit()
+ {
+ $this->moduleInit();
+ if (($this->Auth()->isAuthenticated() || $this->requiresLogin())
+ && $this->getFrontController()->getDefaultModule() !== $this->getModuleName()) {
+ $this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->getModuleName());
+ }
+ }
+
+ /**
+ * Prepare module action controller initialization
+ */
+ protected function moduleInit()
+ {
+ }
+
+ public function Config($file = null)
+ {
+ if ($file === null) {
+ if ($this->config === null) {
+ $this->config = Config::module($this->getModuleName());
+ }
+ return $this->config;
+ } else {
+ if (! array_key_exists($file, $this->configs)) {
+ $this->configs[$file] = Config::module($this->getModuleName(), $file);
+ }
+ return $this->configs[$file];
+ }
+ }
+
+ /**
+ * Return this controller's module
+ *
+ * @return Module
+ */
+ public function Module()
+ {
+ if ($this->module === null) {
+ $this->module = Icinga::app()->getModuleManager()->getModule($this->getModuleName());
+ }
+
+ return $this->module;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Controller\ActionController::postDispatchXhr() For the method documentation.
+ */
+ public function postDispatchXhr()
+ {
+ parent::postDispatchXhr();
+ $this->getResponse()->setHeader('X-Icinga-Module', $this->getModuleName(), true);
+ }
+}
diff --git a/library/Icinga/Web/Controller/StaticController.php b/library/Icinga/Web/Controller/StaticController.php
new file mode 100644
index 0000000..f5ce163
--- /dev/null
+++ b/library/Icinga/Web/Controller/StaticController.php
@@ -0,0 +1,87 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Request;
+
+class StaticController
+{
+ /**
+ * Handle incoming request
+ *
+ * @param Request $request
+ *
+ * @returns void
+ */
+ public function handle(Request $request)
+ {
+ $app = Icinga::app();
+
+ // +4 because strlen('/lib') === 4
+ $assetPath = ltrim(substr($request->getRequestUri(), strlen($request->getBaseUrl()) + 4), '/');
+
+ $library = null;
+ foreach ($app->getLibraries() as $candidate) {
+ if (substr($assetPath, 0, strlen($candidate->getName())) === $candidate->getName()) {
+ $library = $candidate;
+ $assetPath = ltrim(substr($assetPath, strlen($candidate->getName())), '/');
+ break;
+ }
+ }
+
+ if ($library === null) {
+ $app->getResponse()
+ ->setHttpResponseCode(404);
+
+ return;
+ }
+
+ $assetRoot = $library->getStaticAssetPath();
+ if (empty($assetRoot)) {
+ $app->getResponse()
+ ->setHttpResponseCode(404);
+
+ return;
+ }
+
+ $filePath = $assetRoot . DIRECTORY_SEPARATOR . $assetPath;
+ $dirPath = realpath(dirname($filePath)); // dirname, because the file may be a link
+
+ if ($dirPath === false
+ || substr($dirPath, 0, strlen($assetRoot)) !== $assetRoot
+ || ! is_file($filePath)
+ ) {
+ $app->getResponse()
+ ->setHttpResponseCode(404);
+
+ return;
+ }
+
+ $fileStat = stat($filePath);
+ $eTag = sprintf(
+ '%x-%x-%x',
+ $fileStat['ino'],
+ $fileStat['size'],
+ (float) str_pad($fileStat['mtime'], 16, '0')
+ );
+
+ $app->getResponse()->setHeader(
+ 'Cache-Control',
+ 'public, max-age=1814400, stale-while-revalidate=604800',
+ true
+ );
+
+ if ($request->getServer('HTTP_IF_NONE_MATCH') === $eTag) {
+ $app->getResponse()
+ ->setHttpResponseCode(304);
+ } else {
+ $app->getResponse()
+ ->setHeader('ETag', $eTag)
+ ->setHeader('Content-Type', mime_content_type($filePath), true)
+ ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $fileStat['mtime']) . ' GMT')
+ ->setBody(file_get_contents($filePath));
+ }
+ }
+}