diff options
Diffstat (limited to 'vendor/ipl/web/src/Compat')
-rw-r--r-- | vendor/ipl/web/src/Compat/CompatController.php | 329 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/CompatDecorator.php | 14 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/CompatForm.php | 25 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/Multipart.php | 33 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/SearchControls.php | 269 | ||||
-rw-r--r-- | vendor/ipl/web/src/Compat/ViewRenderer.php | 60 |
6 files changed, 730 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..08e69f1 --- /dev/null +++ b/vendor/ipl/web/src/Compat/CompatController.php @@ -0,0 +1,329 @@ +<?php + +namespace ipl\Web\Compat; + +use GuzzleHttp\Psr7\ServerRequest; +use InvalidArgumentException; +use Icinga\Web\Controller; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlString; +use ipl\Html\ValidHtml; +use ipl\Web\Control\PaginationControl; +use ipl\Web\Control\SearchBar; +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); + + 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; + } + + /** + * 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->getParams()->toString()); + } + } + + 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..b759b8f --- /dev/null +++ b/vendor/ipl/web/src/Compat/CompatForm.php @@ -0,0 +1,25 @@ +<?php + +namespace ipl\Web\Compat; + +use ipl\Html\Form; +use ipl\I18n\Translation; +use ipl\Web\FormDecorator\IcingaFormDecorator; + +class CompatForm extends Form +{ + use Translation; + + protected $defaultAttributes = ['class' => 'icinga-form icinga-controls']; + + public function hasDefaultElementDecorator() + { + if (parent::hasDefaultElementDecorator()) { + return true; + } + + $this->setDefaultElementDecorator(new IcingaFormDecorator()); + + return true; + } +} 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..c40204d --- /dev/null +++ b/vendor/ipl/web/src/Compat/SearchControls.php @@ -0,0 +1,269 @@ +<?php + +namespace ipl\Web\Compat; + +use GuzzleHttp\Psr7\ServerRequest; +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 + */ + abstract public function fetchFilterColumns(Query $query); + + /** + * 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']); + + $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()->getTableName() + )); + } + + 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(); + $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()); + }); + + 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()->getTableName() + )); + } + + 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(); + $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); + }); + + 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()->getTableName()); + $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/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(); + } +} |