Completed
Push — device-groups ( d263d8...cfdf4d )
by Tony
03:49
created

DeviceGroup::getDeviceIdsRaw()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 27
rs 8.439
cc 5
eloc 14
nc 4
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 DB;
29
use Illuminate\Database\Eloquent\Model;
30
31
/**
32
 * App\Models\DeviceGroup
33
 *
34
 * @property integer $id
35
 * @property string $name
36
 * @property string $desc
37
 * @property string $pattern
38
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Device[] $devices
39
 * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereId($value)
40
 * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereName($value)
41
 * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereDesc($value)
42
 * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup wherePattern($value)
43
 * @mixin \Eloquent
44
 */
45
class DeviceGroup extends Model
46
{
47
    /**
48
     * Indicates if the model should be timestamped.
49
     *
50
     * @var bool
51
     */
52
    public $timestamps = false;
53
    /**
54
     * The table associated with the model.
55
     *
56
     * @var string
57
     */
58
    protected $table = 'device_groups';
59
    /**
60
     * The primary key column name.
61
     *
62
     * @var string
63
     */
64
    protected $primaryKey = 'id';
65
    /**
66
     * Virtual attributes
67
     *
68
     * @var string
69
     */
70
    protected $appends = ['deviceCount'];
71
72
    /**
73
     * The attributes that can be mass assigned.
74
     *
75
     * @var array
76
     */
77
    protected $fillable = ['name', 'desc', 'pattern'];
78
79
    /**
80
     * Fetch the device counts for groups
81
     * Use DeviceGroups::with('deviceCountRelation') to eager load
82
     *
83
     * @return int
84
     */
85
    public function getDeviceCountAttribute()
86
    {
87
        // if relation is not loaded already, let's do it first
88
        if (!$this->relationLoaded('deviceCountRelation')) {
89
            $this->load('deviceCountRelation');
90
        }
91
92
        $related = $this->getRelation('deviceCountRelation')->first();
93
94
        // then return the count directly
95
        return ($related) ? (int)$related->count : 0;
96
    }
97
98
    /**
99
     * Set the pattern attribute
100
     * Update the relationships when set
101
     *
102
     * @param $pattern
103
     */
104
    public function setPatternAttribute($pattern)
105
    {
106
        $this->attributes['pattern'] = $pattern;
107
108
        // we need an id to add relationships
109
        if (is_null($this->id)) {
110
            $this->save();
111
        }
112
113
        // update the relationships (deletes and adds as needed)
114
        $this->devices()->sync($this->getDeviceIdsRaw($pattern));
115
    }
116
117
    /**
118
     * Relationship to App\Models\Device
119
     *
120
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
121
     */
122
    public function devices()
123
    {
124
        return $this->belongsToMany('App\Models\Device', 'device_group_device', 'device_group_id', 'device_id');
125
    }
126
127
    // ---- Accessors/Mutators ----
128
129
    /**
130
     * Get an array of the device ids from this group by re-querying the database with
131
     * either the specified pattern or the saved pattern of this group
132
     *
133
     * @param null $pattern Optional, will use the pattern from this group if not specified
134
     * @return array
135
     */
136
    public function getDeviceIdsRaw($pattern = null)
137
    {
138
        if (is_null($pattern)) {
139
            $pattern = $this->pattern;
140
        }
141
142
        $tables = $this->getTablesFromPattern($pattern);
143
144
        $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...
145
        if (count($tables) == 1) {
146
            $query = DB::table($tables[0])->select('device_id');
147
        } else {
148
            $query = DB::table('devices')->select('devices.device_id')->distinct();
149
150
            foreach ($tables as $table) {
151
                if ($table == 'devices') {
152
                    // skip devices table as we used that as the base.
153
                    continue;
154
                }
155
156
                $query = $query->join($table, 'devices.device_id', '=', $table.'.device_id');
157
            }
158
        }
159
160
        // match the device ids
161
        return $query->whereRaw($pattern)->pluck('device_id');
162
    }
163
164
    /**
165
     * Extract an array of tables in a pattern
166
     *
167
     * @param $pattern
168
     * @return array
169
     */
170
    private function getTablesFromPattern($pattern)
171
    {
172
        preg_match_all('/[A-Za-z_]+(?=\.[A-Za-z_]+ )/', $pattern, $tables);
173
        $tables = array_keys(array_flip($tables[0])); // unique tables only
174
        if (is_null($tables)) {
175
            return [];
176
        }
177
        return $tables;
178
    }
179
180
    /**
181
     * Check if the stored pattern is v1
182
     * Convert it to v2 for display
183
     * Currently, it will only be updated in the database if the user saves the rule in the ui
184
     *
185
     * @param $pattern
186
     * @return array
187
     */
188
    public function getPatternAttribute($pattern)
189
    {
190
        // If this is a v1 pattern, convert it to v2 style
191
        if (starts_with($pattern, '%')) {
192
            $pattern = $this->convertV1Pattern($pattern);
193
194
            $this->pattern = $pattern;  //TODO: does not save, only updates this instance
195
        }
196
197
        return $pattern;
198
    }
199
200
    // ---- Define Relationships ----
201
202
    /**
203
     * Convert a v1 device group pattern to v2 style
204
     *
205
     * @param $pattern
206
     * @return array
207
     */
208
    private function convertV1Pattern($pattern)
209
    {
210
        $pattern = rtrim($pattern, ' &&');
211
        $pattern = rtrim($pattern, ' ||');
212
213
        $ops = ['=', '!=', '<', '<=', '>', '>='];
214
        $parts = str_getcsv($pattern, ' ');
215
        $out = "";
216
217
        $count = count($parts);
218
        for ($i = 0; $i < $count; $i++) {
219
            $cur = $parts[$i];
220
221
            if (starts_with($cur, '%')) {
222
                // table and column
223
                $out .= substr($cur, 1).' ';
224
            } elseif (substr($cur, -1) == '~') {
225
                // like operator
226
                $content = $parts[++$i]; // grab the content so we can format it
227
228
                if (str_contains($content, '@')) {
229
                    // contains wildcard
230
                    $content = str_replace('@', '%', $content);
231
                } else {
232
                    // assume substring
233
                    $content = '%'.$content.'%';
234
                }
235
236
                if (starts_with($cur, '!')) {
237
                    // prepend NOT
238
                    $out .= 'NOT ';
239
                }
240
241
                $out .= "LIKE('".$content."') ";
242
243
            } elseif ($cur == '&&') {
244
                $out .= 'AND ';
245
            } elseif ($cur == '||') {
246
                $out .= 'OR ';
247
            } elseif (in_array($cur, $ops)) {
248
                // passthrough operators
249
                $out .= $cur.' ';
250
            } else {
251
                // user supplied input
252
                $out .= "'".trim($cur, '"\'')."' "; // TODO: remove trim, only needed with invalid input
253
            }
254
        }
255
        return rtrim($out);
256
    }
257
258
    /**
259
     * Relationship allows us to eager load device counts
260
     * DeviceGroups::with('deviceCountRelation')
261
     *
262
     * @return mixed
263
     */
264
    public function deviceCountRelation()
265
    {
266
        return $this->devices()->selectRaw('`device_group_device`.`device_group_id`, count(*) as count')->groupBy('pivot_device_group_id');
267
    }
268
}
269