1<?php
2/**
3 * AlertUtil.php
4 *
5 * Extending the built in logging to add an event logger function
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19 *
20 * @link       https://www.librenms.org
21 * @copyright  2019 KanREN, Inc.
22 * @author     Heath Barnhart <hbarnhart@kanren.net>
23 */
24
25namespace LibreNMS\Alert;
26
27use App\Models\Device;
28use App\Models\User;
29use DeviceCache;
30use LibreNMS\Config;
31use PHPMailer\PHPMailer\PHPMailer;
32
33class AlertUtil
34{
35    /**
36     * Get the rule_id for a specific alert
37     *
38     * @param int $alert_id
39     * @return mixed|null
40     */
41    private static function getRuleId($alert_id)
42    {
43        $query = 'SELECT `rule_id` FROM `alerts` WHERE `id`=?';
44
45        return dbFetchCell($query, [$alert_id]);
46    }
47
48    /**
49     * Get the transport for a given alert_id
50     *
51     * @param int $alert_id
52     * @return array
53     */
54    public static function getAlertTransports($alert_id)
55    {
56        $query = "SELECT b.transport_id, b.transport_type, b.transport_name FROM alert_transport_map AS a LEFT JOIN alert_transports AS b ON b.transport_id=a.transport_or_group_id WHERE a.target_type='single' AND a.rule_id=? UNION DISTINCT SELECT d.transport_id, d.transport_type, d.transport_name FROM alert_transport_map AS a LEFT JOIN alert_transport_groups AS b ON a.transport_or_group_id=b.transport_group_id LEFT JOIN transport_group_transport AS c ON b.transport_group_id=c.transport_group_id LEFT JOIN alert_transports AS d ON c.transport_id=d.transport_id WHERE a.target_type='group' AND a.rule_id=?";
57        $rule_id = self::getRuleId($alert_id);
58
59        return dbFetchRows($query, [$rule_id, $rule_id]);
60    }
61
62    /**
63     * Returns the default transports
64     *
65     * @return array
66     */
67    public static function getDefaultAlertTransports()
68    {
69        $query = 'SELECT transport_id, transport_type, transport_name FROM alert_transports WHERE is_default=true';
70
71        return dbFetchRows($query);
72    }
73
74    /**
75     * Find contacts for alert
76     * @param array $results Rule-Result
77     * @return array
78     */
79    public static function getContacts($results)
80    {
81        if (empty($results)) {
82            return [];
83        }
84        if (Config::get('alert.default_only') === true || Config::get('alerts.email.default_only') === true) {
85            $email = Config::get('alert.default_mail', Config::get('alerts.email.default'));
86
87            return $email ? [$email => ''] : [];
88        }
89        $users = User::query()->thisAuth()->get();
90        $contacts = [];
91        $uids = [];
92        foreach ($results as $result) {
93            $tmp = null;
94            if (is_numeric($result['bill_id'])) {
95                $tmpa = dbFetchRows('SELECT user_id FROM bill_perms WHERE bill_id = ?', [$result['bill_id']]);
96                foreach ($tmpa as $tmp) {
97                    $uids[$tmp['user_id']] = $tmp['user_id'];
98                }
99            }
100            if (is_numeric($result['port_id'])) {
101                $tmpa = dbFetchRows('SELECT user_id FROM ports_perms WHERE port_id = ?', [$result['port_id']]);
102                foreach ($tmpa as $tmp) {
103                    $uids[$tmp['user_id']] = $tmp['user_id'];
104                }
105            }
106            if (is_numeric($result['device_id'])) {
107                if (Config::get('alert.syscontact') == true) {
108                    if (dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_bool' AND device_id = ?", [$result['device_id']])) {
109                        $tmpa = dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_string' AND device_id = ?", [$result['device_id']]);
110                    } else {
111                        $tmpa = dbFetchCell('SELECT sysContact FROM devices WHERE device_id = ?', [$result['device_id']]);
112                    }
113                    if (! empty($tmpa)) {
114                        $contacts[$tmpa] = '';
115                    }
116                }
117                $tmpa = dbFetchRows('SELECT user_id FROM devices_perms WHERE device_id = ?', [$result['device_id']]);
118                foreach ($tmpa as $tmp) {
119                    $uids[$tmp['user_id']] = $tmp['user_id'];
120                }
121            }
122        }
123        foreach ($users as $user) {
124            if (empty($user['email'])) {
125                continue; // no email, skip this user
126            }
127            if (empty($user['realname'])) {
128                $user['realname'] = $user['username'];
129            }
130            if (Config::get('alert.globals') && ($user['level'] >= 5 && $user['level'] < 10)) {
131                $contacts[$user['email']] = $user['realname'];
132            } elseif (Config::get('alert.admins') && $user['level'] == 10) {
133                $contacts[$user['email']] = $user['realname'];
134            } elseif (Config::get('alert.users') == true && in_array($user['user_id'], $uids)) {
135                $contacts[$user['email']] = $user['realname'];
136            }
137        }
138
139        $tmp_contacts = [];
140        foreach ($contacts as $email => $name) {
141            if (strstr($email, ',')) {
142                $split_contacts = preg_split('/[,\s]+/', $email);
143                foreach ($split_contacts as $split_email) {
144                    if (! empty($split_email)) {
145                        $tmp_contacts[$split_email] = $name;
146                    }
147                }
148            } else {
149                $tmp_contacts[$email] = $name;
150            }
151        }
152
153        if (! empty($tmp_contacts)) {
154            // Validate contacts so we can fall back to default if configured.
155            $mail = new PHPMailer();
156            foreach ($tmp_contacts as $tmp_email => $tmp_name) {
157                if ($mail->validateAddress($tmp_email) != true) {
158                    unset($tmp_contacts[$tmp_email]);
159                }
160            }
161        }
162
163        // Copy all email alerts to default contact if configured.
164        $default_mail = Config::get('alert.default_mail');
165        if (! isset($tmp_contacts[$default_mail]) && Config::get('alert.default_copy')) {
166            $tmp_contacts[$default_mail] = '';
167        }
168        // Send email to default contact if no other contact found
169        if (empty($tmp_contacts) && Config::get('alert.default_if_none') && $default_mail) {
170            $tmp_contacts[$default_mail] = '';
171        }
172
173        return $tmp_contacts;
174    }
175
176    public static function getRules($device_id)
177    {
178        $query = 'SELECT DISTINCT a.* FROM alert_rules a
179        LEFT JOIN alert_device_map d ON a.id=d.rule_id AND (a.invert_map = 0 OR a.invert_map = 1 AND d.device_id = ?)
180        LEFT JOIN alert_group_map g ON a.id=g.rule_id AND (a.invert_map = 0 OR a.invert_map = 1 AND g.group_id IN (SELECT DISTINCT device_group_id FROM device_group_device WHERE device_id = ?))
181        LEFT JOIN alert_location_map l ON a.id=l.rule_id AND (a.invert_map = 0 OR a.invert_map = 1 AND l.location_id IN (SELECT DISTINCT location_id FROM devices WHERE device_id = ?))
182        LEFT JOIN device_group_device dg ON g.group_id=dg.device_group_id AND dg.device_id = ?
183        WHERE a.disabled = 0 AND (
184            (d.device_id IS NULL AND g.group_id IS NULL)
185            OR (a.invert_map = 0 AND (d.device_id=? OR dg.device_id=?))
186            OR (a.invert_map = 1  AND (d.device_id != ? OR d.device_id IS NULL) AND (dg.device_id != ? OR dg.device_id IS NULL))
187        )';
188
189        $params = [$device_id, $device_id, $device_id, $device_id, $device_id, $device_id, $device_id, $device_id];
190
191        return dbFetchRows($query, $params);
192    }
193
194    /**
195     * Check if device is under maintenance
196     * @param int $device_id Device-ID
197     * @return bool
198     */
199    public static function isMaintenance($device_id)
200    {
201        return DeviceCache::get($device_id)->isUnderMaintenance();
202    }
203
204    /**
205     * Check if device is set to ignore alerts
206     * @param int $device_id Device-ID
207     * @return bool
208     */
209    public static function hasDisableNotify($device_id)
210    {
211        $device = Device::find($device_id);
212
213        return ! is_null($device) && $device->disable_notify;
214    }
215
216    /**
217     * Process Macros
218     * @param string $rule Rule to process
219     * @param int $x Recursion-Anchor
220     * @return string|bool
221     */
222    public static function runMacros($rule, $x = 1)
223    {
224        $macros = Config::get('alert.macros.rule', []);
225        krsort($macros);
226        foreach ($macros as $macro => $value) {
227            if (! strstr($macro, ' ')) {
228                $rule = str_replace('%macros.' . $macro, '(' . $value . ')', $rule);
229            }
230        }
231        if (strstr($rule, '%macros.')) {
232            if (++$x < 30) {
233                $rule = self::runMacros($rule, $x);
234            } else {
235                return false;
236            }
237        }
238
239        return $rule;
240    }
241}
242