diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:30:08 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:30:08 +0000 |
commit | 4ce65d59ca91871cfd126497158200a818720bce (patch) | |
tree | e277def01fc7eba7dbc21c4a4ae5576e8aa2cf1f /vendor/ipl/web/src/Compat | |
parent | Initial commit. (diff) | |
download | icinga-php-library-4ce65d59ca91871cfd126497158200a818720bce.tar.xz icinga-php-library-4ce65d59ca91871cfd126497158200a818720bce.zip |
Adding upstream version 0.13.1.upstream/0.13.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/ipl/web/src/Compat')
-rw-r--r-- | vendor/ipl/web/src/Compat/CompatController.php | 512 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/CompatDecorator.php | 14 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/CompatForm.php | 100 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/Multipart.php | 33 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/SearchControls.php | 260 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/StyleWithNonce.php | 25 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/ViewRenderer.php | 60 |
7 files changed, 1004 insertions, 0 deletions
diff --git a/vendor/ipl/web/src/Compat/CompatController.php b/vendor/ipl/web/src/Compat/CompatController.php new file mode 100644 index 0000000..f4c2fb0 --- /dev/null +++ b/vendor/ipl/web/src/Compat/CompatController.php @@ -0,0 +1,512 @@ +<?php + +namespace ipl\Web\Compat; + +use GuzzleHttp\Psr7\ServerRequest; +use Icinga\Application\Version; +use InvalidArgumentException; +use Icinga\Web\Controller; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlString; +use ipl\Html\ValidHtml; +use ipl\Orm\Query; +use ipl\Stdlib\Contract\Paginatable; +use ipl\Web\Control\LimitControl; +use ipl\Web\Control\PaginationControl; +use ipl\Web\Control\SearchBar; +use ipl\Web\Control\SortControl; +use ipl\Web\Layout\Content; +use ipl\Web\Layout\Controls; +use ipl\Web\Layout\Footer; +use ipl\Web\Url; +use ipl\Web\Widget\Tabs; +use LogicException; +use Psr\Http\Message\ServerRequestInterface; + +class CompatController extends Controller +{ + /** @var Content */ + protected $content; + + /** @var Controls */ + protected $controls; + + /** @var HtmlDocument */ + protected $document; + + /** @var Footer */ + protected $footer; + + /** @var Tabs */ + protected $tabs; + + /** @var array */ + protected $parts; + + protected function prepareInit() + { + parent::prepareInit(); + + $this->params->shift('isIframe'); + $this->params->shift('showFullscreen'); + $this->params->shift('showCompact'); + $this->params->shift('renderLayout'); + $this->params->shift('_disableLayout'); + $this->params->shift('_dev'); + if ($this->params->get('view') === 'compact') { + $this->params->remove('view'); + } + + $this->document = new HtmlDocument(); + $this->document->setSeparator("\n"); + $this->controls = new Controls(); + $this->controls->setAttribute('id', $this->getRequest()->protectId('controls')); + $this->content = new Content(); + $this->content->setAttribute('id', $this->getRequest()->protectId('content')); + $this->footer = new Footer(); + $this->footer->setAttribute('id', $this->getRequest()->protectId('footer')); + $this->tabs = new Tabs(); + $this->tabs->setAttribute('id', $this->getRequest()->protectId('tabs')); + $this->parts = []; + + $this->view->tabs = $this->tabs; + $this->controls->setTabs($this->tabs); + + ViewRenderer::inject(); + + $this->view->document = $this->document; + } + + /** + * Get the current server request + * + * @return ServerRequestInterface + */ + public function getServerRequest() + { + return ServerRequest::fromGlobals(); + } + + /** + * Get the document + * + * @return HtmlDocument + */ + public function getDocument() + { + return $this->document; + } + + /** + * Get the tabs + * + * @return Tabs + */ + public function getTabs() + { + return $this->tabs; + } + + /** + * Add content + * + * @param ValidHtml $content + * + * @return $this + */ + protected function addContent(ValidHtml $content) + { + $this->content->add($content); + + return $this; + } + + /** + * Add a control + * + * @param ValidHtml $control + * + * @return $this + */ + protected function addControl(ValidHtml $control) + { + $this->controls->add($control); + + if ( + $control instanceof PaginationControl + || $control instanceof LimitControl + || $control instanceof SortControl + || $control instanceof SearchBar + ) { + $this->controls->getAttributes() + ->get('class') + ->removeValue('default-layout') + ->addValue('default-layout'); + } + + return $this; + } + + /** + * Add footer + * + * @param ValidHtml $footer + * + * @return $this + */ + protected function addFooter(ValidHtml $footer) + { + $this->footer->add($footer); + + return $this; + } + + /** + * Add a part to be served as multipart-content + * + * If an id is passed the element is used as-is as the part's content. + * Otherwise (no id given) the element's content is used instead. + * + * @param ValidHtml $element + * @param string $id If not given, this is taken from $element + * + * @throws InvalidArgumentException If no id is given and the element also does not have one + * + * @return $this + */ + protected function addPart(ValidHtml $element, $id = null) + { + $part = new Multipart(); + + if ($id === null) { + if (! $element instanceof BaseHtmlElement) { + throw new InvalidArgumentException('If no id is given, $element must be a BaseHtmlElement'); + } + + $id = $element->getAttributes()->get('id')->getValue(); + if (! $id) { + throw new InvalidArgumentException('Element has no id'); + } + + $part->addFrom($element); + } else { + $part->add($element); + } + + $this->parts[] = $part->setFor($id); + + return $this; + } + + /** + * Set the given title as the window's title + * + * @param string $title + * @param mixed ...$args + * + * @return $this + */ + protected function setTitle($title, ...$args) + { + if (! empty($args)) { + $title = vsprintf($title, $args); + } + + $this->view->title = $title; + + return $this; + } + + /** + * Add an active tab with the given title and set it as the window's title too + * + * @param string $title + * @param mixed ...$args + * + * @return $this + */ + protected function addTitleTab($title, ...$args) + { + $this->setTitle($title, ...$args); + + $tabName = uniqid(); + $this->getTabs()->add($tabName, [ + 'label' => $this->view->title, + 'url' => $this->getRequest()->getUrl() + ])->activate($tabName); + + return $this; + } + + /** + * 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}. + * + * @param Paginatable $paginatable + * + * @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 + * @param ?array|string $defaultSort Optional default sort column + * + * @return SortControl + */ + public function createSortControl(Query $query, array $columns): SortControl + { + $sortControl = SortControl::create($columns); + + $this->params->shift($sortControl->getSortParam()); + + $sortControl->handleRequest($this->getServerRequest()); + + $defaultSort = null; + + if (func_num_args() === 3) { + $defaultSort = func_get_args()[2]; + } + + return $sortControl->apply($query, $defaultSort); + } + + /** + * Send a multipart update instead of a standard response + * + * As part of a multipart update, the tabs, content and footer as well as selected controls are + * transmitted in a way the client can render them exclusively instead of a full column reload. + * + * By default the only control included in the response is the pagination control, if added. + * + * @param BaseHtmlElement ...$additionalControls Additional controls to include + * + * @throws LogicException In case an additional control has not been added + */ + public function sendMultipartUpdate(BaseHtmlElement ...$additionalControls) + { + $searchBar = null; + $pagination = null; + $redirectUrl = null; + foreach ($this->controls->getContent() as $control) { + if ($control instanceof PaginationControl) { + $pagination = $control; + } elseif ($control instanceof SearchBar) { + $searchBar = $control; + $redirectUrl = $control->getRedirectUrl(); /** @var Url $redirectUrl */ + } + } + + if ($searchBar !== null && ($changes = $searchBar->getChanges()) !== null) { + $this->addPart(HtmlString::create(json_encode($changes)), 'Behavior:InputEnrichment'); + } + + foreach ($additionalControls as $control) { + $this->addPart($control); + } + + if ($searchBar !== null && $this->content->isEmpty() && ! $searchBar->isValid()) { + // No content and an invalid search bar? That's it then, further updates are not required + return; + } + + if ($this->tabs->count() > 0) { + if ($redirectUrl !== null) { + $this->tabs->setRefreshUrl($redirectUrl); + $this->tabs->getActiveTab()->setUrl($redirectUrl); + + // As long as we still depend on the legacy tab implementation + // there is no other way to influence what the tab extensions + // use as url. (https://github.com/Icinga/icingadb-web/issues/373) + $oldPathInfo = $this->getRequest()->getPathInfo(); + $oldQuery = $_SERVER['QUERY_STRING']; + $this->getRequest()->setPathInfo('/' . $redirectUrl->getPath()); + $_SERVER['QUERY_STRING'] = $redirectUrl->getParams()->toString(); + $this->tabs->ensureAssembled(); + $this->getRequest()->setPathInfo($oldPathInfo); + $_SERVER['QUERY_STRING'] = $oldQuery; + } + + $this->addPart($this->tabs); + } + + if ($pagination !== null) { + if ($redirectUrl !== null) { + $pagination->setUrl(clone $redirectUrl); + } + + $this->addPart($pagination); + } + + if (! $this->content->isEmpty()) { + $this->addPart($this->content); + } + + if (! $this->footer->isEmpty()) { + $this->addPart($this->footer); + } + + if ($redirectUrl !== null) { + $this->getResponse()->setHeader('X-Icinga-Location-Query', $redirectUrl->getQueryString()); + } + } + + /** + * Instruct the client to side-load additional updates + * + * If an item in the given array is indexed by an integer, its value will be used by the client to refresh + * the parent of the element identified by it. The value is expected to be a valid CSS selector such + * as `.foo`, `#foo`. If indexed by a string, the client will use this index to identify a container (by id) and + * will use the value (a URL) to load content into it. Since Icinga Web >= 2.12, the indices can be specified with + * or without the `#` indicator. If you require compatibility with older Icinga Web versions, you have to specify + * the indices (container ids) without the `#` char. + * + * @param array $updates + * + * @return void + */ + public function sendExtraUpdates(array $updates) + { + if (empty($updates)) { + return; + } + + $extraUpdates = []; + foreach ($updates as $key => $value) { + if (is_int($key)) { + $extraUpdates[] = $value; + } else { + $extraUpdates[] = sprintf( + '%s;%s', + $key, + $value instanceof Url ? $value->getAbsoluteUrl() : $value + ); + } + } + + $this->getResponse()->setHeader('X-Icinga-Extra-Updates', join(',', $extraUpdates)); + } + + /** + * Close the modal content and refresh the related view + * + * NOTE: If you use this with older Icinga Web versions (< 2.12), you will need to specify a valid redirect url, + * that will produce the same result as using the `__REFRESH__` redirect with the latest Icinga Web version. + * + * This is supposed to be used in combination with a modal view and closes only the modal, + * and refreshes the modal opener (regardless of whether it is col1 or col2). + * + * @param Url|string $url + * @param bool $refreshCol1 Whether to refresh col1 after the redirect. Is just for compatibility reasons and + * won't be used with latest Icinga Web versions. + * + * @return never + */ + public function closeModalAndRefreshRelatedView($url, bool $refreshCol1 = false) + { + if (version_compare(Version::VERSION, '2.12.0', '<')) { + if (! $url) { + throw new InvalidArgumentException('No redirect url provided'); + } + + if ($refreshCol1) { + $this->sendExtraUpdates(['#col1']); + } + + $this->redirectNow($url); + } else { + $this->redirectNow('__REFRESH__'); + } + } + + /** + * Close the modal content and refresh all the remaining views + * + * NOTE: If you use this with older Icinga Web versions (< 2.12), you will need to specify a valid redirect url, + * that will produce the same result as using the `__REFRESH__` redirect with the latest Icinga Web version. + * + * This is supposed to be used in combination with a modal view and closes only the modal content. It refreshes + * the modal opener (expects to be always col2) and forces a refresh of col1. + * + * @param Url|string $url + * + * @return never + */ + public function closeModalAndRefreshRemainingViews($url) + { + $this->sendExtraUpdates(['#col1']); + + $this->closeModalAndRefreshRelatedView($url); + } + + /** + * Redirect using `__CLOSE__` + * + * Change to a single column layout and refresh col1 + * + * @return never + */ + public function switchToSingleColumnLayout() + { + $this->redirectNow('__CLOSE__'); + } + + public function postDispatch() + { + if (empty($this->parts)) { + if (! $this->content->isEmpty()) { + $this->document->prepend($this->content); + + if (! $this->view->compact && ! $this->controls->isEmpty()) { + $this->document->prepend($this->controls); + } + + if (! $this->footer->isEmpty()) { + $this->document->add($this->footer); + } + } + } else { + $partSeparator = base64_encode(random_bytes(16)); + $this->getResponse()->setHeader('X-Icinga-Multipart-Content', $partSeparator); + + $this->document->setSeparator("\n$partSeparator\n"); + $this->document->add($this->parts); + } + + parent::postDispatch(); + } +} diff --git a/vendor/ipl/web/src/Compat/CompatDecorator.php b/vendor/ipl/web/src/Compat/CompatDecorator.php new file mode 100644 index 0000000..856b758 --- /dev/null +++ b/vendor/ipl/web/src/Compat/CompatDecorator.php @@ -0,0 +1,14 @@ +<?php + +namespace ipl\Web\Compat; + +use ipl\Web\FormDecorator\IcingaFormDecorator; + +/** + * Compat form element decorator based on div elements + * + * @deprecated Use {@see \ipl\Web\FormDecorator\IcingaFormDecorator} instead + */ +class CompatDecorator extends IcingaFormDecorator +{ +} diff --git a/vendor/ipl/web/src/Compat/CompatForm.php b/vendor/ipl/web/src/Compat/CompatForm.php new file mode 100644 index 0000000..97ad10c --- /dev/null +++ b/vendor/ipl/web/src/Compat/CompatForm.php @@ -0,0 +1,100 @@ +<?php + +namespace ipl\Web\Compat; + +use http\Exception\InvalidArgumentException; +use ipl\Html\Contract\FormSubmitElement; +use ipl\Html\Form; +use ipl\Html\FormElement\SubmitButtonElement; +use ipl\Html\FormElement\SubmitElement; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlString; +use ipl\I18n\Translation; +use ipl\Web\FormDecorator\IcingaFormDecorator; + +class CompatForm extends Form +{ + use Translation; + + protected $defaultAttributes = ['class' => 'icinga-form icinga-controls']; + + /** + * Render the content of the element to HTML + * + * A duplicate of the primary submit button is being prepended if there is more than one present + * + * @return string + */ + public function renderContent(): string + { + if (count($this->submitElements) > 1) { + return (new HtmlDocument()) + ->setHtmlContent( + $this->duplicateSubmitButton($this->submitButton), + new HtmlString(parent::renderContent()) + ) + ->render(); + } + + return parent::renderContent(); + } + + public function hasDefaultElementDecorator() + { + if (parent::hasDefaultElementDecorator()) { + return true; + } + + $this->setDefaultElementDecorator(new IcingaFormDecorator()); + + return true; + } + + protected function ensureDefaultElementLoaderRegistered() + { + if (! $this->defaultElementLoaderRegistered) { + $this->addPluginLoader( + 'element', + 'ipl\\Web\\FormElement', + 'Element' + ); + + parent::ensureDefaultElementLoaderRegistered(); + } + + return $this; + } + + /** + * Return a duplicate of the given submit button with the `class` attribute fixed to `primary-submit-btn-duplicate` + * + * @param FormSubmitElement $originalSubmitButton + * + * @return FormSubmitElement + */ + public function duplicateSubmitButton(FormSubmitElement $originalSubmitButton): FormSubmitElement + { + $attributes = (clone $originalSubmitButton->getAttributes()) + ->set('class', 'primary-submit-btn-duplicate'); + $attributes->remove('id'); + // Remove to avoid `type="submit submit"` in SubmitButtonElement + $attributes->remove('type'); + + if ($originalSubmitButton instanceof SubmitElement) { + $newSubmitButton = new SubmitElement($originalSubmitButton->getName(), $attributes); + $newSubmitButton->setLabel($originalSubmitButton->getButtonLabel()); + + return $newSubmitButton; + } elseif ($originalSubmitButton instanceof SubmitButtonElement) { + $newSubmitButton = new SubmitButtonElement($originalSubmitButton->getName(), $attributes); + $newSubmitButton->setSubmitValue($originalSubmitButton->getSubmitValue()); + + return $newSubmitButton; + } + + throw new InvalidArgumentException(sprintf( + 'Cannot duplicate submit button of type "%s"', + get_class($originalSubmitButton) + )); + } +} diff --git a/vendor/ipl/web/src/Compat/Multipart.php b/vendor/ipl/web/src/Compat/Multipart.php new file mode 100644 index 0000000..432f837 --- /dev/null +++ b/vendor/ipl/web/src/Compat/Multipart.php @@ -0,0 +1,33 @@ +<?php + +namespace ipl\Web\Compat; + +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlString; + +class Multipart extends HtmlDocument +{ + /** @var string */ + protected $for; + + protected $contentSeparator = "\n"; + + /** + * Set the container's id which this part is for + * + * @param string $id + * + * @return $this + */ + public function setFor($id) + { + $this->for = $id; + + return $this; + } + + protected function assemble() + { + $this->prepend(HtmlString::create(sprintf('for=%s', $this->for))); + } +} diff --git a/vendor/ipl/web/src/Compat/SearchControls.php b/vendor/ipl/web/src/Compat/SearchControls.php new file mode 100644 index 0000000..f6e74ab --- /dev/null +++ b/vendor/ipl/web/src/Compat/SearchControls.php @@ -0,0 +1,260 @@ +<?php + +namespace ipl\Web\Compat; + +use GuzzleHttp\Psr7\ServerRequest; +use ipl\Html\Html; +use ipl\Orm\Exception\InvalidRelationException; +use ipl\Orm\Query; +use ipl\Stdlib\Seq; +use ipl\Web\Control\SearchBar; +use ipl\Web\Control\SearchEditor; +use ipl\Web\Filter\QueryString; +use ipl\Web\Url; +use ipl\Stdlib\Filter; + +trait SearchControls +{ + /** + * Fetch available filter columns for the given query + * + * @param Query $query + * + * @return array<string, string> Keys are column paths, values are labels + */ + public function fetchFilterColumns(Query $query) + { + $columns = []; + foreach ($query->getResolver()->getColumnDefinitions($query->getModel()) as $name => $definition) { + $columns[$name] = $definition->getLabel(); + } + + return $columns; + } + + /** + * Get whether {@see SearchControls::createSearchBar()} and {@see SearchControls::createSearchEditor()} + * should handle form submits. + * + * @return bool + */ + private function callHandleRequest() + { + return true; + } + + /** + * Create and return the SearchBar + * + * @param Query $query The query being filtered + * @param Url $redirectUrl Url to redirect to upon success + * @param array $preserveParams Query params to preserve when redirecting + * + * @return SearchBar + */ + public function createSearchBar(Query $query, ...$params): SearchBar + { + $requestUrl = Url::fromRequest(); + $preserveParams = array_pop($params) ?? []; + $redirectUrl = array_pop($params); + + if ($redirectUrl !== null) { + $redirectUrl->addParams($requestUrl->onlyWith($preserveParams)->getParams()->toArray(false)); + } else { + $redirectUrl = $requestUrl->onlyWith($preserveParams); + } + + $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->setRedirectUrl($redirectUrl); + $searchBar->setAction($redirectUrl->getAbsoluteUrl()); + $searchBar->setIdProtector([$this->getRequest(), 'protectId']); + $searchBar->addWrapper(Html::tag('div', ['class' => 'search-controls'])); + + $moduleName = $this->getRequest()->getModuleName(); + $controllerName = $this->getRequest()->getControllerName(); + + if (method_exists($this, 'completeAction')) { + $searchBar->setSuggestionUrl(Url::fromPath( + "$moduleName/$controllerName/complete", + ['_disableLayout' => true, 'showCompact' => true] + )); + } + + if (method_exists($this, 'searchEditorAction')) { + $searchBar->setEditorUrl(Url::fromPath( + "$moduleName/$controllerName/search-editor" + )->setParams($redirectUrl->getParams())); + } + + $filterColumns = $this->fetchFilterColumns($query); + $columnValidator = function (SearchBar\ValidatedColumn $column) use ($query, $filterColumns) { + $searchPath = $column->getSearchValue(); + if (strpos($searchPath, '.') === false) { + $column->setSearchValue($query->getResolver()->qualifyPath( + $searchPath, + $query->getModel()->getTableAlias() + )); + } + + try { + $definition = $query->getResolver()->getColumnDefinition($searchPath); + } catch (InvalidRelationException $_) { + list($columnPath, $columnLabel) = Seq::find($filterColumns, $searchPath, false); + if ($columnPath === null) { + $column->setMessage(t('Is not a valid column')); + $column->setSearchValue($searchPath); // Resets the qualification made above + } else { + $column->setSearchValue($columnPath); + $column->setLabel($columnLabel); + } + } + + if (isset($definition)) { + $column->setLabel($definition->getLabel()); + } + }; + + $searchBar->on(SearchBar::ON_ADD, $columnValidator) + ->on(SearchBar::ON_INSERT, $columnValidator) + ->on(SearchBar::ON_SAVE, $columnValidator) + ->on(SearchBar::ON_SENT, function (SearchBar $form) { + /** @var Url $redirectUrl */ + $redirectUrl = $form->getRedirectUrl(); + $redirectUrl->setFilter($form->getFilter()); + $form->setRedirectUrl($redirectUrl); + })->on(SearchBar::ON_SUCCESS, function (SearchBar $form) { + $this->getResponse()->redirectAndExit($form->getRedirectUrl()); + }); + + if ($this->callHandleRequest()) { + $searchBar->handleRequest(ServerRequest::fromGlobals()); + } + + return $searchBar; + } + + /** + * Create and return the SearchEditor + * + * @param Query $query The query being filtered + * @param Url $redirectUrl Url to redirect to upon success + * @param array $preserveParams Query params to preserve when redirecting + * + * @return SearchEditor + */ + public function createSearchEditor(Query $query, ...$params): SearchEditor + { + $requestUrl = Url::fromRequest(); + $preserveParams = array_pop($params) ?? []; + $redirectUrl = array_pop($params); + $moduleName = $this->getRequest()->getModuleName(); + $controllerName = $this->getRequest()->getControllerName(); + + if ($redirectUrl !== null) { + $redirectUrl->addParams($requestUrl->onlyWith($preserveParams)->getParams()->toArray(false)); + } else { + $redirectUrl = Url::fromPath("$moduleName/$controllerName"); + if (! empty($preserveParams)) { + $redirectUrl->setParams($requestUrl->onlyWith($preserveParams)->getParams()); + } + } + + $editor = new SearchEditor(); + $editor->setRedirectUrl($redirectUrl); + $editor->setAction($requestUrl->getAbsoluteUrl()); + $editor->setQueryString((string) $this->params->without($preserveParams)); + + if (method_exists($this, 'completeAction')) { + $editor->setSuggestionUrl(Url::fromPath( + "$moduleName/$controllerName/complete", + ['_disableLayout' => true, 'showCompact' => true] + )); + } + + $editor->getParser()->on(QueryString::ON_CONDITION, function (Filter\Condition $condition) use ($query) { + if ($condition->getColumn()) { + $this->enrichFilterCondition($condition, $query); + } + }); + + $filterColumns = $this->fetchFilterColumns($query); + $editor->on(SearchEditor::ON_VALIDATE_COLUMN, function ( + Filter\Condition $condition + ) use ( + $query, + $filterColumns + ) { + $searchPath = $condition->getColumn(); + if (strpos($searchPath, '.') === false) { + $condition->setColumn($query->getResolver()->qualifyPath( + $searchPath, + $query->getModel()->getTableAlias() + )); + } + + try { + $query->getResolver()->getColumnDefinition($searchPath); + } catch (InvalidRelationException $_) { + $columnPath = Seq::findKey( + $filterColumns, + $condition->metaData()->get('columnLabel', $searchPath), + false + ); + if ($columnPath === null) { + $condition->setColumn($searchPath); + throw new SearchBar\SearchException(t('Is not a valid column')); + } else { + $condition->setColumn($columnPath); + } + } + })->on(SearchEditor::ON_SUCCESS, function (SearchEditor $form) { + /** @var Url $redirectUrl */ + $redirectUrl = $form->getRedirectUrl(); + $redirectUrl->setFilter($form->getFilter()); + + $this->getResponse() + ->setHeader('X-Icinga-Container', '_self') + ->redirectAndExit($redirectUrl); + }); + + if ($this->callHandleRequest()) { + $editor->handleRequest(ServerRequest::fromGlobals()); + } + + return $editor; + } + + /** + * 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()->getTableAlias()); + $condition->setColumn($path); + } + + try { + $label = $query->getResolver()->getColumnDefinition($path)->getLabel(); + } catch (InvalidRelationException $_) { + $label = null; + } + + if (isset($label)) { + $condition->metaData()->set('columnLabel', $label); + } + } +} diff --git a/vendor/ipl/web/src/Compat/StyleWithNonce.php b/vendor/ipl/web/src/Compat/StyleWithNonce.php new file mode 100644 index 0000000..f4c7185 --- /dev/null +++ b/vendor/ipl/web/src/Compat/StyleWithNonce.php @@ -0,0 +1,25 @@ +<?php + +namespace ipl\Web\Compat; + +use Icinga\Application\Version; +use Icinga\Util\Csp; +use ipl\Web\Style; + +/** + * Use this class to define inline style which is compatible + * with Icinga Web < 2.12 and with CSP support in >= 2.12 + */ +class StyleWithNonce extends Style +{ + public function getNonce(): ?string + { + if ($this->nonce === null) { + $this->nonce = version_compare(Version::VERSION, '2.12.0', '>=') + ? Csp::getStyleNonce() ?? '' + : ''; + } + + return parent::getNonce(); + } +} diff --git a/vendor/ipl/web/src/Compat/ViewRenderer.php b/vendor/ipl/web/src/Compat/ViewRenderer.php new file mode 100644 index 0000000..48ddcc3 --- /dev/null +++ b/vendor/ipl/web/src/Compat/ViewRenderer.php @@ -0,0 +1,60 @@ +<?php + +namespace ipl\Web\Compat; + +use Zend_Controller_Action_Helper_ViewRenderer as Zf1ViewRenderer; +use Zend_Controller_Action_HelperBroker as Zf1HelperBroker; + +class ViewRenderer extends Zf1ViewRenderer +{ + /** + * Inject the view renderer + */ + public static function inject() + { + /** @var \Zend_Controller_Action_Helper_ViewRenderer $viewRenderer */ + $viewRenderer = Zf1HelperBroker::getStaticHelper('ViewRenderer'); + + $inject = new static(); + + foreach (get_object_vars($viewRenderer) as $property => $value) { + if ($property === '_inflector') { + continue; + } + + $inject->$property = $value; + } + + Zf1HelperBroker::removeHelper('ViewRenderer'); + Zf1HelperBroker::addHelper($inject); + } + + public function getName() + { + return 'ViewRenderer'; + } + + /** + * Render the view w/o using a view script + * + * {@inheritdoc} + */ + public function render($action = null, $name = null, $noController = null) + { + $view = $this->view; + + if ($view->document->isEmpty() || $this->getRequest()->getParam('error_handler') !== null) { + parent::render($action, $name, $noController); + + return; + } + + if ($name === null) { + $name = $this->getResponseSegment(); + } + + $this->getResponse()->appendBody($view->document->render(), $name); + + $this->setNoRender(); + } +} |