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

DeviceGroup::convertV1Pattern()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 39
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 39
rs 8.4444
c 0
b 0
f 0
cc 8
nc 8
nop 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 Permissions;
29
use DB;
30
31
class DeviceGroup extends BaseModel
32
{
33
    public $timestamps = false;
34
    protected $appends = ['patternSql'];
35
    protected $fillable = ['name', 'desc', 'pattern', 'params'];
36
    protected $casts = ['params' => 'array'];
37
38
    // ---- Helper Functions ----
39
40
    public function updateRelations()
41
    {
42
        // we need an id to add relationships
43
        if (is_null($this->id)) {
0 ignored issues
show
introduced by
The condition is_null($this->id) is always false.
Loading history...
44
            $this->save();
45
        }
46
47
        $device_ids = $this->getDeviceIdsRaw();
48
49
        // update the relationships (deletes and adds as needed)
50
        $this->devices()->sync($device_ids);
51
    }
52
53
    /**
54
     * Get an array of the device ids from this group by re-querying the database with
55
     * either the specified pattern or the saved pattern of this group
56
     *
57
     * @param string $statement Optional, will use the pattern from this group if not specified
58
     * @param array $params array of paremeters
59
     * @return array
60
     */
61
    public function getDeviceIdsRaw($statement = null, $params = null)
62
    {
63
        if (is_null($statement)) {
64
            $statement = $this->pattern;
65
        }
66
67
        if (is_null($params)) {
68
            if (empty($this->params)) {
69
                if (!starts_with($statement, '%')) {
70
                    // can't build sql
71
                    return [];
72
                }
73
            } else {
74
                $params = $this->params;
75
            }
76
        }
77
78
        $statement = $this->applyGroupMacros($statement);
79
        $tables = $this->getTablesFromPattern($statement);
0 ignored issues
show
Bug introduced by
It seems like $statement can also be of type false; however, parameter $pattern of App\Models\DeviceGroup::getTablesFromPattern() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

79
        $tables = $this->getTablesFromPattern(/** @scrutinizer ignore-type */ $statement);
Loading history...
80
81
        $query = null;
82
        if (count($tables) == 1) {
83
            $query = DB::table($tables[0])->select('device_id')->distinct();
84
        } else {
85
            $query = DB::table('devices')->select('devices.device_id')->distinct();
86
87
            foreach ($tables as $table) {
88
                // skip devices table, we used that as the base.
89
                if ($table == 'devices') {
90
                    continue;
91
                }
92
93
                $query = $query->join($table, 'devices.device_id', '=', $table.'.device_id');
94
            }
95
        }
96
97
        // match the device ids
98
        if (is_null($params)) {
99
            return $query->whereRaw($statement)->pluck('device_id')->toArray();
0 ignored issues
show
Bug introduced by
It seems like $statement can also be of type false; however, parameter $sql of Illuminate\Database\Query\Builder::whereRaw() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

99
            return $query->whereRaw(/** @scrutinizer ignore-type */ $statement)->pluck('device_id')->toArray();
Loading history...
100
        } else {
101
            return $query->whereRaw($statement, $params)->pluck('device_id')->toArray();
102
        }
103
    }
104
105
    /**
106
     * Process Macros
107
     *
108
     * @param string $pattern Rule to process
109
     * @param int $x Recursion-Anchor, do not pass
110
     * @return string|boolean
111
     */
112
    public static function applyGroupMacros($pattern, $x = 1)
113
    {
114
        if (!str_contains($pattern, 'macros.')) {
115
            return $pattern;
116
        }
117
118
        foreach (\LibreNMS\Config::get('alert.macros.group', []) as $macro => $value) {
119
            $value = str_replace(['%', '&&', '||'], ['', 'AND', 'OR'], $value);  // this might need something more complex
120
            if (!str_contains($macro, ' ')) {
121
                $pattern = str_replace('macros.'.$macro, '('.$value.')', $pattern);
122
            }
123
        }
124
125
        if (str_contains($pattern, 'macros.')) {
126
            if (++$x < 30) {
127
                $pattern = self::applyGroupMacros($pattern, $x);
128
            } else {
129
                return false;
130
            }
131
        }
132
        return $pattern;
133
    }
134
135
    /**
136
     * Extract an array of tables in a pattern
137
     *
138
     * @param string $pattern
139
     * @return array
140
     */
141
    private function getTablesFromPattern($pattern)
142
    {
143
        preg_match_all('/[A-Za-z_]+(?=\.[A-Za-z_]+ )/', $pattern, $tables);
144
        if (is_null($tables)) {
145
            return [];
146
        }
147
        return array_keys(array_flip($tables[0])); // unique tables only
148
    }
149
150
    /**
151
     * Convert a v1 device group pattern to sql that can be ingested by jQuery-QueryBuilder
152
     *
153
     * @param $pattern
154
     * @return array
155
     */
156
    private function convertV1Pattern($pattern)
157
    {
158
        $pattern = rtrim($pattern, ' &&');
159
        $pattern = rtrim($pattern, ' ||');
160
161
        $ops = ['=', '!=', '<', '<=', '>', '>='];
162
        $parts = str_getcsv($pattern, ' '); // tokenize the pattern, respecting quoted parts
163
        $out = "";
164
165
        $count = count($parts);
166
        for ($i = 0; $i < $count; $i++) {
167
            $cur = $parts[$i];
168
169
            if (starts_with($cur, '%')) {
170
                // table and column or macro
171
                $out .= substr($cur, 1).' ';
172
            } elseif (substr($cur, -1) == '~') {
173
                // like operator
174
                $content = $parts[++$i]; // grab the content so we can format it
175
176
                if (starts_with($cur, '!')) {
177
                    // prepend NOT
178
                    $out .= 'NOT ';
179
                }
180
181
                $out .= "LIKE('".$this->convertRegexToLike($content)."') ";
182
            } elseif ($cur == '&&') {
183
                $out .= 'AND ';
184
            } elseif ($cur == '||') {
185
                $out .= 'OR ';
186
            } elseif (in_array($cur, $ops)) {
187
                // pass-through operators
188
                $out .= $cur.' ';
189
            } else {
190
                // user supplied input
191
                $out .= "'".trim($cur, '"\'')."' "; // TODO: remove trim, only needed with invalid input
192
            }
193
        }
194
        return rtrim($out);
0 ignored issues
show
Bug Best Practice introduced by
The expression return rtrim($out) returns the type string which is incompatible with the documented return type array.
Loading history...
195
    }
196
197
    /**
198
     * Convert sql regex to like, many common uses can be converted
199
     * Should only be used to convert v1 patterns
200
     *
201
     * @param $pattern
202
     * @return string
203
     */
204
    private function convertRegexToLike($pattern)
205
    {
206
        $startAnchor = starts_with($pattern, '^');
207
        $endAnchor = ends_with($pattern, '$');
208
209
        $pattern = trim($pattern, '^$');
210
211
        $wildcards = ['@', '.*'];
212
        if (str_contains($pattern, $wildcards)) {
213
            // contains wildcard
214
            $pattern = str_replace($wildcards, '%', $pattern);
215
        }
216
217
        // add ends appropriately
218
        if ($startAnchor && !$endAnchor) {
219
            $pattern .= '%';
220
        } elseif (!$startAnchor && $endAnchor) {
221
            $pattern = '%'.$pattern;
222
        }
223
224
        // if there are no wildcards, assume substring
225
        if (!str_contains($pattern, '%')) {
226
            $pattern = '%'.$pattern.'%';
227
        }
228
229
        return $pattern;
230
    }
231
232
    // ---- Accessors/Mutators ----
233
234
    /**
235
     * Returns an sql formatted string
236
     * Mostly, this is for ingestion by JQuery-QueryBuilder
237
     *
238
     * @return string
239
     */
240
    public function getPatternSqlAttribute()
241
    {
242
        $sql = $this->pattern;
243
244
        // fill in parameters
245
        foreach ((array)$this->params as $value) {
246
            if (!is_numeric($value) && !starts_with($value, "'")) {
247
                $value = "'".$value."'";
248
            }
249
            $sql = preg_replace('/\?/', $value, $sql, 1);
250
        }
251
        return $sql;
252
    }
253
254
    /**
255
     * Custom mutator for params attribute
256
     * Allows already encoded json to pass through
257
     *
258
     * @param array|string $params
259
     */
260
//    public function setParamsAttribute($params)
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
261
//    {
262
//        if (!Util::isJson($params)) {
263
//            $params = json_encode($params);
264
//        }
265
//
266
//        $this->attributes['params'] = $params;
267
//    }
268
269
    /**
270
     * Check if the stored pattern is v1
271
     * Convert it to v2 for display
272
     * Currently, it will only be updated in the database if the user saves the rule in the ui
273
     *
274
     * @param $pattern
275
     * @return string
276
     */
277
    public function getPatternAttribute($pattern)
278
    {
279
        // If this is a v1 pattern, convert it to sql
280
        if (starts_with($pattern, '%')) {
281
            return $this->convertV1Pattern($pattern);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->convertV1Pattern($pattern) returns the type array which is incompatible with the documented return type string.
Loading history...
282
        }
283
284
        return $pattern;
285
    }
286
287
    // ---- Query Scopes ----
288
289
    public function scopeHasAccess($query, User $user)
290
    {
291
        if ($user->hasGlobalRead()) {
292
            return $query;
293
        }
294
295
        return $query->whereIn('id', Permissions::deviceGroupsForUser($user));
0 ignored issues
show
Bug introduced by
The method deviceGroupsForUser() does not exist on App\Facades\Permissions. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

295
        return $query->whereIn('id', Permissions::/** @scrutinizer ignore-call */ deviceGroupsForUser($user));
Loading history...
296
    }
297
298
    // ---- Define Relationships ----
299
300
301
    public function alertSchedules()
302
    {
303
        return $this->morphToMany('App\Models\AlertSchedule', 'alert_schedulable', 'alert_schedulables', 'schedule_id', 'schedule_id');
304
    }
305
306
    public function rules()
307
    {
308
        return $this->belongsToMany('App\Models\AlertRule', 'alert_group_map', 'group_id', 'rule_id');
309
    }
310
311
    public function devices()
312
    {
313
        return $this->belongsToMany('App\Models\Device', 'device_group_device', 'device_group_id', 'device_id');
314
    }
315
316
    public function services()
317
    {
318
        return $this->belongsToMany('App\Models\Service', 'device_group_device', 'device_group_id', 'device_id');
319
    }
320
}
321