1<?php 2/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ 3 4namespace Icinga\Module\Monitoring\DataView; 5 6use IteratorAggregate; 7use Icinga\Application\Hook; 8use Icinga\Data\ConnectionInterface; 9use Icinga\Data\Filter\Filter; 10use Icinga\Data\Filter\FilterMatch; 11use Icinga\Data\FilterColumns; 12use Icinga\Data\PivotTable; 13use Icinga\Data\QueryInterface; 14use Icinga\Data\SortRules; 15use Icinga\Exception\QueryException; 16use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery; 17use Icinga\Module\Monitoring\Backend\MonitoringBackend; 18use Icinga\Web\Request; 19use Icinga\Web\Url; 20 21/** 22 * A read-only view of an underlying query 23 */ 24abstract class DataView implements QueryInterface, SortRules, FilterColumns, IteratorAggregate 25{ 26 /** 27 * The query used to populate the view 28 * 29 * @var IdoQuery 30 */ 31 protected $query; 32 33 protected $connection; 34 35 protected $isSorted = false; 36 37 /** 38 * The cache for all filter columns 39 * 40 * @var array 41 */ 42 protected $filterColumns; 43 44 /** 45 * Create a new view 46 * 47 * @param ConnectionInterface $connection 48 * @param array $columns 49 */ 50 public function __construct(ConnectionInterface $connection, array $columns = null) 51 { 52 $this->connection = $connection; 53 $this->query = $connection->query($this->getQueryName(), $columns); 54 } 55 56 /** 57 * Return a iterator for all rows of the result set 58 * 59 * @return IdoQuery 60 */ 61 public function getIterator() 62 { 63 return $this->getQuery(); 64 } 65 66 /** 67 * Return the current position of the result set's iterator 68 * 69 * @return int 70 */ 71 public function getIteratorPosition() 72 { 73 return $this->query->getIteratorPosition(); 74 } 75 76 /** 77 * Get the query name this data view relies on 78 * 79 * By default this is this class' name without its namespace 80 * 81 * @return string 82 */ 83 public static function getQueryName() 84 { 85 $tableName = explode('\\', get_called_class()); 86 $tableName = end($tableName); 87 return $tableName; 88 } 89 90 public function where($condition, $value = null) 91 { 92 $this->query->where($condition, $value); 93 return $this; 94 } 95 96 public function dump() 97 { 98 if (! $this->isSorted) { 99 $this->order(); 100 } 101 return $this->query->dump(); 102 } 103 104 /** 105 * Retrieve columns provided by this view 106 * 107 * @return array 108 */ 109 abstract public function getColumns(); 110 111 /** 112 * Create view from request 113 * 114 * @param Request $request 115 * @param array $columns 116 * 117 * @return static 118 * @deprecated Use $backend->select()->from($viewName) instead 119 */ 120 public static function fromRequest($request, array $columns = null) 121 { 122 $view = new static(MonitoringBackend::instance($request->getParam('backend')), $columns); 123 $view->applyUrlFilter($request); 124 125 return $view; 126 } 127 128 protected function getHookedColumns() 129 { 130 $columns = array(); 131 foreach (Hook::all('monitoring/dataviewExtension') as $hook) { 132 foreach ($hook->getAdditionalQueryColumns($this->getQueryName()) as $col) { 133 $columns[] = $col; 134 } 135 } 136 137 return $columns; 138 } 139 140 // TODO: This is not the right place for this, move it away 141 protected function applyUrlFilter($request = null) 142 { 143 $url = Url::fromRequest(); 144 145 $limit = $url->shift('limit'); 146 $sort = $url->shift('sort'); 147 $dir = $url->shift('dir'); 148 $page = $url->shift('page'); 149 $format = $url->shift('format'); 150 $view = $url->shift('showCompact'); 151 $view = $url->shift('backend'); 152 foreach ($url->getParams() as $k => $v) { 153 $this->where($k, $v); 154 } 155 if ($sort) { 156 $this->order($sort, $dir); 157 } 158 } 159 160 /** 161 * Create view from params 162 * 163 * @param array $params 164 * @param array $columns 165 * 166 * @return static 167 */ 168 public static function fromParams(array $params, array $columns = null) 169 { 170 $view = new static(MonitoringBackend::instance($params['backend']), $columns); 171 172 foreach ($params as $key => $value) { 173 if ($view->isValidFilterTarget($key)) { 174 $view->where($key, $value); 175 } 176 } 177 178 if (isset($params['sort'])) { 179 $order = isset($params['order']) ? $params['order'] : null; 180 if ($order !== null) { 181 if (strtolower($order) === 'desc') { 182 $order = self::SORT_DESC; 183 } else { 184 $order = self::SORT_ASC; 185 } 186 } 187 188 $view->sort($params['sort'], $order); 189 } 190 return $view; 191 } 192 193 /** 194 * Check whether the given column is a valid filter column 195 * 196 * @param string $column 197 * 198 * @return bool 199 */ 200 public function isValidFilterTarget($column) 201 { 202 // Customvar 203 if ($column[0] === '_' && preg_match('/^_(?:host|service)_/i', $column)) { 204 return true; 205 } 206 return in_array($column, $this->getColumns()) || in_array($column, $this->getStaticFilterColumns()); 207 } 208 209 /** 210 * Return all filter columns with their optional label as key 211 * 212 * This will merge the results of self::getColumns(), self::getStaticFilterColumns() and 213 * self::getDynamicFilterColumns() *once*. (i.e. subsequent calls of this function will 214 * return the same result.) 215 * 216 * @return array 217 */ 218 public function getFilterColumns() 219 { 220 if ($this->filterColumns === null) { 221 $columns = array_merge( 222 $this->getColumns(), 223 $this->getStaticFilterColumns(), 224 $this->getDynamicFilterColumns() 225 ); 226 227 $this->filterColumns = array(); 228 foreach ($columns as $label => $column) { 229 if (is_int($label)) { 230 $label = ucwords(str_replace('_', ' ', $column)); 231 } 232 233 if ($this->query->isCaseInsensitive($column)) { 234 $label .= ' ' . t('(Case insensitive)'); 235 } 236 237 $this->filterColumns[$label] = $column; 238 } 239 } 240 241 return $this->filterColumns; 242 } 243 244 /** 245 * Return all static filter columns 246 * 247 * @return array 248 */ 249 public function getStaticFilterColumns() 250 { 251 return array(); 252 } 253 254 /** 255 * Return all dynamic filter columns such as custom variables 256 * 257 * @return array 258 */ 259 public function getDynamicFilterColumns() 260 { 261 $columns = array(); 262 if (! $this->query->allowsCustomVars()) { 263 return $columns; 264 } 265 266 $query = MonitoringBackend::instance() 267 ->select() 268 ->from('customvar', array('varname', 'object_type')) 269 ->where('is_json', 0) 270 ->where('object_type_id', array(1, 2)) 271 ->getQuery()->group(array('varname', 'object_type')); 272 foreach ($query as $row) { 273 if ($row->object_type === 'host') { 274 $label = t('Host') . ' ' . ucwords(str_replace('_', ' ', $row->varname)); 275 $columns[$label] = '_host_' . $row->varname; 276 } else { // $row->object_type === 'service' 277 $label = t('Service') . ' ' . ucwords(str_replace('_', ' ', $row->varname)); 278 $columns[$label] = '_service_' . $row->varname; 279 } 280 } 281 282 return $columns; 283 } 284 285 /** 286 * Return the current filter 287 * 288 * @return Filter 289 */ 290 public function getFilter() 291 { 292 return $this->query->getFilter(); 293 } 294 295 /** 296 * Return a pivot table for the given columns based on the current query 297 * 298 * @param string $xAxisColumn The column to use for the x axis 299 * @param string $yAxisColumn The column to use for the y axis 300 * @param Filter $xAxisFilter The filter to apply on a query for the x axis 301 * @param Filter $yAxisFilter The filter to apply on a query for the y axis 302 * 303 * @return PivotTable 304 */ 305 public function pivot($xAxisColumn, $yAxisColumn, Filter $xAxisFilter = null, Filter $yAxisFilter = null) 306 { 307 $pivot = new PivotTable($this->query, $xAxisColumn, $yAxisColumn); 308 return $pivot->setXAxisFilter($xAxisFilter)->setYAxisFilter($yAxisFilter); 309 } 310 311 /** 312 * Sort the rows, according to the specified sort column and order 313 * 314 * @param string $column Sort column 315 * @param string $order Sort order, one of the SORT_ constants 316 * 317 * @return $this 318 * @throws QueryException If the sort column is not allowed 319 * @see DataView::SORT_ASC 320 * @see DataView::SORT_DESC 321 * @deprecated Use DataView::order() instead 322 */ 323 public function sort($column = null, $order = null) 324 { 325 $sortRules = $this->getSortRules(); 326 if ($column === null) { 327 // Use first available sort rule as default 328 if (empty($sortRules)) { 329 return $this; 330 } 331 $sortColumns = reset($sortRules); 332 if (! isset($sortColumns['columns'])) { 333 $sortColumns['columns'] = array(key($sortRules)); 334 } 335 } else { 336 if (isset($sortRules[$column])) { 337 $sortColumns = $sortRules[$column]; 338 if (! isset($sortColumns['columns'])) { 339 $sortColumns['columns'] = array($column); 340 } 341 } else { 342 $sortColumns = array( 343 'columns' => array($column), 344 'order' => $order 345 ); 346 }; 347 } 348 349 $order = $order === null ? (isset($sortColumns['order']) ? $sortColumns['order'] : static::SORT_ASC) : $order; 350 $order = (strtoupper($order) === static::SORT_ASC) ? 'ASC' : 'DESC'; 351 352 foreach ($sortColumns['columns'] as $column) { 353 list($column, $direction) = $this->query->splitOrder($column); 354 if (! $this->isValidFilterTarget($column)) { 355 throw new QueryException( 356 mt('monitoring', 'The sort column "%s" is not allowed in "%s".'), 357 $column, 358 get_class($this) 359 ); 360 } 361 $this->query->order($column, $direction !== null ? $direction : $order); 362 } 363 $this->isSorted = true; 364 return $this; 365 } 366 367 /** 368 * Retrieve default sorting rules for particular columns. These involve sort order and potential additional to sort 369 * 370 * @return array 371 */ 372 public function getSortRules() 373 { 374 return array(); 375 } 376 377 /** 378 * Sort result set either by the given column (and direction) or the sort defaults 379 * 380 * @param string $column 381 * @param string $direction 382 * 383 * @return $this 384 */ 385 public function order($column = null, $direction = null) 386 { 387 return $this->sort($column, $direction); 388 } 389 390 /** 391 * Whether an order is set 392 * 393 * @return bool 394 */ 395 public function hasOrder() 396 { 397 return $this->query->hasOrder(); 398 } 399 400 /** 401 * Get the order if any 402 * 403 * @return array|null 404 */ 405 public function getOrder() 406 { 407 return $this->query->getOrder(); 408 } 409 410 public function getMappedField($field) 411 { 412 return $this->query->getMappedField($field); 413 } 414 415 /** 416 * Return the query which was created in the constructor 417 * 418 * @return \Icinga\Data\SimpleQuery 419 */ 420 public function getQuery() 421 { 422 if (! $this->isSorted) { 423 $this->order(); 424 } 425 return $this->query; 426 } 427 428 public function applyFilter(Filter $filter) 429 { 430 $this->validateFilterColumns($filter); 431 432 return $this->addFilter($filter); 433 } 434 435 /** 436 * Validates recursive the Filter columns against the isValidFilterTarget() method 437 * 438 * @param Filter $filter 439 * 440 * @throws \Icinga\Data\Filter\FilterException 441 */ 442 public function validateFilterColumns(Filter $filter) 443 { 444 if ($filter instanceof FilterMatch) { 445 if (! $this->isValidFilterTarget($filter->getColumn())) { 446 throw new QueryException( 447 mt('monitoring', 'The filter column "%s" is not allowed here.'), 448 $filter->getColumn() 449 ); 450 } 451 } 452 453 if (method_exists($filter, 'filters')) { 454 foreach ($filter->filters() as $filter) { 455 $this->validateFilterColumns($filter); 456 } 457 } 458 } 459 460 public function clearFilter() 461 { 462 $this->query->clearFilter(); 463 return $this; 464 } 465 466 /** 467 * @deprecated(EL): Only use DataView::applyFilter() for applying filter because all other functions are missing 468 * column validation. Filter::matchAny() for the IdoQuery (or the DbQuery or the SimpleQuery I didn't have a look) 469 * is required for the filter to work properly. 470 */ 471 public function setFilter(Filter $filter) 472 { 473 $this->query->setFilter($filter); 474 return $this; 475 } 476 477 /** 478 * Get the view's search columns 479 * 480 * @return string[] 481 */ 482 public function getSearchColumns() 483 { 484 return array(); 485 } 486 487 /** 488 * @deprecated(EL): Only use DataView::applyFilter() for applying filter because all other functions are missing 489 * column validation. 490 */ 491 public function addFilter(Filter $filter) 492 { 493 $this->query->addFilter($filter); 494 return $this; 495 } 496 497 /** 498 * Count result set 499 * 500 * @return int 501 */ 502 public function count() 503 { 504 return $this->query->count(); 505 } 506 507 /** 508 * Set whether the query should peek ahead for more results 509 * 510 * Enabling this causes the current query limit to be increased by one. The potential extra row being yielded will 511 * be removed from the result set. Note that this only applies when fetching multiple results of limited queries. 512 * 513 * @return $this 514 */ 515 public function peekAhead($state = true) 516 { 517 $this->query->peekAhead($state); 518 return $this; 519 } 520 521 /** 522 * Return whether the query did not yield all available results 523 * 524 * @return bool 525 */ 526 public function hasMore() 527 { 528 return $this->query->hasMore(); 529 } 530 531 /** 532 * Return whether this query will or has yielded any result 533 * 534 * @return bool 535 */ 536 public function hasResult() 537 { 538 return $this->query->hasResult(); 539 } 540 541 /** 542 * Set a limit count and offset 543 * 544 * @param int $count Number of rows to return 545 * @param int $offset Start returning after this many rows 546 * 547 * @return self 548 */ 549 public function limit($count = null, $offset = null) 550 { 551 $this->query->limit($count, $offset); 552 return $this; 553 } 554 555 /** 556 * Whether a limit is set 557 * 558 * @return bool 559 */ 560 public function hasLimit() 561 { 562 return $this->query->hasLimit(); 563 } 564 565 /** 566 * Get the limit if any 567 * 568 * @return int|null 569 */ 570 public function getLimit() 571 { 572 return $this->query->getLimit(); 573 } 574 575 /** 576 * Whether an offset is set 577 * 578 * @return bool 579 */ 580 public function hasOffset() 581 { 582 return $this->query->hasOffset(); 583 } 584 585 /** 586 * Get the offset if any 587 * 588 * @return int|null 589 */ 590 public function getOffset() 591 { 592 return $this->query->getOffset(); 593 } 594 595 /** 596 * Retrieve an array containing all rows of the result set 597 * 598 * @return array 599 */ 600 public function fetchAll() 601 { 602 return $this->getQuery()->fetchAll(); 603 } 604 605 /** 606 * Fetch the first row of the result set 607 * 608 * @return mixed 609 */ 610 public function fetchRow() 611 { 612 return $this->getQuery()->fetchRow(); 613 } 614 615 /** 616 * Fetch the first column of all rows of the result set as an array 617 * 618 * @return array 619 */ 620 public function fetchColumn() 621 { 622 return $this->getQuery()->fetchColumn(); 623 } 624 625 /** 626 * Fetch the first column of the first row of the result set 627 * 628 * @return string 629 */ 630 public function fetchOne() 631 { 632 return $this->getQuery()->fetchOne(); 633 } 634 635 /** 636 * Fetch all rows of the result set as an array of key-value pairs 637 * 638 * The first column is the key, the second column is the value. 639 * 640 * @return array 641 */ 642 public function fetchPairs() 643 { 644 return $this->getQuery()->fetchPairs(); 645 } 646} 647