1<?php
2/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
3
4namespace Icinga\Module\Monitoring\Object;
5
6use stdClass;
7use InvalidArgumentException;
8use Icinga\Authentication\Auth;
9use Icinga\Application\Config;
10use Icinga\Data\Filter\Filter;
11use Icinga\Data\Filterable;
12use Icinga\Exception\InvalidPropertyException;
13use Icinga\Exception\ProgrammingError;
14use Icinga\Module\Monitoring\Backend\MonitoringBackend;
15use Icinga\Util\GlobFilter;
16use Icinga\Web\UrlParams;
17
18/**
19 * A monitored Icinga object, i.e. host or service
20 */
21abstract class MonitoredObject implements Filterable
22{
23    /**
24     * Type host
25     */
26    const TYPE_HOST = 'host';
27
28    /**
29     * Type service
30     */
31    const TYPE_SERVICE = 'service';
32
33    /**
34     * Acknowledgement of the host or service if any
35     *
36     * @var object
37     */
38    protected $acknowledgement;
39
40    /**
41     * Backend to fetch object information from
42     *
43     * @var MonitoringBackend
44     */
45    protected $backend;
46
47    /**
48     * Comments
49     *
50     * @var array
51     */
52    protected $comments;
53
54    /**
55     * This object's obfuscated custom variables
56     *
57     * @var array
58     */
59    protected $customvars;
60
61    /**
62     * The host custom variables
63     *
64     * @var array
65     */
66    protected $hostVariables;
67
68    /**
69     * The service custom variables
70     *
71     * @var array
72     */
73    protected $serviceVariables;
74
75    /**
76     * Contact groups
77     *
78     * @var array
79     */
80    protected $contactgroups;
81
82    /**
83     * Contacts
84     *
85     * @var array
86     */
87    protected $contacts;
88
89    /**
90     * Downtimes
91     *
92     * @var array
93     */
94    protected $downtimes;
95
96    /**
97     * Event history
98     *
99     * @var \Icinga\Module\Monitoring\DataView\EventHistory
100     */
101    protected $eventhistory;
102
103    /**
104     * Filter
105     *
106     * @var Filter
107     */
108    protected $filter;
109
110    /**
111     * Host groups
112     *
113     * @var array
114     */
115    protected $hostgroups;
116
117    /**
118     * Prefix of the Icinga object, i.e. 'host_' or 'service_'
119     *
120     * @var string
121     */
122    protected $prefix;
123
124    /**
125     * Properties
126     *
127     * @var object
128     */
129    protected $properties;
130
131    /**
132     * Service groups
133     *
134     * @var array
135     */
136    protected $servicegroups;
137
138    /**
139     * Type of the Icinga object, i.e. 'host' or 'service'
140     *
141     * @var string
142     */
143    protected $type;
144
145    /**
146     * Stats
147     *
148     * @var object
149     */
150    protected $stats;
151
152    /**
153     * The properties to hide from the user
154     *
155     * @var GlobFilter
156     */
157    protected $blacklistedProperties = null;
158
159    /**
160     * Create a monitored object, i.e. host or service
161     *
162     * @param MonitoringBackend $backend Backend to fetch object information from
163     */
164    public function __construct(MonitoringBackend $backend)
165    {
166        $this->backend = $backend;
167    }
168
169    /**
170     * Get the object's data view
171     *
172     * @return \Icinga\Module\Monitoring\DataView\DataView
173     */
174    abstract protected function getDataView();
175
176    /**
177     * Get all note urls configured for this monitored object
178     *
179     * @return array All note urls as a string
180     */
181    abstract public function getNotesUrls();
182
183    /**
184     * {@inheritdoc}
185     */
186    public function addFilter(Filter $filter)
187    {
188        // Left out on purpose. Interface is deprecated.
189    }
190
191    /**
192     * {@inheritdoc}
193     */
194    public function applyFilter(Filter $filter)
195    {
196        $this->getFilter()->addFilter($filter);
197
198        return $this;
199    }
200
201    /**
202     * {@inheritdoc}
203     */
204    public function getFilter()
205    {
206        if ($this->filter === null) {
207            $this->filter = Filter::matchAll();
208        }
209
210        return $this->filter;
211    }
212
213    /**
214     * {@inheritdoc}
215     */
216    public function setFilter(Filter $filter)
217    {
218        // Left out on purpose. Interface is deprecated.
219    }
220
221    /**
222     * {@inheritdoc}
223     */
224    public function where($condition, $value = null)
225    {
226        // Left out on purpose. Interface is deprecated.
227    }
228
229    /**
230     * Return whether this object matches the given filter
231     *
232     * @param   Filter  $filter
233     *
234     * @return  bool
235     *
236     * @throws  ProgrammingError    In case the object cannot be found
237     *
238     * @deprecated      Use $filter->matches($object) instead
239     */
240    public function matches(Filter $filter)
241    {
242        if ($this->properties === null && $this->fetch() === false) {
243            throw new ProgrammingError(
244                'Unable to apply filter. Object %s of type %s not found.',
245                $this->getName(),
246                $this->getType()
247            );
248        }
249
250        return $filter->matches($this);
251    }
252
253    /**
254     * Require the object's type to be one of the given types
255     *
256     * @param   array $oneOf
257     *
258     * @return  bool
259     * @throws  InvalidArgumentException If the object's type is not one of the given types.
260     */
261    public function assertOneOf(array $oneOf)
262    {
263        if (! in_array($this->type, $oneOf)) {
264            throw new InvalidArgumentException;
265        }
266        return true;
267    }
268
269    /**
270     * Fetch the object's properties
271     *
272     * @return bool
273     */
274    public function fetch()
275    {
276        $properties = $this->getDataView()->applyFilter($this->getFilter())->getQuery()->fetchRow();
277
278        if ($properties === false) {
279            return false;
280        }
281
282        if (isset($properties->host_contacts)) {
283            $this->contacts = array();
284            foreach (preg_split('~,~', $properties->host_contacts) as $contact) {
285                $this->contacts[] = (object) array(
286                    'contact_name'  => $contact,
287                    'contact_alias' => $contact,
288                    'contact_email' => null,
289                    'contact_pager' => null,
290                );
291            }
292        }
293
294        $this->properties = $properties;
295
296        return true;
297    }
298
299    /**
300     * Fetch the object's acknowledgement
301     */
302    public function fetchAcknowledgement()
303    {
304        if ($this->comments === null) {
305            $this->fetchComments();
306        }
307
308        return $this;
309    }
310
311    /**
312     * Fetch the object's comments
313     *
314     * @return $this
315     */
316    public function fetchComments()
317    {
318        $commentsView = $this->backend->select()->from('comment', array(
319            'author'            => 'comment_author_name',
320            'comment'           => 'comment_data',
321            'expiration'        => 'comment_expiration',
322            'id'                => 'comment_internal_id',
323            'name'              => 'comment_name',
324            'persistent'        => 'comment_is_persistent',
325            'timestamp'         => 'comment_timestamp',
326            'type'              => 'comment_type'
327        ));
328        if ($this->type === self::TYPE_SERVICE) {
329            $commentsView
330                ->where('service_host_name', $this->host_name)
331                ->where('service_description', $this->service_description);
332        } else {
333            $commentsView->where('host_name', $this->host_name);
334        }
335        $commentsView
336            ->where('comment_type', array('ack', 'comment'))
337            ->where('object_type', $this->type);
338
339        $comments = $commentsView->fetchAll();
340
341        if ((bool) $this->properties->{$this->prefix . 'acknowledged'}) {
342            $ackCommentIdx = null;
343
344            foreach ($comments as $i => $comment) {
345                if ($comment->type === 'ack') {
346                    $this->acknowledgement = new Acknowledgement(array(
347                        'author'            => $comment->author,
348                        'comment'           => $comment->comment,
349                        'entry_time'        => $comment->timestamp,
350                        'expiration_time'   => $comment->expiration,
351                        'sticky'            => (int) $this->properties->{$this->prefix . 'acknowledgement_type'} === 2
352                    ));
353                    $ackCommentIdx = $i;
354                    break;
355                }
356            }
357
358            if ($ackCommentIdx !== null) {
359                unset($comments[$ackCommentIdx]);
360            }
361        }
362
363        $this->comments = $comments;
364
365        return $this;
366    }
367
368    /**
369     * Fetch the object's contact groups
370     *
371     * @return $this
372     */
373    public function fetchContactgroups()
374    {
375        $contactsGroups = $this->backend->select()->from('contactgroup', array(
376            'contactgroup_name',
377            'contactgroup_alias'
378        ));
379        if ($this->type === self::TYPE_SERVICE) {
380            $contactsGroups
381                ->where('service_host_name', $this->host_name)
382                ->where('service_description', $this->service_description);
383        } else {
384            $contactsGroups->where('host_name', $this->host_name);
385        }
386        $this->contactgroups = $contactsGroups;
387        return $this;
388    }
389
390    /**
391     * Fetch the object's contacts
392     *
393     * @return $this
394     */
395    public function fetchContacts()
396    {
397        $contacts = $this->backend->select()->from("{$this->type}contact", array(
398            'contact_name',
399            'contact_alias',
400            'contact_email',
401            'contact_pager',
402        ));
403        if ($this->type === self::TYPE_SERVICE) {
404            $contacts
405                ->where('service_host_name', $this->host_name)
406                ->where('service_description', $this->service_description);
407        } else {
408            $contacts->where('host_name', $this->host_name);
409        }
410        $this->contacts = $contacts;
411        return $this;
412    }
413
414    /**
415     * Fetch this object's obfuscated custom variables
416     *
417     * @return  $this
418     */
419    public function fetchCustomvars()
420    {
421        $blacklist = array();
422        $blacklistPattern = '';
423
424        if (($blacklistConfig = Config::module('monitoring')->get('security', 'protected_customvars', '')) !== '') {
425            foreach (explode(',', $blacklistConfig) as $customvar) {
426                $nonWildcards = array();
427                foreach (explode('*', $customvar) as $nonWildcard) {
428                    $nonWildcards[] = preg_quote($nonWildcard, '/');
429                }
430                $blacklist[] = implode('.*', $nonWildcards);
431            }
432            $blacklistPattern = '/^(' . implode('|', $blacklist) . ')$/i';
433        }
434
435        if ($this->type === self::TYPE_SERVICE) {
436            $this->fetchServiceVariables();
437            $customvars = $this->serviceVariables;
438        } else {
439            $this->fetchHostVariables();
440            $customvars = $this->hostVariables;
441        }
442
443        $this->customvars = $customvars;
444        $this->hideBlacklistedProperties();
445
446        if ($blacklistPattern) {
447            $this->customvars = $this->obfuscateCustomVars($this->customvars, $blacklistPattern);
448        }
449
450        return $this;
451    }
452
453    /**
454     * Obfuscate custom variables recursively
455     *
456     * @param stdClass|array    $customvars         The custom variables to obfuscate
457     * @param string            $blacklistPattern   Which custom variables to obfuscate
458     *
459     * @return stdClass|array                       The obfuscated custom variables
460     */
461    protected function obfuscateCustomVars($customvars, $blacklistPattern)
462    {
463        $obfuscatedCustomVars = array();
464        foreach ($customvars as $name => $value) {
465            if ($blacklistPattern && preg_match($blacklistPattern, $name)) {
466                $obfuscatedCustomVars[$name] = '***';
467            } else {
468                $obfuscatedCustomVars[$name] = $value instanceof stdClass || is_array($value)
469                    ? $this->obfuscateCustomVars($value, $blacklistPattern)
470                    : $value;
471            }
472        }
473        return $customvars instanceof stdClass ? (object) $obfuscatedCustomVars : $obfuscatedCustomVars;
474    }
475
476    /**
477     * Hide all blacklisted properties from the user as restricted by monitoring/blacklist/properties
478     *
479     * Currently this only affects the custom variables
480     */
481    protected function hideBlacklistedProperties()
482    {
483        if ($this->blacklistedProperties === null) {
484            $this->blacklistedProperties = new GlobFilter(
485                Auth::getInstance()->getRestrictions('monitoring/blacklist/properties')
486            );
487        }
488
489        $allProperties = $this->blacklistedProperties->removeMatching(
490            array($this->type => array('vars' => $this->customvars))
491        );
492        $this->customvars = isset($allProperties[$this->type]['vars']) ? $allProperties[$this->type]['vars'] : array();
493    }
494
495    /**
496     * Fetch the host custom variables related to this object
497     *
498     * @return  $this
499     */
500    public function fetchHostVariables()
501    {
502        $query = $this->backend->select()->from('customvar', array(
503            'varname',
504            'varvalue',
505            'is_json'
506        ))
507            ->where('object_type', static::TYPE_HOST)
508            ->where('host_name', $this->host_name);
509
510        $this->hostVariables = array();
511        foreach ($query as $row) {
512            if ($row->is_json) {
513                $this->hostVariables[strtolower($row->varname)] = json_decode($row->varvalue);
514            } else {
515                $this->hostVariables[strtolower($row->varname)] = $row->varvalue;
516            }
517        }
518
519        return $this;
520    }
521
522    /**
523     * Fetch the service custom variables related to this object
524     *
525     * @return  $this
526     *
527     * @throws  ProgrammingError    In case this object is not a service
528     */
529    public function fetchServiceVariables()
530    {
531        if ($this->type !== static::TYPE_SERVICE) {
532            throw new ProgrammingError('Cannot fetch service custom variables for non-service objects');
533        }
534
535        $query = $this->backend->select()->from('customvar', array(
536            'varname',
537            'varvalue',
538            'is_json'
539        ))
540            ->where('object_type', static::TYPE_SERVICE)
541            ->where('host_name', $this->host_name)
542            ->where('service_description', $this->service_description);
543
544        $this->serviceVariables = array();
545        foreach ($query as $row) {
546            if ($row->is_json) {
547                $this->serviceVariables[strtolower($row->varname)] = json_decode($row->varvalue);
548            } else {
549                $this->serviceVariables[strtolower($row->varname)] = $row->varvalue;
550            }
551        }
552
553        return $this;
554    }
555
556    /**
557     * Fetch the object's downtimes
558     *
559     * @return $this
560     */
561    public function fetchDowntimes()
562    {
563        $downtimes = $this->backend->select()->from('downtime', array(
564            'author_name'       => 'downtime_author_name',
565            'comment'           => 'downtime_comment',
566            'duration'          => 'downtime_duration',
567            'end'               => 'downtime_end',
568            'entry_time'        => 'downtime_entry_time',
569            'id'                => 'downtime_internal_id',
570            'is_fixed'          => 'downtime_is_fixed',
571            'is_flexible'       => 'downtime_is_flexible',
572            'is_in_effect'      => 'downtime_is_in_effect',
573            'name'              => 'downtime_name',
574            'objecttype'        => 'object_type',
575            'scheduled_end'     => 'downtime_scheduled_end',
576            'scheduled_start'   => 'downtime_scheduled_start',
577            'start'             => 'downtime_start'
578        ))
579            ->where('object_type', $this->type)
580            ->order('downtime_is_in_effect', 'DESC')
581            ->order('downtime_scheduled_start', 'ASC');
582        if ($this->type === self::TYPE_SERVICE) {
583            $downtimes
584                ->where('service_host_name', $this->host_name)
585                ->where('service_description', $this->service_description);
586        } else {
587            $downtimes
588                ->where('host_name', $this->host_name);
589        }
590        $this->downtimes = $downtimes->getQuery()->fetchAll();
591        return $this;
592    }
593
594    /**
595     * Fetch the object's event history
596     *
597     * @return $this
598     */
599    public function fetchEventhistory()
600    {
601        $eventHistory = $this->backend
602            ->select()
603            ->from(
604                'eventhistory',
605                array(
606                    'id',
607                    'object_type',
608                    'host_name',
609                    'host_display_name',
610                    'service_description',
611                    'service_display_name',
612                    'timestamp',
613                    'state',
614                    'output',
615                    'type'
616                )
617            )
618            ->where('object_type', $this->type)
619            ->where('host_name', $this->host_name);
620
621        if ($this->type === self::TYPE_SERVICE) {
622            $eventHistory->where('service_description', $this->service_description);
623        }
624
625        $this->eventhistory = $eventHistory;
626        return $this;
627    }
628
629    /**
630     * Fetch the object's host groups
631     *
632     * @return $this
633     */
634    public function fetchHostgroups()
635    {
636        $this->hostgroups = $this->backend->select()
637            ->from('hostgroup', array('hostgroup_name', 'hostgroup_alias'))
638            ->where('host_name', $this->host_name)
639            ->applyFilter($this->getFilter())
640            ->fetchPairs();
641        return $this;
642    }
643
644    /**
645     * Fetch the object's service groups
646     *
647     * @return $this
648     */
649    public function fetchServicegroups()
650    {
651        $query = $this->backend->select()
652            ->from('servicegroup', array('servicegroup_name', 'servicegroup_alias'))
653            ->where('host_name', $this->host_name);
654
655        if ($this->type === self::TYPE_SERVICE) {
656            $query->where('service_description', $this->service_description);
657        }
658
659        $this->servicegroups = $query->applyFilter($this->getFilter())->fetchPairs();
660        return $this;
661    }
662
663    /**
664     * Fetch stats
665     *
666     * @return $this
667     */
668    public function fetchStats()
669    {
670        $this->stats = $this->backend->select()->from('servicestatussummary', array(
671            'services_total',
672            'services_ok',
673            'services_critical',
674            'services_critical_unhandled',
675            'services_critical_handled',
676            'services_warning',
677            'services_warning_unhandled',
678            'services_warning_handled',
679            'services_unknown',
680            'services_unknown_unhandled',
681            'services_unknown_handled',
682            'services_pending',
683        ))
684            ->where('service_host_name', $this->host_name)
685            ->applyFilter($this->getFilter())
686            ->fetchRow();
687        return $this;
688    }
689
690    /**
691     * Get all action urls configured for this monitored object
692     *
693     * @return array    All note urls as a string
694     */
695    public function getActionUrls()
696    {
697        return $this->resolveAllStrings(
698            MonitoredObject::parseAttributeUrls($this->action_url)
699        );
700    }
701
702    /**
703     * Get the type of the object
704     *
705     * @param   bool $translate
706     *
707     * @return  string
708     */
709    public function getType($translate = false)
710    {
711        if ($translate !== false) {
712            switch ($this->type) {
713                case self::TYPE_HOST:
714                    $type = mt('montiroing', 'host');
715                    break;
716                case self::TYPE_SERVICE:
717                    $type = mt('monitoring', 'service');
718                    break;
719                default:
720                    throw new InvalidArgumentException('Invalid type ' . $this->type);
721            }
722        } else {
723            $type = $this->type;
724        }
725        return $type;
726    }
727
728    /**
729     * Parse the content of the action_url or notes_url attributes
730     *
731     * Find all occurences of http links, separated by whitespaces and quoted
732     * by single or double-ticks.
733     *
734     * @link http://docs.icinga.com/latest/de/objectdefinitions.html
735     *
736     * @param   string  $urlString  A string containing one or more urls
737     * @return  array                   Array of urls as strings
738     */
739    public static function parseAttributeUrls($urlString)
740    {
741        if (empty($urlString)) {
742            return array();
743        }
744        $links = array();
745        if (strpos($urlString, "' ") === false) {
746            $links[] = $urlString;
747        } else {
748            // parse notes-url format
749            foreach (explode("' ", $urlString) as $url) {
750                $url = strpos($url, "'") === 0 ? substr($url, 1) : $url;
751                $url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url;
752                $links[] = $url;
753            }
754        }
755        return $links;
756    }
757
758    /**
759     * Fetch all available data of the object
760     *
761     * @return $this
762     */
763    public function populate()
764    {
765        $this
766            ->fetchComments()
767            ->fetchContactgroups()
768            ->fetchContacts()
769            ->fetchCustomvars()
770            ->fetchDowntimes();
771
772        // Call fetchHostgroups or fetchServicegroups depending on the object's type
773        $fetchGroups = 'fetch' . ucfirst($this->type) . 'groups';
774        $this->$fetchGroups();
775
776        return $this;
777    }
778
779    /**
780     * Resolve macros in all given strings in the current object context
781     *
782     * @param   array   $strs   An array of urls as string
783     *
784     * @return  array
785     */
786    protected function resolveAllStrings(array $strs)
787    {
788        foreach ($strs as $i => $str) {
789            $strs[$i] = Macro::resolveMacros($str, $this);
790        }
791        return $strs;
792    }
793
794    /**
795     * Set the object's properties
796     *
797     * @param   object $properties
798     *
799     * @return  $this
800     */
801    public function setProperties($properties)
802    {
803        $this->properties = (object) $properties;
804        return $this;
805    }
806
807    public function __isset($name)
808    {
809        if (property_exists($this->properties, $name)) {
810            return isset($this->properties->$name);
811        } elseif (property_exists($this, $name)) {
812            return isset($this->$name);
813        }
814        return false;
815    }
816
817    public function __get($name)
818    {
819        if (property_exists($this->properties, $name)) {
820            return $this->properties->$name;
821        } elseif (property_exists($this, $name)) {
822            if ($this->$name === null) {
823                $fetchMethod = 'fetch' . ucfirst($name);
824                $this->$fetchMethod();
825            }
826
827            return $this->$name;
828        } elseif (preg_match('/^_(host|service)_(.+)/i', $name, $matches)) {
829            if (strtolower($matches[1]) === static::TYPE_HOST) {
830                if ($this->hostVariables === null) {
831                    $this->fetchHostVariables();
832                }
833
834                $customvars = $this->hostVariables;
835            } else {
836                if ($this->serviceVariables === null) {
837                    $this->fetchServiceVariables();
838                }
839
840                $customvars = $this->serviceVariables;
841            }
842
843            $variableName = strtolower($matches[2]);
844            if (isset($customvars[$variableName])) {
845                return $customvars[$variableName];
846            }
847
848            return null; // Unknown custom variables MUST NOT throw an error
849        } elseif (in_array($name, array('contact_name', 'contactgroup_name', 'hostgroup_name', 'servicegroup_name'))) {
850            if ($name === 'contact_name') {
851                if ($this->contacts === null) {
852                    $this->fetchContacts();
853                }
854
855                return array_map(function ($el) {
856                    return $el->contact_name;
857                }, $this->contacts);
858            } elseif ($name === 'contactgroup_name') {
859                if ($this->contactgroups === null) {
860                    $this->fetchContactgroups();
861                }
862
863                return array_map(function ($el) {
864                    return $el->contactgroup_name;
865                }, $this->contactgroups);
866            } elseif ($name === 'hostgroup_name') {
867                if ($this->hostgroups === null) {
868                    $this->fetchHostgroups();
869                }
870
871                return array_keys($this->hostgroups);
872            } else { // $name === 'servicegroup_name'
873                if ($this->servicegroups === null) {
874                    $this->fetchServicegroups();
875                }
876
877                return array_keys($this->servicegroups);
878            }
879        } elseif (strpos($name, $this->prefix) !== 0) {
880            $propertyName = strtolower($name);
881            $prefixedName = $this->prefix . $propertyName;
882            if (property_exists($this->properties, $prefixedName)) {
883                return $this->properties->$prefixedName;
884            }
885
886            if ($this->type === static::TYPE_HOST) {
887                if ($this->hostVariables === null) {
888                    $this->fetchHostVariables();
889                }
890
891                $customvars = $this->hostVariables;
892            } else { // $this->type === static::TYPE_SERVICE
893                if ($this->serviceVariables === null) {
894                    $this->fetchServiceVariables();
895                }
896
897                $customvars = $this->serviceVariables;
898            }
899
900            if (isset($customvars[$propertyName])) {
901                return $customvars[$propertyName];
902            }
903        }
904
905        throw new InvalidPropertyException('Can\'t access property \'%s\'. Property does not exist.', $name);
906    }
907
908    /**
909     * @deprecated
910     */
911    public static function fromParams(UrlParams $params)
912    {
913        if ($params->has('service') && $params->has('host')) {
914            return new Service(MonitoringBackend::instance(), $params->get('host'), $params->get('service'));
915        } elseif ($params->has('host')) {
916            return new Host(MonitoringBackend::instance(), $params->get('host'));
917        }
918        return null;
919    }
920}
921