diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:44:51 +0000 |
commit | a1ec78bf0dc93d0e05e5f066f1949dc3baecea06 (patch) | |
tree | ee596ce1bc9840661386f96f9b8d1f919a106317 /vendor/gipfl/icingaweb2 | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-incubator-upstream/0.20.0.tar.xz icingaweb2-module-incubator-upstream/0.20.0.zip |
Adding upstream version 0.20.0.upstream/0.20.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gipfl/icingaweb2')
34 files changed, 3917 insertions, 0 deletions
diff --git a/vendor/gipfl/icingaweb2/composer.json b/vendor/gipfl/icingaweb2/composer.json new file mode 100644 index 0000000..25a757f --- /dev/null +++ b/vendor/gipfl/icingaweb2/composer.json @@ -0,0 +1,21 @@ +{ + "name": "gipfl/icingaweb2", + "type": "library", + "description": "Helpers and glue for Icinga Web 2", + "homepage": "https://github.com/gipfl/icingaweb2", + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\IcingaWeb2\\": "src/" + } + }, + "require": { + "php": ">=5.6", + "ext-ctype": "*", + "gipfl/format": ">=0.3", + "gipfl/translation": ">=0.1", + "ipl/html": ">=0.2.1" + } +} diff --git a/vendor/gipfl/icingaweb2/public/css/11-action-bar.less b/vendor/gipfl/icingaweb2/public/css/11-action-bar.less new file mode 100644 index 0000000..ab3d737 --- /dev/null +++ b/vendor/gipfl/icingaweb2/public/css/11-action-bar.less @@ -0,0 +1,105 @@ +div.gipfl-action-bar { + > * { + vertical-align: middle; + } + .pagination-control { + float: none; + clear: none; + display: inline-block; + line-height: inherit; + margin: 0; + } + + form input { + margin: 0; + } +} + +div.gipfl-action-bar a, div.gipfl-action-bar form i { + color: @icinga-blue; +} + +div.gipfl-action-bar a.badge { + color: inherit; +} + +div.gipfl-action-bar > a { + margin-right: 1em; +} + +#layout.twocols div.gipfl-action-bar .pagination-control { + li { + display: none; + } + + li:nth-child(1), li.active, li:last-child { + display: list-item; + } + + li.active a { + border-bottom: none; + } +} + +/** Dropdown in action bar **/ +div.gipfl-action-bar ul { + padding: 0; + margin: 0; + + li { + list-style-type: none; + a { display: block; } + } +} + +div.gipfl-action-bar > ul { + display: inline-block; + > li { + display: inline-block; + } + ul { + padding: 0.5em 1em 0 0; + min-width: 10em; + position: absolute; + display: none; + background-color: @menu-flyout-bg-color; + border: 1px solid @gray-light; + box-shadow: 0.3em 0.3em 0.3em 0 rgba(0, 0, 0, 0.2); + + a { + display: block; + padding: 0.3em 0.5em 0.3em 1em; + margin: 0; + outline: none; + color: @menu-flyout-color; + + &:hover { + text-decoration: underline; + &:before { + text-decoration: none; + } + } + } + + li.active a { + font-weight: bold; + } + } + > li:hover ul { + display: block; + z-index: 2; + } + > li > a { + padding: 0.2em 0.5em; + } + > li:hover { + background-color: @tr-active-color; + border-top-left-radius: 0.1em; + border-top-right-radius: 0.1em; + & > a { + color: @icinga-blue; + text-decoration: none; + } + } +} +/** END of dropdown in action bar **/ diff --git a/vendor/gipfl/icingaweb2/public/css/12-quicksearch.less b/vendor/gipfl/icingaweb2/public/css/12-quicksearch.less new file mode 100644 index 0000000..a5da7e9 --- /dev/null +++ b/vendor/gipfl/icingaweb2/public/css/12-quicksearch.less @@ -0,0 +1,17 @@ +form.gipfl-quicksearch { + display: block; + input.search { + width: 8em; + min-width: unset; + border: none; + background-color: inherit; + padding-left: 2em; + margin-left: 1.5em; + font-size: 0.75em; + font-weight: normal; + &:focus { + width: 16em; + border: none; + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/CompatController.php b/vendor/gipfl/icingaweb2/src/CompatController.php new file mode 100644 index 0000000..952b4b5 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/CompatController.php @@ -0,0 +1,581 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use gipfl\IcingaWeb2\Controller\Extension\AutoRefreshHelper; +use gipfl\IcingaWeb2\Controller\Extension\ConfigHelper; +use gipfl\Translation\StaticTranslator; +use gipfl\Translation\TranslationHelper; +use gipfl\IcingaWeb2\Controller\Extension\ControlsAndContentHelper; +use gipfl\IcingaWeb2\Widget\ControlsAndContent; +use gipfl\IcingaWeb2\Zf1\SimpleViewRenderer; +use GuzzleHttp\Psr7\ServerRequest; +use Icinga\Application\Benchmark; +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Manager; +use Icinga\Application\Modules\Module; +use Icinga\Authentication\Auth; +use Icinga\Exception\ProgrammingError; +use Icinga\File\Pdf; +use Icinga\Security\SecurityException; +use Icinga\User; +use Icinga\Web\Notification; +use Icinga\Web\Session; +use Icinga\Web\UrlParams; +use Icinga\Web\Url as WebUrl; +use Icinga\Web\Window; +use InvalidArgumentException; +use Psr\Http\Message\ServerRequestInterface; +use RuntimeException; +use Zend_Controller_Action; +use Zend_Controller_Action_HelperBroker as ZfHelperBroker; +use Zend_Controller_Request_Abstract as ZfRequest; +use Zend_Controller_Response_Abstract as ZfResponse; + +/** + * Class CompatController + * @method \Icinga\Web\Request getRequest() { + * {@inheritdoc} + * @return \Icinga\Web\Request + * } + * + * @method \Icinga\Web\Response getResponse() { + * {@inheritdoc} + * @return \Icinga\Web\Response + * } + */ +class CompatController extends Zend_Controller_Action implements ControlsAndContent +{ + use AutoRefreshHelper; + use ControlsAndContentHelper; + use ConfigHelper; + use TranslationHelper; + + /** @var bool Whether the controller requires the user to be authenticated */ + protected $requiresAuthentication = true; + + protected $applicationName = 'Icinga Web'; + + /** @var string The current module's name */ + private $moduleName; + + private $module; + + private $window; + + // https://github.com/joshbuchea/HEAD + + /** @var SimpleViewRenderer */ + protected $viewRenderer; + + /** @var bool */ + private $reloadCss = false; + + /** @var bool */ + private $rerenderLayout = false; + + /** @var string */ + private $xhrLayout = 'inline'; + + /** @var \Zend_Layout */ + private $layout; + + /** @var string The inner layout (inside the body) to use */ + private $innerLayout = 'body'; + + /** + * Authentication manager + * + * @var Auth|null + */ + private $auth; + + /** @var UrlParams */ + protected $params; + + /** + * The constructor starts benchmarking, loads the configuration and sets + * other useful controller properties + * + * @param ZfRequest $request + * @param ZfResponse $response + * @param array $invokeArgs Any additional invocation arguments + * @throws SecurityException + */ + public function __construct( + ZfRequest $request, + ZfResponse $response, + array $invokeArgs = array() + ) { + /** @var \Icinga\Web\Request $request */ + /** @var \Icinga\Web\Response $response */ + $this->setRequest($request) + ->setResponse($response) + ->_setInvokeArgs($invokeArgs); + + $this->prepareViewRenderer(); + $this->_helper = new ZfHelperBroker($this); + + $this->handlerBrowserWindows(); + $this->initializeTranslator(); + $this->initializeLayout(); + + $this->view->compact = $request->getParam('view') === 'compact'; + $url = $this->url(); + $this->params = $url->getParams(); + + if ($url->shift('showCompact')) { + $this->view->compact = true; + } + + $this->checkPermissionBasics(); + Benchmark::measure('Ready to initialize the controller'); + $this->prepareInit(); + $this->init(); + } + + protected function initializeLayout() + { + $url = $this->url(); + $layout = $this->layout = $this->_helper->layout(); + $layout->isIframe = $url->shift('isIframe'); + $layout->showFullscreen = $url->shift('showFullscreen'); + $layout->moduleName = $this->getModuleName(); + if ($this->rerenderLayout = $url->shift('renderLayout')) { + $this->xhrLayout = $this->innerLayout; + } + if ($url->shift('_disableLayout')) { + $this->layout->disableLayout(); + } + } + + /** + * Prepare controller initialization + * + * As it should not be required for controllers to call the parent's init() method, + * base controllers should use prepareInit() in order to prepare the controller + * initialization. + * + * @see \Zend_Controller_Action::init() For the controller initialization method. + */ + protected function prepareInit() + { + } + + /** + * Return the current module's name + * + * @return string + */ + public function getModuleName() + { + if ($this->moduleName === null) { + $this->moduleName = $this->getRequest()->getModuleName(); + } + + return $this->moduleName; + } + + protected function setModuleName($name) + { + $this->moduleName = $name; + + return $this; + } + + /** + * Return this controller's module + * + * @return Module + * @codingStandardsIgnoreStart + */ + public function Module() + { + // @codingStandardsIgnoreEnd + if ($this->module === null) { + try { + $this->module = Icinga::app()->getModuleManager()->getModule($this->getModuleName()); + } catch (ProgrammingError $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } + + return $this->module; + } + + /** + * @return Window + * @codingStandardsIgnoreStart + */ + public function Window() + { + // @codingStandardsIgnoreEnd + if ($this->window === null) { + $this->window = new Window( + $this->getRequestHeader('X-Icinga-WindowId', Window::UNDEFINED) + ); + } + return $this->window; + } + + protected function handlerBrowserWindows() + { + if ($this->isXhr()) { + $id = $this->getRequestHeader('X-Icinga-WindowId', Window::UNDEFINED); + + if ($id === Window::UNDEFINED) { + $this->window = new Window($id); + $this->_response->setHeader('X-Icinga-WindowId', Window::generateId()); + } + } + } + + /** + * @return ServerRequestInterface + */ + protected function getServerRequest() + { + return ServerRequest::fromGlobals(); + } + + protected function getRequestHeader($key, $default = null) + { + try { + $value = $this->getRequest()->getHeader($key); + } catch (\Zend_Controller_Request_Exception $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + + if ($value === null) { + return $default; + } else { + return $value; + } + } + + /** + * @throws SecurityException + */ + protected function checkPermissionBasics() + { + $request = $this->getRequest(); + // $auth->authenticate($request, $response, $this->requiresLogin()); + if ($this->requiresLogin()) { + if (! $request->isXmlHttpRequest() && $request->isApiRequest()) { + Auth::getInstance()->challengeHttp(); + } + $this->redirectToLogin(Url::fromRequest()); + } + if (($this->Auth()->isAuthenticated() || $this->requiresLogin()) + && $this->getFrontController()->getDefaultModule() !== $this->getModuleName()) { + $this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->getModuleName()); + } + } + + protected function initializeTranslator() + { + $moduleName = $this->getModuleName(); + $domain = $moduleName !== 'default' ? $moduleName : 'icinga'; + $this->view->translationDomain = $domain; + StaticTranslator::set(new Translator($domain)); + } + + public function init() + { + // Hint: we intentionally do not call our parent's init() method + } + + /** + * Get the authentication manager + * + * @return Auth + * @codingStandardsIgnoreStart + */ + public function Auth() + { + // @codingStandardsIgnoreEnd + if ($this->auth === null) { + $this->auth = Auth::getInstance(); + } + return $this->auth; + } + + /** + * Whether the current user has the given permission + * + * @param string $permission Name of the permission + * + * @return bool + */ + public function hasPermission($permission) + { + return $this->Auth()->hasPermission($permission); + } + + /** + * Assert that the current user has the given permission + * + * @param string $permission Name of the permission + * + * @throws SecurityException If the current user lacks the given permission + */ + public function assertPermission($permission) + { + if (! $this->Auth()->hasPermission($permission)) { + throw new SecurityException('No permission for %s', $permission); + } + } + + /** + * Return restriction information for an eventually authenticated user + * + * @param string $name Restriction name + * + * @return array + */ + public function getRestrictions($name) + { + return $this->Auth()->getRestrictions($name); + } + + /** + * Check whether the controller requires a login. That is when the controller requires authentication and the + * user is currently not authenticated + * + * @return bool + */ + protected function requiresLogin() + { + if (! $this->requiresAuthentication) { + return false; + } + + return ! $this->Auth()->isAuthenticated(); + } + + public function prepareViewRenderer() + { + $this->viewRenderer = new SimpleViewRenderer(); + $this->viewRenderer->replaceZendViewRenderer(); + $this->view = $this->viewRenderer->view; + } + + /** + * @return SimpleViewRenderer + */ + public function getViewRenderer() + { + return $this->viewRenderer; + } + + protected function redirectXhr($url) + { + if (! $url instanceof WebUrl) { + $url = Url::fromPath($url); + } + + if ($this->rerenderLayout) { + $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes'); + } + if ($this->reloadCss) { + $this->getResponse()->setHeader('X-Icinga-Reload-Css', 'now'); + } + + $this->shutdownSession(); + + $this->getResponse() + ->setHeader('X-Icinga-Redirect', rawurlencode($url->getAbsoluteUrl())) + ->sendHeaders(); + + exit; + } + + /** + * Detect whether the current request requires changes in the layout and apply them before rendering + * + * @see Zend_Controller_Action::postDispatch() + */ + public function postDispatch() + { + Benchmark::measure('Action::postDispatch()'); + + $layout = $this->layout; + $req = $this->getRequest(); + $layout->innerLayout = $this->innerLayout; + + /** @var User $user */ + if ($user = $req->getUser()) { + if ((bool) $user->getPreferences()->getValue('icingaweb', 'show_benchmark', false)) { + if ($layout->isEnabled()) { + $layout->benchmark = $this->renderBenchmark(); + } + } + + if (! (bool) $user->getPreferences()->getValue('icingaweb', 'auto_refresh', true)) { + $this->disableAutoRefresh(); + } + } + + if ($req->getParam('format') === 'pdf') { + $this->shutdownSession(); + $this->sendAsPdf(); + return; + } + + if ($this->isXhr()) { + $this->postDispatchXhr(); + } + + $this->shutdownSession(); + } + + public function postDispatchXhr() + { + $this->layout->setLayout($this->xhrLayout); + $resp = $this->getResponse(); + + $notifications = Notification::getInstance(); + if ($notifications->hasMessages()) { + $notificationList = array(); + foreach ($notifications->popMessages() as $m) { + $notificationList[] = rawurlencode($m->type . ' ' . $m->message); + } + $resp->setHeader('X-Icinga-Notification', implode('&', $notificationList), true); + } + + if ($this->reloadCss) { + $resp->setHeader('X-Icinga-CssReload', 'now', true); + } + + if ($this->title) { + if (preg_match('~[\r\n]~', $this->title)) { + // TODO: Innocent exception and error log for hack attempts + throw new InvalidArgumentException('No newlines allowed in page title'); + } + $resp->setHeader( + 'X-Icinga-Title', + // TODO: Config + rawurlencode($this->title . ' :: ' . $this->applicationName), + true + ); + } else { + // TODO: config + $resp->setHeader('X-Icinga-Title', rawurlencode($this->applicationName), true); + } + + if ($this->rerenderLayout) { + $this->getResponse()->setHeader('X-Icinga-Container', 'layout', true); + } + + if (isset($this->autorefreshInterval)) { + $resp->setHeader('X-Icinga-Refresh', $this->autorefreshInterval, true); + } + + if ($name = $this->getModuleName()) { + $this->getResponse()->setHeader('X-Icinga-Module', $name, true); + } + } + + /** + * Redirect to login + * + * XHR will always redirect to __SELF__ if an URL to redirect to after successful login is set. __SELF__ instructs + * JavaScript to redirect to the current window's URL if it's an auto-refresh request or to redirect to the URL + * which required login if it's not an auto-refreshing one. + * + * XHR will respond with HTTP status code 403 Forbidden. + * + * @param Url|string $redirect URL to redirect to after successful login + */ + protected function redirectToLogin($redirect = null) + { + $login = Url::fromPath('authentication/login'); + if ($this->isXhr()) { + if ($redirect !== null) { + $login->setParam('redirect', '__SELF__'); + } + + $this->_response->setHttpResponseCode(403); + } elseif ($redirect !== null) { + if (! $redirect instanceof Url) { + $redirect = Url::fromPath($redirect); + } + + if (($relativeUrl = $redirect->getRelativeUrl())) { + $login->setParam('redirect', $relativeUrl); + } + } + + $this->rerenderLayout()->redirectNow($login); + } + + protected function sendAsPdf() + { + $pdf = new Pdf(); + $pdf->renderControllerAction($this); + } + + /** + * Render the benchmark + * + * @return string Benchmark HTML + */ + protected function renderBenchmark() + { + $this->viewRenderer->postDispatch(); + Benchmark::measure('Response ready'); + return Benchmark::renderToHtml()/* + . '<pre style="height: 16em; vertical-overflow: scroll">' + . print_r(get_included_files(), 1) + . '</pre>'*/; + } + + public function isXhr() + { + return $this->getRequest()->isXmlHttpRequest(); + } + + protected function redirectHttp($url) + { + if (! $url instanceof Url) { + $url = Url::fromPath($url); + } + $this->shutdownSession(); + $this->_helper->Redirector->gotoUrlAndExit($url->getRelativeUrl()); + } + + /** + * Redirect to a specific url, updating the browsers URL field + * + * @param Url|string $url The target to redirect to + **/ + public function redirectNow($url) + { + if ($this->isXhr()) { + $this->redirectXhr($url); + } else { + $this->redirectHttp($url); + } + } + + protected function shutdownSession() + { + $session = Session::getSession(); + if ($session->hasChanged()) { + $session->write(); + } + } + + protected function rerenderLayout() + { + $this->rerenderLayout = true; + $this->xhrLayout = 'layout'; + return $this; + } + + protected function reloadCss() + { + $this->reloadCss = true; + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Controller/Extension/AutoRefreshHelper.php b/vendor/gipfl/icingaweb2/src/Controller/Extension/AutoRefreshHelper.php new file mode 100644 index 0000000..5b899ba --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Controller/Extension/AutoRefreshHelper.php @@ -0,0 +1,30 @@ +<?php + +namespace gipfl\IcingaWeb2\Controller\Extension; + +use InvalidArgumentException; + +trait AutoRefreshHelper +{ + /** @var int|null */ + private $autorefreshInterval; + + public function setAutorefreshInterval($interval) + { + if (! is_int($interval) || $interval < 1) { + throw new InvalidArgumentException( + 'Setting autorefresh interval smaller than 1 second is not allowed' + ); + } + $this->autorefreshInterval = $interval; + $this->layout->autorefreshInterval = $interval; + return $this; + } + + public function disableAutoRefresh() + { + $this->autorefreshInterval = null; + $this->layout->autorefreshInterval = null; + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Controller/Extension/ConfigHelper.php b/vendor/gipfl/icingaweb2/src/Controller/Extension/ConfigHelper.php new file mode 100644 index 0000000..72cefa7 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Controller/Extension/ConfigHelper.php @@ -0,0 +1,46 @@ +<?php + +namespace gipfl\IcingaWeb2\Controller\Extension; + +use Icinga\Application\Config; + +trait ConfigHelper +{ + private $config; + + private $configs = []; + + /** + * @param null $file + * @return Config + * @codingStandardsIgnoreStart + */ + public function Config($file = null) + { + // @codingStandardsIgnoreEnd + if ($this->moduleName === null) { + if ($file === null) { + return Config::app(); + } else { + return Config::app($file); + } + } else { + return $this->getModuleConfig($file); + } + } + + public function getModuleConfig($file = null) + { + if ($file === null) { + if ($this->config === null) { + $this->config = Config::module($this->getModuleName()); + } + return $this->config; + } else { + if (! array_key_exists($file, $this->configs)) { + $this->configs[$file] = Config::module($this->getModuleName(), $file); + } + return $this->configs[$file]; + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Controller/Extension/ControlsAndContentHelper.php b/vendor/gipfl/icingaweb2/src/Controller/Extension/ControlsAndContentHelper.php new file mode 100644 index 0000000..00ef389 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Controller/Extension/ControlsAndContentHelper.php @@ -0,0 +1,173 @@ +<?php + +namespace gipfl\IcingaWeb2\Controller\Extension; + +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\Content; +use gipfl\IcingaWeb2\Widget\Controls; +use gipfl\IcingaWeb2\Widget\Tabs; +use ipl\Html\HtmlDocument; + +trait ControlsAndContentHelper +{ + /** @var Controls */ + private $controls; + + /** @var Content */ + private $content; + + protected $title; + + /** @var Url */ + private $url; + + /** @var Url */ + private $originalUrl; + + /** + * TODO: Not sure whether we need dedicated Content/Controls classes, + * a simple Container with a class name might suffice here + * + * @return Controls + */ + public function controls() + { + if ($this->controls === null) { + $this->view->controls = $this->controls = new Controls(); + } + + return $this->controls; + } + + /** + * @param Tabs|null $tabs + * @return Tabs + */ + public function tabs(Tabs $tabs = null) + { + if ($tabs === null) { + return $this->controls()->getTabs(); + } else { + $this->controls()->setTabs($tabs); + return $tabs; + } + } + + /** + * @param HtmlDocument|null $actionBar + * @return HtmlDocument + */ + public function actions(HtmlDocument $actionBar = null) + { + if ($actionBar === null) { + return $this->controls()->getActionBar(); + } else { + $this->controls()->setActionBar($actionBar); + return $actionBar; + } + } + + /** + * @return Content + */ + public function content() + { + if ($this->content === null) { + $this->view->content = $this->content = new Content(); + } + + return $this->content; + } + + /** + * @param $title + * @return $this + */ + public function setTitle($title) + { + $this->title = $this->makeTitle(func_get_args()); + return $this; + } + + /** + * @param $title + * @return $this + */ + public function addTitle($title) + { + $title = $this->makeTitle(func_get_args()); + $this->title = $title; + $this->controls()->addTitle($title); + + return $this; + } + + private function makeTitle($args) + { + $title = array_shift($args); + + if (empty($args)) { + return $title; + } else { + return vsprintf($title, $args); + } + } + + /** + * @param string $title + * @param mixed $url + * @param string $name + * @return $this + */ + public function addSingleTab($title, $url = null, $name = 'main') + { + if ($url === null) { + $url = $this->url(); + } + + $this->tabs()->add($name, [ + 'label' => $title, + 'url' => $url, + ])->activate($name); + + return $this; + } + + /** + * @return Url + */ + public function url() + { + if ($this->url === null) { + $this->url = $this->getOriginalUrl(); + } + + return $this->url; + } + + /** + * @return Url + */ + public function getOriginalUrl() + { + if ($this->originalUrl === null) { + $this->originalUrl = clone($this->getUrlFromRequest()); + } + + return clone($this->originalUrl); + } + + /** + * @return Url + */ + protected function getUrlFromRequest() + { + /** @var \Icinga\Web\Request $request */ + $request = $this->getRequest(); + $webUrl = $request->getUrl(); + + return Url::fromPath( + $webUrl->getPath() + )->setParams($webUrl->getParams()); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Data/Paginatable.php b/vendor/gipfl/icingaweb2/src/Data/Paginatable.php new file mode 100644 index 0000000..8cd3963 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Data/Paginatable.php @@ -0,0 +1,64 @@ +<?php + +namespace gipfl\IcingaWeb2\Data; + +use Countable; + +interface Paginatable extends Countable +{ + /** + * Set a limit count and offset + * + * @param int $count Number of rows to return + * @param int $offset Skip that many rows + * + * @return self + */ + public function limit($count = null, $offset = null); + + /** + * Whether a limit is set + * + * @return bool + */ + public function hasLimit(); + + /** + * Get the limit if any + * + * @return int|null + */ + public function getLimit(); + + /** + * Set limit + * + * @param int $limit Number of rows to return + * + * @return int|null + */ + public function setLimit($limit); + + /** + * Whether an offset is set + * + * @return bool + */ + public function hasOffset(); + + /** + * Get the offset if any + * + * @return int|null + */ + public function getOffset(); + + /** + * Set offset + * + * @param int $offset Skip that many rows + * + * @return int|null + */ + public function setOffset($offset); +} diff --git a/vendor/gipfl/icingaweb2/src/Data/SimpleQueryPaginationAdapter.php b/vendor/gipfl/icingaweb2/src/Data/SimpleQueryPaginationAdapter.php new file mode 100644 index 0000000..d6a1b97 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Data/SimpleQueryPaginationAdapter.php @@ -0,0 +1,68 @@ +<?php + +namespace gipfl\IcingaWeb2\Data; + +use Icinga\Application\Benchmark; +use Icinga\Data\SimpleQuery; + +class SimpleQueryPaginationAdapter implements Paginatable +{ + /** @var SimpleQuery */ + private $query; + + public function __construct(SimpleQuery $query) + { + $this->query = $query; + } + + #[\ReturnTypeWillChange] + public function count() + { + Benchmark::measure('Running count() for pagination'); + $count = $this->query->count(); + Benchmark::measure("Counted $count rows"); + + return $count; + } + + public function limit($count = null, $offset = null) + { + $this->query->limit($count, $offset); + } + + public function hasLimit() + { + return $this->getLimit() !== null; + } + + public function getLimit() + { + return $this->query->getLimit(); + } + + public function setLimit($limit) + { + $this->query->limit( + $limit, + $this->getOffset() + ); + } + + public function hasOffset() + { + return $this->getOffset() !== null; + } + + public function getOffset() + { + return $this->query->getOffset(); + } + + public function setOffset($offset) + { + $this->query->limit( + $this->getLimit(), + $offset + ); + } +} diff --git a/vendor/gipfl/icingaweb2/src/FakeRequest.php b/vendor/gipfl/icingaweb2/src/FakeRequest.php new file mode 100644 index 0000000..854c26e --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/FakeRequest.php @@ -0,0 +1,32 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use Icinga\Web\Request; +use RuntimeException; + +class FakeRequest extends Request +{ + /** @var string */ + private static $baseUrl; + + public static function setConfiguredBaseUrl($url) + { + self::$baseUrl = $url; + } + + public function setUrl(Url $url) + { + $this->url = $url; + return $this; + } + + public function getBaseUrl($raw = false) + { + if (self::$baseUrl === null) { + throw new RuntimeException('Cannot determine base URL on CLI if not configured'); + } else { + return self::$baseUrl; + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Icon.php b/vendor/gipfl/icingaweb2/src/Icon.php new file mode 100644 index 0000000..0001e12 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Icon.php @@ -0,0 +1,27 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use ipl\Html\BaseHtmlElement; + +class Icon extends BaseHtmlElement +{ + protected $tag = 'i'; + + public function __construct($name, $attributes = null) + { + $this->setAttributes($attributes); + $this->getAttributes()->add('class', array('icon', 'icon-' . $name)); + } + + /** + * @param string $name + * @param array $attributes + * + * @return static + */ + public static function create($name, array $attributes = null) + { + return new static($name, $attributes); + } +} diff --git a/vendor/gipfl/icingaweb2/src/IconHelper.php b/vendor/gipfl/icingaweb2/src/IconHelper.php new file mode 100644 index 0000000..1c8af9d --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/IconHelper.php @@ -0,0 +1,89 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use InvalidArgumentException; + +/** + * Icon helper class + * + * Should help to reduce redundant icon-lookup code. Currently with hardcoded + * icons only, could easily provide support for all of them as follows: + * + * $confFile = Icinga::app() + * ->getApplicationDir('fonts/fontello-ifont/config.json'); + * + * $font = json_decode(file_get_contents($confFile)); + * // 'icon-' is to be found in $font->css_prefix_text + * foreach ($font->glyphs as $icon) { + * // $icon->css (= 'help') -> 0x . dechex($icon->code) + * } + */ +class IconHelper +{ + private $icons = [ + 'minus' => 'e806', + 'trash' => 'e846', + 'plus' => 'e805', + 'cancel' => 'e804', + 'help' => 'e85b', + 'angle-double-right' => 'e87b', + 'up-big' => 'e825', + 'down-big' => 'e828', + 'down-open' => 'e821', + ]; + + private $mappedUtf8Icons; + + private $reversedUtf8Icons; + + private static $instance; + + public function __construct() + { + $this->prepareIconMappings(); + } + + public static function instance() + { + if (self::$instance === null) { + self::$instance = new static; + } + + return self::$instance; + } + + public function characterIconName($character) + { + if (array_key_exists($character, $this->reversedUtf8Icons)) { + return $this->reversedUtf8Icons[$character]; + } else { + throw new InvalidArgumentException('There is no mapping for the given character'); + } + } + + protected function hexToCharacter($hex) + { + return json_decode('"\u' . $hex . '"'); + } + + public function iconCharacter($name) + { + if (array_key_exists($name, $this->mappedUtf8Icons)) { + return $this->mappedUtf8Icons[$name]; + } else { + return $this->mappedUtf8Icons['help']; + } + } + + protected function prepareIconMappings() + { + $this->mappedUtf8Icons = []; + $this->reversedUtf8Icons = []; + foreach ($this->icons as $name => $hex) { + $character = $this->hexToCharacter($hex); + $this->mappedUtf8Icons[$name] = $character; + $this->reversedUtf8Icons[$character] = $name; + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Img.php b/vendor/gipfl/icingaweb2/src/Img.php new file mode 100644 index 0000000..3c68adb --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Img.php @@ -0,0 +1,83 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use Icinga\Web\Url as WebUrl; +use ipl\Html\Attribute; +use ipl\Html\BaseHtmlElement; + +class Img extends BaseHtmlElement +{ + protected $tag = 'img'; + + /** @var Url */ + protected $url; + + protected $defaultAttributes = array('alt' => ''); + + protected function __construct() + { + } + + /** + * @param Url|string $url + * @param array $urlParams + * @param array $attributes + * + * @return static + */ + public static function create($url, $urlParams = null, array $attributes = null) + { + /** @var Img $img */ + $img = new static(); + $img->setAttributes($attributes); + $img->getAttributes()->registerAttributeCallback('src', array($img, 'getSrcAttribute')); + $img->setUrl($url, $urlParams); + return $img; + } + + public function setUrl($url, $urlParams) + { + if ($url instanceof WebUrl) { // Hint: Url is also a WebUrl + if ($urlParams !== null) { + $url->addParams($urlParams); + } + + $this->url = $url; + } else { + if ($urlParams === null) { + if (is_string($url) && substr($url, 0, 5) === 'data:') { + $this->url = $url; + return; + } else { + $this->url = Url::fromPath($url); + } + } else { + $this->url = Url::fromPath($url, $urlParams); + } + } + + $this->url->getParams(); + } + + /** + * @return Attribute + */ + public function getSrcAttribute() + { + if (is_string($this->url)) { + return new Attribute('src', $this->url); + } else { + return new Attribute('src', $this->getUrl()->getAbsoluteUrl('&')); + } + } + + /** + * @return Url + */ + public function getUrl() + { + // TODO: What if null? #? + return $this->url; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Link.php b/vendor/gipfl/icingaweb2/src/Link.php new file mode 100644 index 0000000..e6e4de9 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Link.php @@ -0,0 +1,85 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use Icinga\Web\Url as WebUrl; +use ipl\Html\Attribute; +use ipl\Html\BaseHtmlElement; +use ipl\Html\ValidHtml; + +class Link extends BaseHtmlElement +{ + protected $tag = 'a'; + + /** @var Url */ + protected $url; + + /** + * Link constructor. + * @param $content + * @param $url + * @param null $urlParams + * @param array|null $attributes + */ + public function __construct($content, $url, $urlParams = null, array $attributes = null) + { + $this->setContent($content); + $this->setAttributes($attributes); + $this->getAttributes()->registerAttributeCallback('href', array($this, 'getHrefAttribute')); + $this->setUrl($url, $urlParams); + } + + /** + * @param ValidHtml|array|string $content + * @param Url|string $url + * @param array $urlParams + * @param mixed $attributes + * + * @return static + */ + public static function create($content, $url, $urlParams = null, array $attributes = null) + { + $link = new static($content, $url, $urlParams, $attributes); + return $link; + } + + /** + * @param $url + * @param $urlParams + */ + public function setUrl($url, $urlParams) + { + if ($url instanceof WebUrl) { // Hint: Url is also a WebUrl + if ($urlParams !== null) { + $url->addParams($urlParams); + } + + $this->url = $url; + } else { + if ($urlParams === null) { + $this->url = Url::fromPath($url); + } else { + $this->url = Url::fromPath($url, $urlParams); + } + } + + $this->url->getParams(); + } + + /** + * @return Attribute + */ + public function getHrefAttribute() + { + return new Attribute('href', $this->getUrl()->getAbsoluteUrl('&')); + } + + /** + * @return Url + */ + public function getUrl() + { + // TODO: What if null? #? + return $this->url; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/Extension/MultiSelect.php b/vendor/gipfl/icingaweb2/src/Table/Extension/MultiSelect.php new file mode 100644 index 0000000..7a5a3ff --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/Extension/MultiSelect.php @@ -0,0 +1,29 @@ +<?php + +namespace gipfl\IcingaWeb2\Table\Extension; + +use gipfl\IcingaWeb2\Url; + +// Could also be a static method, MultiSelect::enable($table) +trait MultiSelect +{ + protected function enableMultiSelect($url, $sourceUrl, array $keys) + { + /** @var $table \ipl\Html\BaseHtmlElement */ + $table = $this; + $table->addAttributes([ + 'class' => 'multiselect' + ]); + + $prefix = 'data-icinga-multiselect'; + $multi = [ + "$prefix-url" => Url::fromPath($url), + "$prefix-controllers" => Url::fromPath($sourceUrl), + "$prefix-data" => implode(',', $keys), + ]; + + $table->addAttributes($multi); + + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/Extension/QuickSearch.php b/vendor/gipfl/icingaweb2/src/Table/Extension/QuickSearch.php new file mode 100644 index 0000000..0f0cec3 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/Extension/QuickSearch.php @@ -0,0 +1,74 @@ +<?php + +namespace gipfl\IcingaWeb2\Table\Extension; + +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\Controls; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +trait QuickSearch +{ + /** @var BaseHtmlElement */ + private $quickSearchForm; + + public function getQuickSearch(BaseHtmlElement $parent, Url $url) + { + $this->requireQuickSearchForm($parent, $url); + $search = $url->getParam('q'); + return $search; + } + + private function requireQuickSearchForm(BaseHtmlElement $parent, Url $url) + { + if ($this->quickSearchForm === null) { + $this->quickSearchForm = $this->buildQuickSearchForm($parent, $url); + } + } + + private function buildQuickSearchForm(BaseHtmlElement $parent, Url $url) + { + $search = $url->getParam('q'); + + $form = Html::tag('form', [ + 'action' => $url->without(array('q', 'page', 'modifyFilter'))->getAbsoluteUrl(), + 'class' => ['gipfl-quicksearch'], + 'method' => 'GET' + ]); + + $form->add( + Html::tag('input', [ + 'type' => 'text', + 'name' => 'q', + 'title' => $this->translate('Search is simple! Try to combine multiple words'), + 'value' => $search, + 'placeholder' => $this->translate('Search...'), + 'class' => 'search' + ]) + ); + + $this->addQuickSearchToControls($parent, $form); + + return $form; + } + + protected function addQuickSearchToControls(BaseHtmlElement $parent, BaseHtmlElement $form) + { + if ($parent instanceof Controls) { + $title = $parent->getTitleElement(); + if ($title === null) { + $parent->prepend($form); + } else { + $input = $form->getFirst('input'); + $form->remove($input); + $title->add($input); + $form->add($title); + $parent->setTitleElement($form); + } + } else { + $parent->prepend($form); + } + + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/Extension/ZfSortablePriority.php b/vendor/gipfl/icingaweb2/src/Table/Extension/ZfSortablePriority.php new file mode 100644 index 0000000..cb1eac6 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/Extension/ZfSortablePriority.php @@ -0,0 +1,263 @@ +<?php + +namespace gipfl\IcingaWeb2\Table\Extension; + +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\IconHelper; +use gipfl\ZfDb\Exception\SelectException; +use gipfl\ZfDb\Select; +use Icinga\Web\Request; +use Icinga\Web\Response; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlString; +use RuntimeException; +use Zend_Db_Select_Exception as ZfDbSelectException; + +/** + * Trait ZfSortablePriority + * + * Assumes to run in a ZfQueryBasedTable + */ +trait ZfSortablePriority +{ + /** @var Request */ + protected $request; + + /** @var Response */ + protected $response; + + public function handleSortPriorityActions(Request $request, Response $response) + { + $this->request = $request; + $this->response = $response; + return $this; + } + + protected function reallyHandleSortPriorityActions() + { + $request = $this->request; + + if ($request->isPost() && $this->hasBeenSent($request)) { + // $this->fixPriorities(); + foreach (array_keys($request->getPost()) as $key) { + if (substr($key, 0, 8) === 'MOVE_UP_') { + $id = (int) substr($key, 8); + $this->moveRow($id, 'up'); + } + if (substr($key, 0, 10) === 'MOVE_DOWN_') { + $id = (int) substr($key, 10); + $this->moveRow($id, 'down'); + } + } + $this->response->redirectAndExit($request->getUrl()); + } + } + + protected function hasBeenSent(Request $request) + { + return $request->getPost('__FORM_NAME') === $this->getUniqueFormName(); + } + + protected function addSortPriorityButtons(BaseHtmlElement $tr, $row) + { + $tr->add( + Html::tag( + 'td', + null, + $this->createUpDownButtons($row->{$this->getKeyColumn()}) + ) + ); + + return $tr; + } + + protected function getKeyColumn() + { + if (isset($this->keyColumn)) { + return $this->keyColumn; + } else { + throw new RuntimeException( + 'ZfSortablePriority requires keyColumn' + ); + } + } + + protected function getPriorityColumn() + { + if (isset($this->priorityColumn)) { + return $this->priorityColumn; + } else { + throw new RuntimeException( + 'ZfSortablePriority requires priorityColumn' + ); + } + } + + protected function getPriorityColumns() + { + return [ + 'id' => $this->getKeyColumn(), + 'prio' => $this->getPriorityColumn() + ]; + } + + protected function moveRow($id, $direction) + { + /** @var $this ZfQueryBasedTable */ + $db = $this->db(); + /** @var $this ZfQueryBasedTable */ + $query = $this->getQuery(); + $tableParts = $this->getQueryPart(Select::FROM); + $alias = key($tableParts); + $table = $tableParts[$alias]['tableName']; + + $whereParts = $this->getQueryPart(Select::WHERE); + unset($query); + if (empty($whereParts)) { + $where = ''; + } else { + $where = ' AND ' . implode(' ', $whereParts); + } + + $prioCol = $this->getPriorityColumn(); + $keyCol = $this->getKeyColumn(); + $myPrio = (int) $db->fetchOne( + $db->select() + ->from($table, $prioCol) + ->where("$keyCol = ?", $id) + ); + + $op = $direction === 'up' ? '<' : '>'; + $sortDir = $direction === 'up' ? 'DESC' : 'ASC'; + $query = $db->select() + ->from([$alias => $table], $this->getPriorityColumns()) + ->where("$prioCol $op ?", $myPrio) + ->order("$prioCol $sortDir") + ->limit(1); + + if (! empty($whereParts)) { + $query->where(implode(' ', $whereParts)); + } + + $next = $db->fetchRow($query); + + if ($next) { + $sql = 'UPDATE %s %s' + . ' SET %s = CASE WHEN %s = %s THEN %d ELSE %d END' + . ' WHERE %s IN (%s, %s)'; + + $query = sprintf( + $sql, + $table, + $alias, + $prioCol, + $keyCol, + $id, + (int) $next->prio, + $myPrio, + $keyCol, + $id, + (int) $next->id + ) . $where; + + $db->query($query); + } + } + + protected function getSortPriorityTitle() + { + /** @var ZfQueryBasedTable $table */ + $table = $this; + + return Html::tag( + 'span', + ['title' => $table->translate('Change priority')], + $table->translate('Prio') + ); + } + + protected function createUpDownButtons($key) + { + /** @var ZfQueryBasedTable $table */ + $table = $this; + $up = $this->createIconButton( + "MOVE_UP_$key", + 'up-big', + $table->translate('Move up (raise priority)') + ); + $down = $this->createIconButton( + "MOVE_DOWN_$key", + 'down-big', + $table->translate('Move down (lower priority)') + ); + + if ($table->isOnFirstRow()) { + $up->getAttributes()->add('disabled', 'disabled'); + } + + if ($table->isOnLastRow()) { + $down->getAttributes()->add('disabled', 'disabled'); + } + + return [$down, $up]; + } + + protected function createIconButton($key, $icon, $title) + { + return Html::tag('input', [ + 'type' => 'submit', + 'class' => 'icon-button', + 'name' => $key, + 'title' => $title, + 'value' => IconHelper::instance()->iconCharacter($icon) + ]); + } + + protected function getUniqueFormName() + { + $parts = explode('\\', get_class($this)); + return end($parts); + } + + protected function renderWithSortableForm() + { + if ($this->request === null) { + return parent::render(); + } + $this->reallyHandleSortPriorityActions(); + + $url = $this->request->getUrl(); + // TODO: No margin for form + $form = Html::tag('form', [ + 'action' => $url->getAbsoluteUrl(), + 'method' => 'POST' + ], [ + Html::tag('input', [ + 'type' => 'hidden', + 'name' => '__FORM_NAME', + 'value' => $this->getUniqueFormName() + ]), + new HtmlString(parent::render()) + ]); + + return $form->render(); + } + + protected function getQueryPart($part) + { + /** @var ZfQueryBasedTable $table */ + $table = $this; + /** @var Select|\Zend_Db_Select $query */ + $query = $table->getQuery(); + try { + return $query->getPart($part); + } catch (SelectException $e) { + // Will not happen if $part is correct. + throw new RuntimeException($e); + } catch (ZfDbSelectException $e) { + // Will not happen if $part is correct. + throw new RuntimeException($e); + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/QueryBasedTable.php b/vendor/gipfl/icingaweb2/src/Table/QueryBasedTable.php new file mode 100644 index 0000000..e9281c7 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/QueryBasedTable.php @@ -0,0 +1,281 @@ +<?php + +namespace gipfl\IcingaWeb2\Table; + +use Countable; +use gipfl\Format\LocalDateFormat; +use gipfl\IcingaWeb2\Data\Paginatable; +use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer; +use gipfl\IcingaWeb2\Table\Extension\QuickSearch; +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\ControlsAndContent; +use gipfl\IcingaWeb2\Widget\Paginator; +use gipfl\Translation\TranslationHelper; +use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; +use ipl\Html\Table; + +abstract class QueryBasedTable extends Table implements Countable +{ + use TranslationHelper; + use QuickSearch; + + protected $defaultAttributes = [ + 'class' => ['common-table', 'table-row-selectable'], + 'data-base-target' => '_next', + ]; + + private $fetchedRows; + + private $firstRow; + + private $lastRow = false; + + private $rowNumber; + + private $rowNumberOnPage; + + protected $lastDay; + + /** @var Paginator|null Will usually be defined at rendering time */ + protected $paginator; + + private $isUsEnglish; + + private $dateFormatter; + + protected $searchColumns = []; + + /** + * @return Paginatable + */ + abstract protected function getPaginationAdapter(); + + abstract public function getQuery(); + + public function getPaginator(Url $url) + { + return new Paginator( + $this->getPaginationAdapter(), + $url + ); + } + + public function count() + { + return $this->getPaginationAdapter()->count(); + } + + public function applyFilter(Filter $filter) + { + FilterRenderer::applyToQuery($filter, $this->getQuery()); + return $this; + } + + protected function getSearchColumns() + { + return $this->searchColumns; + } + + public function search($search) + { + if (! empty($search)) { + $query = $this->getQuery(); + $columns = $this->getSearchColumns(); + if (strpos($search, ' ') === false) { + $filter = Filter::matchAny(); + foreach ($columns as $column) { + $filter->addFilter(Filter::expression($column, '=', "*$search*")); + } + } else { + $filter = Filter::matchAll(); + foreach (explode(' ', $search) as $s) { + $sub = Filter::matchAny(); + foreach ($columns as $column) { + $sub->addFilter(Filter::expression($column, '=', "*$s*")); + } + $filter->addFilter($sub); + } + } + + FilterRenderer::applyToQuery($filter, $query); + } + + return $this; + } + + abstract protected function prepareQuery(); + + public function renderContent() + { + $titleColumns = $this->renderTitleColumns(); + if ($titleColumns) { + $this->getHeader()->add($titleColumns); + } + $this->fetchRows(); + + return parent::renderContent(); + } + + protected function renderTitleColumns() + { + // TODO: drop this + if (method_exists($this, 'getColumnsToBeRendered')) { + $columns = $this->getColumnsToBeRendered(); + if (isset($columns) && count($columns)) { + return static::row($columns, null, 'th'); + } + } + + return null; + } + + protected function splitByDay($timestamp) + { + $this->renderDayIfNew((int) $timestamp); + } + + public function isOnFirstPage() + { + if ($this->paginator === null) { + // No paginator? Then there should be only a single page + return true; + } + + return $this->paginator->getCurrentPage() === 1; + } + + public function isOnFirstRow() + { + return $this->firstRow === true; + } + + public function isOnLastRow() + { + return $this->lastRow === true; + } + + protected function fetchRows() + { + $firstPage = $this->isOnFirstPage(); + $this->rowNumberOnPage = 0; + $this->rowNumber = $this->getPaginationAdapter()->getOffset(); + $lastRow = count($this); + foreach ($this->fetch() as $row) { + $this->rowNumber++; + $this->rowNumberOnPage++; + if (null === $this->firstRow) { + if ($firstPage) { + $this->firstRow = true; + } else { + $this->firstRow = false; + } + } elseif (true === $this->firstRow) { + $this->firstRow = false; + } + if ($lastRow === $this->rowNumber) { + $this->lastRow = true; + } + // Hint: do not fetch the body first, the row might want to replace it + $tr = $this->renderRow($row); + $this->add($tr); + } + } + + protected function renderRow($row) + { + return $this::row([$row]); + } + + /** + * @deprecated + * @return bool + */ + protected function isUsEnglish() + { + if ($this->isUsEnglish === null) { + $this->isUsEnglish = in_array(setlocale(LC_ALL, 0), ['en_US', 'en_US.UTF-8', 'C']); + } + + return $this->isUsEnglish; + } + + /** + * @param int $timestamp + */ + protected function renderDayIfNew($timestamp) + { + $day = $this->getDateFormatter()->getFullDay($timestamp); + + if ($this->lastDay !== $day) { + $this->nextHeader()->add( + $this::th($day, [ + 'colspan' => 2, + 'class' => 'table-header-day' + ]) + ); + + $this->lastDay = $day; + $this->nextBody(); + } + } + + abstract protected function fetchQueryRows(); + + public function fetch() + { + $parts = explode('\\', get_class($this)); + $name = end($parts); + Benchmark::measure("Fetching data for $name table"); + $rows = $this->fetchQueryRows(); + $this->fetchedRows = count($rows); + Benchmark::measure("Fetched $this->fetchedRows rows for $name table"); + + return $rows; + } + + protected function initializeOptionalQuickSearch(ControlsAndContent $controller) + { + $columns = $this->getSearchColumns(); + if (! empty($columns)) { + $this->search( + $this->getQuickSearch( + $controller->controls(), + $controller->url() + ) + ); + } + } + + /** + * @param ControlsAndContent $controller + * @return $this + */ + public function renderTo(ControlsAndContent $controller) + { + $url = $controller->url(); + $c = $controller->content(); + $this->paginator = $this->getPaginator($url); + $this->initializeOptionalQuickSearch($controller); + $controller->actions()->add($this->paginator); + $c->add($this); + + // TODO: move elsewhere + if (method_exists($this, 'dumpSqlQuery')) { + if ($url->getParam('format') === 'sql') { + $c->prepend($this->dumpSqlQuery($url)); + } + } + + return $this; + } + + protected function getDateFormatter() + { + if ($this->dateFormatter === null) { + $this->dateFormatter = new LocalDateFormat(); + } + + return $this->dateFormatter; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/SimpleQueryBasedTable.php b/vendor/gipfl/icingaweb2/src/Table/SimpleQueryBasedTable.php new file mode 100644 index 0000000..8d6015a --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/SimpleQueryBasedTable.php @@ -0,0 +1,34 @@ +<?php + +namespace gipfl\IcingaWeb2\Table; + +use Icinga\Data\SimpleQuery; +use gipfl\IcingaWeb2\Data\SimpleQueryPaginationAdapter; + +abstract class SimpleQueryBasedTable extends QueryBasedTable +{ + /** @var SimpleQuery */ + private $query; + + protected function getPaginationAdapter() + { + return new SimpleQueryPaginationAdapter($this->getQuery()); + } + + protected function fetchQueryRows() + { + return $this->query->fetchAll(); + } + + /** + * @return SimpleQuery + */ + public function getQuery() + { + if ($this->query === null) { + $this->query = $this->prepareQuery(); + } + + return $this->query; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/ZfQueryBasedTable.php b/vendor/gipfl/icingaweb2/src/Table/ZfQueryBasedTable.php new file mode 100644 index 0000000..8a421d5 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/ZfQueryBasedTable.php @@ -0,0 +1,149 @@ +<?php + +namespace gipfl\IcingaWeb2\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\ControlsAndContent; +use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer; +use gipfl\IcingaWeb2\Zf1\Db\SelectPaginationAdapter; +use gipfl\ZfDb\Adapter\Adapter as Db; +use gipfl\ZfDb\Select as DbSelect; +use Icinga\Data\Db\DbConnection; +use Icinga\Data\Filter\Filter; +use ipl\Html\DeferredText; +use ipl\Html\Html; +use LogicException; +use Zend_Db_Adapter_Abstract as DbAdapter; + +abstract class ZfQueryBasedTable extends QueryBasedTable +{ + /** @var ?DbConnection */ + private $connection; + + /** @var DbAdapter|Db */ + private $db; + + private $query; + + private $paginationAdapter; + + public function __construct($db) + { + if ($db instanceof Db || $db instanceof DbAdapter) { + $this->db = $db; + } elseif ($db instanceof DbConnection) { + $this->connection = $db; + $this->db = $db->getDbAdapter(); + } else { + throw new LogicException(sprintf( + 'Unable to deal with %s db class', + get_class($db) + )); + } + } + + public static function show(ControlsAndContent $controller, DbConnection $db) + { + $table = new static($db); + $table->renderTo($controller); + } + + public function getCountQuery() + { + return $this->getPaginationAdapter()->getCountQuery(); + } + + protected function getPaginationAdapter() + { + if ($this->paginationAdapter === null) { + $this->paginationAdapter = new SelectPaginationAdapter($this->getQuery()); + } + + return $this->paginationAdapter; + } + + public function applyFilter(Filter $filter) + { + FilterRenderer::applyToQuery($filter, $this->getQuery()); + return $this; + } + + public function search($search) + { + if (! empty($search)) { + $query = $this->getQuery(); + $columns = $this->getSearchColumns(); + if (strpos($search, ' ') === false) { + $filter = Filter::matchAny(); + foreach ($columns as $column) { + $filter->addFilter(Filter::expression($column, '=', "*$search*")); + } + } else { + $filter = Filter::matchAll(); + foreach (explode(' ', $search) as $s) { + $sub = Filter::matchAny(); + foreach ($columns as $column) { + $sub->addFilter(Filter::expression($column, '=', "*$s*")); + } + $filter->addFilter($sub); + } + } + + FilterRenderer::applyToQuery($filter, $query); + } + + return $this; + } + + protected function fetchQueryRows() + { + return $this->db->fetchAll($this->getQuery()); + } + + /** + * @deprecated Might be null, we'll fade it out + * @return ?DbConnection + */ + public function connection() + { + return $this->connection; + } + + public function db() + { + return $this->db; + } + + /** + * @return DbSelect|\Zend_Db_Select + */ + public function getQuery() + { + if ($this->query === null) { + $this->query = $this->prepareQuery(); + } + + return $this->query; + } + + public function dumpSqlQuery(Url $url) + { + $self = $this; + return Html::tag('div', ['class' => 'sql-dump'], [ + Link::create('[ close ]', $url->without('format')), + Html::tag('h3', null, $this->translate('SQL Query')), + Html::tag('pre', null, new DeferredText( + function () use ($self) { + return wordwrap($self->getQuery()); + } + )), + Html::tag('h3', null, $this->translate('Count Query')), + Html::tag('pre', null, new DeferredText( + function () use ($self) { + return wordwrap($self->getCountQuery()); + } + )), + ]); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Translator.php b/vendor/gipfl/icingaweb2/src/Translator.php new file mode 100644 index 0000000..b1cb088 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Translator.php @@ -0,0 +1,26 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use gipfl\Translation\TranslatorInterface; + +class Translator implements TranslatorInterface +{ + /** @var string */ + private $domain; + + public function __construct($domain) + { + $this->domain = $domain; + } + + public function translate($string) + { + $res = dgettext($this->domain, $string); + if ($res === $string && $this->domain !== 'icinga') { + return dgettext('icinga', $string); + } + + return $res; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Url.php b/vendor/gipfl/icingaweb2/src/Url.php new file mode 100644 index 0000000..2c6bf1f --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Url.php @@ -0,0 +1,162 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use Exception; +use Icinga\Application\Icinga; +use Icinga\Exception\ProgrammingError; +use Icinga\Web\Url as WebUrl; +use Icinga\Web\UrlParams; +use InvalidArgumentException; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\UriInterface; +use RuntimeException; + +/** + * Class Url + * + * The main purpose of this class is to get unit tests running on CLI + * Little code from Icinga\Web\Url has been duplicated, as neither fromPath() + * nor getRequest() can be extended in a meaningful way at the time of this + * writing + */ +class Url extends WebUrl +{ + /** + * @param string $url + * @param array $params + * @param null $request + * @return Url + */ + public static function fromPath($url, array $params = array(), $request = null) + { + if ($request === null) { + $request = static::getRequest(); + } + + if (! \is_string($url)) { + throw new InvalidArgumentException(sprintf( + 'url "%s" is not a string', + \var_export($url, 1) + )); + } + + $self = new static; + + if ($url === '#') { + return $self->setPath($url); + } + + $parts = \parse_url($url); + + $self->setBasePath($request->getBaseUrl()); + if (isset($parts['path'])) { + $self->setPath($parts['path']); + } + + if (isset($parts['query'])) { + $params = UrlParams::fromQueryString($parts['query'])->mergeValues($params); + } + + if (isset($parts['fragment'])) { + $self->setAnchor($parts['fragment']); + } + + $self->setParams($params); + return $self; + } + + public static function fromUri(UriInterface $uri) + { + $query = $uri->getQuery(); + $path = $uri->getPath(); + if (\strlen($query)) { + $path .= "?$query"; + } + + return static::fromPath($path); + } + + public static function fromServerRequest(ServerRequestInterface $request) + { + return static::fromUri($request->getUri()); + } + + /** + * Create a new Url class representing the current request + * + * If $params are given, those will be added to the request's parameters + * and overwrite any existing parameters + * + * @param UrlParams|array $params Parameters that should additionally be considered for the url + * @param \Icinga\Web\Request $request A request to use instead of the default one + * + * @return Url + */ + public static function fromRequest($params = array(), $request = null) + { + if ($request === null) { + $request = static::getRequest(); + } + + $url = new Url(); + $url->setPath(\ltrim($request->getPathInfo(), '/')); + $request->getQuery(); + + // $urlParams = UrlParams::fromQueryString($request->getQuery()); + if (isset($_SERVER['QUERY_STRING'])) { + $urlParams = UrlParams::fromQueryString($_SERVER['QUERY_STRING']); + } else { + $urlParams = UrlParams::fromQueryString(''); + foreach ($request->getQuery() as $k => $v) { + $urlParams->set($k, $v); + } + } + + foreach ($params as $k => $v) { + $urlParams->set($k, $v); + } + $url->setParams($urlParams); + $url->setBasePath($request->getBaseUrl()); + + return $url; + } + + public function setBasePath($basePath) + { + if (property_exists($this, 'basePath')) { + parent::setBasePath($basePath); + } else { + $this->setBaseUrl($basePath); + } + + return $this; + } + + public function setParams($params) + { + try { + return parent::setParams($params); + } catch (ProgrammingError $e) { + throw new InvalidArgumentException($e->getMessage(), 0, $e); + } + } + + protected static function getRequest() + { + try { + $app = Icinga::app(); + } catch (ProgrammingError $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + if ($app->isCli()) { + try { + return new FakeRequest(); + } catch (Exception $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + } else { + return $app->getRequest(); + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/ActionBar.php b/vendor/gipfl/icingaweb2/src/Widget/ActionBar.php new file mode 100644 index 0000000..63e6c77 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/ActionBar.php @@ -0,0 +1,25 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\BaseHtmlElement; + +class ActionBar extends BaseHtmlElement +{ + protected $contentSeparator = ' '; + + /** @var string */ + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'gipfl-action-bar']; + + /** + * @param string $target + * @return $this + */ + public function setBaseTarget($target) + { + $this->getAttributes()->set('data-base-target', $target); + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/Content.php b/vendor/gipfl/icingaweb2/src/Widget/Content.php new file mode 100644 index 0000000..92ea115 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/Content.php @@ -0,0 +1,14 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\BaseHtmlElement; + +class Content extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $contentSeparator = "\n"; + + protected $defaultAttributes = ['class' => 'content']; +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/Controls.php b/vendor/gipfl/icingaweb2/src/Widget/Controls.php new file mode 100644 index 0000000..cb52013 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/Controls.php @@ -0,0 +1,163 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; + +class Controls extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $contentSeparator = "\n"; + + protected $defaultAttributes = ['class' => 'controls']; + + /** @var Tabs */ + private $tabs; + + /** @var ActionBar */ + private $actions; + + /** @var string */ + private $title; + + /** @var string */ + private $subTitle; + + /** @var BaseHtmlElement */ + private $titleElement; + + /** + * @param $title + * @param null $subTitle + * @return $this + */ + public function addTitle($title, $subTitle = null) + { + $this->title = $title; + if ($subTitle !== null) { + $this->subTitle = $subTitle; + } + + return $this->setTitleElement($this->renderTitleElement()); + } + + /** + * @param BaseHtmlElement $element + * @return $this + */ + public function setTitleElement(BaseHtmlElement $element) + { + if ($this->titleElement !== null) { + $this->remove($this->titleElement); + } + + $this->titleElement = $element; + $this->prepend($element); + + return $this; + } + + public function getTitleElement() + { + return $this->titleElement; + } + + /** + * @return Tabs + */ + public function getTabs() + { + if ($this->tabs === null) { + $this->tabs = new Tabs(); + } + + return $this->tabs; + } + + /** + * @param Tabs $tabs + * @return $this + */ + public function setTabs(Tabs $tabs) + { + $this->tabs = $tabs; + return $this; + } + + /** + * @param Tabs $tabs + * @return $this + */ + public function prependTabs(Tabs $tabs) + { + if ($this->tabs === null) { + $this->tabs = $tabs; + } else { + $current = $this->tabs->getTabs(); + $this->tabs = $tabs; + foreach ($current as $name => $tab) { + $this->tabs->add($name, $tab); + } + } + + return $this; + } + + /** + * @return ActionBar + */ + public function getActionBar() + { + if ($this->actions === null) { + $this->setActionBar(new ActionBar()); + } + + return $this->actions; + } + + /** + * @param HtmlDocument $actionBar + * @return $this + */ + public function setActionBar(HtmlDocument $actionBar) + { + if ($this->actions !== null) { + $this->remove($this->actions); + } + + $this->actions = $actionBar; + $this->add($actionBar); + + return $this; + } + + /** + * @return BaseHtmlElement + */ + protected function renderTitleElement() + { + $h1 = Html::tag('h1', null, $this->title); + if ($this->subTitle) { + $h1->setSeparator(' ')->add( + Html::tag('small', null, $this->subTitle) + ); + } + + return $h1; + } + + /** + * @return string + */ + public function renderContent() + { + if (null !== $this->tabs) { + $this->prepend($this->tabs); + } + + return parent::renderContent(); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/ControlsAndContent.php b/vendor/gipfl/icingaweb2/src/Widget/ControlsAndContent.php new file mode 100644 index 0000000..8574ce7 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/ControlsAndContent.php @@ -0,0 +1,60 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\HtmlDocument; +use gipfl\IcingaWeb2\Url; + +interface ControlsAndContent +{ + /** + * @return Controls + */ + public function controls(); + + /** + * @return Tabs + */ + public function tabs(); + + /** + * @param HtmlDocument|null $actionBar + * @return HtmlDocument + */ + public function actions(HtmlDocument $actionBar = null); + + /** + * @return Content + */ + public function content(); + + /** + * @param $title + * @return $this + */ + public function setTitle($title); + + /** + * @param $title + * @return $this + */ + public function addTitle($title); + + /** + * @param $title + * @param null $url + * @param string $name + * @return $this + */ + public function addSingleTab($title, $url = null, $name = 'main'); + + /** + * @return Url + */ + public function url(); + + /** + * @return Url + */ + public function getOriginalUrl(); +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/ListItem.php b/vendor/gipfl/icingaweb2/src/Widget/ListItem.php new file mode 100644 index 0000000..fa4b562 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/ListItem.php @@ -0,0 +1,26 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\ValidHtml; + +class ListItem extends BaseHtmlElement +{ + protected $contentSeparator = "\n"; + + /** + * @param ValidHtml|array|string $content + * @param Attributes|array $attributes + * + * @return $this + */ + public function addItem($content, $attributes = null) + { + return $this->add( + Html::tag('li', $attributes, $content) + ); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/NameValueTable.php b/vendor/gipfl/icingaweb2/src/Widget/NameValueTable.php new file mode 100644 index 0000000..971a833 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/NameValueTable.php @@ -0,0 +1,29 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\Table; + +class NameValueTable extends Table +{ + protected $defaultAttributes = ['class' => 'name-value-table']; + + public function createNameValueRow($name, $value) + { + return $this::tr([$this::th($name), $this::td($value)]); + } + + public function addNameValueRow($name, $value) + { + return $this->add($this->createNameValueRow($name, $value)); + } + + public function addNameValuePairs($pairs) + { + foreach ($pairs as $name => $value) { + $this->addNameValueRow($name, $value); + } + + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/Paginator.php b/vendor/gipfl/icingaweb2/src/Widget/Paginator.php new file mode 100644 index 0000000..3c255a7 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/Paginator.php @@ -0,0 +1,463 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use Icinga\Exception\ProgrammingError; +use gipfl\IcingaWeb2\Data\Paginatable; +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use gipfl\Translation\TranslationHelper; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class Paginator extends BaseHtmlElement +{ + use TranslationHelper; + + protected $tag = 'div'; + + protected $defaultAttributes = [ + 'class' => 'pagination-control', + 'role' => 'navigation', + ]; + + /** @var Paginatable The query the paginator widget is created for */ + protected $query; + + /** @var int */ + protected $pageCount; + + /** @var int */ + protected $currentCount; + + /** @var Url */ + protected $url; + + /** @var string */ + protected $pageParam; + + /** @var string */ + protected $perPageParam; + + /** @var int */ + protected $totalCount; + + /** @var int */ + protected $defaultItemCountPerPage = 25; + + public function __construct( + Paginatable $query, + Url $url, + $pageParameter = 'page', + $perPageParameter = 'limit' + ) { + $this->query = $query; + $this->setPageParam($pageParameter); + $this->setPerPageParam($perPageParameter); + $this->setUrl($url); + } + + public function setItemsPerPage($count) + { + // TODO: this should become setOffset once available + $query = $this->getQuery(); + $query->setLimit($count); + + return $this; + } + + protected function setPageParam($pageParam) + { + $this->pageParam = $pageParam; + return $this; + } + + protected function setPerPageParam($perPageParam) + { + $this->perPageParam = $perPageParam; + return $this; + } + + public function getPageParam() + { + return $this->pageParam; + } + + public function getPerPageParam() + { + return $this->perPageParam; + } + + public function getCurrentPage() + { + $query = $this->getQuery(); + if ($query->hasOffset()) { + return ($query->getOffset() / $this->getItemsPerPage()) + 1; + } else { + return 1; + } + } + + protected function setCurrentPage($page) + { + $page = (int) $page; + $offset = $this->firstRowOnPage($page) - 1; + if ($page > 1) { + $query = $this->getQuery(); + $query->setOffset($offset); + } + } + + public function getPageCount() + { + if ($this->pageCount === null) { + $this->pageCount = (int) ceil($this->getTotalItemCount() / $this->getItemsPerPage()); + } + + return $this->pageCount; + } + + protected function getItemsPerPage() + { + $limit = $this->getQuery()->getLimit(); + if ($limit === null) { + throw new ProgrammingError('Something went wrong, got no limit when there should be one'); + } else { + return $limit; + } + } + + public function getTotalItemCount() + { + if ($this->totalCount === null) { + $this->totalCount = count($this->getQuery()); + } + + return $this->totalCount; + } + + public function getPrevious() + { + if ($this->hasPrevious()) { + return $this->getCurrentPage() - 1; + } else { + return null; + } + } + + public function hasPrevious() + { + return $this->getCurrentPage() > 1; + } + + public function getNext() + { + if ($this->hasNext()) { + return $this->getCurrentPage() + 1; + } else { + return null; + } + } + + public function hasNext() + { + return $this->getCurrentPage() < $this->getPageCount(); + } + + public function getQuery() + { + return $this->query; + } + + /** + * Returns an array of "local" pages given the page count and current page number + * + * @return array + */ + protected function getPages() + { + $page = $this->getPageCount(); + $current = $this->getCurrentPage(); + + $range = []; + + if ($page < 10) { + // Show all pages if we have less than 10 + for ($i = 1; $i < 10; $i++) { + if ($i > $page) { + break; + } + + $range[$i] = $i; + } + } else { + // More than 10 pages: + foreach ([1, 2] as $i) { + $range[$i] = $i; + } + + if ($current < 6) { + // We are on page 1-5 from + for ($i = 1; $i <= 7; $i++) { + $range[$i] = $i; + } + } else { + // Current page > 5 + $range[] = '…'; + + if (($page - $current) < 5) { + // Less than 5 pages left + $start = 5 - ($page - $current); + } else { + $start = 1; + } + + for ($i = $current - $start; $i < ($current + (4 - $start)); $i++) { + if ($i > $page) { + break; + } + + $range[$i] = $i; + } + } + + if ($current < ($page - 2)) { + $range[] = '…'; + } + + foreach ([$page - 1, $page] as $i) { + $range[$i] = $i; + } + } + + if (empty($range)) { + $range[] = 1; + } + + return $range; + } + + public function getDefaultItemCountPerPage() + { + return $this->defaultItemCountPerPage; + } + + public function setDefaultItemCountPerPage($count) + { + $this->defaultItemCountPerPage = (int) $count; + return $this; + } + + public function setUrl(Url $url) + { + $page = (int) $url->shift($this->getPageParam()); + $perPage = (int) $url->getParam($this->getPerPageParam()); + if ($perPage > 0) { + $this->setItemsPerPage($perPage); + } else { + if (! $this->getQuery()->hasLimit()) { + $this->setItemsPerPage($this->getDefaultItemCountPerPage()); + } + } + if ($page > 0) { + $this->setCurrentPage($page); + } + + $this->url = $url; + + return $this; + } + + public function getUrl() + { + if ($this->url === null) { + $this->setUrl(Url::fromRequest()); + } + + return $this->url; + } + + public function getPreviousLabel() + { + return $this->getLabel($this->getCurrentPage() - 1); + } + + protected function getNextLabel() + { + return $this->getLabel($this->getCurrentPage() + 1); + } + + protected function getLabel($page) + { + return sprintf( + $this->translate('Show rows %u to %u of %u'), + $this->firstRowOnPage($page), + $this->lastRowOnPage($page), + $this->getTotalItemCount() + ); + } + + protected function renderPrevious() + { + return Html::tag('li', [ + 'class' => 'nav-item' + ], Link::create( + Icon::create('angle-double-left'), + $this->makeUrl($this->getPrevious()), + null, + [ + 'title' => $this->getPreviousLabel(), + 'class' => 'previous-page' + ] + )); + } + + protected function renderNoPrevious() + { + return $this->renderDisabled(Html::tag('span', [ + 'class' => 'previous-page' + ], [ + $this->srOnly($this->translate('Previous page')), + Icon::create('angle-double-left') + ])); + } + + protected function renderNext() + { + return Html::tag('li', [ + 'class' => 'nav-item' + ], Link::create( + Icon::create('angle-double-right'), + $this->makeUrl($this->getNext()), + null, + [ + 'title' => $this->getNextLabel(), + 'class' => 'next-page' + ] + )); + } + + protected function renderNoNext() + { + return $this->renderDisabled(Html::tag('span', [ + 'class' => 'previous-page' + ], [ + $this->srOnly($this->translate('Next page')), + Icon::create('angle-double-right') + ])); + } + + protected function renderDots() + { + return $this->renderDisabled(Html::tag('span', null, '…')); + } + + protected function renderInnerPages() + { + $pages = []; + $current = $this->getCurrentPage(); + + foreach ($this->getPages() as $page) { + if ($page === '…') { + $pages[] = $this->renderDots(); + } else { + $pages[] = Html::tag( + 'li', + $page === $current ? ['class' => 'active'] : null, + $this->makeLink($page) + ); + } + } + + return $pages; + } + + protected function lastRowOnPage($page) + { + $perPage = $this->getItemsPerPage(); + $total = $this->getTotalItemCount(); + $last = $page * $perPage; + if ($last > $total) { + $last = $total; + } + + return $last; + } + + protected function firstRowOnPage($page) + { + return ($page - 1) * $this->getItemsPerPage() + 1; + } + + protected function makeLink($page) + { + return Link::create( + $page, + $this->makeUrl($page), + null, + ['title' => $this->getLabel($page)] + ); + } + + protected function makeUrl($page) + { + if ($page) { + return $this->getUrl()->with('page', $page); + } else { + return $this->getUrl(); + } + } + + protected function srOnly($content) + { + return Html::tag('span', ['class' => 'sr-only'], $content); + } + + protected function renderDisabled($content) + { + return Html::tag('li', [ + 'class' => ['nav-item', 'disabled'], + 'aria-hidden' => 'true' + ], $content); + } + + protected function renderList() + { + return Html::tag( + 'ul', + ['class' => ['nav', 'tab-nav']], + [ + $this->hasPrevious() ? $this->renderPrevious() : $this->renderNoPrevious(), + $this->renderInnerPages(), + $this->hasNext() ? $this->renderNext() : $this->renderNoNext() + ] + ); + } + + public function assemble() + { + $this->add([ + $this->renderScreenReaderHeader(), + $this->renderList() + ]); + } + + protected function renderScreenReaderHeader() + { + return Html::tag('h2', [ + // 'id' => $this->protectId('pagination') -> why? + 'class' => 'sr-only', + 'tab-index' => '-1' + ], $this->translate('Pagination')); + } + + public function render() + { + if ($this->getPageCount() < 2) { + return ''; + } else { + return parent::render(); + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/Tabs.php b/vendor/gipfl/icingaweb2/src/Widget/Tabs.php new file mode 100644 index 0000000..38bf4cd --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/Tabs.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use Exception; +use Icinga\Web\Widget\Tabs as WebTabs; +use InvalidArgumentException; +use ipl\Html\ValidHtml; + +class Tabs extends WebTabs implements ValidHtml +{ + /** + * @param string $name + * @return $this + */ + public function activate($name) + { + try { + parent::activate($name); + } catch (Exception $e) { + throw new InvalidArgumentException( + "Can't activate '$name', there is no such tab" + ); + } + + return $this; + } + + /** + * @param string $name + * @param array|\Icinga\Web\Widget\Tab $tab + * @return $this + */ + public function add($name, $tab) + { + try { + parent::add($name, $tab); + } catch (Exception $e) { + throw new InvalidArgumentException($e->getMessage()); + } + + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Zf1/Db/CountQuery.php b/vendor/gipfl/icingaweb2/src/Zf1/Db/CountQuery.php new file mode 100644 index 0000000..07204b8 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Zf1/Db/CountQuery.php @@ -0,0 +1,93 @@ +<?php + +namespace gipfl\IcingaWeb2\Zf1\Db; + +use gipfl\ZfDb\Select; +use RuntimeException; +use Zend_Db_Select as ZfSelect; + +class CountQuery +{ + /** @var Select|ZfSelect */ + private $query; + + private $maxRows; + + /** + * ZfCountQuery constructor. + * @param Select|ZfSelect $query + */ + public function __construct($query) + { + if ($query instanceof Select || $query instanceof ZfSelect) { + $this->query = $query; + } else { + throw new RuntimeException('Got no supported ZF1 Select object'); + } + } + + public function setMaxRows($max) + { + $this->maxRows = $max; + return $this; + } + + public function getQuery() + { + if ($this->needsSubQuery()) { + return $this->buildSubQuery(); + } else { + return $this->buildSimpleQuery(); + } + } + + protected function hasOneOf($parts) + { + foreach ($parts as $part) { + if ($this->hasPart($part)) { + return true; + } + } + + return false; + } + + protected function hasPart($part) + { + $values = $this->query->getPart($part); + return ! empty($values); + } + + protected function needsSubQuery() + { + return null !== $this->maxRows || $this->hasOneOf([ + Select::GROUP, + Select::UNION + ]); + } + + protected function buildSubQuery() + { + $sub = clone($this->query); + $sub->limit(null, null); + $class = $this->query; + $query = new $class($this->query->getAdapter()); + $query->from($sub, ['cnt' => 'COUNT(*)']); + if (null !== $this->maxRows) { + $sub->limit($this->maxRows + 1); + } + + return $query; + } + + protected function buildSimpleQuery() + { + $query = clone($this->query); + $query->reset(Select::COLUMNS); + $query->reset(Select::ORDER); + $query->reset(Select::LIMIT_COUNT); + $query->reset(Select::LIMIT_OFFSET); + $query->columns(['cnt' => 'COUNT(*)']); + return $query; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Zf1/Db/FilterRenderer.php b/vendor/gipfl/icingaweb2/src/Zf1/Db/FilterRenderer.php new file mode 100644 index 0000000..b51296f --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Zf1/Db/FilterRenderer.php @@ -0,0 +1,335 @@ +<?php + +namespace gipfl\IcingaWeb2\Zf1\Db; + +use gipfl\ZfDb\Adapter\Adapter as Db; +use gipfl\ZfDb\Exception\SelectException; +use gipfl\ZfDb\Expr; +use gipfl\ZfDb\Select; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterAnd; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Data\Filter\FilterNot; +use Icinga\Data\Filter\FilterOr; +use Icinga\Data\SimpleQuery; +use InvalidArgumentException; +use RuntimeException; +use Zend_Db_Adapter_Abstract as DbAdapter; +use Zend_Db_Expr as DbExpr; +use Zend_Db_Select as DbSelect; +use Zend_Db_Select_Exception as DbSelectException; + +class FilterRenderer +{ + private $db; + + /** @var Filter */ + private $filter; + + /** @var array */ + private $columnMap; + + /** @var string */ + private $dbExprClass; + + /** + * FilterRenderer constructor. + * @param Filter $filter + * @param Db|DbAdapter $db + */ + public function __construct(Filter $filter, $db) + { + $this->filter = $filter; + if ($db instanceof Db) { + $this->db = $db; + $this->dbExprClass = Expr::class; + } elseif ($db instanceof DbAdapter) { + $this->db = $db; + $this->dbExprClass = DbExpr::class; + } else { + throw new RuntimeException('Got no supported ZF1 DB adapter'); + } + } + + /** + * @return Expr|DbExpr + */ + public function toDbExpression() + { + return $this->expr($this->render()); + } + + /** + * @return Expr|DbExpr + */ + protected function expr($content) + { + $class = $this->dbExprClass; + return new $class($content); + } + + /** + * @param Filter $filter + * @param Select|DbSelect|SimpleQuery $query + * @return Select|DbSelect|SimpleQuery + */ + public static function applyToQuery(Filter $filter, $query) + { + if ($query instanceof SimpleQuery) { + $query->applyFilter($filter); + return $query; + } + if (! ($query instanceof Select || $query instanceof DbSelect)) { + throw new RuntimeException('Got no supported ZF1 Select object'); + } + + if (! $filter->isEmpty()) { + $renderer = new static($filter, $query->getAdapter()); + $renderer->extractColumnMap($query); + $query->where($renderer->toDbExpression()); + } + + return $query; + } + + protected function lookupColumnAlias($column) + { + if (array_key_exists($column, $this->columnMap)) { + return $this->columnMap[$column]; + } else { + return $column; + } + } + + protected function extractColumnMap($query) + { + $map = []; + try { + $columns = $query->getPart(Select::COLUMNS); + } catch (SelectException $e) { + // Will not happen. + throw new RuntimeException($e->getMessage()); + } catch (DbSelectException $e) { + // Will not happen. + throw new RuntimeException($e->getMessage()); + } + + foreach ($columns as $col) { + if ($col[1] instanceof Expr || $col[1] instanceof DbExpr) { + $map[$col[2]] = (string) $col[1]; + $map[$col[2]] = $col[1]; + } else { + $map[$col[2]] = $col[0] . '.' . $col[1]; + } + } + + $this->columnMap = $map; + } + + /** + * @return string + */ + public function render() + { + return $this->renderFilter($this->filter); + } + + protected function renderFilterChain(FilterChain $filter, $level = 0) + { + $prefix = ''; + + if ($filter instanceof FilterAnd) { + $op = ' AND '; + } elseif ($filter instanceof FilterOr) { + $op = ' OR '; + } elseif ($filter instanceof FilterNot) { + $op = ' AND '; + $prefix = 'NOT '; + } else { + throw new InvalidArgumentException( + 'Cannot render a %s filter chain for Zf Db', + get_class($filter) + ); + } + + $parts = []; + if ($filter->isEmpty()) { + // Hint: we might want to fail here + return ''; + } else { + foreach ($filter->filters() as $f) { + $part = $this->renderFilter($f, $level + 1); + if ($part !== '') { + $parts[] = $part; + } + } + if (empty($parts)) { + // will not happen, as we are not empty + return ''; + } else { + if ($level > 0) { + return "$prefix (" . implode($op, $parts) . ')'; + } else { + return $prefix . implode($op, $parts); + } + } + } + } + + protected function renderFilterExpression(FilterExpression $filter) + { + $col = $this->lookupColumnAlias($filter->getColumn()); + if (! $col instanceof Expr && ! $col instanceof DbExpr && ! ctype_digit($col)) { + $col = $this->db->quoteIdentifier($col); + } + $sign = $filter->getSign(); + $expression = $filter->getExpression(); + + if (is_array($expression)) { + return $this->renderArrayExpression($col, $sign, $expression); + } + + if ($sign === '=') { + if (strpos($expression, '*') === false) { + return $this->renderAny($col, $sign, $expression); + } else { + return $this->renderLike($col, $expression); + } + } + + if ($sign === '!=') { + if (strpos($expression, '*') === false) { + return $this->renderAny($col, $sign, $expression); + } else { + return $this->renderNotLike($col, $expression); + } + } + + return $this->renderAny($col, $sign, $expression); + } + + + protected function renderLike($col, $expression) + { + if ($expression === '*') { + return $this->expr('TRUE'); + } + + return $col . ' LIKE ' . $this->escape($this->escapeWildcards($expression)); + } + + protected function renderNotLike($col, $expression) + { + if ($expression === '*') { + return $this->expr('FALSE'); + } + + return sprintf( + '(%1$s NOT LIKE %2$s OR %1$s IS NULL)', + $col, + $this->escape($this->escapeWildcards($expression)) + ); + } + + protected function renderNotEqual($col, $expression) + { + return sprintf('(%1$s != %2$s OR %1$s IS NULL)', $col, $this->escape($expression)); + } + + protected function renderAny($col, $sign, $expression) + { + return sprintf('%s %s %s', $col, $sign, $this->escape($expression)); + } + + protected function renderArrayExpression($col, $sign, $expression) + { + if ($sign === '=') { + return $col . ' IN (' . $this->escape($expression) . ')'; + } elseif ($sign === '!=') { + return sprintf( + '(%1$s NOT IN (%2$s) OR %1$s IS NULL)', + $col, + $this->escape($expression) + ); + } + + throw new InvalidArgumentException( + 'Array expressions can only be rendered for = and !=, got %s', + $sign + ); + } + + /** + * @param Filter $filter + * @param int $level + * @return string|Expr|DbExpr + */ + protected function renderFilter(Filter $filter, $level = 0) + { + if ($filter instanceof FilterChain) { + return $this->renderFilterChain($filter, $level); + } elseif ($filter instanceof FilterExpression) { + return $this->renderFilterExpression($filter); + } else { + throw new RuntimeException(sprintf( + 'Filter of type FilterChain or FilterExpression expected, got %s', + get_class($filter) + )); + } + } + + protected function escape($value) + { + // bindParam? bindValue? + if (is_array($value)) { + $ret = []; + foreach ($value as $val) { + $ret[] = $this->escape($val); + } + return implode(', ', $ret); + } else { + return $this->db->quote($value); + } + } + + protected function escapeWildcards($value) + { + return preg_replace('/\*/', '%', $value); + } + + public function whereToSql($col, $sign, $expression) + { + if (is_array($expression)) { + if ($sign === '=') { + return $col . ' IN (' . $this->escape($expression) . ')'; + } elseif ($sign === '!=') { + return sprintf('(%1$s NOT IN (%2$s) OR %1$s IS NULL)', $col, $this->escape($expression)); + } + + throw new InvalidArgumentException( + 'Unable to render array expressions with operators other than equal or not equal' + ); + } elseif ($sign === '=' && strpos($expression, '*') !== false) { + if ($expression === '*') { + return $this->expr('TRUE'); + } + + return $col . ' LIKE ' . $this->escape($this->escapeWildcards($expression)); + } elseif ($sign === '!=' && strpos($expression, '*') !== false) { + if ($expression === '*') { + return $this->expr('FALSE'); + } + + return sprintf( + '(%1$s NOT LIKE %2$s OR %1$s IS NULL)', + $col, + $this->escape($this->escapeWildcards($expression)) + ); + } elseif ($sign === '!=') { + return sprintf('(%1$s %2$s %3$s OR %1$s IS NULL)', $col, $sign, $this->escape($expression)); + } else { + return sprintf('%s %s %s', $col, $sign, $this->escape($expression)); + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Zf1/Db/SelectPaginationAdapter.php b/vendor/gipfl/icingaweb2/src/Zf1/Db/SelectPaginationAdapter.php new file mode 100644 index 0000000..599a3ee --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Zf1/Db/SelectPaginationAdapter.php @@ -0,0 +1,111 @@ +<?php + +namespace gipfl\IcingaWeb2\Zf1\Db; + +use gipfl\IcingaWeb2\Data\Paginatable; +use gipfl\ZfDb\Select; +use gipfl\ZfDb\Exception\SelectException; +use Icinga\Application\Benchmark; +use RuntimeException; +use Zend_Db_Select as ZfSelect; +use Zend_Db_Select_Exception as ZfDbSelectException; + +class SelectPaginationAdapter implements Paginatable +{ + private $query; + + private $countQuery; + + private $cachedCount; + + private $cachedCountQuery; + + public function __construct($query) + { + if ($query instanceof Select || $query instanceof ZfSelect) { + $this->query = $query; + } else { + throw new RuntimeException('Got no supported ZF1 Select object'); + } + } + + public function getCountQuery() + { + if ($this->countQuery === null) { + $this->countQuery = (new CountQuery($this->query))->getQuery(); + } + + return $this->countQuery; + } + + #[\ReturnTypeWillChange] + public function count() + { + $queryString = (string) $this->getCountQuery(); + if ($this->cachedCountQuery !== $queryString) { + Benchmark::measure('Running count() for pagination'); + $this->cachedCountQuery = $queryString; + $count = $this->query->getAdapter()->fetchOne( + $queryString + ); + $this->cachedCount = $count; + Benchmark::measure("Counted $count rows"); + } + + return $this->cachedCount; + } + + public function limit($count = null, $offset = null) + { + $this->query->limit($count, $offset); + } + + public function hasLimit() + { + return $this->getLimit() !== null; + } + + public function getLimit() + { + return $this->getQueryPart(Select::LIMIT_COUNT); + } + + public function setLimit($limit) + { + $this->query->limit( + $limit, + $this->getOffset() + ); + } + + public function hasOffset() + { + return $this->getOffset() !== null; + } + + public function getOffset() + { + return $this->getQueryPart(Select::LIMIT_OFFSET); + } + + protected function getQueryPart($part) + { + try { + return $this->query->getPart($part); + } catch (SelectException $e) { + // Will not happen if $part is correct. + throw new RuntimeException($e); + } catch (ZfDbSelectException $e) { + // Will not happen if $part is correct. + throw new RuntimeException($e); + } + } + + public function setOffset($offset) + { + $this->query->limit( + $this->getLimit(), + $offset + ); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Zf1/SimpleViewRenderer.php b/vendor/gipfl/icingaweb2/src/Zf1/SimpleViewRenderer.php new file mode 100644 index 0000000..89b36a4 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Zf1/SimpleViewRenderer.php @@ -0,0 +1,115 @@ +<?php + +namespace gipfl\IcingaWeb2\Zf1; + +use gipfl\IcingaWeb2\Widget\Content; +use gipfl\IcingaWeb2\Widget\Controls; +use ipl\Html\Error; +use Icinga\Application\Icinga; +use ipl\Html\ValidHtml; +use Zend_Controller_Action_Helper_Abstract as Helper; +use Zend_Controller_Action_HelperBroker as HelperBroker; + +class SimpleViewRenderer extends Helper implements ValidHtml +{ + private $disabled = false; + + private $rendered = false; + + /** @var \Zend_View_Interface */ + public $view; + + public function init() + { + // Register view with action controller (unless already registered) + if ((null !== $this->_actionController) && (null === $this->_actionController->view)) { + $this->_actionController->view = $this->view; + } + } + + public function disable($disabled = true) + { + $this->disabled = $disabled; + return $this; + } + + public function replaceZendViewRenderer() + { + /** @var \Zend_Controller_Action_Helper_ViewRenderer $viewRenderer */ + $viewRenderer = Icinga::app()->getViewRenderer(); + $viewRenderer->setNeverRender(); + $viewRenderer->setNeverController(); + HelperBroker::removeHelper('viewRenderer'); + HelperBroker::addHelper($this); + $this->view = $viewRenderer->view; + return $this; + } + + public function render($action = null, $name = null, $noController = null) + { + if (null === $name) { + $name = null; // $this->getResponseSegment(); + } + // Compat. + if (isset($this->_actionController) + && get_class($this->_actionController) === 'Icinga\\Controllers\\ErrorController' + ) { + $html = $this->simulateErrorController(); + } else { + $html = ''; + if (null !== $this->view->controls) { + $html .= $this->view->controls->__toString(); + } + + if (null !== $this->view->content) { + $html .= $this->view->content->__toString(); + } + } + + $this->getResponse()->appendBody($html, $name); + // $this->setNoRender(); + $this->rendered = true; + } + + protected function simulateErrorController() + { + $errorHandler = $this->_actionController->getParam('error_handler'); + if (isset($errorHandler->exception)) { + $error = Error::show($errorHandler->exception); + } else { + $error = 'An unknown error occured'; + } + + /** @var \Icinga\Web\Request $request */ + $request = $this->getRequest(); + $controls = new Controls(); + $controls->getTabs()->add('error', [ + 'label' => t('Error'), + 'url' => $request->getUrl(), + ])->activate('error'); + $content = new Content(); + $content->add($error); + + return $controls . $content; + } + + public function shouldRender() + { + return ! $this->disabled && ! $this->rendered; + } + + public function postDispatch() + { + if ($this->shouldRender()) { + $this->render(); + } + } + + public function getName() + { + // TODO: This is wrong, should be 'viewRenderer' - but that would + // currently break nearly everything, starting with full layout + // rendering + return 'ViewRenderer'; + } +} |