diff options
Diffstat (limited to '')
-rw-r--r-- | library/Icinga/Web/Controller.php | 264 | ||||
-rw-r--r-- | library/Icinga/Web/Controller/ActionController.php | 587 | ||||
-rw-r--r-- | library/Icinga/Web/Controller/AuthBackendController.php | 149 | ||||
-rw-r--r-- | library/Icinga/Web/Controller/BasePreferenceController.php | 39 | ||||
-rw-r--r-- | library/Icinga/Web/Controller/ControllerTabCollector.php | 97 | ||||
-rw-r--r-- | library/Icinga/Web/Controller/Dispatcher.php | 93 | ||||
-rw-r--r-- | library/Icinga/Web/Controller/ModuleActionController.php | 80 | ||||
-rw-r--r-- | library/Icinga/Web/Controller/StaticController.php | 87 |
8 files changed, 1396 insertions, 0 deletions
diff --git a/library/Icinga/Web/Controller.php b/library/Icinga/Web/Controller.php new file mode 100644 index 0000000..a2730d5 --- /dev/null +++ b/library/Icinga/Web/Controller.php @@ -0,0 +1,264 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Web; + +use Icinga\Data\Filterable; +use Icinga\Data\Sortable; +use Icinga\Data\QueryInterface; +use Icinga\Exception\Http\HttpBadRequestException; +use Icinga\Exception\Http\HttpNotFoundException; +use Icinga\Web\Controller\ModuleActionController; +use Icinga\Web\Widget\Limiter; +use Icinga\Web\Widget\Paginator; +use Icinga\Web\Widget\SortBox; + +/** + * This is the controller all modules should inherit from + * We will flip code with the ModuleActionController as soon as a couple + * of pending feature branches are merged back to the master. + * + * @property View $view + */ +class Controller extends ModuleActionController +{ + /** + * Cache for page size configured via user preferences + * + * @var false|int + */ + protected $userPageSize; + + /** + * @see ActionController::init + */ + public function init() + { + parent::init(); + $this->handleSortControlSubmit(); + } + + /** + * Check whether the sort control has been submitted and redirect using GET parameters + */ + protected function handleSortControlSubmit() + { + $request = $this->getRequest(); + if (! $request->isPost()) { + return; + } + + if (($sort = $request->getPost('sort')) || ($direction = $request->getPost('dir'))) { + $url = Url::fromRequest(); + if ($sort) { + $url->setParam('sort', $sort); + $url->remove('dir'); + } else { + $url->setParam('dir', $direction); + } + + $this->redirectNow($url); + } + } + + /** + * Immediately respond w/ HTTP 400 + * + * @param string $message Exception message or exception format string + * @param mixed ...$arg Format string argument + * + * @throws HttpBadRequestException + */ + public function httpBadRequest($message) + { + throw HttpBadRequestException::create(func_get_args()); + } + + /** + * Immediately respond w/ HTTP 404 + * + * @param string $message Exception message or exception format string + * @param mixed ...$arg Format string argument + * + * @throws HttpNotFoundException + */ + public function httpNotFound($message) + { + throw HttpNotFoundException::create(func_get_args()); + } + + /** + * Render the given form using a simple view script + * + * @param Form $form + * @param string $tab + */ + public function renderForm(Form $form, $tab) + { + $this->getTabs()->add(uniqid(), array( + 'active' => true, + 'label' => $tab, + 'url' => Url::fromRequest() + )); + $this->view->form = $form; + $this->render('simple-form', null, true); + } + + /** + * Create a SortBox widget and apply its sort rules on the given query + * + * The widget is set on the `sortBox' view property only if the current view has not been requested as compact + * + * @param array $columns An array containing the sort columns, with the + * submit value as the key and the label as the value + * @param Sortable $query Query to apply the user chosen sort rules on + * @param array $defaults An array containing default sort directions for specific columns + * + * @return $this + */ + protected function setupSortControl(array $columns, Sortable $query = null, array $defaults = null) + { + $request = $this->getRequest(); + $sortBox = SortBox::create('sortbox-' . $request->getActionName(), $columns, $defaults); + $sortBox->setRequest($request); + + if ($query) { + $sortBox->setQuery($query); + $sortBox->handleRequest($request); + } + + if (! $this->view->compact) { + $this->view->sortBox = $sortBox; + } + + return $this; + } + + /** + * Create a Limiter widget at the `limiter' view property + * + * In case the current view has been requested as compact this method does nothing. + * + * @param int $itemsPerPage Default number of items per page + * + * @return $this + */ + protected function setupLimitControl($itemsPerPage = 25) + { + if (! $this->view->compact) { + $this->view->limiter = new Limiter(); + $this->view->limiter->setDefaultLimit($this->getPageSize($itemsPerPage)); + } + + return $this; + } + + /** + * Get the page size configured via user preferences or return the default value + * + * @param int $default + * + * @return int + */ + protected function getPageSize($default) + { + if ($this->userPageSize === null) { + $user = $this->Auth()->getUser(); + if ($user !== null) { + $pageSize = $user->getPreferences()->getValue('icingaweb', 'default_page_size'); + $this->userPageSize = $pageSize ? (int) $pageSize : false; + } else { + $this->userPageSize = false; + } + } + + return $this->userPageSize !== false ? $this->userPageSize : $default; + } + + /** + * Apply the given page limit and number on the given query and setup a paginator for it + * + * The $itemsPerPage and $pageNumber parameters are only applied if not available in the current request. + * The paginator is set on the `paginator' view property only if the current view has not been requested as compact. + * + * @param QueryInterface $query The query to create a paginator for + * @param int $itemsPerPage Default number of items per page + * @param int $pageNumber Default page number + * + * @return $this + */ + protected function setupPaginationControl(QueryInterface $query, $itemsPerPage = 25, $pageNumber = 0) + { + $request = $this->getRequest(); + $limit = $request->getParam('limit', $this->getPageSize($itemsPerPage)); + $page = $request->getParam('page', $pageNumber); + $query->limit($limit, $page > 0 ? ($page - 1) * $limit : 0); + + if (! $this->view->compact) { + $paginator = new Paginator(); + $paginator->setQuery($query); + $this->view->paginator = $paginator; + } + + return $this; + } + + /** + * Create a FilterEditor widget and apply the user's chosen filter options on the given filterable + * + * The widget is set on the `filterEditor' view property only if the current view has not been requested as compact. + * The optional $filterColumns parameter should be an array of key-value pairs where the key is the name of the + * column and the value the label to show to the user. The optional $searchColumns parameter should be an array + * of column names to be used to handle quick searches. + * + * If the given filterable is an instance of Icinga\Data\FilterColumns, $filterable->getFilterColumns() and + * $filterable->getSearchColumns() is called to provide the respective columns if $filterColumns or $searchColumns + * is not given. + * + * @param Filterable $filterable The filterable to create a filter editor for + * @param array $filterColumns The filter columns to offer to the user + * @param array $searchColumns The search columns to utilize for quick searches + * @param array $preserveParams The url parameters to preserve + * + * @return $this + * + * @todo Preserving and ignoring parameters should be configurable (another two method params? property magic?) + */ + protected function setupFilterControl( + Filterable $filterable, + array $filterColumns = null, + array $searchColumns = null, + array $preserveParams = null + ) { + $defaultPreservedParams = array( + 'limit', // setupPaginationControl() + 'sort', // setupSortControl() + 'dir', // setupSortControl() + 'backend', // Framework + 'showCompact', // Framework + '_dev' // Framework + ); + + $editor = Widget::create('filterEditor'); + /** @var \Icinga\Web\Widget\FilterEditor $editor */ + call_user_func_array( + array($editor, 'preserveParams'), + array_merge($defaultPreservedParams, $preserveParams ?: array()) + ); + + $editor + ->setQuery($filterable) + ->ignoreParams('page') // setupPaginationControl() + ->setColumns($filterColumns) + ->setSearchColumns($searchColumns) + ->handleRequest($this->getRequest()); + + if ($this->view->compact) { + $editor->setVisible(false); + } + + $this->view->filterEditor = $editor; + + return $this; + } +} 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)); + } + } +} |