filter === null) { $this->filter = QueryString::parse((string) $this->params); } return $this->filter; } /** * Create and return the LimitControl * * This automatically shifts the limit URL parameter from {@link $params}. * * @return LimitControl */ public function createLimitControl(): LimitControl { $limitControl = new LimitControl(Url::fromRequest()); $limitControl->setDefaultLimit($this->getPageSize(null)); $this->params->shift($limitControl->getLimitParam()); return $limitControl; } /** * Create and return the PaginationControl * * This automatically shifts the pagination URL parameters from {@link $params}. * * @return PaginationControl */ public function createPaginationControl(Paginatable $paginatable): PaginationControl { $paginationControl = new PaginationControl($paginatable, Url::fromRequest()); $paginationControl->setDefaultPageSize($this->getPageSize(null)); $paginationControl->setAttribute('id', $this->getRequest()->protectId('pagination-control')); $this->params->shift($paginationControl->getPageParam()); $this->params->shift($paginationControl->getPageSizeParam()); return $paginationControl->apply(); } /** * Create and return the SortControl * * This automatically shifts the sort URL parameter from {@link $params}. * * @param Query $query * @param array $columns Possible sort columns as sort string-label pairs * * @return SortControl */ public function createSortControl(Query $query, array $columns): SortControl { $default = (array) $query->getModel()->getDefaultSort(); $normalized = []; foreach ($columns as $key => $value) { $normalized[SortUtil::normalizeSortSpec($key)] = $value; } $sortControl = (new SortControl(Url::fromRequest())) ->setColumns($normalized); if (! empty($default)) { $sortControl->setDefault(SortUtil::normalizeSortSpec($default)); } $sort = $sortControl->getSort(); if (! empty($sort)) { $query->orderBy(SortUtil::createOrderBy($sort)); } $this->params->shift($sortControl->getSortParam()); return $sortControl; } /** * Create and return the SearchBar * * @param Query $query The query being filtered * @param array $preserveParams Query params to preserve when redirecting * * @return SearchBar */ public function createSearchBar(Query $query, array $preserveParams = null): SearchBar { $requestUrl = Url::fromRequest(); $redirectUrl = $preserveParams !== null ? $requestUrl->onlyWith($preserveParams) : (clone $requestUrl)->setParams([]); $filter = QueryString::fromString((string) $this->params) ->on(QueryString::ON_CONDITION, function (Filter\Condition $condition) use ($query) { $this->enrichFilterCondition($condition, $query); }) ->parse(); $searchBar = new SearchBar(); $searchBar->setFilter($filter); $searchBar->setAction($requestUrl->getAbsoluteUrl()); $searchBar->setIdProtector([$this->getRequest(), 'protectId']); if (method_exists($this, 'completeAction')) { $searchBar->setSuggestionUrl(Url::fromPath( 'icingadb/' . $this->getRequest()->getControllerName() . '/complete', ['_disableLayout' => true, 'showCompact' => true] )); } if (method_exists($this, 'searchEditorAction')) { $searchBar->setEditorUrl(Url::fromPath( 'icingadb/' . $this->getRequest()->getControllerName() . '/search-editor' )->setParams($redirectUrl->getParams())); } $metaData = iterator_to_array( ObjectSuggestions::collectFilterColumns($query->getModel(), $query->getResolver()) ); $columnValidator = function (SearchBar\ValidatedColumn $column) use ($query, $metaData) { $columnPath = $column->getSearchValue(); if (($pos = strpos($columnPath, '.vars.')) !== false) { try { $relationPath = $query->getResolver()->qualifyPath( substr($columnPath, 0, $pos + 5), $query->getModel()->getTableName() ); $query->getResolver()->resolveRelation($relationPath); } catch (InvalidRelationException $e) { $column->setMessage(sprintf( t('"%s" is not a valid relation'), substr($e->getRelation(), 0, $pos) )); } } else { if (strpos($columnPath, '.') === false) { $columnPath = $query->getResolver()->qualifyPath($columnPath, $query->getModel()->getTableName()); } if (! isset($metaData[$columnPath])) { list($columnPath, $columnLabel) = Seq::find($metaData, $column->getSearchValue(), false); if ($columnPath === null) { $column->setMessage(t('Is not a valid column')); } else { $column->setSearchValue($columnPath); $column->setLabel($columnLabel); } } else { $column->setLabel($metaData[$columnPath]); } } }; $searchBar->on(SearchBar::ON_ADD, $columnValidator) ->on(SearchBar::ON_INSERT, $columnValidator) ->on(SearchBar::ON_SAVE, $columnValidator) ->on(SearchBar::ON_SENT, function (SearchBar $form) use ($redirectUrl) { $existingParams = $redirectUrl->getParams(); $redirectUrl->setQueryString(QueryString::render($form->getFilter())); foreach ($existingParams->toArray(false) as $name => $value) { if (is_int($name)) { $name = $value; $value = true; } $redirectUrl->getParams()->addEncoded($name, $value); } $form->setRedirectUrl($redirectUrl); })->on(SearchBar::ON_SUCCESS, function (SearchBar $form) { $this->getResponse()->redirectAndExit($form->getRedirectUrl()); })->handleRequest(ServerRequest::fromGlobals()); Html::tag('div', ['class' => 'filter'])->wrap($searchBar); return $searchBar; } /** * Create and return the SearchEditor * * @param Query $query The query being filtered * @param array $preserveParams Query params to preserve when redirecting * * @return SearchEditor */ public function createSearchEditor(Query $query, array $preserveParams = null): SearchEditor { $requestUrl = Url::fromRequest(); $redirectUrl = Url::fromPath('icingadb/' . $this->getRequest()->getControllerName()); if (! empty($preserveParams)) { $redirectUrl->setParams($requestUrl->onlyWith($preserveParams)->getParams()); } $editor = new SearchEditor(); $editor->setQueryString((string) $this->params->without($preserveParams)); $editor->setAction($requestUrl->getAbsoluteUrl()); if (method_exists($this, 'completeAction')) { $editor->setSuggestionUrl(Url::fromPath( 'icingadb/' . $this->getRequest()->getControllerName() . '/complete', ['_disableLayout' => true, 'showCompact' => true] )); } $editor->getParser()->on(QueryString::ON_CONDITION, function (Filter\Condition $condition) use ($query) { if ($condition->getColumn()) { $this->enrichFilterCondition($condition, $query); } }); $metaData = iterator_to_array( ObjectSuggestions::collectFilterColumns($query->getModel(), $query->getResolver()) ); $editor->on(SearchEditor::ON_VALIDATE_COLUMN, function (Filter\Condition $condition) use ($query, $metaData) { $column = $condition->getColumn(); if (($pos = strpos($column, '.vars.')) !== false) { try { $query->getResolver()->resolveRelation(substr($column, 0, $pos + 5)); } catch (InvalidRelationException $e) { throw new SearchBar\SearchException(sprintf( t('"%s" is not a valid relation'), substr($e->getRelation(), 0, $pos) )); } } else { if (! isset($metaData[$column])) { $path = Seq::findKey($metaData, $condition->metaData()->get('columnLabel', $column), false); if ($path === null) { throw new SearchBar\SearchException(t('Is not a valid column')); } else { $condition->setColumn($path); } } } })->on(SearchEditor::ON_SUCCESS, function (SearchEditor $form) use ($redirectUrl) { $existingParams = $redirectUrl->getParams(); $redirectUrl->setQueryString(QueryString::render($form->getFilter())); foreach ($existingParams->toArray(false) as $name => $value) { if (is_int($name)) { $name = $value; $value = true; } $redirectUrl->getParams()->addEncoded($name, $value); } $this->getResponse() ->setHeader('X-Icinga-Container', '_self') ->redirectAndExit($redirectUrl); })->handleRequest(ServerRequest::fromGlobals()); return $editor; } /** * Create and return a ContinueWith * * This will automatically be appended to the SearchBar's wrapper. It's not necessary * to add it separately as control or content! * * @param Url $detailsUrl * @param SearchBar $searchBar * * @return ContinueWith */ public function createContinueWith(Url $detailsUrl, SearchBar $searchBar): ContinueWith { $continueWith = new ContinueWith($detailsUrl, [$searchBar, 'getFilter']); $continueWith->setTitle(t('Show bulk processing actions for all filtered results')); $continueWith->setBaseTarget('_next'); $continueWith->getAttributes() ->set('id', $this->getRequest()->protectId('continue-with')); $searchBar->getWrapper()->add($continueWith); return $continueWith; } /** * Create and return the ViewModeSwitcher * * This automatically shifts the view mode URL parameter from {@link $params}. * * @param PaginationControl $paginationControl * @param LimitControl $limitControl * @param bool $verticalPagination * * @return ViewModeSwitcher */ public function createViewModeSwitcher( PaginationControl $paginationControl, LimitControl $limitControl, bool $verticalPagination = false ): ViewModeSwitcher { $viewModeSwitcher = new ViewModeSwitcher(); $viewModeSwitcher->setIdProtector([$this->getRequest(), 'protectId']); $user = $this->Auth()->getUser(); if (($preferredModes = $user->getAdditional('icingadb.view_modes')) === null) { try { $preferredModes = Json::decode( $user->getPreferences()->getValue('icingadb', 'view_modes', '[]'), true ); } catch (JsonDecodeException $e) { Logger::error('Failed to load preferred view modes for user "%s": %s', $user->getUsername(), $e); $preferredModes = []; } $user->setAdditional('icingadb.view_modes', $preferredModes); } $requestRoute = $this->getRequest()->getUrl()->getPath(); if (isset($preferredModes[$requestRoute])) { $viewModeSwitcher->setDefaultViewMode($preferredModes[$requestRoute]); } $viewModeSwitcher->populate([ $viewModeSwitcher->getViewModeParam() => $this->params->shift($viewModeSwitcher->getViewModeParam()) ]); $session = $this->Window()->getSessionNamespace( 'icingadb-viewmode-' . $this->Window()->getContainerId() ); $viewModeSwitcher->on( ViewModeSwitcher::ON_SUCCESS, function (ViewModeSwitcher $viewModeSwitcher) use ( $user, $preferredModes, $paginationControl, $verticalPagination, &$session ) { $viewMode = $viewModeSwitcher->getValue($viewModeSwitcher->getViewModeParam()); $requestUrl = Url::fromRequest(); $preferredModes[$requestUrl->getPath()] = $viewMode; $user->setAdditional('icingadb.view_modes', $preferredModes); try { $preferencesStore = PreferencesStore::create(new ConfigObject([ 'store' => Config::app()->get('global', 'config_backend', 'db'), 'resource' => Config::app()->get('global', 'config_resource') ]), $user); $preferencesStore->load(); $preferencesStore->save( new Preferences(['icingadb' => ['view_modes' => Json::encode($preferredModes)]]) ); } catch (Exception $e) { Logger::error('Failed to save preferred view mode for user "%s": %s', $user->getUsername(), $e); } $pageParam = $paginationControl->getPageParam(); $limitParam = LimitControl::DEFAULT_LIMIT_PARAM; $currentPage = $paginationControl->getCurrentPageNumber(); $requestUrl->setParam($viewModeSwitcher->getViewModeParam(), $viewMode); if (! $requestUrl->hasParam($limitParam)) { if ($viewMode === 'minimal') { $session->set('previous_page', $currentPage); $session->set('request_path', $requestUrl->getPath()); $limit = $paginationControl->getLimit(); if (! $verticalPagination) { // We are computing it based on the first element being rendered on this current page $currentPage = (int) (floor((($currentPage * $limit) - $limit) / ($limit * 2)) + 1); } else { $currentPage = (int) (round($currentPage * $limit / ($limit * 2))); } $session->set('current_page', $currentPage); } elseif ($viewModeSwitcher->getDefaultViewMode() === 'minimal') { $limit = $paginationControl->getLimit(); if ($currentPage === $session->get('current_page')) { // No other page numbers have been selected, i.e the user only // switches back and forth without changing the page numbers $currentPage = $session->get('previous_page'); } elseif (! $verticalPagination) { $currentPage = (int) (floor((($currentPage * $limit) - $limit) / ($limit / 2)) + 1); } else { $currentPage = (int) (floor($currentPage * $limit / ($limit / 2))); } $session->clear(); } if (($requestUrl->hasParam($pageParam) && $currentPage > 1) || $currentPage > 1) { $requestUrl->setParam($pageParam, $currentPage); } else { $requestUrl->remove($pageParam); } } $this->redirectNow($requestUrl); } )->handleRequest(ServerRequest::fromGlobals()); if ($viewModeSwitcher->getViewMode() === 'minimal') { $hasLimitParam = Url::fromRequest()->hasParam($limitControl->getLimitParam()); if ($paginationControl->getDefaultPageSize() <= LimitControl::DEFAULT_LIMIT && ! $hasLimitParam) { $paginationControl->setDefaultPageSize($paginationControl->getDefaultPageSize() * 2); $limitControl->setDefaultLimit($limitControl->getDefaultLimit() * 2); $paginationControl->apply(); } } $requestPath = $session->get('request_path'); if ($requestPath && $requestPath !== $requestRoute) { $session->clear(); } return $viewModeSwitcher; } /** * Process a search request * * @param Query $query * * @return void */ public function handleSearchRequest(Query $query) { $q = trim($this->params->shift('q', ''), ' *'); if (! $q) { return; } $filter = Filter::any(); foreach ($query->getModel()->getSearchColumns() as $column) { $filter->add(Filter::equal($column, "*$q*")); } $requestUrl = Url::fromRequest(); $existingParams = $requestUrl->getParams()->without('q'); $requestUrl->setQueryString(QueryString::render($filter)); foreach ($existingParams->toArray(false) as $name => $value) { $requestUrl->getParams()->addEncoded($name, $value); } $this->getResponse()->redirectAndExit($requestUrl); } /** * Require permission to access the given route * * @param string $name If NULL, the current controller name is used * * @throws SecurityException */ public function assertRouteAccess(string $name = null) { if (! $name) { $name = $this->getRequest()->getControllerName(); } if (! $this->isPermittedRoute($name)) { throw new SecurityException('No permission to access this route'); } } public function export(Query ...$queries) { if ($this->format === 'sql') { foreach ($queries as $query) { list($sql, $values) = $query->getDb()->getQueryBuilder()->assembleSelect($query->assembleSelect()); $unused = []; foreach ($values as $value) { $pos = strpos($sql, '?'); if ($pos !== false) { if (is_string($value)) { $value = "'" . $value . "'"; } $sql = substr_replace($sql, $value, $pos, 1); } else { $unused[] = $value; } } if (!empty($unused)) { $sql .= ' /* Unused values: "' . join('", "', $unused) . '" */'; } $this->content->add(Html::tag('pre', $sql)); } return true; } if ($this->format === 'json' || $this->format === 'csv') { $isJsonFormat = $this->format === 'json'; $response = $this->getResponse(); $fileName = $this->view->title; if ($isJsonFormat) { $response ->setHeader('Content-Type', 'application/json') ->setHeader('Cache-Control', 'no-store') ->setHeader( 'Content-Disposition', 'attachment; filename=' . $fileName . '.json' ) ->sendResponse(); } else { $response ->setHeader('Content-Type', 'text/csv') ->setHeader('Cache-Control', 'no-store') ->setHeader( 'Content-Disposition', 'attachment; filename=' . $fileName . '.csv' ) ->sendResponse(); } ob_end_clean(); Environment::raiseExecutionTime(); $query = $queries[0]; $query->limit(Url::fromRequest()->getParam('limit')); $col = array_merge((array) $query->getModel()->getKeyName(), $query->getModel()->getColumns()); $tableName = $query->getModel()->getTableName(); foreach ($query->getWith() as $relationPath => $relation) { $relatedCols = $relation->getTarget()->getColumns(); foreach ($relatedCols as $alias => $name) { if (is_int($alias)) { $alias = $name; } $col[] = $relationPath . '.' . $alias; } } if ($isJsonFormat) { echo '['; } $rs = $query->execute()->disableCache(); foreach ($rs as $i => $row) { $result = []; if ($i > 0) { $separator = $isJsonFormat ? ',' : "\r\n"; echo $separator; } foreach ($col as $alias => $name) { if (is_int($alias)) { $alias = $name; } if (strpos($alias, '.') !== false) { $properties = array_slice(explode('.', $alias), 1); $alias = implode('.', $properties); $val = $row; do { $column = array_shift($properties); $val = $val->$column; } while (! empty($properties) && $val !== null); } else { $val = $row->$alias; } if ( $alias === 'id' || substr($alias, -3) === '_id' || substr($alias, -9) === '_checksum' || substr($alias, -4) === '_bin' ) { $val = base64_encode($val); } if ($isJsonFormat) { // Json $result[$alias] = $val; } elseif (is_bool($val)) { // CSV $result[$alias] = $val ? 'true' : 'false'; } elseif (is_string($val)) { $result[$alias] = '"' . str_replace('"', '""', $val) . '"'; } else { $result[$alias] = $val; } } if ($i === 0 && ! $isJsonFormat) { echo implode(',', array_keys($result)) . "\r\n"; } $data = $isJsonFormat ? Json::sanitize($result) : implode(',', array_values($result)); echo $data; } if ($isJsonFormat) { echo ']'; } exit; } $this->getTabs()->enableDataExports(); } /** * @todo Consider making this the default in Icinga Web 2 */ protected function sendAsPdf() { if (! Icinga::app()->getModuleManager()->has('pdfexport')) { throw new ConfigurationError('The pdfexport module is required for exports to PDF'); } putenv('ICINGAWEB_EXPORT_FORMAT=pdf'); Environment::raiseMemoryLimit('512M'); Environment::raiseExecutionTime(300); $time = DateFormatter::formatDateTime(time()); $doc = (new PrintableHtmlDocument()) ->setTitle($this->view->title) ->setHeader(Html::wantHtml([ Html::tag('span', ['class' => 'title']), Html::tag('time', null, $time) ])) ->setFooter(Html::wantHtml([ Html::tag('span', null, [ t('Page') . ' ', Html::tag('span', ['class' => 'pageNumber']), ' / ', Html::tag('span', ['class' => 'totalPages']) ]), Html::tag('p', null, Url::fromRequest()->setParams($this->params)) ])) ->addHtml($this->content); $doc->getAttributes()->add('class', 'icinga-module module-icingadb'); Pdfexport::first()->streamPdfFromHtml($doc, sprintf( '%s-%s', $this->view->title ?: $this->getRequest()->getActionName(), $time )); } public function dispatch($action) { // Notify helpers of action preDispatch state $this->_helper->notifyPreDispatch(); $this->preDispatch(); if ($this->getRequest()->isDispatched()) { // If pre-dispatch hooks introduced a redirect then stop dispatch // @see ZF-7496 if (! $this->getResponse()->isRedirect()) { $interceptable = $this->$action(); if ($interceptable instanceof Generator) { foreach ($interceptable as $stopSignal) { if ($stopSignal === true) { $this->formatProcessed = true; break; } } } } $this->postDispatch(); } // whats actually important here is that this action controller is // shutting down, regardless of dispatching; notify the helpers of this // state $this->_helper->notifyPostDispatch(); } /** * Enrich the filter condition with meta data from the query * * @param Filter\Condition $condition * @param Query $query * * @return void */ protected function enrichFilterCondition(Filter\Condition $condition, Query $query) { $path = $condition->getColumn(); if (strpos($path, '.') === false) { $path = $query->getResolver()->qualifyPath($path, $query->getModel()->getTableName()); $condition->setColumn($path); } if (strpos($path, '.vars.') !== false) { list($target, $varName) = explode('.vars.', $path); if (strpos($target, '.') === false) { // Programmatically translated since the full definition is available in class ObjectSuggestions $condition->metaData()->set( 'columnLabel', sprintf(t(ucfirst($target) . ' %s', '..'), $varName) ); } } else { $label = Seq::findValue( ObjectSuggestions::collectFilterColumns($query->getModel(), $query->getResolver()), $path ); if ($label !== null) { $condition->metaData()->set('columnLabel', $label); } } } protected function addContent(ValidHtml $content) { if ($content instanceof BaseItemList) { $this->content->getAttributes()->add('class', 'full-width'); } return parent::addContent($content); } public function filter(Query $query, Filter\Rule $filter = null): self { if ($this->format !== 'sql' || $this->hasPermission('config/authentication/roles/show')) { $this->applyRestrictions($query); } if ($query instanceof UnionQuery) { foreach ($query->getUnions() as $query) { $query->filter($filter ?: $this->getFilter()); } } else { $query->filter($filter ?: $this->getFilter()); } return $this; } public function preDispatch() { parent::preDispatch(); $this->format = $this->params->shift('format'); } public function postDispatch() { if (! $this->formatProcessed && $this->format !== null && $this->format !== 'pdf') { // The purpose of this is not only to show that a requested format isn't supported. // It's main purpose is to not allow to bypass restrictions with `?format=sql` as // it may be possible that an action applies restrictions, but doesn't support any // output formats. Since the restrictions are bypassed in method `$this->filter()` // for the SQL output format and the actual format processing is part of a different // method (`$this->export()`) which needs to be called explicitly by an action, // it's otherwise possible for bad individuals to access unrestricted data. $this->httpBadRequest(t('This route does not support the requested output format')); } parent::postDispatch(); } protected function moduleInit() { Icinga::app()->getFrontController() ->getPlugin('Zend_Controller_Plugin_ErrorHandler') ->setErrorHandlerModule('icingadb'); } }