summaryrefslogtreecommitdiffstats
path: root/vendor/ipl/web/src/Compat
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/ipl/web/src/Compat')
-rw-r--r--vendor/ipl/web/src/Compat/CompatController.php512
-rw-r--r--vendor/ipl/web/src/Compat/CompatDecorator.php14
-rw-r--r--vendor/ipl/web/src/Compat/CompatForm.php100
-rw-r--r--vendor/ipl/web/src/Compat/Multipart.php33
-rw-r--r--vendor/ipl/web/src/Compat/SearchControls.php260
-rw-r--r--vendor/ipl/web/src/Compat/StyleWithNonce.php25
-rw-r--r--vendor/ipl/web/src/Compat/ViewRenderer.php60
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 &lt; 2.12 and with CSP support in &gt;= 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();
+ }
+}