Passed
Push — master ( cfc51d...c2b09b )
by Tony
19:05 queued 10:21
created

QueryDevicesFromGroup()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 11
rs 10
1
<?php
2
/*
3
 * Copyright (C) 2015 Daniel Preussker <[email protected]>
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
/**
19
 * Device-Grouping
20
 * @author Daniel Preussker <[email protected]>
21
 * @author Tony Murray <[email protected]>
22
 * @copyright 2016 f0o, murrant, LibreNMS
23
 * @license GPL
24
 * @package LibreNMS
25
 * @subpackage Devices
26
 */
27
28
use LibreNMS\Config;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Config. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
29
30
/**
31
 * Add a new device group
32
 * @param $pattern
33
 * @param $name
34
 * @param $desc
35
 * @return int|string
36
 */
37
function AddDeviceGroup($name, $desc, $pattern)
38
{
39
    $group_id = dbInsert(array('name' => $name, 'desc' => $desc, 'pattern' => $pattern), 'device_groups');
40
    if ($group_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $group_id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
41
        UpdateDeviceGroup($group_id);
42
    }
43
    return $group_id;
44
}
45
46
/**
47
 * Update a device group
48
 * @param $group_id
49
 * @param $pattern
50
 * @param $name
51
 * @param $desc
52
 * @return bool
53
 */
54
function EditDeviceGroup($group_id, $name = null, $desc = null, $pattern = null)
55
{
56
    $vars = array();
57
    if (!is_null($name)) {
58
        $vars['name'] = $name;
59
    }
60
    if (!is_null($desc)) {
61
        $vars['desc'] = $desc;
62
    }
63
    if (!is_null($pattern)) {
64
        $vars['pattern'] = $pattern;
65
    }
66
67
    $success = dbUpdate($vars, 'device_groups', 'id=?', array($group_id)) >= 0;
68
69
    if ($success) {
70
        UpdateDeviceGroup($group_id);
71
    }
72
    return $success;
73
}
74
75
/**
76
 * Generate SQL from Group-Pattern
77
 * @param string $pattern Pattern to generate SQL for
78
 * @param string $search What to searchid for
79
 * @param int $extra
80
 * @return string sql to perform the search
81
 */
82
function GenGroupSQL($pattern, $search = '', $extra = 0)
83
{
84
    $pattern = RunGroupMacros($pattern);
85
    if ($pattern === false) {
86
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
87
    }
88
89
    if (starts_with($pattern, '%')) {
90
        // v1 pattern
91
        $tables = array();
92
        $words = explode(' ', $pattern);
93
        foreach ($words as $word) {
94
            if (starts_with($word, '%') && str_contains($word, '.')) {
95
                list($table, $column) = explode('.', $word, 2);
96
                $table = str_replace('%', '', $table);
97
                $tables[] = mres(str_replace('(', '', $table));
98
                $pattern = str_replace($word, $table . '.' . $column, $pattern);
99
            }
100
        }
101
        $tables = array_keys(array_flip($tables));
102
    } else {
103
        // v2 pattern
104
        $tables = getTablesFromPattern($pattern);
105
    }
106
107
    $pattern = rtrim($pattern, '&|');
108
109
    if ($tables[0] != 'devices' && dbFetchCell('SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_NAME = ? && COLUMN_NAME = ?', array($tables[0],'device_id')) != 1) {
110
        //Our first table has no valid glue, prepend the 'devices' table to it!
111
        array_unshift($tables, 'devices');
112
        $tables = array_unique($tables); // remove devices from later in the array if it exists
113
    }
114
    $x = sizeof($tables)-1;
115
    $i = 0;
116
    $join = "";
117
    while ($i < $x) {
118
        if (isset($tables[$i+1])) {
119
            $gtmp = ResolveGlues(array($tables[$i+1]), 'device_id');
120
            if ($gtmp === false) {
121
                //Cannot resolve glue-chain. Rule is invalid.
122
                return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
123
            }
124
            $last = "";
125
            $qry = "";
126
            foreach ($gtmp as $glue) {
127
                if (empty($last)) {
128
                    list($tmp,$last) = explode('.', $glue);
129
                    $qry .= $glue.' = ';
130
                } else {
131
                    $parts = explode('.', $glue);
132
                    if (count($parts) == 3) {
133
                        list($tmp, $new, $last) = $parts;
134
                    } else {
135
                        list($tmp,$new) = $parts;
136
                    }
137
                    $qry .= $tmp.'.'.$last.' && '.$tmp.'.'.$new.' = ';
138
                    $last = $new;
139
                }
140
                if (!in_array($tmp, $tables)) {
141
                    $tables[] = $tmp;
142
                }
143
            }
144
            $join .= "( ".$qry.$tables[0].".device_id ) && ";
145
        }
146
        $i++;
147
    }
148
    if ($extra === 1) {
149
        $sql_extra = ",`devices`.*";
150
    }
151
    if (!empty($search)) {
152
        $search = str_replace("(", "", $tables[0]).'.'.$search. ' AND';
153
    }
154
    if (!empty($join)) {
0 ignored issues
show
introduced by
The condition empty($join) is always true.
Loading history...
155
        $join = '('.rtrim($join, '& ').') &&';
156
    }
157
    $sql = 'SELECT DISTINCT('.str_replace('(', '', $tables[0]).'.device_id)'.$sql_extra.' FROM '.implode(',', $tables).' WHERE '.$join.' '.$search.' ('.str_replace(array('%', '@', '!~', '~'), array('', '.*', 'NOT REGEXP', 'REGEXP'), $pattern).')';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sql_extra does not seem to be defined for all execution paths leading up to this point.
Loading history...
158
    return $sql;
159
}//end GenGroupSQL()
160
161
162
/**
163
 * Extract an array of tables in a pattern
164
 *
165
 * @param string $pattern
166
 * @return array
167
 */
