Merge "Make it possible to sort on simple custom columns"
[ninja.git] / modules / reports / libraries / Report_options.php
blob1374c9f5a9139d16492313426b0baf160121f48e
1 <?php defined('SYSPATH') OR die('No direct access allowed.');
3 /**
4 * Report_options is an object representing the user-selected report
6 * It's created to improve consistency between report types and frontend/backend
7 */
8 class Report_options implements ArrayAccess, Iterator, Countable {
9 /**
10 * Can contains options that must be renamed when provided - for legacy links
12 protected $rename_options;
14 /**
15 * Contains a definition of all legal keys for this type of Report_options
17 protected $properties = array(
18 'report_id' => array(
19 'type' => 'int',
20 'default' => false,
21 'description' => 'Saved report id - not to be confused with schedule_id'
23 'report_name' => array(
24 'type' => 'string',
25 'default' => false,
26 'description' => 'Name of the report'
28 'report_type' => array(
29 'type' => 'enum',
30 'default' => false,
31 'options' => array(
32 'hosts' => 'host_name',
33 'services' => 'service_description',
34 'hostgroups' => 'hostgroup',
35 'servicegroups' => 'servicegroup'
37 'description' => 'The type of objects in the report, set automatically by setting the actual objects'
39 'report_period' => array(
40 'type' => 'enum',
41 'default' => 'last7days',
42 'description' => 'A report period to generate the report over, may automatically set {start,end}_time'
44 'rpttimeperiod' => array(
45 'type' => 'enum',
46 'default' => false,
47 'description' => 'If we are to mask the alerts by a certain (nagios) timeperiod, and if so, which one'
49 'scheduleddowntimeasuptime' => array(
50 'type' => 'enum',
51 'default' => 0,
52 'description' => 'Schedule downtime as uptime: yes, no, "yes, but tell me when you cheated"'
54 'assumestatesduringnotrunning' => array(
55 'type' => 'bool',
56 'default' => true,
57 'description' => 'Whether to assume states during not running'
59 'includesoftstates' => array(
60 'type' => 'bool',
61 'default' => false, 'description' => 'Include soft states, yes/no?'
63 'host_name' => array(
64 'type' => 'objsel',
65 'default' => array(),
66 'description' => 'Hosts to include (note: array)'
68 'service_description' => array(
69 'type' => 'objsel',
70 'default' => array(),
71 'description' => 'Services to include (note: array)'
73 'hostgroup' => array(
74 'type' => 'array',
75 'default' => array(),
76 'description' => 'Hostgroups to include (note: array)'
78 'servicegroup' => array(
79 'type' => 'array',
80 'default' => array(),
81 'description' => 'Servicegroups to include (note: array)'
83 'start_time' => array(
84 'type' => 'timestamp',
85 'default' => 0,
86 'description' => 'Start time for report, timestamp or date-like string'
88 'end_time' => array(
89 'type' => 'timestamp',
90 'default' => 0,
91 'description' => 'End time for report, timestamp or date-like string'
93 'sla_mode' => array(
94 'type' => 'enum',
95 'default' => 0,
96 'description' => 'Use worst, average, or best state for calculating overall health'
98 'host_filter_status' => array(
99 'type' => 'array',
100 'default' => array(),
101 'description' => 'Key: hide these. Value: map them to this instead (-2 means "secret")'
103 'service_filter_status' => array(
104 'type' => 'array',
105 'default' => array(),
106 'description' => 'Key: hide these. Value: map them to this instead (-2 means "secret")'
108 'output_format' => array(
109 'type' => 'enum',
110 'default' => 'html',
111 'options' => array(
112 'html' => 'html',
113 'csv' => 'csv',
114 'pdf' => 'pdf'
116 'generated' => true,
117 'description' => 'What type of report to generate (manually selected in web UI, generated from filename for saved reports)'
119 'skin' => array(
120 'type' => 'string',
121 'default' => '',
122 'description' => 'Use the following skin for rendering the report'
124 'use_alias' => array(
125 'type' => 'bool',
126 'default' => false,
127 'description' => "Use object's aliases instead of their names"
129 'description' => array(
130 'type' => 'string',
131 'default' => false
133 'include_alerts' => array(
134 'type' => 'bool',
135 'default' => false
140 * Placeholder used instead of fetching all objects the user is
141 * authorized to see, to be able to fetch lazily and avoid large
142 * queries or filtering large result sets.
144 const ALL_AUTHORIZED = '*';
147 * The the explicitly set options. Should not be accessed directly,
148 * outside of debugging.
150 public $options = array();
153 * Properties should be completely setup - with translations and all - before
154 * loading any options, and options are loaded by the construct, so do
155 * initialization here.
157 public function setup_properties()
159 if (isset($this->properties['report_period']))
160 $this->properties['report_period']['options'] = array(
161 "today" => _('Today'),
162 "last24hours" => _('Last 24 hours'),
163 "yesterday" => _('Yesterday'),
164 "thisweek" => _('This week'),
165 "last7days" => _('Last 7 days'),
166 "lastweek" => _('Last week'),
167 "thismonth" => _('This month'),
168 "last31days" => _('Last 31 days'),
169 "lastmonth" => _('Last month'),
170 'last3months' => _('Last 3 months'),
171 'lastquarter' => _('Last quarter'),
172 'last6months' => _('Last 6 months'),
173 'last12months' => _('Last 12 months'),
174 "thisyear" => _('This year'),
175 "lastyear" => _('Last year'),
176 'custom' => _('Custom'));
177 if (isset($this->properties['scheduleddowntimeasuptime']))
178 $this->properties['scheduleddowntimeasuptime']['options'] = array(
179 0 => _('Actual state'),
180 1 => _('Uptime'),
181 2 => _('Uptime, with difference'));
182 if (isset($this->properties['sla_mode']))
183 $this->properties['sla_mode']['options'] = array(
184 0 => _('Group availability (Worst state)'),
185 1 => _('Average'),
186 2 => _('Cluster mode (Best state)'));
187 if (isset($this->properties['rpttimeperiod']))
188 $this->properties['rpttimeperiod']['options'] = Old_Timeperiod_Model::get_all();
189 if (isset($this->properties['skin']))
190 $this->properties['skin']['default'] = config::get('config.current_skin', '*');
192 $this->rename_options = array(
193 't1' => 'start_time',
194 't2' => 'end_time',
195 'host' => 'host_name',
196 'service' => 'service_description',
197 'hostgroup_name' => 'hostgroup',
198 'servicegroup_name' => 'servicegroup'
203 * Public constructor, which optionally takes an iterable with properties to set
205 public function __construct($options=false) {
206 $this->setup_properties();
207 if ($options)
208 $this->set_options($options);
212 * Required by ArrayAccess
214 public function offsetGet($str)
216 if (!isset($this->properties[$str]))
217 return NULL;
219 return arr::search($this->options, $str, $this->properties[$str]['default']);
223 * Required by ArrayAccess
225 public function offsetSet($key, $val)
227 $this->set($key, $val);
231 * Required by ArrayAccess
233 public function offsetExists($key)
235 return isset($this->properties[$key]);
239 * Required by ArrayAccess
241 public function offsetUnset($key)
243 unset($this->options[$key]);
247 * This looks silly...
249 function properties()
251 return $this->properties;
255 * For applicable keys, this returns a list of all possible values
257 public function get_alternatives($key) {
258 if (!isset($this->properties[$key]))
259 return false;
260 if ($this->properties[$key]['type'] !== 'enum' && $this->properties[$key]['type'] !== 'array')
261 return false;
262 return $this->properties[$key]['options'];
266 * Returns the user-friendly value, given a machine-friendly option key
268 public function get_value($key) {
269 if (!isset($this->properties[$key]))
270 return false;
271 if ($this->properties[$key]['type'] !== 'enum')
272 return false;
273 if (!isset($this->properties[$key]['options'][$this[$key]]))
274 return $key;
275 return $this->properties[$key]['options'][$this[$key]];
279 * Return all objects (hosts or services) this report applies to
280 * This will return hosts or services regardless if the report object selection
281 * uses groups or not.
283 public function get_report_members() {
284 switch ($this['report_type']) {
285 case 'hosts':
286 case 'services':
287 return $this[$this->get_value('report_type')];
288 case 'hostgroups':
289 $all = HostPool_Model::all();
290 $set = HostPool_Model::none();
292 foreach ($this['hostgroup'] as $group) {
293 $set = $set->union($all->reduce_by('groups', $group, '>='));
295 $res = array();
296 foreach ($set->it(array('name'), array()) as $arr) {
297 $res[] = $arr->get_key();
299 return $res;
300 case 'servicegroups':
301 $all = ServicePool_Model::all();
302 $set = ServicePool_Model::none();
304 foreach ($this['servicegroup'] as $group) {
305 $set = $set->union($all->reduce_by('groups', $group, '>='));
307 $res = array();
308 foreach ($set->it(array('host.name', 'description'), array()) as $arr) {
309 $res[] = $arr->get_key();
311 return $res;
313 return false;
317 * Update the options for the report
318 * @param $options New options
320 public function set_options($options)
322 $errors = false;
323 foreach ($options as $name => $value) {
324 $errors |= intval(!$this->set($name, $value));
327 return $errors ? false : true;
332 * Calculates $this['start_time'] and $this['end_time'] based on an
333 * availability report style period such as "today", "last24hours"
334 * or "lastmonth".
336 * @param $report_period The textual period to set our options by
337 * @return false on errors, true on success
339 protected function calculate_time($report_period)
341 $now = time();
342 $year_now = date('Y', $now);
343 $month_now = date('m', $now);
344 $day_now = date('d', $now);
345 $week_now = date('W', $now);
346 $weekday_now = date('w', $now)-1;
347 $time_start = false;
348 $time_end = false;
350 switch ($report_period) {
351 case 'today':
352 $time_start = mktime(0, 0, 0, $month_now, $day_now, $year_now);
353 $time_end = $now;
354 break;
355 case 'last24hours':
356 $time_start = mktime(date('H', $now), date('i', $now), date('s', $now), $month_now, $day_now -1, $year_now);
357 $time_end = $now;
358 break;
359 case 'yesterday':
360 $time_start = mktime(0, 0, 0, $month_now, $day_now -1, $year_now);
361 $time_end = mktime(0, 0, 0, $month_now, $day_now, $year_now);
362 break;
363 case 'thisweek':
364 $time_start = strtotime('today - '.$weekday_now.' days');
365 $time_end = $now;
366 break;
367 case 'last7days':
368 $time_start = strtotime('now - 7 days');
369 $time_end = $now;
370 break;
371 case 'lastweek':
372 $time_start = strtotime('midnight last monday -7 days');
373 $time_end = strtotime('midnight last monday');
374 break;
375 case 'thismonth':
376 $time_start = strtotime('midnight '.$year_now.'-'.$month_now.'-01');
377 $time_end = $now;
378 break;
379 case 'last31days':
380 $time_start = strtotime('now - 31 days');
381 $time_end = $now;
382 break;
383 case 'lastmonth':
384 $time_start = strtotime('midnight '.$year_now.'-'.$month_now.'-01 -1 month');
385 $time_end = strtotime('midnight '.$year_now.'-'.$month_now.'-01');
386 break;
387 case 'thisyear':
388 $time_start = strtotime('midnight '.$year_now.'-01-01');
389 $time_end = $now;
390 break;
391 case 'lastyear':
392 $time_start = strtotime('midnight '.$year_now.'-01-01 -1 year');
393 $time_end = strtotime('midnight '.$year_now.'-01-01');
394 break;
395 case 'last12months':
396 $time_start = strtotime('midnight '.$year_now.'-'.$month_now.'-01 -12 months');
397 $time_end = strtotime('midnight '.$year_now.'-'.$month_now.'-01');
398 break;
399 case 'last3months':
400 $time_start = strtotime('midnight '.$year_now.'-'.$month_now.'-01 -3 months');
401 $time_end = strtotime('midnight '.$year_now.'-'.$month_now.'-01');
402 break;
403 case 'last6months':
404 $time_start = strtotime('midnight '.$year_now.'-'.$month_now.'-01 -6 months');
405 $time_end = strtotime('midnight '.$year_now.'-'.$month_now.'-01');
406 break;
407 case 'lastquarter':
408 $t = getdate();
409 if($t['mon'] <= 3){
410 $lqstart = ($t['year']-1)."-10-01";
411 $lqend = ($t['year']-1)."-12-31";
412 } elseif ($t['mon'] <= 6) {
413 $lqstart = $t['year']."-01-01";
414 $lqend = $t['year']."-03-31";
415 } elseif ($t['mon'] <= 9){
416 $lqstart = $t['year']."-04-01";
417 $lqend = $t['year']."-06-30";
418 } else {
419 $lqstart = $t['year']."-07-01";
420 $lqend = $t['year']."-09-30";
422 $time_start = strtotime($lqstart);
423 $time_end = strtotime($lqend);
424 break;
425 case 'custom':
426 # we'll have "start_time" and "end_time" in
427 # the options when this happens
428 return true;
429 default:
430 # unknown option, ie bogosity
431 return false;
434 if($time_start > $now)
435 $time_start = $now;
437 if($time_end > $now)
438 $time_end = $now;
440 $this->options['start_time'] = $time_start;
441 $this->options['end_time'] = $time_end;
442 return true;
446 * Set an option, with some validation
448 * @param $name Option name
449 * @param $value Option value
450 * @return false on error, else true
452 public function set($name, $value)
454 if (isset($this->rename_options[$name])) {
455 if (is_callable($this->rename_options[$name])) {
456 try {
457 $value = call_user_func($this->rename_options[$name], $name, $value, $this);
458 } catch (Exception $e) {
459 return false;
462 else if (is_string($this->rename_options[$name])) {
463 $name = $this->rename_options[$name];
467 if (!$this->validate_value($name, $value)) {
468 return false;
470 return $this->update_value($name, $value);
474 * Validates that $value isn't obviously unsuitable for $key
476 * Warning: you probably want to use set() or utilize the ArrayAccess API
478 protected function validate_value($key, &$value)
480 if (!isset($this->properties[$key])) {
481 return false;
483 switch ($this->properties[$key]['type']) {
484 case 'bool':
485 if ($value == 1 || !strcasecmp($value, "true") || !empty($value))
486 $value = true;
487 else
488 $value = false;
489 break;
490 case 'int':
491 if (!is_numeric($value) || $value != intval($value))
492 return false;
493 $value = intval($value);
494 break;
495 case 'string':
496 if (!is_string($value))
497 return false;
498 break;
499 case 'objsel':
500 if ($value == self::ALL_AUTHORIZED)
501 return true;
502 case 'array':
503 if (!is_array($value))
504 return false;
505 break;
506 case 'timestamp':
507 if (!is_numeric($value)) {
508 if (strstr($value, '-') === false)
509 return false;
510 $value = strtotime($value);
511 if ($value === false)
512 return false;
514 break;
515 case 'object':
516 if (!is_object($value)) {
517 return false;
519 break;
520 case 'enum':
521 if (!isset($this->properties[$key]['options'][$value]))
522 return false;
523 break;
524 default:
525 # this is an exception and should never ever happen
526 return false;
528 return true;
532 * Will actually set the provided $name to the value $value
534 * Warning: you probably want to use set() or utilize the ArrayAccess API
536 protected function update_value($name, $value)
538 switch ($name) {
539 case 'report_period':
540 if (!$this->calculate_time($value))
541 return false;
542 break;
543 case 'summary_items':
544 if ($value < 0)
545 return false;
546 break;
547 case 'host_filter_status':
548 $value = array_intersect_key($value, Reports_Model::$host_states);
549 $value = array_filter($value, function($val) {
550 return is_numeric($val);
552 break;
553 case 'service_filter_status':
554 $value = array_intersect_key($value, Reports_Model::$service_states);
555 $value = array_filter($value, function($val) {
556 return is_numeric($val);
558 break;
559 case 'host_name':
560 if (!$value)
561 return false;
562 $this->options['hostgroup'] = array();
563 $this->options['servicegroup'] = array();
564 $this->options['service_description'] = array();
565 $this->options['host_name'] = $value;
566 $this['report_type'] = 'hosts';
567 $this->hosts = array();
568 return true;
569 case 'service_description':
570 if (!$value)
571 return false;
572 if($value != self::ALL_AUTHORIZED) {
573 foreach ($value as $svc) {
574 if (strpos($svc, ';') === false)
575 return false;
578 $this->options['hostgroup'] = array();
579 $this->options['servicegroup'] = array();
580 $this->options['host_name'] = array();
581 $this->options['service_description'] = $value;
582 $this['report_type'] = 'services';
583 $this->services = array();
584 return true;
585 case 'hostgroup':
586 if (!$value)
587 return false;
588 $this->options['host_name'] = array();
589 $this->options['service_description'] = array();
590 $this->options['servicegroup'] = array();
591 $this->options['hostgroup'] = $value;
592 $this['report_type'] = 'hostgroups';
593 $this->hosts = array();
594 return true;
595 case 'servicegroup':
596 if (!$value)
597 return false;
598 $this->options['host_name'] = array();
599 $this->options['service_description'] = array();
600 $this->options['hostgroup'] = array();
601 $this->options['servicegroup'] = $value;
602 $this['report_type'] = 'servicegroups';
603 $this->services = array();
604 return true;
605 case 'start_time':
606 case 'end_time':
607 // value "impossible", or value already set by report_period
608 // (we consider anything before 1980 impossible, or at least unreasonable)
609 if ($value <= 315525600 || $value === 'undefined' || (isset($this->options[$name]) && isset($this->options['report_period']) && $this->options['report_period'] != 'custom'))
610 return false;
611 if (!is_numeric($value))
612 $value = strtotime($value);
613 break;
614 default:
615 break;
617 if (!isset($this->properties[$name]))
618 return false;
619 $this->options[$name] = $value;
620 return true;
624 * Generate a standard HTTP keyval string, suitable for URLs or POST bodies.
625 * @param $anonymous If true, any option on the exact objects in this report
626 * will be purged, so it's suitable for linking to sub-reports.
627 * If false, all options will be kept, completely describing
628 * this exact report.
629 * @param $obj_only Does more-or-less the inverse of $anonymous - if true, don't
630 * include anything that does not refer to the members of the report.
632 public function as_keyval_string($anonymous=false, $obj_only=false) {
633 $opts_str = '';
634 foreach ($this as $key => $val) {
635 if ($obj_only && !in_array($key, array('host_name', 'service_description', 'hostgroup', 'servicegroup', 'report_type')))
636 continue;
637 if ($anonymous && in_array($key, array('host_name', 'service_description', 'hostgroup', 'servicegroup', 'report_type', 'report_id')))
638 continue;
639 if (is_array($val)) {
640 foreach ($val as $vk => $member) {
641 $opts_str .= "&amp;{$key}[$vk]=".urlencode($member);
643 continue;
645 $opts_str .= "&amp;$key=".urlencode($val);
647 return substr($opts_str, 5);
651 * Return the report as a HTML string of hidden form elements
653 public function as_form($anonymous=false, $obj_only=false) {
654 $html_options = '';
655 foreach ($this as $key => $val) {
656 if ($obj_only && !in_array($key, array('host_name', 'service_description', 'hostgroup', 'servicegroup', 'report_type')))
657 continue;
658 if ($anonymous && in_array($key, array('host_name', 'service_description', 'hostgroup', 'servicegroup', 'report_type', 'report_id')))
659 continue;
660 if (is_array($val)) {
661 foreach ($val as $k => $v)
662 $html_options .= form::hidden($key.'['.$k.']', $v);
664 else {
665 $html_options .= form::hidden($key, $val);
668 return $html_options;
672 * Return the report as a JSON string
674 public function as_json() {
675 $opts = $this->options;
676 if ($this->get_value('report_type')) {
677 // because the person who wrote the js became sick of all our special cases,
678 // it expects the objects to be called 'objects'. Which makes sense, really...
679 if ($this[$this->get_value('report_type')])
680 $opts['objects'] = $this[$this->get_value('report_type')];
681 unset($opts[$this->get_value('report_type')]);
683 return json_encode($opts);
687 * Expand the private structure, to make a traversal iterate over all the properties
689 public function expand() {
690 foreach ($this->properties as $key => $_) {
691 $this[$key] = $this[$key];
696 * Return the given timestamp typed property as a date string of the configured kind
698 public function get_date($var) {
699 $format = cal::get_calendar_format(true);
700 return date($format, $this[$var]);
704 * Return the given timestamp typed property as a time string
706 public function get_time($var) {
707 return date('H:i', $this[$var]);
710 /** Required by Iterator */
711 function rewind() { reset($this->options); }
712 /** Required by Iterator */
713 function current() { return current($this->options); }
714 /** Required by Iterator */
715 function key() { return key($this->options); }
716 /** Required by Iterator */
717 function next() {
718 do {
719 $x = next($this->options);
720 } while ($x !== false && isset($this->properties[key($this->options)]['generated']));
722 /** Required by Iterator */
723 function valid() { return array_key_exists(key($this->options), $this->options); }
724 /** Required by Countable */
725 function count() { return count($this->options); }
727 /** Print the options themselves when printing the object */
728 function __toString() { return var_export($this->options, true); }
731 * Finds properties to inject into.. myself
733 * You probably want setup_options_obj instead.
735 * @param $input array = false Autodiscovers options using superglobals: $input > POST > GET
736 * @return array
738 static function discover_options($input = false)
740 # not using $_REQUEST, because that includes weird, scary session vars
741 if (!empty($input)) {
742 $report_info = $input;
743 } else if (!empty($_POST)) {
744 $report_info = $_POST;
745 } else {
746 $report_info = $_GET;
749 if(isset($report_info['cal_start'], $report_info['cal_end'], $report_info['report_period']) &&
750 $report_info['cal_start'] &&
751 $report_info['cal_end'] &&
752 $report_info['report_period'] == 'custom'
755 if(!isset($report_info['time_start']) || $report_info['time_start'] === "") {
756 $report_info['time_start'] = "00:00";
758 if(!isset($report_info['time_end']) || $report_info['time_end'] === "") {
759 $report_info['time_end'] = "23:59";
762 $report_info['start_time'] = DateTime::createFromFormat(
763 nagstat::date_format(),
764 $report_info['cal_start'].' '.$report_info['time_start'].':00'
765 )->getTimestamp();
766 $report_info['end_time'] = DateTime::createFromFormat(
767 nagstat::date_format(),
768 $report_info['cal_end'].' '.$report_info['time_end'].':00'
769 )->getTimestamp();
771 unset(
772 $report_info['cal_start'],
773 $report_info['cal_end'],
774 $report_info['time_start'],
775 $report_info['time_end']
779 return $report_info;
783 * So, my issue is, basically, that op5reports needs to override how what
784 * is loaded from DB, and our saved_reports_model is useless and fragile
785 * and scary and I'll kill it, as part of fixing #7491 at the very latest.
786 * Until then, I need to expose this algorithm so op5report can access it
787 * without having to copy-paste it or access the functionality the normal
788 * way.
791 static protected function merge_with_loaded($options, $report_info, $saved_report_info)
793 if (empty($saved_report_info))
794 return false;
795 foreach ($saved_report_info as $key => $sri) {
796 if (isset($report_info[$key]) || !isset($options->properties[$key]) || $options[$key] !== $options->properties[$key]['default'] || ($options->properties[$key]['type'] === 'bool' && count($report_info) > 3))
797 continue;
798 $options[$key] = $sri;
800 if ($options['report_type'] && isset($saved_report_info['objects']) && !$options[$options->get_value('report_type')])
801 $options[$options->get_value('report_type')] = $saved_report_info['objects'];
802 return true;
806 * Combines the provided properties with any saved information.
808 * You probably want setup_options_obj instead.
810 protected static function create_options_obj($type, $report_info = false) {
811 $options = new static($report_info);
813 if (isset($report_info['report_id']) && !empty($report_info['report_id'])) {
814 $saved_report_info = Saved_reports_Model::get_report_info($type, $report_info['report_id']);
815 if (!empty($saved_report_info))
816 static::merge_with_loaded($options, $report_info, $saved_report_info);
817 else
818 unset($options['report_id']);
820 if (isset($options->properties['report_period']) && !isset($options->options['report_period']) && isset($options->properties['report_period']['default']))
821 $options->calculate_time($options['report_period']);
822 return $options;
826 * @param $type string avail|sla|summary
827 * @param $input array = false
828 * @return Report_options
830 public static function setup_options_obj($type, $input = false)
832 if (is_a($input, 'Report_options')) {
833 $class = get_class($input);
834 return new $class($input);
836 $class = ucfirst($type) . '_options';
837 if (!class_exists($class))
838 $class = 'Report_options';
840 $report_info = $class::discover_options($input);
841 $options = $class::create_options_obj($type, $report_info);
842 return $options;