Completed
Push — device-groups ( b8bad5...cfb6a1 )
by Tony
03:03
created

DeviceGroup   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 2

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 37
c 6
b 0
f 0
lcom 2
cbo 2
dl 0
loc 280
rs 8.6

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getDeviceCountAttribute() 0 12 3
A setPatternAttribute() 0 12 2
A devices() 0 4 1
B getDeviceIdsRaw() 0 29 5
B applyGroupMacros() 0 22 6
A getTablesFromPattern() 0 8 2
A getPatternAttribute() 0 9 2
C convertV1Pattern() 0 41 8
C convertRegexToLike() 0 27 7
A deviceCountRelation() 0 4 1
1
<?php
2
/**
3
 * DeviceGroup.php
4
 *
5
 * Dynamic groups of devices
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 <http://www.gnu.org/licenses/>.
19
 *
20
 * @package    LibreNMS
21
 * @link       http://librenms.org
22
 * @copyright  2016 Tony Murray
23
 * @author     Tony Murray <[email protected]>
24
 */
25
26
namespace App\Models;
27
28
use DB;
29
use Illuminate\Database\Eloquent\Model;
30
use Settings;
31
32
/**
33
 * App\Models\DeviceGroup
34
 *
35
 * @property integer $id
36
 * @property string $name
37
 * @property string $desc
38
 * @property string $pattern
39
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Device[] $devices
40
 * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereId($value)
41
 * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereName($value)
42
 * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereDesc($value)
43
 * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup wherePattern($value)
44
 * @mixin \Eloquent
45
 */