168
function getTablesFromPattern($pattern)
169
{
170
    preg_match_all('/[A-Za-z_]+(?=\.[A-Za-z_]+ )/', $pattern, $tables);
171
    if (is_null($tables)) {
172
        return array();
173
    }
174
    return array_keys(array_flip($tables[0])); // unique tables only
175
}
176
177
/**
178
 * Run the group queries again to get fresh list of devices for this group
179
 * @param integer $group_id Group-ID
180
 * @return string
181
 */
182
function QueryDevicesFromGroup($group_id)
183
{
184
    $group = dbFetchRow('SELECT pattern,params FROM device_groups WHERE id = ?', array($group_id));
185
    $pattern = rtrim($group['pattern'], '&|');
186
    $params = (array)json_decode($group['params']);
187
    if (!empty($pattern)) {
188
        $result = dbFetchColumn(GenGroupSQL($pattern), $params);
189
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type array|array<mixed,mixed> which is incompatible with the documented return type string.
Loading history...
190
    }
191
192
    return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
193
}//end QueryDevicesFromGroup()
194
195
/**
196
 * Get an array of all the device ids belonging to this group_id
197
 * @param $group_id
198
 * @param bool $nested Return an array of arrays containing 'device_id'. (for API compatibility)
199
 * @param string $full Return all fields from devices_id
200
 * @return array
201
 */
202
function GetDevicesFromGroup($group_id, $nested = false, $full = '')
203
{
204
    if ($full) {
205
        $query = 'SELECT `device_groups`.`name`, `devices`.* FROM `device_groups` INNER JOIN `device_group_device` ON `device_groups`.`id` = `device_group_device`.`device_group_id` INNER JOIN `devices` ON `device_group_device`.`device_id` = `devices`.`device_id` WHERE `device_groups`.`id` = ?';
206
    } else {
207
        $query = 'SELECT `device_id` FROM `device_group_device` WHERE `device_group_id` = ? ';
208
    }
209
    if ($nested) {
210
        return dbFetchRows($query, array($group_id));
211
    } else {
212
        return dbFetchColumn($query, array($group_id));
213
    }
214
}//end GetDevicesFromGroup()
215
216
/**
217
 * Get all Device-Groups
218
 * @return array
219
 */
220
function GetDeviceGroups()
221
{
222
    return dbFetchRows('SELECT * FROM device_groups ORDER BY name');
223
}//end GetDeviceGroups()
224
225
/**
226
 * Run the group queries again to get fresh list of groups for this device
227
 * @param integer $device_id Device-ID
228
 * @param int $extra Return extra info about the groups (name, desc, pattern)
229
 * @return array
230
 */
231
function QueryGroupsFromDevice($device_id, $extra = 0)
232
{
233
    $ret = array();
234
    foreach (GetDeviceGroups() as $group) {
235
        $params = (array)json_decode($group['params']);
236
        array_unshift($params, $device_id);
237
        if (dbFetchCell(GenGroupSQL($group['pattern'], 'device_id=?', $extra).' LIMIT 1', $params) == $device_id) {
238
            if ($extra === 0) {
239
                $ret[] = $group['id'];
240
            } else {
241
                $ret[] = $group;
242
            }
243
        }
244
    }
245
246
    return $ret;
247
}//end QueryGroupsFromDevice()
248
249
/**
250
 * Get the Device Group IDs of a Device from the database
251
 * @param $device_id
252
 * @param int $extra Return extra info about the groups (name, desc, pattern)
253
 * @return array
254
 */
