Completed
Push — device-groups ( bd1023...7084dc )
by Tony
03:41
created

DeviceGroup::applyGroupMacros()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
rs 8.6737
cc 6
eloc 13
nc 10
nop 2
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');
148
        } else {
149
            $query = DB::table('devices')->select('devices.device_id')->distinct();
150
151
            foreach ($tables as $table) {
152
                if ($table == 'devices') {
153
                    // skip devices table as we used that as the base.
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
    // ---- Accessors/Mutators ----
166
167
    /**
168
     * Process Macros
169
     * @param string $pattern Rule to process
170
     * @param int $x Recursion-Anchor, do not pass
171
     * @return string|boolean
172
     */
173
    public static function applyGroupMacros($pattern, $x = 1)
174
    {
175
        if (!str_contains($pattern, 'macros.')) {
176
            return $pattern;
177
        }
178
179
        foreach (Settings::get('alert.macros.group', []) as $macro => $value) {
180
            $value = str_replace(['%', '&&', '||'], ['', 'AND', 'OR'], $value);  // this might need something more complex
181
            if (!str_contains($macro, ' ')) {
182
                $pattern = str_replace('macros.'.$macro, '('.$value.')', $pattern);
183
            }
184
        }
185
186
        if (str_contains($pattern, 'macros.')) {
187
            if (++$x < 30) {
188
                $pattern = self::applyGroupMacros($pattern, $x);
189
            } else {
190
                return false;
191
            }
192
        }
193
        return $pattern;
194
    }
195
196
    /**
197
     * Extract an array of tables in a pattern
198
     *
199
     * @param string $pattern
200
     * @return array
201
     */
202
    private function getTablesFromPattern($pattern)
203
    {
204
        preg_match_all('/[A-Za-z_]+(?=\.[A-Za-z_]+ )/', $pattern, $tables);
205
        $tables = array_keys(array_flip($tables[0])); // unique tables only
206
        if (is_null($tables)) {
207
            return [];
208
        }
209
        return $tables;
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 v2 style
223
        if (starts_with($pattern, '%')) {
224
            $pattern = $this->convertV1Pattern($pattern);
225
226
            $this->pattern = $pattern; //TODO: does not save, only updates this instance
227
        }
228
229
        return $pattern;
230
    }
231
232
    // ---- Define Relationships ----
233
234
    /**
235
     * Convert a v1 device group pattern to v2 style
236
     *
237
     * @param $pattern
238
     * @return array
239
     */
240
    private function convertV1Pattern($pattern)
241
    {
242
        $pattern = rtrim($pattern, ' &&');
243
        $pattern = rtrim($pattern, ' ||');
244
245
        $ops = ['=', '!=', '<', '<=', '>', '>='];
246
        $parts = str_getcsv($pattern, ' '); // tokenize the pattern, respecting quoted parts
247
        $out = "";
248
249
        $count = count($parts);
250
        for ($i = 0; $i < $count; $i++) {
251
            $cur = $parts[$i];
252
253
            if (starts_with($cur, '%')) {
254
                // table and column or macro
255
                $out .= substr($cur, 1).' ';
256
            } elseif (substr($cur, -1) == '~') {
257
                // like operator
258
                $content = $parts[++$i]; // grab the content so we can format it
259
260
                if (str_contains($content, '@')) {
261
                    // contains wildcard
262
                    $content = str_replace('@', '%', $content);
263
                } else {
264
                    // assume substring
265
                    $content = '%'.$content.'%';
266
                }
267
268
                if (starts_with($cur, '!')) {
269
                    // prepend NOT
270
                    $out .= 'NOT ';
271
                }
272
273
                $out .= "LIKE('".$content."') ";
274
275
            } elseif ($cur == '&&') {
276
                $out .= 'AND ';
277
            } elseif ($cur == '||') {
278
                $out .= 'OR ';
279
            } elseif (in_array($cur, $ops)) {
280
                // pass-through operators
281
                $out .= $cur.' ';
282
            } else {
283
                // user supplied input
284
                $out .= "'".trim($cur, '"\'')."' "; // TODO: remove trim, only needed with invalid input
285
            }
286
        }
287
        return rtrim($out);
288
    }
289
290
    /**
291
     * Relationship allows us to eager load device counts
292
     * DeviceGroups::with('deviceCountRelation')
293
     *
294
     * @return mixed
295
     */
296
    public function deviceCountRelation()
297
    {
298
        return $this->devices()->selectRaw('`device_group_device`.`device_group_id`, count(*) as count')->groupBy('pivot_device_group_id');
299
    }
300
}
301