46
class DeviceGroup extends Model
47
{
48
    /**
49
     * Indicates if the model should be timestamped.
50
     *
51
     * @var bool
52
     */
53
    public $timestamps = false;
54
    /**
55
     * The table associated with the model.
56
     *
57
     * @var string
58
     */
59
    protected $table = 'device_groups';
60
    /**
61
     * The primary key column name.
62
     *
63
     * @var string
64
     */
65
    protected $primaryKey = 'id';
66
    /**
67
     * Virtual attributes
68
     *
69
     * @var string
70
     */
71
    protected $appends = ['deviceCount'];
72
73
    /**
74
     * The attributes that can be mass assigned.
75
     *
76
     * @var array
77
     */
78
    protected $fillable = ['name', 'desc', 'pattern'];
79
80
    /**
81
     * Fetch the device counts for groups
82
     * Use DeviceGroups::with('deviceCountRelation') to eager load
83
     *
84
     * @return int
85
     */
86
    public function getDeviceCountAttribute()
87
    {
88
        // if relation is not loaded already, let's do it first
89
        if (!$this->relationLoaded('deviceCountRelation')) {
90
            $this->load('deviceCountRelation');
91
        }
92
93
        $related = $this->getRelation('deviceCountRelation')->first();
94
95
        // then return the count directly
96
        return ($related) ? (int)$related->count : 0;
97
    }
98
99
    /**
100
     * Set the pattern attribute
101
     * Update the relationships when set
102
     *
103
     * @param $pattern
104
     */
105
    public function setPatternAttribute($pattern)
106
    {
107
        $this->attributes['pattern'] = $pattern;
108
109
        // we need an id to add relationships
110
        if (is_null($this->id)) {
111
            $this->save();
112
        }
113
114
        // update the relationships (deletes and adds as needed)
115
        $this->devices()->sync($this->getDeviceIdsRaw($pattern));
116
    }
117
118
    /**
119
     * Relationship to App\Models\Device
120
     *
121
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
122
     */
123
    public function devices()
124
    {
125
        return $this->belongsToMany('App\Models\Device', 'device_group_device', 'device_group_id', 'device_id');
126
    }
127
128
    /**
129
     * Get an array of the device ids from this group by re-querying the database with
130
     * either the specified pattern or the saved pattern of this group
131
     *
132
     * @param null $pattern Optional, will use the pattern from this group if not specified
133
     * @return array
134
     */
135
    public function getDeviceIdsRaw($pattern = null)
136
    {
137
        if (is_null($pattern)) {
138
            $pattern = $this->pattern;
139
        }
140
141
        $pattern = $this->applyGroupMacros($pattern);
142
143
        $tables = $this->getTablesFromPattern($pattern);
0 ignored issues
show
Bug introduced by
It seems like $pattern defined by $this->applyGroupMacros($pattern) on line 141 can also be of type boolean; however, App\Models\DeviceGroup::getTablesFromPattern() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
144
145
        $query = null;
0 ignored issues
show
Unused Code introduced by
$query is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
146
        if (count($tables) == 1) {
147
            $query = DB::table($tables[0])->select('device_id')->distinct();
148
        } else {
149
            $query = DB::table('devices')->select('devices.device_id')->distinct();
150
151
            foreach ($tables as $table) {
152
                // skip devices table, we used that as the base.
153
                if ($table == 'devices') {
154
                    continue;
155
                }
156
157
                $query = $query->join($table, 'devices.device_id', '=', $table.'.device_id');
158
            }
159
        }
160
161
        // match the device ids
162
        return $query->whereRaw($pattern)->pluck('device_id');
163
    }
164
165
    /**
166
     * Process Macros
167
     *
168
     * @param string $pattern Rule to process
169
     * @param int $x Recursion-Anchor, do not pass
170
     * @return string|boolean
171
     */
172
    public static function applyGroupMacros($pattern, $x = 1)
173
    {
174
        if (!str_contains($pattern, 'macros.')) {
175
            return $pattern;
176
        }
177
178
        foreach (Settings::get('alert.macros.group', []) as $macro => $value) {
179
            $value = str_replace(['%', '&&', '||'], ['', 'AND', 'OR'], $value);  // this might need something more complex
180
            if (!str_contains($macro, ' ')) {
181
                $pattern = str_replace('macros.'.$macro, '('.$value.')', $pattern);
182
            }
183
        }
184
185
        if (str_contains($pattern, 'macros.')) {
186
            if (++$x < 30) {
187
                $pattern = self::applyGroupMacros($pattern, $x);
188
            } else {
189
                return false;
190
            }
191
        }
192
        return $pattern;
193
    }
194
195
    // ---- Accessors/Mutators ----
196
197
    /**
198
     * Extract an array of tables in a pattern
199
     *
200
     * @param string $pattern
201
     * @return array
202
     */
203
    private function getTablesFromPattern($pattern)
204
    {
205
        preg_match_all('/[A-Za-z_]+(?=\.[A-Za-z_]+ )/', $pattern, $tables);
206
        if (is_null($tables)) {
207
            return [];
208
        }
209
        return array_keys(array_flip($tables[0])); // unique tables only
210
    }
211
212
    /**
213
     * Check if the stored pattern is v1
214
     * Convert it to v2 for display
215
     * Currently, it will only be updated in the database if the user saves the rule in the ui
216
     *
217
     * @param $pattern
218
     * @return string
219
     */
220
    public function getPatternAttribute($pattern)
221
    {
222
        // If this is a v1 pattern, convert it to sql
223
        if (starts_with($pattern, '%')) {
224
            return $this->convertV1Pattern($pattern);
225
        }
226
227
        return $pattern;
228
    }
229
230
    /**
231
     * Convert a v1 device group pattern to sql that can be ingested by jQuery-QueryBuilder
232
     *
233
     * @param $pattern
234
     * @return array
235
     */
236
    private function convertV1Pattern($pattern)
237
    {
238
        $pattern = rtrim($pattern, ' &&');
239
        $pattern = rtrim($pattern, ' ||');
240
241
        $ops = ['=', '!=', '<', '<=', '>', '>='];
242
        $parts = str_getcsv($pattern, ' '); // tokenize the pattern, respecting quoted parts
243
        $out = "";
244
245
        $count = count($parts);
246
        for ($i = 0; $i < $count; $i++) {
247
            $cur = $parts[$i];
248
249
            if (starts_with($cur, '%')) {
250
                // table and column or macro
251
                $out .= substr($cur, 1).' ';
252
            } elseif (substr($cur, -1) == '~') {
253
                // like operator
254
                $content = $parts[++$i]; // grab the content so we can format it
255
256
                if (starts_with($cur, '!')) {
257
                    // prepend NOT
258
                    $out .= 'NOT ';
259
                }
260
261
                $out .= "LIKE('".$this->convertRegexToLike($content)."') ";
262
263
            } elseif ($cur == '&&') {
264
                $out .= 'AND ';
265
            } elseif ($cur == '||') {
266
                $out .= 'OR ';
267
            } elseif (in_array($cur, $ops)) {
268
                // pass-through operators
269
                $out .= $cur.' ';
270
            } else {
271
                // user supplied input
272
                $out .= "'".trim($cur, '"\'')."' "; // TODO: remove trim, only needed with invalid input
273
            }
274
        }
275
        return rtrim($out);
276
    }
277
278
    // ---- Define Relationships ----
279
280
    /**
281
     * Convert sql regex to like, many common uses can be converted
282
     * Should only be used to convert v1 patterns
283
     *
284
     * @param $pattern
285
     * @return string
286
     */
287
    private function convertRegexToLike($pattern)
288
    {
289
        $startAnchor = starts_with($pattern, '^');
290
        $endAnchor = ends_with($pattern, '$');
291
292
        $pattern = trim($pattern, '^$');
293
294
        $wildcards = ['@', '.*'];
295
        if (str_contains($pattern, $wildcards)) {
296
            // contains wildcard
297
            $pattern = str_replace($wildcards, '%', $pattern);
298
        }
299
300
        // add ends appropriately
301
        if ($startAnchor && !$endAnchor) {
302
            $pattern .= '%';
303
        } elseif (!$startAnchor && $endAnchor) {
304
            $pattern = '%'.$pattern;
305
        }
306
307
        // if there are no wildcards, assume substring
308
        if (!str_contains($pattern, '%')) {
309
            $pattern = '%'.$pattern.'%';
310
        }
311
312
        return $pattern;
313
    }
314
315
    /**
316
     * Relationship allows us to eager load device counts
317
     * DeviceGroups::with('deviceCountRelation')
318
     *
319
     * @return mixed
320
     */
321
    public function deviceCountRelation()
322
    {
323
        return $this->devices()->selectRaw('`device_group_device`.`device_group_id`, count(*) as count')->groupBy('pivot_device_group_id');
324
    }
325
}
326