255
function GetGroupsFromDevice($device_id, $extra = 0)
256
{
257
    $ret = array();
258
    if ($extra === 0) {
259
        $ret = dbFetchColumn('SELECT `device_group_id` FROM `device_group_device` WHERE `device_id`=?', array($device_id));
260
    } else {
261
        $ret = dbFetchRows('SELECT `device_groups`.* FROM `device_group_device` LEFT JOIN `device_groups` ON `device_group_device`.`device_group_id`=`device_groups`.`id` WHERE `device_group_device`.`device_id`=?', array($device_id));
262
    }
263
    return $ret;
264
}//end GetGroupsFromDeviceDB()
265
266
/**
267
 * Process Macros
268
 * @param string $rule Rule to process
269
 * @param int $x Recursion-Anchor
270
 * @return string|boolean
271
 */
272
function RunGroupMacros($rule, $x = 1)
273
{
274
    $macros = Config::get('alert.macros.group', []);
275
276
    krsort($macros);
277
    foreach ($macros as $macro => $value) {
278
        if (!strstr($macro, " ")) {
279
            $rule = str_replace('%macros.'.$macro, '('.$value.')', $rule);
280
        }
281
    }
282
    if (strstr($rule, "%macros")) {
283
        if (++$x < 30) {
284
            $rule = RunGroupMacros($rule, $x);
285
        } else {
286
            return false;
287
        }
288
    }
289
    return $rule;
290
}//end RunGroupMacros()
291
292
293
/**
294
 * Update device-group relationship for the given device id
295
 * @param $device_id
296
 */
297
function UpdateGroupsForDevice($device_id)
298
{
299
    d_echo("### Start Device Groups ###\n");
300
    $queried_groups = QueryGroupsFromDevice($device_id);
301
    $db_groups = GetGroupsFromDevice($device_id);
302
303
    // compare the arrays to get the added and removed groups
304
    $added_groups = array_diff($queried_groups, $db_groups);
305
    $removed_groups = array_diff($db_groups, $queried_groups);
306
307
    d_echo("Groups Added: ".implode(',', $added_groups).PHP_EOL);
308
    d_echo("Groups Removed: ".implode(',', $removed_groups).PHP_EOL);
309
310
    // insert new groups
311
    $insert = array();
312
    foreach ($added_groups as $group_id) {
313
        $insert[] = array('device_id' => $device_id, 'device_group_id' => $group_id);
314
    }
315
    if (!empty($insert)) {
316
        dbBulkInsert($insert, 'device_group_device');
317
    }
318
319
    // remove old groups
320
    if (!empty($removed_groups)) {
321
        dbDelete('device_group_device', '`device_id`=? AND `device_group_id` IN ' . dbGenPlaceholders(count($removed_groups)), array_merge([$device_id], $removed_groups));
322
    }
323
    d_echo("### End Device Groups ###\n");
324
}
325
326
/**
327
 * Update the device-group relationship for the given group id
328
 * @param $group_id
329
 */
330
function UpdateDeviceGroup($group_id)
331
{
332
    $queried_devices = QueryDevicesFromGroup($group_id);
333
    $db_devices = GetDevicesFromGroup($group_id);
334
335
    // compare the arrays to get the added and removed devices
336
    $added_devices = array_diff($queried_devices, $db_devices);
0 ignored issues
show
Bug introduced by
$queried_devices of type string is incompatible with the type array expected by parameter $array1 of array_diff(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

336
    $added_devices = array_diff(/** @scrutinizer ignore-type */ $queried_devices, $db_devices);
Loading history...
337
    $removed_devices = array_diff($db_devices, $queried_devices);
0 ignored issues
show
Bug introduced by
$queried_devices of type string is incompatible with the type array expected by parameter $array2 of array_diff(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

337
    $removed_devices = array_diff($db_devices, /** @scrutinizer ignore-type */ $queried_devices);
Loading history...
338
339
    // insert new devices
340
    $insert = array();
341
    foreach ($added_devices as $device_id) {
342
        $insert[] = array('device_id' => $device_id, 'device_group_id' => $group_id);
343
    }
344
    if (!empty($insert)) {
345
        dbBulkInsert($insert, 'device_group_device');
346
    }
347
348
    // remove old devices
349
    if (!empty($removed_devices)) {
350
        dbDelete('device_group_device', '`device_group_id`=? AND `device_id` IN ' . dbGenPlaceholders(count($removed_devices)), array_merge([$group_id], $removed_devices));
351
    }
352
}
353
354
/**
355
 * Fill in params into the pattern, replacing placeholders (?)
356
 * If $params is empty or null, just returns $pattern
357
 *
358
 * @return string
359
 */
360
function formatDeviceGroupPattern($pattern, $params)
361
{
362
    // fill in parameters
363
    foreach ((array)$params as $value) {
364
        if (!is_numeric($value) && !starts_with($value, "'")) {
365
            $value = "'".$value."'";
366
        }
367
        $pattern = preg_replace('/\?/', $value, $pattern, 1);
368
    }
369
370
    return $pattern;
371
}
372