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